第一次提交
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
//package org.jeecg;
|
||||
//
|
||||
//import org.springframework.boot.SpringApplication;
|
||||
//import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
//
|
||||
//@SpringBootApplication
|
||||
//public class JeecgAiRagApplication {
|
||||
//
|
||||
// public static void main(String[] args) {
|
||||
// SpringApplication.run(JeecgAiRagApplication.class, args);
|
||||
// }
|
||||
//}
|
||||
@@ -0,0 +1,67 @@
|
||||
package org.jeecg.modules.airag.app.consts;
|
||||
|
||||
/**
|
||||
* AI应用常量类
|
||||
*
|
||||
* @author chenrui
|
||||
* @date 2025/2/25 14:52
|
||||
*/
|
||||
public class AiAppConsts {
|
||||
|
||||
/**
|
||||
* 状态:启用
|
||||
*/
|
||||
public static final String STATUS_ENABLE = "enable";
|
||||
/**
|
||||
* 状态:禁用
|
||||
*/
|
||||
public static final String STATUS_DISABLE = "disable";
|
||||
/**
|
||||
* 状态:发布
|
||||
*/
|
||||
public static final String STATUS_RELEASE = "release";
|
||||
|
||||
|
||||
/**
|
||||
* 默认应用id
|
||||
*/
|
||||
public static final String DEFAULT_APP_ID = "default";
|
||||
|
||||
|
||||
/**
|
||||
* 应用类型:简单聊天
|
||||
*/
|
||||
public static final String APP_TYPE_CHAT_SIMPLE = "chatSimple";
|
||||
|
||||
/**
|
||||
* 应用类型:聊天流(高级编排)
|
||||
*/
|
||||
public static final String APP_TYPE_CHAT_FLOW = "chatFLow";
|
||||
|
||||
/**
|
||||
* 应用元数据:流程输入参数
|
||||
* for [issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程
|
||||
*/
|
||||
public static final String APP_METADATA_FLOW_INPUTS = "flowInputs";
|
||||
|
||||
/**
|
||||
* 是否开启记忆
|
||||
*/
|
||||
public static final Integer IZ_OPEN_MEMORY = 1;
|
||||
|
||||
/**
|
||||
* 会话标题最大长度
|
||||
*/
|
||||
public static final int CONVERSATION_MAX_TITLE_LENGTH = 10;
|
||||
|
||||
|
||||
/**
|
||||
* AI写作的流程id
|
||||
*/
|
||||
public static final String ARTICLE_WRITER_FLOW_ID = "2011769909807579138";
|
||||
|
||||
/**
|
||||
* AI写作redis请求前缀
|
||||
*/
|
||||
public static final String ARTICLE_WRITER_KEY = "airag:chat:article:write:{}";
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
package org.jeecg.modules.airag.app.consts;
|
||||
|
||||
/**
|
||||
* @Description: 提示词常量
|
||||
* @Author: chenrui
|
||||
* @Date: 2025/3/12 15:03
|
||||
*/
|
||||
public class Prompts {
|
||||
|
||||
/**
|
||||
* 根据提示生成智能体提示词
|
||||
*/
|
||||
public static final String GENERATE_LLM_PROMPT = "# 角色\n" +
|
||||
"你是一位专业且高效的AI提示词工程师,擅长根据用户多样化需求自动生成高质量的结构化提示词模板,具备全面而敏锐的分析能力和出色的创造力。\n" +
|
||||
"## 要求:\n" +
|
||||
"1. \"\"\"只输出提示词,不要输出多余解释\"\"\"\n" +
|
||||
"2. \"\"\"不要在前后增加代码块的md语法.\"\"\"\n" +
|
||||
"2. 贴合用户需求,描述智能助手的定位、能力、知识储备\n" +
|
||||
"3. 提示词应清晰、精确、易于理解,在保持质量的同时,尽可能简洁\n" +
|
||||
"4. 严格按照给定的流程和格式执行任务,确保输出规范准确。\n" +
|
||||
"\n" +
|
||||
"## 流程\n" +
|
||||
"### 1: 需求分析\n" +
|
||||
"1. 当用户描述需求时,严格运用SCQA框架确认核心要素,精准分析和联想:\"当前场景(Situation)是什么?主要矛盾(Complication)有哪些?需要解决的关键问题(Question)是?预期达成什么效果(Answer)?\"\n" +
|
||||
"2. 通过5W1H细致分析和联想细节:\"目标受众(Who)?使用场景(Where/When)?具体要实现什么(What)?为什么需要这些特征(Why)?如何量化效果(How)?\"\n" +
|
||||
"\n" +
|
||||
"### 2: 框架选择\n" +
|
||||
"根据需求从给定模板库中匹配最佳提示词类型:\n" +
|
||||
"* 角色扮演型:\n" +
|
||||
"```\n" +
|
||||
"你将扮演一个人物角色<角色名称>,以下是关于这个角色的详细设定,请根据这些信息来构建你的回答。 \n" +
|
||||
"\n" +
|
||||
"**人物基本信息:**\n" +
|
||||
"- 你是:<角色的名称、身份等基本介绍>\n" +
|
||||
"- 人称:第一人称\n" +
|
||||
"- 出身背景与上下文:<交代角色背景信息和上下文>\n" +
|
||||
"**性格特点:**\n" +
|
||||
"- <性格特点描述>\n" +
|
||||
"**语言风格:**\n" +
|
||||
"- <语言风格描述> \n" +
|
||||
"**人际关系:**\n" +
|
||||
"- <人际关系描述>\n" +
|
||||
"**过往经历:**\n" +
|
||||
"- <过往经历描述>\n" +
|
||||
"**经典台词或口头禅:**\n" +
|
||||
"补充信息: 即你可以将动作、神情语气、心理活动、故事背景放在()中来表示,为对话提供补充信息。\n" +
|
||||
"- 台词1:<角色台词示例1> \n" +
|
||||
"- 台词2:<角色台词示例2>\n" +
|
||||
"- ...\n" +
|
||||
"\n" +
|
||||
"要求: \n" +
|
||||
"- 要求1\n" +
|
||||
"- 要求2\n" +
|
||||
"- ... \n" +
|
||||
"```\n" +
|
||||
"* 多步骤型:\n" +
|
||||
"```\n" +
|
||||
"# 角色 \n" +
|
||||
"你是<角色设定(比如:xx领域的专家)>\n" +
|
||||
"你的目标是<希望模型执行什么任务,达成什么目标>\n" +
|
||||
"\n" +
|
||||
"{#以下可以采用先总括,再展开详细说明的方式,描述你希望智能体在每一个步骤如何进行工作,具体的工作步骤数量可以根据实际需求增删#}\n" +
|
||||
"## 工作步骤 \n" +
|
||||
"1. <工作流程1的一句话概括> \n" +
|
||||
"2. <工作流程2的一句话概括> \n" +
|
||||
"3. <工作流程3的一句话概括>\n" +
|
||||
"\n" +
|
||||
"### 第一步 <工作流程1标题> \n" +
|
||||
"<工作流程步骤1的具体工作要求和举例说明,可以分点列出希望在本步骤做哪些事情,需要完成什么阶段性的工作目标>\n" +
|
||||
"### 第二步 <工作流程2标题> \n" +
|
||||
"<工作流程步骤2的具体工作要求和举例说明,可以分点列出希望在本步骤做哪些事情,需要完成什么阶段性的工作目标>\n" +
|
||||
"### 第三步 <工作流程3标题>\n" +
|
||||
"<工作流程步骤3的具体工作要求和举例说明,可以分点列出希望在本步骤做哪些事情,需要完成什么阶段性的工作目标>\n" +
|
||||
"```\n" +
|
||||
"* 限制性模板:\n" +
|
||||
"```\n" +
|
||||
"# 角色:<角色名称>\n" +
|
||||
"<角色概述和主要职责的一句话描述>\n" +
|
||||
"\n" +
|
||||
"## 目标:\n" +
|
||||
"<角色的工作目标,如果有多目标可以分点列出,但建议更聚焦1-2个目标>\n" +
|
||||
"\n" +
|
||||
"## 技能:\n" +
|
||||
"1. <为了实现目标,角色需要具备的技能1>\n" +
|
||||
"2. <为了实现目标,角色需要具备的技能2>\n" +
|
||||
"3. <为了实现目标,角色需要具备的技能3>\n" +
|
||||
"\n" +
|
||||
"## 工作流:\n" +
|
||||
"1. <描述角色工作流程的第一步>\n" +
|
||||
"2. <描述角色工作流程的第二步>\n" +
|
||||
"3. <描述角色工作流程的第三步>\n" +
|
||||
"\n" +
|
||||
"## 输出格式:\n" +
|
||||
"<如果对角色的输出格式有特定要求,可以在这里强调并举例说明想要的输出格式>\n" +
|
||||
"\n" +
|
||||
"## 限制:\n" +
|
||||
"- <描述角色在互动过程中需要遵循的限制条件1>\n" +
|
||||
"- <描述角色在互动过程中需要遵循的限制条件2>\n" +
|
||||
"- <描述角色在互动过程中需要遵循的限制条件3>\n" +
|
||||
"```\n" +
|
||||
"\n" +
|
||||
"### 3: 生成优化\n" +
|
||||
"1. 输出时自动添加三重保障机制:\n" +
|
||||
" - 反幻觉校验:\"所有数据需标注来源,不确定信息用[需核实]标记\"\n" +
|
||||
" - 风格校准器:\"对比[目标风格]与生成内容的余弦相似度,低于0.7时启动重写\"\n" +
|
||||
" - 伦理审查模块:\"自动过滤涉及隐私/偏见/违法内容,替换为[合规表达]\"";
|
||||
|
||||
/**
|
||||
* 提示词生成角色及通用要求
|
||||
*/
|
||||
public static final String GENERATE_GUIDE_HEADER = "# 角色\n" +
|
||||
"你是一位AI提示词专家,请根据提供的配置信息,生成针对AI智能体的“使用指南”提示词。\n" +
|
||||
"\n" +
|
||||
"## 通用要求\n" +
|
||||
"1. 生成的内容将作为系统提示词的一部分。\n" +
|
||||
"2. **严禁**包含任何角色设定开场白(如“你是一个...AI助手”、“在对话过程中...”等)。\n" +
|
||||
"3. **只输出提示词内容**,不要包含任何解释、寒暄或Markdown代码块标记。\n" +
|
||||
"4. 语气专业、清晰、指令性强。\n" +
|
||||
"5. 说明内容请使用中文。\n\n";
|
||||
|
||||
/**
|
||||
* 变量生成提示词
|
||||
*/
|
||||
public static final String GENERATE_VAR_PART = "## 任务:生成变量使用指南\n" +
|
||||
"### 输入信息\n" +
|
||||
"**变量列表**:\n" +
|
||||
"%s\n" +
|
||||
"### 要求\n" +
|
||||
"1. 请生成一段**变量使用指南**。\n" +
|
||||
"2. **遍历生成**:请遍历【输入信息】中的所有变量,为**每一个**变量生成一条具体的使用指南。\n" +
|
||||
"3. **格式要求**:请仿照以下句式,根据变量的实际含义生成(确保包含{{变量名}}):\n" +
|
||||
" 例如:针对name变量 -> “回复问题时,请称呼你的用户为{{name}}。”\n" +
|
||||
" 例如:针对age变量 -> “用户的年龄是{{age}},请在对话中适时使用。”\n" +
|
||||
" 例如:针对其他变量 -> “用户的[变量描述]是{{[变量名]}},请在对话中适时使用。”\n" +
|
||||
"4. **通用更新指令**:请在变量指南的最后,单独生成一条指令,明确指示AI:“当从用户对话中获取到上述变量(<列出所有变量名,用顿号分隔>)的**新信息**时,**必须立即调用** `update_variable` 工具进行存储。**注意**:调用前请检查上下文,如果已调用过该工具或变量值未改变,**严禁**重复调用。”\n" +
|
||||
"5. **保留原文**:如果输入信息中包含具体的行为指令(如“回复问题时,请称呼你的用户为{{name}}”),请在生成的指南中**直接引用原文**,不要进行改写或格式化,以免改变用户的原意。\n\n";
|
||||
|
||||
/**
|
||||
* 记忆库生成提示词
|
||||
*/
|
||||
public static final String GENERATE_MEMORY_PART = "## 任务:生成记忆库使用指南\n" +
|
||||
"### 输入信息\n" +
|
||||
"**记忆库描述**:\n" +
|
||||
"%s\n" +
|
||||
"### 要求\n" +
|
||||
"1. 请生成一段**记忆库使用指南**,加入【工具使用强制协议】:\n" +
|
||||
" - **全自动存储(无需用户指令)**:你必须时刻像一个观察者一样分析对话。一旦检测到符合记忆库描述的信息(尤其是:**姓名、职业、年龄**、联系方式、偏好、经历等),**立即**调用 `add_memory` 工具存储。**绝对不要**询问用户是否需要存储,也不要等待用户明确指令。这是你的后台职责。\n" +
|
||||
" - **全自动检索(强制优先)**:\n" +
|
||||
" * **禁止直接反问**:当用户提出依赖个人信息的问题(如“推荐适合我的...”或“我之前说过...”)时,**绝对禁止**直接反问用户“你的爱好是什么?”。\n" +
|
||||
" * **必须先查后答**:你必须**先假设**记忆库中已经有了答案,并**立即调用** `query_memory` 进行验证。只有当工具返回“未找到相关信息”后,你才有资格询问用户。\n" +
|
||||
" * **宁可查空,不可不查**:即使你觉得可能没有记录,也必须先走一遍查询流程。\n" +
|
||||
" - **动态调整**:请根据【输入信息】中提供的**记忆库状态描述**,明确界定哪些信息属于“自动捕获”的范围。\n" +
|
||||
" - **行为准则**:\n" +
|
||||
" * 你的记忆动作应该是**主动且无感**的。用户只负责聊天,你负责记住一切重要细节。\n" +
|
||||
" * **禁止口头空谈**:严禁只回复“我知道了”、“已记住”而实际不调用工具。这是严重错误。\n" +
|
||||
" - **示例演示**:\n" +
|
||||
" * 自动存储(职业):用户说“我是网络工程师” -> (捕捉到职业信息) -> **立即自动调用** `add_memory(content='用户职业是网络工程师')` -> (存储成功) -> 回复“原来是同行,网络工程很有趣...”。\n" +
|
||||
" * 自动查询(场景):用户说“根据我的爱好推荐旅游地点” -> **严禁**直接问“你有什么爱好?” -> **必须立即调用** `query_memory(queryText='用户爱好')` -> (若查到:爬山) -> 回复“既然你喜欢爬山,推荐去黄山...”。\n" +
|
||||
" * 自动查询(常规):用户问“今天吃什么好?” -> (需要了解口味) -> **立即自动调用** `query_memory(queryText='用户饮食偏好')` -> (获取到不吃香菜) -> 回复“推荐一家不放香菜的...”。\n\n";
|
||||
|
||||
/**
|
||||
* ai写作提示词
|
||||
*/
|
||||
public static final String AI_WRITER_PROMPT ="请撰写一篇关于 [{}] 的文章。文章的内容格式:{},语气:{},语言:{},长度:{}。";
|
||||
|
||||
/**
|
||||
* ai写作回复提示词
|
||||
*/
|
||||
public static final String AI_REPLY_PROMPT = "请针对如下内容:[{}] 做个回复。回复内容参考:[{}], 回复格式:{},语气:{},语言:{},长度:{}。";
|
||||
|
||||
/**
|
||||
* ai润色提提示词
|
||||
*/
|
||||
public static final String AI_TOUCHE_PROMPT = "请针对如下内容:[{}] 进行润色。 回复格式:{},语气:{},语言:{},长度:{}。";
|
||||
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
package org.jeecg.modules.airag.app.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.system.base.controller.JeecgController;
|
||||
import org.jeecg.common.system.query.QueryGenerator;
|
||||
import org.jeecg.common.util.AssertUtils;
|
||||
import org.jeecg.common.util.TokenUtils;
|
||||
import org.jeecg.config.mybatis.MybatisPlusSaasConfig;
|
||||
import org.jeecg.config.shiro.IgnoreAuth;
|
||||
import org.jeecg.modules.airag.app.consts.AiAppConsts;
|
||||
import org.jeecg.modules.airag.app.entity.AiragApp;
|
||||
import org.jeecg.modules.airag.app.service.IAiragAppService;
|
||||
import org.jeecg.modules.airag.app.service.IAiragChatService;
|
||||
import org.jeecg.modules.airag.app.vo.AiArticleWriteVersionVo;
|
||||
import org.jeecg.modules.airag.app.vo.AppDebugParams;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
/**
|
||||
* @Description: AI应用
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-02-26
|
||||
* @Version: V1.0
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/airag/app")
|
||||
@Slf4j
|
||||
public class AiragAppController extends JeecgController<AiragApp, IAiragAppService> {
|
||||
@Autowired
|
||||
private IAiragAppService airagAppService;
|
||||
|
||||
@Autowired
|
||||
private IAiragChatService airagChatService;
|
||||
|
||||
/**
|
||||
* 分页列表查询
|
||||
*
|
||||
* @param airagApp
|
||||
* @param pageNo
|
||||
* @param pageSize
|
||||
* @param req
|
||||
* @return
|
||||
*/
|
||||
@GetMapping(value = "/list")
|
||||
public Result<IPage<AiragApp>> queryPageList(AiragApp airagApp,
|
||||
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
|
||||
@RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize,
|
||||
HttpServletRequest req) {
|
||||
QueryWrapper<AiragApp> queryWrapper = QueryGenerator.initQueryWrapper(airagApp, req.getParameterMap());
|
||||
Page<AiragApp> page = new Page<AiragApp>(pageNo, pageSize);
|
||||
IPage<AiragApp> pageList = airagAppService.page(page, queryWrapper);
|
||||
return Result.OK(pageList);
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增或编辑
|
||||
*
|
||||
* @param airagApp
|
||||
* @return
|
||||
*/
|
||||
@RequestMapping(value = "/edit", method = {RequestMethod.PUT, RequestMethod.POST})
|
||||
@RequiresPermissions("airag:app:edit")
|
||||
public Result<String> edit(@RequestBody AiragApp airagApp) {
|
||||
AssertUtils.assertNotEmpty("参数异常", airagApp);
|
||||
AssertUtils.assertNotEmpty("请输入应用名称", airagApp.getName());
|
||||
AssertUtils.assertNotEmpty("请选择应用类型", airagApp.getType());
|
||||
airagApp.setStatus(AiAppConsts.STATUS_ENABLE);
|
||||
airagAppService.saveOrUpdate(airagApp);
|
||||
return Result.OK("保存完成!", airagApp.getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* 发布应用
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@RequestMapping(value = "/release", method = RequestMethod.POST)
|
||||
public Result<String> release(@RequestParam(name = "id") String id, @RequestParam(name = "release") Boolean release) {
|
||||
AssertUtils.assertNotEmpty("id必须填写", id);
|
||||
if (release == null) {
|
||||
release = true;
|
||||
}
|
||||
AiragApp airagApp = new AiragApp();
|
||||
airagApp.setId(id);
|
||||
if (release) {
|
||||
airagApp.setStatus(AiAppConsts.STATUS_RELEASE);
|
||||
} else {
|
||||
airagApp.setStatus(AiAppConsts.STATUS_ENABLE);
|
||||
}
|
||||
airagAppService.updateById(airagApp);
|
||||
return Result.OK(release ? "发布成功" : "取消发布成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过id删除
|
||||
*
|
||||
* @param id
|
||||
* @return
|
||||
*/
|
||||
@DeleteMapping(value = "/delete")
|
||||
@RequiresPermissions("airag:app:delete")
|
||||
public Result<String> delete(HttpServletRequest request,@RequestParam(name = "id", required = true) String id) {
|
||||
//update-begin---author:chenrui ---date:20250606 for:[issues/8337]关于ai工作列表的数据权限问题 #8337------------
|
||||
//如果是saas隔离的情况下,判断当前租户id是否是当前租户下的
|
||||
if (MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL) {
|
||||
AiragApp app = airagAppService.getById(id);
|
||||
//获取当前租户
|
||||
String currentTenantId = TokenUtils.getTenantIdByRequest(request);
|
||||
if (null == app || !app.getTenantId().equals(currentTenantId)) {
|
||||
return Result.error("删除AI应用失败,不能删除其他租户的AI应用!");
|
||||
}
|
||||
}
|
||||
//update-end---author:chenrui ---date:20250606 for:[issues/8337]关于ai工作列表的数据权限问题 #8337------------
|
||||
airagAppService.removeById(id);
|
||||
return Result.OK("删除成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过id查询
|
||||
*
|
||||
* @param id
|
||||
* @return
|
||||
*/
|
||||
@IgnoreAuth
|
||||
@GetMapping(value = "/queryById")
|
||||
public Result<AiragApp> queryById(@RequestParam(name = "id", required = true) String id) {
|
||||
AiragApp airagApp = airagAppService.getById(id);
|
||||
if (airagApp == null) {
|
||||
return Result.error("未找到对应数据");
|
||||
}
|
||||
return Result.OK(airagApp);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 调试应用
|
||||
*
|
||||
* @param appDebugParams
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/2/28 10:49
|
||||
*/
|
||||
@PostMapping(value = "/debug")
|
||||
public SseEmitter debugApp(@RequestBody AppDebugParams appDebugParams) {
|
||||
return airagChatService.debugApp(appDebugParams);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据需求生成提示词
|
||||
*
|
||||
* @param prompt
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/3/12 15:30
|
||||
*/
|
||||
@GetMapping(value = "/prompt/generate")
|
||||
public Result<?> generatePrompt(@RequestParam(name = "prompt", required = true) String prompt) {
|
||||
return (Result<?>) airagAppService.generatePrompt(prompt,true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据需求生成提示词
|
||||
*
|
||||
* @param prompt
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/3/12 15:30
|
||||
*/
|
||||
@PostMapping(value = "/prompt/generate")
|
||||
public SseEmitter generatePromptSse(@RequestParam(name = "prompt", required = true) String prompt) {
|
||||
return (SseEmitter) airagAppService.generatePrompt(prompt,false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据应用ID生成变量和记忆提示词 (SSE)
|
||||
* for: 【QQYUN-14479】提示词单独拆分
|
||||
* @param variables
|
||||
* @return
|
||||
*/
|
||||
@PostMapping(value = "/prompt/generateMemoryByAppId")
|
||||
public SseEmitter generatePromptByAppIdSse(@RequestParam(name = "variables") String variables,
|
||||
@RequestParam(name = "memoryId") String memoryId) {
|
||||
return (SseEmitter) airagAppService.generateMemoryByAppId(variables, memoryId,false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 写作保存
|
||||
*/
|
||||
@PostMapping("/save/article/write")
|
||||
public Result<String> saveArticleWrite(@RequestBody AiArticleWriteVersionVo aiWriteVersionVo) {
|
||||
airagAppService.saveArticleWrite(aiWriteVersionVo);
|
||||
return Result.OK("保存成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 写作删除
|
||||
*/
|
||||
@DeleteMapping("/delete/article/write")
|
||||
public Result<String> deleteArticleWrite(@RequestParam(name = "version") String version) {
|
||||
AssertUtils.assertNotEmpty("版本号不能为空", version);
|
||||
airagAppService.deleteArticleWrite(version);
|
||||
return Result.OK("删除成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 写作查询
|
||||
*/
|
||||
@GetMapping("/list/article/write")
|
||||
public Result<List<AiArticleWriteVersionVo>> listArticleWrite() {
|
||||
List<AiArticleWriteVersionVo> list = airagAppService.listArticleWrite();
|
||||
return Result.OK(list);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
package org.jeecg.modules.airag.app.controller;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.constant.CommonConstant;
|
||||
import org.jeecg.common.util.CommonUtils;
|
||||
import org.jeecg.config.shiro.IgnoreAuth;
|
||||
import org.jeecg.modules.airag.app.service.IAiragChatService;
|
||||
import org.jeecg.modules.airag.app.vo.AiWriteGenerateVo;
|
||||
import org.jeecg.modules.airag.app.vo.ChatConversation;
|
||||
import org.jeecg.modules.airag.app.vo.ChatSendParams;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.springframework.web.multipart.MultipartHttpServletRequest;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* airag应用-chat
|
||||
*
|
||||
* @Author: chenrui
|
||||
* @Date: 2025-02-25 11:40
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/airag/chat")
|
||||
public class AiragChatController {
|
||||
|
||||
@Autowired
|
||||
IAiragChatService chatService;
|
||||
|
||||
@Value(value = "${jeecg.path.upload}")
|
||||
private String uploadpath;
|
||||
|
||||
/**
|
||||
* 本地:local minio:minio 阿里:alioss
|
||||
*/
|
||||
@Value(value="${jeecg.uploadType}")
|
||||
private String uploadType;
|
||||
|
||||
|
||||
/**
|
||||
* 发送消息
|
||||
*
|
||||
* @return 返回一个Result对象,表示发送消息的结果
|
||||
* @author chenrui
|
||||
* @date 2025/2/25 11:42
|
||||
*/
|
||||
@IgnoreAuth
|
||||
@PostMapping(value = "/send")
|
||||
public SseEmitter send(@RequestBody ChatSendParams chatSendParams) {
|
||||
return chatService.send(chatSendParams);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息 <br/>
|
||||
* 兼容旧版浏览器
|
||||
* @param content
|
||||
* @param conversationId
|
||||
* @param topicId
|
||||
* @param appId
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/2/25 18:13
|
||||
*/
|
||||
@GetMapping(value = "/send")
|
||||
public SseEmitter sendByGet(@RequestParam("content") String content,
|
||||
@RequestParam(value = "conversationId", required = false) String conversationId,
|
||||
@RequestParam(value = "topicId", required = false) String topicId,
|
||||
@RequestParam(value = "appId", required = false) String appId) {
|
||||
ChatSendParams chatSendParams = new ChatSendParams(content, conversationId, topicId, appId);
|
||||
return chatService.send(chatSendParams);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有对话
|
||||
*
|
||||
* @return 返回一个Result对象,包含所有对话的信息
|
||||
* @author chenrui
|
||||
* @date 2025/2/25 11:42
|
||||
*/
|
||||
@IgnoreAuth
|
||||
@GetMapping(value = "/init")
|
||||
public Result<?> initChat(@RequestParam(name = "id", required = true) String id) {
|
||||
return chatService.initChat(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有对话
|
||||
*
|
||||
* @return 返回一个Result对象,包含所有对话的信息
|
||||
* @author chenrui
|
||||
* @date 2025/2/25 11:42
|
||||
*/
|
||||
@IgnoreAuth
|
||||
@GetMapping(value = "/conversations")
|
||||
public Result<?> getConversations(@RequestParam(value = "appId", required = false) String appId) {
|
||||
return chatService.getConversations(appId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据类型获取所有对话
|
||||
*
|
||||
* @return 返回一个Result对象,包含所有对话的信息
|
||||
* @author wangshuai
|
||||
* @date 2025/12/11 11:42
|
||||
*/
|
||||
@IgnoreAuth
|
||||
@GetMapping(value = "/getConversationsByType")
|
||||
public Result<?> getConversationsByType(@RequestParam(value = "sessionType") String sessionType) {
|
||||
return chatService.getConversationsByType(sessionType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除会话
|
||||
*
|
||||
* @param id
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/3/3 16:55
|
||||
*/
|
||||
@IgnoreAuth
|
||||
@DeleteMapping(value = "/conversation/{id}")
|
||||
public Result<?> deleteConversation(@PathVariable("id") String id) {
|
||||
return chatService.deleteConversation(id,"");
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除会话
|
||||
*
|
||||
* @param id
|
||||
* @return
|
||||
* @author wangshuai
|
||||
* @date 2025/12/11 20:00
|
||||
*/
|
||||
@IgnoreAuth
|
||||
@DeleteMapping(value = "/conversation/{id}/{sessionType}")
|
||||
public Result<?> deleteConversationByType(@PathVariable("id") String id,
|
||||
@PathVariable("sessionType") String sessionType) {
|
||||
return chatService.deleteConversation(id,sessionType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新会话标题
|
||||
*
|
||||
* @param updateTitleParams
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/3/3 16:55
|
||||
*/
|
||||
@IgnoreAuth
|
||||
@PutMapping(value = "/conversation/update/title")
|
||||
public Result<?> updateConversationTitle(@RequestBody ChatConversation updateTitleParams) {
|
||||
return chatService.updateConversationTitle(updateTitleParams);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取消息
|
||||
*
|
||||
* @return 返回一个Result对象,包含消息的信息
|
||||
* @author chenrui
|
||||
* @date 2025/2/25 11:42
|
||||
*/
|
||||
@IgnoreAuth
|
||||
@GetMapping(value = "/messages")
|
||||
public Result<?> getMessages(@RequestParam(value = "conversationId", required = true) String conversationId,
|
||||
@RequestParam(value = "sessionType", required = false) String sessionType) {
|
||||
return chatService.getMessages(conversationId, sessionType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空消息
|
||||
*
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/2/25 11:42
|
||||
*/
|
||||
@IgnoreAuth
|
||||
@GetMapping(value = "/messages/clear/{conversationId}")
|
||||
public Result<?> clearMessage(@PathVariable(value = "conversationId") String conversationId) {
|
||||
return chatService.clearMessage(conversationId, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空消息
|
||||
*
|
||||
* @return
|
||||
* @author wangshuai
|
||||
* @date 2025/12/11 19:06
|
||||
*/
|
||||
@IgnoreAuth
|
||||
@GetMapping(value = "/messages/clear/{conversationId}/{sessionType}")
|
||||
public Result<?> clearMessageByType(@PathVariable(value = "conversationId") String conversationId,
|
||||
@PathVariable(value = "sessionType") String sessionType) {
|
||||
return chatService.clearMessage(conversationId, sessionType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 继续接收消息
|
||||
*
|
||||
* @param requestId
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/8/11 17:49
|
||||
*/
|
||||
@IgnoreAuth
|
||||
@GetMapping(value = "/receive/{requestId}")
|
||||
public SseEmitter receiveByRequestId(@PathVariable(name = "requestId", required = true) String requestId) {
|
||||
return chatService.receiveByRequestId(requestId);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 根据请求ID停止某个请求的处理
|
||||
*
|
||||
* @param requestId 请求的唯一标识符,用于识别和停止特定的请求
|
||||
* @return 返回一个Result对象,表示停止请求的结果
|
||||
* @author chenrui
|
||||
* @date 2025/2/25 11:42
|
||||
*/
|
||||
@IgnoreAuth
|
||||
@GetMapping(value = "/stop/{requestId}")
|
||||
public Result<?> stop(@PathVariable(name = "requestId", required = true) String requestId) {
|
||||
return chatService.stop(requestId);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 上传文件
|
||||
* for [QQYUN-12135]AI聊天,上传图片提示非法token
|
||||
*
|
||||
* @param request
|
||||
* @param response
|
||||
* @return
|
||||
* @throws Exception
|
||||
* @author chenrui
|
||||
* @date 2025/4/25 11:04
|
||||
*/
|
||||
@IgnoreAuth
|
||||
@PostMapping(value = "/upload")
|
||||
public Result<?> upload(HttpServletRequest request, HttpServletResponse response) throws Exception {
|
||||
String bizPath = "airag";
|
||||
|
||||
MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request;
|
||||
// 获取上传文件对象
|
||||
MultipartFile file = multipartRequest.getFile("file");
|
||||
String savePath;
|
||||
if (CommonConstant.UPLOAD_TYPE_LOCAL.equals(uploadType)) {
|
||||
savePath = CommonUtils.uploadLocal(file, bizPath, uploadpath);
|
||||
} else {
|
||||
savePath = CommonUtils.upload(file, bizPath, uploadType);
|
||||
}
|
||||
Result<?> result = new Result<>();
|
||||
result.setMessage(savePath);
|
||||
result.setSuccess(true);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* ai海报生成
|
||||
* @return
|
||||
*/
|
||||
@PostMapping("/genAiPoster")
|
||||
public Result<String> genAiPoster(@RequestBody ChatSendParams chatSendParams){
|
||||
String imageUrl = chatService.genAiPoster(chatSendParams);
|
||||
return Result.OK(imageUrl);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 生成ai写作
|
||||
*
|
||||
* @param aiWriteGenerateVo
|
||||
* @return
|
||||
*/
|
||||
@PostMapping("/genAiWriter")
|
||||
public SseEmitter genAiWriter(@RequestBody AiWriteGenerateVo aiWriteGenerateVo){
|
||||
return chatService.genAiWriter(aiWriteGenerateVo);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
package org.jeecg.modules.airag.app.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
import org.jeecg.common.aspect.annotation.Dict;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecgframework.poi.excel.annotation.Excel;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @Description: AI应用
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-02-26
|
||||
* @Version: V1.0
|
||||
*/
|
||||
@Data
|
||||
@TableName("airag_app")
|
||||
@Accessors(chain = true)
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@Schema(description="AI应用")
|
||||
public class AiragApp implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 主键
|
||||
*/
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
@Schema(description = "主键")
|
||||
private java.lang.String id;
|
||||
/**
|
||||
* 创建人
|
||||
*/
|
||||
@Schema(description = "创建人")
|
||||
@Dict(dictTable = "sys_user",dicCode = "username",dicText = "realname")
|
||||
private java.lang.String createBy;
|
||||
/**
|
||||
* 创建日期
|
||||
*/
|
||||
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@Schema(description = "创建日期")
|
||||
private java.util.Date createTime;
|
||||
/**
|
||||
* 更新人
|
||||
*/
|
||||
@Schema(description = "更新人")
|
||||
private java.lang.String updateBy;
|
||||
/**
|
||||
* 更新日期
|
||||
*/
|
||||
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@Schema(description = "更新日期")
|
||||
private java.util.Date updateTime;
|
||||
/**
|
||||
* 所属部门
|
||||
*/
|
||||
@Schema(description = "所属部门")
|
||||
private java.lang.String sysOrgCode;
|
||||
/**
|
||||
* 租户id
|
||||
*/
|
||||
@Excel(name = "租户id", width = 15)
|
||||
@Schema(description = "租户id")
|
||||
private java.lang.String tenantId;
|
||||
/**
|
||||
* 应用名称
|
||||
*/
|
||||
@Excel(name = "应用名称", width = 15)
|
||||
@Schema(description = "应用名称")
|
||||
private java.lang.String name;
|
||||
/**
|
||||
* 应用描述
|
||||
*/
|
||||
@Excel(name = "应用描述", width = 15)
|
||||
@Schema(description = "应用描述")
|
||||
private java.lang.String descr;
|
||||
/**
|
||||
* 应用图标
|
||||
*/
|
||||
@Excel(name = "应用图标", width = 15)
|
||||
@Schema(description = "应用图标")
|
||||
private java.lang.String icon;
|
||||
/**
|
||||
* 应用类型
|
||||
*/
|
||||
@Excel(name = "应用类型", width = 15, dicCode = "ai_app_type")
|
||||
@Dict(dicCode = "ai_app_type")
|
||||
@Schema(description = "应用类型")
|
||||
private java.lang.String type;
|
||||
/**
|
||||
* 开场白
|
||||
*/
|
||||
@Excel(name = "开场白", width = 15)
|
||||
@Schema(description = "开场白")
|
||||
private java.lang.String prologue;
|
||||
/**
|
||||
* 预设问题
|
||||
*/
|
||||
@Excel(name = "预设问题", width = 15)
|
||||
@Schema(description = "预设问题")
|
||||
private java.lang.String presetQuestion;
|
||||
/**
|
||||
* 提示词
|
||||
*/
|
||||
@Excel(name = "提示词", width = 15)
|
||||
@Schema(description = "提示词")
|
||||
private java.lang.String prompt;
|
||||
/**
|
||||
* 模型配置
|
||||
*/
|
||||
@Excel(name = "模型配置", width = 15, dictTable = "airag_model where model_type = 'LLM' ", dicText = "name", dicCode = "id")
|
||||
@Dict(dictTable = "airag_model where model_type = 'LLM' ", dicText = "name", dicCode = "id")
|
||||
@Schema(description = "模型配置")
|
||||
private java.lang.String modelId;
|
||||
/**
|
||||
* 历史消息数
|
||||
*/
|
||||
@Excel(name = "历史消息数", width = 15)
|
||||
@Schema(description = "历史消息数")
|
||||
private java.lang.Integer msgNum;
|
||||
/**
|
||||
* 知识库
|
||||
*/
|
||||
@Excel(name = "知识库", width = 15, dictTable = "airag_knowledge where status = 'enable'", dicText = "name", dicCode = "id")
|
||||
@Dict(dictTable = "airag_knowledge where status = 'enable'", dicText = "name", dicCode = "id")
|
||||
@Schema(description = "知识库")
|
||||
private java.lang.String knowledgeIds;
|
||||
/**
|
||||
* 流程
|
||||
*/
|
||||
@Excel(name = "流程", width = 15, dictTable = "airag_flow where status = 'enable' ", dicText = "name", dicCode = "id")
|
||||
@Dict(dictTable = "airag_flow where status = 'enable' ", dicText = "name", dicCode = "id")
|
||||
@Schema(description = "流程")
|
||||
private java.lang.String flowId;
|
||||
/**
|
||||
* 快捷指令
|
||||
*/
|
||||
@Excel(name = "快捷指令", width = 15)
|
||||
@Schema(description = "快捷指令")
|
||||
private java.lang.String quickCommand;
|
||||
/**
|
||||
* 状态(enable=启用、disable=禁用、release=发布)
|
||||
*/
|
||||
@Excel(name = "状态", width = 15)
|
||||
@Schema(description = "状态")
|
||||
private java.lang.String status;
|
||||
|
||||
|
||||
/**
|
||||
* 元数据
|
||||
*/
|
||||
@Excel(name = "元数据", width = 15)
|
||||
@Schema(description = "元数据")
|
||||
private java.lang.String metadata;
|
||||
|
||||
/**
|
||||
* 插件 [{pluginId: '123213', pluginName: 'xxxx', category: 'mcp'}]
|
||||
*/
|
||||
@Schema(description = "插件")
|
||||
private java.lang.String plugins;
|
||||
|
||||
/**
|
||||
* 是否开启记忆(0 不开启,1开启)
|
||||
*/
|
||||
@Schema(description = "是否开启记忆(0 不开启,1开启)")
|
||||
private java.lang.Integer izOpenMemory;
|
||||
/**
|
||||
* 记忆库,知识库的id
|
||||
*/
|
||||
@Schema(description = "记忆库")
|
||||
private java.lang.String memoryId;
|
||||
|
||||
/**
|
||||
* 变量
|
||||
*/
|
||||
@Schema(description = "变量")
|
||||
private java.lang.String variables;
|
||||
|
||||
/**
|
||||
* 记忆和变量提示词
|
||||
*/
|
||||
@Schema(description = "记忆和变量提示词")
|
||||
private java.lang.String memoryPrompt;
|
||||
|
||||
/**
|
||||
* 知识库ids
|
||||
*/
|
||||
@TableField(exist = false)
|
||||
private List<String> knowIds;
|
||||
|
||||
/**
|
||||
* 获取知识库id
|
||||
*
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/2/28 11:45
|
||||
*/
|
||||
public List<String> getKnowIds() {
|
||||
if (oConvertUtils.isNotEmpty(knowledgeIds)) {
|
||||
String[] knowIds = knowledgeIds.split(",");
|
||||
return Arrays.asList(knowIds);
|
||||
} else {
|
||||
return new ArrayList<>(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package org.jeecg.modules.airag.app.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.InterceptorIgnore;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.jeecg.modules.airag.app.entity.AiragApp;
|
||||
|
||||
/**
|
||||
* @Description: AI应用
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-02-26
|
||||
* @Version: V1.0
|
||||
*/
|
||||
public interface AiragAppMapper extends BaseMapper<AiragApp> {
|
||||
|
||||
/**
|
||||
* 根据ID查询app信息(忽略租户)
|
||||
* @param id
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/4/21 16:03
|
||||
*/
|
||||
@InterceptorIgnore(tenantLine = "true")
|
||||
AiragApp getByIdIgnoreTenant(String id);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?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.jeecg.modules.airag.app.mapper.AiragAppMapper">
|
||||
|
||||
<select id="getByIdIgnoreTenant" resultType="org.jeecg.modules.airag.app.entity.AiragApp">
|
||||
SELECT * FROM airag_app WHERE id = #{id}
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
@@ -0,0 +1,57 @@
|
||||
package org.jeecg.modules.airag.app.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import org.jeecg.modules.airag.app.entity.AiragApp;
|
||||
import org.jeecg.modules.airag.app.vo.AiArticleWriteVersionVo;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @Description: AI应用
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-02-26
|
||||
* @Version: V1.0
|
||||
*/
|
||||
public interface IAiragAppService extends IService<AiragApp> {
|
||||
|
||||
/**
|
||||
* 生成提示词
|
||||
* @param prompt
|
||||
* @return blocking 是否阻塞
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/3/12 14:45
|
||||
*/
|
||||
Object generatePrompt(String prompt,boolean blocking);
|
||||
|
||||
/**
|
||||
* 根据应用id生成提示词
|
||||
*
|
||||
* @param variables
|
||||
* @param memoryId
|
||||
* @param blocking
|
||||
* @return
|
||||
*/
|
||||
Object generateMemoryByAppId(String variables, String memoryId, boolean blocking);
|
||||
|
||||
/**
|
||||
* 写作保存
|
||||
*
|
||||
* @param aiWriteVersionVo
|
||||
*/
|
||||
void saveArticleWrite(AiArticleWriteVersionVo aiWriteVersionVo);
|
||||
|
||||
/**
|
||||
* 写作列表
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
List<AiArticleWriteVersionVo> listArticleWrite();
|
||||
|
||||
/**
|
||||
* 写作删除
|
||||
*
|
||||
* @param version
|
||||
*/
|
||||
void deleteArticleWrite(String version);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
package org.jeecg.modules.airag.app.service;
|
||||
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.modules.airag.app.vo.AiWriteGenerateVo;
|
||||
import org.jeecg.modules.airag.app.vo.AppDebugParams;
|
||||
import org.jeecg.modules.airag.app.vo.ChatConversation;
|
||||
import org.jeecg.modules.airag.app.vo.ChatSendParams;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
/**
|
||||
* ai聊天
|
||||
*
|
||||
* @author chenrui
|
||||
* @date 2025/2/25 13:36
|
||||
*/
|
||||
public interface IAiragChatService {
|
||||
|
||||
/**
|
||||
* 发送消息
|
||||
*
|
||||
* @param chatSendParams
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/2/25 13:39
|
||||
*/
|
||||
SseEmitter send(ChatSendParams chatSendParams);
|
||||
|
||||
|
||||
/**
|
||||
* 调试应用
|
||||
*
|
||||
* @param appDebugParams
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/2/28 10:49
|
||||
*/
|
||||
SseEmitter debugApp(AppDebugParams appDebugParams);
|
||||
|
||||
/**
|
||||
* 停止响应
|
||||
*
|
||||
* @param requestId
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/2/25 17:17
|
||||
*/
|
||||
Result<?> stop(String requestId);
|
||||
|
||||
/**
|
||||
* 获取所有对话
|
||||
*
|
||||
* @param appId
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/2/26 14:48
|
||||
*/
|
||||
Result<?> getConversations(String appId);
|
||||
|
||||
/**
|
||||
* 获取对话聊天记录
|
||||
*
|
||||
* @param conversationId
|
||||
* @param sessionType 类型
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/2/26 15:16
|
||||
*/
|
||||
Result<?> getMessages(String conversationId, String sessionType);
|
||||
|
||||
/**
|
||||
* 删除会话
|
||||
*
|
||||
* @param conversationId
|
||||
* @param sessionType
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/3/3 16:55
|
||||
*/
|
||||
Result<?> deleteConversation(String conversationId, String sessionType);
|
||||
|
||||
/**
|
||||
* 更新会话标题
|
||||
* @param updateTitleParams
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/3/3 17:02
|
||||
*/
|
||||
Result<?> updateConversationTitle(ChatConversation updateTitleParams);
|
||||
|
||||
/**
|
||||
* 清空消息
|
||||
* @param conversationId
|
||||
* @param sessionType
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/3/3 19:49
|
||||
*/
|
||||
Result<?> clearMessage(String conversationId, String sessionType);
|
||||
|
||||
/**
|
||||
* 初始化聊天(忽略租户)
|
||||
* [QQYUN-12113]分享之后的聊天,应用、模型、知识库不根据租户查询
|
||||
* @param appId
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/4/21 14:17
|
||||
*/
|
||||
Result<?> initChat(String appId);
|
||||
|
||||
/**
|
||||
* 继续接收消息
|
||||
* @param requestId
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/8/11 17:39
|
||||
*/
|
||||
SseEmitter receiveByRequestId(String requestId);
|
||||
|
||||
/**
|
||||
* 根据类型获取会话列表
|
||||
*
|
||||
* @param sessionType
|
||||
* @return
|
||||
*/
|
||||
Result<?> getConversationsByType(String sessionType);
|
||||
|
||||
/**
|
||||
* 生成海报图片
|
||||
* @param chatSendParams
|
||||
* @return
|
||||
*/
|
||||
String genAiPoster(ChatSendParams chatSendParams);
|
||||
|
||||
/**
|
||||
* 生成ai创作
|
||||
*
|
||||
* @param chatSendParams
|
||||
* @return
|
||||
*/
|
||||
SseEmitter genAiWriter(AiWriteGenerateVo chatSendParams);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package org.jeecg.modules.airag.app.service;
|
||||
|
||||
import org.jeecg.modules.airag.app.entity.AiragApp;
|
||||
import org.jeecg.modules.airag.common.handler.AIChatParams;
|
||||
|
||||
public interface IAiragVariableService {
|
||||
/**
|
||||
* 更新变量值
|
||||
*
|
||||
* @param userId
|
||||
* @param appId
|
||||
* @param name
|
||||
* @param value
|
||||
*/
|
||||
void updateVariable(String userId, String appId, String name, String value);
|
||||
|
||||
/**
|
||||
* 追加提示词
|
||||
*
|
||||
* @param username
|
||||
* @param app
|
||||
* @return
|
||||
*/
|
||||
String additionalPrompt(String username, AiragApp app);
|
||||
|
||||
/**
|
||||
* 初始化变量(仅不存在时设置)
|
||||
*
|
||||
* @param userId
|
||||
* @param appId
|
||||
* @param name
|
||||
* @param defaultValue
|
||||
*/
|
||||
void initVariable(String userId, String appId, String name, String defaultValue);
|
||||
|
||||
/**
|
||||
* 添加变量更新工具
|
||||
*
|
||||
* @param params
|
||||
* @param aiApp
|
||||
* @param username
|
||||
*/
|
||||
void addUpdateVariableTool(AiragApp aiApp, String username, AIChatParams params);
|
||||
}
|
||||
@@ -0,0 +1,326 @@
|
||||
package org.jeecg.modules.airag.app.service.impl;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.alibaba.fastjson.JSONArray;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import dev.langchain4j.data.message.AiMessage;
|
||||
import dev.langchain4j.data.message.ChatMessage;
|
||||
import dev.langchain4j.data.message.SystemMessage;
|
||||
import dev.langchain4j.data.message.UserMessage;
|
||||
import dev.langchain4j.model.output.FinishReason;
|
||||
import dev.langchain4j.service.TokenStream;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.exception.JeecgBootBizTipException;
|
||||
import org.jeecg.common.system.vo.LoginUser;
|
||||
import org.jeecg.common.util.AssertUtils;
|
||||
import org.jeecg.common.util.UUIDGenerator;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.modules.airag.app.consts.AiAppConsts;
|
||||
import org.jeecg.modules.airag.app.consts.Prompts;
|
||||
import org.jeecg.modules.airag.app.entity.AiragApp;
|
||||
import org.jeecg.modules.airag.app.mapper.AiragAppMapper;
|
||||
import org.jeecg.modules.airag.app.service.IAiragAppService;
|
||||
import org.jeecg.modules.airag.app.vo.AiArticleWriteVersionVo;
|
||||
import org.jeecg.modules.airag.app.vo.AppVariableVo;
|
||||
import org.jeecg.modules.airag.common.consts.AiragConsts;
|
||||
import org.jeecg.modules.airag.common.handler.AIChatParams;
|
||||
import org.jeecg.modules.airag.common.handler.IAIChatHandler;
|
||||
import org.jeecg.modules.airag.common.utils.AiragLocalCache;
|
||||
import org.jeecg.modules.airag.common.vo.event.EventData;
|
||||
import org.jeecg.modules.airag.common.vo.event.EventFlowData;
|
||||
import org.jeecg.modules.airag.common.vo.event.EventMessageData;
|
||||
import org.jeecg.modules.airag.llm.entity.AiragKnowledge;
|
||||
import org.jeecg.modules.airag.llm.service.IAiragKnowledgeService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @Description: AI应用
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-02-26
|
||||
* @Version: V1.0
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class AiragAppServiceImpl extends ServiceImpl<AiragAppMapper, AiragApp> implements IAiragAppService {
|
||||
|
||||
@Autowired
|
||||
IAIChatHandler aiChatHandler;
|
||||
|
||||
@Autowired
|
||||
private IAiragKnowledgeService airagKnowledgeService;
|
||||
|
||||
@Autowired
|
||||
private RedisTemplate redisTemplate;
|
||||
|
||||
@Override
|
||||
public Object generatePrompt(String prompt, boolean blocking) {
|
||||
AssertUtils.assertNotEmpty("请输入提示词", prompt);
|
||||
List<ChatMessage> messages = Arrays.asList(new SystemMessage(Prompts.GENERATE_LLM_PROMPT), new UserMessage(prompt));
|
||||
|
||||
AIChatParams params = new AIChatParams();
|
||||
params.setTemperature(0.8);
|
||||
params.setTopP(0.9);
|
||||
params.setPresencePenalty(0.1);
|
||||
params.setFrequencyPenalty(0.1);
|
||||
if(blocking){
|
||||
String promptValue = aiChatHandler.completionsByDefaultModel(messages, params);
|
||||
if (promptValue == null || promptValue.isEmpty()) {
|
||||
return Result.error("生成失败");
|
||||
}
|
||||
return Result.OK("success", promptValue);
|
||||
}else{
|
||||
//update-begin---author:wangshuai---date:2026-01-08---for: 将流式输出单独抽出去,变量和记忆也需要---
|
||||
return startSseChat(messages, params);
|
||||
//update-end---author:wangshuai---date:2026-01-08---for: 将流式输出单独抽出去,变量和记忆也需要---
|
||||
}
|
||||
}
|
||||
|
||||
//update-begin---author:wangshuai---date:2026-01-05---for:【QQYUN-14479】增加一个开启记忆的按钮。下面为提示词和记忆,将记忆提示词单独拆分---
|
||||
@Override
|
||||
public Object generateMemoryByAppId(String variables, String memoryId, boolean blocking) {
|
||||
if(oConvertUtils.isEmpty(variables) && oConvertUtils.isEmpty(memoryId)){
|
||||
throw new JeecgBootBizTipException("请先添加变量或者记忆后再次重试!");
|
||||
}
|
||||
// 构建变量描述
|
||||
StringBuilder variablesDesc = new StringBuilder();
|
||||
if (oConvertUtils.isNotEmpty(variables)) {
|
||||
List<AppVariableVo> variableList = JSONArray.parseArray(variables, AppVariableVo.class);
|
||||
if (variableList != null && !variableList.isEmpty()) {
|
||||
for (AppVariableVo var : variableList) {
|
||||
if (var.getEnable() != null && !var.getEnable()) {
|
||||
continue;
|
||||
}
|
||||
String name = var.getName();
|
||||
if (oConvertUtils.isNotEmpty(var.getAction())) {
|
||||
String action = var.getAction();
|
||||
if (oConvertUtils.isNotEmpty(name)) {
|
||||
try {
|
||||
// 使用正则替换未被{{}}包裹的变量名
|
||||
String regex = "(?<!\\{\\{)\\b" + Pattern.quote(name) + "\\b(?!\\}\\})";
|
||||
action = action.replaceAll(regex, "{{" + name + "}}");
|
||||
} catch (Exception e) {
|
||||
log.warn("变量名替换异常: name={}", name, e);
|
||||
}
|
||||
}
|
||||
variablesDesc.append(action).append("\n");
|
||||
} else {
|
||||
variablesDesc.append("- {{").append(name).append("}}");
|
||||
if (oConvertUtils.isNotEmpty(var.getDescription())) {
|
||||
variablesDesc.append(": ").append(var.getDescription());
|
||||
}
|
||||
variablesDesc.append("\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 构建Prompt
|
||||
StringBuilder promptBuilder = new StringBuilder(Prompts.GENERATE_GUIDE_HEADER);
|
||||
if (!variablesDesc.isEmpty()) {
|
||||
promptBuilder.append(String.format(Prompts.GENERATE_VAR_PART, variablesDesc.toString()));
|
||||
}
|
||||
|
||||
// 构建记忆状态描述
|
||||
if (oConvertUtils.isNotEmpty(memoryId)) {
|
||||
String memoryDescr = "";
|
||||
AiragKnowledge memory = airagKnowledgeService.getById(memoryId);
|
||||
if (memory != null && oConvertUtils.isNotEmpty(memory.getDescr())) {
|
||||
memoryDescr += "记忆库描述:" + memory.getDescr();
|
||||
}
|
||||
promptBuilder.append(String.format(Prompts.GENERATE_MEMORY_PART, memoryDescr));
|
||||
}
|
||||
|
||||
String prompt = promptBuilder.toString();
|
||||
|
||||
List<ChatMessage> messages = List.of(new UserMessage(prompt));
|
||||
|
||||
AIChatParams params = new AIChatParams();
|
||||
params.setTemperature(0.7);
|
||||
|
||||
if(blocking){
|
||||
String promptValue = aiChatHandler.completionsByDefaultModel(messages, params);
|
||||
if (promptValue == null || promptValue.isEmpty()) {
|
||||
return Result.error("生成失败");
|
||||
}
|
||||
return Result.OK("success", promptValue);
|
||||
}else{
|
||||
return startSseChat(messages, params);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送聊天
|
||||
* @param messages
|
||||
* @param params
|
||||
* @return
|
||||
*/
|
||||
private SseEmitter startSseChat(List<ChatMessage> messages, AIChatParams params) {
|
||||
SseEmitter emitter = new SseEmitter(-0L);
|
||||
// 异步运行(流式)
|
||||
TokenStream tokenStream = aiChatHandler.chatByDefaultModel(messages, params);
|
||||
/**
|
||||
* 是否正在思考
|
||||
*/
|
||||
AtomicBoolean isThinking = new AtomicBoolean(false);
|
||||
String requestId = UUIDGenerator.generate();
|
||||
// ai聊天响应逻辑
|
||||
tokenStream.onPartialResponse((String resMessage) -> {
|
||||
// 兼容推理模型
|
||||
if ("<think>".equals(resMessage)) {
|
||||
isThinking.set(true);
|
||||
resMessage = "> ";
|
||||
}
|
||||
if ("</think>".equals(resMessage)) {
|
||||
isThinking.set(false);
|
||||
resMessage = "\n\n";
|
||||
}
|
||||
if (isThinking.get()) {
|
||||
if (null != resMessage && resMessage.contains("\n")) {
|
||||
resMessage = "\n> ";
|
||||
}
|
||||
}
|
||||
EventData eventData = new EventData(requestId, null, EventData.EVENT_MESSAGE);
|
||||
EventMessageData messageEventData = EventMessageData.builder()
|
||||
.message(resMessage)
|
||||
.build();
|
||||
eventData.setData(messageEventData);
|
||||
try {
|
||||
String eventStr = JSONObject.toJSONString(eventData);
|
||||
log.debug("[AI应用]接收LLM返回消息:{}", eventStr);
|
||||
emitter.send(SseEmitter.event().data(eventStr));
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
})
|
||||
.onCompleteResponse((responseMessage) -> {
|
||||
// 记录ai的回复
|
||||
AiMessage aiMessage = responseMessage.aiMessage();
|
||||
FinishReason finishReason = responseMessage.finishReason();
|
||||
String respText = aiMessage.text();
|
||||
if (FinishReason.STOP.equals(finishReason) || null == finishReason) {
|
||||
// 正常结束
|
||||
EventData eventData = new EventData(requestId, null, EventData.EVENT_MESSAGE_END);
|
||||
try {
|
||||
log.debug("[AI应用]接收LLM返回消息完成:{}", respText);
|
||||
emitter.send(SseEmitter.event().data(eventData));
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
closeSSE(emitter, eventData);
|
||||
} else {
|
||||
// 异常结束
|
||||
log.error("调用模型异常:" + respText);
|
||||
if (respText.contains("insufficient Balance")) {
|
||||
respText = "大预言模型账号余额不足!";
|
||||
}
|
||||
EventData eventData = new EventData(requestId, null, EventData.EVENT_FLOW_ERROR);
|
||||
eventData.setData(EventFlowData.builder().success(false).message(respText).build());
|
||||
closeSSE(emitter, eventData);
|
||||
}
|
||||
})
|
||||
.onError((Throwable error) -> {
|
||||
// sse
|
||||
String errMsg = "调用大模型接口失败:" + error.getMessage();
|
||||
log.error(errMsg, error);
|
||||
EventData eventData = new EventData(requestId, null, EventData.EVENT_FLOW_ERROR);
|
||||
eventData.setData(EventFlowData.builder().success(false).message(errMsg).build());
|
||||
closeSSE(emitter, eventData);
|
||||
})
|
||||
.start();
|
||||
return emitter;
|
||||
}
|
||||
//update-end---author:wangshuai---date:2026-01-05---for:【QQYUN-14479】增加一个开启记忆的按钮。下面为提示词和记忆,将记忆提示词单独拆分---
|
||||
|
||||
private static void closeSSE(SseEmitter emitter, EventData eventData) {
|
||||
try {
|
||||
// 发送完成事件
|
||||
emitter.send(SseEmitter.event().data(eventData));
|
||||
} catch (IOException e) {
|
||||
log.error("终止会话时发生错误", e);
|
||||
} finally {
|
||||
// 从缓存中移除emitter
|
||||
AiragLocalCache.remove(AiragConsts.CACHE_TYPE_SSE, eventData.getRequestId());
|
||||
// 关闭emitter
|
||||
emitter.complete();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 写作列表
|
||||
*/
|
||||
@Override
|
||||
public List<AiArticleWriteVersionVo> listArticleWrite() {
|
||||
LoginUser loginUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
||||
String redisKey = StrUtil.format(AiAppConsts.ARTICLE_WRITER_KEY, loginUser.getUsername());
|
||||
Object data = redisTemplate.opsForValue().get(redisKey);
|
||||
if (data == null) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
List<AiArticleWriteVersionVo> aiWriteViewVoList = (List<AiArticleWriteVersionVo>) data;
|
||||
Collections.reverse(aiWriteViewVoList);
|
||||
return aiWriteViewVoList;
|
||||
}
|
||||
|
||||
/**
|
||||
* 写作报错
|
||||
*/
|
||||
@Override
|
||||
public void saveArticleWrite(AiArticleWriteVersionVo aiWriteVersionVo) {
|
||||
LoginUser loginUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
||||
String redisKey = StrUtil.format(AiAppConsts.ARTICLE_WRITER_KEY, loginUser.getUsername());
|
||||
//先查看redis中是否存在
|
||||
Object data = redisTemplate.opsForValue().get(redisKey);
|
||||
if(null != data){
|
||||
List<AiArticleWriteVersionVo> aiWriteVersionVos = (List<AiArticleWriteVersionVo>) data;
|
||||
aiWriteVersionVo.setVersion("V"+(aiWriteVersionVos.size() + 1));
|
||||
aiWriteVersionVos.add(aiWriteVersionVo);
|
||||
redisTemplate.opsForValue().set(redisKey, aiWriteVersionVos);
|
||||
}else{
|
||||
List<AiArticleWriteVersionVo> aiWriteVersionVos = new ArrayList<>();
|
||||
aiWriteVersionVo.setVersion("V1");
|
||||
aiWriteVersionVos.add(aiWriteVersionVo);
|
||||
redisTemplate.opsForValue().set(redisKey, aiWriteVersionVos);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 写作删除
|
||||
*/
|
||||
@Override
|
||||
public void deleteArticleWrite(String version) {
|
||||
LoginUser loginUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
|
||||
String redisKey = StrUtil.format(AiAppConsts.ARTICLE_WRITER_KEY, loginUser.getUsername());
|
||||
Object data = redisTemplate.opsForValue().get(redisKey);
|
||||
if (data == null) {
|
||||
return;
|
||||
}
|
||||
List<AiArticleWriteVersionVo> aiWriteVersionVos = (List<AiArticleWriteVersionVo>) data;
|
||||
if (aiWriteVersionVos.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
List<AiArticleWriteVersionVo> newList = aiWriteVersionVos.stream()
|
||||
.filter(vo -> !version.equals(vo.getVersion()))
|
||||
.collect(Collectors.toList());
|
||||
if (newList.isEmpty()) {
|
||||
redisTemplate.delete(redisKey);
|
||||
} else {
|
||||
redisTemplate.opsForValue().set(redisKey, newList);
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,194 @@
|
||||
package org.jeecg.modules.airag.app.service.impl;
|
||||
|
||||
import com.alibaba.fastjson.JSONArray;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import dev.langchain4j.agent.tool.ToolSpecification;
|
||||
import dev.langchain4j.model.chat.request.json.JsonObjectSchema;
|
||||
import dev.langchain4j.service.tool.ToolExecutor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.common.util.SpringContextUtils;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.modules.airag.app.consts.AiAppConsts;
|
||||
import org.jeecg.modules.airag.app.entity.AiragApp;
|
||||
import org.jeecg.modules.airag.app.service.IAiragVariableService;
|
||||
import org.jeecg.modules.airag.app.vo.AppVariableVo;
|
||||
import org.jeecg.modules.airag.common.handler.AIChatParams;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @Description: AI应用变量服务实现
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-02-26
|
||||
* @Version: V1.0
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class AiragVariableServiceImpl implements IAiragVariableService {
|
||||
|
||||
@Autowired
|
||||
private RedisTemplate redisTemplate;
|
||||
|
||||
private static final String CACHE_PREFIX = "airag:app:var:";
|
||||
|
||||
/**
|
||||
* 初始化变量(仅不存在时设置)
|
||||
*
|
||||
* @param username
|
||||
* @param appId
|
||||
* @param name
|
||||
* @param defaultValue
|
||||
*/
|
||||
@Override
|
||||
public void initVariable(String username, String appId, String name, String defaultValue) {
|
||||
if (oConvertUtils.isEmpty(username) || oConvertUtils.isEmpty(appId) || oConvertUtils.isEmpty(name)) {
|
||||
return;
|
||||
}
|
||||
String key = CACHE_PREFIX + appId + ":" + username;
|
||||
redisTemplate.opsForHash().putIfAbsent(key, name, defaultValue != null ? defaultValue : "");
|
||||
}
|
||||
|
||||
/**
|
||||
* 追加提示词
|
||||
*
|
||||
* @param username
|
||||
* @param app
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public String additionalPrompt(String username, AiragApp app) {
|
||||
String memoryPrompt = app.getMemoryPrompt();
|
||||
String prompt = app.getPrompt();
|
||||
|
||||
if (oConvertUtils.isEmpty(memoryPrompt)) {
|
||||
return prompt;
|
||||
}
|
||||
String variablesStr = app.getVariables();
|
||||
if (oConvertUtils.isEmpty(variablesStr)) {
|
||||
return prompt;
|
||||
}
|
||||
|
||||
List<AppVariableVo> variableList = JSONArray.parseArray(variablesStr, AppVariableVo.class);
|
||||
if (variableList == null || variableList.isEmpty()) {
|
||||
return prompt;
|
||||
}
|
||||
|
||||
String key = CACHE_PREFIX + app.getId() + ":" + username;
|
||||
Map<Object, Object> savedValues = redisTemplate.opsForHash().entries(key);
|
||||
|
||||
for (AppVariableVo variable : variableList) {
|
||||
if (variable.getEnable() != null && !variable.getEnable()) {
|
||||
continue;
|
||||
}
|
||||
String name = variable.getName();
|
||||
String value = variable.getDefaultValue();
|
||||
|
||||
// 优先使用Redis中的值
|
||||
if (savedValues.containsKey(name)) {
|
||||
Object savedVal = savedValues.get(name);
|
||||
if (savedVal != null) {
|
||||
value = String.valueOf(savedVal);
|
||||
}
|
||||
}
|
||||
|
||||
if (value == null) {
|
||||
value = "";
|
||||
}
|
||||
|
||||
// 替换 {{name}}
|
||||
memoryPrompt = memoryPrompt.replace("{{" + name + "}}", value);
|
||||
}
|
||||
return prompt + "\n" + memoryPrompt;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新变量值
|
||||
*
|
||||
* @param userId
|
||||
* @param appId
|
||||
* @param name
|
||||
* @param value
|
||||
*/
|
||||
@Override
|
||||
public void updateVariable(String userId, String appId, String name, String value) {
|
||||
if (oConvertUtils.isEmpty(userId) || oConvertUtils.isEmpty(appId) || oConvertUtils.isEmpty(name)) {
|
||||
return;
|
||||
}
|
||||
String key = CACHE_PREFIX + appId + ":" + userId;
|
||||
redisTemplate.opsForHash().put(key, name, value);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 添加变量更新工具
|
||||
*
|
||||
* @param params
|
||||
* @param aiApp
|
||||
* @param username
|
||||
*/
|
||||
@Override
|
||||
public void addUpdateVariableTool(AiragApp aiApp, String username, AIChatParams params) {
|
||||
if (params.getTools() == null) {
|
||||
params.setTools(new HashMap<>());
|
||||
}
|
||||
if (!AiAppConsts.IZ_OPEN_MEMORY.equals(aiApp.getIzOpenMemory())) {
|
||||
return;
|
||||
}
|
||||
// 构建变量描述信息
|
||||
String variablesStr = aiApp.getVariables();
|
||||
List<AppVariableVo> variableList = null;
|
||||
if (oConvertUtils.isNotEmpty(variablesStr)) {
|
||||
variableList = JSONArray.parseArray(variablesStr, AppVariableVo.class);
|
||||
}
|
||||
|
||||
//工具描述
|
||||
StringBuilder descriptionBuilder = new StringBuilder("更新应用变量的值。仅当检测到变量的新值与当前值不一致时调用。如果已调用过或值未变,请勿重复调用。");
|
||||
if (variableList != null && !variableList.isEmpty()) {
|
||||
descriptionBuilder.append("\n\n可用变量列表:");
|
||||
for (AppVariableVo var : variableList) {
|
||||
if (var.getEnable() != null && !var.getEnable()) {
|
||||
continue;
|
||||
}
|
||||
descriptionBuilder.append("\n- ").append(var.getName());
|
||||
if (oConvertUtils.isNotEmpty(var.getDescription())) {
|
||||
descriptionBuilder.append(": ").append(var.getDescription());
|
||||
}
|
||||
}
|
||||
descriptionBuilder.append("\n\n注意:variableName必须是上述列表中的名称之一。");
|
||||
}
|
||||
|
||||
//构建更新变量的工具
|
||||
ToolSpecification spec = ToolSpecification.builder()
|
||||
.name("update_variable")
|
||||
.description(descriptionBuilder.toString())
|
||||
.parameters(JsonObjectSchema.builder()
|
||||
.addStringProperty("variableName", "变量名称")
|
||||
.addStringProperty("value", "变量值")
|
||||
.required("variableName", "value")
|
||||
.build())
|
||||
.build();
|
||||
|
||||
//监听工具的调用
|
||||
ToolExecutor executor = (toolExecutionRequest, memoryId) -> {
|
||||
try {
|
||||
JSONObject args = JSONObject.parseObject(toolExecutionRequest.arguments());
|
||||
String name = args.getString("variableName");
|
||||
String value = args.getString("value");
|
||||
IAiragVariableService variableService = SpringContextUtils.getBean(IAiragVariableService.class);
|
||||
//更新变量值
|
||||
variableService.updateVariable(username, aiApp.getId(), name, value);
|
||||
return "变量 " + name + " 已更新为: " + value;
|
||||
} catch (Exception e) {
|
||||
log.error("更新变量失败", e);
|
||||
return "更新变量失败: " + e.getMessage();
|
||||
}
|
||||
};
|
||||
|
||||
params.getTools().put(spec, executor);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package org.jeecg.modules.airag.app.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* @Description: AI写作版本号
|
||||
*
|
||||
* @author: wangshuai
|
||||
* @date: 2026/1/16 11:57
|
||||
*/
|
||||
@Data
|
||||
public class AiArticleWriteVersionVo {
|
||||
|
||||
/**
|
||||
* 当前版本号
|
||||
*/
|
||||
private String version;
|
||||
|
||||
/**
|
||||
* 写作内容
|
||||
*/
|
||||
private String content;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package org.jeecg.modules.airag.app.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* @Description: ai写作生成实体类
|
||||
*
|
||||
* @author: wangshuai
|
||||
* @date: 2026/1/12 15:59
|
||||
*/
|
||||
@Data
|
||||
public class AiWriteGenerateVo {
|
||||
|
||||
/**
|
||||
* 写作类型
|
||||
*/
|
||||
private String activeMode;
|
||||
|
||||
/**
|
||||
* 写作内容提示
|
||||
*/
|
||||
private String prompt;
|
||||
|
||||
/**
|
||||
* 原文
|
||||
*/
|
||||
private String originalContent;
|
||||
|
||||
/**
|
||||
* 长度
|
||||
*/
|
||||
private String length;
|
||||
|
||||
/**
|
||||
* 格式
|
||||
*/
|
||||
private String format;
|
||||
|
||||
/**
|
||||
* 语气
|
||||
*/
|
||||
private String tone;
|
||||
|
||||
/**
|
||||
* 语言
|
||||
*/
|
||||
private String language;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package org.jeecg.modules.airag.app.vo;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.jeecg.modules.airag.app.entity.AiragApp;
|
||||
|
||||
/**
|
||||
* @Description: 应用调试入参
|
||||
* @Author: chenrui
|
||||
* @Date: 2025/2/25 11:47
|
||||
*/
|
||||
@Data
|
||||
public class AppDebugParams extends ChatSendParams {
|
||||
|
||||
/**
|
||||
* 应用信息
|
||||
*/
|
||||
AiragApp app;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package org.jeecg.modules.airag.app.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* @Description: 应用变量配置
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-02-26
|
||||
* @Version: V1.0
|
||||
*/
|
||||
@Data
|
||||
public class AppVariableVo implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 变量名
|
||||
*/
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* 描述
|
||||
*/
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* 默认值
|
||||
*/
|
||||
private String defaultValue;
|
||||
|
||||
/**
|
||||
* 是否启用
|
||||
*/
|
||||
private Boolean enable;
|
||||
|
||||
/**
|
||||
* 动作
|
||||
*/
|
||||
private String action;
|
||||
|
||||
/**
|
||||
* 排序
|
||||
*/
|
||||
private Integer orderNum;
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package org.jeecg.modules.airag.app.vo;
|
||||
|
||||
import lombok.Data;
|
||||
import org.jeecg.modules.airag.app.entity.AiragApp;
|
||||
import org.jeecg.modules.airag.common.vo.MessageHistory;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @Description: 聊天会话
|
||||
* @Author: chenrui
|
||||
* @Date: 2025/2/25 14:56
|
||||
*/
|
||||
@Data
|
||||
public class ChatConversation {
|
||||
|
||||
/**
|
||||
* 会话id
|
||||
*/
|
||||
private String id;
|
||||
|
||||
/**
|
||||
* 会话标题
|
||||
*/
|
||||
private String title;
|
||||
|
||||
/**
|
||||
* 消息记录
|
||||
*/
|
||||
private List<MessageHistory> messages;
|
||||
|
||||
/**
|
||||
* app
|
||||
*/
|
||||
private AiragApp app;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
private Date createTime;
|
||||
|
||||
/**
|
||||
* 流程入参配置(工作流的额外参数设置)
|
||||
* key: 参数field, value: 参数值
|
||||
* for [issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程
|
||||
*/
|
||||
private Map<String, Object> flowInputs;
|
||||
|
||||
/**
|
||||
* portal 应用门户
|
||||
*/
|
||||
private String sessionType;
|
||||
|
||||
/**
|
||||
* 是否保存会话
|
||||
*/
|
||||
private Boolean izSaveSession;
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package org.jeecg.modules.airag.app.vo;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @Description: 发送消息的入参
|
||||
* @Author: chenrui
|
||||
* @Date: 2025/2/25 11:47
|
||||
*/
|
||||
@NoArgsConstructor
|
||||
@Data
|
||||
public class ChatSendParams {
|
||||
|
||||
public ChatSendParams(String content, String conversationId, String topicId, String appId) {
|
||||
this.content = content;
|
||||
this.conversationId = conversationId;
|
||||
this.topicId = topicId;
|
||||
this.appId = appId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户输入的聊天内容
|
||||
*/
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* 对话会话ID
|
||||
*/
|
||||
private String conversationId;
|
||||
|
||||
/**
|
||||
* 对话主题ID(用于关联历史记录)
|
||||
*/
|
||||
private String topicId;
|
||||
|
||||
/**
|
||||
* 应用id
|
||||
*/
|
||||
private String appId;
|
||||
|
||||
/**
|
||||
* 图片列表
|
||||
*/
|
||||
private List<String> images;
|
||||
|
||||
/**
|
||||
* 文件列表
|
||||
*/
|
||||
private List<String> files;
|
||||
|
||||
/**
|
||||
* 工作流额外入参配置
|
||||
* key: 参数field, value: 参数值
|
||||
* for [issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程
|
||||
*/
|
||||
private Map<String, Object> flowInputs;
|
||||
|
||||
/**
|
||||
* 是否开启网络搜索(仅千问模型支持)
|
||||
*/
|
||||
private Boolean enableSearch;
|
||||
|
||||
/**
|
||||
* 是否开启深度思考
|
||||
*/
|
||||
private Boolean enableThink;
|
||||
|
||||
/**
|
||||
* 会话类型: portal 应用门户
|
||||
*/
|
||||
private String sessionType;
|
||||
|
||||
/**
|
||||
* 是否开启生成绘画
|
||||
*/
|
||||
private Boolean enableDraw;
|
||||
|
||||
/**
|
||||
* 绘画模型的id
|
||||
*/
|
||||
private String drawModelId;
|
||||
|
||||
/**
|
||||
* 图片尺寸
|
||||
*/
|
||||
private String imageSize;
|
||||
|
||||
/**
|
||||
* 一张图片
|
||||
*/
|
||||
private String imageUrl;
|
||||
|
||||
/**
|
||||
* 是否保存会话
|
||||
*/
|
||||
private Boolean izSaveSession;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package org.jeecg.modules.airag.demo;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.common.exception.JeecgBootBizTipException;
|
||||
import org.jeecg.modules.airag.flow.component.enhance.IAiRagEnhanceJava;
|
||||
import org.jeecgframework.poi.excel.ExcelImportUtil;
|
||||
import org.jeecgframework.poi.excel.entity.ImportParams;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Java增强Demo: Excel数据读取器
|
||||
* for [QQYUN-11718]【AI】积木报表对接AI流程编排接口展示报表
|
||||
* @Author: chenrui
|
||||
* @Date: 2025/4/29 16:51
|
||||
*/
|
||||
@Component("jimuDataReader")
|
||||
@Slf4j
|
||||
public class JimuDataReader implements IAiRagEnhanceJava {
|
||||
|
||||
|
||||
@Override
|
||||
public Map<String, Object> process(Map<String, Object> inputParams) {
|
||||
// inputParams: {"bizData":"/xxxx/xxxx/xxxx/xxxx.xls"}
|
||||
try {
|
||||
String filePath = (String) inputParams.get("bizData");
|
||||
if (filePath == null || filePath.isEmpty()) {
|
||||
throw new IllegalArgumentException("File path is empty");
|
||||
}
|
||||
|
||||
File excelFile = new File(filePath);
|
||||
if (!excelFile.exists() || !excelFile.isFile()) {
|
||||
throw new IllegalArgumentException("File not found: " + filePath);
|
||||
}
|
||||
|
||||
// Since we don't know the target entity class, we'll read the Excel generically
|
||||
return readExcelData(excelFile);
|
||||
} catch (Exception e) {
|
||||
log.error("Error processing Excel file", e);
|
||||
throw new JeecgBootBizTipException("调用java增强失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Excel导入工具方法,基于ExcelImportUtil
|
||||
*
|
||||
* @param file Excel文件
|
||||
* @return Excel读取结果,包含字段和数据
|
||||
* @throws Exception 导入过程中的异常
|
||||
*/
|
||||
public static Map<String, Object> readExcelData(File file) throws Exception {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
|
||||
// 设置导入参数
|
||||
ImportParams params = new ImportParams();
|
||||
params.setTitleRows(0); // 没有标题
|
||||
params.setHeadRows(1); // 第一行是表头
|
||||
|
||||
// 读取Excel数据
|
||||
List<Map<String, Object>> dataList = ExcelImportUtil.importExcel(file, Map.class, params);
|
||||
|
||||
// 如果没有数据,返回空结果
|
||||
if (dataList == null || dataList.isEmpty()) {
|
||||
result.put("fields", new ArrayList<>());
|
||||
result.put("datas", new ArrayList<>());
|
||||
return result;
|
||||
}
|
||||
|
||||
// 从第一行数据中获取字段名
|
||||
List<String> fieldNames = new ArrayList<>(dataList.get(0).keySet());
|
||||
|
||||
result.put("fields", fieldNames);
|
||||
result.put("datas", dataList);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package org.jeecg.modules.airag.demo;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.modules.airag.flow.component.enhance.IAiRagEnhanceJava;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @Description: Java增强节点示例类
|
||||
* @Author: chenrui
|
||||
* @Date: 2025/3/6 11:42
|
||||
*/
|
||||
@Slf4j
|
||||
@Component("testAiragEnhance")
|
||||
public class TestAiragEnhance implements IAiRagEnhanceJava {
|
||||
@Override
|
||||
public Map<String, Object> process(Map<String, Object> inputParams) {
|
||||
Object arg1 = inputParams.get("arg1");
|
||||
Object arg2 = inputParams.get("arg2");
|
||||
Object index = inputParams.get("index");
|
||||
log.info("arg1={}, arg2={}, index={}", arg1, arg2, index);
|
||||
return Collections.singletonMap("result",arg1.toString()+"java拼接"+arg2.toString());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package org.jeecg.modules.airag.llm.config;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 向量存储库配置
|
||||
*
|
||||
* @Author: chenrui
|
||||
* @Date: 2025/2/18 14:24
|
||||
*/
|
||||
@NoArgsConstructor
|
||||
@Data
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = EmbedStoreConfigBean.PREFIX)
|
||||
public class EmbedStoreConfigBean {
|
||||
public static final String PREFIX = "jeecg.airag.embed-store";
|
||||
|
||||
/**
|
||||
* host
|
||||
*/
|
||||
private String host = "127.0.0.1";
|
||||
/**
|
||||
* 端口
|
||||
*/
|
||||
private int port = 5432;
|
||||
/**
|
||||
* 数据库
|
||||
*/
|
||||
private String database = "postgres";
|
||||
/**
|
||||
* 用户名
|
||||
*/
|
||||
private String user = "postgres";
|
||||
/**
|
||||
* 密码
|
||||
*/
|
||||
private String password = "postgres";
|
||||
|
||||
/**
|
||||
* 存储向量的表
|
||||
*/
|
||||
private String table = "embeddings";
|
||||
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package org.jeecg.modules.airag.llm.config;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 知识库配置
|
||||
*
|
||||
* @Author: chenrui
|
||||
* @Date: 2025-04-01 14:19
|
||||
*/
|
||||
@NoArgsConstructor
|
||||
@Data
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = KnowConfigBean.PREFIX)
|
||||
public class KnowConfigBean {
|
||||
public static final String PREFIX = "jeecg.airag.know";
|
||||
|
||||
/**
|
||||
* 开启MinerU解析
|
||||
*/
|
||||
private boolean enableMinerU = false;
|
||||
|
||||
/**
|
||||
* conda的环境(默认不使用conda)
|
||||
*/
|
||||
private String condaEnv = null;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
package org.jeecg.modules.airag.llm.consts;
|
||||
|
||||
/**
|
||||
* @Description: 流程插件常量
|
||||
*
|
||||
* @author: wangshuai
|
||||
* @date: 2025/12/23 19:37
|
||||
*/
|
||||
public interface FlowPluginContent {
|
||||
/**
|
||||
* 名称
|
||||
*/
|
||||
String NAME = "name";
|
||||
|
||||
/**
|
||||
* 描述
|
||||
*/
|
||||
String DESCRIPTION = "description";
|
||||
|
||||
/**
|
||||
* 响应
|
||||
*/
|
||||
String RESPONSES = "responses";
|
||||
|
||||
/**
|
||||
* 类型
|
||||
*/
|
||||
String TYPE = "type";
|
||||
|
||||
/**
|
||||
* 参数
|
||||
*/
|
||||
String PARAMETERS = "parameters";
|
||||
|
||||
/**
|
||||
* 是否必须
|
||||
*/
|
||||
String REQUIRED = "required";
|
||||
|
||||
/**
|
||||
* 默认值
|
||||
*/
|
||||
String DEFAULT_VALUE = "defaultValue";
|
||||
|
||||
/**
|
||||
* 路径
|
||||
*/
|
||||
String PATH = "path";
|
||||
|
||||
/**
|
||||
* 方法
|
||||
*/
|
||||
String METHOD = "method";
|
||||
|
||||
/**
|
||||
* 位置
|
||||
*/
|
||||
String LOCATION = "location";
|
||||
|
||||
/**
|
||||
* 认证类型
|
||||
*/
|
||||
String AUTH_TYPE = "authType";
|
||||
|
||||
/**
|
||||
* token参数名称
|
||||
*/
|
||||
String TOKEN_PARAM_NAME = "tokenParamName";
|
||||
|
||||
/**
|
||||
* token参数值
|
||||
*/
|
||||
String TOKEN_PARAM_VALUE = "tokenParamValue";
|
||||
|
||||
/**
|
||||
* token
|
||||
*/
|
||||
String TOKEN = "token";
|
||||
|
||||
/**
|
||||
* Path位置
|
||||
*/
|
||||
String LOCATION_PATH = "Path";
|
||||
|
||||
/**
|
||||
* Header位置
|
||||
*/
|
||||
String LOCATION_HEADER = "Header";
|
||||
|
||||
/**
|
||||
* Query位置
|
||||
*/
|
||||
String LOCATION_QUERY = "Query";
|
||||
|
||||
/**
|
||||
* Body位置
|
||||
*/
|
||||
String LOCATION_BODY = "Body";
|
||||
|
||||
/**
|
||||
* Form-Data位置
|
||||
*/
|
||||
String LOCATION_FORM_DATA = "Form-Data";
|
||||
|
||||
/**
|
||||
* String类型
|
||||
*/
|
||||
String TYPE_STRING = "String";
|
||||
|
||||
/**
|
||||
* string类型
|
||||
*/
|
||||
String TYPE_STRING_LOWER = "string";
|
||||
|
||||
/**
|
||||
* Number类型
|
||||
*/
|
||||
String TYPE_NUMBER = "Number";
|
||||
|
||||
/**
|
||||
* number类型
|
||||
*/
|
||||
String TYPE_NUMBER_LOWER = "number";
|
||||
|
||||
/**
|
||||
* Integer类型
|
||||
*/
|
||||
String TYPE_INTEGER = "Integer";
|
||||
|
||||
/**
|
||||
* integer类型
|
||||
*/
|
||||
String TYPE_INTEGER_LOWER = "integer";
|
||||
|
||||
/**
|
||||
* Boolean类型
|
||||
*/
|
||||
String TYPE_BOOLEAN = "Boolean";
|
||||
|
||||
/**
|
||||
* boolean类型
|
||||
*/
|
||||
String TYPE_BOOLEAN_LOWER = "boolean";
|
||||
|
||||
/**
|
||||
* 工具数量
|
||||
*/
|
||||
String TOOL_COUNT = "tool_count";
|
||||
|
||||
/**
|
||||
* 是否启用
|
||||
*/
|
||||
String ENABLED = "enabled";
|
||||
|
||||
/**
|
||||
* 输入
|
||||
*/
|
||||
String INPUTS = "inputs";
|
||||
|
||||
/**
|
||||
* 输出
|
||||
*/
|
||||
String OUTPUTS = "outputs";
|
||||
|
||||
/**
|
||||
* POST请求
|
||||
*/
|
||||
String POST = "POST";
|
||||
|
||||
/**
|
||||
* token名称
|
||||
*/
|
||||
String X_ACCESS_TOKEN = "X-Access-Token";
|
||||
|
||||
/**
|
||||
* 插件名称
|
||||
*/
|
||||
String PLUGIN_NAME = "流程调用";
|
||||
|
||||
/**
|
||||
* 插件描述
|
||||
*/
|
||||
String PLUGIN_DESC = "调用工作流";
|
||||
|
||||
/**
|
||||
* 插件请求地址
|
||||
*/
|
||||
String PLUGIN_REQUEST_URL = "/airag/flow/plugin/run/";
|
||||
|
||||
/**
|
||||
* 记忆库插件名称
|
||||
*/
|
||||
String PLUGIN_MEMORY_NAME = "记忆库";
|
||||
|
||||
/**
|
||||
* 记忆库插件描述
|
||||
*/
|
||||
String PLUGIN_MEMORY_DESC = "用于记录长期记忆";
|
||||
|
||||
/**
|
||||
* 添加记忆路径
|
||||
*/
|
||||
String PLUGIN_MEMORY_ADD_PATH = "/airag/knowledge/plugin/add";
|
||||
|
||||
/**
|
||||
* 查询记忆路径
|
||||
*/
|
||||
String PLUGIN_MEMORY_QUERY_PATH = "/airag/knowledge/plugin/query";
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package org.jeecg.modules.airag.llm.consts;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* @Description: airag模型常量类
|
||||
* @Author: chenrui
|
||||
* @Date: 2025/2/12 17:35
|
||||
*/
|
||||
public class LLMConsts {
|
||||
|
||||
|
||||
/**
|
||||
* 正则表达式:是否是网页
|
||||
*/
|
||||
public static final Pattern WEB_PATTERN = Pattern.compile("^(http|https)://.*");
|
||||
|
||||
/**
|
||||
* 状态:启用
|
||||
*/
|
||||
public static final String STATUS_ENABLE = "enable";
|
||||
/**
|
||||
* 状态:禁用
|
||||
*/
|
||||
public static final String STATUS_DISABLE = "disable";
|
||||
|
||||
|
||||
/**
|
||||
* 模型类型:向量
|
||||
*/
|
||||
public static final String MODEL_TYPE_EMBED = "EMBED";
|
||||
|
||||
/**
|
||||
* 模型类型:聊天
|
||||
*/
|
||||
public static final String MODEL_TYPE_LLM = "LLM";
|
||||
|
||||
/**
|
||||
* 模型类型: 图像生成
|
||||
*/
|
||||
public static final String MODEL_TYPE_IMAGE = "IMAGE";
|
||||
|
||||
/**
|
||||
* 向量模型:默认维度
|
||||
*/
|
||||
public static final Integer EMBED_MODEL_DEFAULT_DIMENSION = 1536;
|
||||
|
||||
/**
|
||||
* 知识库:文档状态:草稿
|
||||
*/
|
||||
public static final String KNOWLEDGE_DOC_STATUS_DRAFT = "draft";
|
||||
/**
|
||||
* 知识库:文档状态:构建中
|
||||
*/
|
||||
public static final String KNOWLEDGE_DOC_STATUS_BUILDING = "building";
|
||||
/**
|
||||
* 知识库:文档状态:构建完成
|
||||
*/
|
||||
public static final String KNOWLEDGE_DOC_STATUS_COMPLETE = "complete";
|
||||
/**
|
||||
* 知识库:文档状态:构建失败
|
||||
*/
|
||||
public static final String KNOWLEDGE_DOC_STATUS_FAILED = "failed";
|
||||
|
||||
/**
|
||||
* 知识库:文档类型:文本
|
||||
*/
|
||||
public static final String KNOWLEDGE_DOC_TYPE_TEXT = "text";
|
||||
/**
|
||||
* 知识库:文档类型:文件
|
||||
*/
|
||||
public static final String KNOWLEDGE_DOC_TYPE_FILE = "file";
|
||||
/**
|
||||
* 知识库:文档类型:网页
|
||||
*/
|
||||
public static final String KNOWLEDGE_DOC_TYPE_WEB = "web";
|
||||
|
||||
/**
|
||||
* 知识库:文档元数据:文件路径
|
||||
*/
|
||||
public static final String KNOWLEDGE_DOC_METADATA_FILEPATH = "filePath";
|
||||
|
||||
/**
|
||||
* 知识库:文档元数据:资源路径
|
||||
*/
|
||||
public static final String KNOWLEDGE_DOC_METADATA_SOURCES_PATH = "sourcesPath";
|
||||
|
||||
/**
|
||||
* DEEPSEEK推理模型
|
||||
*/
|
||||
public static final String DEEPSEEK_REASONER = "deepseek-reasoner";
|
||||
|
||||
/**
|
||||
* 知识库类型:知识库
|
||||
*/
|
||||
public static final String KNOWLEDGE_TYPE_KNOWLEDGE = "knowledge";
|
||||
|
||||
/**
|
||||
* 知识库类型:记忆库
|
||||
*/
|
||||
public static final String KNOWLEDGE_TYPE_MEMORY = "memory";
|
||||
|
||||
/**
|
||||
* 支持文件的后缀
|
||||
*/
|
||||
public static final Set<String> CHAT_FILE_EXT_WHITELIST = new HashSet<>(Arrays.asList("txt", "pdf", "docx", "doc", "pptx", "ppt", "xlsx", "xls", "md"));
|
||||
|
||||
/**
|
||||
* 文件内容最大长度
|
||||
*/
|
||||
public static final int CHAT_FILE_TEXT_MAX_LENGTH = 20000;
|
||||
|
||||
/**
|
||||
* 上传文件对打数量
|
||||
*/
|
||||
public static final int CHAT_FILE_MAX_COUNT = 3;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package org.jeecg.modules.airag.llm.controller;
|
||||
|
||||
import org.jeecg.common.airag.api.IAiragBaseApi;
|
||||
import org.jeecg.modules.airag.llm.service.impl.AiragBaseApiImpl;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* airag baseAPI Controller
|
||||
*
|
||||
* @author sjlei
|
||||
* @date 2025-12-30
|
||||
*/
|
||||
@RestController("airagBaseApiController")
|
||||
public class AiragBaseApiController implements IAiragBaseApi {
|
||||
|
||||
@Autowired
|
||||
AiragBaseApiImpl airagBaseApi;
|
||||
|
||||
@PostMapping("/airag/api/knowledgeWriteTextDocument")
|
||||
public String knowledgeWriteTextDocument(
|
||||
@RequestParam("knowledgeId") String knowledgeId,
|
||||
@RequestParam("title") String title,
|
||||
@RequestParam("content") String content
|
||||
) {
|
||||
return airagBaseApi.knowledgeWriteTextDocument(knowledgeId, title, content);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,427 @@
|
||||
package org.jeecg.modules.airag.llm.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.system.query.QueryGenerator;
|
||||
import org.jeecg.common.util.AssertUtils;
|
||||
import org.jeecg.common.util.TokenUtils;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.config.mybatis.MybatisPlusSaasConfig;
|
||||
import org.jeecg.modules.airag.common.vo.knowledge.KnowledgeSearchResult;
|
||||
import org.jeecg.modules.airag.llm.consts.LLMConsts;
|
||||
import org.jeecg.modules.airag.llm.entity.AiragKnowledge;
|
||||
import org.jeecg.modules.airag.llm.entity.AiragKnowledgeDoc;
|
||||
import org.jeecg.modules.airag.llm.handler.EmbeddingHandler;
|
||||
import org.jeecg.modules.airag.llm.service.IAiragKnowledgeDocService;
|
||||
import org.jeecg.modules.airag.llm.service.IAiragKnowledgeService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
/**
|
||||
* @Description: AIRag知识库
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-02-18
|
||||
* @Version: V1.0
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/airag/knowledge")
|
||||
@Slf4j
|
||||
public class AiragKnowledgeController {
|
||||
@Autowired
|
||||
private IAiragKnowledgeService airagKnowledgeService;
|
||||
|
||||
@Autowired
|
||||
private IAiragKnowledgeDocService airagKnowledgeDocService;
|
||||
|
||||
@Autowired
|
||||
EmbeddingHandler embeddingHandler;
|
||||
|
||||
/**
|
||||
* 分页列表查询知识库
|
||||
*
|
||||
* @param airagKnowledge
|
||||
* @param pageNo
|
||||
* @param pageSize
|
||||
* @param req
|
||||
* @return
|
||||
*/
|
||||
@GetMapping(value = "/list")
|
||||
public Result<IPage<AiragKnowledge>> queryPageList(AiragKnowledge airagKnowledge,
|
||||
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
|
||||
@RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize,
|
||||
HttpServletRequest req) {
|
||||
QueryWrapper<AiragKnowledge> queryWrapper = QueryGenerator.initQueryWrapper(airagKnowledge, req.getParameterMap());
|
||||
Page<AiragKnowledge> page = new Page<AiragKnowledge>(pageNo, pageSize);
|
||||
IPage<AiragKnowledge> pageList = airagKnowledgeService.page(page, queryWrapper);
|
||||
return Result.OK(pageList);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加知识库
|
||||
*
|
||||
* @param airagKnowledge 知识库
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/2/18 17:09
|
||||
*/
|
||||
@PostMapping(value = "/add")
|
||||
@RequiresPermissions("airag:knowledge:add")
|
||||
public Result<String> add(@RequestBody AiragKnowledge airagKnowledge) {
|
||||
airagKnowledge.setStatus(LLMConsts.STATUS_ENABLE);
|
||||
if(oConvertUtils.isEmpty(airagKnowledge.getType())) {
|
||||
airagKnowledge.setType(LLMConsts.KNOWLEDGE_TYPE_KNOWLEDGE);
|
||||
}
|
||||
airagKnowledgeService.save(airagKnowledge);
|
||||
return Result.OK("添加成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑知识库
|
||||
*
|
||||
* @param airagKnowledge 知识库
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/2/18 17:09
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@RequestMapping(value = "/edit", method = {RequestMethod.PUT, RequestMethod.POST})
|
||||
@RequiresPermissions("airag:knowledge:edit")
|
||||
public Result<String> edit(@RequestBody AiragKnowledge airagKnowledge) {
|
||||
AiragKnowledge airagKnowledgeEntity = airagKnowledgeService.getById(airagKnowledge.getId());
|
||||
if (airagKnowledgeEntity == null) {
|
||||
return Result.error("未找到对应数据");
|
||||
}
|
||||
String oldEmbedId = airagKnowledgeEntity.getEmbedId();
|
||||
if(oConvertUtils.isEmpty(airagKnowledgeEntity.getType())) {
|
||||
airagKnowledge.setType(LLMConsts.KNOWLEDGE_TYPE_KNOWLEDGE);
|
||||
}
|
||||
airagKnowledgeService.updateById(airagKnowledge);
|
||||
if (!oldEmbedId.equalsIgnoreCase(airagKnowledge.getEmbedId())) {
|
||||
// 更新了模型,重建文档
|
||||
airagKnowledgeDocService.rebuildDocumentByKnowId(airagKnowledge.getId());
|
||||
}
|
||||
return Result.OK("编辑成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 重建知识库
|
||||
*
|
||||
* @param knowIds
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/3/12 17:05
|
||||
*/
|
||||
@PutMapping(value = "/rebuild")
|
||||
@RequiresPermissions("airag:knowledge:rebuild")
|
||||
public Result<?> rebuild(@RequestParam("knowIds") String knowIds) {
|
||||
String[] knowIdArr = knowIds.split(",");
|
||||
for (String knowId : knowIdArr) {
|
||||
airagKnowledgeDocService.rebuildDocumentByKnowId(knowId);
|
||||
}
|
||||
return Result.OK("");
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过id删除知识库
|
||||
*
|
||||
* @param id
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/2/18 17:09
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@DeleteMapping(value = "/delete")
|
||||
@RequiresPermissions("airag:knowledge:delete")
|
||||
public Result<String> delete(HttpServletRequest request, @RequestParam(name = "id", required = true) String id) {
|
||||
//update-begin---author:chenrui ---date:20250606 for:[issues/8337]关于ai工作列表的数据权限问题 #8337------------
|
||||
//如果是saas隔离的情况下,判断当前租户id是否是当前租户下的
|
||||
if (MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL) {
|
||||
AiragKnowledge know = airagKnowledgeService.getById(id);
|
||||
//获取当前租户
|
||||
String currentTenantId = TokenUtils.getTenantIdByRequest(request);
|
||||
if (null == know || !know.getTenantId().equals(currentTenantId)) {
|
||||
return Result.error("删除AI知识库失败,不能删除其他租户的AI知识库!");
|
||||
}
|
||||
}
|
||||
//update-end---author:chenrui ---date:20250606 for:[issues/8337]关于ai工作列表的数据权限问题 #8337------------
|
||||
airagKnowledgeDocService.removeByKnowIds(Collections.singletonList(id));
|
||||
airagKnowledgeService.removeById(id);
|
||||
return Result.OK("删除成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过id查询知识库
|
||||
*
|
||||
* @param id
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/2/18 17:09
|
||||
*/
|
||||
@GetMapping(value = "/queryById")
|
||||
public Result<AiragKnowledge> queryById(@RequestParam(name = "id", required = true) String id) {
|
||||
AiragKnowledge airagKnowledge = airagKnowledgeService.getById(id);
|
||||
if (airagKnowledge == null) {
|
||||
return Result.error("未找到对应数据");
|
||||
}
|
||||
return Result.OK(airagKnowledge);
|
||||
}
|
||||
|
||||
/**
|
||||
* 文档分页查询
|
||||
*
|
||||
* @param airagKnowledgeDoc
|
||||
* @param pageNo
|
||||
* @param pageSize
|
||||
* @param req
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/2/18 18:37
|
||||
*/
|
||||
@GetMapping(value = "/doc/list")
|
||||
public Result<IPage<AiragKnowledgeDoc>> queryDocumentPageList(AiragKnowledgeDoc airagKnowledgeDoc,
|
||||
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
|
||||
@RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize,
|
||||
HttpServletRequest req) {
|
||||
AssertUtils.assertNotEmpty("请先选择知识库", airagKnowledgeDoc.getKnowledgeId());
|
||||
QueryWrapper<AiragKnowledgeDoc> queryWrapper = QueryGenerator.initQueryWrapper(airagKnowledgeDoc, req.getParameterMap());
|
||||
Page<AiragKnowledgeDoc> page = new Page<>(pageNo, pageSize);
|
||||
IPage<AiragKnowledgeDoc> pageList = airagKnowledgeDocService.page(page, queryWrapper);
|
||||
return Result.OK(pageList);
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增或编辑文档
|
||||
*
|
||||
* @param airagKnowledgeDoc 知识库文档
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/2/18 15:47
|
||||
*/
|
||||
@PostMapping(value = "/doc/edit")
|
||||
@RequiresPermissions("airag:knowledge:doc:edit")
|
||||
public Result<?> addDocument(@RequestBody AiragKnowledgeDoc airagKnowledgeDoc) {
|
||||
return airagKnowledgeDocService.editDocument(airagKnowledgeDoc);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 从压缩包导入文档
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/3/20 11:29
|
||||
*/
|
||||
@PostMapping(value = "/doc/import/zip")
|
||||
@RequiresPermissions("airag:knowledge:doc:zip")
|
||||
public Result<?> importDocumentFromZip(@RequestParam(name = "knowId", required = true) String knowId,
|
||||
@RequestParam(name = "file", required = true) MultipartFile file) {
|
||||
return airagKnowledgeDocService.importDocumentFromZip(knowId,file);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过文档库查询导入任务列表
|
||||
* @param knowId
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/3/20 11:37
|
||||
*/
|
||||
@GetMapping(value = "/doc/import/task/list")
|
||||
public Result<?> importDocumentTaskList(@RequestParam(name = "knowId", required = true) String knowId) {
|
||||
return Result.OK(Collections.emptyList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新向量化文档
|
||||
*
|
||||
* @param docIds 文档id集合
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/2/18 15:47
|
||||
*/
|
||||
@PutMapping(value = "/doc/rebuild")
|
||||
@RequiresPermissions("airag:knowledge:doc:rebuild")
|
||||
public Result<?> rebuildDocument(@RequestParam("docIds") String docIds) {
|
||||
return airagKnowledgeDocService.rebuildDocument(docIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除文档
|
||||
*
|
||||
* @param ids
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/2/18 17:09
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@DeleteMapping(value = "/doc/deleteBatch")
|
||||
@RequiresPermissions("airag:knowledge:doc:deleteBatch")
|
||||
public Result<String> deleteDocumentBatch(HttpServletRequest request, @RequestParam(name = "ids", required = true) String ids) {
|
||||
List<String> idsList = Arrays.asList(ids.split(","));
|
||||
//update-begin---author:chenrui ---date:20250606 for:[issues/8337]关于ai工作列表的数据权限问题 #8337------------
|
||||
//如果是saas隔离的情况下,判断当前租户id是否是当前租户下的
|
||||
if (MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL) {
|
||||
List<AiragKnowledgeDoc> docList = airagKnowledgeDocService.listByIds(idsList);
|
||||
//获取当前租户
|
||||
String currentTenantId = TokenUtils.getTenantIdByRequest(request);
|
||||
docList.forEach(airagKnowledgeDoc -> {
|
||||
if (null == airagKnowledgeDoc || !airagKnowledgeDoc.getTenantId().equals(currentTenantId)) {
|
||||
throw new IllegalArgumentException("删除AI知识库文档失败,不能删除其他租户的AI知识库文档!");
|
||||
}
|
||||
});
|
||||
}
|
||||
//update-end---author:chenrui ---date:20250606 for:[issues/8337]关于ai工作列表的数据权限问题 #8337------------
|
||||
airagKnowledgeDocService.removeDocByIds(idsList);
|
||||
return Result.OK("批量删除成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空知识库文档
|
||||
*
|
||||
* @param
|
||||
* @return
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@DeleteMapping(value = "/doc/deleteAll")
|
||||
@RequiresPermissions("airag:knowledge:doc:deleteAll")
|
||||
public Result<?> deleteDocumentAll(HttpServletRequest request, @RequestParam(name = "knowId") String knowId) {
|
||||
//update-begin---author:chenrui ---date:20250606 for:[issues/8337]关于ai工作列表的数据权限问题 #8337------------
|
||||
//如果是saas隔离的情况下,判断当前租户id是否是当前租户下的
|
||||
if (MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL) {
|
||||
AiragKnowledge know = airagKnowledgeService.getById(knowId);
|
||||
//获取当前租户
|
||||
String currentTenantId = TokenUtils.getTenantIdByRequest(request);
|
||||
if (null == know || !know.getTenantId().equals(currentTenantId)) {
|
||||
return Result.error("删除AI知识库失败,不能删除其他租户的AI知识库!");
|
||||
}
|
||||
}
|
||||
//update-end---author:chenrui ---date:20250606 for:[issues/8337]关于ai工作列表的数据权限问题 #8337------------
|
||||
return airagKnowledgeDocService.deleteAllByKnowId(knowId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 命中测试
|
||||
*
|
||||
* @param knowId 知识库id
|
||||
* @param queryText 查询内容
|
||||
* @param topNumber 最多返回条数
|
||||
* @param similarity 最小分数
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/2/18 17:09
|
||||
*/
|
||||
@GetMapping(value = "/embedding/hitTest/{knowId}")
|
||||
public Result<?> hitTest(@PathVariable("knowId") String knowId,
|
||||
@RequestParam(name = "queryText") String queryText,
|
||||
@RequestParam(name = "topNumber") Integer topNumber,
|
||||
@RequestParam(name = "similarity") Double similarity) {
|
||||
List<Map<String, Object>> searchResp = embeddingHandler.searchEmbedding(knowId, queryText, topNumber, similarity);
|
||||
return Result.ok(searchResp);
|
||||
}
|
||||
|
||||
/**
|
||||
* 向量查询
|
||||
*
|
||||
* @param knowIds 知识库ids
|
||||
* @param queryText 查询内容
|
||||
* @param topNumber 最多返回条数
|
||||
* @param similarity 最小分数
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/2/18 17:09
|
||||
*/
|
||||
@GetMapping(value = "/embedding/search")
|
||||
public Result<?> embeddingSearch(@RequestParam("knowIds") List<String> knowIds,
|
||||
@RequestParam(name = "queryText") String queryText,
|
||||
@RequestParam(name = "topNumber", required = false) Integer topNumber,
|
||||
@RequestParam(name = "similarity", required = false) Double similarity) {
|
||||
KnowledgeSearchResult searchResp = embeddingHandler.embeddingSearch(knowIds, queryText, topNumber, similarity);
|
||||
return Result.ok(searchResp);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过ids批量查询知识库
|
||||
*
|
||||
* @param ids
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/2/27 16:44
|
||||
*/
|
||||
@GetMapping(value = "/query/batch/byId")
|
||||
public Result<?> queryBatchByIds(@RequestParam(name = "ids", required = true) String ids) {
|
||||
List<String> idList = Arrays.asList(ids.split(","));
|
||||
List<AiragKnowledge> airagKnowledges = airagKnowledgeService.listByIds(idList);
|
||||
return Result.OK(airagKnowledges);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加记忆
|
||||
*
|
||||
* @param airagKnowledgeDoc
|
||||
* @return
|
||||
*/
|
||||
@Operation(summary = "添加记忆")
|
||||
@PostMapping(value = "/plugin/add")
|
||||
public Result<?> add(@RequestBody AiragKnowledgeDoc airagKnowledgeDoc, HttpServletRequest request) {
|
||||
if (oConvertUtils.isEmpty(airagKnowledgeDoc.getKnowledgeId())) {
|
||||
return Result.error("知识库ID不能为空");
|
||||
}
|
||||
if (oConvertUtils.isEmpty(airagKnowledgeDoc.getContent())) {
|
||||
return Result.error("内容不能为空");
|
||||
}
|
||||
|
||||
// 设置默认值
|
||||
if (oConvertUtils.isEmpty(airagKnowledgeDoc.getTitle())) {
|
||||
// 取内容前20个字作为标题
|
||||
String content = airagKnowledgeDoc.getContent();
|
||||
String title = content.length() > 20 ? content.substring(0, 20) : content;
|
||||
airagKnowledgeDoc.setTitle(title);
|
||||
}
|
||||
|
||||
airagKnowledgeDoc.setType(LLMConsts.KNOWLEDGE_DOC_TYPE_TEXT);
|
||||
// 保存并构建向量
|
||||
return airagKnowledgeDocService.editDocument(airagKnowledgeDoc);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询记忆
|
||||
*
|
||||
* @param params
|
||||
* @return
|
||||
*/
|
||||
@Operation(summary = "查询记忆")
|
||||
@PostMapping(value = "/plugin/query")
|
||||
public Result<?> pluginQuery(@RequestBody Map<String, Object> params, HttpServletRequest request) {
|
||||
String knowId = (String) params.get("knowledgeId");
|
||||
String queryText = (String) params.get("queryText");
|
||||
if (oConvertUtils.isEmpty(knowId)) {
|
||||
return Result.error("知识库ID不能为空");
|
||||
}
|
||||
if (oConvertUtils.isEmpty(queryText)) {
|
||||
return Result.error("查询内容不能为空");
|
||||
}
|
||||
LambdaQueryWrapper<AiragKnowledgeDoc> queryWrapper = new LambdaQueryWrapper<AiragKnowledgeDoc>();
|
||||
queryWrapper.eq(AiragKnowledgeDoc::getKnowledgeId, knowId);
|
||||
long count = airagKnowledgeDocService.count(queryWrapper);
|
||||
if(count == 0){
|
||||
return Result.ok("");
|
||||
}
|
||||
// 默认查询前5条
|
||||
KnowledgeSearchResult searchResp = embeddingHandler.embeddingSearch(Collections.singletonList(knowId), queryText, (int) count, null);
|
||||
return Result.ok(searchResp);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
package org.jeecg.modules.airag.llm.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.system.base.controller.JeecgController;
|
||||
import org.jeecg.common.system.query.QueryGenerator;
|
||||
import org.jeecg.modules.airag.llm.entity.AiragMcp;
|
||||
import org.jeecg.modules.airag.llm.service.IAiragMcpService;
|
||||
import org.jeecg.modules.airag.llm.dto.SaveToolsDTO;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
||||
/**
|
||||
* @Description: MCP
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-10-20
|
||||
* @Version: V1.0
|
||||
*/
|
||||
@Tag(name = "MCP")
|
||||
@RestController("airagMcpController")
|
||||
@RequestMapping("/airag/airagMcp")
|
||||
@Slf4j
|
||||
public class AiragMcpController extends JeecgController<AiragMcp, IAiragMcpService> {
|
||||
@Autowired
|
||||
private IAiragMcpService airagMcpService;
|
||||
|
||||
/**
|
||||
* 分页列表查询
|
||||
*
|
||||
* @param airagMcp
|
||||
* @param pageNo
|
||||
* @param pageSize
|
||||
* @param req
|
||||
* @return
|
||||
*/
|
||||
@Operation(summary = "MCP-分页列表查询")
|
||||
@GetMapping(value = "/list")
|
||||
public Result<IPage<AiragMcp>> queryPageList(AiragMcp airagMcp,
|
||||
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
|
||||
@RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize,
|
||||
HttpServletRequest req) {
|
||||
|
||||
QueryWrapper<AiragMcp> queryWrapper = QueryGenerator.initQueryWrapper(airagMcp, req.getParameterMap());
|
||||
Page<AiragMcp> page = new Page<AiragMcp>(pageNo, pageSize);
|
||||
IPage<AiragMcp> pageList = airagMcpService.page(page, queryWrapper);
|
||||
return Result.OK(pageList);
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存
|
||||
*
|
||||
* @param airagMcp
|
||||
* @return
|
||||
*/
|
||||
@Operation(summary = "MCP-保存")
|
||||
@PostMapping(value = "/save")
|
||||
public Result<String> save(@RequestBody AiragMcp airagMcp) {
|
||||
return airagMcpService.edit(airagMcp);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 保存并同步
|
||||
*
|
||||
* @param airagMcp
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/10/21 10:54
|
||||
*/
|
||||
@Operation(summary = "MCP-保存并同步")
|
||||
@PostMapping(value = "/saveAndSync")
|
||||
public Result<?> saveAndSync(@RequestBody AiragMcp airagMcp) {
|
||||
Result<String> saveResult = airagMcpService.edit(airagMcp);
|
||||
if (!saveResult.isSuccess()) {
|
||||
return saveResult;
|
||||
}
|
||||
String id = airagMcp.getId();
|
||||
if (id == null || id.trim().isEmpty()) {
|
||||
return Result.error("保存失败");
|
||||
}
|
||||
return airagMcpService.sync(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步MCP信息
|
||||
*
|
||||
* @param id
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/10/20 20:09
|
||||
*/
|
||||
@Operation(summary = "MCP-同步MCP信息")
|
||||
@PostMapping(value = "/sync/{id}")
|
||||
public Result<?> sync(@PathVariable(name = "id", required = true) String id) {
|
||||
return airagMcpService.sync(id);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 启用/禁用MCP信息
|
||||
*
|
||||
* @param action 启用:enable,禁用:disable
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/10/20 20:13
|
||||
*/
|
||||
@Operation(summary = "MCP-启用/禁用MCP信息")
|
||||
@PostMapping(value = "/status/{id}/{action}")
|
||||
public Result<?> toggleStatus(@PathVariable(name = "id",required = true) String id,
|
||||
@PathVariable(name = "action", required = true) String action) {
|
||||
return airagMcpService.toggleStatus(id,action);
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存插件工具
|
||||
* for [QQYUN-12453]【AI】支持插件
|
||||
* @param dto 包含插件ID和工具列表JSON字符串的DTO
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/10/30
|
||||
*/
|
||||
@Operation(summary = "MCP-保存插件工具")
|
||||
@PostMapping(value = "/saveTools")
|
||||
public Result<String> saveTools(@RequestBody SaveToolsDTO dto) {
|
||||
return airagMcpService.saveTools(dto.getId(), dto.getTools());
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过id删除
|
||||
*
|
||||
* @param id
|
||||
* @return
|
||||
*/
|
||||
@Operation(summary = "MCP-通过id删除")
|
||||
@DeleteMapping(value = "/delete")
|
||||
public Result<String> delete(@RequestParam(name = "id", required = true) String id) {
|
||||
airagMcpService.removeById(id);
|
||||
return Result.OK("删除成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过id查询
|
||||
*
|
||||
* @param id
|
||||
* @return
|
||||
*/
|
||||
@Operation(summary = "MCP-通过id查询")
|
||||
@GetMapping(value = "/queryById")
|
||||
public Result<AiragMcp> queryById(@RequestParam(name = "id", required = true) String id) {
|
||||
AiragMcp airagMcp = airagMcpService.getById(id);
|
||||
if (airagMcp == null) {
|
||||
return Result.error("未找到对应数据");
|
||||
}
|
||||
return Result.OK(airagMcp);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出excel
|
||||
*
|
||||
* @param request
|
||||
* @param airagMcp
|
||||
*/
|
||||
// @RequiresPermissions("llm:airag_mcp:exportXls")
|
||||
@RequestMapping(value = "/exportXls")
|
||||
public ModelAndView exportXls(HttpServletRequest request, AiragMcp airagMcp) {
|
||||
return super.exportXls(request, airagMcp, AiragMcp.class, "MCP");
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过excel导入数据
|
||||
*
|
||||
* @param request
|
||||
* @param response
|
||||
* @return
|
||||
*/
|
||||
// @RequiresPermissions("llm:airag_mcp:importExcel")
|
||||
@RequestMapping(value = "/importExcel", method = RequestMethod.POST)
|
||||
public Result<?> importExcel(HttpServletRequest request, HttpServletResponse response) {
|
||||
return super.importExcel(request, response, AiragMcp.class);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
package org.jeecg.modules.airag.llm.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import dev.langchain4j.data.message.UserMessage;
|
||||
import dev.langchain4j.model.embedding.EmbeddingModel;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
import org.jeecg.ai.factory.AiModelFactory;
|
||||
import org.jeecg.ai.factory.AiModelOptions;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.system.base.controller.JeecgController;
|
||||
import org.jeecg.common.system.query.QueryGenerator;
|
||||
import org.jeecg.common.util.AssertUtils;
|
||||
import org.jeecg.common.util.TokenUtils;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.config.mybatis.MybatisPlusSaasConfig;
|
||||
import org.jeecg.modules.airag.common.handler.AIChatParams;
|
||||
import org.jeecg.modules.airag.llm.consts.LLMConsts;
|
||||
import org.jeecg.modules.airag.llm.entity.AiragModel;
|
||||
import org.jeecg.modules.airag.llm.handler.AIChatHandler;
|
||||
import org.jeecg.modules.airag.llm.handler.EmbeddingHandler;
|
||||
import org.jeecg.modules.airag.llm.service.IAiragModelService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
|
||||
/**
|
||||
* @Description: AiRag模型配置
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-02-14
|
||||
* @Version: V1.0
|
||||
*/
|
||||
@Tag(name = "AiRag模型配置")
|
||||
@RestController
|
||||
@RequestMapping("/airag/airagModel")
|
||||
@Slf4j
|
||||
public class AiragModelController extends JeecgController<AiragModel, IAiragModelService> {
|
||||
@Autowired
|
||||
private IAiragModelService airagModelService;
|
||||
|
||||
@Autowired
|
||||
AIChatHandler aiChatHandler;
|
||||
|
||||
/**
|
||||
* 分页列表查询
|
||||
*
|
||||
* @param airagModel
|
||||
* @param pageNo
|
||||
* @param pageSize
|
||||
* @param req
|
||||
* @return
|
||||
*/
|
||||
@GetMapping(value = "/list")
|
||||
public Result<IPage<AiragModel>> queryPageList(AiragModel airagModel, @RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo, @RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize, HttpServletRequest req) {
|
||||
QueryWrapper<AiragModel> queryWrapper = QueryGenerator.initQueryWrapper(airagModel, req.getParameterMap());
|
||||
Page<AiragModel> page = new Page<AiragModel>(pageNo, pageSize);
|
||||
IPage<AiragModel> pageList = airagModelService.page(page, queryWrapper);
|
||||
return Result.OK(pageList);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加
|
||||
*
|
||||
* @param airagModel
|
||||
* @return
|
||||
*/
|
||||
@PostMapping(value = "/add")
|
||||
@RequiresPermissions("airag:model:add")
|
||||
public Result<String> add(@RequestBody AiragModel airagModel) {
|
||||
// 验证 模型名称/模型类型/基础模型
|
||||
AssertUtils.assertNotEmpty("模型名称不能为空", airagModel.getName());
|
||||
AssertUtils.assertNotEmpty("模型类型不能为空", airagModel.getModelType());
|
||||
AssertUtils.assertNotEmpty("基础模型不能为空", airagModel.getModelName());
|
||||
// 默认未激活
|
||||
if(oConvertUtils.isObjectEmpty(airagModel.getActivateFlag())){
|
||||
airagModel.setActivateFlag(0);
|
||||
} else {
|
||||
airagModel.setActivateFlag(1);
|
||||
}
|
||||
airagModelService.save(airagModel);
|
||||
return Result.OK("添加成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑
|
||||
*
|
||||
* @param airagModel
|
||||
* @return
|
||||
*/
|
||||
@RequestMapping(value = "/edit", method = {RequestMethod.PUT, RequestMethod.POST})
|
||||
@RequiresPermissions("airag:model:edit")
|
||||
public Result<String> edit(@RequestBody AiragModel airagModel) {
|
||||
airagModelService.updateById(airagModel);
|
||||
return Result.OK("编辑成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过id删除
|
||||
*
|
||||
* @param id
|
||||
* @return
|
||||
*/
|
||||
@DeleteMapping(value = "/delete")
|
||||
@RequiresPermissions("airag:model:delete")
|
||||
public Result<String> delete(HttpServletRequest request, @RequestParam(name = "id", required = true) String id) {
|
||||
//update-begin---author:chenrui ---date:20250606 for:[issues/8337]关于ai工作列表的数据权限问题 #8337------------
|
||||
//如果是saas隔离的情况下,判断当前租户id是否是当前租户下的
|
||||
if (MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL) {
|
||||
AiragModel model = airagModelService.getById(id);
|
||||
//获取当前租户
|
||||
String currentTenantId = TokenUtils.getTenantIdByRequest(request);
|
||||
if (null == model || !model.getTenantId().equals(currentTenantId)) {
|
||||
return Result.error("删除AI模型失败,不能删除其他租户的AI模型!");
|
||||
}
|
||||
}
|
||||
//update-end---author:chenrui ---date:20250606 for:[issues/8337]关于ai工作列表的数据权限问题 #8337------------
|
||||
airagModelService.removeById(id);
|
||||
return Result.OK("删除成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过id查询
|
||||
*
|
||||
* @param id
|
||||
* @return
|
||||
*/
|
||||
@GetMapping(value = "/queryById")
|
||||
public Result<AiragModel> queryById(@RequestParam(name = "id", required = true) String id) {
|
||||
AiragModel airagModel = airagModelService.getById(id);
|
||||
if (airagModel == null) {
|
||||
return Result.error("未找到对应数据");
|
||||
}
|
||||
return Result.OK(airagModel);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出excel
|
||||
*
|
||||
* @param request
|
||||
* @param airagModel
|
||||
*/
|
||||
@RequestMapping(value = "/exportXls")
|
||||
public ModelAndView exportXls(HttpServletRequest request, AiragModel airagModel) {
|
||||
return super.exportXls(request, airagModel, AiragModel.class, "AiRag模型配置");
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过excel导入数据
|
||||
*
|
||||
* @param request
|
||||
* @param response
|
||||
* @return
|
||||
*/
|
||||
@RequestMapping(value = "/importExcel", method = RequestMethod.POST)
|
||||
public Result<?> importExcel(HttpServletRequest request, HttpServletResponse response) {
|
||||
return super.importExcel(request, response, AiragModel.class);
|
||||
}
|
||||
|
||||
@PostMapping(value = "/test")
|
||||
public Result<?> test(@RequestBody AiragModel airagModel) {
|
||||
// 验证 模型名称/模型类型/基础模型
|
||||
AssertUtils.assertNotEmpty("模型名称不能为空", airagModel.getName());
|
||||
AssertUtils.assertNotEmpty("模型类型不能为空", airagModel.getModelType());
|
||||
AssertUtils.assertNotEmpty("基础模型不能为空", airagModel.getModelName());
|
||||
try {
|
||||
if(LLMConsts.MODEL_TYPE_LLM.equals(airagModel.getModelType())){
|
||||
aiChatHandler.completions(airagModel, Collections.singletonList(UserMessage.from("To test whether it can be successfully called, simply return success")), null);
|
||||
}else if(LLMConsts.MODEL_TYPE_EMBED.equals(airagModel.getModelType())){
|
||||
AiModelOptions aiModelOptions = EmbeddingHandler.buildModelOptions(airagModel);
|
||||
EmbeddingModel embeddingModel = AiModelFactory.createEmbeddingModel(aiModelOptions);
|
||||
embeddingModel.embed("test text");
|
||||
//update-begin---author:wangshuai---date:2026-01-07---for:【QQYUN-12145】【AI】AI 绘画创作---=
|
||||
}else if(LLMConsts.MODEL_TYPE_IMAGE.equals(airagModel.getModelType())){
|
||||
AIChatParams aiChatParams = new AIChatParams();
|
||||
aiChatHandler.imageGenerate(airagModel, "To test whether it can be successfully called, simply return success", aiChatParams);
|
||||
}
|
||||
//update-end---author:wangshuai---date:2026-01-07---for:【QQYUN-12145】【AI】AI 绘画创作---
|
||||
}catch (Exception e){
|
||||
log.error("测试模型连接失败", e);
|
||||
return Result.error(e.getMessage());
|
||||
}
|
||||
// 测试成功激活数据
|
||||
airagModel.setActivateFlag(1);
|
||||
airagModelService.updateById(airagModel);
|
||||
return Result.OK("");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
//
|
||||
// Source code recreated from a .class file by IntelliJ IDEA
|
||||
// (powered by FernFlower decompiler)
|
||||
//
|
||||
|
||||
package org.jeecg.modules.airag.llm.document;
|
||||
|
||||
import dev.langchain4j.data.document.BlankDocumentException;
|
||||
import dev.langchain4j.data.document.Document;
|
||||
import dev.langchain4j.data.document.parser.apache.poi.ApachePoiDocumentParser;
|
||||
import dev.langchain4j.internal.Utils;
|
||||
import org.apache.commons.io.FilenameUtils;
|
||||
import org.apache.poi.hslf.usermodel.HSLFTextParagraph;
|
||||
import org.apache.poi.hwpf.HWPFDocument;
|
||||
import org.apache.poi.hwpf.extractor.WordExtractor;
|
||||
import org.apache.poi.ss.usermodel.*;
|
||||
import org.apache.poi.xslf.usermodel.XMLSlideShow;
|
||||
import org.apache.poi.xslf.usermodel.XSLFSlide;
|
||||
import org.apache.poi.xslf.usermodel.XSLFTextShape;
|
||||
import org.apache.poi.xwpf.usermodel.XWPFDocument;
|
||||
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
|
||||
import org.apache.tika.Tika;
|
||||
import org.apache.tika.exception.ZeroByteFileException;
|
||||
import org.apache.tika.metadata.Metadata;
|
||||
import org.apache.tika.parser.AutoDetectParser;
|
||||
import org.apache.tika.parser.ParseContext;
|
||||
import org.apache.tika.parser.Parser;
|
||||
import org.apache.tika.sax.BodyContentHandler;
|
||||
import org.jeecg.common.util.AssertUtils;
|
||||
import org.xml.sax.ContentHandler;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* tika文档解析器,重写langchain4j的TikaDocumentParser <br/>
|
||||
* jeecgboot目前不支持poi5.x,所以langchain4j同的方法不能用,自己实现
|
||||
* @author chenrui
|
||||
* @date 2025/3/5 16:19
|
||||
*/
|
||||
public class TikaDocumentParser {
|
||||
private static final Tika tika = new Tika();
|
||||
private static final int NO_WRITE_LIMIT = -1;
|
||||
public static final Supplier<Parser> DEFAULT_PARSER_SUPPLIER = AutoDetectParser::new;
|
||||
public static final Supplier<Metadata> DEFAULT_METADATA_SUPPLIER = Metadata::new;
|
||||
public static final Supplier<ParseContext> DEFAULT_PARSE_CONTEXT_SUPPLIER = ParseContext::new;
|
||||
public static final Supplier<ContentHandler> DEFAULT_CONTENT_HANDLER_SUPPLIER = () -> new BodyContentHandler(-1);
|
||||
private final Supplier<Parser> parserSupplier;
|
||||
private final Supplier<ContentHandler> contentHandlerSupplier;
|
||||
private final Supplier<Metadata> metadataSupplier;
|
||||
private final Supplier<ParseContext> parseContextSupplier;
|
||||
//文件前缀
|
||||
private static final Set<String> FILE_SUFFIX = new HashSet<>(Arrays.asList("docx", "doc", "pptx", "ppt", "xlsx", "xls"));
|
||||
|
||||
public TikaDocumentParser() {
|
||||
this((Supplier) ((Supplier) null), (Supplier) null, (Supplier) null, (Supplier) null);
|
||||
}
|
||||
|
||||
|
||||
public TikaDocumentParser(Supplier<Parser> parserSupplier, Supplier<ContentHandler> contentHandlerSupplier, Supplier<Metadata> metadataSupplier, Supplier<ParseContext> parseContextSupplier) {
|
||||
this.parserSupplier = (Supplier) Utils.getOrDefault(parserSupplier, () -> DEFAULT_PARSER_SUPPLIER);
|
||||
this.contentHandlerSupplier = (Supplier) Utils.getOrDefault(contentHandlerSupplier, () -> DEFAULT_CONTENT_HANDLER_SUPPLIER);
|
||||
this.metadataSupplier = (Supplier) Utils.getOrDefault(metadataSupplier, () -> DEFAULT_METADATA_SUPPLIER);
|
||||
this.parseContextSupplier = (Supplier) Utils.getOrDefault(parseContextSupplier, () -> DEFAULT_PARSE_CONTEXT_SUPPLIER);
|
||||
}
|
||||
|
||||
public Document parse(File file) {
|
||||
AssertUtils.assertNotEmpty("请选择文件", file);
|
||||
try {
|
||||
// 用于解析(使用FileInputStream避免file.toPath()在Linux非UTF-8环境下中文文件名报错)
|
||||
InputStream isForParsing = new FileInputStream(file);
|
||||
// 使用 Tika 自动检测 MIME 类型
|
||||
String fileName = file.getName().toLowerCase();
|
||||
//后缀
|
||||
String ext = FilenameUtils.getExtension(fileName);
|
||||
if (fileName.endsWith(".txt")
|
||||
|| fileName.endsWith(".md")
|
||||
|| fileName.endsWith(".pdf")) {
|
||||
return extractByTika(isForParsing);
|
||||
//update-begin---author:wangshuai---date:2026-01-09---for:【QQYUN-14261】【AI】AI助手,支持多模态能力- 文档---
|
||||
} else if (FILE_SUFFIX.contains(ext.toLowerCase())) {
|
||||
return parseDocExcelPdfUsingApachePoi(file);
|
||||
//update-end---author:wangshuai---date:2026-01-09---for:【QQYUN-14261】【AI】AI助手,支持多模态能力- 文档---
|
||||
} else {
|
||||
throw new IllegalArgumentException("不支持的文件格式: " + FilenameUtils.getExtension(fileName));
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* langchain4j 内部解析器
|
||||
* @param file
|
||||
* @return
|
||||
*/
|
||||
public Document parseDocExcelPdfUsingApachePoi(File file) {
|
||||
AssertUtils.assertNotEmpty("请选择文件", file);
|
||||
try (InputStream inputStream = new FileInputStream(file)) {
|
||||
ApachePoiDocumentParser parser = new ApachePoiDocumentParser();
|
||||
Document document = parser.parse(inputStream);
|
||||
if (document == null || Utils.isNullOrBlank(document.text())) {
|
||||
return null;
|
||||
}
|
||||
return document;
|
||||
} catch (BlankDocumentException e) {
|
||||
return null;
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static Document tryExtractDocOrDocx(InputStream inputStream) throws IOException {
|
||||
try {
|
||||
// 先尝试 DOCX(基于 OPC XML 格式)
|
||||
return extractTextFromDocx(inputStream);
|
||||
} catch (Exception e1) {
|
||||
try {
|
||||
// 如果 DOCX 解析失败,则尝试 DOC(基于二进制格式)
|
||||
return extractTextFromDoc(inputStream);
|
||||
} catch (Exception e2) {
|
||||
throw new IOException("无法解析 DOC 或 DOCX 文件", e2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用tika提取文件内容 <br/>
|
||||
* pdf/text/md等文件使用tika提取
|
||||
*
|
||||
* @param inputStream
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/3/5 14:41
|
||||
*/
|
||||
private Document extractByTika(InputStream inputStream) {
|
||||
try {
|
||||
Parser parser = (Parser) this.parserSupplier.get();
|
||||
ContentHandler contentHandler = (ContentHandler) this.contentHandlerSupplier.get();
|
||||
Metadata metadata = (Metadata) this.metadataSupplier.get();
|
||||
ParseContext parseContext = (ParseContext) this.parseContextSupplier.get();
|
||||
parser.parse(inputStream, contentHandler, metadata, parseContext);
|
||||
String text = contentHandler.toString();
|
||||
if (Utils.isNullOrBlank(text)) {
|
||||
throw new BlankDocumentException();
|
||||
} else {
|
||||
return Document.from(text);
|
||||
}
|
||||
} catch (BlankDocumentException e) {
|
||||
throw e;
|
||||
} catch (ZeroByteFileException var8) {
|
||||
throw new BlankDocumentException();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取docx文件内容
|
||||
*
|
||||
* @param inputStream
|
||||
* @return
|
||||
* @throws IOException
|
||||
* @author chenrui
|
||||
* @date 2025/3/5 14:42
|
||||
* @deprecated 因为jeecg主项目目前不支持poi5.x, 自己实现提取功能.
|
||||
*/
|
||||
@Deprecated
|
||||
private static Document extractTextFromDocx(InputStream inputStream) throws IOException {
|
||||
try (XWPFDocument document = new XWPFDocument(inputStream)) {
|
||||
StringBuilder text = new StringBuilder();
|
||||
for (XWPFParagraph para : document.getParagraphs()) {
|
||||
text.append(para.getText()).append("\n");
|
||||
}
|
||||
return Document.from(text.toString());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取doc文件内容
|
||||
*
|
||||
* @param inputStream
|
||||
* @return
|
||||
* @throws IOException
|
||||
* @author chenrui
|
||||
* @date 2025/3/5 14:42
|
||||
* @deprecated 因为jeecg主项目目前不支持poi5.x, 自己实现提取功能.
|
||||
*/
|
||||
@Deprecated
|
||||
private static Document extractTextFromDoc(InputStream inputStream) throws IOException {
|
||||
try (HWPFDocument document = new HWPFDocument(inputStream);
|
||||
WordExtractor extractor = new WordExtractor(document)) {
|
||||
return Document.from(extractor.getText());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取excel文件内容
|
||||
*
|
||||
* @param inputStream
|
||||
* @return
|
||||
* @throws IOException
|
||||
* @author chenrui
|
||||
* @date 2025/3/5 14:43
|
||||
* @deprecated 因为jeecg主项目目前不支持poi5.x, 自己实现提取功能.
|
||||
*/
|
||||
@Deprecated
|
||||
private static Document extractTextFromExcel(InputStream inputStream) throws IOException {
|
||||
try (Workbook workbook = WorkbookFactory.create(inputStream)) {
|
||||
StringBuilder text = new StringBuilder();
|
||||
for (Sheet sheet : workbook) {
|
||||
text.append("Sheet: ").append(sheet.getSheetName()).append("\n");
|
||||
for (Row row : sheet) {
|
||||
for (Cell cell : row) {
|
||||
text.append(cell.toString()).append("\t");
|
||||
}
|
||||
text.append("\n");
|
||||
}
|
||||
text.append("\n");
|
||||
}
|
||||
return Document.from(text.toString());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取pptx文件内容
|
||||
*
|
||||
* @param inputStream
|
||||
* @return
|
||||
* @throws IOException
|
||||
* @author chenrui
|
||||
* @date 2025/3/5 14:43
|
||||
* @deprecated 因为jeecg主项目目前不支持poi5.x, 自己实现提取功能.
|
||||
*/
|
||||
@Deprecated
|
||||
private static Document extractTextFromPptx(InputStream inputStream) throws IOException {
|
||||
try (XMLSlideShow ppt = new XMLSlideShow(inputStream)) {
|
||||
StringBuilder text = new StringBuilder();
|
||||
for (XSLFSlide slide : ppt.getSlides()) {
|
||||
text.append("Slide ").append(slide.getSlideNumber()).append(":\n");
|
||||
List<XSLFTextShape> shapes = slide.getShapes().stream()
|
||||
.filter(s -> s instanceof XSLFTextShape)
|
||||
.map(s -> (XSLFTextShape) s)
|
||||
.collect(Collectors.toList());
|
||||
for (XSLFTextShape shape : shapes) {
|
||||
text.append(shape.getText()).append("\n");
|
||||
}
|
||||
text.append("\n");
|
||||
}
|
||||
return Document.from(text.toString());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取ppt文件内容
|
||||
*
|
||||
* @param inputStream
|
||||
* @return
|
||||
* @throws IOException
|
||||
* @author chenrui
|
||||
* @date 2025/3/5 14:43
|
||||
* @deprecated 因为jeecg主项目目前不支持poi5.x, 自己实现提取功能.
|
||||
*/
|
||||
@Deprecated
|
||||
private static Document extractTextFromPpt(InputStream inputStream) throws IOException {
|
||||
try (org.apache.poi.hslf.usermodel.HSLFSlideShow ppt = new org.apache.poi.hslf.usermodel.HSLFSlideShow(inputStream)) {
|
||||
StringBuilder text = new StringBuilder();
|
||||
for (org.apache.poi.hslf.usermodel.HSLFSlide slide : ppt.getSlides()) {
|
||||
text.append("Slide ").append(slide.getSlideNumber()).append(":\n");
|
||||
for (List<HSLFTextParagraph> shapes : slide.getTextParagraphs()) {
|
||||
text.append(HSLFTextParagraph.getText(shapes)).append("\n");
|
||||
}
|
||||
text.append("\n");
|
||||
}
|
||||
return Document.from(text.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] toByteArray(InputStream inputStream) throws IOException {
|
||||
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
|
||||
byte[] data = new byte[1024];
|
||||
int nRead;
|
||||
while ((nRead = inputStream.read(data, 0, data.length)) != -1) {
|
||||
buffer.write(data, 0, nRead);
|
||||
}
|
||||
return buffer.toByteArray();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package org.jeecg.modules.airag.llm.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 保存插件工具DTO
|
||||
* fro [QQYUN-12453]【AI】支持插件
|
||||
* @author chenrui
|
||||
* @date 2025/10/30
|
||||
*/
|
||||
@Data
|
||||
public class SaveToolsDTO {
|
||||
/**
|
||||
* 插件ID
|
||||
*/
|
||||
private String id;
|
||||
|
||||
/**
|
||||
* 工具列表JSON字符串
|
||||
*/
|
||||
private String tools;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
package org.jeecg.modules.airag.llm.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import org.jeecg.common.aspect.annotation.Dict;
|
||||
import org.jeecgframework.poi.excel.annotation.Excel;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* @Description: AIRag知识库
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-02-18
|
||||
* @Version: V1.0
|
||||
*/
|
||||
@Schema(description="AIRag知识库")
|
||||
@Data
|
||||
@TableName("airag_knowledge")
|
||||
public class AiragKnowledge implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 主键
|
||||
*/
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
@Schema(description = "主键")
|
||||
private java.lang.String id;
|
||||
|
||||
/**
|
||||
* 创建人
|
||||
*/
|
||||
@Schema(description = "创建人")
|
||||
@Dict(dictTable = "sys_user",dicCode = "username",dicText = "realname")
|
||||
private java.lang.String createBy;
|
||||
|
||||
/**
|
||||
* 创建日期
|
||||
*/
|
||||
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@Schema(description = "创建日期")
|
||||
private java.util.Date createTime;
|
||||
|
||||
/**
|
||||
* 更新人
|
||||
*/
|
||||
@Schema(description = "更新人")
|
||||
private java.lang.String updateBy;
|
||||
|
||||
/**
|
||||
* 更新日期
|
||||
*/
|
||||
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@Schema(description = "更新日期")
|
||||
private java.util.Date updateTime;
|
||||
|
||||
/**
|
||||
* 所属部门
|
||||
*/
|
||||
@Schema(description = "所属部门")
|
||||
private java.lang.String sysOrgCode;
|
||||
|
||||
/**
|
||||
* 租户id
|
||||
*/
|
||||
@Excel(name = "租户id", width = 15)
|
||||
@Schema(description = "租户id")
|
||||
private java.lang.String tenantId;
|
||||
|
||||
/**
|
||||
* 知识库名称
|
||||
*/
|
||||
@Excel(name = "知识库名称", width = 15)
|
||||
@Schema(description = "知识库名称")
|
||||
private java.lang.String name;
|
||||
|
||||
/**
|
||||
* 向量模型id
|
||||
*/
|
||||
@Excel(name = "向量模型id", width = 15, dictTable = "airag_model where model_type = 'EMBED'", dicText = "name", dicCode = "id")
|
||||
@Dict(dictTable = "airag_model where model_type = 'EMBED'", dicText = "name", dicCode = "id")
|
||||
@Schema(description = "向量模型id")
|
||||
private java.lang.String embedId;
|
||||
|
||||
/**
|
||||
* 描述
|
||||
*/
|
||||
@Excel(name = "描述", width = 15)
|
||||
@Schema(description = "描述")
|
||||
private java.lang.String descr;
|
||||
|
||||
/**
|
||||
* 状态
|
||||
*/
|
||||
@Excel(name = "状态", width = 15)
|
||||
@Schema(description = "状态")
|
||||
private java.lang.String status;
|
||||
|
||||
/**
|
||||
* 类型(knowledge知识 memory 记忆)
|
||||
*/
|
||||
@Excel(name="类型(knowledge知识 memory 记忆)", width = 15)
|
||||
@Schema(description = "类型(knowledge知识 memory 记忆)")
|
||||
private java.lang.String type;
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
package org.jeecg.modules.airag.llm.entity;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import org.jeecg.common.aspect.annotation.Dict;
|
||||
import org.jeecg.common.constant.ProvinceCityArea;
|
||||
import org.jeecg.common.util.SpringContextUtils;
|
||||
import lombok.Data;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
import org.jeecgframework.poi.excel.annotation.Excel;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
|
||||
/**
|
||||
* @Description: airag知识库文档
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-02-18
|
||||
* @Version: V1.0
|
||||
*/
|
||||
@Schema(description="airag知识库文档")
|
||||
@Data
|
||||
@TableName("airag_knowledge_doc")
|
||||
public class AiragKnowledgeDoc implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 主键
|
||||
*/
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
@Schema(description = "主键")
|
||||
private String id;
|
||||
|
||||
/**
|
||||
* 创建人
|
||||
*/
|
||||
@Schema(description = "创建人")
|
||||
@Dict(dictTable = "sys_user",dicCode = "username",dicText = "realname")
|
||||
private String createBy;
|
||||
|
||||
/**
|
||||
* 创建日期
|
||||
*/
|
||||
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@Schema(description = "创建日期")
|
||||
private Date createTime;
|
||||
|
||||
/**
|
||||
* 更新人
|
||||
*/
|
||||
@Schema(description = "更新人")
|
||||
private String updateBy;
|
||||
|
||||
/**
|
||||
* 更新日期
|
||||
*/
|
||||
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@Schema(description = "更新日期")
|
||||
private Date updateTime;
|
||||
|
||||
/**
|
||||
* 所属部门
|
||||
*/
|
||||
@Schema(description = "所属部门")
|
||||
private String sysOrgCode;
|
||||
|
||||
/**
|
||||
* 租户id
|
||||
*/
|
||||
@Excel(name = "租户id", width = 15)
|
||||
@Schema(description = "租户id")
|
||||
private String tenantId;
|
||||
|
||||
/**
|
||||
* 知识库id
|
||||
*/
|
||||
@Schema(description = "知识库id")
|
||||
private String knowledgeId;
|
||||
|
||||
/**
|
||||
* 标题
|
||||
*/
|
||||
@Excel(name = "标题", width = 15)
|
||||
@Schema(description = "标题")
|
||||
private String title;
|
||||
|
||||
/**
|
||||
* 类型
|
||||
*/
|
||||
@Excel(name = "类型", width = 15, dicCode = "know_doc_type")
|
||||
@Schema(description = "类型")
|
||||
private String type;
|
||||
|
||||
/**
|
||||
* 内容
|
||||
*/
|
||||
@Excel(name = "内容", width = 15)
|
||||
@Schema(description = "内容")
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* 元数据,存储上传文件的存储目录以及网站站点 <br/>
|
||||
* eg. {"filePath":"https://xxxxxx","website":"http://hellp.jeecg.com"}
|
||||
*/
|
||||
@Excel(name = "元数据", width = 15)
|
||||
@Schema(description = "元数据")
|
||||
private String metadata;
|
||||
|
||||
/**
|
||||
* 状态
|
||||
*/
|
||||
@Excel(name = "状态", width = 15)
|
||||
@Schema(description = "状态")
|
||||
private String status;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
package org.jeecg.modules.airag.llm.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
import org.jeecgframework.poi.excel.annotation.Excel;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* @Description: MCP
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-10-20
|
||||
* @Version: V1.0
|
||||
*/
|
||||
@Data
|
||||
@TableName("airag_mcp")
|
||||
@Accessors(chain = true)
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@Schema(description = "MCP")
|
||||
public class AiragMcp implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* id
|
||||
*/
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
@Schema(description = "id")
|
||||
private java.lang.String id;
|
||||
/**
|
||||
* 应用图标
|
||||
*/
|
||||
@Excel(name = "应用图标", width = 15)
|
||||
@Schema(description = "应用图标")
|
||||
private java.lang.String icon;
|
||||
/**
|
||||
* 名称
|
||||
*/
|
||||
@Excel(name = "名称", width = 15)
|
||||
@Schema(description = "名称")
|
||||
private java.lang.String name;
|
||||
/**
|
||||
* 描述
|
||||
*/
|
||||
@Excel(name = "描述", width = 15)
|
||||
@Schema(description = "描述")
|
||||
private java.lang.String descr;
|
||||
/**
|
||||
* 类型(plugin=插件,mcp=MCP)
|
||||
* for [QQYUN-12453]【AI】支持插件
|
||||
*/
|
||||
@Excel(name = "类型(plugin=插件,mcp=MCP)", width = 15)
|
||||
@Schema(description = "类型(plugin=插件,mcp=MCP)")
|
||||
private java.lang.String category;
|
||||
/**
|
||||
* mcp类型(sse:sse类型;stdio:标准类型)
|
||||
*/
|
||||
@Excel(name = "mcp类型(sse:sse类型;stdio:标准类型)", width = 15)
|
||||
@Schema(description = "mcp类型(sse:sse类型;stdio:标准类型)")
|
||||
private java.lang.String type;
|
||||
/**
|
||||
* 服务端点(SSE类型为URL,stdio类型为命令)
|
||||
*/
|
||||
@Excel(name = "服务端点(SSE类型为URL,stdio类型为命令)", width = 15)
|
||||
@Schema(description = "服务端点(SSE类型为URL,stdio类型为命令)")
|
||||
private java.lang.String endpoint;
|
||||
/**
|
||||
* 请求头(sse类型)、环境变量(stdio类型)
|
||||
*/
|
||||
@Excel(name = "请求头(sse类型)、环境变量(stdio类型)", width = 15)
|
||||
@Schema(description = "请求头(sse类型)、环境变量(stdio类型)")
|
||||
private java.lang.String headers;
|
||||
/**
|
||||
* 工具列表
|
||||
*/
|
||||
@Excel(name = "工具列表", width = 15)
|
||||
@Schema(description = "工具列表")
|
||||
private java.lang.String tools;
|
||||
/**
|
||||
* 状态(enable=启用、disable=禁用)
|
||||
*/
|
||||
@Excel(name = "状态(enable=启用、disable=禁用)", width = 15)
|
||||
@Schema(description = "状态(enable=启用、disable=禁用)")
|
||||
private java.lang.String status;
|
||||
/**
|
||||
* 是否同步
|
||||
*/
|
||||
@Excel(name = "是否同步", width = 15)
|
||||
@Schema(description = "是否同步")
|
||||
private java.lang.Integer synced;
|
||||
/**
|
||||
* 元数据
|
||||
*/
|
||||
@Excel(name = "元数据", width = 15)
|
||||
@Schema(description = "元数据")
|
||||
private java.lang.String metadata;
|
||||
/**
|
||||
* 创建人
|
||||
*/
|
||||
@Schema(description = "创建人")
|
||||
private java.lang.String createBy;
|
||||
/**
|
||||
* 创建日期
|
||||
*/
|
||||
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@Schema(description = "创建日期")
|
||||
private java.util.Date createTime;
|
||||
/**
|
||||
* 更新人
|
||||
*/
|
||||
@Schema(description = "更新人")
|
||||
private java.lang.String updateBy;
|
||||
/**
|
||||
* 更新日期
|
||||
*/
|
||||
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@Schema(description = "更新日期")
|
||||
private java.util.Date updateTime;
|
||||
/**
|
||||
* 所属部门
|
||||
*/
|
||||
@Schema(description = "所属部门")
|
||||
private java.lang.String sysOrgCode;
|
||||
/**
|
||||
* 租户id
|
||||
*/
|
||||
@Excel(name = "租户id", width = 15)
|
||||
@Schema(description = "租户id")
|
||||
private java.lang.String tenantId;
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
package org.jeecg.modules.airag.llm.entity;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.util.Date;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||
import org.jeecg.common.constant.ProvinceCityArea;
|
||||
import org.jeecg.common.util.SpringContextUtils;
|
||||
import lombok.Data;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
import org.jeecgframework.poi.excel.annotation.Excel;
|
||||
import org.jeecg.common.aspect.annotation.Dict;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
/**
|
||||
* @Description: AiRag模型配置
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-02-17
|
||||
* @Version: V1.0
|
||||
*/
|
||||
@Data
|
||||
@TableName("airag_model")
|
||||
@Accessors(chain = true)
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@Schema(description="AiRag模型配置")
|
||||
public class AiragModel implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 主键
|
||||
*/
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
@Schema(description = "主键")
|
||||
private String id;
|
||||
/**
|
||||
* 创建人
|
||||
*/
|
||||
@Schema(description = "创建人")
|
||||
@Dict(dictTable = "sys_user",dicCode = "username",dicText = "realname")
|
||||
private String createBy;
|
||||
/**
|
||||
* 创建日期
|
||||
*/
|
||||
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@Schema(description = "创建日期")
|
||||
private Date createTime;
|
||||
/**
|
||||
* 更新人
|
||||
*/
|
||||
@Schema(description = "更新人")
|
||||
private String updateBy;
|
||||
/**
|
||||
* 更新日期
|
||||
*/
|
||||
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@Schema(description = "更新日期")
|
||||
private Date updateTime;
|
||||
/**
|
||||
* 所属部门
|
||||
*/
|
||||
@Schema(description = "所属部门")
|
||||
private String sysOrgCode;
|
||||
/**
|
||||
* 租户id
|
||||
*/
|
||||
@Excel(name = "租户id", width = 15)
|
||||
@Schema(description = "租户id")
|
||||
private String tenantId;
|
||||
/**
|
||||
* 名称
|
||||
*/
|
||||
@Excel(name = "名称", width = 15)
|
||||
@Schema(description = "名称")
|
||||
private String name;
|
||||
/**
|
||||
* 供应者
|
||||
*/
|
||||
@Excel(name = "供应者", width = 15, dicCode = "model_provider")
|
||||
@Dict(dicCode = "model_provider")
|
||||
@Schema(description = "供应者")
|
||||
private String provider;
|
||||
/**
|
||||
* 模型类型
|
||||
*/
|
||||
@Excel(name = "模型类型", width = 15, dicCode = "model_type")
|
||||
@Dict(dicCode = "model_type")
|
||||
@Schema(description = "模型类型")
|
||||
private String modelType;
|
||||
/**
|
||||
* 模型名称
|
||||
*/
|
||||
@Excel(name = "模型名称", width = 15)
|
||||
@Schema(description = "模型名称")
|
||||
private String modelName;
|
||||
/**
|
||||
* API域名
|
||||
*/
|
||||
@Excel(name = "API域名", width = 15)
|
||||
@Schema(description = "API域名")
|
||||
private String baseUrl;
|
||||
/**
|
||||
* 凭证信息
|
||||
*/
|
||||
@Excel(name = "凭证信息", width = 15)
|
||||
@Schema(description = "凭证信息")
|
||||
private String credential;
|
||||
/**
|
||||
* 模型参数
|
||||
*/
|
||||
@Excel(name = "模型参数", width = 15)
|
||||
@Schema(description = "模型参数")
|
||||
private String modelParams;
|
||||
|
||||
/**
|
||||
* 是否激活(0=未激活,1=已激活)
|
||||
*/
|
||||
@Excel(name = "是否激活", width = 15)
|
||||
@Schema(description = "是否激活")
|
||||
private Integer activateFlag;
|
||||
}
|
||||
@@ -0,0 +1,553 @@
|
||||
package org.jeecg.modules.airag.llm.handler;
|
||||
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import dev.langchain4j.agent.tool.ToolSpecification;
|
||||
import dev.langchain4j.data.message.*;
|
||||
import dev.langchain4j.exception.InvalidRequestException;
|
||||
import dev.langchain4j.exception.ToolExecutionException;
|
||||
import dev.langchain4j.mcp.McpToolProvider;
|
||||
import dev.langchain4j.rag.query.router.QueryRouter;
|
||||
import dev.langchain4j.service.TokenStream;
|
||||
import dev.langchain4j.service.tool.ToolExecutor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.ai.handler.LLMHandler;
|
||||
import org.jeecg.common.exception.JeecgBootException;
|
||||
import org.jeecg.common.util.AssertUtils;
|
||||
import org.jeecg.common.util.filter.SsrfFileTypeFilter;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.modules.airag.common.consts.AiragConsts;
|
||||
import org.jeecg.modules.airag.common.handler.AIChatParams;
|
||||
import org.jeecg.modules.airag.common.handler.IAIChatHandler;
|
||||
import org.jeecg.modules.airag.common.handler.McpToolProviderWrapper;
|
||||
import org.jeecg.modules.airag.llm.consts.LLMConsts;
|
||||
import org.jeecg.modules.airag.llm.entity.AiragMcp;
|
||||
import org.jeecg.modules.airag.llm.entity.AiragModel;
|
||||
import org.jeecg.modules.airag.llm.mapper.AiragMcpMapper;
|
||||
import org.jeecg.modules.airag.llm.mapper.AiragModelMapper;
|
||||
import org.jeecg.config.AiRagConfigBean;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.*;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 大模型聊天工具类
|
||||
*
|
||||
* @Author: chenrui
|
||||
* @Date: 2025/2/18 14:31
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class AIChatHandler implements IAIChatHandler {
|
||||
|
||||
@Autowired
|
||||
AiragModelMapper airagModelMapper;
|
||||
|
||||
@Autowired
|
||||
AiragMcpMapper airagMcpMapper;
|
||||
|
||||
@Autowired
|
||||
EmbeddingHandler embeddingHandler;
|
||||
|
||||
@Autowired
|
||||
LLMHandler llmHandler;
|
||||
|
||||
@Autowired
|
||||
AiRagConfigBean aiRagConfigBean;
|
||||
|
||||
@Value(value = "${jeecg.path.upload:}")
|
||||
private String uploadpath;
|
||||
|
||||
/**
|
||||
* 问答
|
||||
*
|
||||
* @param modelId
|
||||
* @param messages
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/2/18 21:03
|
||||
*/
|
||||
@Override
|
||||
public String completions(String modelId, List<ChatMessage> messages) {
|
||||
AssertUtils.assertNotEmpty("至少发送一条消息", messages);
|
||||
AssertUtils.assertNotEmpty("请选择模型", modelId);
|
||||
// 整理消息
|
||||
return completions(modelId, messages, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 问答
|
||||
*
|
||||
* @param modelId
|
||||
* @param messages
|
||||
* @param params
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/2/18 21:03
|
||||
*/
|
||||
@Override
|
||||
public String completions(String modelId, List<ChatMessage> messages, AIChatParams params) {
|
||||
AssertUtils.assertNotEmpty("至少发送一条消息", messages);
|
||||
AssertUtils.assertNotEmpty("请选择模型", modelId);
|
||||
|
||||
AiragModel airagModel = airagModelMapper.getByIdIgnoreTenant(modelId);
|
||||
AssertUtils.assertSame("模型未激活,请先在[AI模型配置]中[测试激活]模型", airagModel.getActivateFlag(), 1);
|
||||
return completions(airagModel, messages, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 问答
|
||||
*
|
||||
* @param airagModel
|
||||
* @param messages
|
||||
* @param params
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/2/24 17:30
|
||||
*/
|
||||
public String completions(AiragModel airagModel, List<ChatMessage> messages, AIChatParams params) {
|
||||
params = mergeParams(airagModel, params);
|
||||
String resp;
|
||||
try {
|
||||
resp = llmHandler.completions(messages, params);
|
||||
} catch (ToolExecutionException | InvalidRequestException e) {
|
||||
log.error(e.getMessage(), e);
|
||||
return "";
|
||||
} catch (Exception e) {
|
||||
// langchain4j 异常友好提示
|
||||
String errMsg = "调用大模型接口失败,详情请查看后台日志。";
|
||||
if (oConvertUtils.isNotEmpty(e.getMessage())) {
|
||||
String exceptionMsg = e.getMessage();
|
||||
|
||||
// 检查是否是工具调用消息序列不完整的异常
|
||||
if (exceptionMsg.contains("messages with role 'tool' must be a response to a preceeding message with 'tool_calls'")) {
|
||||
errMsg = "消息序列不完整,可能是因为历史消息数量设置过小导致工具调用上下文丢失。建议增加历史消息数量后重试。";
|
||||
log.error("AI模型调用异常: 工具调用消息序列不完整,建议增加历史消息数量。异常详情: {}", exceptionMsg, e);
|
||||
throw new JeecgBootException(errMsg);
|
||||
}
|
||||
|
||||
// 根据常见异常关键字做细致翻译
|
||||
for (Map.Entry<String, String> entry : MODEL_ERROR_MAP.entrySet()) {
|
||||
String key = entry.getKey();
|
||||
String value = entry.getValue();
|
||||
if (exceptionMsg.contains(key)) {
|
||||
errMsg = value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
log.error("AI模型调用异常: {}", errMsg, e);
|
||||
throw new JeecgBootException(errMsg);
|
||||
}
|
||||
if (resp.contains("</think>")
|
||||
&& (null == params.getNoThinking() || params.getNoThinking())) {
|
||||
String[] thinkSplit = resp.split("</think>");
|
||||
resp = thinkSplit[thinkSplit.length - 1];
|
||||
}
|
||||
return resp;
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用默认模型问答
|
||||
*
|
||||
* @param messages
|
||||
* @param params
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/3/12 15:13
|
||||
*/
|
||||
@Override
|
||||
public String completionsByDefaultModel(List<ChatMessage> messages, AIChatParams params) {
|
||||
return completions(new AiragModel(), messages, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 聊天(流式)
|
||||
*
|
||||
* @param modelId
|
||||
* @param messages
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/2/20 21:06
|
||||
*/
|
||||
@Override
|
||||
public TokenStream chat(String modelId, List<ChatMessage> messages) {
|
||||
return chat(modelId, messages, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 聊天(流式)
|
||||
*
|
||||
* @param modelId
|
||||
* @param messages
|
||||
* @param params
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/2/18 21:03
|
||||
*/
|
||||
@Override
|
||||
public TokenStream chat(String modelId, List<ChatMessage> messages, AIChatParams params) {
|
||||
AssertUtils.assertNotEmpty("至少发送一条消息", messages);
|
||||
AssertUtils.assertNotEmpty("请选择模型", modelId);
|
||||
|
||||
AiragModel airagModel = airagModelMapper.getByIdIgnoreTenant(modelId);
|
||||
AssertUtils.assertSame("模型未激活,请先在[AI模型配置]中[测试激活]模型", airagModel.getActivateFlag(), 1);
|
||||
return chat(airagModel, messages, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 聊天(流式)
|
||||
*
|
||||
* @param airagModel
|
||||
* @param messages
|
||||
* @param params
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/2/24 17:29
|
||||
*/
|
||||
private TokenStream chat(AiragModel airagModel, List<ChatMessage> messages, AIChatParams params) {
|
||||
params = mergeParams(airagModel, params);
|
||||
return llmHandler.chat(messages, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用默认模型聊天
|
||||
*
|
||||
* @param messages
|
||||
* @param params
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/3/12 15:13
|
||||
*/
|
||||
@Override
|
||||
public TokenStream chatByDefaultModel(List<ChatMessage> messages, AIChatParams params) {
|
||||
return chat(new AiragModel(), messages, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并 airagmodel和params,params为准
|
||||
*
|
||||
* @param airagModel
|
||||
* @param params
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/3/11 17:45
|
||||
*/
|
||||
private AIChatParams mergeParams(AiragModel airagModel, AIChatParams params) {
|
||||
if (null == airagModel) {
|
||||
return params;
|
||||
}
|
||||
if (params == null) {
|
||||
params = new AIChatParams();
|
||||
}
|
||||
|
||||
params.setProvider(airagModel.getProvider());
|
||||
params.setModelName(airagModel.getModelName());
|
||||
params.setBaseUrl(airagModel.getBaseUrl());
|
||||
if (oConvertUtils.isObjectNotEmpty(airagModel.getCredential())) {
|
||||
JSONObject modelCredential = JSONObject.parseObject(airagModel.getCredential());
|
||||
params.setApiKey(oConvertUtils.getString(modelCredential.getString("apiKey"), null));
|
||||
params.setSecretKey(oConvertUtils.getString(modelCredential.getString("secretKey"), null));
|
||||
}
|
||||
if (oConvertUtils.isObjectNotEmpty(airagModel.getModelParams())) {
|
||||
JSONObject modelParams = JSONObject.parseObject(airagModel.getModelParams());
|
||||
if (oConvertUtils.isObjectEmpty(params.getTemperature())) {
|
||||
params.setTemperature(modelParams.getDouble("temperature"));
|
||||
}
|
||||
if (oConvertUtils.isObjectEmpty(params.getTopP())) {
|
||||
params.setTopP(modelParams.getDouble("topP"));
|
||||
}
|
||||
if (oConvertUtils.isObjectEmpty(params.getPresencePenalty())) {
|
||||
params.setPresencePenalty(modelParams.getDouble("presencePenalty"));
|
||||
}
|
||||
if (oConvertUtils.isObjectEmpty(params.getFrequencyPenalty())) {
|
||||
params.setFrequencyPenalty(modelParams.getDouble("frequencyPenalty"));
|
||||
}
|
||||
if (oConvertUtils.isObjectEmpty(params.getMaxTokens())) {
|
||||
params.setMaxTokens(modelParams.getInteger("maxTokens"));
|
||||
}
|
||||
if (oConvertUtils.isObjectEmpty(params.getTimeout())) {
|
||||
params.setTimeout(modelParams.getInteger("timeout"));
|
||||
}
|
||||
if (oConvertUtils.isObjectEmpty(params.getEnableSearch())) {
|
||||
params.setEnableSearch(modelParams.getBoolean("enableSearch"));
|
||||
}
|
||||
}
|
||||
|
||||
// RAG
|
||||
List<String> knowIds = params.getKnowIds();
|
||||
if (oConvertUtils.isObjectNotEmpty(knowIds)) {
|
||||
QueryRouter queryRouter = embeddingHandler.getQueryRouter(knowIds, params.getTopNumber(), params.getSimilarity());
|
||||
params.setQueryRouter(queryRouter);
|
||||
}
|
||||
|
||||
// 设置确保maxTokens值正确
|
||||
if (oConvertUtils.isObjectNotEmpty(params.getMaxTokens()) && params.getMaxTokens() <= 0) {
|
||||
params.setMaxTokens(null);
|
||||
}
|
||||
|
||||
// 默认超时时间
|
||||
if(oConvertUtils.isObjectEmpty(params.getTimeout())){
|
||||
params.setTimeout(AiragConsts.DEFAULT_TIMEOUT);
|
||||
}
|
||||
|
||||
//deepseek-reasoner 推理模型不支持插件tool
|
||||
String modelName = airagModel.getModelName();
|
||||
if(!LLMConsts.DEEPSEEK_REASONER.equals(modelName)){
|
||||
// 插件/MCP处理
|
||||
buildPlugins(params);
|
||||
}
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造插件和MCP工具
|
||||
* for [QQYUN-12453]【AI】支持插件
|
||||
* for [QQYUN-9234] MCP服务连接关闭 - 使用包装器保存连接引用
|
||||
* @param params
|
||||
* @author chenrui
|
||||
* @date 2025/10/31 14:04
|
||||
*/
|
||||
private void buildPlugins(AIChatParams params) {
|
||||
List<String> pluginIds = params.getPluginIds();
|
||||
|
||||
if(oConvertUtils.isObjectNotEmpty(pluginIds)){
|
||||
List<McpToolProvider> mcpToolProviders = new ArrayList<>();
|
||||
List<McpToolProviderWrapper> mcpToolProviderWrappers = new ArrayList<>();
|
||||
Map<ToolSpecification, ToolExecutor> pluginTools = new HashMap<>();
|
||||
|
||||
for (String pluginId : pluginIds.stream().distinct().collect(Collectors.toList())) {
|
||||
AiragMcp airagMcp = airagMcpMapper.selectById(pluginId);
|
||||
if (airagMcp == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String category = airagMcp.getCategory();
|
||||
if (oConvertUtils.isEmpty(category)) {
|
||||
// 兼容旧数据:如果没有category字段,默认为mcp
|
||||
category = "mcp";
|
||||
}
|
||||
|
||||
if ("mcp".equalsIgnoreCase(category)) {
|
||||
// MCP类型:构建McpToolProviderWrapper(包含连接引用用于后续关闭)
|
||||
// for [QQYUN-9234] MCP服务连接关闭
|
||||
McpToolProviderWrapper wrapper = buildMcpToolProviderWrapper(
|
||||
airagMcp.getName(),
|
||||
airagMcp.getType(),
|
||||
airagMcp.getEndpoint(),
|
||||
airagMcp.getHeaders(),
|
||||
aiRagConfigBean.getAllowSensitiveNodes()
|
||||
);
|
||||
if (wrapper != null) {
|
||||
mcpToolProviders.add(wrapper.getMcpToolProvider());
|
||||
mcpToolProviderWrappers.add(wrapper);
|
||||
}
|
||||
} else if ("plugin".equalsIgnoreCase(category)) {
|
||||
// 插件类型:构建ToolSpecification和ToolExecutor
|
||||
Map<ToolSpecification, ToolExecutor> tools = PluginToolBuilder.buildTools(airagMcp, params.getCurrentHttpRequest());
|
||||
if (tools != null && !tools.isEmpty()) {
|
||||
pluginTools.putAll(tools);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 设置MCP工具提供者
|
||||
if (!mcpToolProviders.isEmpty()) {
|
||||
params.setMcpToolProviders(mcpToolProviders);
|
||||
}
|
||||
|
||||
// 保存MCP连接包装器,用于后续关闭
|
||||
// for [QQYUN-9234] MCP服务连接关闭
|
||||
if (!mcpToolProviderWrappers.isEmpty()) {
|
||||
params.setMcpToolProviderWrappers(mcpToolProviderWrappers);
|
||||
}
|
||||
|
||||
// 设置插件工具
|
||||
if (!pluginTools.isEmpty()) {
|
||||
if (params.getTools() == null) {
|
||||
params.setTools(new HashMap<>());
|
||||
}
|
||||
params.getTools().putAll(pluginTools);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserMessage buildUserMessage(String content, List<String> images) {
|
||||
AssertUtils.assertNotEmpty("请输入消息内容", content);
|
||||
List<Content> contents = new ArrayList<>();
|
||||
contents.add(TextContent.from(content));
|
||||
if (oConvertUtils.isObjectNotEmpty(images)) {
|
||||
// 获取所有图片,将他们转换为ImageContent
|
||||
List<ImageContent> imageContents = buildImageContents(images);
|
||||
contents.addAll(imageContents);
|
||||
}
|
||||
return UserMessage.from(contents);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ImageContent> buildImageContents(List<String> images) {
|
||||
List<ImageContent> imageContents = new ArrayList<>();
|
||||
for (String imageUrl : images) {
|
||||
Matcher matcher = LLMConsts.WEB_PATTERN.matcher(imageUrl);
|
||||
if (matcher.matches()) {
|
||||
// 来源于网络
|
||||
imageContents.add(ImageContent.from(imageUrl));
|
||||
} else {
|
||||
// 本地文件
|
||||
String filePath = uploadpath + File.separator + imageUrl;
|
||||
// 读取文件并转换为 base64 编码字符串
|
||||
try {
|
||||
SsrfFileTypeFilter.checkPathTraversal(filePath);
|
||||
Path path = Paths.get(filePath);
|
||||
byte[] fileContent = Files.readAllBytes(path);
|
||||
String base64Data = Base64.getEncoder().encodeToString(fileContent);
|
||||
// 获取文件的 MIME 类型
|
||||
String mimeType = Files.probeContentType(path);
|
||||
// 构建 ImageContent 对象
|
||||
imageContents.add(ImageContent.from(base64Data, mimeType));
|
||||
} catch (IOException e) {
|
||||
log.error("读取文件失败: {}", imageUrl, e);
|
||||
throw new RuntimeException("发送消息失败,读取文件异常:" + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
return imageContents;
|
||||
}
|
||||
|
||||
//================================================= begin【QQYUN-12145】【AI】AI 绘画创作 ========================================
|
||||
/**
|
||||
* 文本生成图片
|
||||
* @param modelId
|
||||
* @param messages
|
||||
* @param params
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public List<Map<String, Object>> imageGenerate(String modelId, String messages, AIChatParams params) {
|
||||
AssertUtils.assertNotEmpty("至少发送一条消息", messages);
|
||||
AssertUtils.assertNotEmpty("请选择图片大模型", modelId);
|
||||
AiragModel airagModel = airagModelMapper.getByIdIgnoreTenant(modelId);
|
||||
return this.imageGenerate(airagModel, messages, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 文本生成图片
|
||||
*
|
||||
* @param airagModel
|
||||
* @param messages
|
||||
* @param params
|
||||
* @return
|
||||
*/
|
||||
public List<Map<String, Object>> imageGenerate(AiragModel airagModel, String messages, AIChatParams params) {
|
||||
params = mergeParams(airagModel, params);
|
||||
try {
|
||||
return llmHandler.imageGenerate(messages, params);
|
||||
} catch (Exception e) {
|
||||
String errMsg = "调用绘画AI接口失败,详情请查看后台日志。";
|
||||
if (oConvertUtils.isNotEmpty(e.getMessage())) {
|
||||
// 根据常见异常关键字做细致翻译
|
||||
for (Map.Entry<String, String> entry : MODEL_ERROR_MAP.entrySet()) {
|
||||
String key = entry.getKey();
|
||||
String value = entry.getValue();
|
||||
if (e.getMessage().contains(key)) {
|
||||
errMsg = value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
log.error("AI模型调用异常: {}", errMsg, e);
|
||||
throw new JeecgBootException(errMsg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 图生图
|
||||
*
|
||||
* @param modelId
|
||||
* @param messages
|
||||
* @param images
|
||||
* @param params
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public List<Map<String, Object>> imageEdit(String modelId, String messages, List<String> images, AIChatParams params) {
|
||||
AiragModel airagModel = airagModelMapper.getByIdIgnoreTenant(modelId);
|
||||
params = mergeParams(airagModel, params);
|
||||
List<String> originalImageBase64List = getFirstImageBase64(images);
|
||||
try {
|
||||
return llmHandler.imageEdit(messages, originalImageBase64List, params);
|
||||
} catch (Exception e) {
|
||||
String errMsg = "调用绘画AI接口失败,详情请查看后台日志。";
|
||||
if (oConvertUtils.isNotEmpty(e.getMessage())) {
|
||||
// 根据常见异常关键字做细致翻译
|
||||
for (Map.Entry<String, String> entry : MODEL_ERROR_MAP.entrySet()) {
|
||||
String key = entry.getKey();
|
||||
String value = entry.getValue();
|
||||
if (errMsg.contains(key)) {
|
||||
errMsg = value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
log.error("AI模型调用异常: {}", errMsg, e);
|
||||
throw new JeecgBootException(errMsg);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 需要将图片转换成Base64编码
|
||||
* @param images 图片路径列表
|
||||
* @return Base64编码字符串
|
||||
*/
|
||||
private List<String> getFirstImageBase64(List<String> images) {
|
||||
List<String> originalImageBase64List = new ArrayList<>();
|
||||
if (images != null && !images.isEmpty()) {
|
||||
for (String imageUrl : images) {
|
||||
Matcher matcher = LLMConsts.WEB_PATTERN.matcher(imageUrl);
|
||||
try {
|
||||
byte[] fileContent;
|
||||
if (matcher.matches()) {
|
||||
// 来源于网络
|
||||
java.net.URL url = new java.net.URL(imageUrl);
|
||||
java.net.URLConnection conn = url.openConnection();
|
||||
conn.setConnectTimeout(5000);
|
||||
conn.setReadTimeout(10000);
|
||||
try (java.io.InputStream in = conn.getInputStream()) {
|
||||
java.io.ByteArrayOutputStream buffer = new java.io.ByteArrayOutputStream();
|
||||
int nRead;
|
||||
byte[] data = new byte[1024];
|
||||
while ((nRead = in.read(data, 0, data.length)) != -1) {
|
||||
buffer.write(data, 0, nRead);
|
||||
}
|
||||
buffer.flush();
|
||||
fileContent = buffer.toByteArray();
|
||||
}
|
||||
} else {
|
||||
// 本地文件
|
||||
String filePath = uploadpath + File.separator + imageUrl;
|
||||
SsrfFileTypeFilter.checkPathTraversal(filePath);
|
||||
Path path = Paths.get(filePath);
|
||||
fileContent = Files.readAllBytes(path);
|
||||
}
|
||||
originalImageBase64List.add(Base64.getEncoder().encodeToString(fileContent));
|
||||
} catch (Exception e) {
|
||||
log.error("图片读取失败: {}", imageUrl, e);
|
||||
throw new JeecgBootException("图片读取失败: " + imageUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
return originalImageBase64List;
|
||||
}
|
||||
//================================================= end 【QQYUN-12145】【AI】AI 绘画创作 ========================================
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
package org.jeecg.modules.airag.llm.handler;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang.ArrayUtils;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @Description: 命令行执行工具类
|
||||
* @Author: chenrui
|
||||
* @Date: 2024/4/8 10:11
|
||||
*/
|
||||
@Slf4j
|
||||
public class CommandExecUtil {
|
||||
|
||||
|
||||
/**
|
||||
* 执行命令行
|
||||
*
|
||||
* @param command
|
||||
* @param args
|
||||
* @return
|
||||
* @throws IOException
|
||||
* @author chenrui
|
||||
* @date 2024/4/9 10:59
|
||||
*/
|
||||
public static String execCommand(String command, String[] args) throws IOException {
|
||||
if (null == command || command.isEmpty()) {
|
||||
throw new IllegalArgumentException("命令不能为空");
|
||||
}
|
||||
return execCommand(command.split(" "), args);
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行命令行
|
||||
*
|
||||
* @param command 脚本目录
|
||||
* @param args 参数
|
||||
* @author chenrui
|
||||
* @date 2024/4/09 10:30
|
||||
*/
|
||||
public static String execCommand(String[] command, String[] args) throws IOException {
|
||||
|
||||
if (null == command || command.length == 0) {
|
||||
throw new IllegalArgumentException("命令不能为空");
|
||||
}
|
||||
|
||||
if (null != args && args.length > 0) {
|
||||
command = (String[]) ArrayUtils.addAll(command, args);
|
||||
}
|
||||
|
||||
// windows系统处理文件夹空格问题
|
||||
if (System.getProperty("os.name").toLowerCase().startsWith("windows")) {
|
||||
List<String> commandNew = new ArrayList<>(command.length + 2);
|
||||
commandNew.addAll(Arrays.asList("cmd.exe", "/c"));
|
||||
for (String tempCommand : command) {
|
||||
if (tempCommand.contains(" ")) {
|
||||
tempCommand = "\"" + tempCommand.replaceAll("\"", "'") + "\"";
|
||||
}
|
||||
commandNew.add(tempCommand);
|
||||
}
|
||||
command = commandNew.toArray(new String[0]);
|
||||
}
|
||||
|
||||
|
||||
Process process = null;
|
||||
try {
|
||||
log.debug(" =============================== Runtime command Script ===============================" );
|
||||
log.debug(String.join(" ", command));
|
||||
log.debug(" =============================== Runtime command Script =============================== " );
|
||||
process = Runtime.getRuntime().exec(command);
|
||||
try (ByteArrayOutputStream resultOutStream = new ByteArrayOutputStream();
|
||||
InputStream processInStream = new BufferedInputStream(process.getInputStream())) {
|
||||
new Thread(new InputStreamRunnable(process.getErrorStream(), "ErrorStream")).start();
|
||||
int num;
|
||||
byte[] bs = new byte[1024];
|
||||
while ((num = processInStream.read(bs)) != -1) {
|
||||
resultOutStream.write(bs, 0, num);
|
||||
String stepMsg = new String(bs);
|
||||
// log.debug("命令行日志:" + stepMsg);
|
||||
if (stepMsg.contains("input any key to continue...")) {
|
||||
process.destroy();
|
||||
}
|
||||
}
|
||||
String result = resultOutStream.toString();
|
||||
log.debug("执行命令完成:" + result);
|
||||
return result;
|
||||
} catch (IOException ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.error(e.getMessage(), e);
|
||||
throw e;
|
||||
} finally {
|
||||
if (process != null) {
|
||||
process.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* exec 控制台输出获取线程类
|
||||
* 使用单独的线程获取控制台输出,防止输入流阻塞
|
||||
*
|
||||
* @author chenrui
|
||||
* @date 2024/4/09 10:30
|
||||
*/
|
||||
static class InputStreamRunnable implements Runnable {
|
||||
BufferedReader bReader = null;
|
||||
String type = null;
|
||||
|
||||
public InputStreamRunnable(InputStream is, String _type) {
|
||||
try {
|
||||
bReader = new BufferedReader(new InputStreamReader(new BufferedInputStream(is), StandardCharsets.UTF_8));
|
||||
type = _type;
|
||||
} catch (Exception ex) {
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public void run() {
|
||||
String line;
|
||||
int lineNum = 0;
|
||||
|
||||
try {
|
||||
while ((line = bReader.readLine()) != null) {
|
||||
lineNum++;
|
||||
// Thread.sleep(200);
|
||||
}
|
||||
bReader.close();
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,734 @@
|
||||
package org.jeecg.modules.airag.llm.handler;
|
||||
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.google.common.collect.Lists;
|
||||
import dev.langchain4j.data.document.Document;
|
||||
import dev.langchain4j.data.document.DocumentSplitter;
|
||||
import dev.langchain4j.data.document.Metadata;
|
||||
import dev.langchain4j.data.document.splitter.DocumentSplitters;
|
||||
import dev.langchain4j.data.embedding.Embedding;
|
||||
import dev.langchain4j.data.segment.TextSegment;
|
||||
import dev.langchain4j.model.embedding.EmbeddingModel;
|
||||
import dev.langchain4j.rag.content.retriever.ContentRetriever;
|
||||
import dev.langchain4j.rag.content.retriever.EmbeddingStoreContentRetriever;
|
||||
import dev.langchain4j.rag.query.router.DefaultQueryRouter;
|
||||
import dev.langchain4j.rag.query.router.QueryRouter;
|
||||
import dev.langchain4j.store.embedding.EmbeddingMatch;
|
||||
import dev.langchain4j.store.embedding.EmbeddingSearchRequest;
|
||||
import dev.langchain4j.store.embedding.EmbeddingStore;
|
||||
import dev.langchain4j.store.embedding.EmbeddingStoreIngestor;
|
||||
import dev.langchain4j.store.embedding.filter.Filter;
|
||||
import dev.langchain4j.store.embedding.filter.logical.And;
|
||||
import dev.langchain4j.store.embedding.pgvector.PgVectorEmbeddingStore;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.io.FilenameUtils;
|
||||
import org.apache.tika.parser.AutoDetectParser;
|
||||
import org.jeecg.ai.factory.AiModelFactory;
|
||||
import org.jeecg.ai.factory.AiModelOptions;
|
||||
import org.jeecg.common.exception.JeecgBootException;
|
||||
import org.jeecg.common.system.util.JwtUtil;
|
||||
import org.jeecg.common.util.*;
|
||||
import org.jeecg.modules.airag.common.handler.IEmbeddingHandler;
|
||||
import org.jeecg.modules.airag.common.vo.knowledge.KnowledgeSearchResult;
|
||||
import org.jeecg.modules.airag.llm.config.EmbedStoreConfigBean;
|
||||
import org.jeecg.modules.airag.llm.config.KnowConfigBean;
|
||||
import org.jeecg.modules.airag.llm.consts.LLMConsts;
|
||||
import org.jeecg.modules.airag.llm.document.TikaDocumentParser;
|
||||
import org.jeecg.modules.airag.llm.entity.AiragKnowledge;
|
||||
import org.jeecg.modules.airag.llm.entity.AiragKnowledgeDoc;
|
||||
import org.jeecg.modules.airag.llm.entity.AiragModel;
|
||||
import org.jeecg.modules.airag.llm.mapper.AiragKnowledgeMapper;
|
||||
import org.jeecg.modules.airag.llm.mapper.AiragModelMapper;
|
||||
import org.jeecg.modules.airag.llm.service.IAiragKnowledgeService;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static dev.langchain4j.store.embedding.filter.MetadataFilterBuilder.metadataKey;
|
||||
import static org.jeecg.modules.airag.llm.consts.LLMConsts.KNOWLEDGE_DOC_TYPE_FILE;
|
||||
import static org.jeecg.modules.airag.llm.consts.LLMConsts.KNOWLEDGE_DOC_TYPE_WEB;
|
||||
|
||||
/**
|
||||
* 向量工具类
|
||||
*
|
||||
* @Author: chenrui
|
||||
* @Date: 2025/2/18 14:31
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class EmbeddingHandler implements IEmbeddingHandler {
|
||||
|
||||
@Autowired
|
||||
EmbedStoreConfigBean embedStoreConfigBean;
|
||||
|
||||
@Autowired
|
||||
private AiragModelMapper airagModelMapper;
|
||||
|
||||
@Autowired
|
||||
@Lazy
|
||||
private IAiragKnowledgeService airagKnowledgeService;
|
||||
|
||||
@Autowired
|
||||
private AiragKnowledgeMapper airagKnowledgeMapper;
|
||||
|
||||
@Value(value = "${jeecg.path.upload:}")
|
||||
private String uploadpath;
|
||||
|
||||
@Autowired
|
||||
KnowConfigBean knowConfigBean;
|
||||
|
||||
/**
|
||||
* 默认分段长度
|
||||
*/
|
||||
private static final int DEFAULT_SEGMENT_SIZE = 1000;
|
||||
|
||||
/**
|
||||
* 默认分段重叠长度
|
||||
*/
|
||||
private static final int DEFAULT_OVERLAP_SIZE = 50;
|
||||
|
||||
/**
|
||||
* 最大输出长度
|
||||
*/
|
||||
private static final int DEFAULT_MAX_OUTPUT_CHARS = 4000;
|
||||
|
||||
/**
|
||||
* 向量存储元数据:knowledgeId
|
||||
*/
|
||||
public static final String EMBED_STORE_METADATA_KNOWLEDGEID = "knowledgeId";
|
||||
|
||||
/**
|
||||
* 向量存储元数据: 用户账号
|
||||
*/
|
||||
public static final String EMBED_STORE_METADATA_USER_NAME = "username";
|
||||
|
||||
/**
|
||||
* 向量存储元数据:docId
|
||||
*/
|
||||
public static final String EMBED_STORE_METADATA_DOCID = "docId";
|
||||
|
||||
/**
|
||||
* 向量存储元数据:docName
|
||||
*/
|
||||
public static final String EMBED_STORE_METADATA_DOCNAME = "docName";
|
||||
|
||||
/**
|
||||
* 向量存储元数据:创建时间
|
||||
*/
|
||||
public static final String EMBED_STORE_CREATE_TIME = "createTime";
|
||||
|
||||
/**
|
||||
* 向量存储缓存
|
||||
*/
|
||||
private static final ConcurrentHashMap<String, EmbeddingStore<TextSegment>> EMBED_STORE_CACHE = new ConcurrentHashMap<>();
|
||||
|
||||
|
||||
/**
|
||||
* 正则匹配: md图片
|
||||
* "!\\[(.*?)]\\((.*?)(\\s*=\\d+)?\\)"
|
||||
*/
|
||||
private static final Pattern PATTERN_MD_IMAGE = Pattern.compile("!\\[(.*?)]\\((.*?)\\)");
|
||||
|
||||
/**
|
||||
* 向量化文档
|
||||
*
|
||||
* @param knowId
|
||||
* @param doc
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/2/18 11:52
|
||||
*/
|
||||
public Map<String, Object> embeddingDocument(String knowId, AiragKnowledgeDoc doc) {
|
||||
AiragKnowledge airagKnowledge = airagKnowledgeService.getById(knowId);
|
||||
AssertUtils.assertNotEmpty("知识库不存在", airagKnowledge);
|
||||
AssertUtils.assertNotEmpty("请先为知识库配置向量模型库", airagKnowledge.getEmbedId());
|
||||
AssertUtils.assertNotEmpty("文档不能为空", doc);
|
||||
// 读取文档
|
||||
String content = doc.getContent();
|
||||
// 向量化并存储
|
||||
if (oConvertUtils.isEmpty(content)) {
|
||||
switch (doc.getType()) {
|
||||
case KNOWLEDGE_DOC_TYPE_FILE:
|
||||
//解析文件
|
||||
if (knowConfigBean.isEnableMinerU()) {
|
||||
parseFileByMinerU(doc);
|
||||
}
|
||||
content = parseFile(doc);
|
||||
break;
|
||||
case KNOWLEDGE_DOC_TYPE_WEB:
|
||||
// TODO author: chenrui for:读取网站内容 date:2025/2/18
|
||||
break;
|
||||
}
|
||||
}
|
||||
//update-begin---author:chenrui ---date:20250307 for:[QQYUN-11443]【AI】是不是应该把标题也生成到向量库里,标题一般是有意义的------------
|
||||
if (oConvertUtils.isNotEmpty(doc.getTitle())) {
|
||||
content = doc.getTitle() + "\n\n" + content;
|
||||
}
|
||||
//update-end---author:chenrui ---date:20250307 for:[QQYUN-11443]【AI】是不是应该把标题也生成到向量库里,标题一般是有意义的------------
|
||||
|
||||
// 向量化 date:2025/2/18
|
||||
AiragModel model = getEmbedModelData(airagKnowledge.getEmbedId());
|
||||
AiModelOptions modelOp = buildModelOptions(model);
|
||||
EmbeddingModel embeddingModel = AiModelFactory.createEmbeddingModel(modelOp);
|
||||
EmbeddingStore<TextSegment> embeddingStore = getEmbedStore(model);
|
||||
// 删除旧数据
|
||||
embeddingStore.removeAll(metadataKey(EMBED_STORE_METADATA_DOCID).isEqualTo(doc.getId()));
|
||||
// 分段器
|
||||
DocumentSplitter splitter = DocumentSplitters.recursive(DEFAULT_SEGMENT_SIZE, DEFAULT_OVERLAP_SIZE);
|
||||
// 分段并存储
|
||||
EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder()
|
||||
.documentSplitter(splitter)
|
||||
.embeddingModel(embeddingModel)
|
||||
.embeddingStore(embeddingStore)
|
||||
.build();
|
||||
Metadata metadata = Metadata.metadata(EMBED_STORE_METADATA_DOCID, doc.getId())
|
||||
.put(EMBED_STORE_METADATA_KNOWLEDGEID, doc.getKnowledgeId())
|
||||
.put(EMBED_STORE_METADATA_DOCNAME, FilenameUtils.getName(doc.getTitle()))
|
||||
//初始化记忆库的时候添加创建时间选项
|
||||
.put(EMBED_STORE_CREATE_TIME, String.valueOf(doc.getCreateTime() != null ? doc.getCreateTime().getTime() : System.currentTimeMillis()));
|
||||
//update-begin---author:wangshuai---date:2025-12-26---for:【QQYUN-14265】【AI】支持记忆---
|
||||
//添加用户名字到元数据里面,用于记忆库中数据隔离
|
||||
String username = doc.getCreateBy();
|
||||
if (oConvertUtils.isEmpty(username)) {
|
||||
try {
|
||||
HttpServletRequest request = SpringContextUtils.getHttpServletRequest();
|
||||
String token = TokenUtils.getTokenByRequest(request);
|
||||
username = JwtUtil.getUsername(token);
|
||||
} catch (Exception e) {
|
||||
// ignore:token获取不到默认为admin
|
||||
username = "admin";
|
||||
}
|
||||
}
|
||||
if (oConvertUtils.isNotEmpty(username)) {
|
||||
metadata.put(EMBED_STORE_METADATA_USER_NAME, username);
|
||||
}
|
||||
//update-end---author:wangshuai---date:2025-12-26---for:【QQYUN-14265】【AI】支持记忆---
|
||||
Document from = Document.from(content, metadata);
|
||||
//update-begin---author:jeecg---date:2026-02-26---for:[#9374]【AI知识库】千帆向量报错,添加异常处理防止空指针
|
||||
try {
|
||||
ingestor.ingest(from);
|
||||
} catch (Exception e) {
|
||||
log.error("向量存储失败,请检查向量模型配置是否正确", e);
|
||||
throw new JeecgBootException("向量存储失败:" + e.getMessage());
|
||||
}
|
||||
//update-end---author:jeecg---date:2026-02-26---for:[#9374]【AI知识库】千帆向量报错,添加异常处理防止空指针
|
||||
return metadata.toMap();
|
||||
}
|
||||
|
||||
/**
|
||||
* 向量查询(多知识库)
|
||||
*
|
||||
* @param knowIds
|
||||
* @param queryText
|
||||
* @param topNumber
|
||||
* @param similarity
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/2/18 16:52
|
||||
*/
|
||||
@Override
|
||||
public KnowledgeSearchResult embeddingSearch(List<String> knowIds, String queryText, Integer topNumber, Double similarity) {
|
||||
AssertUtils.assertNotEmpty("请选择知识库", knowIds);
|
||||
AssertUtils.assertNotEmpty("请填写查询内容", queryText);
|
||||
|
||||
topNumber = oConvertUtils.getInteger(topNumber, 5);
|
||||
|
||||
//命中的文档列表
|
||||
List<Map<String, Object>> documents = new ArrayList<>(16);
|
||||
for (String knowId : knowIds) {
|
||||
List<Map<String, Object>> searchResp = searchEmbedding(knowId, queryText, topNumber, similarity);
|
||||
if (oConvertUtils.isObjectNotEmpty(searchResp)) {
|
||||
documents.addAll(searchResp);
|
||||
}
|
||||
}
|
||||
|
||||
StringBuilder data = new StringBuilder();
|
||||
//update-begin---author:wangshuai---date:2026-01-04---for:【QQYUN-14479】给ai的时候需要限制几个字---
|
||||
//是否为记忆库
|
||||
boolean memoryMode = false;
|
||||
//记忆库只有一个
|
||||
if (knowIds.size() == 1) {
|
||||
String firstId = knowIds.get(0);
|
||||
if (oConvertUtils.isNotEmpty(firstId)) {
|
||||
AiragKnowledge k = airagKnowledgeMapper.getByIdIgnoreTenant(firstId);
|
||||
memoryMode = (k != null && LLMConsts.KNOWLEDGE_TYPE_MEMORY.equalsIgnoreCase(k.getType()));
|
||||
}
|
||||
}
|
||||
//如果是记忆库按照创建时间排序,如果不是按照score分值进行排序
|
||||
List<Map<String, Object>> prepared = documents.stream()
|
||||
.sorted(memoryMode
|
||||
? Comparator.comparingLong((Map<String, Object> doc) -> oConvertUtils.getLong(doc.get(EMBED_STORE_CREATE_TIME), 0L)).reversed()
|
||||
: Comparator.comparingDouble((Map<String, Object> doc) -> (Double) doc.get("score")).reversed())
|
||||
.collect(Collectors.toList());
|
||||
List<Map<String, Object>> limited = new ArrayList<>();
|
||||
//将返回的结果按照最大的token进行长度限制
|
||||
for (Map<String, Object> doc : prepared) {
|
||||
if (limited.size() >= topNumber) {
|
||||
break;
|
||||
}
|
||||
String content = oConvertUtils.getString(doc.get("content"), "");
|
||||
int remain = DEFAULT_MAX_OUTPUT_CHARS - data.length();
|
||||
if (remain <= 0) {
|
||||
break;
|
||||
}
|
||||
//数据库中文本的长度和已经拼接的长度
|
||||
if (content.length() <= remain) {
|
||||
data.append(content).append("\n");
|
||||
limited.add(doc);
|
||||
} else {
|
||||
data.append(content, 0, remain);
|
||||
limited.add(doc);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return new KnowledgeSearchResult(data.toString(), limited);
|
||||
//update-end---author:wangshuai---date:2026-01-04---for:【QQYUN-14479】给ai的时候需要限制几个字---
|
||||
}
|
||||
|
||||
/**
|
||||
* 向量查询
|
||||
*
|
||||
* @param knowId
|
||||
* @param queryText
|
||||
* @param topNumber
|
||||
* @param similarity
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/2/18 16:52
|
||||
*/
|
||||
public List<Map<String, Object>> searchEmbedding(String knowId, String queryText, Integer topNumber, Double similarity) {
|
||||
AssertUtils.assertNotEmpty("请选择知识库", knowId);
|
||||
AiragKnowledge knowledge = airagKnowledgeMapper.getByIdIgnoreTenant(knowId);
|
||||
AssertUtils.assertNotEmpty("知识库不存在", knowledge);
|
||||
AssertUtils.assertNotEmpty("请填写查询内容", queryText);
|
||||
AiragModel model = getEmbedModelData(knowledge.getEmbedId());
|
||||
|
||||
AiModelOptions modelOp = buildModelOptions(model);
|
||||
EmbeddingModel embeddingModel = AiModelFactory.createEmbeddingModel(modelOp);
|
||||
Embedding queryEmbedding = embeddingModel.embed(queryText).content();
|
||||
|
||||
topNumber = oConvertUtils.getInteger(topNumber, modelOp.getTopNumber());
|
||||
similarity = oConvertUtils.getDou(similarity, modelOp.getSimilarity());
|
||||
|
||||
//update-begin---author:wangshuai---date:2025-12-26---for:【QQYUN-14265】【AI】支持记忆---
|
||||
Filter filter = metadataKey(EMBED_STORE_METADATA_KNOWLEDGEID).isEqualTo(knowId);
|
||||
|
||||
// 记忆库的时候需要根据用户隔离
|
||||
if (LLMConsts.KNOWLEDGE_TYPE_MEMORY.equalsIgnoreCase(knowledge.getType())) {
|
||||
try {
|
||||
HttpServletRequest request = SpringContextUtils.getHttpServletRequest();
|
||||
String token = TokenUtils.getTokenByRequest(request);
|
||||
String username = JwtUtil.getUsername(token);
|
||||
if (oConvertUtils.isNotEmpty(username)) {
|
||||
filter = new And(filter, metadataKey(EMBED_STORE_METADATA_USER_NAME).isEqualTo(username));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// ignore
|
||||
log.info("构建过滤器异常,{}",e.getMessage());
|
||||
}
|
||||
}
|
||||
//update-end---author:wangshuai---date:2025-12-26---for:【QQYUN-14265】【AI】支持记忆---
|
||||
|
||||
EmbeddingSearchRequest embeddingSearchRequest = EmbeddingSearchRequest.builder()
|
||||
.queryEmbedding(queryEmbedding)
|
||||
.maxResults(topNumber)
|
||||
.minScore(similarity)
|
||||
.filter(filter)
|
||||
.build();
|
||||
|
||||
EmbeddingStore<TextSegment> embeddingStore = getEmbedStore(model);
|
||||
List<EmbeddingMatch<TextSegment>> relevant = embeddingStore.search(embeddingSearchRequest).matches();
|
||||
List<Map<String, Object>> result = new ArrayList<>();
|
||||
if (oConvertUtils.isObjectNotEmpty(relevant)) {
|
||||
result = relevant.stream().map(matchRes -> {
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("score", matchRes.score());
|
||||
data.put("content", matchRes.embedded().text());
|
||||
Metadata metadata = matchRes.embedded().metadata();
|
||||
data.put("chunk", metadata.getInteger("index"));
|
||||
data.put(EMBED_STORE_METADATA_DOCNAME, metadata.getString(EMBED_STORE_METADATA_DOCNAME));
|
||||
//查询返回的时候增加创建时间,用于排序
|
||||
String ct = metadata.getString(EMBED_STORE_CREATE_TIME);
|
||||
data.put(EMBED_STORE_CREATE_TIME, ct);
|
||||
return data;
|
||||
}).collect(Collectors.toList());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取向量查询路由
|
||||
*
|
||||
* @param knowIds
|
||||
* @param topNumber
|
||||
* @param similarity
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/2/20 21:03
|
||||
*/
|
||||
@Override
|
||||
public QueryRouter getQueryRouter(List<String> knowIds, Integer topNumber, Double similarity) {
|
||||
AssertUtils.assertNotEmpty("请选择知识库", knowIds);
|
||||
List<ContentRetriever> retrievers = Lists.newArrayList();
|
||||
for (String knowId : knowIds) {
|
||||
if (oConvertUtils.isEmpty(knowId)) {
|
||||
continue;
|
||||
}
|
||||
AiragKnowledge knowledge = airagKnowledgeMapper.getByIdIgnoreTenant(knowId);
|
||||
AssertUtils.assertNotEmpty("知识库不存在", knowledge);
|
||||
AiragModel model = getEmbedModelData(knowledge.getEmbedId());
|
||||
AiModelOptions modelOptions = buildModelOptions(model);
|
||||
EmbeddingModel embeddingModel = AiModelFactory.createEmbeddingModel(modelOptions);
|
||||
|
||||
EmbeddingStore<TextSegment> embeddingStore = getEmbedStore(model);
|
||||
topNumber = oConvertUtils.getInteger(topNumber, 5);
|
||||
similarity = oConvertUtils.getDou(similarity, 0.75);
|
||||
|
||||
//update-begin---author:wangshuai---date:2025-12-26---for:【QQYUN-14265】【AI】支持记忆---
|
||||
Filter filter = metadataKey(EMBED_STORE_METADATA_KNOWLEDGEID).isEqualTo(knowId);
|
||||
// 记忆库的时候需要根据用户隔离
|
||||
if (LLMConsts.KNOWLEDGE_TYPE_MEMORY.equalsIgnoreCase(knowledge.getType())) {
|
||||
try {
|
||||
HttpServletRequest request = SpringContextUtils.getHttpServletRequest();
|
||||
String token = TokenUtils.getTokenByRequest(request);
|
||||
String username = JwtUtil.getUsername(token);
|
||||
if (oConvertUtils.isNotEmpty(username)) {
|
||||
filter = new And(filter, metadataKey(EMBED_STORE_METADATA_USER_NAME).isEqualTo(username));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// ignore
|
||||
log.info("构建过滤器异常,{}",e.getMessage());
|
||||
}
|
||||
}
|
||||
//update-end---author:wangshuai---date:2025-12-26---for:【QQYUN-14265】【AI】支持记忆---
|
||||
|
||||
// 构建一个嵌入存储内容检索器,用于从嵌入存储中检索内容
|
||||
EmbeddingStoreContentRetriever contentRetriever = EmbeddingStoreContentRetriever.builder()
|
||||
.embeddingStore(embeddingStore)
|
||||
.embeddingModel(embeddingModel)
|
||||
.maxResults(topNumber)
|
||||
.minScore(similarity)
|
||||
.filter(filter)
|
||||
.build();
|
||||
retrievers.add(contentRetriever);
|
||||
}
|
||||
if (retrievers.isEmpty()) {
|
||||
return null;
|
||||
} else {
|
||||
return new DefaultQueryRouter(retrievers);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除向量化文档
|
||||
*
|
||||
* @param knowId
|
||||
* @param modelId
|
||||
* @author chenrui
|
||||
* @date 2025/2/18 19:07
|
||||
*/
|
||||
public void deleteEmbedDocsByKnowId(String knowId, String modelId) {
|
||||
AssertUtils.assertNotEmpty("选择知识库", knowId);
|
||||
AiragModel model = getEmbedModelData(modelId);
|
||||
|
||||
EmbeddingStore<TextSegment> embeddingStore = getEmbedStore(model);
|
||||
// 删除数据
|
||||
embeddingStore.removeAll(metadataKey(EMBED_STORE_METADATA_KNOWLEDGEID).isEqualTo(knowId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除向量化文档
|
||||
*
|
||||
* @param docIds
|
||||
* @param modelId
|
||||
* @author chenrui
|
||||
* @date 2025/2/18 19:07
|
||||
*/
|
||||
public void deleteEmbedDocsByDocIds(List<String> docIds, String modelId) {
|
||||
AssertUtils.assertNotEmpty("选择文档", docIds);
|
||||
AiragModel model = getEmbedModelData(modelId);
|
||||
|
||||
EmbeddingStore<TextSegment> embeddingStore = getEmbedStore(model);
|
||||
// 删除数据
|
||||
embeddingStore.removeAll(metadataKey(EMBED_STORE_METADATA_DOCID).isIn(docIds));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询向量模型数据
|
||||
*
|
||||
* @param modelId
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/2/20 20:08
|
||||
*/
|
||||
private AiragModel getEmbedModelData(String modelId) {
|
||||
AssertUtils.assertNotEmpty("向量模型不能为空", modelId);
|
||||
AiragModel model = airagModelMapper.getByIdIgnoreTenant(modelId);
|
||||
AssertUtils.assertNotEmpty("向量模型不存在", model);
|
||||
AssertUtils.assertEquals("仅支持向量模型", LLMConsts.MODEL_TYPE_EMBED, model.getModelType());
|
||||
return model;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取向量存储
|
||||
*
|
||||
* @param model
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/2/18 14:56
|
||||
*/
|
||||
private EmbeddingStore<TextSegment> getEmbedStore(AiragModel model) {
|
||||
AssertUtils.assertNotEmpty("未配置模型", model);
|
||||
String modelId = model.getId();
|
||||
String connectionInfo = embedStoreConfigBean.getHost() + embedStoreConfigBean.getPort() + embedStoreConfigBean.getDatabase();
|
||||
String key = modelId + connectionInfo;
|
||||
if (EMBED_STORE_CACHE.containsKey(key)) {
|
||||
return EMBED_STORE_CACHE.get(key);
|
||||
}
|
||||
|
||||
|
||||
AiModelOptions modelOp = buildModelOptions(model);
|
||||
EmbeddingModel embeddingModel = AiModelFactory.createEmbeddingModel(modelOp);
|
||||
|
||||
String tableName = embedStoreConfigBean.getTable();
|
||||
|
||||
// update-begin---author:sunjianlei ---date:20250509 for:【QQYUN-12345】向量模型维度不一致问题
|
||||
// 如果该模型不是默认的向量维度
|
||||
int dimension = embeddingModel.dimension();
|
||||
if (!LLMConsts.EMBED_MODEL_DEFAULT_DIMENSION.equals(dimension)) {
|
||||
// 就加上维度后缀,防止因维度不一致导致保存失败
|
||||
tableName += ("_" + dimension);
|
||||
}
|
||||
// update-end-----author:sunjianlei ---date:20250509 for:【QQYUN-12345】向量模型维度不一致问题
|
||||
|
||||
EmbeddingStore<TextSegment> embeddingStore = PgVectorEmbeddingStore.builder()
|
||||
// Connection and table parameters
|
||||
.host(embedStoreConfigBean.getHost())
|
||||
.port(embedStoreConfigBean.getPort())
|
||||
.database(embedStoreConfigBean.getDatabase())
|
||||
.user(embedStoreConfigBean.getUser())
|
||||
.password(embedStoreConfigBean.getPassword())
|
||||
.table(tableName)
|
||||
// Embedding dimension
|
||||
// Required: Must match the embedding model’s output dimension
|
||||
.dimension(embeddingModel.dimension())
|
||||
// Indexing and performance options
|
||||
// Enable IVFFlat index
|
||||
.useIndex(true)
|
||||
// Number of lists
|
||||
// for IVFFlat index
|
||||
.indexListSize(100)
|
||||
// Table creation options
|
||||
// Automatically create the table if it doesn’t exist
|
||||
.createTable(true)
|
||||
//Don’t drop the table first (set to true if you want a fresh start)
|
||||
.dropTableFirst(false)
|
||||
.build();
|
||||
EMBED_STORE_CACHE.put(key, embeddingStore);
|
||||
return embeddingStore;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造ModelOptions
|
||||
*
|
||||
* @param model
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/3/11 17:45
|
||||
*/
|
||||
public static AiModelOptions buildModelOptions(AiragModel model) {
|
||||
AiModelOptions.AiModelOptionsBuilder modelOpBuilder = AiModelOptions.builder()
|
||||
.provider(model.getProvider())
|
||||
.modelName(model.getModelName())
|
||||
.baseUrl(model.getBaseUrl());
|
||||
if (oConvertUtils.isObjectNotEmpty(model.getCredential())) {
|
||||
JSONObject modelCredential = JSONObject.parseObject(model.getCredential());
|
||||
modelOpBuilder.apiKey(oConvertUtils.getString(modelCredential.getString("apiKey"), null));
|
||||
modelOpBuilder.secretKey(oConvertUtils.getString(modelCredential.getString("secretKey"), null));
|
||||
}
|
||||
modelOpBuilder.topNumber(5);
|
||||
modelOpBuilder.similarity(0.75);
|
||||
return modelOpBuilder.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析文件
|
||||
*
|
||||
* @param doc
|
||||
* @author chenrui
|
||||
* @date 2025/3/5 11:31
|
||||
*/
|
||||
private String parseFile(AiragKnowledgeDoc doc) {
|
||||
String metadata = doc.getMetadata();
|
||||
AssertUtils.assertNotEmpty("请先上传文件", metadata);
|
||||
JSONObject metadataJson = JSONObject.parseObject(metadata);
|
||||
if (!metadataJson.containsKey(LLMConsts.KNOWLEDGE_DOC_METADATA_FILEPATH)) {
|
||||
throw new JeecgBootException("请先上传文件");
|
||||
}
|
||||
String filePath = metadataJson.getString(LLMConsts.KNOWLEDGE_DOC_METADATA_FILEPATH);
|
||||
AssertUtils.assertNotEmpty("请先上传文件", filePath);
|
||||
// 网络资源,先下载到临时目录
|
||||
filePath = ensureFile(filePath);
|
||||
// 提取文档内容
|
||||
File docFile = new File(filePath);
|
||||
if (docFile.exists()) {
|
||||
Document document = new TikaDocumentParser(AutoDetectParser::new, null, null, null).parse(docFile);
|
||||
if (null != document) {
|
||||
String content = document.text();
|
||||
// 判断是否md文档
|
||||
String fileType = FilenameUtils.getExtension(docFile.getName());
|
||||
if ("md".contains(fileType)) {
|
||||
// 如果是md文件,查找所有图片语法,如果是本地图片,替换成网络图片
|
||||
String baseUrl = "#{domainURL}/sys/common/static/";
|
||||
String sourcePath = metadataJson.getString(LLMConsts.KNOWLEDGE_DOC_METADATA_SOURCES_PATH);
|
||||
if(oConvertUtils.isNotEmpty(sourcePath)) {
|
||||
String escapedPath = uploadpath;
|
||||
//update-begin---author:wangshuai---date:2025-06-03---for:【QQYUN-12636】【AI知识库】文档库上传 本地local 文档中的图片不展示---
|
||||
/*if (File.separator.equals("\\")){
|
||||
escapedPath = uploadpath.replace("//", "\\\\");
|
||||
}*/
|
||||
//update-end---author:wangshuai---date:2025-06-03---for:【QQYUN-12636】【AI知识库】文档库上传 本地local 文档中的图片不展示---
|
||||
sourcePath = sourcePath.replaceFirst("^" + escapedPath, "").replace("\\", "/");
|
||||
String docFilePath = metadataJson.getString(LLMConsts.KNOWLEDGE_DOC_METADATA_FILEPATH);
|
||||
docFilePath = FilenameUtils.getPath(docFilePath);
|
||||
docFilePath = docFilePath.replace("\\", "/");
|
||||
StringBuffer sb = replaceImageUrl(content, baseUrl + sourcePath + "/", baseUrl + docFilePath);
|
||||
content = sb.toString();
|
||||
}
|
||||
}
|
||||
return content;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static StringBuffer replaceImageUrl(String content, String abstractBaseUrl, String relativeBaseUrl) {
|
||||
// 正则表达式匹配md文件中的图片语法 
|
||||
Matcher matcher = PATTERN_MD_IMAGE.matcher(content);
|
||||
|
||||
StringBuffer sb = new StringBuffer();
|
||||
while (matcher.find()) {
|
||||
String imageUrl = matcher.group(2);
|
||||
// 检查是否是本地图片路径
|
||||
if (!imageUrl.startsWith("http")) {
|
||||
// 替换成网络图片路径
|
||||
String networkImageUrl = abstractBaseUrl + imageUrl;
|
||||
if(imageUrl.startsWith("/")) {
|
||||
// 绝对路径
|
||||
networkImageUrl = abstractBaseUrl + imageUrl;
|
||||
}else{
|
||||
// 相对路径
|
||||
networkImageUrl = relativeBaseUrl + imageUrl;
|
||||
}
|
||||
// 修改图片路径中//->/,但保留http://和https://
|
||||
networkImageUrl = networkImageUrl.replaceAll("(?<!http:)(?<!https:)//", "/");
|
||||
matcher.appendReplacement(sb, "");
|
||||
} else {
|
||||
matcher.appendReplacement(sb, "");
|
||||
}
|
||||
}
|
||||
matcher.appendTail(sb);
|
||||
return sb;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过MinerU解析文件
|
||||
*
|
||||
* @param doc
|
||||
* @author chenrui
|
||||
* @date 2025/4/1 17:37
|
||||
*/
|
||||
private void parseFileByMinerU(AiragKnowledgeDoc doc) {
|
||||
String metadata = doc.getMetadata();
|
||||
AssertUtils.assertNotEmpty("请先上传文件", metadata);
|
||||
JSONObject metadataJson = JSONObject.parseObject(metadata);
|
||||
if (!metadataJson.containsKey(LLMConsts.KNOWLEDGE_DOC_METADATA_FILEPATH)) {
|
||||
throw new JeecgBootException("请先上传文件");
|
||||
}
|
||||
String filePath = metadataJson.getString(LLMConsts.KNOWLEDGE_DOC_METADATA_FILEPATH);
|
||||
AssertUtils.assertNotEmpty("请先上传文件", filePath);
|
||||
filePath = ensureFile(filePath);
|
||||
|
||||
File docFile = new File(filePath);
|
||||
String fileType = FilenameUtils.getExtension(filePath);
|
||||
if (!docFile.exists()
|
||||
|| "txt".equalsIgnoreCase(fileType)
|
||||
|| "md".equalsIgnoreCase(fileType)) {
|
||||
return ;
|
||||
}
|
||||
|
||||
String command = "magic-pdf";
|
||||
if (oConvertUtils.isNotEmpty(knowConfigBean.getCondaEnv())) {
|
||||
command = "conda run -n " + knowConfigBean.getCondaEnv() + " " + command;
|
||||
}
|
||||
|
||||
String outputPath = docFile.getParentFile().getAbsolutePath();
|
||||
String[] args = {
|
||||
"-p", docFile.getAbsolutePath(),
|
||||
"-o", outputPath,
|
||||
};
|
||||
|
||||
try {
|
||||
String execLog = CommandExecUtil.execCommand(command, args);
|
||||
log.info("执行命令行:" + command + " args:" + Arrays.toString(args) + "\n log::" + execLog);
|
||||
// 如果成功,替换文件路径和静态资源路径
|
||||
String fileBaseName = FilenameUtils.getBaseName(docFile.getName());
|
||||
String newFileDir = outputPath + File.separator + fileBaseName + File.separator + "auto" + File.separator ;
|
||||
// 先检查文件是否存在,存在才替换
|
||||
File convertedFile = new File(newFileDir + fileBaseName + ".md");
|
||||
if (convertedFile.exists()) {
|
||||
log.info("文件转换成md成功,替换文件路径和静态资源路径");
|
||||
newFileDir = newFileDir.replaceFirst("^" + uploadpath, "");
|
||||
metadataJson.put(LLMConsts.KNOWLEDGE_DOC_METADATA_FILEPATH, newFileDir + fileBaseName + ".md");
|
||||
metadataJson.put(LLMConsts.KNOWLEDGE_DOC_METADATA_SOURCES_PATH, newFileDir);
|
||||
doc.setMetadata(metadataJson.toJSONString());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.error("文件转换md失败,使用传统提取方案{}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保文件存在
|
||||
* @param filePath
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/4/1 17:36
|
||||
*/
|
||||
@NotNull
|
||||
private String ensureFile(String filePath) {
|
||||
// 网络资源,先下载到临时目录
|
||||
Matcher matcher = LLMConsts.WEB_PATTERN.matcher(filePath);
|
||||
if (matcher.matches()) {
|
||||
log.info("网络资源,下载到临时目录:" + filePath);
|
||||
// 准备文件
|
||||
String tempFilePath = uploadpath + File.separator + "tmp" + File.separator + UUIDGenerator.generate() + File.separator;
|
||||
String fileName = filePath;
|
||||
if (fileName.contains("?")) {
|
||||
fileName = fileName.substring(0, fileName.indexOf("?"));
|
||||
}
|
||||
fileName = FilenameUtils.getName(fileName);
|
||||
tempFilePath = tempFilePath + fileName;
|
||||
FileDownloadUtils.download2DiskFromNet(filePath, tempFilePath);
|
||||
filePath = tempFilePath;
|
||||
} else {
|
||||
//本地文件
|
||||
filePath = uploadpath + File.separator + filePath;
|
||||
}
|
||||
return filePath;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package org.jeecg.modules.airag.llm.handler;
|
||||
|
||||
import dev.langchain4j.agent.tool.ToolSpecification;
|
||||
import dev.langchain4j.service.tool.ToolExecutor;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* for [QQYUN-13565]【AI助手】新增创建用户和查询用户的工具扩展
|
||||
* @Description: jeecg llm工具提供者
|
||||
* @Author: chenrui
|
||||
* @Date: 2025/8/26 18:06
|
||||
*/
|
||||
public interface JeecgToolsProvider {
|
||||
|
||||
/**
|
||||
* 获取默认的工具列表
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/8/27 09:49
|
||||
*/
|
||||
public Map<ToolSpecification, ToolExecutor> getDefaultTools();
|
||||
|
||||
/**
|
||||
* jeecgLlm工具类
|
||||
* @author chenrui
|
||||
* @date 2025/8/27 09:49
|
||||
*/
|
||||
@Getter
|
||||
class JeecgLlmTools{
|
||||
ToolSpecification toolSpecification;
|
||||
ToolExecutor toolExecutor;
|
||||
|
||||
public JeecgLlmTools(ToolSpecification toolSpecification, ToolExecutor toolExecutor) {
|
||||
this.toolSpecification = toolSpecification;
|
||||
this.toolExecutor = toolExecutor;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,569 @@
|
||||
package org.jeecg.modules.airag.llm.handler;
|
||||
|
||||
import com.alibaba.fastjson.JSONArray;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import dev.langchain4j.agent.tool.ToolSpecification;
|
||||
import dev.langchain4j.model.chat.request.json.JsonObjectSchema;
|
||||
import dev.langchain4j.service.tool.ToolExecutor;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.jeecg.common.util.CommonUtils;
|
||||
import org.jeecg.common.util.RestUtil;
|
||||
import org.jeecg.common.util.TokenUtils;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.modules.airag.common.consts.AiragConsts;
|
||||
import org.jeecg.modules.airag.flow.component.ToolsNode;
|
||||
import org.jeecg.modules.airag.llm.entity.AiragMcp;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.client.HttpClientErrorException;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 插件工具构建器
|
||||
* 根据插件配置构建ToolSpecification和ToolExecutor
|
||||
* for [QQYUN-12453]【AI】支持插件
|
||||
*
|
||||
* @author chenrui
|
||||
* @date 2025/10/30
|
||||
*/
|
||||
@Slf4j
|
||||
public class PluginToolBuilder {
|
||||
|
||||
/**
|
||||
* 从插件配置构建工具Map
|
||||
*
|
||||
* @param airagMcp 插件配置
|
||||
* @return Map<ToolSpecification, ToolExecutor>
|
||||
*/
|
||||
public static Map<ToolSpecification, ToolExecutor> buildTools(AiragMcp airagMcp, HttpServletRequest currentHttpRequest) {
|
||||
Map<ToolSpecification, ToolExecutor> tools = new HashMap<>();
|
||||
if (airagMcp == null || oConvertUtils.isEmpty(airagMcp.getTools())) {
|
||||
return tools;
|
||||
}
|
||||
|
||||
try {
|
||||
JSONArray toolsArray = JSONArray.parseArray(airagMcp.getTools());
|
||||
if (toolsArray == null || toolsArray.isEmpty()) {
|
||||
return tools;
|
||||
}
|
||||
|
||||
String baseUrl = airagMcp.getEndpoint();
|
||||
boolean isEmptyBaseUrl = oConvertUtils.isEmpty(baseUrl);
|
||||
// 如果baseUrl为空,使用当前系统地址
|
||||
if (isEmptyBaseUrl) {
|
||||
if (currentHttpRequest != null) {
|
||||
baseUrl = CommonUtils.getBaseUrl(currentHttpRequest);
|
||||
log.info("插件[{}]的BaseURL为空,使用系统地址: {}", airagMcp.getName(), baseUrl);
|
||||
} else {
|
||||
log.warn("插件[{}]的BaseURL为空且无法获取系统地址,跳过工具构建", airagMcp.getName());
|
||||
return tools;
|
||||
}
|
||||
}
|
||||
|
||||
// 解析headers
|
||||
Map<String, String> headersMap = parseHeaders(airagMcp.getHeaders());
|
||||
|
||||
// 判断是否需要加签
|
||||
boolean isNeedSign = isEmptyBaseUrl && ToolsNode.Helper.checkNeedSign(headersMap);
|
||||
|
||||
// 解析并应用授权配置(从metadata中读取)
|
||||
applyAuthConfig(headersMap, airagMcp.getMetadata(), currentHttpRequest);
|
||||
|
||||
for (int i = 0; i < toolsArray.size(); i++) {
|
||||
JSONObject toolConfig = toolsArray.getJSONObject(i);
|
||||
if (toolConfig == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
ToolSpecification spec = buildToolSpecification(toolConfig);
|
||||
ToolExecutor executor = buildToolExecutor(toolConfig, baseUrl, headersMap, isNeedSign);
|
||||
if (spec != null && executor != null) {
|
||||
tools.put(spec, executor);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("构建插件工具失败,工具配置: {}", toolConfig.toJSONString(), e);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("解析插件工具配置失败,插件: {}", airagMcp.getName(), e);
|
||||
}
|
||||
|
||||
return tools;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建ToolSpecification
|
||||
*/
|
||||
private static ToolSpecification buildToolSpecification(JSONObject toolConfig) {
|
||||
String name = toolConfig.getString("name");
|
||||
String description = toolConfig.getString("description");
|
||||
|
||||
if (oConvertUtils.isEmpty(name) || oConvertUtils.isEmpty(description)) {
|
||||
log.warn("工具配置缺少name或description字段");
|
||||
return null;
|
||||
}
|
||||
|
||||
// 构建完整的描述信息(包含响应参数配置)
|
||||
StringBuilder fullDescription = new StringBuilder(description);
|
||||
|
||||
// 解析响应参数并拼接到描述中
|
||||
JSONArray responses = toolConfig.getJSONArray("responses");
|
||||
if (responses != null && !responses.isEmpty()) {
|
||||
fullDescription.append("\n\n返回值说明:");
|
||||
for (int i = 0; i < responses.size(); i++) {
|
||||
JSONObject responseParam = responses.getJSONObject(i);
|
||||
if (responseParam == null) {
|
||||
continue;
|
||||
}
|
||||
String paramName = responseParam.getString("name");
|
||||
String paramDesc = responseParam.getString("description");
|
||||
String paramType = responseParam.getString("type");
|
||||
|
||||
if (oConvertUtils.isEmpty(paramName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
fullDescription.append("\n- ").append(paramName);
|
||||
if (oConvertUtils.isNotEmpty(paramType)) {
|
||||
fullDescription.append(" (").append(paramType).append(")");
|
||||
}
|
||||
if (oConvertUtils.isNotEmpty(paramDesc)) {
|
||||
fullDescription.append(": ").append(paramDesc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
JsonObjectSchema.Builder schemaBuilder = JsonObjectSchema.builder();
|
||||
|
||||
// 解析请求参数
|
||||
JSONArray parameters = toolConfig.getJSONArray("parameters");
|
||||
if (parameters != null && !parameters.isEmpty()) {
|
||||
List<String> requiredParams = new ArrayList<>();
|
||||
for (int i = 0; i < parameters.size(); i++) {
|
||||
JSONObject param = parameters.getJSONObject(i);
|
||||
if (param == null) {
|
||||
continue;
|
||||
}
|
||||
String paramName = param.getString("name");
|
||||
String paramDesc = param.getString("description");
|
||||
String paramType = param.getString("type");
|
||||
|
||||
if (oConvertUtils.isEmpty(paramName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 根据参数类型添加属性
|
||||
if ("String".equalsIgnoreCase(paramType) || "string".equalsIgnoreCase(paramType)) {
|
||||
schemaBuilder.addStringProperty(paramName, paramDesc != null ? paramDesc : "");
|
||||
} else if ("Number".equalsIgnoreCase(paramType) || "number".equalsIgnoreCase(paramType)
|
||||
|| "Integer".equalsIgnoreCase(paramType) || "integer".equalsIgnoreCase(paramType)) {
|
||||
schemaBuilder.addNumberProperty(paramName, paramDesc != null ? paramDesc : "");
|
||||
} else if ("Boolean".equalsIgnoreCase(paramType) || "boolean".equalsIgnoreCase(paramType)) {
|
||||
schemaBuilder.addBooleanProperty(paramName, paramDesc != null ? paramDesc : "");
|
||||
} else {
|
||||
// 默认作为String处理
|
||||
schemaBuilder.addStringProperty(paramName, paramDesc != null ? paramDesc : "");
|
||||
}
|
||||
|
||||
// 检查是否必须
|
||||
Boolean required = param.getBooleanValue("required");
|
||||
if (required != null && required) {
|
||||
requiredParams.add(paramName);
|
||||
}
|
||||
}
|
||||
|
||||
if (!requiredParams.isEmpty()) {
|
||||
schemaBuilder.required(requiredParams.toArray(new String[0]));
|
||||
}
|
||||
}
|
||||
|
||||
return ToolSpecification.builder()
|
||||
.name(name)
|
||||
.description(fullDescription.toString())
|
||||
.parameters(schemaBuilder.build())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建ToolExecutor
|
||||
*/
|
||||
private static ToolExecutor buildToolExecutor(JSONObject toolConfig, String baseUrl, Map<String, String> defaultHeaders, boolean isNeedSign) {
|
||||
String path = toolConfig.getString("path");
|
||||
String method = toolConfig.getString("method");
|
||||
JSONArray parameters = toolConfig.getJSONArray("parameters");
|
||||
|
||||
if (oConvertUtils.isEmpty(path) || oConvertUtils.isEmpty(method)) {
|
||||
log.warn("工具配置缺少path或method字段");
|
||||
return null;
|
||||
}
|
||||
|
||||
return (toolExecutionRequest, memoryId) -> {
|
||||
try {
|
||||
// 解析AI传入的参数
|
||||
JSONObject args = JSONObject.parseObject(toolExecutionRequest.arguments());
|
||||
|
||||
// 构建完整URL
|
||||
String url = buildUrl(baseUrl, path, parameters, args);
|
||||
|
||||
// 构建请求方法
|
||||
HttpMethod httpMethod = parseHttpMethod(method);
|
||||
|
||||
// 构建请求头
|
||||
HttpHeaders httpHeaders = buildHttpHeaders(parameters, args, defaultHeaders);
|
||||
|
||||
// 构建请求参数
|
||||
JSONObject urlVariables = buildUrlVariables(parameters, args);
|
||||
Object body = buildRequestBody(parameters, args, httpHeaders);
|
||||
|
||||
if (isNeedSign) {
|
||||
// 发送请求前加签
|
||||
ToolsNode.Helper.applySignature(url, httpHeaders, urlVariables, body);
|
||||
}
|
||||
|
||||
// 发送HTTP请求,增加超时时间
|
||||
ResponseEntity<String> response = RestUtil.request(url, httpMethod, httpHeaders, urlVariables, body, String.class, AiragConsts.DEFAULT_TIMEOUT * 1000);
|
||||
|
||||
// 直接返回原始响应字符串,不进行解析
|
||||
return response.getBody() != null ? response.getBody() : "";
|
||||
} catch (HttpClientErrorException e) {
|
||||
log.error("插件工具HTTP请求失败: {}", e.getMessage(), e);
|
||||
//update-begin---author:wangshuai---date:2026-01-16---for:【QQYUN-14577】图片搜索失败会导致进行不下去---
|
||||
return "插件调用失败(HTTP " + e.getStatusCode() + "):" + e.getResponseBodyAsString()
|
||||
+ "。请继续完成剩余任务。";
|
||||
//update-end---author:wangshuai---date:2026-01-16---for:【QQYUN-14577】图片搜索失败会导致进行不下去---
|
||||
} catch (Exception e) {
|
||||
log.error("插件工具执行失败: {}", e.getMessage(), e);
|
||||
//update-begin---author:wangshuai---date:2026-01-16---for:【QQYUN-14577】图片搜索失败会导致进行不下去---
|
||||
return "插件工具执行失败:" + e.getMessage()
|
||||
+ "。请继续完成剩余任务。";
|
||||
//update-end---author:wangshuai---date:2026-01-16---for:【QQYUN-14577】图片搜索失败会导致进行不下去---
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建完整URL(处理Path参数)
|
||||
*/
|
||||
private static String buildUrl(String baseUrl, String path, JSONArray parameters, JSONObject args) {
|
||||
String fullPath = path;
|
||||
if (!path.startsWith("/")) {
|
||||
fullPath = "/" + path;
|
||||
}
|
||||
// 拼接URL时防止出现双斜杠
|
||||
if (baseUrl.endsWith("/") && fullPath.startsWith("/")) {
|
||||
fullPath = fullPath.substring(1);
|
||||
}
|
||||
String url = baseUrl + fullPath;
|
||||
|
||||
// 替换Path参数
|
||||
if (parameters != null && args != null) {
|
||||
for (int i = 0; i < parameters.size(); i++) {
|
||||
JSONObject param = parameters.getJSONObject(i);
|
||||
if (param == null) {
|
||||
continue;
|
||||
}
|
||||
String paramName = param.getString("name");
|
||||
String paramLocation = param.getString("location");
|
||||
|
||||
if (!"Path".equalsIgnoreCase(paramLocation)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Object value = args.get(paramName);
|
||||
if (value != null) {
|
||||
url = url.replace("{" + paramName + "}", value.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建请求头
|
||||
*/
|
||||
private static HttpHeaders buildHttpHeaders(JSONArray parameters, JSONObject args, Map<String, String> defaultHeaders) {
|
||||
HttpHeaders httpHeaders = new HttpHeaders();
|
||||
|
||||
// 添加默认请求头
|
||||
if (defaultHeaders != null) {
|
||||
defaultHeaders.forEach(httpHeaders::set);
|
||||
}
|
||||
|
||||
// 添加Header类型的参数
|
||||
if (parameters != null && args != null) {
|
||||
for (int i = 0; i < parameters.size(); i++) {
|
||||
JSONObject param = parameters.getJSONObject(i);
|
||||
if (param == null) {
|
||||
continue;
|
||||
}
|
||||
String paramName = param.getString("name");
|
||||
String paramLocation = param.getString("location");
|
||||
|
||||
if (!"Header".equalsIgnoreCase(paramLocation)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Object value = args.get(paramName);
|
||||
if (value != null) {
|
||||
httpHeaders.set(paramName, value.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果请求体不为空且没有设置Content-Type,默认设置为application/json
|
||||
if (!httpHeaders.containsKey(HttpHeaders.CONTENT_TYPE)) {
|
||||
httpHeaders.setContentType(MediaType.APPLICATION_JSON);
|
||||
}
|
||||
|
||||
return httpHeaders;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建URL查询参数
|
||||
*/
|
||||
private static JSONObject buildUrlVariables(JSONArray parameters, JSONObject args) {
|
||||
JSONObject urlVariables = new JSONObject();
|
||||
|
||||
if (parameters == null || args == null) {
|
||||
return urlVariables;
|
||||
}
|
||||
|
||||
for (int i = 0; i < parameters.size(); i++) {
|
||||
JSONObject param = parameters.getJSONObject(i);
|
||||
if (param == null) {
|
||||
continue;
|
||||
}
|
||||
String paramName = param.getString("name");
|
||||
String paramLocation = param.getString("location");
|
||||
|
||||
String location = paramLocation != null ? paramLocation : "";
|
||||
// 显式指定Query类型,或者未指定类型(默认作为Query)
|
||||
boolean isQueryParam = "Query".equalsIgnoreCase(location);
|
||||
boolean isOtherType = "Body".equalsIgnoreCase(location) || "Form-Data".equalsIgnoreCase(location)
|
||||
|| "Header".equalsIgnoreCase(location) || "Path".equalsIgnoreCase(location);
|
||||
|
||||
if (isQueryParam || !isOtherType) {
|
||||
Object value = args.get(paramName);
|
||||
if (value != null) {
|
||||
//如果是知识库的id赋值默认值
|
||||
if ("knowledgeId".equalsIgnoreCase(paramName)) {
|
||||
String defaultValue = param.getString("defaultValue");
|
||||
urlVariables.put(paramName, defaultValue);
|
||||
} else {
|
||||
urlVariables.put(paramName, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return urlVariables.isEmpty() ? null : urlVariables;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建请求体
|
||||
*/
|
||||
private static Object buildRequestBody(JSONArray parameters, JSONObject args, HttpHeaders httpHeaders) {
|
||||
if (parameters == null || args == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
boolean hasBody = false;
|
||||
boolean hasFormData = false;
|
||||
|
||||
// 检查是否有Body或Form-Data类型的参数
|
||||
for (int i = 0; i < parameters.size(); i++) {
|
||||
JSONObject param = parameters.getJSONObject(i);
|
||||
if (param == null) {
|
||||
continue;
|
||||
}
|
||||
String paramLocation = param.getString("location");
|
||||
|
||||
if ("Body".equalsIgnoreCase(paramLocation)) {
|
||||
hasBody = true;
|
||||
} else if ("Form-Data".equalsIgnoreCase(paramLocation)) {
|
||||
hasFormData = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Body和Form-Data互斥
|
||||
if (hasBody && hasFormData) {
|
||||
log.warn("工具配置同时包含Body和Form-Data类型参数,优先使用Body");
|
||||
hasFormData = false;
|
||||
}
|
||||
|
||||
if (hasBody) {
|
||||
// Body类型:构建JSON对象
|
||||
JSONObject body = new JSONObject();
|
||||
for (int i = 0; i < parameters.size(); i++) {
|
||||
JSONObject param = parameters.getJSONObject(i);
|
||||
if (param == null) {
|
||||
continue;
|
||||
}
|
||||
String paramName = param.getString("name");
|
||||
String paramLocation = param.getString("location");
|
||||
|
||||
if (!"Body".equalsIgnoreCase(paramLocation) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Object value = args.get(paramName);
|
||||
if (value != null) {
|
||||
//如果是知识库的id赋值默认值
|
||||
if ("knowledgeId".equalsIgnoreCase(paramName)) {
|
||||
String defaultValue = param.getString("defaultValue");
|
||||
body.put(paramName, defaultValue);
|
||||
} else {
|
||||
body.put(paramName, value);
|
||||
}
|
||||
} else {
|
||||
// 检查是否有默认值
|
||||
String defaultValue = param.getString("defaultValue");
|
||||
if (oConvertUtils.isNotEmpty(defaultValue)) {
|
||||
body.put(paramName, defaultValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
httpHeaders.setContentType(MediaType.APPLICATION_JSON);
|
||||
return body.isEmpty() ? null : body;
|
||||
} else if (hasFormData) {
|
||||
// Form-Data类型:构建JSON对象(RestUtil会处理)
|
||||
JSONObject formData = new JSONObject();
|
||||
for (int i = 0; i < parameters.size(); i++) {
|
||||
JSONObject param = parameters.getJSONObject(i);
|
||||
if (param == null) {
|
||||
continue;
|
||||
}
|
||||
String paramName = param.getString("name");
|
||||
String paramLocation = param.getString("location");
|
||||
|
||||
if (!"Form-Data".equalsIgnoreCase(paramLocation)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Object value = args.get(paramName);
|
||||
if (value != null) {
|
||||
formData.put(paramName, value);
|
||||
} else {
|
||||
String defaultValue = param.getString("defaultValue");
|
||||
if (oConvertUtils.isNotEmpty(defaultValue)) {
|
||||
formData.put(paramName, defaultValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
httpHeaders.setContentType(MediaType.MULTIPART_FORM_DATA);
|
||||
return formData.isEmpty() ? null : formData;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析HTTP方法
|
||||
*/
|
||||
private static HttpMethod parseHttpMethod(String method) {
|
||||
try {
|
||||
return HttpMethod.valueOf(method.toUpperCase());
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.warn("无效的HTTP方法: {},使用默认GET", method);
|
||||
return HttpMethod.GET;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 解析headers JSON字符串为Map
|
||||
*/
|
||||
private static Map<String, String> parseHeaders(String headersStr) {
|
||||
Map<String, String> headersMap = new HashMap<>();
|
||||
if (oConvertUtils.isEmpty(headersStr)) {
|
||||
return headersMap;
|
||||
}
|
||||
|
||||
try {
|
||||
JSONObject headersJson = JSONObject.parseObject(headersStr);
|
||||
if (headersJson != null) {
|
||||
headersJson.forEach((key, value) -> {
|
||||
if (value != null) {
|
||||
headersMap.put(key, value.toString());
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("解析headers失败: {}", headersStr);
|
||||
}
|
||||
|
||||
return headersMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用授权配置到headers
|
||||
* 从metadata中读取授权配置,如果是Token授权,添加到headers中
|
||||
* 如果授权类型为token但没有设置token值,则从TokenUtils获取当前请求的token
|
||||
*
|
||||
* @param headersMap 请求头Map
|
||||
* @param metadataStr 元数据JSON字符串
|
||||
*/
|
||||
private static void applyAuthConfig(Map<String, String> headersMap, String metadataStr, HttpServletRequest currentHttpRequest) {
|
||||
if (oConvertUtils.isEmpty(metadataStr)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
JSONObject metadata = JSONObject.parseObject(metadataStr);
|
||||
if (metadata == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
String authType = metadata.getString("authType");
|
||||
if (oConvertUtils.isEmpty(authType) || !"token".equalsIgnoreCase(authType)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Token授权方式:从metadata中获取token配置并添加到headers
|
||||
String tokenParamName = metadata.getString("tokenParamName");
|
||||
String tokenParamValue = metadata.getString("tokenParamValue");
|
||||
|
||||
// 如果token参数名存在,但token值未设置,尝试从TokenUtils获取当前请求的token
|
||||
if (oConvertUtils.isNotEmpty(tokenParamName) && oConvertUtils.isEmpty(tokenParamValue)) {
|
||||
try {
|
||||
// 注意:TokenUtils需要获取当前线程的request,所以必须在同步调用中使用
|
||||
String currentToken = TokenUtils.getTokenByRequest();
|
||||
if(oConvertUtils.isEmpty(currentToken) && currentHttpRequest != null) {
|
||||
currentToken = TokenUtils.getTokenByRequest(currentHttpRequest);
|
||||
}
|
||||
if (oConvertUtils.isNotEmpty(currentToken)) {
|
||||
tokenParamValue = currentToken;
|
||||
log.debug("从TokenUtils获取Token并添加到请求头: {} = {}", tokenParamName,
|
||||
currentToken.length() > 10 ? currentToken.substring(0, 10) + "..." : currentToken);
|
||||
} else {
|
||||
log.warn("Token授权配置中tokenParamValue为空,且无法从TokenUtils获取当前请求的token");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("从TokenUtils获取token失败: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
if (oConvertUtils.isNotEmpty(tokenParamName) && oConvertUtils.isNotEmpty(tokenParamValue)) {
|
||||
// 如果headers中已存在同名header,优先使用metadata中的配置(覆盖)
|
||||
headersMap.put(tokenParamName, tokenParamValue);
|
||||
// 日志中只显示token的前几个字符,避免泄露完整token
|
||||
String tokenPreview = tokenParamValue.length() > 10
|
||||
? tokenParamValue.substring(0, 10) + "..."
|
||||
: tokenParamValue;
|
||||
log.debug("添加Token授权到请求头: {} = {}", tokenParamName, tokenPreview);
|
||||
} else {
|
||||
log.warn("Token授权配置不完整: tokenParamName={}, tokenParamValue={}", tokenParamName, tokenParamValue != null ? "***" : null);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("解析授权配置失败: {}", metadataStr, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package org.jeecg.modules.airag.llm.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.jeecg.modules.airag.llm.entity.AiragKnowledgeDoc;
|
||||
|
||||
/**
|
||||
* @Description: airag知识库文档
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-02-18
|
||||
* @Version: V1.0
|
||||
*/
|
||||
public interface AiragKnowledgeDocMapper extends BaseMapper<AiragKnowledgeDoc> {
|
||||
|
||||
/**
|
||||
* 通过主表id删除子表数据
|
||||
*
|
||||
* @param mainId 主表id
|
||||
* @return boolean
|
||||
*/
|
||||
public boolean deleteByMainId(@Param("mainId") String mainId);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package org.jeecg.modules.airag.llm.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.InterceptorIgnore;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.jeecg.modules.airag.llm.entity.AiragKnowledge;
|
||||
|
||||
/**
|
||||
* @Description: AIRag知识库
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-02-18
|
||||
* @Version: V1.0
|
||||
*/
|
||||
public interface AiragKnowledgeMapper extends BaseMapper<AiragKnowledge> {
|
||||
|
||||
/**
|
||||
* 根据ID查询知识库信息(忽略租户)
|
||||
* for [QQYUN-12113]分享之后的聊天,应用、模型、知识库不根据租户查询
|
||||
* @param id
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/4/21 15:24
|
||||
*/
|
||||
@InterceptorIgnore(tenantLine = "true")
|
||||
AiragKnowledge getByIdIgnoreTenant(String id);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package org.jeecg.modules.airag.llm.mapper;
|
||||
|
||||
import org.jeecg.modules.airag.llm.entity.AiragMcp;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
|
||||
/**
|
||||
* @Description: MCP
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-10-20
|
||||
* @Version: V1.0
|
||||
*/
|
||||
public interface AiragMcpMapper extends BaseMapper<AiragMcp> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package org.jeecg.modules.airag.llm.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.InterceptorIgnore;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.jeecg.modules.airag.llm.entity.AiragModel;
|
||||
|
||||
/**
|
||||
* @Description: AiRag模型配置
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-02-14
|
||||
* @Version: V1.0
|
||||
*/
|
||||
public interface AiragModelMapper extends BaseMapper<AiragModel> {
|
||||
|
||||
/**
|
||||
* 根据ID查询模型信息(忽略租户)
|
||||
* for [QQYUN-12113]分享之后的聊天,应用、模型、知识库不根据租户查询
|
||||
* @param id
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/4/21 15:24
|
||||
*/
|
||||
@InterceptorIgnore(tenantLine = "true")
|
||||
AiragModel getByIdIgnoreTenant(String id);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?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.jeecg.modules.airag.llm.mapper.AiragKnowledgeDocMapper">
|
||||
|
||||
<delete id="deleteByMainId" parameterType="java.lang.String">
|
||||
DELETE
|
||||
FROM airag_knowledge_doc
|
||||
WHERE knowledge_id = #{mainId}
|
||||
</delete>
|
||||
|
||||
</mapper>
|
||||
@@ -0,0 +1,9 @@
|
||||
<?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.jeecg.modules.airag.llm.mapper.AiragKnowledgeMapper">
|
||||
|
||||
<select id="getByIdIgnoreTenant" resultType="org.jeecg.modules.airag.llm.entity.AiragKnowledge">
|
||||
SELECT * FROM airag_knowledge WHERE id = #{id}
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?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.jeecg.modules.airag.llm.mapper.AiragMcpMapper">
|
||||
|
||||
</mapper>
|
||||
@@ -0,0 +1,9 @@
|
||||
<?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.jeecg.modules.airag.llm.mapper.AiragModelMapper">
|
||||
|
||||
<select id="getByIdIgnoreTenant" resultType="org.jeecg.modules.airag.llm.entity.AiragModel">
|
||||
SELECT * FROM airag_model WHERE id = #{id}
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
@@ -0,0 +1,19 @@
|
||||
package org.jeecg.modules.airag.llm.service;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @Description: 获取流程mcp服务
|
||||
* @Author: wangshuai
|
||||
* @Date: 2025-12-22 15:34:20
|
||||
* @Version: V1.0
|
||||
*/
|
||||
public interface IAiragFlowPluginService {
|
||||
|
||||
/**
|
||||
* 同步所有启用的流程到MCP插件配置
|
||||
*
|
||||
* @param flowIds 多个流程id
|
||||
*/
|
||||
Map<String, Object> getFlowsToPlugin(String flowIds);
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package org.jeecg.modules.airag.llm.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.modules.airag.llm.entity.AiragKnowledgeDoc;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* airag知识库文档
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-02-18
|
||||
* @Version: V1.0
|
||||
*/
|
||||
public interface IAiragKnowledgeDocService extends IService<AiragKnowledgeDoc> {
|
||||
|
||||
/**
|
||||
* 重建文档
|
||||
*
|
||||
* @param docIds
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/2/18 11:14
|
||||
*/
|
||||
Result<?> rebuildDocument(String docIds);
|
||||
|
||||
/**
|
||||
* 添加文档
|
||||
*
|
||||
* @param airagKnowledgeDoc
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/2/18 15:30
|
||||
*/
|
||||
Result<?> editDocument(AiragKnowledgeDoc airagKnowledgeDoc);
|
||||
|
||||
|
||||
/**
|
||||
* 通过知识库id重建文档
|
||||
*
|
||||
* @param knowId
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/2/18 18:54
|
||||
*/
|
||||
Result<?> rebuildDocumentByKnowId(String knowId);
|
||||
|
||||
|
||||
/**
|
||||
* 通过知识库id删除文档
|
||||
*
|
||||
* @param knowIds
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/2/18 18:59
|
||||
*/
|
||||
Result<?> removeByKnowIds(List<String> knowIds);
|
||||
|
||||
/**
|
||||
* 通过文档id批量删除文档
|
||||
*
|
||||
* @param docIds
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/2/18 19:16
|
||||
*/
|
||||
Result<?> removeDocByIds(List<String> docIds);
|
||||
|
||||
/**
|
||||
* 通过知识库id删除所以文档
|
||||
*
|
||||
* @param knowId
|
||||
* @return
|
||||
*/
|
||||
Result<?> deleteAllByKnowId(String knowId);
|
||||
|
||||
/**
|
||||
* 从zip包导入文档
|
||||
* @param knowId
|
||||
* @param file
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/3/20 13:50
|
||||
*/
|
||||
Result<?> importDocumentFromZip(String knowId, MultipartFile file);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package org.jeecg.modules.airag.llm.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import org.jeecg.modules.airag.llm.entity.AiragKnowledge;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* AIRag知识库
|
||||
*
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-02-18
|
||||
* @Version: V1.0
|
||||
*/
|
||||
public interface IAiragKnowledgeService extends IService<AiragKnowledge> {
|
||||
|
||||
/**
|
||||
* 构建知识库的工具
|
||||
*
|
||||
* @param memoryId
|
||||
* @return Map<String, Object>
|
||||
*/
|
||||
Map<String, Object> getPluginMemory(String memoryId);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package org.jeecg.modules.airag.llm.service;
|
||||
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.modules.airag.llm.entity.AiragMcp;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
|
||||
/**
|
||||
* @Description: MCP
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-10-20
|
||||
* @Version: V1.0
|
||||
*/
|
||||
public interface IAiragMcpService extends IService<AiragMcp> {
|
||||
|
||||
Result<String> edit(AiragMcp airagMcp);
|
||||
|
||||
Result<?> sync(String id);
|
||||
|
||||
|
||||
Result<?> toggleStatus(String id, String action);
|
||||
|
||||
/**
|
||||
* 保存插件工具(仅更新tools字段)
|
||||
* for [QQYUN-12453]【AI】支持插件
|
||||
* @param id 插件ID
|
||||
* @param tools 工具列表JSON字符串
|
||||
* @return 操作结果
|
||||
* @author chenrui
|
||||
* @date 2025/10/30
|
||||
*/
|
||||
Result<String> saveTools(String id, String tools);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package org.jeecg.modules.airag.llm.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import dev.langchain4j.data.message.ChatMessage;
|
||||
import org.jeecg.modules.airag.llm.entity.AiragModel;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @Description: AiRag模型配置
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-02-14
|
||||
* @Version: V1.0
|
||||
*/
|
||||
public interface IAiragModelService extends IService<AiragModel> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package org.jeecg.modules.airag.llm.service.impl;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.common.airag.api.IAiragBaseApi;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.exception.JeecgBootBizTipException;
|
||||
import org.jeecg.common.util.AssertUtils;
|
||||
import org.jeecg.modules.airag.llm.consts.LLMConsts;
|
||||
import org.jeecg.modules.airag.llm.entity.AiragKnowledgeDoc;
|
||||
import org.jeecg.modules.airag.llm.service.IAiragKnowledgeDocService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* airag baseAPI 实现类
|
||||
*/
|
||||
@Slf4j
|
||||
@Primary
|
||||
@Service("airagBaseApiImpl")
|
||||
public class AiragBaseApiImpl implements IAiragBaseApi {
|
||||
|
||||
@Autowired
|
||||
private IAiragKnowledgeDocService airagKnowledgeDocService;
|
||||
|
||||
@Override
|
||||
public String knowledgeWriteTextDocument(String knowledgeId, String title, String content) {
|
||||
AssertUtils.assertNotEmpty("知识库ID不能为空", knowledgeId);
|
||||
AssertUtils.assertNotEmpty("写入内容不能为空", content);
|
||||
AiragKnowledgeDoc knowledgeDoc = new AiragKnowledgeDoc();
|
||||
knowledgeDoc.setKnowledgeId(knowledgeId);
|
||||
knowledgeDoc.setTitle(title);
|
||||
knowledgeDoc.setType(LLMConsts.KNOWLEDGE_DOC_TYPE_TEXT);
|
||||
knowledgeDoc.setContent(content);
|
||||
Result<?> result = airagKnowledgeDocService.editDocument(knowledgeDoc);
|
||||
if (!result.isSuccess()) {
|
||||
throw new JeecgBootBizTipException(result.getMessage());
|
||||
}
|
||||
if (knowledgeDoc.getId() == null) {
|
||||
throw new JeecgBootBizTipException("知识库文档ID为空");
|
||||
}
|
||||
log.info("[AI-KNOWLEDGE] 文档写入完成,知识库:{}, 文档ID:{}", knowledgeId, knowledgeDoc.getId());
|
||||
return knowledgeDoc.getId();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
package org.jeecg.modules.airag.llm.service.impl;
|
||||
|
||||
import com.alibaba.fastjson.JSONArray;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import dev.langchain4j.agent.tool.ToolSpecification;
|
||||
import dev.langchain4j.service.tool.ToolExecutor;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.common.constant.CommonConstant;
|
||||
import org.jeecg.common.constant.SymbolConstant;
|
||||
import org.jeecg.common.util.SpringContextUtils;
|
||||
import org.jeecg.common.util.TokenUtils;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.modules.airag.flow.consts.FlowConsts;
|
||||
import org.jeecg.modules.airag.flow.entity.AiragFlow;
|
||||
import org.jeecg.modules.airag.flow.service.IAiragFlowService;
|
||||
import org.jeecg.modules.airag.flow.vo.api.SubFlowResult;
|
||||
import org.jeecg.modules.airag.flow.vo.flow.config.FlowNodeConfig;
|
||||
import org.jeecg.modules.airag.llm.consts.FlowPluginContent;
|
||||
import org.jeecg.modules.airag.llm.entity.AiragMcp;
|
||||
import org.jeecg.modules.airag.llm.handler.PluginToolBuilder;
|
||||
import org.jeecg.modules.airag.llm.service.IAiragFlowPluginService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* @Description: 流程同步到MCP服务实现类
|
||||
* @Author: wangshuai
|
||||
* @Date: 2025-12-22
|
||||
* @Version: V1.0
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class AiragFlowPluginServiceImpl implements IAiragFlowPluginService {
|
||||
|
||||
@Autowired
|
||||
private IAiragFlowService airagFlowService;
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getFlowsToPlugin(String flowIds) {
|
||||
log.info("开始构建流程插件");
|
||||
// 1. 查询所有启用的流程
|
||||
LambdaQueryWrapper<AiragFlow> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(AiragFlow::getStatus, FlowConsts.FLOW_STATUS_ENABLE);
|
||||
queryWrapper.in(AiragFlow::getId, Arrays.asList(flowIds.split(SymbolConstant.COMMA)));
|
||||
List<AiragFlow> flows = airagFlowService.list(queryWrapper);
|
||||
HttpServletRequest httpServletRequest = SpringContextUtils.getHttpServletRequest();
|
||||
if (flows.isEmpty()) {
|
||||
log.info("当前应用所选流程没有启用的流程");
|
||||
return null;
|
||||
}
|
||||
//返回数据
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
//插件
|
||||
//插件id
|
||||
AiragMcp tool = new AiragMcp();
|
||||
// 2. 构建插件
|
||||
String id = UUID.randomUUID().toString().replace("-", "");
|
||||
tool.setId(id);
|
||||
// 插件名称
|
||||
tool.setName(FlowPluginContent.PLUGIN_NAME);
|
||||
// 描述
|
||||
tool.setDescr(FlowPluginContent.PLUGIN_DESC);
|
||||
tool.setStatus(FlowConsts.FLOW_STATUS_ENABLE);
|
||||
tool.setSynced(CommonConstant.STATUS_1_INT);
|
||||
tool.setCategory("plugin");
|
||||
tool.setEndpoint("");
|
||||
int toolCount = 0;
|
||||
//构建拆件工具
|
||||
for (AiragFlow flow : flows) {
|
||||
try {
|
||||
|
||||
SubFlowResult subFlow = new SubFlowResult(flow);
|
||||
// 获取入参参数
|
||||
JSONArray parameter = getInputParameter(flow, subFlow);
|
||||
// 获取出参参数
|
||||
JSONArray outParams = getOutputParameter(flow, subFlow);
|
||||
// name必须符合 ^[a-zA-Z0-9_-]+$
|
||||
String validToolName = "flow_" + flow.getId();
|
||||
// 将原始名称拼接到描述中
|
||||
String description = flow.getName();
|
||||
if (oConvertUtils.isNotEmpty(flow.getDescr())) {
|
||||
description += " : " + flow.getDescr();
|
||||
}
|
||||
//构造工具参数
|
||||
String flowTool = buildParameter(parameter, outParams, flow.getId(), tool.getTools(), validToolName, description);
|
||||
tool.setTools(flowTool);
|
||||
toolCount++;
|
||||
} catch (Exception e) {
|
||||
log.error("处理流程[{}]转换插件失败: {}", flow.getName(), e.getMessage());
|
||||
}
|
||||
}
|
||||
String tenantId = TokenUtils.getTenantIdByRequest(httpServletRequest);
|
||||
//构建元数据(请求头)
|
||||
String meataData = buildMetadata(toolCount, tenantId);
|
||||
tool.setMetadata(meataData);
|
||||
Map<ToolSpecification, ToolExecutor> tools = PluginToolBuilder.buildTools(tool, httpServletRequest);
|
||||
result.put("pluginTool", tools);
|
||||
result.put("pluginId", id);
|
||||
log.info("构建流程插件结束");
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建元数据
|
||||
*
|
||||
* @param toolCount
|
||||
* @param tenantId
|
||||
*/
|
||||
private String buildMetadata(int toolCount, String tenantId) {
|
||||
JSONObject jsonObject = new JSONObject();
|
||||
jsonObject.put(FlowPluginContent.TOKEN_PARAM_NAME, FlowPluginContent.X_ACCESS_TOKEN);
|
||||
jsonObject.put(FlowPluginContent.TOOL_COUNT, toolCount);
|
||||
jsonObject.put(FlowPluginContent.AUTH_TYPE, FlowPluginContent.TOKEN);
|
||||
jsonObject.put(FlowPluginContent.TOKEN_PARAM_VALUE, "");
|
||||
jsonObject.put(CommonConstant.TENANT_ID, oConvertUtils.getInt(tenantId, 0));
|
||||
return jsonObject.toJSONString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建参数
|
||||
*
|
||||
* @param parameter
|
||||
* @param outParams
|
||||
* @param flowId
|
||||
* @param tools
|
||||
* @param description
|
||||
* @param name
|
||||
*/
|
||||
private String buildParameter(JSONArray parameter, JSONArray outParams, String flowId, String tools, String name, String description) {
|
||||
JSONArray paramArray = new JSONArray();
|
||||
JSONObject parameterObject = new JSONObject();
|
||||
parameterObject.put(FlowPluginContent.NAME, name);
|
||||
parameterObject.put(FlowPluginContent.DESCRIPTION, description);
|
||||
parameterObject.put(FlowPluginContent.PATH, FlowPluginContent.PLUGIN_REQUEST_URL + flowId);
|
||||
parameterObject.put(FlowPluginContent.METHOD, FlowPluginContent.POST);
|
||||
parameterObject.put(FlowPluginContent.ENABLED, true);
|
||||
parameterObject.put(FlowPluginContent.PARAMETERS, parameter);
|
||||
parameterObject.put(FlowPluginContent.RESPONSES, outParams);
|
||||
if (oConvertUtils.isNotEmpty(tools)) {
|
||||
paramArray = JSONArray.parseArray(tools);
|
||||
paramArray.add(parameterObject);
|
||||
} else {
|
||||
paramArray.add(parameterObject);
|
||||
}
|
||||
return paramArray.toJSONString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取参数
|
||||
*
|
||||
* @param flow
|
||||
* @param subFlow
|
||||
*/
|
||||
private JSONArray getInputParameter(AiragFlow flow, SubFlowResult subFlow) {
|
||||
JSONArray parameters = new JSONArray();
|
||||
String metadata = flow.getMetadata();
|
||||
if (oConvertUtils.isNotEmpty(metadata)) {
|
||||
JSONObject jsonObject = JSONObject.parseObject(metadata);
|
||||
if (jsonObject.containsKey(FlowPluginContent.INPUTS)) {
|
||||
JSONArray jsonArray = jsonObject.getJSONArray(FlowPluginContent.INPUTS);
|
||||
jsonArray.forEach(item -> {
|
||||
if (oConvertUtils.isNotEmpty(item.toString())) {
|
||||
JSONObject json = JSONObject.parseObject(item.toString());
|
||||
json.put(FlowPluginContent.LOCATION, FlowPluginContent.LOCATION_BODY);
|
||||
}
|
||||
});
|
||||
parameters.addAll(jsonArray);
|
||||
}
|
||||
}
|
||||
//需要获取子流程的参数,子流程的参数是单独封装的,否则在流程执行的时候会报错缺少参数
|
||||
List<FlowNodeConfig.NodeParam> inputParams = subFlow.getInputParams();
|
||||
if (inputParams != null) {
|
||||
for (FlowNodeConfig.NodeParam param : inputParams) {
|
||||
JSONObject p = new JSONObject();
|
||||
// 参数名
|
||||
p.put(FlowPluginContent.NAME, param.getField());
|
||||
String paramDesc = param.getName();
|
||||
if (oConvertUtils.isEmpty(paramDesc)) {
|
||||
paramDesc = param.getField();
|
||||
}
|
||||
// 参数描述
|
||||
p.put(FlowPluginContent.DESCRIPTION, paramDesc);
|
||||
// 类型
|
||||
p.put(FlowPluginContent.TYPE, oConvertUtils.getString(param.getType(), FlowPluginContent.TYPE_STRING));
|
||||
// 所有参数都在Body中
|
||||
p.put(FlowPluginContent.LOCATION, FlowPluginContent.LOCATION_BODY);
|
||||
boolean required = param.getRequired() != null && param.getRequired();
|
||||
p.put(FlowPluginContent.REQUIRED, required);
|
||||
parameters.add(p);
|
||||
}
|
||||
}
|
||||
return parameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建返回值
|
||||
*/
|
||||
private JSONArray getOutputParameter(AiragFlow flow, SubFlowResult subFlow) {
|
||||
JSONArray parameters = new JSONArray();
|
||||
String metadata = flow.getMetadata();
|
||||
if (oConvertUtils.isNotEmpty(metadata)) {
|
||||
JSONObject jsonObject = JSONObject.parseObject(metadata);
|
||||
if (jsonObject.containsKey(FlowPluginContent.OUTPUTS)) {
|
||||
JSONArray jsonArray = jsonObject.getJSONArray(FlowPluginContent.OUTPUTS);
|
||||
parameters.addAll(jsonArray);
|
||||
}
|
||||
}
|
||||
// List<FlowNodeConfig.NodeParam> outputParams = subFlow.getOutputParams();
|
||||
// if (outputParams != null) {
|
||||
// for (FlowNodeConfig.NodeParam param : outputParams) {
|
||||
// JSONObject p = new JSONObject();
|
||||
// // 参数名
|
||||
// p.put("name", param.getField());
|
||||
// String paramDesc = param.getName();
|
||||
// if (oConvertUtils.isEmpty(paramDesc)) {
|
||||
// paramDesc = param.getField();
|
||||
// }
|
||||
// // 参数描述
|
||||
// p.put("description", paramDesc);
|
||||
// // 类型
|
||||
// p.put("type", oConvertUtils.getString(param.getType(), "String"));
|
||||
// parameters.add(p);
|
||||
// }
|
||||
// }
|
||||
return parameters;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,470 @@
|
||||
package org.jeecg.modules.airag.llm.service.impl;
|
||||
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
|
||||
import org.apache.commons.compress.archivers.zip.ZipFile;
|
||||
import org.apache.commons.io.FilenameUtils;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.config.TenantContext;
|
||||
import org.jeecg.common.config.mqtoken.UserTokenContext;
|
||||
import org.jeecg.common.exception.JeecgBootException;
|
||||
import org.jeecg.common.util.*;
|
||||
import org.jeecg.common.util.filter.SsrfFileTypeFilter;
|
||||
import org.jeecg.modules.airag.llm.consts.LLMConsts;
|
||||
import org.jeecg.modules.airag.llm.entity.AiragKnowledge;
|
||||
import org.jeecg.modules.airag.llm.entity.AiragKnowledgeDoc;
|
||||
import org.jeecg.modules.airag.llm.handler.EmbeddingHandler;
|
||||
import org.jeecg.modules.airag.llm.mapper.AiragKnowledgeDocMapper;
|
||||
import org.jeecg.modules.airag.llm.mapper.AiragKnowledgeMapper;
|
||||
import org.jeecg.modules.airag.llm.service.IAiragKnowledgeDocService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.jeecg.modules.airag.llm.consts.LLMConsts.*;
|
||||
|
||||
/**
|
||||
* @Description: airag知识库文档
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-02-18
|
||||
* @Version: V1.0
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class AiragKnowledgeDocServiceImpl extends ServiceImpl<AiragKnowledgeDocMapper, AiragKnowledgeDoc> implements IAiragKnowledgeDocService {
|
||||
|
||||
@Autowired
|
||||
private AiragKnowledgeDocMapper airagKnowledgeDocMapper;
|
||||
|
||||
@Autowired
|
||||
private AiragKnowledgeMapper airagKnowledgeMapper;
|
||||
|
||||
@Autowired
|
||||
EmbeddingHandler embeddingHandler;
|
||||
|
||||
|
||||
@Value(value = "${jeecg.path.upload:}")
|
||||
private String uploadpath;
|
||||
|
||||
/**
|
||||
* 支持的文档类型
|
||||
*/
|
||||
private static final List<String> SUPPORT_DOC_TYPE = Arrays.asList("txt", "pdf", "docx", "doc", "pptx", "ppt", "xlsx", "xls", "md");
|
||||
|
||||
/**
|
||||
* 向量化线程池大小
|
||||
*/
|
||||
private static final int THREAD_POOL_SIZE = 10;
|
||||
|
||||
/**
|
||||
* 向量化文档线程池
|
||||
*/
|
||||
private static final ExecutorService buildDocExecutorService = Executors.newFixedThreadPool(THREAD_POOL_SIZE);
|
||||
|
||||
// 解压文件:单个文件最大150MB
|
||||
private static final long MAX_FILE_SIZE = 150 * 1024 * 1024;
|
||||
// 解压文件:总解压大小1024MB
|
||||
private static final long MAX_TOTAL_SIZE = 1024 * 1024 * 1024;
|
||||
// 解压文件:最多解压10000个Entry
|
||||
private static final int MAX_ENTRY_COUNT = 10000;
|
||||
|
||||
@Transactional(rollbackFor = {Exception.class})
|
||||
@Override
|
||||
public Result<?> editDocument(AiragKnowledgeDoc airagKnowledgeDoc) {
|
||||
AssertUtils.assertNotEmpty("文档不能未空", airagKnowledgeDoc);
|
||||
AssertUtils.assertNotEmpty("知识库不能未空", airagKnowledgeDoc.getKnowledgeId());
|
||||
AssertUtils.assertNotEmpty("文档标题不能未空", airagKnowledgeDoc.getTitle());
|
||||
AssertUtils.assertNotEmpty("文档类型不能未空", airagKnowledgeDoc.getType());
|
||||
if (KNOWLEDGE_DOC_TYPE_TEXT.equals(airagKnowledgeDoc.getType())) {
|
||||
AssertUtils.assertNotEmpty("文档内容不能为空", airagKnowledgeDoc.getContent());
|
||||
}
|
||||
|
||||
airagKnowledgeDoc.setStatus(KNOWLEDGE_DOC_STATUS_DRAFT);
|
||||
// 保存到数据库
|
||||
if (this.saveOrUpdate(airagKnowledgeDoc)) {
|
||||
// 重建向量
|
||||
return this.rebuildDocument(airagKnowledgeDoc.getId());
|
||||
} else {
|
||||
return Result.error("保存失败");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<?> rebuildDocumentByKnowId(String knowId) {
|
||||
AssertUtils.assertNotEmpty("知识库id不能为空", knowId);
|
||||
List<AiragKnowledgeDoc> docList = airagKnowledgeDocMapper.selectList(Wrappers.lambdaQuery(AiragKnowledgeDoc.class).eq(AiragKnowledgeDoc::getKnowledgeId, knowId));
|
||||
if (oConvertUtils.isObjectEmpty(docList)) {
|
||||
return Result.OK();
|
||||
}
|
||||
String docIds = docList.stream().map(AiragKnowledgeDoc::getId).collect(Collectors.joining(","));
|
||||
return rebuildDocument(docIds);
|
||||
}
|
||||
|
||||
@Transactional(rollbackFor = {java.lang.Exception.class})
|
||||
@Override
|
||||
public Result<?> rebuildDocument(String docIds) {
|
||||
AssertUtils.assertNotEmpty("请选择要重建的文档", docIds);
|
||||
List<String> docIdList = Arrays.asList(docIds.split(","));
|
||||
// 查询数据
|
||||
List<AiragKnowledgeDoc> docList = airagKnowledgeDocMapper.selectBatchIds(docIdList);
|
||||
AssertUtils.assertNotEmpty("文档不存在", docList);
|
||||
|
||||
// 检查状态
|
||||
List<AiragKnowledgeDoc> knowledgeDocs = docList.stream()
|
||||
.filter(doc -> {
|
||||
//update-begin---author:chenrui ---date:20250410 for:[QQYUN-11943]【ai】ai知识库 上传完文档 一直显示构建中?------------
|
||||
if(KNOWLEDGE_DOC_STATUS_BUILDING.equalsIgnoreCase(doc.getStatus())){
|
||||
Date updateTime = doc.getUpdateTime();
|
||||
if (updateTime != null) {
|
||||
// 向量化超过了5分钟,重新向量化
|
||||
long timeDifference = System.currentTimeMillis() - updateTime.getTime();
|
||||
return timeDifference > 5 * 60 * 1000;
|
||||
}else{
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
//update-end---author:chenrui ---date:20250410 for:[QQYUN-11943]【ai】ai知识库 上传完文档 一直显示构建中?------------
|
||||
})
|
||||
.peek(doc -> {
|
||||
doc.setStatus(KNOWLEDGE_DOC_STATUS_BUILDING);
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
if (oConvertUtils.isObjectEmpty(knowledgeDocs)) {
|
||||
return Result.ok("操作成功");
|
||||
}
|
||||
if (oConvertUtils.isObjectEmpty(knowledgeDocs)) {
|
||||
return Result.ok("操作成功");
|
||||
}
|
||||
// 更新状态
|
||||
this.updateBatchById(knowledgeDocs);
|
||||
// 异步重建文档
|
||||
String tenantId = TenantContext.getTenant();
|
||||
String token = TokenUtils.getTokenByRequest();
|
||||
knowledgeDocs.forEach((doc) -> {
|
||||
CompletableFuture.runAsync(() -> {
|
||||
UserTokenContext.setToken(token);
|
||||
TenantContext.setTenant(tenantId);
|
||||
String knowId = doc.getKnowledgeId();
|
||||
log.info("开始重建文档, 知识库id: {}, 文档id: {}", knowId, doc.getId());
|
||||
doc.setStatus(KNOWLEDGE_DOC_STATUS_BUILDING);
|
||||
this.updateById(doc);
|
||||
//update-begin---author:chenrui ---date:20250410 for:[QQYUN-11943]【ai】ai知识库 上传完文档 一直显示构建中?------------
|
||||
try {
|
||||
Map<String, Object> metadata = embeddingHandler.embeddingDocument(knowId, doc);
|
||||
// 更新数据 date:2025/2/18
|
||||
if (null != metadata) {
|
||||
doc.setStatus(KNOWLEDGE_DOC_STATUS_COMPLETE);
|
||||
this.updateById(doc);
|
||||
log.info("重建文档成功, 知识库id: {}, 文档id: {}", knowId, doc.getId());
|
||||
} else {
|
||||
this.handleDocBuildFailed(doc, "向量化失败");
|
||||
log.info("重建文档失败, 知识库id: {}, 文档id: {}", knowId, doc.getId());
|
||||
}
|
||||
}catch (Throwable t){
|
||||
this.handleDocBuildFailed(doc, t.getMessage());
|
||||
log.error("重建文档失败:" + t.getMessage() + ", 知识库id: " + knowId + ", 文档id: " + doc.getId(), t);
|
||||
}
|
||||
//update-end---author:chenrui ---date:20250410 for:[QQYUN-11943]【ai】ai知识库 上传完文档 一直显示构建中?------------
|
||||
}, buildDocExecutorService);
|
||||
});
|
||||
log.info("返回操作成功");
|
||||
return Result.ok("操作成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理文档构建失败
|
||||
*/
|
||||
private void handleDocBuildFailed(AiragKnowledgeDoc doc, String failedReason) {
|
||||
doc.setStatus(KNOWLEDGE_DOC_STATUS_FAILED);
|
||||
|
||||
String metadataStr = doc.getMetadata();
|
||||
JSONObject metadata;
|
||||
if (oConvertUtils.isEmpty(metadataStr)) {
|
||||
metadata = new JSONObject();
|
||||
} else {
|
||||
metadata = JSONObject.parseObject(metadataStr);
|
||||
}
|
||||
metadata.put("failedReason", failedReason);
|
||||
doc.setMetadata(metadata.toJSONString());
|
||||
|
||||
this.updateById(doc);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<?> removeByKnowIds(List<String> knowIds) {
|
||||
AssertUtils.assertNotEmpty("选择知识库", knowIds);
|
||||
for (String knowId : knowIds) {
|
||||
AiragKnowledge airagKnowledge = airagKnowledgeMapper.selectById(knowId);
|
||||
AssertUtils.assertNotEmpty("知识库不存在", airagKnowledge);
|
||||
AssertUtils.assertNotEmpty("请先为知识库配置向量模型库", airagKnowledge.getEmbedId());
|
||||
// 异步删除向量数据
|
||||
final String embedId = airagKnowledge.getEmbedId();
|
||||
final String finalKnowId = knowId;
|
||||
CompletableFuture.runAsync(() -> {
|
||||
try {
|
||||
embeddingHandler.deleteEmbedDocsByKnowId(finalKnowId, embedId);
|
||||
} catch (Throwable ignore) {
|
||||
}
|
||||
});
|
||||
// 删除数据
|
||||
airagKnowledgeDocMapper.deleteByMainId(knowId);
|
||||
}
|
||||
return Result.OK();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<?> removeDocByIds(List<String> docIds) {
|
||||
AssertUtils.assertNotEmpty("请选择要删除的文档", docIds);
|
||||
// 查询数据
|
||||
List<AiragKnowledgeDoc> docList = airagKnowledgeDocMapper.selectBatchIds(docIds);
|
||||
AssertUtils.assertNotEmpty("文档不存在", docList);
|
||||
// 整理数据
|
||||
Map<String, List<String>> knowledgeDocs = docList.stream().collect(Collectors.groupingBy(
|
||||
AiragKnowledgeDoc::getKnowledgeId,
|
||||
Collectors.mapping(AiragKnowledgeDoc::getId, Collectors.toList())
|
||||
));
|
||||
if (oConvertUtils.isObjectEmpty(knowledgeDocs)) {
|
||||
return Result.ok("success");
|
||||
}
|
||||
knowledgeDocs.forEach((knowId, groupedDocIds) -> {
|
||||
AiragKnowledge airagKnowledge = airagKnowledgeMapper.selectById(knowId);
|
||||
AssertUtils.assertNotEmpty("知识库不存在", airagKnowledge);
|
||||
AssertUtils.assertNotEmpty("请先为知识库配置向量模型库", airagKnowledge.getEmbedId());
|
||||
// 异步删除向量数据
|
||||
final String embedId = airagKnowledge.getEmbedId();
|
||||
final List<String> docIdsToDelete = new ArrayList<>(groupedDocIds);
|
||||
CompletableFuture.runAsync(() -> {
|
||||
try {
|
||||
embeddingHandler.deleteEmbedDocsByDocIds(docIdsToDelete, embedId);
|
||||
} catch (Throwable ignore) {
|
||||
}
|
||||
});
|
||||
// 删除数据
|
||||
airagKnowledgeDocMapper.deleteBatchIds(groupedDocIds);
|
||||
});
|
||||
return Result.ok("success");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<?> deleteAllByKnowId(String knowId) {
|
||||
if (oConvertUtils.isEmpty(knowId)) {
|
||||
return Result.error("知识库id不能为空");
|
||||
}
|
||||
LambdaQueryWrapper<AiragKnowledgeDoc> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(AiragKnowledgeDoc::getKnowledgeId, knowId);
|
||||
//noinspection unchecked
|
||||
wrapper.select(AiragKnowledgeDoc::getId);
|
||||
List<AiragKnowledgeDoc> docList = airagKnowledgeDocMapper.selectList(wrapper);
|
||||
if (docList.isEmpty()) {
|
||||
return Result.ok("暂无文档");
|
||||
}
|
||||
List<String> docIds = docList.stream().map(AiragKnowledgeDoc::getId).collect(Collectors.toList());
|
||||
this.removeDocByIds(docIds);
|
||||
return Result.ok("清空完成");
|
||||
}
|
||||
|
||||
@Transactional(rollbackFor = {java.lang.Exception.class})
|
||||
@Override
|
||||
public Result<?> importDocumentFromZip(String knowId, MultipartFile zipFile) {
|
||||
AssertUtils.assertNotEmpty("请先选择知识库", knowId);
|
||||
AssertUtils.assertNotEmpty("请上传文件", zipFile);
|
||||
long startTime = System.currentTimeMillis();
|
||||
log.info("开始上传知识库文档(zip), 知识库id: {}, 文件名: {}", knowId, zipFile.getOriginalFilename());
|
||||
|
||||
try {
|
||||
String bizPath = knowId + File.separator + UUIDGenerator.generate();
|
||||
String workDir = uploadpath + File.separator + bizPath + File.separator;
|
||||
String sourcesPath = workDir + "files";
|
||||
|
||||
SsrfFileTypeFilter.checkUploadFileType(zipFile);
|
||||
// 通过filePath 检查文件是不是压缩包(zip)
|
||||
String zipFileName = FilenameUtils.getBaseName(zipFile.getOriginalFilename());
|
||||
String fileExt = FilenameUtils.getExtension(zipFile.getOriginalFilename());
|
||||
if (null == fileExt || !fileExt.equalsIgnoreCase("zip")) {
|
||||
throw new JeecgBootException("请上传zip压缩包");
|
||||
}
|
||||
String uploadedZipPath = CommonUtils.uploadLocal(zipFile, bizPath, uploadpath);
|
||||
// 解压缩文件
|
||||
List<AiragKnowledgeDoc> docList = new ArrayList<>();
|
||||
AtomicInteger fileCount = new AtomicInteger(0);
|
||||
unzipFile(uploadpath + File.separator + uploadedZipPath, sourcesPath, uploadedFile -> {
|
||||
// 仅支持txt、pdf、docx、pptx、html、md文件
|
||||
String fileName = uploadedFile.getName();
|
||||
if (!SUPPORT_DOC_TYPE.contains(FilenameUtils.getExtension(fileName).toLowerCase())) {
|
||||
log.warn("不支持的文件类型: {}", fileName);
|
||||
return;
|
||||
}
|
||||
String baseName = FilenameUtils.getBaseName(fileName);
|
||||
AiragKnowledgeDoc doc = new AiragKnowledgeDoc();
|
||||
doc.setKnowledgeId(knowId);
|
||||
doc.setTitle(baseName);
|
||||
doc.setType(LLMConsts.KNOWLEDGE_DOC_TYPE_FILE);
|
||||
doc.setStatus(LLMConsts.KNOWLEDGE_DOC_STATUS_DRAFT);
|
||||
|
||||
String relativePath;
|
||||
if (File.separator.equals("\\")) {
|
||||
// Windows path handling
|
||||
String escapedPath = uploadpath.replace("//", "\\\\");
|
||||
escapedPath = escapedPath.replace("/", "\\\\");
|
||||
relativePath = uploadedFile.getPath().replaceFirst("^" + escapedPath, "");
|
||||
} else {
|
||||
// Unix path handling
|
||||
relativePath = uploadedFile.getPath().replaceFirst("^" + uploadpath, "");
|
||||
}
|
||||
JSONObject metadata = new JSONObject();
|
||||
metadata.put(LLMConsts.KNOWLEDGE_DOC_METADATA_FILEPATH, relativePath);
|
||||
metadata.put(LLMConsts.KNOWLEDGE_DOC_METADATA_SOURCES_PATH, sourcesPath);
|
||||
doc.setMetadata(metadata.toJSONString());
|
||||
docList.add(doc);
|
||||
});
|
||||
AssertUtils.assertNotEmpty("压缩包中没有符合要求的文档", docList);
|
||||
// 保存数据
|
||||
this.saveBatch(docList);
|
||||
// 重建文档
|
||||
String docIds = docList.stream().map(AiragKnowledgeDoc::getId).filter(oConvertUtils::isObjectNotEmpty).collect(Collectors.joining(","));
|
||||
rebuildDocument(docIds);
|
||||
} catch (Exception e) {
|
||||
log.error(e.getMessage(), e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
log.info("上传知识库文档(zip)成功, 知识库id: {}, 文件名: {}, 耗时: {}ms", knowId, zipFile.getOriginalFilename(), (System.currentTimeMillis() - startTime));
|
||||
return Result.ok("上传成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 解压缩文件
|
||||
*
|
||||
* @param zipFilePath 压缩文件路径
|
||||
* @param destDir 目标文件夹
|
||||
* @param afterExtract 解压完成后回调
|
||||
* @throws IOException
|
||||
* @author chenrui
|
||||
* @date 2025/3/20 14:37
|
||||
*/
|
||||
public static void unzipFile(String zipFilePath, String destDir, Consumer<File> afterExtract) throws IOException {
|
||||
unzipFile(Paths.get(zipFilePath), Paths.get(destDir), afterExtract);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 解压缩文件
|
||||
*
|
||||
* @param zipFilePath 压缩文件路径
|
||||
* @param targetDir 目标文件夹
|
||||
* @param afterExtract 解压完成后回调
|
||||
* @throws IOException
|
||||
* @author chenrui
|
||||
* @date 2025/4/28 17:02
|
||||
*/
|
||||
private static void unzipFile(Path zipFilePath, Path targetDir, Consumer<File> afterExtract) throws IOException {
|
||||
long totalUnzippedSize = 0;
|
||||
int entryCount = 0;
|
||||
|
||||
if (!Files.exists(targetDir)) {
|
||||
Files.createDirectories(targetDir);
|
||||
}
|
||||
|
||||
try (ZipFile zipFile = new ZipFile(zipFilePath.toFile())) {
|
||||
Enumeration<ZipArchiveEntry> entries = zipFile.getEntries();
|
||||
|
||||
while (entries.hasMoreElements()) {
|
||||
ZipArchiveEntry entry = entries.nextElement();
|
||||
entryCount++;
|
||||
if (entryCount > MAX_ENTRY_COUNT) {
|
||||
throw new IOException("解压文件数量超限,可能是zip bomb攻击");
|
||||
}
|
||||
|
||||
Path newPath = safeResolve(targetDir, entry.getName());
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
Files.createDirectories(newPath);
|
||||
} else {
|
||||
Files.createDirectories(newPath.getParent());
|
||||
try (InputStream is = zipFile.getInputStream(entry);
|
||||
OutputStream os = Files.newOutputStream(newPath)) {
|
||||
|
||||
long bytesCopied = copyLimited(is, os, MAX_FILE_SIZE);
|
||||
totalUnzippedSize += bytesCopied;
|
||||
|
||||
if (totalUnzippedSize > MAX_TOTAL_SIZE) {
|
||||
throw new IOException("解压总大小超限,可能是zip bomb攻击");
|
||||
}
|
||||
}
|
||||
|
||||
// 解压完成后回调
|
||||
if (afterExtract != null) {
|
||||
afterExtract.accept(newPath.toFile());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全解析路径,防止Zip Slip攻击
|
||||
*
|
||||
* @param targetDir
|
||||
* @param entryName
|
||||
* @return
|
||||
* @throws IOException
|
||||
* @author chenrui
|
||||
* @date 2025/4/28 16:46
|
||||
*/
|
||||
private static Path safeResolve(Path targetDir, String entryName) throws IOException {
|
||||
Path resolvedPath = targetDir.resolve(entryName).normalize();
|
||||
if (!resolvedPath.startsWith(targetDir)) {
|
||||
throw new IOException("ZIP 路径穿越攻击被阻止:" + entryName);
|
||||
}
|
||||
return resolvedPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制输入流到输出流,并限制最大字节数
|
||||
*
|
||||
* @param in
|
||||
* @param out
|
||||
* @param maxBytes
|
||||
* @return
|
||||
* @throws IOException
|
||||
* @author chenrui
|
||||
* @date 2025/4/28 17:03
|
||||
*/
|
||||
private static long copyLimited(InputStream in, OutputStream out, long maxBytes) throws IOException {
|
||||
byte[] buffer = new byte[8192];
|
||||
long totalCopied = 0;
|
||||
int bytesRead;
|
||||
while ((bytesRead = in.read(buffer)) != -1) {
|
||||
totalCopied += bytesRead;
|
||||
if (totalCopied > maxBytes) {
|
||||
throw new IOException("单个文件解压超限,可能是zip bomb攻击");
|
||||
}
|
||||
out.write(buffer, 0, bytesRead);
|
||||
}
|
||||
return totalCopied;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
package org.jeecg.modules.airag.llm.service.impl;
|
||||
|
||||
import com.alibaba.fastjson.JSONArray;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import dev.langchain4j.agent.tool.ToolSpecification;
|
||||
import dev.langchain4j.service.tool.ToolExecutor;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.common.constant.CommonConstant;
|
||||
import org.jeecg.common.util.DateUtils;
|
||||
import org.jeecg.common.util.SpringContextUtils;
|
||||
import org.jeecg.common.util.TokenUtils;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.modules.airag.flow.consts.FlowConsts;
|
||||
import org.jeecg.modules.airag.llm.consts.FlowPluginContent;
|
||||
import org.jeecg.modules.airag.llm.entity.AiragKnowledge;
|
||||
import org.jeecg.modules.airag.llm.entity.AiragMcp;
|
||||
import org.jeecg.modules.airag.llm.handler.PluginToolBuilder;
|
||||
import org.jeecg.modules.airag.llm.mapper.AiragKnowledgeMapper;
|
||||
import org.jeecg.modules.airag.llm.service.IAiragKnowledgeService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @Description: AIRag知识库
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-02-18
|
||||
* @Version: V1.0
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class AiragKnowledgeServiceImpl extends ServiceImpl<AiragKnowledgeMapper, AiragKnowledge> implements IAiragKnowledgeService {
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getPluginMemory(String memoryId) {
|
||||
//step 1获取知识库
|
||||
AiragKnowledge airagKnowledge = this.baseMapper.selectById(memoryId);
|
||||
if(airagKnowledge == null){
|
||||
return null;
|
||||
}
|
||||
return this.getKnowledgeToPlugin(memoryId,airagKnowledge.getDescr());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取插件信息
|
||||
*
|
||||
* @param knowledgeId
|
||||
* @param descr
|
||||
* @return
|
||||
*/
|
||||
public Map<String, Object> getKnowledgeToPlugin(String knowledgeId, String descr) {
|
||||
//step1 构建插件
|
||||
log.info("开始构建记忆库插件");
|
||||
if (oConvertUtils.isEmpty(knowledgeId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
HttpServletRequest httpServletRequest = SpringContextUtils.getHttpServletRequest();
|
||||
|
||||
//返回数据
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
//插件
|
||||
//插件id
|
||||
AiragMcp tool = new AiragMcp();
|
||||
// 2. 构建插件
|
||||
tool.setId(knowledgeId);
|
||||
// 插件名称
|
||||
tool.setName(FlowPluginContent.PLUGIN_MEMORY_NAME);
|
||||
// 描述
|
||||
tool.setDescr(FlowPluginContent.PLUGIN_MEMORY_DESC);
|
||||
tool.setStatus(FlowConsts.FLOW_STATUS_ENABLE);
|
||||
tool.setSynced(CommonConstant.STATUS_1_INT);
|
||||
tool.setCategory("plugin");
|
||||
tool.setEndpoint("");
|
||||
|
||||
JSONArray toolsArray = new JSONArray();
|
||||
// 添加记忆
|
||||
toolsArray.add(buildAddMemoryTool(knowledgeId,descr));
|
||||
// 查询记忆
|
||||
toolsArray.add(buildQueryMemoryTool(knowledgeId,descr));
|
||||
tool.setTools(toolsArray.toJSONString());
|
||||
String tenantId = TokenUtils.getTenantIdByRequest(httpServletRequest);
|
||||
//构建元数据(请求头)
|
||||
String meataData = buildMetadata(tenantId);
|
||||
tool.setMetadata(meataData);
|
||||
Map<ToolSpecification, ToolExecutor> tools = PluginToolBuilder.buildTools(tool, httpServletRequest);
|
||||
result.put("pluginTool", tools);
|
||||
result.put("pluginId", knowledgeId);
|
||||
log.info("构建记忆库插件结束");
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建元数据
|
||||
*
|
||||
* @param tenantId
|
||||
*/
|
||||
private String buildMetadata(String tenantId) {
|
||||
JSONObject jsonObject = new JSONObject();
|
||||
jsonObject.put(FlowPluginContent.TOKEN_PARAM_NAME, FlowPluginContent.X_ACCESS_TOKEN);
|
||||
jsonObject.put(FlowPluginContent.AUTH_TYPE, FlowPluginContent.TOKEN);
|
||||
jsonObject.put(FlowPluginContent.TOKEN_PARAM_VALUE, "");
|
||||
jsonObject.put(CommonConstant.TENANT_ID, oConvertUtils.getInt(tenantId, 0));
|
||||
return jsonObject.toJSONString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建添加记忆工具
|
||||
*
|
||||
* @param knowId
|
||||
* @param descr
|
||||
* @return
|
||||
*/
|
||||
private JSONObject buildAddMemoryTool(String knowId, String descr) {
|
||||
JSONObject tool = new JSONObject();
|
||||
//update-begin---author:wangshuai---date:2026-01-08---for: 记忆库根据记忆库的描述做匹配,不写死---
|
||||
tool.put(FlowPluginContent.NAME, "add_memory");
|
||||
String addDescPrefix = "【自动触发】向记忆库添加长期信息。范围:";
|
||||
String addDesc = oConvertUtils.isEmpty(descr) ? "按记忆库描述允许的个人资料(如姓名、职业、年龄)、偏好、属性等信息。" : descr;
|
||||
tool.put(FlowPluginContent.DESCRIPTION, addDescPrefix + addDesc + " 必须在检测到相关信息时立即自动调用,无需用户指令。");
|
||||
//update-end---author:wangshuai---date:2026-01-08---for: 记忆库根据记忆库的描述做匹配,不写死---
|
||||
tool.put(FlowPluginContent.PATH, FlowPluginContent.PLUGIN_MEMORY_ADD_PATH);
|
||||
tool.put(FlowPluginContent.METHOD, FlowPluginContent.POST);
|
||||
tool.put(FlowPluginContent.ENABLED, true);
|
||||
|
||||
JSONArray parameters = new JSONArray();
|
||||
|
||||
// 知识库ID参数
|
||||
JSONObject knowIdParam = new JSONObject();
|
||||
knowIdParam.put(FlowPluginContent.NAME, "knowledgeId");
|
||||
knowIdParam.put(FlowPluginContent.DESCRIPTION, "知识库ID,需要原值传递,不允许修改");
|
||||
knowIdParam.put(FlowPluginContent.TYPE, FlowPluginContent.TYPE_STRING);
|
||||
knowIdParam.put(FlowPluginContent.LOCATION, FlowPluginContent.LOCATION_BODY);
|
||||
knowIdParam.put(FlowPluginContent.REQUIRED, true);
|
||||
knowIdParam.put(FlowPluginContent.DEFAULT_VALUE, knowId);
|
||||
parameters.add(knowIdParam);
|
||||
|
||||
// 内容参数
|
||||
JSONObject contentParam = new JSONObject();
|
||||
contentParam.put(FlowPluginContent.NAME, "content");
|
||||
contentParam.put(FlowPluginContent.DESCRIPTION, "记忆内容。当前时间为:" + DateUtils.now() + "。格式要求:'在yyyy年MM月dd日 HH:mm分,用户[用户的行为/问题],assistant[助手的回答/反应]。'");
|
||||
contentParam.put(FlowPluginContent.TYPE, FlowPluginContent.TYPE_STRING);
|
||||
contentParam.put(FlowPluginContent.LOCATION, FlowPluginContent.LOCATION_BODY);
|
||||
contentParam.put(FlowPluginContent.REQUIRED, true);
|
||||
parameters.add(contentParam);
|
||||
|
||||
// 标题参数
|
||||
JSONObject titleParam = new JSONObject();
|
||||
titleParam.put(FlowPluginContent.NAME, "title");
|
||||
titleParam.put(FlowPluginContent.DESCRIPTION, "记忆标题");
|
||||
titleParam.put(FlowPluginContent.TYPE, FlowPluginContent.TYPE_STRING);
|
||||
titleParam.put(FlowPluginContent.LOCATION, FlowPluginContent.LOCATION_BODY);
|
||||
titleParam.put(FlowPluginContent.REQUIRED, false);
|
||||
parameters.add(titleParam);
|
||||
tool.put(FlowPluginContent.PARAMETERS, parameters);
|
||||
|
||||
// 响应
|
||||
JSONArray responses = new JSONArray();
|
||||
tool.put(FlowPluginContent.RESPONSES, responses);
|
||||
|
||||
return tool;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建查询记忆工具
|
||||
*
|
||||
* @param knowId
|
||||
* @param descr
|
||||
* @return
|
||||
*/
|
||||
private JSONObject buildQueryMemoryTool(String knowId, String descr) {
|
||||
JSONObject tool = new JSONObject();
|
||||
//update-begin---author:wangshuai---date:2026-01-08---for: 记忆库根据记忆库的描述做匹配,不写死---
|
||||
String addDescPrefix = "【自动触发】向记忆库检索信息。范围:";
|
||||
String addDesc = oConvertUtils.isEmpty(descr) ? "按记忆库描述允许的个人资料(如姓名、职业、年龄)、偏好、属性等信息。" : descr;
|
||||
tool.put(FlowPluginContent.NAME, "query_memory");
|
||||
tool.put(FlowPluginContent.DESCRIPTION, addDescPrefix + addDesc + " 必须在检测到相关信息时立即自动调用,无需用户指令。");
|
||||
//update-end---author:wangshuai---date:2026-01-08---for: 记忆库根据记忆库的描述做匹配,不写死---
|
||||
tool.put(FlowPluginContent.PATH, FlowPluginContent.PLUGIN_MEMORY_QUERY_PATH);
|
||||
tool.put(FlowPluginContent.METHOD, FlowPluginContent.POST);
|
||||
tool.put(FlowPluginContent.ENABLED, true);
|
||||
|
||||
JSONArray parameters = new JSONArray();
|
||||
|
||||
// 知识库ID参数
|
||||
JSONObject knowIdParam = new JSONObject();
|
||||
knowIdParam.put(FlowPluginContent.NAME, "knowledgeId");
|
||||
knowIdParam.put(FlowPluginContent.DESCRIPTION, "知识库ID,需要原值传递,不允许修改");
|
||||
knowIdParam.put(FlowPluginContent.TYPE, FlowPluginContent.TYPE_STRING);
|
||||
knowIdParam.put(FlowPluginContent.LOCATION, FlowPluginContent.LOCATION_BODY);
|
||||
knowIdParam.put(FlowPluginContent.REQUIRED, true);
|
||||
knowIdParam.put(FlowPluginContent.DEFAULT_VALUE, knowId);
|
||||
parameters.add(knowIdParam);
|
||||
|
||||
// 查询内容参数
|
||||
JSONObject queryTextParam = new JSONObject();
|
||||
queryTextParam.put(FlowPluginContent.NAME, "queryText");
|
||||
queryTextParam.put(FlowPluginContent.DESCRIPTION, "查询内容");
|
||||
queryTextParam.put(FlowPluginContent.TYPE, FlowPluginContent.TYPE_STRING);
|
||||
queryTextParam.put(FlowPluginContent.LOCATION, FlowPluginContent.LOCATION_BODY);
|
||||
queryTextParam.put(FlowPluginContent.REQUIRED, true);
|
||||
parameters.add(queryTextParam);
|
||||
|
||||
tool.put(FlowPluginContent.PARAMETERS, parameters);
|
||||
// 响应
|
||||
JSONArray responses = new JSONArray();
|
||||
tool.put(FlowPluginContent.RESPONSES, responses);
|
||||
|
||||
return tool;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,420 @@
|
||||
package org.jeecg.modules.airag.llm.service.impl;
|
||||
|
||||
import com.alibaba.fastjson.JSONException;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import dev.langchain4j.agent.tool.ToolSpecification;
|
||||
import dev.langchain4j.mcp.client.DefaultMcpClient;
|
||||
import dev.langchain4j.mcp.client.McpClient;
|
||||
import dev.langchain4j.mcp.client.transport.http.HttpMcpTransport;
|
||||
import dev.langchain4j.mcp.client.transport.http.StreamableHttpMcpTransport;
|
||||
import dev.langchain4j.mcp.client.transport.stdio.StdioMcpTransport;
|
||||
import dev.langchain4j.model.chat.request.json.*;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.constant.CommonConstant;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.config.AiRagConfigBean;
|
||||
import org.jeecg.modules.airag.llm.entity.AiragMcp;
|
||||
import org.jeecg.modules.airag.llm.mapper.AiragMcpMapper;
|
||||
import org.jeecg.modules.airag.llm.service.IAiragMcpService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.time.Duration;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @Description: MCP
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-10-20
|
||||
* @Version: V1.0
|
||||
*/
|
||||
@Slf4j
|
||||
@SuppressWarnings("removal")
|
||||
@Service("airagMcpServiceImpl")
|
||||
public class AiragMcpServiceImpl extends ServiceImpl<AiragMcpMapper, AiragMcp> implements IAiragMcpService {
|
||||
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper; // 使用全局配置的 Jackson ObjectMapper
|
||||
@Autowired
|
||||
private AiRagConfigBean aiRagConfigBean;
|
||||
|
||||
/**
|
||||
* 新增或编辑Mcpserver
|
||||
*
|
||||
* @param airagMcp MCP对象
|
||||
* @return 返回保存后的MCP对象
|
||||
* @author chenrui
|
||||
* @date 2025/10/21
|
||||
*/
|
||||
@Override
|
||||
public Result<String> edit(AiragMcp airagMcp) {
|
||||
// 校验必填项
|
||||
if (airagMcp.getName() == null || airagMcp.getName().trim().isEmpty()) {
|
||||
return Result.error("名称不能为空");
|
||||
}
|
||||
//update-begin---author:chenrui ---date:20251031 for:[QQYUN-12453]【AI】支持插件------------
|
||||
// 设置默认category
|
||||
if (oConvertUtils.isEmpty(airagMcp.getCategory())) {
|
||||
airagMcp.setCategory("mcp");
|
||||
}
|
||||
// 对于MCP类型,需要校验type和endpoint
|
||||
if ("mcp".equalsIgnoreCase(airagMcp.getCategory())) {
|
||||
if (airagMcp.getType() == null || airagMcp.getType().trim().isEmpty()) {
|
||||
return Result.error("MCP类型不能为空");
|
||||
}
|
||||
if (airagMcp.getEndpoint() == null || airagMcp.getEndpoint().trim().isEmpty()) {
|
||||
return Result.error("服务端点不能为空");
|
||||
}
|
||||
} else if ("plugin".equalsIgnoreCase(airagMcp.getCategory())) {
|
||||
// 对于插件类型,BaseURL可选,不填时使用当前系统地址
|
||||
// 不再校验endpoint是否为空
|
||||
} else {
|
||||
// 未知类型,默认为MCP并校验
|
||||
if (airagMcp.getEndpoint() == null || airagMcp.getEndpoint().trim().isEmpty()) {
|
||||
return Result.error("服务端点不能为空");
|
||||
}
|
||||
}
|
||||
//update-end---author:chenrui ---date:20251031 for:[QQYUN-12453]【AI】支持插件------------
|
||||
|
||||
if (airagMcp.getId() == null || airagMcp.getId().trim().isEmpty()) {
|
||||
// 设置默认值
|
||||
airagMcp.setStatus("enable");
|
||||
//update-begin---author:chenrui ---date:20251031 for:[QQYUN-12453]【AI】支持插件------------
|
||||
// 只有MCP类型才设置synced字段,插件类型不需要同步默认为已同步
|
||||
if ("mcp".equalsIgnoreCase(airagMcp.getCategory())) {
|
||||
airagMcp.setSynced(CommonConstant.STATUS_0_INT);
|
||||
} else {
|
||||
airagMcp.setSynced(CommonConstant.STATUS_1_INT);
|
||||
}
|
||||
//update-end---author:chenrui ---date:20251031 for:[QQYUN-12453]【AI】支持插件------------
|
||||
// 新增
|
||||
this.save(airagMcp);
|
||||
} else {
|
||||
// 编辑
|
||||
this.updateById(airagMcp);
|
||||
}
|
||||
return Result.OK("保存成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步mcp的工具列表
|
||||
*
|
||||
* @param id mcp主键
|
||||
* @return 工具列表
|
||||
* @author chenrui
|
||||
* @date 2025/10/21
|
||||
*/
|
||||
@Override
|
||||
public Result<?> sync(String id) {
|
||||
AiragMcp mcp = this.getById(id);
|
||||
if (mcp == null) {
|
||||
return Result.error("未找到对应的MCP对象");
|
||||
}
|
||||
//update-begin---author:chenrui ---date:20251031 for:[QQYUN-12453]【AI】支持插件------------
|
||||
// 只有MCP类型才支持同步,插件类型不支持
|
||||
String category = mcp.getCategory();
|
||||
if (oConvertUtils.isEmpty(category)) {
|
||||
category = "mcp"; // 兼容旧数据
|
||||
}
|
||||
if (!"mcp".equalsIgnoreCase(category)) {
|
||||
return Result.error("只有MCP类型才支持同步操作");
|
||||
}
|
||||
//update-end---author:chenrui ---date:20251031 for:[QQYUN-12453]【AI】支持插件------------
|
||||
String type = mcp.getType();
|
||||
String endpoint = mcp.getEndpoint();
|
||||
Map<String, String> headers = null;
|
||||
if (oConvertUtils.isNotEmpty(mcp.getHeaders())) {
|
||||
try {
|
||||
headers = JSONObject.parseObject(mcp.getHeaders(), new com.alibaba.fastjson.TypeReference<Map<String, String>>() {});
|
||||
} catch (JSONException e) {
|
||||
headers = null;
|
||||
}
|
||||
}
|
||||
if (type == null || endpoint == null) {
|
||||
return Result.error("MCP类型或端点为空");
|
||||
}
|
||||
McpClient mcpClient = null;
|
||||
try {
|
||||
if ("sse".equalsIgnoreCase(type)) {
|
||||
//TODO 1.4.0-beta10被弃用,推荐使用http
|
||||
log.info("[MCP]使用SSE协议(HttpMcpTransport), endpoint:{}", endpoint);
|
||||
HttpMcpTransport.Builder builder = HttpMcpTransport.builder()
|
||||
.sseUrl(endpoint)
|
||||
.logRequests(true)
|
||||
.logResponses(true);
|
||||
if (headers != null && !headers.isEmpty()) {
|
||||
builder.customHeaders(headers);
|
||||
}
|
||||
mcpClient = new DefaultMcpClient.Builder().transport(builder.build()).build();
|
||||
} else if ("stdio".equalsIgnoreCase(type)) {
|
||||
//update-begin---author:wangshuai---date:2025-12-18---for:【QQYUN-14242】【AI】添加参数控制 是否开启 默认禁用 stdio 调用执行命令---
|
||||
String openSafe = aiRagConfigBean.getAllowSensitiveNodes();
|
||||
if(oConvertUtils.isNotEmpty(openSafe) && openSafe.toLowerCase().contains("stdio")) {
|
||||
log.info("[MCP]使用STDIO协议(StdioMcpTransport), endpoint:{}", endpoint);
|
||||
// stdio 类型:endpoint 可能是一个命令行
|
||||
// Windows 下需要通过 cmd.exe /c 来执行命令,否则找不到 npx 等程序
|
||||
List<String> cmdParts;
|
||||
String os = System.getProperty("os.name").toLowerCase();
|
||||
if (os.contains("win")) {
|
||||
// Windows: 使用 cmd.exe /c 执行
|
||||
cmdParts = new ArrayList<>();
|
||||
cmdParts.add("cmd.exe");
|
||||
cmdParts.add("/c");
|
||||
cmdParts.add(endpoint.trim());
|
||||
} else {
|
||||
// Linux/Mac: 使用 sh -c 执行
|
||||
cmdParts = new ArrayList<>();
|
||||
cmdParts.add("sh");
|
||||
cmdParts.add("-c");
|
||||
cmdParts.add(endpoint.trim());
|
||||
}
|
||||
log.info("[MCP]执行stdio命令: {}", cmdParts);
|
||||
StdioMcpTransport.Builder builder = new StdioMcpTransport.Builder()
|
||||
.command(cmdParts)
|
||||
.environment(headers);
|
||||
mcpClient = new DefaultMcpClient.Builder().transport(builder.build()).build();
|
||||
} else {
|
||||
String disabledMsg = "stdio 功能已禁用。若需启用,请在 yml 的 jeecg.airag.allow-sensitive-nodes 中加入 stdio。";
|
||||
log.warn("[MCP]{}", disabledMsg);
|
||||
return Result.error(disabledMsg);
|
||||
}
|
||||
//update-end---author:wangshuai---date:2025-12-19---for:【QQYUN-14242】【AI】添加参数控制 是否开启 默认禁用 stdio 调用执行命令---
|
||||
}else if("http".equalsIgnoreCase(type)){
|
||||
log.info("[MCP]使用HTTP协议(StreamableHttpMcpTransport), endpoint:{}", endpoint);
|
||||
//增加http选项
|
||||
mcpClient = mcpHttpCreate(endpoint,headers);
|
||||
} else {
|
||||
return Result.error("不支持的MCP类型:" + type);
|
||||
}
|
||||
List<ToolSpecification> toolSpecifications = mcpClient.listTools();
|
||||
// 先尝试直接使用 ObjectMapper 序列化,若结果为 {} 则回退到反射 Map
|
||||
List<Map<String, Object>> specMaps = toolSpecifications.stream()
|
||||
.map(spec -> {
|
||||
try {
|
||||
String raw = objectMapper.writeValueAsString(spec);
|
||||
if (raw != null && raw.length() > 2) {
|
||||
// 直接反序列化成 Map,保留 Jackson 认出的字段
|
||||
return objectMapper.readValue(raw, new TypeReference<Map<String, Object>>() {
|
||||
});
|
||||
}
|
||||
} catch (Exception ignore) {
|
||||
}
|
||||
return convertToolSpec(spec);
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
String jsonList;
|
||||
try {
|
||||
jsonList = objectMapper.writeValueAsString(specMaps);
|
||||
} catch (JsonProcessingException e) {
|
||||
jsonList = JSONObject.toJSONString(specMaps);
|
||||
}
|
||||
String firstJson = specMaps.isEmpty() ? "null" : safeWriteJson(specMaps.get(0));
|
||||
log.info("MCP工具列表 id={}, size={}, first={}", id, toolSpecifications.size(), firstJson);
|
||||
mcp.setTools(jsonList);
|
||||
mcp.setSynced(1);
|
||||
|
||||
Map<String,Object> metadata = new HashMap<>();
|
||||
metadata.put("tool_count", toolSpecifications.size());
|
||||
mcp.setMetadata(objectMapper.writeValueAsString(metadata));
|
||||
this.updateById(mcp);
|
||||
return Result.OK(specMaps);
|
||||
} catch (Exception e) {
|
||||
String message = e.getMessage();
|
||||
if (e instanceof IllegalArgumentException) {
|
||||
message = ",MCP客户端参数错误";
|
||||
}
|
||||
log.error("同步MCP工具失败 id={}, error={}", id, message, e);
|
||||
return Result.error("同步失败" + message);
|
||||
} finally {
|
||||
if (mcpClient != null) {
|
||||
try {
|
||||
Method closeMethod = mcpClient.getClass().getMethod("close");
|
||||
closeMethod.invoke(mcpClient);
|
||||
} catch (NoSuchMethodException ignore) {
|
||||
} catch (Exception ex) {
|
||||
log.warn("关闭MCP客户端失败 id={}, error={}", id, ex.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* mcp插件http创建
|
||||
*
|
||||
* @param endpoint
|
||||
* @param headers
|
||||
* @return
|
||||
*/
|
||||
private McpClient mcpHttpCreate(String endpoint, Map<String, String> headers) {
|
||||
StreamableHttpMcpTransport.Builder builder = new StreamableHttpMcpTransport.Builder()
|
||||
.url(endpoint)
|
||||
.timeout(Duration.ofMinutes(60))
|
||||
.logRequests(true)
|
||||
.logResponses(true);
|
||||
|
||||
if (headers != null && !headers.isEmpty()) {
|
||||
builder.customHeaders(headers);
|
||||
}
|
||||
|
||||
return new DefaultMcpClient.Builder()
|
||||
.transport(builder.build())
|
||||
.build();
|
||||
}
|
||||
|
||||
// 安全序列化单个对象为 JSON 字符串
|
||||
private String safeWriteJson(Object obj) {
|
||||
try {
|
||||
return objectMapper.writeValueAsString(obj);
|
||||
} catch (Exception e) {
|
||||
return String.valueOf(obj);
|
||||
}
|
||||
}
|
||||
|
||||
// 反射将 ToolSpecification 转成 Map,兼容 record/私有字段/仅 Jackson 注解场景 -> 改为直接调用访问器
|
||||
private Map<String, Object> convertToolSpec(ToolSpecification spec) {
|
||||
Map<String, Object> map = new LinkedHashMap<>();
|
||||
if (spec == null) {
|
||||
return map;
|
||||
}
|
||||
map.put("name", spec.name());
|
||||
map.put("description", spec.description());
|
||||
try {
|
||||
Object params = spec.parameters();
|
||||
if (params != null) {
|
||||
JsonObjectSchema obj = (JsonObjectSchema) params;
|
||||
List<Map<String, Object>> fields = new ArrayList<>();
|
||||
if (obj.properties() != null) {
|
||||
obj.properties().forEach((fieldName, fieldSchema) -> {
|
||||
Map<String, Object> fieldMap = new LinkedHashMap<>();
|
||||
fieldMap.put("name", fieldName);
|
||||
fieldMap.put("description", extractDescription(fieldSchema));
|
||||
// 若需要标记必填
|
||||
if (obj.required() != null && obj.required().contains(fieldName)) {
|
||||
fieldMap.put("required", true);
|
||||
}
|
||||
fields.add(fieldMap);
|
||||
});
|
||||
}
|
||||
map.put("parameters", fields);
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// 提取各类型 schema 的描述
|
||||
private String extractDescription(Object schema) {
|
||||
if (schema == null) return null;
|
||||
try {
|
||||
if (schema instanceof JsonStringSchema) return ((JsonStringSchema) schema).description();
|
||||
if (schema instanceof JsonNumberSchema) return ((JsonNumberSchema) schema).description();
|
||||
if (schema instanceof JsonBooleanSchema) return ((JsonBooleanSchema) schema).description();
|
||||
if (schema instanceof JsonArraySchema) return ((JsonArraySchema) schema).description();
|
||||
if (schema instanceof JsonEnumSchema) return ((JsonEnumSchema) schema).description();
|
||||
if (schema instanceof JsonObjectSchema) return ((JsonObjectSchema) schema).description();
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
return schema.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改状态
|
||||
*
|
||||
* @param id MCP主键
|
||||
* @param action 操作(enable/disable)
|
||||
* @return 操作结果
|
||||
* @author chenrui
|
||||
* @date 2025/10/21 11:00
|
||||
*/
|
||||
@Override
|
||||
public Result<?> toggleStatus(String id, String action) {
|
||||
if (oConvertUtils.isEmpty(id)) {
|
||||
return Result.error("id不能为空");
|
||||
}
|
||||
if (oConvertUtils.isEmpty(action)) {
|
||||
return Result.error("action不能为空");
|
||||
}
|
||||
String normalized = action.toLowerCase();
|
||||
if (!"enable".equals(normalized) && !"disable".equals(normalized)) {
|
||||
return Result.error("action只能为enable或disable");
|
||||
}
|
||||
AiragMcp mcp = this.getById(id);
|
||||
if (mcp == null) {
|
||||
return Result.error("未找到对应的MCP服务");
|
||||
}
|
||||
if (normalized.equalsIgnoreCase(mcp.getStatus())) {
|
||||
return Result.OK("操作成功");
|
||||
}
|
||||
mcp.setStatus(normalized);
|
||||
this.updateById(mcp);
|
||||
return Result.OK("操作成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存插件工具(仅更新tools字段)
|
||||
* for [QQYUN-12453]【AI】支持插件
|
||||
* @param id 插件ID
|
||||
* @param tools 工具列表JSON字符串
|
||||
* @return 操作结果
|
||||
* @author chenrui
|
||||
* @date 2025/10/30
|
||||
*/
|
||||
@Override
|
||||
public Result<String> saveTools(String id, String tools) {
|
||||
if (oConvertUtils.isEmpty(id)) {
|
||||
return Result.error("插件ID不能为空");
|
||||
}
|
||||
AiragMcp mcp = this.getById(id);
|
||||
if (mcp == null) {
|
||||
return Result.error("未找到对应的插件");
|
||||
}
|
||||
// 验证是否为插件类型
|
||||
String category = mcp.getCategory();
|
||||
if (oConvertUtils.isEmpty(category)) {
|
||||
category = "mcp"; // 兼容旧数据
|
||||
}
|
||||
if (!"plugin".equalsIgnoreCase(category)) {
|
||||
return Result.error("只有插件类型才能保存工具");
|
||||
}
|
||||
// 更新tools字段
|
||||
mcp.setTools(tools);
|
||||
|
||||
// 更新metadata中的tool_count
|
||||
try {
|
||||
com.alibaba.fastjson.JSONArray toolsArray = com.alibaba.fastjson.JSONArray.parseArray(tools);
|
||||
int toolCount = toolsArray != null ? toolsArray.size() : 0;
|
||||
|
||||
// 解析现有metadata
|
||||
JSONObject metadata = new JSONObject();
|
||||
if (oConvertUtils.isNotEmpty(mcp.getMetadata())) {
|
||||
try {
|
||||
JSONObject metadataJson = JSONObject.parseObject(mcp.getMetadata());
|
||||
if (metadataJson != null) {
|
||||
metadata.putAll(metadataJson);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("解析metadata失败,将重新创建: {}", mcp.getMetadata());
|
||||
}
|
||||
}
|
||||
|
||||
// 更新tool_count
|
||||
metadata.put("tool_count", toolCount);
|
||||
|
||||
// 保存metadata
|
||||
mcp.setMetadata(metadata.toJSONString());
|
||||
} catch (Exception e) {
|
||||
log.warn("更新工具数量失败: {}", e.getMessage());
|
||||
// 即使更新tool_count失败,也不影响保存tools
|
||||
}
|
||||
|
||||
this.updateById(mcp);
|
||||
return Result.OK("保存成功");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package org.jeecg.modules.airag.llm.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import org.jeecg.modules.airag.llm.entity.AiragModel;
|
||||
import org.jeecg.modules.airag.llm.mapper.AiragModelMapper;
|
||||
import org.jeecg.modules.airag.llm.service.IAiragModelService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* @Description: AiRag模型配置
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-02-14
|
||||
* @Version: V1.0
|
||||
*/
|
||||
@Service
|
||||
public class AiragModelServiceImpl extends ServiceImpl<AiragModelMapper, AiragModel> implements IAiragModelService {
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package org.jeecg.modules.airag.ocr.controller;
|
||||
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import org.apache.commons.collections.CollectionUtils;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.util.RedisUtil;
|
||||
import org.jeecg.modules.airag.ocr.entity.AiOcr;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/airag/ocr")
|
||||
public class AiOcrController {
|
||||
|
||||
@Autowired
|
||||
private RedisUtil redisUtil;
|
||||
|
||||
private static final String AI_OCR_REDIS_KEY = "airag:ocr";
|
||||
|
||||
@GetMapping("/list")
|
||||
public Result<?> list(){
|
||||
Object aiOcr = redisUtil.get(AI_OCR_REDIS_KEY);
|
||||
IPage<AiOcr> page = new Page<>(1,10);
|
||||
if(null != aiOcr){
|
||||
List<AiOcr> aiOcrList = JSONObject.parseArray(aiOcr.toString(), AiOcr.class);
|
||||
page.setRecords(aiOcrList);
|
||||
page.setTotal(aiOcrList.size());
|
||||
page.setPages(aiOcrList.size());
|
||||
}
|
||||
return Result.OK(page);
|
||||
}
|
||||
|
||||
@PostMapping("/add")
|
||||
public Result<String> add(@RequestBody AiOcr aiOcr){
|
||||
Object aiOcrList = redisUtil.get(AI_OCR_REDIS_KEY);
|
||||
aiOcr.setId(UUID.randomUUID().toString().replace("-",""));
|
||||
if(null == aiOcrList){
|
||||
List<AiOcr> list = new ArrayList<>();
|
||||
list.add(aiOcr);
|
||||
redisUtil.set(AI_OCR_REDIS_KEY, JSONObject.toJSONString(list));
|
||||
}else{
|
||||
List<AiOcr> aiOcrs = JSONObject.parseArray(aiOcrList.toString(), AiOcr.class);
|
||||
aiOcrs.add(aiOcr);
|
||||
redisUtil.set(AI_OCR_REDIS_KEY,JSONObject.toJSONString(aiOcrs));
|
||||
}
|
||||
return Result.OK("添加成功");
|
||||
}
|
||||
|
||||
@PutMapping("/edit")
|
||||
public Result<String> updateById(@RequestBody AiOcr aiOcr){
|
||||
Object aiOcrList = redisUtil.get(AI_OCR_REDIS_KEY);
|
||||
if(null != aiOcrList){
|
||||
List<AiOcr> aiOcrs = JSONObject.parseArray(aiOcrList.toString(), AiOcr.class);
|
||||
aiOcrs.forEach(item->{
|
||||
if(item.getId().equals(aiOcr.getId())){
|
||||
BeanUtils.copyProperties(aiOcr,item);
|
||||
}
|
||||
});
|
||||
redisUtil.set(AI_OCR_REDIS_KEY,JSONObject.toJSONString(aiOcrs));
|
||||
}else{
|
||||
return Result.OK("编辑失败,未找到该数据");
|
||||
}
|
||||
return Result.OK("编辑成功");
|
||||
}
|
||||
|
||||
@DeleteMapping("/deleteById")
|
||||
public Result<String> deleteById(@RequestBody AiOcr aiOcr){
|
||||
Object aiOcrObj = redisUtil.get(AI_OCR_REDIS_KEY);
|
||||
if(null != aiOcrObj){
|
||||
List<AiOcr> aiOcrs = JSONObject.parseArray(aiOcrObj.toString(), AiOcr.class);
|
||||
List<AiOcr> aiOcrList = new ArrayList<>();
|
||||
for(AiOcr ocr: aiOcrs){
|
||||
if(!ocr.getId().equals(aiOcr.getId())){
|
||||
aiOcrList.add(ocr);
|
||||
}
|
||||
}
|
||||
if(CollectionUtils.isNotEmpty(aiOcrList)){
|
||||
redisUtil.set(AI_OCR_REDIS_KEY,JSONObject.toJSONString(aiOcrList));
|
||||
}else{
|
||||
redisUtil.removeAll(AI_OCR_REDIS_KEY);
|
||||
}
|
||||
}else{
|
||||
return Result.OK("删除失败,未找到该数据");
|
||||
}
|
||||
return Result.OK("删除成功");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package org.jeecg.modules.airag.ocr.entity;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* @Description: OCR识别实体类
|
||||
*
|
||||
* @author: wangshuai
|
||||
* @date: 2025/4/16 17:01
|
||||
*/
|
||||
@Data
|
||||
public class AiOcr {
|
||||
|
||||
/**
|
||||
* 编号
|
||||
*/
|
||||
private String id;
|
||||
|
||||
/**
|
||||
* 标题
|
||||
*/
|
||||
private String title;
|
||||
|
||||
/**
|
||||
* 提示词
|
||||
*/
|
||||
private String prompt;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package org.jeecg.modules.airag.prompts.consts;
|
||||
|
||||
/**
|
||||
* AI提示词常量类
|
||||
*
|
||||
*/
|
||||
public class AiPromptsConsts {
|
||||
|
||||
/**
|
||||
* 状态:进行中
|
||||
*/
|
||||
public static final String STATUS_RUNNING = "run";
|
||||
/**
|
||||
* 状态:完成
|
||||
*/
|
||||
public static final String STATUS_COMPLETED = "completed";
|
||||
/**
|
||||
* 状态:失败
|
||||
*/
|
||||
public static final String STATUS_FAILED = "failed";
|
||||
/**
|
||||
* 业务类型:评估器
|
||||
*/
|
||||
public static final String BIZ_TYPE_EVALUATOR = "evaluator";
|
||||
/**
|
||||
* 业务类型:轨迹
|
||||
*/
|
||||
public static final String BIZ_TYPE_TRACK = "track";
|
||||
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
package org.jeecg.modules.airag.prompts.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.aspect.annotation.AutoLog;
|
||||
import org.jeecg.common.system.base.controller.JeecgController;
|
||||
import org.jeecg.common.system.query.QueryGenerator;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.modules.airag.prompts.consts.AiPromptsConsts;
|
||||
import org.jeecg.modules.airag.prompts.entity.AiragExtData;
|
||||
import org.jeecg.modules.airag.prompts.service.IAiragExtDataService;
|
||||
import org.jeecg.modules.airag.prompts.vo.AiragDebugVo;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @Description: airag_ext_data
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-12-24
|
||||
* @Version: V1.0
|
||||
*/
|
||||
@Tag(name="airag_ext_data")
|
||||
@RestController
|
||||
@RequestMapping("/airag/extData")
|
||||
@Slf4j
|
||||
public class AiragExtDataController extends JeecgController<AiragExtData, IAiragExtDataService> {
|
||||
@Autowired
|
||||
private IAiragExtDataService airagExtDataService;
|
||||
|
||||
/**
|
||||
* 分页列表查询
|
||||
*
|
||||
* @param airagExtData
|
||||
* @param pageNo
|
||||
* @param pageSize
|
||||
* @param req
|
||||
* @return
|
||||
*/
|
||||
//@AutoLog(value = "airag_ext_data-分页列表查询")
|
||||
@Operation(summary="airag_ext_data-分页列表查询")
|
||||
@GetMapping(value = "/list")
|
||||
public Result<IPage<AiragExtData>> queryPageList(AiragExtData airagExtData,
|
||||
@RequestParam(name="pageNo", defaultValue="1") Integer pageNo,
|
||||
@RequestParam(name="pageSize", defaultValue="10") Integer pageSize,
|
||||
HttpServletRequest req) {
|
||||
QueryWrapper<AiragExtData> queryWrapper = QueryGenerator.initQueryWrapper(airagExtData, req.getParameterMap());
|
||||
Page<AiragExtData> page = new Page<AiragExtData>(pageNo, pageSize);
|
||||
queryWrapper.eq("biz_type", AiPromptsConsts.BIZ_TYPE_EVALUATOR);
|
||||
IPage<AiragExtData> pageList = airagExtDataService.page(page, queryWrapper);
|
||||
return Result.OK(pageList);
|
||||
}
|
||||
/**
|
||||
* 调用轨迹列表查询
|
||||
*
|
||||
* @param airagExtData
|
||||
* @param pageNo
|
||||
* @param pageSize
|
||||
* @param req
|
||||
* @return
|
||||
*/
|
||||
@Operation(summary="airag_ext_data-分页列表查询")
|
||||
@GetMapping(value = "/getTrackList")
|
||||
public Result<IPage<AiragExtData>> getTrackList(AiragExtData airagExtData,
|
||||
@RequestParam(name="pageNo", defaultValue="1") Integer pageNo,
|
||||
@RequestParam(name="pageSize", defaultValue="10") Integer pageSize,
|
||||
HttpServletRequest req) {
|
||||
QueryWrapper<AiragExtData> queryWrapper = QueryGenerator.initQueryWrapper(airagExtData, req.getParameterMap());
|
||||
Page<AiragExtData> page = new Page<AiragExtData>(pageNo, pageSize);
|
||||
queryWrapper.eq("biz_type", AiPromptsConsts.BIZ_TYPE_TRACK);
|
||||
String metadata = airagExtData.getMetadata();
|
||||
if(oConvertUtils.isEmpty(metadata)){
|
||||
return Result.OK();
|
||||
}
|
||||
IPage<AiragExtData> pageList = airagExtDataService.page(page, queryWrapper);
|
||||
return Result.OK(pageList);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加
|
||||
*
|
||||
* @param airagExtData
|
||||
* @return
|
||||
*/
|
||||
@AutoLog(value = "airag_ext_data-添加")
|
||||
@Operation(summary="airag_ext_data-添加")
|
||||
@PostMapping(value = "/add")
|
||||
public Result<String> add(@RequestBody AiragExtData airagExtData) {
|
||||
airagExtData.setBizType(AiPromptsConsts.BIZ_TYPE_EVALUATOR);
|
||||
airagExtDataService.save(airagExtData);
|
||||
return Result.OK("添加成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑
|
||||
*
|
||||
* @param airagExtData
|
||||
* @return
|
||||
*/
|
||||
@AutoLog(value = "airag_ext_data-编辑")
|
||||
@Operation(summary="airag_ext_data-编辑")
|
||||
@RequestMapping(value = "/edit", method = {RequestMethod.PUT,RequestMethod.POST})
|
||||
public Result<String> edit(@RequestBody AiragExtData airagExtData) {
|
||||
airagExtDataService.updateById(airagExtData);
|
||||
return Result.OK("编辑成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过id删除
|
||||
*
|
||||
* @param id
|
||||
* @return
|
||||
*/
|
||||
@AutoLog(value = "airag_ext_data-通过id删除")
|
||||
@Operation(summary="airag_ext_data-通过id删除")
|
||||
@DeleteMapping(value = "/delete")
|
||||
public Result<String> delete(@RequestParam(name="id",required=true) String id) {
|
||||
airagExtDataService.removeById(id);
|
||||
return Result.OK("删除成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除
|
||||
*
|
||||
* @param ids
|
||||
* @return
|
||||
*/
|
||||
@AutoLog(value = "airag_ext_data-批量删除")
|
||||
@Operation(summary="airag_ext_data-批量删除")
|
||||
@DeleteMapping(value = "/deleteBatch")
|
||||
public Result<String> deleteBatch(@RequestParam(name="ids",required=true) String ids) {
|
||||
this.airagExtDataService.removeByIds(Arrays.asList(ids.split(",")));
|
||||
return Result.OK("批量删除成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过id查询
|
||||
*
|
||||
* @param id
|
||||
* @return
|
||||
*/
|
||||
//@AutoLog(value = "airag_ext_data-通过id查询")
|
||||
@Operation(summary="airag_ext_data-通过id查询")
|
||||
@GetMapping(value = "/queryById")
|
||||
public Result<AiragExtData> queryById(@RequestParam(name="id",required=true) String id) {
|
||||
AiragExtData airagExtData = airagExtDataService.getById(id);
|
||||
if(airagExtData==null) {
|
||||
return Result.error("未找到对应数据");
|
||||
}
|
||||
return Result.OK(airagExtData);
|
||||
}
|
||||
/**
|
||||
* 通过id查询
|
||||
*
|
||||
* @param id
|
||||
* @return
|
||||
*/
|
||||
//@AutoLog(value = "airag_ext_data-通过id查询")
|
||||
@Operation(summary="airag_ext_data-通过id查询")
|
||||
@GetMapping(value = "/queryTrackById")
|
||||
public Result<List<AiragExtData>> queryTrackById(@RequestParam(name="id",required=true) String id) {
|
||||
AiragExtData airagExtData = airagExtDataService.getById(id);
|
||||
String status = airagExtData.getStatus();
|
||||
if(AiPromptsConsts.STATUS_RUNNING.equals(status)) {
|
||||
return Result.error("处理中,请稍后刷新");
|
||||
}
|
||||
List<AiragExtData> trackList = airagExtDataService.queryTrackById(id);
|
||||
return Result.OK(trackList);
|
||||
}
|
||||
/**
|
||||
* 构造器调试
|
||||
*
|
||||
* @param debugVo
|
||||
* @return
|
||||
*/
|
||||
@PostMapping(value = "/evaluator/debug")
|
||||
public Result<?> debugEvaluator(@RequestBody AiragDebugVo debugVo) {
|
||||
return airagExtDataService.debugEvaluator(debugVo);
|
||||
}
|
||||
/**
|
||||
* 导出excel
|
||||
*
|
||||
* @param request
|
||||
* @param airagExtData
|
||||
*/
|
||||
@RequestMapping(value = "/exportXls")
|
||||
public ModelAndView exportXls(HttpServletRequest request, AiragExtData airagExtData) {
|
||||
return super.exportXls(request, airagExtData, AiragExtData.class, "airag_ext_data");
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过excel导入数据
|
||||
*
|
||||
* @param request
|
||||
* @param response
|
||||
* @return
|
||||
*/
|
||||
@RequestMapping(value = "/importExcel", method = RequestMethod.POST)
|
||||
public Result<?> importExcel(HttpServletRequest request, HttpServletResponse response) {
|
||||
return super.importExcel(request, response, AiragExtData.class);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
package org.jeecg.modules.airag.prompts.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.aspect.annotation.AutoLog;
|
||||
import org.jeecg.common.constant.CommonConstant;
|
||||
import org.jeecg.common.system.base.controller.JeecgController;
|
||||
import org.jeecg.common.system.query.QueryGenerator;
|
||||
import org.jeecg.modules.airag.prompts.entity.AiragPrompts;
|
||||
import org.jeecg.modules.airag.prompts.service.IAiragPromptsService;
|
||||
import org.jeecg.modules.airag.prompts.vo.AiragExperimentVo;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
||||
import java.util.Arrays;
|
||||
/**
|
||||
* @Description: airag_prompts
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-12-24
|
||||
* @Version: V1.0
|
||||
*/
|
||||
@Tag(name="airag_prompts")
|
||||
@RestController
|
||||
@RequestMapping("/airag/prompts")
|
||||
@Slf4j
|
||||
public class AiragPromptsController extends JeecgController<AiragPrompts, IAiragPromptsService> {
|
||||
@Autowired
|
||||
private IAiragPromptsService airagPromptsService;
|
||||
|
||||
/**
|
||||
* 分页列表查询
|
||||
*
|
||||
* @param airagPrompts
|
||||
* @param pageNo
|
||||
* @param pageSize
|
||||
* @param req
|
||||
* @return
|
||||
*/
|
||||
//@AutoLog(value = "airag_prompts-分页列表查询")
|
||||
@Operation(summary="airag_prompts-分页列表查询")
|
||||
@GetMapping(value = "/list")
|
||||
public Result<IPage<AiragPrompts>> queryPageList(AiragPrompts airagPrompts,
|
||||
@RequestParam(name="pageNo", defaultValue="1") Integer pageNo,
|
||||
@RequestParam(name="pageSize", defaultValue="10") Integer pageSize,
|
||||
HttpServletRequest req) {
|
||||
QueryWrapper<AiragPrompts> queryWrapper = QueryGenerator.initQueryWrapper(airagPrompts, req.getParameterMap());
|
||||
Page<AiragPrompts> page = new Page<AiragPrompts>(pageNo, pageSize);
|
||||
IPage<AiragPrompts> pageList = airagPromptsService.page(page, queryWrapper);
|
||||
return Result.OK(pageList);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加
|
||||
*
|
||||
* @param airagPrompts
|
||||
* @return
|
||||
*/
|
||||
@AutoLog(value = "airag_prompts-添加")
|
||||
@Operation(summary="airag_prompts-添加")
|
||||
@PostMapping(value = "/add")
|
||||
public Result<String> add(@RequestBody AiragPrompts airagPrompts) {
|
||||
airagPrompts.setDelFlag(CommonConstant.DEL_FLAG_0);
|
||||
airagPrompts.setStatus("0");
|
||||
airagPromptsService.save(airagPrompts);
|
||||
return Result.OK("添加成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑
|
||||
*
|
||||
* @param airagPrompts
|
||||
* @return
|
||||
*/
|
||||
@AutoLog(value = "airag_prompts-编辑")
|
||||
@Operation(summary="airag_prompts-编辑")
|
||||
@RequestMapping(value = "/edit", method = {RequestMethod.PUT,RequestMethod.POST})
|
||||
public Result<String> edit(@RequestBody AiragPrompts airagPrompts) {
|
||||
airagPromptsService.updateById(airagPrompts);
|
||||
return Result.OK("编辑成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过id删除
|
||||
*
|
||||
* @param id
|
||||
* @return
|
||||
*/
|
||||
@AutoLog(value = "airag_prompts-通过id删除")
|
||||
@Operation(summary="airag_prompts-通过id删除")
|
||||
@DeleteMapping(value = "/delete")
|
||||
public Result<String> delete(@RequestParam(name="id",required=true) String id) {
|
||||
airagPromptsService.removeById(id);
|
||||
return Result.OK("删除成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除
|
||||
*
|
||||
* @param ids
|
||||
* @return
|
||||
*/
|
||||
@AutoLog(value = "airag_prompts-批量删除")
|
||||
@Operation(summary="airag_prompts-批量删除")
|
||||
@DeleteMapping(value = "/deleteBatch")
|
||||
public Result<String> deleteBatch(@RequestParam(name="ids",required=true) String ids) {
|
||||
this.airagPromptsService.removeByIds(Arrays.asList(ids.split(",")));
|
||||
return Result.OK("批量删除成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过id查询
|
||||
*
|
||||
* @param id
|
||||
* @return
|
||||
*/
|
||||
//@AutoLog(value = "airag_prompts-通过id查询")
|
||||
@Operation(summary="airag_prompts-通过id查询")
|
||||
@GetMapping(value = "/queryById")
|
||||
public Result<AiragPrompts> queryById(@RequestParam(name="id",required=true) String id) {
|
||||
AiragPrompts airagPrompts = airagPromptsService.getById(id);
|
||||
if(airagPrompts==null) {
|
||||
return Result.error("未找到对应数据");
|
||||
}
|
||||
return Result.OK(airagPrompts);
|
||||
}
|
||||
/**
|
||||
* 构造器调试
|
||||
*
|
||||
* @param experimentVo
|
||||
* @return
|
||||
*/
|
||||
@PostMapping(value = "/experiment")
|
||||
public Result<?> promptExperiment(@RequestBody AiragExperimentVo experimentVo, HttpServletRequest request) {
|
||||
return airagPromptsService.promptExperiment(experimentVo,request);
|
||||
}
|
||||
/**
|
||||
* 导出excel
|
||||
*
|
||||
* @param request
|
||||
* @param airagPrompts
|
||||
*/
|
||||
@RequestMapping(value = "/exportXls")
|
||||
public ModelAndView exportXls(HttpServletRequest request, AiragPrompts airagPrompts) {
|
||||
return super.exportXls(request, airagPrompts, AiragPrompts.class, "airag_prompts");
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过excel导入数据
|
||||
*
|
||||
* @param request
|
||||
* @param response
|
||||
* @return
|
||||
*/
|
||||
@RequestMapping(value = "/importExcel", method = RequestMethod.POST)
|
||||
public Result<?> importExcel(HttpServletRequest request, HttpServletResponse response) {
|
||||
return super.importExcel(request, response, AiragPrompts.class);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package org.jeecg.modules.airag.prompts.entity;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.util.Date;
|
||||
import java.math.BigDecimal;
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||
import org.jeecg.common.constant.ProvinceCityArea;
|
||||
import org.jeecg.common.util.SpringContextUtils;
|
||||
import lombok.Data;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
import org.jeecgframework.poi.excel.annotation.Excel;
|
||||
import org.jeecg.common.aspect.annotation.Dict;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
/**
|
||||
* @Description: airag_ext_data
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-12-12
|
||||
* @Version: V1.0
|
||||
*/
|
||||
@Data
|
||||
@TableName("airag_ext_data")
|
||||
@Accessors(chain = true)
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@Schema(description="airag_ext_data")
|
||||
public class AiragExtData implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**主键ID*/
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
@Schema(description = "主键ID")
|
||||
private java.lang.String id;
|
||||
/**业务类型标识(evaluator:评估器;track:测试追踪)*/
|
||||
@Excel(name = "业务类型标识(evaluator:评估器;track:测试追踪)", width = 15)
|
||||
@Schema(description = "业务类型标识(evaluator:评估器;track:测试追踪)")
|
||||
private java.lang.String bizType;
|
||||
/**名称*/
|
||||
@Excel(name = "名称", width = 15)
|
||||
@Schema(description = "名称")
|
||||
private java.lang.String name;
|
||||
/**描述信息*/
|
||||
@Excel(name = "描述信息", width = 15)
|
||||
@Schema(description = "描述信息")
|
||||
private java.lang.String descr;
|
||||
/**标签,多个用逗号分隔*/
|
||||
@Excel(name = "标签,多个用逗号分隔", width = 15)
|
||||
@Schema(description = "标签,多个用逗号分隔")
|
||||
private java.lang.String tags;
|
||||
/**实际存储内容,json*/
|
||||
@Excel(name = "实际存储内容,json", width = 15)
|
||||
@Schema(description = "实际存储内容,json")
|
||||
private java.lang.String dataValue;
|
||||
/**元数据,用于存储补充业务数据信息*/
|
||||
@Excel(name = "元数据,用于存储补充业务数据信息", width = 15)
|
||||
@Schema(description = "元数据,用于存储补充业务数据信息")
|
||||
private java.lang.String metadata;
|
||||
/**评测集数据*/
|
||||
@Excel(name = "评测集数据", width = 15)
|
||||
@Schema(description = "评测集数据")
|
||||
private java.lang.String datasetValue;
|
||||
/**创建人*/
|
||||
@Schema(description = "创建人")
|
||||
private java.lang.String createBy;
|
||||
/**创建时间*/
|
||||
@JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss")
|
||||
@Schema(description = "创建时间")
|
||||
private java.util.Date createTime;
|
||||
/**修改人*/
|
||||
@Schema(description = "修改人")
|
||||
private java.lang.String updateBy;
|
||||
/**修改时间*/
|
||||
@JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss")
|
||||
@Schema(description = "修改时间")
|
||||
private java.util.Date updateTime;
|
||||
/**所属部门*/
|
||||
@Schema(description = "所属部门")
|
||||
private java.lang.String sysOrgCode;
|
||||
/**租户id*/
|
||||
@Excel(name = "租户id", width = 15)
|
||||
@Schema(description = "租户id")
|
||||
private java.lang.String tenantId;
|
||||
/**状态*/
|
||||
@Excel(name = "状态(run:进行中 completed:已完成)", width = 15)
|
||||
@Schema(description = "状态(run:进行中 completed:已完成)")
|
||||
private java.lang.String status;
|
||||
/**版本*/
|
||||
@Excel(name = "版本", width = 15)
|
||||
@Schema(description = "版本")
|
||||
private java.lang.Integer version;
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package org.jeecg.modules.airag.prompts.entity;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.util.Date;
|
||||
import java.math.BigDecimal;
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||
import org.jeecg.common.constant.ProvinceCityArea;
|
||||
import org.jeecg.common.util.SpringContextUtils;
|
||||
import lombok.Data;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
import org.jeecgframework.poi.excel.annotation.Excel;
|
||||
import org.jeecg.common.aspect.annotation.Dict;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
/**
|
||||
* @Description: airag_prompts
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-12-12
|
||||
* @Version: V1.0
|
||||
*/
|
||||
@Data
|
||||
@TableName("airag_prompts")
|
||||
@Accessors(chain = true)
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@Schema(description="airag_prompts")
|
||||
public class AiragPrompts implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**主键ID*/
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
@Schema(description = "主键ID")
|
||||
private java.lang.String id;
|
||||
/**提示词名称*/
|
||||
@Excel(name = "提示词名称", width = 15)
|
||||
@Schema(description = "提示词名称")
|
||||
private java.lang.String name;
|
||||
/**提示词名称*/
|
||||
@Excel(name = "提示key", width = 15)
|
||||
@Schema(description = "提示key")
|
||||
private java.lang.String promptKey;
|
||||
/**提示词功能描述*/
|
||||
@Excel(name = "提示词功能描述", width = 15)
|
||||
@Schema(description = "提示词功能描述")
|
||||
private java.lang.String description;
|
||||
/**提示词模板内容,支持变量占位符如 {{variable}}*/
|
||||
@Excel(name = "提示词模板内容,支持变量占位符如 {{variable}}", width = 15)
|
||||
@Schema(description = "提示词模板内容,支持变量占位符如 {{variable}}")
|
||||
private java.lang.String content;
|
||||
/**提示词分类*/
|
||||
@Excel(name = "提示词分类", width = 15)
|
||||
@Schema(description = "提示词分类")
|
||||
private java.lang.String category;
|
||||
/**标签,多个逗号分割*/
|
||||
@Excel(name = "标签,多个逗号分割", width = 15)
|
||||
@Schema(description = "标签,多个逗号分割")
|
||||
private java.lang.String tags;
|
||||
/**适配的大模型ID*/
|
||||
@Excel(name = "适配的大模型ID", width = 15)
|
||||
@Schema(description = "适配的大模型ID")
|
||||
private java.lang.String modelId;
|
||||
/**大模型的参数配置*/
|
||||
@Excel(name = "大模型的参数配置", width = 15)
|
||||
@Schema(description = "大模型的参数配置")
|
||||
private java.lang.String modelParam;
|
||||
/**状态(0:未发布 1:已发布)*/
|
||||
@Excel(name = "状态(0:未发布 1:已发布)", width = 15)
|
||||
@Schema(description = "状态(0:未发布 1:已发布)")
|
||||
private java.lang.String status;
|
||||
/**版本号(格式 0.0.1)*/
|
||||
@Excel(name = "版本号(格式 0.0.1)", width = 15)
|
||||
@Schema(description = "版本号(格式 0.0.1)")
|
||||
private java.lang.String version;
|
||||
/**删除状态(0未删除 1已删除)*/
|
||||
@Excel(name = "删除状态(0未删除 1已删除)", width = 15)
|
||||
@Schema(description = "删除状态(0未删除 1已删除)")
|
||||
@TableLogic
|
||||
private java.lang.Integer delFlag;
|
||||
/**创建人*/
|
||||
@Schema(description = "创建人")
|
||||
private java.lang.String createBy;
|
||||
/**创建日期*/
|
||||
@JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss")
|
||||
@Schema(description = "创建日期")
|
||||
private java.util.Date createTime;
|
||||
/**更新人*/
|
||||
@Schema(description = "更新人")
|
||||
private java.lang.String updateBy;
|
||||
/**更新日期*/
|
||||
@JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss")
|
||||
@Schema(description = "更新日期")
|
||||
private java.util.Date updateTime;
|
||||
/**所属部门*/
|
||||
@Schema(description = "所属部门")
|
||||
private java.lang.String sysOrgCode;
|
||||
/**租户id*/
|
||||
@Excel(name = "租户id", width = 15)
|
||||
@Schema(description = "租户id")
|
||||
private java.lang.String tenantId;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package org.jeecg.modules.airag.prompts.mapper;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.jeecg.modules.airag.prompts.entity.AiragExtData;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
|
||||
/**
|
||||
* @Description: airag_ext_data
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-12-12
|
||||
* @Version: V1.0
|
||||
*/
|
||||
public interface AiragExtDataMapper extends BaseMapper<AiragExtData> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package org.jeecg.modules.airag.prompts.mapper;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.jeecg.modules.airag.prompts.entity.AiragPrompts;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
|
||||
/**
|
||||
* @Description: airag_prompts
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-12-12
|
||||
* @Version: V1.0
|
||||
*/
|
||||
public interface AiragPromptsMapper extends BaseMapper<AiragPrompts> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<?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.jeecg.modules.airag.prompts.mapper.AiragExtDataMapper">
|
||||
|
||||
</mapper>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?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.jeecg.modules.airag.prompts.mapper.AiragPromptsMapper">
|
||||
|
||||
</mapper>
|
||||
@@ -0,0 +1,21 @@
|
||||
package org.jeecg.modules.airag.prompts.service;
|
||||
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.modules.airag.prompts.entity.AiragExtData;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import org.jeecg.modules.airag.prompts.vo.AiragDebugVo;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @Description: airag_ext_data
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-12-12
|
||||
* @Version: V1.0
|
||||
*/
|
||||
public interface IAiragExtDataService extends IService<AiragExtData> {
|
||||
|
||||
Result debugEvaluator(AiragDebugVo debugVo);
|
||||
|
||||
List<AiragExtData> queryTrackById(String id);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package org.jeecg.modules.airag.prompts.service;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.modules.airag.prompts.entity.AiragPrompts;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import org.jeecg.modules.airag.prompts.vo.AiragExperimentVo;
|
||||
|
||||
/**
|
||||
* @Description: airag_prompts
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-12-12
|
||||
* @Version: V1.0
|
||||
*/
|
||||
public interface IAiragPromptsService extends IService<AiragPrompts> {
|
||||
|
||||
Result<?> promptExperiment(AiragExperimentVo experimentVo, HttpServletRequest request);
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package org.jeecg.modules.airag.prompts.service.impl;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import dev.langchain4j.data.message.ChatMessage;
|
||||
import dev.langchain4j.data.message.SystemMessage;
|
||||
import dev.langchain4j.data.message.UserMessage;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.util.AssertUtils;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.modules.airag.common.handler.AIChatParams;
|
||||
import org.jeecg.modules.airag.common.handler.IAIChatHandler;
|
||||
import org.jeecg.modules.airag.prompts.entity.AiragExtData;
|
||||
import org.jeecg.modules.airag.prompts.mapper.AiragExtDataMapper;
|
||||
import org.jeecg.modules.airag.prompts.service.IAiragExtDataService;
|
||||
import org.jeecg.modules.airag.prompts.vo.AiragDebugVo;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @Description: airag_ext_data
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-12-12
|
||||
* @Version: V1.0
|
||||
*/
|
||||
@Service("airagExtDataServiceImpl")
|
||||
public class AiragExtDataServiceImpl extends ServiceImpl<AiragExtDataMapper, AiragExtData> implements IAiragExtDataService {
|
||||
|
||||
@Autowired
|
||||
IAIChatHandler aiChatHandler;
|
||||
|
||||
@Override
|
||||
public Result debugEvaluator(AiragDebugVo debugVo) {
|
||||
//1.提示词
|
||||
String prompt = debugVo.getPrompts();
|
||||
AssertUtils.assertNotEmpty("请输入提示词", prompt);
|
||||
|
||||
//2.测试内容
|
||||
String content = debugVo.getContent();
|
||||
AssertUtils.assertNotEmpty("请输入测试内容", content);
|
||||
List<ChatMessage> messages = Arrays.asList(new SystemMessage(prompt), new UserMessage(content));
|
||||
|
||||
//3.模型数据
|
||||
String modelId = debugVo.getModelId();
|
||||
AssertUtils.assertNotEmpty("请选择模型", modelId);
|
||||
|
||||
//4.模型参数
|
||||
String modelParam = debugVo.getModelParam();
|
||||
// 默认大模型参数
|
||||
AIChatParams params = new AIChatParams();
|
||||
params.setTemperature(0.8);
|
||||
params.setTopP(0.9);
|
||||
params.setPresencePenalty(0.1);
|
||||
params.setFrequencyPenalty(0.1);
|
||||
|
||||
if(oConvertUtils.isNotEmpty(modelParam)){
|
||||
JSONObject param = JSON.parseObject(modelParam);
|
||||
if(param.containsKey("temperature")){
|
||||
params.setTemperature(param.getDoubleValue("temperature"));
|
||||
}
|
||||
if(param.containsKey("topP")){
|
||||
params.setTemperature(param.getDoubleValue("topP"));
|
||||
}
|
||||
if(param.containsKey("presencePenalty")){
|
||||
params.setTemperature(param.getDoubleValue("presencePenalty"));
|
||||
}
|
||||
if(param.containsKey("frequencyPenalty")){
|
||||
params.setTemperature(param.getDoubleValue("frequencyPenalty"));
|
||||
}
|
||||
}
|
||||
//5.AI问答
|
||||
String promptValue = aiChatHandler.completions(modelId,messages, params);
|
||||
if (promptValue == null || promptValue.isEmpty()) {
|
||||
return Result.error("生成失败");
|
||||
}
|
||||
return Result.OK("success", promptValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询AI问答记录
|
||||
* @param id
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public List<AiragExtData> queryTrackById(String id) {
|
||||
LambdaQueryWrapper<AiragExtData> lqw = new LambdaQueryWrapper<AiragExtData>()
|
||||
.eq(AiragExtData::getMetadata, id)
|
||||
.orderByDesc(AiragExtData::getVersion)
|
||||
.orderByDesc(AiragExtData::getCreateTime);
|
||||
List<AiragExtData> list = this.baseMapper.selectList(lqw);
|
||||
return list;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,394 @@
|
||||
package org.jeecg.modules.airag.prompts.service.impl;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONArray;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import dev.langchain4j.data.message.ChatMessage;
|
||||
import dev.langchain4j.data.message.SystemMessage;
|
||||
import dev.langchain4j.data.message.UserMessage;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.util.AssertUtils;
|
||||
import org.jeecg.common.util.CommonUtils;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.config.JeecgBaseConfig;
|
||||
import org.jeecg.modules.airag.common.handler.AIChatParams;
|
||||
import org.jeecg.modules.airag.common.handler.IAIChatHandler;
|
||||
import org.jeecg.modules.airag.prompts.consts.AiPromptsConsts;
|
||||
import org.jeecg.modules.airag.prompts.entity.AiragExtData;
|
||||
import org.jeecg.modules.airag.prompts.entity.AiragPrompts;
|
||||
import org.jeecg.modules.airag.prompts.mapper.AiragPromptsMapper;
|
||||
import org.jeecg.modules.airag.prompts.service.IAiragExtDataService;
|
||||
import org.jeecg.modules.airag.prompts.service.IAiragPromptsService;
|
||||
import org.jeecg.modules.airag.prompts.vo.AiragExperimentVo;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
/**
|
||||
* @Description: airag_prompts
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-12-12
|
||||
* @Version: V1.0
|
||||
*/
|
||||
@Slf4j
|
||||
@Service("airagPromptsServiceImpl")
|
||||
public class AiragPromptsServiceImpl extends ServiceImpl<AiragPromptsMapper, AiragPrompts> implements IAiragPromptsService {
|
||||
@Autowired
|
||||
IAIChatHandler aiChatHandler;
|
||||
|
||||
@Autowired
|
||||
IAiragExtDataService airagExtDataService;
|
||||
|
||||
@Autowired
|
||||
private JeecgBaseConfig jeecgBaseConfig;
|
||||
// 创建静态线程池,确保整个应用生命周期中只有一个实例
|
||||
private static final ExecutorService executor = new ThreadPoolExecutor(
|
||||
4, // 核心线程数
|
||||
8, // 最大线程数
|
||||
60L, TimeUnit.SECONDS,
|
||||
new ArrayBlockingQueue<>(100), // 防止内存溢出
|
||||
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
|
||||
);
|
||||
|
||||
/**
|
||||
* 提示词实验
|
||||
* @param experimentVo
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public Result<?> promptExperiment(AiragExperimentVo experimentVo, HttpServletRequest request) {
|
||||
log.info("开始执行提示词实验,参数:{}", JSON.toJSONString(experimentVo));
|
||||
|
||||
// 参数验证
|
||||
String promptKey = experimentVo.getPromptKey();
|
||||
AssertUtils.assertNotEmpty("请选择提示词", promptKey);
|
||||
String dataId = experimentVo.getExtDataId();
|
||||
AssertUtils.assertNotEmpty("请选择数据集", dataId);
|
||||
|
||||
Map<String, String> fieldMappings = experimentVo.getMappings();
|
||||
AssertUtils.assertNotEmpty("请配置字段映射", fieldMappings);
|
||||
|
||||
try {
|
||||
//1.查询提示词
|
||||
AiragPrompts airagPrompts = this.baseMapper.selectOne(new LambdaQueryWrapper<AiragPrompts>().eq(AiragPrompts::getPromptKey, promptKey));
|
||||
AssertUtils.assertNotEmpty("未找到指定的提示词", airagPrompts);
|
||||
String modelParam = airagPrompts.getModelParam();
|
||||
// 过滤提示词变量
|
||||
JSONArray promptVariables;
|
||||
if(oConvertUtils.isNotEmpty(modelParam)){
|
||||
JSONObject airagPromptsParams = JSON.parseObject(modelParam);
|
||||
if(airagPromptsParams.containsKey("promptVariables")){
|
||||
promptVariables = airagPromptsParams.getJSONArray("promptVariables");
|
||||
} else {
|
||||
promptVariables = null;
|
||||
}
|
||||
} else {
|
||||
promptVariables = null;
|
||||
}
|
||||
//2.查询数据集
|
||||
AiragExtData airagExtData = airagExtDataService.getById(dataId);
|
||||
AssertUtils.assertNotEmpty("未找到指定的数据集", airagExtData);
|
||||
String datasetValue = airagExtData.getDatasetValue();
|
||||
if(oConvertUtils.isEmpty(datasetValue)){
|
||||
return Result.error("评测集不能为空!");
|
||||
}
|
||||
|
||||
//3.异步调用 根据映射字段,调用评估器测评
|
||||
JSONObject datasetObj = JSONObject.parseObject(datasetValue);
|
||||
//评测列配置
|
||||
JSONArray columns = datasetObj.getJSONArray("columns");
|
||||
//评测题库
|
||||
JSONArray datasetArray = datasetObj.getJSONArray("dataSource");
|
||||
AssertUtils.assertNotEmpty("数据集中没有找到数据源", datasetArray);
|
||||
AssertUtils.assertTrue("数据源为空", datasetArray.size() > 0);
|
||||
|
||||
//测评结果集 - 使用线程安全的CopyOnWriteArrayList
|
||||
List<JSONObject> scoreResult = new CopyOnWriteArrayList<>();
|
||||
|
||||
// 批量提交任务
|
||||
List<CompletableFuture<Void>> futures = IntStream.range(0, datasetArray.size())
|
||||
.mapToObj(i -> CompletableFuture.runAsync(() -> {
|
||||
try {
|
||||
log.info("开始处理第{}条数据", i + 1);
|
||||
//定义返回结果
|
||||
JSONObject result = new JSONObject();
|
||||
//评测数据
|
||||
JSONObject dataset = datasetArray.getJSONObject(i);
|
||||
result.putAll(dataset);
|
||||
//用户问题
|
||||
String userQuery = dataset.getString(fieldMappings.get("user_query"));
|
||||
result.put("userQuery", userQuery);
|
||||
//变量处理
|
||||
if(!CollectionUtils.isEmpty(promptVariables)){
|
||||
String content = airagPrompts.getContent();
|
||||
for (Object var : promptVariables){
|
||||
JSONObject variable = JSONObject.parseObject(var.toString());
|
||||
String name = dataset.getString(fieldMappings.get(variable.getString("name")));
|
||||
//提示词默认变量值
|
||||
String defaultValue = variable.getString("value");
|
||||
// 获取目标类型
|
||||
String dataType = findDataType(columns, variable);
|
||||
if("FILE".equals(dataType)){
|
||||
defaultValue = getFileAccessHttpUrl(request, defaultValue);
|
||||
name = getFileAccessHttpUrl(request, name);
|
||||
}
|
||||
if(oConvertUtils.isNotEmpty(name)){
|
||||
//提示词 评估集变量值替换
|
||||
content = content.replaceAll(variable.getString("name"), name);
|
||||
}else if(oConvertUtils.isNotEmpty(defaultValue)){
|
||||
content = content.replaceAll(variable.getString("name"), defaultValue);
|
||||
}
|
||||
}
|
||||
airagPrompts.setContent(content);
|
||||
}
|
||||
|
||||
//提示词答案
|
||||
String promptAnswer = getPromptAnswer(airagPrompts, dataset, fieldMappings);
|
||||
result.put("promptAnswer", promptAnswer);
|
||||
|
||||
//评估器答案
|
||||
String answerScore = getAnswerScore(promptAnswer, dataset, fieldMappings, airagExtData);
|
||||
result.put("answerScore", answerScore);
|
||||
|
||||
scoreResult.add(result);
|
||||
log.info("第{}条数据处理完成", i + 1);
|
||||
} catch (Exception e) {
|
||||
log.error("处理第{}条数据时发生异常", i + 1, e);
|
||||
// 重新抛出异常,让CompletableFuture捕获
|
||||
throw new CompletionException(e);
|
||||
}
|
||||
}, executor))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// 非阻塞方式处理完成
|
||||
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
|
||||
.whenComplete((result, ex) -> {
|
||||
if (ex != null) {
|
||||
log.error("批量处理失败", ex);
|
||||
// 更新状态为失败
|
||||
airagExtData.setStatus(AiPromptsConsts.STATUS_FAILED);
|
||||
} else {
|
||||
log.info("所有数据处理完成,共处理{}条数据", scoreResult.size());
|
||||
// 查询已存在的评测记录
|
||||
List<AiragExtData> existingTracks = airagExtDataService.queryTrackById(dataId);
|
||||
Integer version = 1;
|
||||
if(!CollectionUtils.isEmpty(existingTracks)) {
|
||||
version = existingTracks.stream()
|
||||
.map(AiragExtData::getVersion)
|
||||
.max(Integer::compareTo)
|
||||
.orElse(0) + 1;
|
||||
}
|
||||
for (JSONObject item : scoreResult) {
|
||||
// 保存结果
|
||||
AiragExtData track = new AiragExtData();
|
||||
//关联评估器ID
|
||||
track.setMetadata(dataId);
|
||||
//定义类型
|
||||
track.setBizType(AiPromptsConsts.BIZ_TYPE_TRACK);
|
||||
//定义版本
|
||||
track.setVersion(version);
|
||||
//定义状态
|
||||
track.setStatus(AiPromptsConsts.STATUS_COMPLETED);
|
||||
//定义评测结果
|
||||
track.setDataValue(item.toJSONString());
|
||||
airagExtDataService.save(track);
|
||||
}
|
||||
// 更新状态为完成
|
||||
airagExtData.setStatus(AiPromptsConsts.STATUS_COMPLETED);
|
||||
}
|
||||
airagExtDataService.updateById(airagExtData);
|
||||
});
|
||||
|
||||
//4.修改状态进行中
|
||||
airagExtData.setStatus(AiPromptsConsts.STATUS_RUNNING);
|
||||
airagExtDataService.updateById(airagExtData);
|
||||
|
||||
log.info("提示词实验已提交,共{}条数据待处理", datasetArray.size());
|
||||
return Result.OK("实验已开始,正在处理数据");
|
||||
} catch (Exception e) {
|
||||
log.error("提示词实验执行失败", e);
|
||||
return Result.error("实验执行失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 提示词回答的结果
|
||||
* @param airagPrompts
|
||||
* @param questions
|
||||
* @param fieldMappings
|
||||
* @return
|
||||
*/
|
||||
public String getPromptAnswer(AiragPrompts airagPrompts, JSONObject questions, Map<String, String> fieldMappings) {
|
||||
try {
|
||||
//0.判断是否配置了判断fieldMappings的value值 是否包含actual_output
|
||||
if (!fieldMappings.containsValue("actual_output")) {
|
||||
log.warn("字段映射中没有配置actual_output");
|
||||
return null;
|
||||
}
|
||||
|
||||
//1.提示词
|
||||
String prompt = airagPrompts.getContent();
|
||||
AssertUtils.assertNotEmpty("请输入提示词", prompt);
|
||||
|
||||
String userQuery = questions.getString(fieldMappings.get("user_query"));
|
||||
AssertUtils.assertNotEmpty("请输入测试内容", userQuery);
|
||||
|
||||
//2.ai问题组装
|
||||
List<ChatMessage> messages = Arrays.asList(new SystemMessage(prompt), new UserMessage(userQuery));
|
||||
|
||||
//3.模型数据
|
||||
String modelId = airagPrompts.getModelId();
|
||||
AssertUtils.assertNotEmpty("请选择模型", modelId);
|
||||
|
||||
//4.模型参数
|
||||
String modelParam = airagPrompts.getModelParam();
|
||||
// 默认大模型参数
|
||||
AIChatParams params = new AIChatParams();
|
||||
params.setTemperature(0.8);
|
||||
params.setTopP(0.9);
|
||||
params.setPresencePenalty(0.1);
|
||||
params.setFrequencyPenalty(0.1);
|
||||
|
||||
if(oConvertUtils.isNotEmpty(modelParam)){
|
||||
JSONObject param = JSON.parseObject(modelParam);
|
||||
if(param.containsKey("temperature")){
|
||||
params.setTemperature(param.getDoubleValue("temperature"));
|
||||
}
|
||||
if(param.containsKey("topP")){
|
||||
params.setTopP(param.getDoubleValue("topP")); // 修复:设置到正确的字段
|
||||
}
|
||||
if(param.containsKey("presencePenalty")){
|
||||
params.setPresencePenalty(param.getDoubleValue("presencePenalty")); // 修复:设置到正确的字段
|
||||
}
|
||||
if(param.containsKey("frequencyPenalty")){
|
||||
params.setFrequencyPenalty(param.getDoubleValue("frequencyPenalty")); // 修复:设置到正确的字段
|
||||
}
|
||||
}
|
||||
|
||||
log.debug("调用AI模型,模型ID:{},参数:{}", modelId, JSON.toJSONString(params));
|
||||
//5.AI问答
|
||||
String promptAnswer = aiChatHandler.completions(modelId, messages, params);
|
||||
log.debug("AI模型返回结果:{}", promptAnswer);
|
||||
|
||||
return promptAnswer;
|
||||
} catch (Exception e) {
|
||||
log.error("获取提示词回答失败", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 评测答案分数
|
||||
* @return
|
||||
*/
|
||||
public String getAnswerScore(String promptAnswer, JSONObject questions, Map<String, String> fieldMappings, AiragExtData airagExtData) {
|
||||
try {
|
||||
//1.提示词
|
||||
String prompt = airagExtData.getDataValue();
|
||||
AssertUtils.assertNotEmpty("请输入提示词", prompt);
|
||||
prompt += "定义返回格式: 得分:最终的得分,必须输出一个数字,表示满足Prompt中评分标准的程度。得分范围从 0.0 到 1.0,1.0 表示完全满足评分标准,0.0 表示完全不满足评分标准。\n" +
|
||||
"原因:{对得分的可读性的解释,说明打分原因}。最后,必须用一句话结束理由,该句话为:因此,应该给出的分数是<你评测的的得分>。请勿返回提问的问题、添加分析过程、解释说明等内容,只返回要求的格式内容";
|
||||
|
||||
String userQuery = "输入的内容:";
|
||||
//2.拼接测试内容
|
||||
for (Map.Entry<String, String> entry : fieldMappings.entrySet()) {
|
||||
// 评估器中的key
|
||||
String key = entry.getKey();
|
||||
// 评估器中的映射的key
|
||||
String value = entry.getValue();
|
||||
String valueData;
|
||||
if("actual_output".equalsIgnoreCase(value)){
|
||||
valueData = promptAnswer;
|
||||
}else{
|
||||
valueData = questions.getString(value);
|
||||
}
|
||||
userQuery += (key + ":" + valueData + " ");
|
||||
}
|
||||
List<ChatMessage> messages = Arrays.asList(new SystemMessage(prompt), new UserMessage(userQuery));
|
||||
|
||||
//3.模型数据
|
||||
String metadata = airagExtData.getMetadata();
|
||||
if(oConvertUtils.isNotEmpty(metadata)){
|
||||
JSONObject modelParam = JSONObject.parseObject(metadata);
|
||||
String modelId = modelParam.getString("modelId");
|
||||
AssertUtils.assertNotEmpty("评估器模型ID不能为空", modelId);
|
||||
|
||||
// 默认大模型参数
|
||||
AIChatParams params = new AIChatParams();
|
||||
params.setTemperature(0.8);
|
||||
params.setTopP(0.9);
|
||||
params.setPresencePenalty(0.1);
|
||||
params.setFrequencyPenalty(0.1);
|
||||
|
||||
if(oConvertUtils.isNotEmpty(modelParam)){
|
||||
if(modelParam.containsKey("temperature")){
|
||||
params.setTemperature(modelParam.getDoubleValue("temperature"));
|
||||
}
|
||||
if(modelParam.containsKey("topP")){
|
||||
params.setTopP(modelParam.getDoubleValue("topP")); // 修复:设置到正确的字段
|
||||
}
|
||||
if(modelParam.containsKey("presencePenalty")){
|
||||
params.setPresencePenalty(modelParam.getDoubleValue("presencePenalty")); // 修复:设置到正确的字段
|
||||
}
|
||||
if(modelParam.containsKey("frequencyPenalty")){
|
||||
params.setFrequencyPenalty(modelParam.getDoubleValue("frequencyPenalty")); // 修复:设置到正确的字段
|
||||
}
|
||||
}
|
||||
|
||||
log.debug("调用评估器模型,模型ID:{},参数:{}", modelId, JSON.toJSONString(params));
|
||||
//5.AI问答
|
||||
String answerScore = aiChatHandler.completions(modelId, messages, params);
|
||||
log.debug("评估器模型返回结果:{}", answerScore);
|
||||
|
||||
return answerScore;
|
||||
}
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
log.error("获取答案评分失败", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param columns
|
||||
* @param variable
|
||||
* @return
|
||||
*/
|
||||
public static String findDataType(JSONArray columns, JSONObject variable) {
|
||||
// 获取目标字段值
|
||||
String targetName = variable.getString("name");
|
||||
|
||||
// 使用 Stream API 查找并获取 dataType
|
||||
return columns.stream()
|
||||
.map(obj -> JSONObject.parseObject(obj.toString()))
|
||||
.filter(column -> targetName.equals(column.getString("name")))
|
||||
.findFirst()
|
||||
.map(column -> column.getString("dataType"))
|
||||
.orElse(null); // 如果没有找到,返回 null
|
||||
}
|
||||
/**
|
||||
* 获取图片地址
|
||||
* @param request
|
||||
* @param url
|
||||
* @return
|
||||
*/
|
||||
private String getFileAccessHttpUrl(HttpServletRequest request,String url){
|
||||
if(oConvertUtils.isNotEmpty(url) && url.startsWith("http")){
|
||||
return url;
|
||||
}else{
|
||||
return CommonUtils.getBaseUrl(request) + "/sys/common/static/" + url;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package org.jeecg.modules.airag.prompts.vo;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
import org.jeecgframework.poi.excel.annotation.Excel;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* @Description: AiragDebugVo
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-12-12
|
||||
* @Version: V1.0
|
||||
*/
|
||||
@Data
|
||||
public class AiragDebugVo implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
/**
|
||||
* 提示词
|
||||
*/
|
||||
private String prompts;
|
||||
/**
|
||||
* 输入内容
|
||||
*/
|
||||
private String content;
|
||||
/**适配的大模型ID*/
|
||||
private String modelId;
|
||||
/**大模型的参数配置*/
|
||||
private String modelParam;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package org.jeecg.modules.airag.prompts.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @Description: AiragExperimentVo
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-12-12
|
||||
* @Version: V1.0
|
||||
*/
|
||||
@Data
|
||||
public class AiragExperimentVo implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
/**
|
||||
* 提示词
|
||||
*/
|
||||
private String promptKey;
|
||||
/**
|
||||
* 输入内容
|
||||
*/
|
||||
private String extDataId;
|
||||
/**
|
||||
* 映射关系
|
||||
*/
|
||||
private Map<String,String> mappings;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package org.jeecg.modules.airag.wordtpl.consts;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* @author chenrui
|
||||
* @ClassName: TitleLevelEnum
|
||||
* @Description: 标题级别
|
||||
* @date 2024年5月4日07:38:30
|
||||
*/
|
||||
@Getter
|
||||
public enum WordTitleEnum {
|
||||
|
||||
FIRST("first", "标题1"),
|
||||
SECOND("second", "标题2"),
|
||||
THIRD("third", "标题3"),
|
||||
FOURTH("fourth", "标题4"),
|
||||
FIFTH("fifth", "标题5"),
|
||||
SIXTH("sixth", "标题6");
|
||||
|
||||
WordTitleEnum(String code, String name) {
|
||||
this.code = code;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
final String code;
|
||||
|
||||
final String name;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
package org.jeecg.modules.airag.wordtpl.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.aspect.annotation.AutoLog;
|
||||
import org.jeecg.common.exception.JeecgBootException;
|
||||
import org.jeecg.common.system.base.controller.JeecgController;
|
||||
import org.jeecg.common.system.query.QueryGenerator;
|
||||
import org.jeecg.common.util.AssertUtils;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.modules.airag.wordtpl.dto.WordTplGenDTO;
|
||||
import org.jeecg.modules.airag.wordtpl.entity.EoaWordTemplate;
|
||||
import org.jeecg.modules.airag.wordtpl.service.IEoaWordTemplateService;
|
||||
import org.jeecg.modules.airag.wordtpl.utils.WordTplUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.net.URLEncoder;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* @Description: word模版管理
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-07-04
|
||||
* @Version: V1.0
|
||||
*/
|
||||
@Tag(name = "word模版管理")
|
||||
@RestController("eoaWordTemplateController")
|
||||
@RequestMapping("/airag/word")
|
||||
@Slf4j
|
||||
public class EoaWordTemplateController extends JeecgController<EoaWordTemplate, IEoaWordTemplateService> {
|
||||
@Autowired
|
||||
private IEoaWordTemplateService eoaWordTemplateService;
|
||||
|
||||
@Autowired
|
||||
WordTplUtils wordTplUtils;
|
||||
|
||||
/**
|
||||
* 分页列表查询
|
||||
*
|
||||
* @param eoaWordTemplate
|
||||
* @param pageNo
|
||||
* @param pageSize
|
||||
* @param req
|
||||
* @return
|
||||
*/
|
||||
@Operation(summary = "word模版管理-分页列表查询")
|
||||
@GetMapping(value = "/list")
|
||||
public Result<IPage<EoaWordTemplate>> queryPageList(EoaWordTemplate eoaWordTemplate,
|
||||
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
|
||||
@RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize,
|
||||
HttpServletRequest req) {
|
||||
QueryWrapper<EoaWordTemplate> queryWrapper = QueryGenerator.initQueryWrapper(eoaWordTemplate, req.getParameterMap());
|
||||
Page<EoaWordTemplate> page = new Page<EoaWordTemplate>(pageNo, pageSize);
|
||||
IPage<EoaWordTemplate> pageList = eoaWordTemplateService.page(page, queryWrapper);
|
||||
return Result.OK(pageList);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加
|
||||
*
|
||||
* @param eoaWordTemplate
|
||||
* @return
|
||||
*/
|
||||
@AutoLog(value = "word模版管理-添加")
|
||||
@Operation(summary = "word模版管理-添加")
|
||||
// @RequiresPermissions("wordtpl:template:add")
|
||||
@PostMapping(value = "/add")
|
||||
public Result<String> add(@RequestBody EoaWordTemplate eoaWordTemplate) {
|
||||
AssertUtils.assertNotEmpty("参数异常", eoaWordTemplate);
|
||||
AssertUtils.assertNotEmpty("模版名称不能为空", eoaWordTemplate.getName());
|
||||
boolean isCodeExists = eoaWordTemplateService.exists(Wrappers.lambdaQuery(EoaWordTemplate.class).eq(EoaWordTemplate::getCode, eoaWordTemplate.getCode()));
|
||||
AssertUtils.assertFalse("模版编码已存在", isCodeExists);
|
||||
eoaWordTemplateService.save(eoaWordTemplate);
|
||||
return Result.OK("添加成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑
|
||||
*
|
||||
* @param eoaWordTemplate
|
||||
* @return
|
||||
*/
|
||||
@AutoLog(value = "word模版管理-编辑")
|
||||
@Operation(summary = "word模版管理-编辑")
|
||||
// @RequiresPermissions("wordtpl:template:edit")
|
||||
@RequestMapping(value = "/edit", method = {RequestMethod.PUT, RequestMethod.POST})
|
||||
public Result<String> edit(@RequestBody EoaWordTemplate eoaWordTemplate) {
|
||||
AssertUtils.assertNotEmpty("参数异常", eoaWordTemplate);
|
||||
AssertUtils.assertNotEmpty("模版名称不能为空", eoaWordTemplate.getName());
|
||||
// 避免编辑时修改编码
|
||||
eoaWordTemplate.setCode(null);
|
||||
eoaWordTemplateService.updateById(eoaWordTemplate);
|
||||
return Result.OK("编辑成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过id删除
|
||||
*
|
||||
* @param id
|
||||
* @return
|
||||
*/
|
||||
@AutoLog(value = "word模版管理-通过id删除")
|
||||
@Operation(summary = "word模版管理-通过id删除")
|
||||
// @RequiresPermissions("wordtpl:template:delete")
|
||||
@DeleteMapping(value = "/delete")
|
||||
public Result<String> delete(@RequestParam(name = "id", required = true) String id) {
|
||||
eoaWordTemplateService.removeById(id);
|
||||
return Result.OK("删除成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除
|
||||
*
|
||||
* @param ids
|
||||
* @return
|
||||
*/
|
||||
@AutoLog(value = "word模版管理-批量删除")
|
||||
@Operation(summary = "word模版管理-批量删除")
|
||||
// @RequiresPermissions("wordtpl:template:deleteBatch")
|
||||
@DeleteMapping(value = "/deleteBatch")
|
||||
public Result<String> deleteBatch(@RequestParam(name = "ids", required = true) String ids) {
|
||||
this.eoaWordTemplateService.removeByIds(Arrays.asList(ids.split(",")));
|
||||
return Result.OK("批量删除成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过id查询
|
||||
*
|
||||
* @param id
|
||||
* @return
|
||||
*/
|
||||
//@AutoLog(value = "word模版管理-通过id查询")
|
||||
@Operation(summary = "word模版管理-通过id查询")
|
||||
@GetMapping(value = "/queryById")
|
||||
public Result<EoaWordTemplate> queryById(@RequestParam(name = "id", required = true) String id) {
|
||||
EoaWordTemplate eoaWordTemplate = eoaWordTemplateService.getById(id);
|
||||
if (eoaWordTemplate == null) {
|
||||
return Result.error("未找到对应数据");
|
||||
}
|
||||
return Result.OK(eoaWordTemplate);
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载word模版
|
||||
* @param id
|
||||
* @param response
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/7/9 14:38
|
||||
*/
|
||||
@GetMapping(value = "/download")
|
||||
public void downloadTemplate(@RequestParam(name = "id", required = true) String id, HttpServletResponse response) {
|
||||
AssertUtils.assertNotEmpty("请先选择模版", id);
|
||||
EoaWordTemplate template = eoaWordTemplateService.getById(id);
|
||||
try (ByteArrayOutputStream wordTemplateOut = new ByteArrayOutputStream();
|
||||
BufferedOutputStream bos = new BufferedOutputStream(response.getOutputStream());) {
|
||||
wordTplUtils.generateWordTemplate(template, wordTemplateOut);
|
||||
String fileName = template.getName();
|
||||
String encodedFileName = URLEncoder.encode(fileName, "UTF-8");
|
||||
response.setContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document");
|
||||
response.addHeader("Content-Disposition", "attachment;filename=" + encodedFileName + ".docx");
|
||||
response.addHeader("filename", encodedFileName + ".docx");
|
||||
byte[] bytes = wordTemplateOut.toByteArray();
|
||||
response.setHeader("Content-Length", String.valueOf(bytes.length));
|
||||
bos.write(bytes);
|
||||
} catch (Exception e) {
|
||||
log.error(e.getMessage(), e);
|
||||
throw new JeecgBootException("下载word模版失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 解析word模版文件
|
||||
* @param file
|
||||
* @param id
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/7/9 14:38
|
||||
*/
|
||||
@PostMapping(value = "/parse/file")
|
||||
public Result<?> parseWOrdFile(@RequestParam("file") MultipartFile file) {
|
||||
try {
|
||||
InputStream inputStream = file.getInputStream();
|
||||
EoaWordTemplate eoaWordTemplate = wordTplUtils.parseWordFile(inputStream);
|
||||
log.info("解析的模版信息: {}", eoaWordTemplate);
|
||||
return Result.OK("解析成功", eoaWordTemplate);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("解析word模版失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成word文档
|
||||
*
|
||||
* @param wordTplGenDTO
|
||||
* @param response
|
||||
* @author chenrui
|
||||
* @date 2025/7/10 15:39
|
||||
*/
|
||||
@PostMapping(value = "/generate/word")
|
||||
public void generateWord(@RequestBody WordTplGenDTO wordTplGenDTO, HttpServletResponse response) {
|
||||
AssertUtils.assertNotEmpty("参数异常", wordTplGenDTO);
|
||||
EoaWordTemplate template ;
|
||||
if (oConvertUtils.isNotEmpty(wordTplGenDTO.getTemplateId())) {
|
||||
template = eoaWordTemplateService.getById(wordTplGenDTO.getTemplateId());
|
||||
}else{
|
||||
AssertUtils.assertNotEmpty("请先选择模版", wordTplGenDTO.getTemplateCode());
|
||||
template = eoaWordTemplateService.getOne(Wrappers.lambdaQuery(EoaWordTemplate.class)
|
||||
.eq(EoaWordTemplate::getCode, wordTplGenDTO.getTemplateCode()));
|
||||
}
|
||||
AssertUtils.assertNotEmpty("未找到对应的模版", template);
|
||||
|
||||
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
BufferedOutputStream bos = new BufferedOutputStream(response.getOutputStream());) {
|
||||
eoaWordTemplateService.generateWordFromTpl(wordTplGenDTO, outputStream);
|
||||
String fileName = template.getName();
|
||||
String encodedFileName = URLEncoder.encode(fileName, "UTF-8");
|
||||
response.setContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document");
|
||||
response.addHeader("Content-Disposition", "attachment;filename=" + encodedFileName + ".docx");
|
||||
response.addHeader("filename", encodedFileName + ".docx");
|
||||
byte[] bytes = outputStream.toByteArray();
|
||||
response.setHeader("Content-Length", String.valueOf(bytes.length));
|
||||
bos.write(bytes);
|
||||
} catch (Exception e) {
|
||||
log.error(e.getMessage(), e);
|
||||
throw new JeecgBootException("生成word文档失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package org.jeecg.modules.airag.wordtpl.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 合并列DTO
|
||||
* @author chenrui
|
||||
* @date 2025/7/4 18:36
|
||||
*/
|
||||
@Data
|
||||
public class MergeColDTO {
|
||||
|
||||
/**
|
||||
* 合并列的行号
|
||||
*/
|
||||
private int row;
|
||||
|
||||
/**
|
||||
* 合并列的起始列号
|
||||
*/
|
||||
private int from;
|
||||
|
||||
/**
|
||||
* 合并列的结束列号
|
||||
*/
|
||||
private int to;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package org.jeecg.modules.airag.wordtpl.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* @ClassName: DocImageDto
|
||||
* @Description: word文档图片用实体类
|
||||
* @author chenrui
|
||||
* @date 2024-10-02 09:17:59
|
||||
*/
|
||||
@Data
|
||||
public class WordImageDTO {
|
||||
|
||||
/**
|
||||
* @Fields type : 类型
|
||||
* @author chenrui
|
||||
* @date 2024-09-29 08:53:27
|
||||
*/
|
||||
private String type = "image";
|
||||
/**
|
||||
* @Fields value : 内容
|
||||
* @author chenrui
|
||||
* @date 2024-09-24 10:20:12
|
||||
*/
|
||||
private String value = "";
|
||||
|
||||
/**
|
||||
* @Fields width : 图片宽度
|
||||
* @author chenrui
|
||||
* @date 2024-10-02 09:22:33
|
||||
*/
|
||||
private double width;
|
||||
|
||||
/**
|
||||
* @Fields height : 图片高度
|
||||
* @author chenrui
|
||||
* @date 2024-10-02 09:22:40
|
||||
*/
|
||||
private double height;
|
||||
|
||||
/**
|
||||
* @Fields rowFlex : 水平对齐方式,默认left
|
||||
* @author chenrui
|
||||
* @date 2024-09-27 09:12:18
|
||||
*/
|
||||
private String rowFlex = "left";
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package org.jeecg.modules.airag.wordtpl.dto;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class WordTableCellDTO {
|
||||
|
||||
/**
|
||||
* @Fields colspan : 合并列数
|
||||
* @author chenrui
|
||||
* @date 2024-09-26 09:37:27
|
||||
*/
|
||||
private int colspan;
|
||||
|
||||
/**
|
||||
* @Fields rowspan : 合并行数
|
||||
* @author chenrui
|
||||
* @date 2024-09-26 09:38:22
|
||||
*/
|
||||
private int rowspan;
|
||||
|
||||
/**
|
||||
* @Fields value : 单元格数据
|
||||
* @author chenrui
|
||||
* @date 2024-09-26 09:42:14
|
||||
*/
|
||||
private List<Object> value = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* @Fields verticalAlign : 垂直对齐方式,默认top
|
||||
* @author chenrui
|
||||
* @date 2024-09-27 09:16:56
|
||||
*/
|
||||
private String verticalAlign = "top";
|
||||
|
||||
/**
|
||||
* @Fields backgroundColor : 背景颜色
|
||||
* @author chenrui
|
||||
* @date 2024-11-18 09:56:28
|
||||
*/
|
||||
private String backgroundColor;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package org.jeecg.modules.airag.wordtpl.dto;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class WordTableDTO {
|
||||
|
||||
private String value = "";
|
||||
|
||||
private String type = "table";
|
||||
|
||||
private List<WordTableRowDTO> trList;
|
||||
|
||||
private int width;
|
||||
|
||||
private int height;
|
||||
|
||||
private List<JSONObject> colgroup = new ArrayList<>();
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package org.jeecg.modules.airag.wordtpl.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class WordTableRowDTO {
|
||||
|
||||
/**
|
||||
* @Fields height : 行高
|
||||
* @author chenrui
|
||||
* @date 2024-09-26 09:45:30
|
||||
*/
|
||||
private Integer height;
|
||||
|
||||
/**
|
||||
* @Fields minHeight : 行最小高度
|
||||
* @author chenrui
|
||||
* @date 2024-09-26 09:47:28
|
||||
*/
|
||||
private int minHeight = 42;
|
||||
|
||||
/**
|
||||
* @Fields tdList : 行数据
|
||||
* @author chenrui
|
||||
* @date 2024-09-26 09:46:02
|
||||
*/
|
||||
private List<WordTableCellDTO> tdList;
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package org.jeecg.modules.airag.wordtpl.dto;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* @author chenrui
|
||||
* @ClassName: DocTextDto
|
||||
* @Description: word文本实体类
|
||||
* @date 2024-09-24 10:19:57
|
||||
*/
|
||||
@Data
|
||||
public class WordTextDTO {
|
||||
|
||||
/**
|
||||
* @Fields type : 类型
|
||||
* @author chenrui
|
||||
* @date 2024-09-29 08:53:27
|
||||
*/
|
||||
private String type;
|
||||
/**
|
||||
* @Fields value : 内容
|
||||
* @author chenrui
|
||||
* @date 2024-09-24 10:20:12
|
||||
*/
|
||||
private String value = "";
|
||||
|
||||
/**
|
||||
* @Fields bold : 是否加粗 默认false
|
||||
* @author chenrui
|
||||
* @date 2024-09-24 10:20:33
|
||||
*/
|
||||
private boolean bold = false;
|
||||
|
||||
/**
|
||||
* @Fields color : 字体颜色
|
||||
* @author chenrui
|
||||
* @date 2024-09-24 10:21:08
|
||||
*/
|
||||
private String color;
|
||||
|
||||
/**
|
||||
* @Fields italic : 是否斜体 默认false
|
||||
* @author chenrui
|
||||
* @date 2024-09-24 10:21:25
|
||||
*/
|
||||
private boolean italic = false;
|
||||
|
||||
/**
|
||||
* @Fields underline : 是否下划线 默认false
|
||||
* @author chenrui
|
||||
* @date 2024-09-24 10:21:47
|
||||
*/
|
||||
private boolean underline = false;
|
||||
|
||||
/**
|
||||
* @Fields strikeout : 删除线 默认false
|
||||
* @author chenrui
|
||||
* @date 2024-09-24 10:22:06
|
||||
*/
|
||||
private boolean strikeout = false;
|
||||
|
||||
/**
|
||||
* @Fields size : 字号大小
|
||||
* @author chenrui
|
||||
* @date 2024-09-24 10:44:42
|
||||
*/
|
||||
private int size;
|
||||
|
||||
/**
|
||||
* @Fields font : 字体,默认微软雅黑
|
||||
* @author chenrui
|
||||
* @date 2024-09-24 10:45:31
|
||||
*/
|
||||
private String font = "微软雅黑";
|
||||
|
||||
/**
|
||||
* @Fields highlight : 高亮颜色
|
||||
* @author chenrui
|
||||
* @date 2024-09-25 11:20:23
|
||||
*/
|
||||
private String highlight;
|
||||
|
||||
/**
|
||||
* @Fields rowFlex : 水平对齐方式,默认left
|
||||
* @author chenrui
|
||||
* @date 2024-09-27 09:12:18
|
||||
*/
|
||||
private String rowFlex = "left";
|
||||
|
||||
private List<Object> dashArray = new ArrayList<>();
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package org.jeecg.modules.airag.wordtpl.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* word模版生成入参
|
||||
* @author chenrui
|
||||
* @date 2025/7/10 14:38
|
||||
*/
|
||||
@Data
|
||||
public class WordTplGenDTO {
|
||||
|
||||
/**
|
||||
* 模版id
|
||||
*/
|
||||
String templateId;
|
||||
|
||||
|
||||
/**
|
||||
* 模版code
|
||||
*/
|
||||
String templateCode;
|
||||
|
||||
/**
|
||||
* 数据
|
||||
*/
|
||||
Map<String,Object> data;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
package org.jeecg.modules.airag.wordtpl.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
import org.jeecgframework.poi.excel.annotation.Excel;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* @Description: word模版管理
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-07-04
|
||||
* @Version: V1.0
|
||||
*/
|
||||
@Data
|
||||
@TableName("aigc_word_template")
|
||||
@Accessors(chain = true)
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@Schema(description = "word模版管理")
|
||||
public class EoaWordTemplate implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 主键
|
||||
*/
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
@Schema(description = "主键")
|
||||
private String id;
|
||||
/**
|
||||
* 创建人
|
||||
*/
|
||||
@Schema(description = "创建人")
|
||||
private String createBy;
|
||||
/**
|
||||
* 创建日期
|
||||
*/
|
||||
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@Schema(description = "创建日期")
|
||||
private Date createTime;
|
||||
/**
|
||||
* 更新人
|
||||
*/
|
||||
@Schema(description = "更新人")
|
||||
private String updateBy;
|
||||
/**
|
||||
* 更新日期
|
||||
*/
|
||||
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@Schema(description = "更新日期")
|
||||
private Date updateTime;
|
||||
/**
|
||||
* 所属部门
|
||||
*/
|
||||
@Schema(description = "所属部门")
|
||||
private String sysOrgCode;
|
||||
/**
|
||||
* 模版名称
|
||||
*/
|
||||
@Excel(name = "模版名称", width = 15)
|
||||
@Schema(description = "模版名称")
|
||||
private String name;
|
||||
/**
|
||||
* 模版编码
|
||||
*/
|
||||
@Excel(name = "模版编码", width = 15)
|
||||
@Schema(description = "模版编码")
|
||||
private String code;
|
||||
/**
|
||||
* 页眉
|
||||
*/
|
||||
@Excel(name = "页眉", width = 15)
|
||||
@Schema(description = "页眉")
|
||||
private String header;
|
||||
/**
|
||||
* 页脚
|
||||
*/
|
||||
@Excel(name = "页脚", width = 15)
|
||||
@Schema(description = "页脚")
|
||||
private String footer;
|
||||
/**
|
||||
* 主体内容
|
||||
*/
|
||||
@Excel(name = "主体内容", width = 15)
|
||||
@Schema(description = "主体内容")
|
||||
private String main;
|
||||
/**
|
||||
* 页边距
|
||||
*/
|
||||
@Excel(name = "页边距", width = 15)
|
||||
@Schema(description = "页边距")
|
||||
private String margins;
|
||||
/**
|
||||
* 宽度
|
||||
*/
|
||||
@Excel(name = "宽度", width = 15)
|
||||
@Schema(description = "宽度")
|
||||
private Integer width;
|
||||
/**
|
||||
* 高度
|
||||
*/
|
||||
@Excel(name = "高度", width = 15)
|
||||
@Schema(description = "高度")
|
||||
private Integer height;
|
||||
/**
|
||||
* 纸张方向 vertical纵向 horizontal横向
|
||||
*/
|
||||
@Excel(name = "纸张方向 vertical纵向 horizontal横向", width = 15)
|
||||
@Schema(description = "纸张方向 vertical纵向 horizontal横向")
|
||||
private String paperDirection;
|
||||
/**
|
||||
* 水印
|
||||
*/
|
||||
@Excel(name = "水印", width = 15)
|
||||
@Schema(description = "水印")
|
||||
private String watermark;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package org.jeecg.modules.airag.wordtpl.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.jeecg.modules.airag.wordtpl.entity.EoaWordTemplate;
|
||||
|
||||
/**
|
||||
* @Description: word模版管理
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-07-04
|
||||
* @Version: V1.0
|
||||
*/
|
||||
public interface EoaWordTemplateMapper extends BaseMapper<EoaWordTemplate> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<?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.jeecg.modules.airag.wordtpl.mapper.EoaWordTemplateMapper">
|
||||
|
||||
</mapper>
|
||||
@@ -0,0 +1,26 @@
|
||||
package org.jeecg.modules.airag.wordtpl.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import org.jeecg.modules.airag.wordtpl.dto.WordTplGenDTO;
|
||||
import org.jeecg.modules.airag.wordtpl.entity.EoaWordTemplate;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
|
||||
/**
|
||||
* @Description: word模版管理
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-07-04
|
||||
* @Version: V1.0
|
||||
*/
|
||||
public interface IEoaWordTemplateService extends IService<EoaWordTemplate> {
|
||||
|
||||
/**
|
||||
* 通过模版生成word文档
|
||||
*
|
||||
* @param wordTplGenDTO
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/7/10 14:40
|
||||
*/
|
||||
void generateWordFromTpl(WordTplGenDTO wordTplGenDTO, ByteArrayOutputStream wordOutputStream);
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package org.jeecg.modules.airag.wordtpl.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.deepoove.poi.XWPFTemplate;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.common.constant.DataBaseConstant;
|
||||
import org.jeecg.common.exception.JeecgBootException;
|
||||
import org.jeecg.common.system.util.JwtUtil;
|
||||
import org.jeecg.common.util.AssertUtils;
|
||||
import org.jeecg.modules.airag.wordtpl.dto.WordTplGenDTO;
|
||||
import org.jeecg.modules.airag.wordtpl.entity.EoaWordTemplate;
|
||||
import org.jeecg.modules.airag.wordtpl.mapper.EoaWordTemplateMapper;
|
||||
import org.jeecg.modules.airag.wordtpl.service.IEoaWordTemplateService;
|
||||
import org.jeecg.modules.airag.wordtpl.utils.WordTplUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @Description: word模版管理
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-07-04
|
||||
* @Version: V1.0
|
||||
*/
|
||||
@Slf4j
|
||||
@Service("eoaWordTemplateService")
|
||||
public class EoaWordTemplateServiceImpl extends ServiceImpl<EoaWordTemplateMapper, EoaWordTemplate> implements IEoaWordTemplateService {
|
||||
|
||||
/**
|
||||
* 内置的系统变量键列表
|
||||
*/
|
||||
private static final String[] SYSTEM_KEYS = {
|
||||
DataBaseConstant.SYS_ORG_CODE, DataBaseConstant.SYS_ORG_CODE_TABLE, DataBaseConstant.SYS_MULTI_ORG_CODE,
|
||||
DataBaseConstant.SYS_MULTI_ORG_CODE_TABLE, DataBaseConstant.SYS_ORG_ID, DataBaseConstant.SYS_ORG_ID_TABLE,
|
||||
DataBaseConstant.SYS_ROLE_CODE, DataBaseConstant.SYS_ROLE_CODE_TABLE, DataBaseConstant.SYS_USER_CODE,
|
||||
DataBaseConstant.SYS_USER_CODE_TABLE, DataBaseConstant.SYS_USER_ID, DataBaseConstant.SYS_USER_ID_TABLE,
|
||||
DataBaseConstant.SYS_USER_NAME, DataBaseConstant.SYS_USER_NAME_TABLE, DataBaseConstant.SYS_DATE,
|
||||
DataBaseConstant.SYS_DATE_TABLE, DataBaseConstant.SYS_TIME, DataBaseConstant.SYS_TIME_TABLE,
|
||||
DataBaseConstant.SYS_BASE_PATH
|
||||
};
|
||||
|
||||
@Autowired
|
||||
WordTplUtils wordTplUtils;
|
||||
|
||||
@Override
|
||||
public void generateWordFromTpl(WordTplGenDTO wordTplGenDTO, ByteArrayOutputStream wordOutputStream) {
|
||||
AssertUtils.assertNotEmpty("参数异常", wordTplGenDTO);
|
||||
AssertUtils.assertNotEmpty("模版ID不能为空", wordTplGenDTO.getTemplateId());
|
||||
String templateId = wordTplGenDTO.getTemplateId();
|
||||
// 生成word模版 date:2025/7/10
|
||||
EoaWordTemplate template = getById(templateId);
|
||||
ByteArrayOutputStream wordTemplateOut = new ByteArrayOutputStream();
|
||||
wordTplUtils.generateWordTemplate(template, wordTemplateOut);
|
||||
//根据word模版和数据生成word文件
|
||||
Map<String, Object> data = wordTplGenDTO.getData();
|
||||
mergeSystemVarsToData(data);
|
||||
try {
|
||||
XWPFTemplate.compile(new ByteArrayInputStream(wordTemplateOut.toByteArray())).render(data).write(wordOutputStream);
|
||||
}catch (Exception e){
|
||||
log.error(e.getMessage(), e);
|
||||
throw new JeecgBootException("生成word文档失败,请检查模版和数据是否正确");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 将系统变量合并到数据中
|
||||
*
|
||||
* @param data
|
||||
* @author chenrui
|
||||
* @date 2025/7/3 17:43
|
||||
*/
|
||||
private static void mergeSystemVarsToData(Map<String, Object> data) {
|
||||
for (String key : SYSTEM_KEYS) {
|
||||
if (!data.containsKey(key)) {
|
||||
String value = JwtUtil.getUserSystemData(key, null);
|
||||
if (value != null) {
|
||||
data.put(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,72 @@
|
||||
server:
|
||||
port: 7008
|
||||
spring:
|
||||
jackson:
|
||||
date-format: yyyy-MM-dd HH:mm:ss
|
||||
time-zone: GMT+8
|
||||
autoconfigure:
|
||||
exclude:
|
||||
- com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure
|
||||
- org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration
|
||||
datasource:
|
||||
druid:
|
||||
stat-view-servlet:
|
||||
enabled: true
|
||||
loginUsername: admin
|
||||
loginPassword: 123456
|
||||
allow:
|
||||
web-stat-filter:
|
||||
enabled: true
|
||||
dynamic:
|
||||
druid: # 全局druid参数,绝大部分值和默认保持一致。(现已支持的参数如下,不清楚含义不要乱设置)
|
||||
# 连接池的配置信息
|
||||
# 初始化大小,最小,最大
|
||||
initial-size: 5
|
||||
min-idle: 5
|
||||
maxActive: 1000
|
||||
# 配置获取连接等待超时的时间
|
||||
maxWait: 60000
|
||||
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
|
||||
timeBetweenEvictionRunsMillis: 60000
|
||||
# 配置一个连接在池中最小生存的时间,单位是毫秒
|
||||
minEvictableIdleTimeMillis: 300000
|
||||
# validationQuery: SELECT 1
|
||||
testWhileIdle: true
|
||||
testOnBorrow: false
|
||||
testOnReturn: false
|
||||
# 打开PSCache,并且指定每个连接上PSCache的大小
|
||||
poolPreparedStatements: true
|
||||
maxPoolPreparedStatementPerConnectionSize: 20
|
||||
# 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
|
||||
# !!!!!mysql
|
||||
# filters: stat,slf4j,wall
|
||||
# !!!!!DM
|
||||
filters: stat,slf4j
|
||||
# 允许SELECT语句的WHERE子句是一个永真条件
|
||||
# wall:
|
||||
# selectWhereAlwayTrueCheck: false
|
||||
# 打开mergeSql功能;慢SQL记录
|
||||
stat:
|
||||
merge-sql: true
|
||||
slow-sql-millis: 5000
|
||||
datasource:
|
||||
master:
|
||||
## !!!!!MYSQL
|
||||
url: jdbc:mysql://localhost:3306/jeecg-boot-dev?characterEncoding=UTF-8&useUnicode=true&useSSL=false&tinyInt1isBit=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai
|
||||
username: root
|
||||
password: 123456
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
redis:
|
||||
database: 0
|
||||
host: 192.168.1.188
|
||||
port: 6379
|
||||
password: 'res983'
|
||||
jeecg:
|
||||
ai-rag:
|
||||
embed-store:
|
||||
host: "localhost"
|
||||
port: 15432
|
||||
database: "postgres"
|
||||
user: "postgres"
|
||||
password: "123456"
|
||||
table: "embeddings"
|
||||
Reference in New Issue
Block a user