第一次提交

This commit is contained in:
2026-04-03 09:56:14 +08:00
commit 60e2c8debd
3598 changed files with 746659 additions and 0 deletions

View File

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

View File

@@ -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:{}";
}

View File

@@ -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 = "请针对如下内容:[{}] 进行润色。 回复格式:{},语气:{},语言:{},长度:{}。";
}

View File

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

View File

@@ -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 miniominio 阿里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);
}
}

View File

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

View File

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

View File

@@ -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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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());
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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("");
}
}

View File

@@ -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();
}
}

View File

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

View File

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

View File

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

View File

@@ -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类型ssesse类型stdio标准类型
*/
@Excel(name = "mcp类型ssesse类型stdio标准类型", width = 15)
@Schema(description = "mcp类型ssesse类型stdio标准类型")
private java.lang.String type;
/**
* 服务端点SSE类型为URLstdio类型为命令
*/
@Excel(name = "服务端点SSE类型为URLstdio类型为命令", width = 15)
@Schema(description = "服务端点SSE类型为URLstdio类型为命令")
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;
}

View File

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

View File

@@ -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 绘画创作 ========================================
}

View File

@@ -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) {
}
}
}
}

View File

@@ -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) {
// ignoretoken获取不到默认为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 models 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 doesnt exist
.createTable(true)
//Dont 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文件中的图片语法 ![alt text](image url)
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, "![" + matcher.group(1) + "](" + networkImageUrl + ")");
} else {
matcher.appendReplacement(sb, "![" + matcher.group(1) + "](" + imageUrl + ")");
}
}
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;
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -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> {
}

View File

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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

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

View File

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

View 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);
}

View File

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

View File

@@ -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> {
}

View File

@@ -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();
}
}

View File

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

View File

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

View File

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

View File

@@ -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("保存成功");
}
}

View File

@@ -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 {
}

View File

@@ -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("删除成功");
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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> {
}

View File

@@ -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> {
}

View File

@@ -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>

View File

@@ -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>

View File

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

View File

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

View File

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

View File

@@ -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.01.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;
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<>();
}

View File

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

View File

@@ -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<>();
}

View File

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

View File

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

View File

@@ -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> {
}

View File

@@ -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>

View File

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

View File

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

View File

@@ -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"