mirror of
https://github.com/jeecgboot/JeecgBoot.git
synced 2026-02-02 00:25:22 +08:00
v3.9.1 后台代码
This commit is contained in:
@ -0,0 +1,429 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
MCP Stdio 工具 - 修复编码问题
|
||||
确保所有输出都使用UTF-8编码
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
from typing import Dict, Any
|
||||
import logging
|
||||
|
||||
# 强制使用UTF-8编码
|
||||
if sys.platform == "win32":
|
||||
# Windows需要特殊处理
|
||||
import io
|
||||
|
||||
sys.stdin = io.TextIOWrapper(sys.stdin.buffer, encoding='utf-8')
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
|
||||
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
|
||||
else:
|
||||
# Unix-like系统
|
||||
sys.stdin.reconfigure(encoding='utf-8')
|
||||
sys.stdout.reconfigure(encoding='utf-8')
|
||||
sys.stderr.reconfigure(encoding='utf-8')
|
||||
|
||||
# 设置环境变量
|
||||
os.environ['PYTHONIOENCODING'] = 'utf-8'
|
||||
os.environ['PYTHONUTF8'] = '1'
|
||||
|
||||
# 配置日志
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||
encoding='utf-8'
|
||||
)
|
||||
logger = logging.getLogger("mcp-tool")
|
||||
|
||||
|
||||
class FixedMCPServer:
|
||||
"""修复编码问题的MCP服务器"""
|
||||
|
||||
def __init__(self):
|
||||
self.tools = {}
|
||||
self.initialize_tools()
|
||||
|
||||
def initialize_tools(self):
|
||||
"""初始化工具集"""
|
||||
|
||||
# 获取时间
|
||||
self.tools["get_time"] = {
|
||||
"name": "get_time",
|
||||
"description": "获取当前时间",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"format": {
|
||||
"type": "string",
|
||||
"description": "时间格式",
|
||||
"enum": ["iso", "timestamp", "human", "chinese"],
|
||||
"default": "iso"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# 文本处理工具
|
||||
self.tools["text_process"] = {
|
||||
"name": "text_process",
|
||||
"description": "文本处理工具",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"text": {
|
||||
"type": "string",
|
||||
"description": "输入文本"
|
||||
},
|
||||
"operation": {
|
||||
"type": "string",
|
||||
"description": "操作类型",
|
||||
"enum": ["length", "upper", "lower", "reverse", "count_words"],
|
||||
"default": "length"
|
||||
}
|
||||
},
|
||||
"required": ["text"]
|
||||
}
|
||||
}
|
||||
|
||||
# 数据格式工具
|
||||
self.tools["format_data"] = {
|
||||
"name": "format_data",
|
||||
"description": "格式化数据",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "string",
|
||||
"description": "原始数据"
|
||||
},
|
||||
"format": {
|
||||
"type": "string",
|
||||
"description": "格式类型",
|
||||
"enum": ["json", "yaml", "xml"],
|
||||
"default": "json"
|
||||
}
|
||||
},
|
||||
"required": ["data"]
|
||||
}
|
||||
}
|
||||
|
||||
def handle_request(self, request: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""处理请求"""
|
||||
try:
|
||||
method = request.get("method")
|
||||
params = request.get("params", {})
|
||||
|
||||
if method == "tools/list":
|
||||
return self.handle_tools_list()
|
||||
elif method == "tools/call":
|
||||
return self.handle_tool_call(params)
|
||||
elif method == "ping":
|
||||
return {"result": "pong"}
|
||||
else:
|
||||
return self.create_error_response(
|
||||
code=-32601,
|
||||
message="Method not found"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error handling request: {e}")
|
||||
return self.create_error_response(
|
||||
code=-32603,
|
||||
message=f"Internal error: {str(e)}"
|
||||
)
|
||||
|
||||
def handle_tools_list(self) -> Dict[str, Any]:
|
||||
"""列出所有工具 - 确保返回标准JSON"""
|
||||
return {
|
||||
"result": {
|
||||
"tools": list(self.tools.values())
|
||||
}
|
||||
}
|
||||
|
||||
def handle_tool_call(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""调用工具 - 修复响应格式"""
|
||||
name = params.get("name")
|
||||
arguments = params.get("arguments", {})
|
||||
|
||||
if name not in self.tools:
|
||||
return self.create_error_response(
|
||||
code=-32602,
|
||||
message=f"Tool '{name}' not found"
|
||||
)
|
||||
|
||||
try:
|
||||
if name == "get_time":
|
||||
result = self.execute_get_time(arguments)
|
||||
elif name == "text_process":
|
||||
result = self.execute_text_process(arguments)
|
||||
elif name == "format_data":
|
||||
result = self.execute_format_data(arguments)
|
||||
else:
|
||||
return self.create_error_response(
|
||||
code=-32602,
|
||||
message="Tool not implemented"
|
||||
)
|
||||
|
||||
# 确保返回正确的MCP响应格式
|
||||
return self.create_success_response(result)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Tool execution error: {e}")
|
||||
return self.create_error_response(
|
||||
code=-32603,
|
||||
message=f"Tool execution failed: {str(e)}"
|
||||
)
|
||||
|
||||
def execute_get_time(self, args: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""获取时间 - 支持中文"""
|
||||
from datetime import datetime
|
||||
|
||||
try:
|
||||
format_type = args.get("format", "iso")
|
||||
now = datetime.now()
|
||||
|
||||
if format_type == "iso":
|
||||
result = now.isoformat()
|
||||
elif format_type == "timestamp":
|
||||
result = now.timestamp()
|
||||
elif format_type == "human":
|
||||
result = now.strftime("%Y-%m-%d %H:%M:%S")
|
||||
elif format_type == "chinese":
|
||||
result = now.strftime("%Y年%m月%d日 %H时%M分%S秒")
|
||||
else:
|
||||
result = now.isoformat()
|
||||
logger.info(f"当前系统时间:{result}")
|
||||
return {
|
||||
"status": "success",
|
||||
"format": format_type,
|
||||
"time": result,
|
||||
"timestamp": now.timestamp(),
|
||||
"date": now.strftime("%Y-%m-%d"),
|
||||
"time_12h": now.strftime("%I:%M:%S %p")
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {
|
||||
"status": "error",
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
def execute_text_process(self, args: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""文本处理"""
|
||||
try:
|
||||
text = args.get("text", "")
|
||||
operation = args.get("operation", "length")
|
||||
|
||||
if operation == "length":
|
||||
result = len(text)
|
||||
result_str = f"文本长度: {result} 个字符"
|
||||
elif operation == "upper":
|
||||
result = text.upper()
|
||||
result_str = f"大写: {result}"
|
||||
elif operation == "lower":
|
||||
result = text.lower()
|
||||
result_str = f"小写: {result}"
|
||||
elif operation == "reverse":
|
||||
result = text[::-1]
|
||||
result_str = f"反转: {result}"
|
||||
elif operation == "count_words":
|
||||
words = len(text.split())
|
||||
result = words
|
||||
result_str = f"单词数: {words}"
|
||||
else:
|
||||
raise ValueError(f"未知操作: {operation}")
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"operation": operation,
|
||||
"original_text": text,
|
||||
"result": result,
|
||||
"result_str": result_str,
|
||||
"text_length": len(text)
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {
|
||||
"status": "error",
|
||||
"error": str(e),
|
||||
"operation": args.get("operation", "")
|
||||
}
|
||||
|
||||
def execute_format_data(self, args: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""格式化数据"""
|
||||
try:
|
||||
data_str = args.get("data", "")
|
||||
format_type = args.get("format", "json")
|
||||
|
||||
# 尝试解析为JSON
|
||||
try:
|
||||
data = json.loads(data_str)
|
||||
is_json = True
|
||||
except:
|
||||
data = data_str
|
||||
is_json = False
|
||||
|
||||
if format_type == "json":
|
||||
if is_json:
|
||||
result = json.dumps(data, ensure_ascii=False, indent=2)
|
||||
else:
|
||||
# 如果不是JSON,包装成JSON
|
||||
result = json.dumps({"text": data}, ensure_ascii=False, indent=2)
|
||||
elif format_type == "yaml":
|
||||
import yaml
|
||||
result = yaml.dump(data, allow_unicode=True, default_flow_style=False)
|
||||
elif format_type == "xml":
|
||||
# 简单的XML格式化
|
||||
if isinstance(data, dict):
|
||||
result = "<data>"
|
||||
for k, v in data.items():
|
||||
result += f"\n <{k}>{v}</{k}>"
|
||||
result += "\n</data>"
|
||||
else:
|
||||
result = f"<text>{data}</text>"
|
||||
else:
|
||||
result = str(data)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"format": format_type,
|
||||
"original": data_str,
|
||||
"formatted": result,
|
||||
"length": len(result)
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {
|
||||
"status": "error",
|
||||
"error": str(e),
|
||||
"format": args.get("format", "")
|
||||
}
|
||||
|
||||
def create_success_response(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""创建成功响应 - 确保符合MCP规范"""
|
||||
# 将数据转换为JSON字符串作为文本内容
|
||||
content_text = json.dumps(data, ensure_ascii=False, indent=2)
|
||||
|
||||
return {
|
||||
"result": {
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": content_text
|
||||
}
|
||||
],
|
||||
"isError": False
|
||||
}
|
||||
}
|
||||
|
||||
def create_error_response(self, code: int, message: str) -> Dict[str, Any]:
|
||||
"""创建错误响应"""
|
||||
return {
|
||||
"error": {
|
||||
"code": code,
|
||||
"message": message
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def safe_json_dump(data: Dict[str, Any]) -> str:
|
||||
"""安全的JSON序列化,确保UTF-8编码"""
|
||||
try:
|
||||
return json.dumps(data, ensure_ascii=False, separators=(',', ':'))
|
||||
except:
|
||||
# 如果失败,使用ASCII转义
|
||||
return json.dumps(data, ensure_ascii=True, separators=(',', ':'))
|
||||
|
||||
|
||||
def main():
|
||||
"""主函数 - 修复Stdio通信"""
|
||||
logger.info("启动MCP Stdio服务器 (修复编码版)...")
|
||||
|
||||
server = FixedMCPServer()
|
||||
|
||||
# 初始握手消息
|
||||
init_message = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"result": {
|
||||
"protocolVersion": "2024-11-05",
|
||||
"capabilities": {
|
||||
"tools": {}
|
||||
},
|
||||
"serverInfo": {
|
||||
"name": "fixed-mcp-server",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# 发送初始化响应
|
||||
try:
|
||||
sys.stdout.write(safe_json_dump(init_message) + "\n")
|
||||
sys.stdout.flush()
|
||||
except Exception as e:
|
||||
logger.error(f"发送初始化消息失败: {e}")
|
||||
return
|
||||
|
||||
logger.info("MCP服务器已初始化")
|
||||
|
||||
# 主循环
|
||||
line_num = 0
|
||||
while True:
|
||||
try:
|
||||
line = sys.stdin.readline()
|
||||
if not line:
|
||||
logger.info("输入流结束")
|
||||
break
|
||||
|
||||
line = line.strip()
|
||||
line_num += 1
|
||||
|
||||
if not line:
|
||||
continue
|
||||
|
||||
logger.info(f"收到第 {line_num} 行: {line[:100]}...")
|
||||
|
||||
try:
|
||||
request = json.loads(line)
|
||||
logger.info(f"解析请求: {request.get('method', 'unknown')}")
|
||||
|
||||
# 处理请求
|
||||
response = server.handle_request(request)
|
||||
response["jsonrpc"] = "2.0"
|
||||
response["id"] = request.get("id")
|
||||
|
||||
# 发送响应
|
||||
response_json = safe_json_dump(response)
|
||||
sys.stdout.write(response_json + "\n")
|
||||
sys.stdout.flush()
|
||||
|
||||
logger.info(f"发送响应: {response.get('result', response.get('error', {}))}")
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"JSON解析错误: {e}")
|
||||
error_response = {
|
||||
"jsonrpc": "2.0",
|
||||
"error": {
|
||||
"code": -32700,
|
||||
"message": f"Parse error at line {line_num}"
|
||||
},
|
||||
"id": None
|
||||
}
|
||||
sys.stdout.write(safe_json_dump(error_response) + "\n")
|
||||
sys.stdout.flush()
|
||||
|
||||
except KeyboardInterrupt:
|
||||
logger.info("接收到中断信号")
|
||||
break
|
||||
except Exception as e:
|
||||
logger.error(f"未处理的错误: {e}")
|
||||
break
|
||||
|
||||
logger.info("MCP服务器已停止")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Binary file not shown.
@ -6,7 +6,7 @@
|
||||
<parent>
|
||||
<groupId>org.jeecgframework.boot3</groupId>
|
||||
<artifactId>jeecg-boot-module</artifactId>
|
||||
<version>3.9.0</version>
|
||||
<version>3.9.1</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>jeecg-boot-module-airag</artifactId>
|
||||
@ -33,7 +33,7 @@
|
||||
<properties>
|
||||
<kotlin.version>2.2.0</kotlin.version>
|
||||
<liteflow.version>2.15.0</liteflow.version>
|
||||
<apache-tika.version>2.9.1</apache-tika.version>
|
||||
<apache-tika.version>3.2.3</apache-tika.version>
|
||||
</properties>
|
||||
|
||||
<dependencyManagement>
|
||||
@ -41,14 +41,14 @@
|
||||
<dependency>
|
||||
<groupId>dev.langchain4j</groupId>
|
||||
<artifactId>langchain4j-bom</artifactId>
|
||||
<version>1.3.0</version>
|
||||
<version>1.9.1</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>dev.langchain4j</groupId>
|
||||
<artifactId>langchain4j-community-bom</artifactId>
|
||||
<version>1.3.0-beta9</version>
|
||||
<version>1.9.1-beta17</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
@ -75,7 +75,7 @@
|
||||
<dependency>
|
||||
<groupId>org.jeecgframework.boot3</groupId>
|
||||
<artifactId>jeecg-aiflow</artifactId>
|
||||
<version>3.9.0.1</version>
|
||||
<version>3.9.1-beta</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>commons-io</groupId>
|
||||
@ -107,16 +107,16 @@
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.yomahub</groupId>
|
||||
<artifactId>liteflow-script-python</artifactId>
|
||||
<artifactId>liteflow-script-groovy</artifactId>
|
||||
<version>${liteflow.version}</version>
|
||||
<scope>provided</scope>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<!-- end 注意:这几个依赖体积较大,每个约50MB。若发布时需要使用,请将 <scope>provided</scope> 删除 -->
|
||||
|
||||
<!-- aiflow 脚本依赖 -->
|
||||
<dependency>
|
||||
<groupId>com.yomahub</groupId>
|
||||
<artifactId>liteflow-script-groovy</artifactId>
|
||||
<artifactId>liteflow-script-python</artifactId>
|
||||
<version>${liteflow.version}</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
@ -151,6 +151,11 @@
|
||||
<groupId>dev.langchain4j</groupId>
|
||||
<artifactId>langchain4j-open-ai</artifactId>
|
||||
</dependency>
|
||||
<!-- langChain4j mcp support -->
|
||||
<dependency>
|
||||
<groupId>dev.langchain4j</groupId>
|
||||
<artifactId>langchain4j-mcp</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>dev.langchain4j</groupId>
|
||||
<artifactId>langchain4j-ollama</artifactId>
|
||||
@ -197,7 +202,11 @@
|
||||
<artifactId>langchain4j-pgvector</artifactId>
|
||||
<version>1.3.0-beta9</version>
|
||||
</dependency>
|
||||
<!-- langChain4j Document Parser -->
|
||||
<!-- langChain4j Document Parser 适用于excel、ppt、word -->
|
||||
<dependency>
|
||||
<groupId>dev.langchain4j</groupId>
|
||||
<artifactId>langchain4j-document-parser-apache-poi</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.tika</groupId>
|
||||
<artifactId>tika-core</artifactId>
|
||||
@ -224,7 +233,12 @@
|
||||
<artifactId>tika-parser-text-module</artifactId>
|
||||
<version>${apache-tika.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- word模版引擎 -->
|
||||
<dependency>
|
||||
<groupId>com.deepoove</groupId>
|
||||
<artifactId>poi-tl</artifactId>
|
||||
<version>1.12.2</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@ -44,4 +44,19 @@ public class AiAppConsts {
|
||||
*/
|
||||
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 WRITER_APP_ID = "2010634128233779202";
|
||||
}
|
||||
|
||||
@ -104,4 +104,68 @@ public class Prompts {
|
||||
" - 反幻觉校验:\"所有数据需标注来源,不确定信息用[需核实]标记\"\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 = "请针对如下内容:[{}] 做个回复。回复内容参考:[{}], 回复格式:{},语气:{},语言:{},长度:{}。";
|
||||
|
||||
}
|
||||
|
||||
@ -22,7 +22,6 @@ import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* @Description: AI应用
|
||||
@ -179,4 +178,16 @@ public class AiragAppController extends JeecgController<AiragApp, IAiragAppServi
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -8,6 +8,7 @@ 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;
|
||||
@ -102,6 +103,19 @@ public class AiragChatController {
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除会话
|
||||
*
|
||||
@ -113,7 +127,22 @@ public class AiragChatController {
|
||||
@IgnoreAuth
|
||||
@DeleteMapping(value = "/conversation/{id}")
|
||||
public Result<?> deleteConversation(@PathVariable("id") String id) {
|
||||
return chatService.deleteConversation(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);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -139,8 +168,9 @@ public class AiragChatController {
|
||||
*/
|
||||
@IgnoreAuth
|
||||
@GetMapping(value = "/messages")
|
||||
public Result<?> getMessages(@RequestParam(value = "conversationId", required = true) String conversationId) {
|
||||
return chatService.getMessages(conversationId);
|
||||
public Result<?> getMessages(@RequestParam(value = "conversationId", required = true) String conversationId,
|
||||
@RequestParam(value = "sessionType", required = false) String sessionType) {
|
||||
return chatService.getMessages(conversationId, sessionType);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -153,7 +183,21 @@ public class AiragChatController {
|
||||
@IgnoreAuth
|
||||
@GetMapping(value = "/messages/clear/{conversationId}")
|
||||
public Result<?> clearMessage(@PathVariable(value = "conversationId") String conversationId) {
|
||||
return chatService.clearMessage(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);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -217,4 +261,25 @@ public class AiragChatController {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -173,6 +173,29 @@ public class AiragApp implements Serializable {
|
||||
@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
|
||||
*/
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
package org.jeecg.modules.airag.app.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.modules.airag.app.entity.AiragApp;
|
||||
|
||||
/**
|
||||
@ -21,4 +20,14 @@ public interface IAiragAppService extends IService<AiragApp> {
|
||||
* @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);
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
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;
|
||||
@ -59,21 +60,23 @@ public interface IAiragChatService {
|
||||
* 获取对话聊天记录
|
||||
*
|
||||
* @param conversationId
|
||||
* @param sessionType 类型
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/2/26 15:16
|
||||
*/
|
||||
Result<?> getMessages(String conversationId);
|
||||
Result<?> getMessages(String conversationId, String sessionType);
|
||||
|
||||
/**
|
||||
* 删除会话
|
||||
*
|
||||
* @param conversationId
|
||||
* @param sessionType
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/3/3 16:55
|
||||
*/
|
||||
Result<?> deleteConversation(String conversationId);
|
||||
Result<?> deleteConversation(String conversationId, String sessionType);
|
||||
|
||||
/**
|
||||
* 更新会话标题
|
||||
@ -87,11 +90,12 @@ public interface IAiragChatService {
|
||||
/**
|
||||
* 清空消息
|
||||
* @param conversationId
|
||||
* @param sessionType
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/3/3 19:49
|
||||
*/
|
||||
Result<?> clearMessage(String conversationId);
|
||||
Result<?> clearMessage(String conversationId, String sessionType);
|
||||
|
||||
/**
|
||||
* 初始化聊天(忽略租户)
|
||||
@ -111,4 +115,27 @@ public interface IAiragChatService {
|
||||
* @date 2025/8/11 17:39
|
||||
*/
|
||||
SseEmitter receiveByRequestId(String requestId);
|
||||
|
||||
/**
|
||||
* 根据类型获取会话列表
|
||||
*
|
||||
* @param sessionType
|
||||
* @return
|
||||
*/
|
||||
Result<?> getConversationsByType(String sessionType);
|
||||
|
||||
/**
|
||||
* 生成海报图片
|
||||
* @param chatSendParams
|
||||
* @return
|
||||
*/
|
||||
String genAiPoster(ChatSendParams chatSendParams);
|
||||
|
||||
/**
|
||||
* 生成ai创作
|
||||
*
|
||||
* @param chatSendParams
|
||||
* @return
|
||||
*/
|
||||
SseEmitter genAiWriter(AiWriteGenerateVo chatSendParams);
|
||||
}
|
||||
|
||||
@ -0,0 +1,44 @@
|
||||
package org.jeecg.modules.airag.app.service;
|
||||
|
||||
import org.jeecg.modules.airag.app.entity.AiragApp;
|
||||
import org.jeecg.modules.airag.common.handler.AIChatParams;
|
||||
|
||||
public interface IAiragVariableService {
|
||||
/**
|
||||
* 更新变量值
|
||||
*
|
||||
* @param userId
|
||||
* @param appId
|
||||
* @param name
|
||||
* @param value
|
||||
*/
|
||||
void updateVariable(String userId, String appId, String name, String value);
|
||||
|
||||
/**
|
||||
* 追加提示词
|
||||
*
|
||||
* @param username
|
||||
* @param app
|
||||
* @return
|
||||
*/
|
||||
String additionalPrompt(String username, AiragApp app);
|
||||
|
||||
/**
|
||||
* 初始化变量(仅不存在时设置)
|
||||
*
|
||||
* @param userId
|
||||
* @param appId
|
||||
* @param name
|
||||
* @param defaultValue
|
||||
*/
|
||||
void initVariable(String userId, String appId, String name, String defaultValue);
|
||||
|
||||
/**
|
||||
* 添加变量更新工具
|
||||
*
|
||||
* @param params
|
||||
* @param aiApp
|
||||
* @param username
|
||||
*/
|
||||
void addUpdateVariableTool(AiragApp aiApp, String username, AIChatParams params);
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
package org.jeecg.modules.airag.app.service.impl;
|
||||
|
||||
import com.alibaba.fastjson.JSONArray;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import dev.langchain4j.data.message.AiMessage;
|
||||
@ -10,12 +11,15 @@ import dev.langchain4j.model.output.FinishReason;
|
||||
import dev.langchain4j.service.TokenStream;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.exception.JeecgBootBizTipException;
|
||||
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.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.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;
|
||||
@ -23,6 +27,8 @@ 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.stereotype.Service;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
@ -31,6 +37,7 @@ import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* @Description: AI应用
|
||||
@ -45,6 +52,9 @@ public class AiragAppServiceImpl extends ServiceImpl<AiragAppMapper, AiragApp> i
|
||||
@Autowired
|
||||
IAIChatHandler aiChatHandler;
|
||||
|
||||
@Autowired
|
||||
private IAiragKnowledgeService airagKnowledgeService;
|
||||
|
||||
@Override
|
||||
public Object generatePrompt(String prompt, boolean blocking) {
|
||||
AssertUtils.assertNotEmpty("请输入提示词", prompt);
|
||||
@ -62,81 +72,167 @@ public class AiragAppServiceImpl extends ServiceImpl<AiragAppMapper, AiragApp> i
|
||||
}
|
||||
return Result.OK("success", promptValue);
|
||||
}else{
|
||||
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> ";
|
||||
//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);
|
||||
}
|
||||
}
|
||||
EventData eventData = new EventData(requestId, null, EventData.EVENT_MESSAGE);
|
||||
EventMessageData messageEventData = EventMessageData.builder()
|
||||
.message(resMessage)
|
||||
.build();
|
||||
eventData.setData(messageEventData);
|
||||
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 {
|
||||
String eventStr = JSONObject.toJSONString(eventData);
|
||||
log.debug("[AI应用]接收LLM返回消息:{}", eventStr);
|
||||
emitter.send(SseEmitter.event().data(eventStr));
|
||||
log.debug("[AI应用]接收LLM返回消息完成:{}", respText);
|
||||
emitter.send(SseEmitter.event().data(eventData));
|
||||
} 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;
|
||||
}
|
||||
} 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 {
|
||||
|
||||
@ -1,24 +1,36 @@
|
||||
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 dev.langchain4j.agent.tool.ToolExecutionRequest;
|
||||
import dev.langchain4j.agent.tool.ToolSpecification;
|
||||
import dev.langchain4j.data.document.Document;
|
||||
import dev.langchain4j.data.image.Image;
|
||||
import dev.langchain4j.data.message.*;
|
||||
import dev.langchain4j.model.output.FinishReason;
|
||||
import dev.langchain4j.service.TokenStream;
|
||||
import dev.langchain4j.service.tool.ToolExecutor;
|
||||
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.common.api.vo.Result;
|
||||
import org.jeecg.common.constant.SymbolConstant;
|
||||
import org.jeecg.common.exception.JeecgBootBizTipException;
|
||||
import org.jeecg.common.exception.JeecgBootException;
|
||||
import org.jeecg.common.system.api.ISysBaseAPI;
|
||||
import org.jeecg.common.system.util.JwtUtil;
|
||||
import org.jeecg.common.util.*;
|
||||
import org.jeecg.config.JeecgBaseConfig;
|
||||
import org.jeecg.config.vo.Path;
|
||||
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.IAiragChatService;
|
||||
import org.jeecg.modules.airag.app.service.IAiragVariableService;
|
||||
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;
|
||||
@ -35,18 +47,26 @@ 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.FlowRunParams;
|
||||
import org.jeecg.modules.airag.llm.consts.LLMConsts;
|
||||
import org.jeecg.modules.airag.llm.document.TikaDocumentParser;
|
||||
import org.jeecg.modules.airag.llm.entity.AiragModel;
|
||||
import org.jeecg.modules.airag.llm.handler.AIChatHandler;
|
||||
import org.jeecg.modules.airag.llm.handler.JeecgToolsProvider;
|
||||
import org.jeecg.modules.airag.llm.mapper.AiragModelMapper;
|
||||
import org.jeecg.modules.airag.llm.service.IAiragFlowPluginService;
|
||||
import org.jeecg.modules.airag.llm.service.IAiragKnowledgeService;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.redis.core.BoundValueOperations;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
@ -85,6 +105,18 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
|
||||
@Autowired
|
||||
AiragModelMapper airagModelMapper;
|
||||
|
||||
@Autowired
|
||||
IAiragFlowPluginService airagFlowPluginService;
|
||||
|
||||
@Autowired
|
||||
IAiragKnowledgeService airagKnowledgeService;
|
||||
|
||||
@Autowired
|
||||
IAiragVariableService airagVariableService;
|
||||
|
||||
@Autowired
|
||||
JeecgBaseConfig jeecgBaseConfig;
|
||||
|
||||
/**
|
||||
* 重新接收消息
|
||||
@ -105,10 +137,13 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
if (oConvertUtils.isNotEmpty(chatSendParams.getAppId())) {
|
||||
app = airagAppMapper.getByIdIgnoreTenant(chatSendParams.getAppId());
|
||||
}
|
||||
ChatConversation chatConversation = getOrCreateChatConversation(app, conversationId);
|
||||
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||
ChatConversation chatConversation = getOrCreateChatConversation(app, conversationId, chatSendParams.getSessionType());
|
||||
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||
// 更新标题
|
||||
if (oConvertUtils.isEmpty(chatConversation.getTitle())) {
|
||||
chatConversation.setTitle(userMessage.length() > 5 ? userMessage.substring(0, 5) : userMessage);
|
||||
int maxLength = AiAppConsts.CONVERSATION_MAX_TITLE_LENGTH;
|
||||
chatConversation.setTitle(userMessage.length() > maxLength ? userMessage.substring(0, maxLength) : userMessage);
|
||||
}
|
||||
//update-begin---author:chenrui ---date:20251106 for:[issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程------------
|
||||
// 保存工作流入参配置(如果有)
|
||||
@ -116,6 +151,12 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
chatConversation.setFlowInputs(chatSendParams.getFlowInputs());
|
||||
}
|
||||
//update-end---author:chenrui ---date:20251106 for:[issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程------------
|
||||
//是否保存会话
|
||||
if(null != chatSendParams.getIzSaveSession()){
|
||||
chatConversation.setIzSaveSession(chatSendParams.getIzSaveSession());
|
||||
}
|
||||
// 保存变量
|
||||
saveVariables(app);
|
||||
// 发送消息
|
||||
return doChat(chatConversation, topicId, chatSendParams);
|
||||
}
|
||||
@ -130,7 +171,9 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
String topicId = oConvertUtils.getString(appDebugParams.getTopicId(), UUIDGenerator.generate());
|
||||
AiragApp app = appDebugParams.getApp();
|
||||
app.setId("__DEBUG_APP");
|
||||
ChatConversation chatConversation = getOrCreateChatConversation(app, topicId);
|
||||
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||
ChatConversation chatConversation = getOrCreateChatConversation(app, topicId, "");
|
||||
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||
//update-begin---author:chenrui ---date:20251106 for:[issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程------------
|
||||
// 保存工作流入参配置(如果有)
|
||||
if (oConvertUtils.isObjectNotEmpty(appDebugParams.getFlowInputs())) {
|
||||
@ -140,7 +183,9 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
// 发送消息
|
||||
SseEmitter emitter = doChat(chatConversation, topicId, appDebugParams);
|
||||
//保存会话
|
||||
saveChatConversation(chatConversation, true, null);
|
||||
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||
saveChatConversation(chatConversation, true, null, "");
|
||||
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||
return emitter;
|
||||
}
|
||||
|
||||
@ -247,9 +292,11 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<?> getMessages(String conversationId) {
|
||||
public Result<?> getMessages(String conversationId, String sessionType) {
|
||||
AssertUtils.assertNotEmpty("请先选择会话", conversationId);
|
||||
String key = getConversationCacheKey(conversationId, null);
|
||||
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||
String key = getConversationCacheKey(conversationId, null, sessionType);
|
||||
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||
if (oConvertUtils.isEmpty(key)) {
|
||||
return Result.ok(Collections.emptyList());
|
||||
}
|
||||
@ -273,6 +320,7 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
.role(msg.getRole())
|
||||
.content(msg.getContent())
|
||||
.images(msg.getImages())
|
||||
.files(msg.getFiles())
|
||||
.datetime(msg.getDatetime())
|
||||
.build();
|
||||
// 不设置toolExecutionRequests和toolExecutionResult
|
||||
@ -282,21 +330,30 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
}
|
||||
result.put("messages", messages);
|
||||
result.put("flowInputs", chatConversation.getFlowInputs());
|
||||
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||
if(oConvertUtils.isNotEmpty(sessionType)){
|
||||
result.put("appData", chatConversation.getApp());
|
||||
}
|
||||
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||
return Result.ok(result);
|
||||
//update-end---author:chenrui ---date:20251106 for:[issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程------------
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<?> clearMessage(String conversationId) {
|
||||
public Result<?> clearMessage(String conversationId, String sessionType) {
|
||||
AssertUtils.assertNotEmpty("请先选择会话", conversationId);
|
||||
String key = getConversationCacheKey(conversationId, null);
|
||||
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||
String key = getConversationCacheKey(conversationId, null,sessionType);
|
||||
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||
if (oConvertUtils.isEmpty(key)) {
|
||||
return Result.ok(Collections.emptyList());
|
||||
}
|
||||
ChatConversation chatConversation = (ChatConversation) redisTemplate.boundValueOps(key).get();
|
||||
if (null != chatConversation && oConvertUtils.isObjectNotEmpty(chatConversation.getMessages())) {
|
||||
chatConversation.getMessages().clear();
|
||||
saveChatConversation(chatConversation);
|
||||
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||
saveChatConversation(chatConversation,sessionType);
|
||||
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||
}
|
||||
return Result.ok();
|
||||
}
|
||||
@ -443,9 +500,11 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<?> deleteConversation(String conversationId) {
|
||||
public Result<?> deleteConversation(String conversationId, String sessionType) {
|
||||
AssertUtils.assertNotEmpty("请选择要删除的会话", conversationId);
|
||||
String key = getConversationCacheKey(conversationId, null);
|
||||
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||
String key = getConversationCacheKey(conversationId, null, sessionType);
|
||||
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||
if (oConvertUtils.isNotEmpty(key)) {
|
||||
Boolean delete = redisTemplate.delete(key);
|
||||
if (delete) {
|
||||
@ -463,14 +522,18 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
AssertUtils.assertNotEmpty("请先选择会话", updateTitleParams);
|
||||
AssertUtils.assertNotEmpty("请先选择会话", updateTitleParams.getId());
|
||||
AssertUtils.assertNotEmpty("请输入会话标题", updateTitleParams.getTitle());
|
||||
String key = getConversationCacheKey(updateTitleParams.getId(), null);
|
||||
String key = getConversationCacheKey(updateTitleParams.getId(), null, updateTitleParams.getSessionType());
|
||||
if (oConvertUtils.isEmpty(key)) {
|
||||
log.warn("[ai-chat]删除会话:未找到会话:{}", updateTitleParams.getId());
|
||||
return Result.ok();
|
||||
}
|
||||
ChatConversation chatConversation = (ChatConversation) redisTemplate.boundValueOps(key).get();
|
||||
chatConversation.setTitle(updateTitleParams.getTitle());
|
||||
saveChatConversation(chatConversation);
|
||||
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||
if (chatConversation != null) {
|
||||
chatConversation.setTitle(updateTitleParams.getTitle());
|
||||
}
|
||||
saveChatConversation(chatConversation,updateTitleParams.getSessionType());
|
||||
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
@ -479,15 +542,21 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
*
|
||||
* @param conversationId
|
||||
* @param httpRequest
|
||||
* @param sessionType 会话类型
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/2/25 19:27
|
||||
*/
|
||||
private String getConversationCacheKey(String conversationId, HttpServletRequest httpRequest) {
|
||||
private String getConversationCacheKey(String conversationId, HttpServletRequest httpRequest, String sessionType) {
|
||||
if (oConvertUtils.isEmpty(conversationId)) {
|
||||
return null;
|
||||
}
|
||||
String key = getConversationDirCacheKey(httpRequest);
|
||||
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||
if(oConvertUtils.isNotEmpty(sessionType)){
|
||||
key = key + ":" + sessionType;
|
||||
}
|
||||
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||
key = key + ":" + conversationId;
|
||||
return key;
|
||||
}
|
||||
@ -522,18 +591,21 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
*
|
||||
* @param app
|
||||
* @param conversationId
|
||||
* @param sessionType
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/2/25 19:19
|
||||
*/
|
||||
@NotNull
|
||||
private ChatConversation getOrCreateChatConversation(AiragApp app, String conversationId) {
|
||||
private ChatConversation getOrCreateChatConversation(AiragApp app, String conversationId, String sessionType) {
|
||||
if (oConvertUtils.isObjectEmpty(app)) {
|
||||
app = new AiragApp();
|
||||
app.setId(AiAppConsts.DEFAULT_APP_ID);
|
||||
}
|
||||
ChatConversation chatConversation = null;
|
||||
String key = getConversationCacheKey(conversationId, null);
|
||||
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||
String key = getConversationCacheKey(conversationId, null,sessionType);
|
||||
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||
if (oConvertUtils.isNotEmpty(key)) {
|
||||
chatConversation = (ChatConversation) redisTemplate.boundValueOps(key).get();
|
||||
}
|
||||
@ -569,8 +641,8 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
* @author chenrui
|
||||
* @date 2025/2/25 19:27
|
||||
*/
|
||||
private void saveChatConversation(ChatConversation chatConversation) {
|
||||
saveChatConversation(chatConversation, false, null);
|
||||
private void saveChatConversation(ChatConversation chatConversation, String sessionType) {
|
||||
saveChatConversation(chatConversation, false, null, sessionType);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -581,11 +653,19 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
* @author chenrui
|
||||
* @date 2025/2/25 19:27
|
||||
*/
|
||||
private void saveChatConversation(ChatConversation chatConversation, boolean temp, HttpServletRequest httpRequest) {
|
||||
private void saveChatConversation(ChatConversation chatConversation, boolean temp, HttpServletRequest httpRequest, String sessionType) {
|
||||
if (null == chatConversation) {
|
||||
return;
|
||||
}
|
||||
String key = getConversationCacheKey(chatConversation.getId(), httpRequest);
|
||||
|
||||
//如果是不保存会话直接返回
|
||||
if(null != chatConversation.getIzSaveSession() && !chatConversation.getIzSaveSession()){
|
||||
return;
|
||||
}
|
||||
|
||||
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||
String key = getConversationCacheKey(chatConversation.getId(), httpRequest, sessionType);
|
||||
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||
if (oConvertUtils.isEmpty(key)) {
|
||||
return;
|
||||
}
|
||||
@ -680,6 +760,10 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
* @date 2025/2/25 19:05
|
||||
*/
|
||||
private void appendMessage(List<ChatMessage> messages, ChatMessage message, ChatConversation chatConversation, String topicId) {
|
||||
appendMessage(messages, message, chatConversation, topicId, null, null);
|
||||
}
|
||||
|
||||
private void appendMessage(List<ChatMessage> messages, ChatMessage message, ChatConversation chatConversation, String topicId, List<String> files, String saveContent) {
|
||||
|
||||
if (message.type().equals(ChatMessageType.SYSTEM)) {
|
||||
// 系统消息,放到消息列表最前面,并且不记录历史
|
||||
@ -709,8 +793,22 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
textContent.append(((TextContent) content).text()).append("\n");
|
||||
}
|
||||
});
|
||||
historyMessage.setContent(textContent.toString());
|
||||
//update-begin---author:wangshuai---date:2026-01-12---for:【QQYUN-14261】【AI】AI助手,支持多模态能力- 文档---
|
||||
if (oConvertUtils.isNotEmpty(saveContent)) {
|
||||
historyMessage.setContent(saveContent);
|
||||
} else {
|
||||
historyMessage.setContent(textContent.toString());
|
||||
}
|
||||
historyMessage.setImages(images);
|
||||
// 保存文件信息
|
||||
if (oConvertUtils.isNotEmpty(files)) {
|
||||
List<MessageHistory.FileHistory> fileHistories = new ArrayList<>();
|
||||
for (String file : files) {
|
||||
fileHistories.add(new MessageHistory.FileHistory(file));
|
||||
}
|
||||
historyMessage.setFiles(fileHistories);
|
||||
}
|
||||
//update-end---author:wangshuai---date:2026-01-12---for:【QQYUN-14261】【AI】AI助手,支持多模态能力- 文档---
|
||||
} else if (message.type().equals(ChatMessageType.AI)) {
|
||||
historyMessage.setRole(AiragConsts.MESSAGE_ROLE_AI);
|
||||
AiMessage aiMessage = (AiMessage) message;
|
||||
@ -766,9 +864,20 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
AiragLocalCache.put(AiragConsts.CACHE_TYPE_SSE_HISTORY_MSG, requestId, new CopyOnWriteArrayList<>());
|
||||
try {
|
||||
// 组装用户消息
|
||||
UserMessage userMessage = aiChatHandler.buildUserMessage(sendParams.getContent(), sendParams.getImages());
|
||||
String content = sendParams.getContent();
|
||||
//将文件内容给提示词
|
||||
if(!CollectionUtils.isEmpty(sendParams.getFiles())){
|
||||
content = buildContentWithFiles(content, sendParams.getFiles());
|
||||
}
|
||||
UserMessage userMessage = aiChatHandler.buildUserMessage(content, sendParams.getImages());
|
||||
// 追加消息
|
||||
appendMessage(messages, userMessage, chatConversation, topicId);
|
||||
//update-begin---author:wangshuai---date:2026-01-09---for:【QQYUN-14261】【AI】AI助手,支持多模态能力- 文档---
|
||||
appendMessage(messages, userMessage, chatConversation, topicId, sendParams.getFiles(), sendParams.getContent());
|
||||
//update-end---author:wangshuai---date:2026-01-09---for:【QQYUN-14261】【AI】AI助手,支持多模态能力- 文档---
|
||||
// 绘画AI逻辑:当开启生成绘画时调用
|
||||
if (oConvertUtils.isObjectNotEmpty(sendParams.getEnableDraw()) && sendParams.getEnableDraw()) {
|
||||
return genImageChat(emitter,sendParams,requestId,messages,chatConversation,topicId);
|
||||
}
|
||||
/* 这里应该是有几种情况:
|
||||
* 1. 非ai应用:获取默认模型->开始聊天
|
||||
* 2. AI应用-聊天助手(ChatAssistant):从应用信息组装模型和提示词->开始聊天
|
||||
@ -781,7 +890,7 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
sendWithFlow(requestId, aiApp.getFlowId(), chatConversation, topicId, messages, sendParams);
|
||||
} else {
|
||||
// AI应用-聊天助手(ChatAssistant):从应用信息组装模型和提示词
|
||||
sendWithAppChat(requestId, messages, chatConversation, topicId, sendParams);
|
||||
sendWithAppChat(requestId, messages, chatConversation, topicId, sendParams, aiApp.getFlowId(), aiApp.getMemoryId());
|
||||
}
|
||||
} else {
|
||||
// 发消息
|
||||
@ -789,7 +898,13 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
if (oConvertUtils.isObjectNotEmpty(sendParams.getEnableSearch())) {
|
||||
aiChatParams.setEnableSearch(sendParams.getEnableSearch());
|
||||
}
|
||||
sendWithDefault(requestId, chatConversation, topicId, null, messages, aiChatParams);
|
||||
// 设置深度思考搜索参数
|
||||
if (oConvertUtils.isObjectNotEmpty(sendParams.getEnableThink())) {
|
||||
aiChatParams.setReturnThinking(sendParams.getEnableThink());
|
||||
}
|
||||
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||
sendWithDefault(requestId, chatConversation, topicId, null, messages, aiChatParams, sendParams.getSessionType());
|
||||
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||
}
|
||||
// 发送就绪消息
|
||||
EventData eventRequestId = new EventData(requestId, null, EventData.EVENT_INIT_REQUEST_ID, chatConversation.getId(), topicId);
|
||||
@ -804,6 +919,59 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
return emitter;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成图片
|
||||
*
|
||||
* @param emitter
|
||||
* @param sendParams
|
||||
* @param requestId
|
||||
* @param messages
|
||||
* @param chatConversation
|
||||
* @param topicId
|
||||
* @return
|
||||
*/
|
||||
private SseEmitter genImageChat(SseEmitter emitter, ChatSendParams sendParams, String requestId, List<ChatMessage> messages, ChatConversation chatConversation, String topicId) {
|
||||
AssertUtils.assertNotEmpty("请选择绘画模型", sendParams.getDrawModelId());
|
||||
AIChatParams aiChatParams = new AIChatParams();
|
||||
try {
|
||||
List<String> images = sendParams.getImages();
|
||||
List<Map<String, Object>> imageList = new ArrayList<>();
|
||||
if(CollectionUtils.isEmpty(images)) {
|
||||
//生成图片
|
||||
imageList = aiChatHandler.imageGenerate(sendParams.getDrawModelId(), sendParams.getContent(), aiChatParams);
|
||||
} else {
|
||||
//图生图
|
||||
imageList = aiChatHandler.imageEdit(sendParams.getDrawModelId(), sendParams.getContent(), images, aiChatParams);
|
||||
}
|
||||
// 记录历史消息
|
||||
String imageMarkdown = imageList.stream().map(map -> {
|
||||
String newUrl = this.uploadImage(map);
|
||||
return "";
|
||||
}).collect(Collectors.joining("\n"));
|
||||
AiMessage aiMessage = new AiMessage(imageMarkdown);
|
||||
appendMessage(messages, aiMessage, chatConversation, topicId);
|
||||
// 处理绘画结果并通过SSE返回给客户端
|
||||
EventData eventData = new EventData(requestId, null, EventData.EVENT_MESSAGE, chatConversation.getId(), topicId);
|
||||
EventMessageData messageEventData = EventMessageData.builder().message(imageMarkdown).build();
|
||||
eventData.setData(messageEventData);
|
||||
eventData.setRequestId(requestId);
|
||||
sendMessage2Client(emitter, eventData);
|
||||
// 保存会话
|
||||
saveChatConversation(chatConversation, false, SpringContextUtils.getHttpServletRequest(), sendParams.getSessionType());
|
||||
eventData = new EventData(requestId, null, EventData.EVENT_MESSAGE_END, chatConversation.getId(), topicId);
|
||||
eventData.setRequestId(requestId);
|
||||
sendMessage2Client(emitter, eventData);
|
||||
} catch (Exception e) {
|
||||
log.error("绘画AI调用异常", e);
|
||||
EventData errorEventData = new EventData(requestId, null, EventData.EVENT_FLOW_ERROR, chatConversation.getId(), topicId);
|
||||
EventMessageData messageEventData = EventMessageData.builder().message("绘画AI调用失败:" + e.getMessage()).build();
|
||||
errorEventData.setData(messageEventData);
|
||||
errorEventData.setRequestId(requestId);
|
||||
closeSSE(emitter, errorEventData);
|
||||
}
|
||||
return emitter;
|
||||
}
|
||||
|
||||
/**
|
||||
* 运行流程
|
||||
*
|
||||
@ -875,7 +1043,9 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
sendMessage2Client(emitter, msgEventData);
|
||||
appendMessage(messages, aiMessage, chatConversation, topicId);
|
||||
// 保存会话
|
||||
saveChatConversation(chatConversation, false, httpRequest);
|
||||
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||
saveChatConversation(chatConversation, false, httpRequest, sendParams.getSessionType());
|
||||
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||
}
|
||||
}else{
|
||||
//update-begin---author:chenrui ---date:20250425 for:[QQYUN-12203]AI 聊天,超时或者服务器报错,给个友好提示------------
|
||||
@ -908,16 +1078,31 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
* @param chatConversation
|
||||
* @param topicId
|
||||
* @param sendParams
|
||||
* @param flowId
|
||||
* @param memoryId
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/2/28 10:41
|
||||
*/
|
||||
private void sendWithAppChat(String requestId, List<ChatMessage> messages, ChatConversation chatConversation, String topicId, ChatSendParams sendParams) {
|
||||
private void sendWithAppChat(String requestId, List<ChatMessage> messages, ChatConversation chatConversation, String topicId, ChatSendParams sendParams, String flowId, String memoryId) {
|
||||
AiragApp aiApp = chatConversation.getApp();
|
||||
String modelId = aiApp.getModelId();
|
||||
AssertUtils.assertNotEmpty("请先选择模型", modelId);
|
||||
// AI应用提示词
|
||||
String prompt = aiApp.getPrompt();
|
||||
|
||||
String username = "jeecg";
|
||||
try {
|
||||
HttpServletRequest req = SpringContextUtils.getHttpServletRequest();
|
||||
username = JwtUtil.getUserNameByToken(req);
|
||||
} catch (Exception e) {
|
||||
log.error(e.getMessage());
|
||||
}
|
||||
//将变量中的题试题替换并追加
|
||||
if(oConvertUtils.isObjectNotEmpty(aiApp.getVariables())) {
|
||||
prompt = airagVariableService.additionalPrompt(username, aiApp);
|
||||
}
|
||||
|
||||
if (oConvertUtils.isNotEmpty(prompt)) {
|
||||
appendMessage(messages, new SystemMessage(prompt), chatConversation, topicId);
|
||||
}
|
||||
@ -943,6 +1128,9 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
if (metadata.containsKey("maxTokens")) {
|
||||
aiChatParams.setMaxTokens(metadata.getInteger("maxTokens"));
|
||||
}
|
||||
if (metadata.containsKey(FlowConsts.FLOW_NODE_OPTION_TIME_OUT)) {
|
||||
aiChatParams.setTimeout(oConvertUtils.getInt(metadata.getInteger(FlowConsts.FLOW_NODE_OPTION_TIME_OUT), 300));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -964,16 +1152,70 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
aiChatParams.setPluginIds(pluginIds);
|
||||
}
|
||||
}
|
||||
|
||||
//流程不为空,构建插件
|
||||
if(oConvertUtils.isNotEmpty(flowId)){
|
||||
Map<String, Object> result = airagFlowPluginService.getFlowsToPlugin(flowId);
|
||||
this.addPluginToParams(aiChatParams, result);
|
||||
}
|
||||
|
||||
// 设置网络搜索参数(如果前端传递了)
|
||||
if (sendParams != null && oConvertUtils.isObjectNotEmpty(sendParams.getEnableSearch())) {
|
||||
aiChatParams.setEnableSearch(sendParams.getEnableSearch());
|
||||
}
|
||||
|
||||
// 设置深度思考参数(如果前端传递了)
|
||||
if (sendParams != null && oConvertUtils.isObjectNotEmpty(sendParams.getEnableThink())) {
|
||||
aiChatParams.setReturnThinking(sendParams.getEnableThink());
|
||||
}
|
||||
|
||||
// 设置记忆库的插件
|
||||
if(sendParams != null && oConvertUtils.isNotEmpty(memoryId)){
|
||||
//开启记忆
|
||||
if(null == aiApp.getIzOpenMemory() || AiAppConsts.IZ_OPEN_MEMORY.equals(aiApp.getIzOpenMemory())){
|
||||
Map<String, Object> pluginMemory = airagKnowledgeService.getPluginMemory(memoryId);
|
||||
this.addPluginToParams(aiChatParams, pluginMemory);
|
||||
}
|
||||
}
|
||||
|
||||
//设置变量的插件
|
||||
// 添加系统级工具:变量更新
|
||||
if (oConvertUtils.isNotEmpty(aiApp.getId())) {
|
||||
airagVariableService.addUpdateVariableTool(aiApp,username,aiChatParams);
|
||||
}
|
||||
|
||||
// 打印流程耗时日志
|
||||
printChatDuration(requestId, "构造应用自定义参数完成");
|
||||
// 发消息
|
||||
sendWithDefault(requestId, chatConversation, topicId, modelId, messages, aiChatParams);
|
||||
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||
sendWithDefault(requestId, chatConversation, topicId, modelId, messages, aiChatParams, sendParams.getSessionType());
|
||||
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加插件到参数中
|
||||
*
|
||||
* @param aiChatParams
|
||||
* @param result
|
||||
*/
|
||||
private void addPluginToParams(AIChatParams aiChatParams, Map<String, Object> result) {
|
||||
if (result == null) {
|
||||
return;
|
||||
}
|
||||
Map<ToolSpecification, ToolExecutor> flowsToPlugin = (Map<ToolSpecification, ToolExecutor>) result.get("pluginTool");
|
||||
String pluginId = (String) result.get("pluginId");
|
||||
if (aiChatParams.getTools() == null) {
|
||||
aiChatParams.setTools(new HashMap<>());
|
||||
}
|
||||
if (flowsToPlugin != null) {
|
||||
aiChatParams.getTools().putAll(flowsToPlugin);
|
||||
}
|
||||
if (aiChatParams.getPluginIds() == null) {
|
||||
aiChatParams.setPluginIds(new ArrayList<>());
|
||||
}
|
||||
if (oConvertUtils.isNotEmpty(pluginId)) {
|
||||
aiChatParams.getPluginIds().add(pluginId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -984,11 +1226,12 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
* @param topicId
|
||||
* @param modelId
|
||||
* @param messages
|
||||
* @param sessionType
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/2/25 19:24
|
||||
*/
|
||||
private void sendWithDefault(String requestId, ChatConversation chatConversation, String topicId, String modelId, List<ChatMessage> messages, AIChatParams aiChatParams) {
|
||||
private void sendWithDefault(String requestId, ChatConversation chatConversation, String topicId, String modelId, List<ChatMessage> messages, AIChatParams aiChatParams, String sessionType) {
|
||||
// 调用ai聊天
|
||||
if (null == aiChatParams) {
|
||||
aiChatParams = new AIChatParams();
|
||||
@ -997,11 +1240,16 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
if(chatConversation.getApp().getId().equals(AiAppConsts.DEFAULT_APP_ID)){
|
||||
aiChatParams.setTools(jeecgToolsProvider.getDefaultTools());
|
||||
}
|
||||
aiChatParams.setKnowIds(chatConversation.getApp().getKnowIds());
|
||||
if(CollectionUtils.isEmpty(aiChatParams.getKnowIds())){
|
||||
aiChatParams.setKnowIds(chatConversation.getApp().getKnowIds());
|
||||
} else {
|
||||
aiChatParams.getKnowIds().addAll(chatConversation.getApp().getKnowIds());
|
||||
}
|
||||
aiChatParams.setMaxMsgNumber(oConvertUtils.getInt(chatConversation.getApp().getMsgNum(), 5));
|
||||
aiChatParams.setCurrentHttpRequest(SpringContextUtils.getHttpServletRequest());
|
||||
aiChatParams.setReturnThinking(true);
|
||||
HttpServletRequest httpRequest = SpringContextUtils.getHttpServletRequest();
|
||||
// for [QQYUN-9234] MCP服务连接关闭 - 保存参数引用用于在回调中关闭MCP连接
|
||||
final AIChatParams finalAiChatParams = aiChatParams;
|
||||
TokenStream chatStream;
|
||||
try {
|
||||
// 打印流程耗时日志
|
||||
@ -1013,6 +1261,8 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error(e.getMessage(), e);
|
||||
// for [QQYUN-9234] MCP服务连接关闭 - 异常时关闭MCP连接
|
||||
finalAiChatParams.closeMcpConnections();
|
||||
// sse
|
||||
SseEmitter emitter = AiragLocalCache.get(AiragConsts.CACHE_TYPE_SSE, requestId);
|
||||
if (null == emitter) {
|
||||
@ -1098,6 +1348,8 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
// 打印流程耗时日志
|
||||
printChatDuration(requestId, "LLM输出消息完成");
|
||||
AiragLocalCache.remove(AiragConsts.CACHE_TYPE_SSE_SEND_TIME, requestId);
|
||||
// for [QQYUN-9234] MCP服务连接关闭 - 聊天完成时关闭MCP连接
|
||||
finalAiChatParams.closeMcpConnections();
|
||||
// 记录ai的回复
|
||||
AiMessage aiMessage = responseMessage.aiMessage();
|
||||
FinishReason finishReason = responseMessage.finishReason();
|
||||
@ -1113,7 +1365,9 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
EventData eventData = new EventData(requestId, null, EventData.EVENT_MESSAGE_END, chatConversation.getId(), topicId);
|
||||
appendMessage(messages, aiMessage, chatConversation, topicId);
|
||||
// 保存会话
|
||||
saveChatConversation(chatConversation, false, httpRequest);
|
||||
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||
saveChatConversation(chatConversation, false, httpRequest, sessionType);
|
||||
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||
closeSSE(emitter, eventData);
|
||||
} else if (FinishReason.LENGTH.equals(finishReason)) {
|
||||
// 上下文长度超过限制
|
||||
@ -1137,6 +1391,8 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
// 打印流程耗时日志
|
||||
printChatDuration(requestId, "LLM输出消息异常");
|
||||
AiragLocalCache.remove(AiragConsts.CACHE_TYPE_SSE_SEND_TIME, requestId);
|
||||
// for [QQYUN-9234] MCP服务连接关闭 - 聊天异常时关闭MCP连接
|
||||
finalAiChatParams.closeMcpConnections();
|
||||
// sse
|
||||
SseEmitter emitter = AiragLocalCache.get(AiragConsts.CACHE_TYPE_SSE, requestId);
|
||||
if (null == emitter) {
|
||||
@ -1201,7 +1457,7 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
*/
|
||||
private static void sendMessage2Client(SseEmitter emitter, EventData eventData) {
|
||||
try {
|
||||
log.info("发送消息:{}", eventData.getRequestId());
|
||||
log.debug("发送消息:{}", eventData.getRequestId());
|
||||
String eventStr = JSONObject.toJSONString(eventData);
|
||||
log.debug("[AI应用]接收LLM返回消息:{}", eventStr);
|
||||
emitter.send(SseEmitter.event().data(eventStr));
|
||||
@ -1251,7 +1507,9 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
if (oConvertUtils.isEmpty(chatConversation.getId())) {
|
||||
return;
|
||||
}
|
||||
String key = getConversationCacheKey(chatConversation.getId(), null);
|
||||
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||
String key = getConversationCacheKey(chatConversation.getId(), null,"");
|
||||
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||
if (oConvertUtils.isEmpty(key)) {
|
||||
return;
|
||||
}
|
||||
@ -1281,10 +1539,13 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
if (oConvertUtils.isNotEmpty(summaryTitle)) {
|
||||
cachedConversation.setTitle(summaryTitle);
|
||||
} else {
|
||||
cachedConversation.setTitle(question.length() > 5 ? question.substring(0, 5) : question);
|
||||
int maxLength = AiAppConsts.CONVERSATION_MAX_TITLE_LENGTH;
|
||||
cachedConversation.setTitle(question.length() > maxLength ? question.substring(0, maxLength) : question);
|
||||
}
|
||||
//保存会话
|
||||
saveChatConversation(cachedConversation);
|
||||
//update-begin---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||
saveChatConversation(cachedConversation,"");
|
||||
//update-end---author:wangshuai---date:2025-12-10---for:【QQYUN-14127】【AI】AI应用门户---
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -1329,4 +1590,296 @@ public class AiragChatServiceImpl implements IAiragChatService {
|
||||
log.info("[AI-CHAT]{},requestId:{},耗时:{}s", message, requestId, (System.currentTimeMillis() - beginTime) / 1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 根据会话类型获取会话信息
|
||||
*
|
||||
* @param sessionType
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public Result<?> getConversationsByType(String sessionType) {
|
||||
String key = getConversationDirCacheKey(null);
|
||||
key = key + ":" + sessionType + ":*";
|
||||
List<String> keys = redisUtil.scan(key);
|
||||
// 如果键集合为空,返回空列表
|
||||
if (keys.isEmpty()) {
|
||||
return Result.ok(Collections.emptyList());
|
||||
}
|
||||
|
||||
// 遍历键集合,获取对应的 ChatConversation 对象
|
||||
List<ChatConversation> conversations = new ArrayList<>();
|
||||
for (Object k : keys) {
|
||||
ChatConversation conversation = (ChatConversation) redisTemplate.boundValueOps(k).get();
|
||||
|
||||
if (conversation != null) {
|
||||
AiragApp app = conversation.getApp();
|
||||
if (null == app) {
|
||||
continue;
|
||||
}
|
||||
conversation.setApp(null);
|
||||
conversation.setMessages(null);
|
||||
conversations.add(conversation);
|
||||
}
|
||||
}
|
||||
|
||||
// 对会话列表按创建时间降序排序
|
||||
conversations.sort((o1, o2) -> {
|
||||
Date date1 = o1.getCreateTime();
|
||||
Date date2 = o2.getCreateTime();
|
||||
if (date1 == null && date2 == null) {
|
||||
return 0;
|
||||
}
|
||||
if (date1 == null) {
|
||||
return 1;
|
||||
}
|
||||
if (date2 == null) {
|
||||
return -1;
|
||||
}
|
||||
return date2.compareTo(date1);
|
||||
});
|
||||
|
||||
// 返回结果
|
||||
return Result.ok(conversations);
|
||||
}
|
||||
|
||||
//================================================= begin 【QQYUN-14269】【AI】支持变量 ========================================
|
||||
/**
|
||||
* 初始化变量(仅不存在时设置)
|
||||
*/
|
||||
private void saveVariables(AiragApp app) {
|
||||
if(null == app){
|
||||
return;
|
||||
}
|
||||
if(!AiAppConsts.IZ_OPEN_MEMORY.equals(app.getIzOpenMemory())){
|
||||
return;
|
||||
}
|
||||
if (oConvertUtils.isObjectNotEmpty(app.getVariables())) {
|
||||
// 变量替换
|
||||
String username = "jeecg";
|
||||
try {
|
||||
HttpServletRequest req = SpringContextUtils.getHttpServletRequest();
|
||||
username = JwtUtil.getUserNameByToken(req);
|
||||
} catch (Exception e) {
|
||||
log.error(e.getMessage());
|
||||
}
|
||||
if (oConvertUtils.isNotEmpty(username) && oConvertUtils.isNotEmpty(app.getId())) {
|
||||
String variables = app.getVariables();
|
||||
JSONArray objects = JSONArray.parseArray(variables);
|
||||
for (int i = 0; i < objects.size(); i++) {
|
||||
JSONObject jsonObject = objects.getJSONObject(i);
|
||||
String name = jsonObject.getString("name");
|
||||
String defaultValue = jsonObject.getString("defaultValue");
|
||||
if (oConvertUtils.isNotEmpty(name)) {
|
||||
airagVariableService.initVariable(username, app.getId(), name, defaultValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//================================================= end 【QQYUN-14269】【AI】支持变量 ========================================
|
||||
|
||||
/**
|
||||
* ai海报生成
|
||||
*
|
||||
* @param chatSendParams
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public String genAiPoster(ChatSendParams chatSendParams) {
|
||||
AssertUtils.assertNotEmpty("请选择绘画模型", chatSendParams.getDrawModelId());
|
||||
AssertUtils.assertNotEmpty("请填写提示词", chatSendParams.getContent());
|
||||
AIChatParams aiChatParams = new AIChatParams();
|
||||
if(oConvertUtils.isNotEmpty(chatSendParams.getImageSize())){
|
||||
aiChatParams.setImageSize(chatSendParams.getImageSize());
|
||||
}
|
||||
String image= chatSendParams.getImageUrl();
|
||||
List<Map<String, Object>> imageList = new ArrayList<>();
|
||||
if(oConvertUtils.isEmpty(image)) {
|
||||
//生成图片
|
||||
imageList = aiChatHandler.imageGenerate(chatSendParams.getDrawModelId(), chatSendParams.getContent(), aiChatParams);
|
||||
} else {
|
||||
//图生图
|
||||
imageList = aiChatHandler.imageEdit(chatSendParams.getDrawModelId(), chatSendParams.getContent(), Arrays.asList(image.split(SymbolConstant.COMMA)), aiChatParams);
|
||||
}
|
||||
return imageList.stream().map(this::uploadImage).collect(Collectors.joining("\n"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传图片
|
||||
*
|
||||
* @param map
|
||||
* @return
|
||||
*/
|
||||
private String uploadImage(Map<String, Object> map) {
|
||||
if (null == map || map.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
try {
|
||||
String type = String.valueOf(map.get("type"));
|
||||
String value = String.valueOf(map.get("value"));
|
||||
byte[] data = new byte[1024];
|
||||
// 判断是否是base64
|
||||
if ("base64".equals(type)) {
|
||||
if(value.startsWith("data:image")){
|
||||
value = value.substring(value.indexOf(",") + 1);
|
||||
}
|
||||
data = Base64.getDecoder().decode(value);
|
||||
} else {
|
||||
//下载网络图片
|
||||
InputStream inputStream = FileDownloadUtils.getDownInputStream(value, "");
|
||||
if (inputStream != null) {
|
||||
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
|
||||
byte[] inpByte = new byte[1024]; // 1KB缓冲区
|
||||
int nRead;
|
||||
while ((nRead = inputStream.read(inpByte, 0, data.length)) != -1) {
|
||||
buffer.write(inpByte, 0, nRead);
|
||||
}
|
||||
data = buffer.toByteArray();
|
||||
}
|
||||
}
|
||||
if (data != null) {
|
||||
Path path = jeecgBaseConfig.getPath();
|
||||
String bizPath = "chat";
|
||||
String url = CommonUtils.uploadOnlineImage(data, path.getUpload(), bizPath, jeecgBaseConfig.getUploadType());
|
||||
if("local".equals(jeecgBaseConfig.getUploadType())){
|
||||
url = "#{domainURL}/" + url;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("上传图片失败", e);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
//================================================= begin【QQYUN-14261】【AI】AI助手,支持多模态能力- 文档========================================
|
||||
/**
|
||||
* 构建文件内容
|
||||
*
|
||||
* @param content
|
||||
* @param files
|
||||
* @return
|
||||
*/
|
||||
private String buildContentWithFiles(String content, List<String> files) {
|
||||
String filesText = parseFilesToText(files);
|
||||
if (oConvertUtils.isEmpty(content)) {
|
||||
content = "请基于我提供的附件内容回答问题。";
|
||||
}else{
|
||||
content = content + "\n\n请基于我提供的附件内容回答问题。";
|
||||
}
|
||||
if (oConvertUtils.isNotEmpty(filesText)) {
|
||||
if (oConvertUtils.isNotEmpty(content)) {
|
||||
content = content + "\n\n" + filesText;
|
||||
} else {
|
||||
content = filesText;
|
||||
}
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将文件转换成text
|
||||
*
|
||||
* @param files
|
||||
* @return
|
||||
*/
|
||||
private String parseFilesToText(List<String> files) {
|
||||
if (com.baomidou.mybatisplus.core.toolkit.CollectionUtils.isEmpty(files)) {
|
||||
return "";
|
||||
}
|
||||
StringBuilder sb = new StringBuilder();
|
||||
TikaDocumentParser parser = new TikaDocumentParser(AutoDetectParser::new, null, null, null);
|
||||
int parsedCount = 0;
|
||||
for (String fileRef : files) {
|
||||
if (parsedCount >= LLMConsts.CHAT_FILE_MAX_COUNT) {
|
||||
break;
|
||||
}
|
||||
if (oConvertUtils.isEmpty(fileRef)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String fileRefWithoutQuery = fileRef;
|
||||
if (fileRefWithoutQuery.contains("?")) {
|
||||
fileRefWithoutQuery = fileRefWithoutQuery.substring(0, fileRefWithoutQuery.indexOf("?"));
|
||||
}
|
||||
String fileName = FilenameUtils.getName(fileRefWithoutQuery);
|
||||
String ext = FilenameUtils.getExtension(fileName);
|
||||
if (oConvertUtils.isEmpty(ext) || !LLMConsts.CHAT_FILE_EXT_WHITELIST.contains(ext.toLowerCase())) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
File file = ensureLocalFile(fileRef, fileName);
|
||||
if (file == null || !file.exists() || !file.isFile()) {
|
||||
continue;
|
||||
}
|
||||
Document document = parser.parse(file);
|
||||
if (document == null || oConvertUtils.isEmpty(document.text())) {
|
||||
continue;
|
||||
}
|
||||
String text = document.text().trim();
|
||||
if (text.length() > LLMConsts.CHAT_FILE_TEXT_MAX_LENGTH) {
|
||||
text = text.substring(0, LLMConsts.CHAT_FILE_TEXT_MAX_LENGTH);
|
||||
}
|
||||
sb.append("附件[").append(fileName).append("]内容:\n").append(text).append("\n\n");
|
||||
parsedCount++;
|
||||
if (sb.length() > LLMConsts.CHAT_FILE_TEXT_MAX_LENGTH) {
|
||||
break;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("附件解析失败: {}, {}", fileRef, e.getMessage());
|
||||
}
|
||||
}
|
||||
return sb.toString().trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取本地文件
|
||||
*
|
||||
* @param fileRef
|
||||
* @param fileName
|
||||
* @return
|
||||
* @throws IOException
|
||||
*/
|
||||
private File ensureLocalFile(String fileRef, String fileName) {
|
||||
String uploadpath = jeecgBaseConfig.getPath().getUpload();
|
||||
if (LLMConsts.WEB_PATTERN.matcher(fileRef).matches()) {
|
||||
String tempDir = uploadpath + File.separator + "chat" + File.separator + UUID.randomUUID() + File.separator;
|
||||
File dir = new File(tempDir);
|
||||
if (!dir.exists() && !dir.mkdirs()) {
|
||||
return null;
|
||||
}
|
||||
String tempFilePath = tempDir + fileName;
|
||||
FileDownloadUtils.download2DiskFromNet(fileRef, tempFilePath);
|
||||
return new File(tempFilePath);
|
||||
}
|
||||
return new File(uploadpath + File.separator + fileRef);
|
||||
}
|
||||
//================================================= end【QQYUN-14261】【AI】AI助手,支持多模态能力- 文档========================================
|
||||
|
||||
|
||||
/**
|
||||
* ai创作
|
||||
*
|
||||
* @param aiWriteGenerateVo
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public SseEmitter genAiWriter(AiWriteGenerateVo aiWriteGenerateVo) {
|
||||
String activeMode = "compose";
|
||||
ChatSendParams sendParams = new ChatSendParams();
|
||||
sendParams.setAppId(AiAppConsts.WRITER_APP_ID);
|
||||
String content = "";
|
||||
//写作
|
||||
if (activeMode.equals(aiWriteGenerateVo.getActiveMode())) {
|
||||
content = StrUtil.format(Prompts.AI_WRITER_PROMPT, aiWriteGenerateVo.getPrompt(), aiWriteGenerateVo.getFormat(), aiWriteGenerateVo.getTone(), aiWriteGenerateVo.getLanguage(), aiWriteGenerateVo.getLength());
|
||||
} else {
|
||||
//回复
|
||||
content = StrUtil.format(Prompts.AI_REPLY_PROMPT, aiWriteGenerateVo.getPrompt(), aiWriteGenerateVo.getOriginalContent(), aiWriteGenerateVo.getFormat(), aiWriteGenerateVo.getTone(), aiWriteGenerateVo.getLanguage(), aiWriteGenerateVo.getLength());
|
||||
}
|
||||
sendParams.setContent(content);
|
||||
sendParams.setIzSaveSession(false);
|
||||
return this.send(sendParams);
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,194 @@
|
||||
package org.jeecg.modules.airag.app.service.impl;
|
||||
|
||||
import com.alibaba.fastjson.JSONArray;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import dev.langchain4j.agent.tool.ToolSpecification;
|
||||
import dev.langchain4j.model.chat.request.json.JsonObjectSchema;
|
||||
import dev.langchain4j.service.tool.ToolExecutor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.common.util.SpringContextUtils;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.modules.airag.app.consts.AiAppConsts;
|
||||
import org.jeecg.modules.airag.app.entity.AiragApp;
|
||||
import org.jeecg.modules.airag.app.service.IAiragVariableService;
|
||||
import org.jeecg.modules.airag.app.vo.AppVariableVo;
|
||||
import org.jeecg.modules.airag.common.handler.AIChatParams;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @Description: AI应用变量服务实现
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-02-26
|
||||
* @Version: V1.0
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class AiragVariableServiceImpl implements IAiragVariableService {
|
||||
|
||||
@Autowired
|
||||
private RedisTemplate redisTemplate;
|
||||
|
||||
private static final String CACHE_PREFIX = "airag:app:var:";
|
||||
|
||||
/**
|
||||
* 初始化变量(仅不存在时设置)
|
||||
*
|
||||
* @param username
|
||||
* @param appId
|
||||
* @param name
|
||||
* @param defaultValue
|
||||
*/
|
||||
@Override
|
||||
public void initVariable(String username, String appId, String name, String defaultValue) {
|
||||
if (oConvertUtils.isEmpty(username) || oConvertUtils.isEmpty(appId) || oConvertUtils.isEmpty(name)) {
|
||||
return;
|
||||
}
|
||||
String key = CACHE_PREFIX + appId + ":" + username;
|
||||
redisTemplate.opsForHash().putIfAbsent(key, name, defaultValue != null ? defaultValue : "");
|
||||
}
|
||||
|
||||
/**
|
||||
* 追加提示词
|
||||
*
|
||||
* @param username
|
||||
* @param app
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public String additionalPrompt(String username, AiragApp app) {
|
||||
String memoryPrompt = app.getMemoryPrompt();
|
||||
String prompt = app.getPrompt();
|
||||
|
||||
if (oConvertUtils.isEmpty(memoryPrompt)) {
|
||||
return prompt;
|
||||
}
|
||||
String variablesStr = app.getVariables();
|
||||
if (oConvertUtils.isEmpty(variablesStr)) {
|
||||
return prompt;
|
||||
}
|
||||
|
||||
List<AppVariableVo> variableList = JSONArray.parseArray(variablesStr, AppVariableVo.class);
|
||||
if (variableList == null || variableList.isEmpty()) {
|
||||
return prompt;
|
||||
}
|
||||
|
||||
String key = CACHE_PREFIX + app.getId() + ":" + username;
|
||||
Map<Object, Object> savedValues = redisTemplate.opsForHash().entries(key);
|
||||
|
||||
for (AppVariableVo variable : variableList) {
|
||||
if (variable.getEnable() != null && !variable.getEnable()) {
|
||||
continue;
|
||||
}
|
||||
String name = variable.getName();
|
||||
String value = variable.getDefaultValue();
|
||||
|
||||
// 优先使用Redis中的值
|
||||
if (savedValues.containsKey(name)) {
|
||||
Object savedVal = savedValues.get(name);
|
||||
if (savedVal != null) {
|
||||
value = String.valueOf(savedVal);
|
||||
}
|
||||
}
|
||||
|
||||
if (value == null) {
|
||||
value = "";
|
||||
}
|
||||
|
||||
// 替换 {{name}}
|
||||
memoryPrompt = memoryPrompt.replace("{{" + name + "}}", value);
|
||||
}
|
||||
return prompt + "\n" + memoryPrompt;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新变量值
|
||||
*
|
||||
* @param userId
|
||||
* @param appId
|
||||
* @param name
|
||||
* @param value
|
||||
*/
|
||||
@Override
|
||||
public void updateVariable(String userId, String appId, String name, String value) {
|
||||
if (oConvertUtils.isEmpty(userId) || oConvertUtils.isEmpty(appId) || oConvertUtils.isEmpty(name)) {
|
||||
return;
|
||||
}
|
||||
String key = CACHE_PREFIX + appId + ":" + userId;
|
||||
redisTemplate.opsForHash().put(key, name, value);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 添加变量更新工具
|
||||
*
|
||||
* @param params
|
||||
* @param aiApp
|
||||
* @param username
|
||||
*/
|
||||
@Override
|
||||
public void addUpdateVariableTool(AiragApp aiApp, String username, AIChatParams params) {
|
||||
if (params.getTools() == null) {
|
||||
params.setTools(new HashMap<>());
|
||||
}
|
||||
if (!AiAppConsts.IZ_OPEN_MEMORY.equals(aiApp.getIzOpenMemory())) {
|
||||
return;
|
||||
}
|
||||
// 构建变量描述信息
|
||||
String variablesStr = aiApp.getVariables();
|
||||
List<AppVariableVo> variableList = null;
|
||||
if (oConvertUtils.isNotEmpty(variablesStr)) {
|
||||
variableList = JSONArray.parseArray(variablesStr, AppVariableVo.class);
|
||||
}
|
||||
|
||||
//工具描述
|
||||
StringBuilder descriptionBuilder = new StringBuilder("更新应用变量的值。仅当检测到变量的新值与当前值不一致时调用。如果已调用过或值未变,请勿重复调用。");
|
||||
if (variableList != null && !variableList.isEmpty()) {
|
||||
descriptionBuilder.append("\n\n可用变量列表:");
|
||||
for (AppVariableVo var : variableList) {
|
||||
if (var.getEnable() != null && !var.getEnable()) {
|
||||
continue;
|
||||
}
|
||||
descriptionBuilder.append("\n- ").append(var.getName());
|
||||
if (oConvertUtils.isNotEmpty(var.getDescription())) {
|
||||
descriptionBuilder.append(": ").append(var.getDescription());
|
||||
}
|
||||
}
|
||||
descriptionBuilder.append("\n\n注意:variableName必须是上述列表中的名称之一。");
|
||||
}
|
||||
|
||||
//构建更新变量的工具
|
||||
ToolSpecification spec = ToolSpecification.builder()
|
||||
.name("update_variable")
|
||||
.description(descriptionBuilder.toString())
|
||||
.parameters(JsonObjectSchema.builder()
|
||||
.addStringProperty("variableName", "变量名称")
|
||||
.addStringProperty("value", "变量值")
|
||||
.required("variableName", "value")
|
||||
.build())
|
||||
.build();
|
||||
|
||||
//监听工具的调用
|
||||
ToolExecutor executor = (toolExecutionRequest, memoryId) -> {
|
||||
try {
|
||||
JSONObject args = JSONObject.parseObject(toolExecutionRequest.arguments());
|
||||
String name = args.getString("variableName");
|
||||
String value = args.getString("value");
|
||||
IAiragVariableService variableService = SpringContextUtils.getBean(IAiragVariableService.class);
|
||||
//更新变量值
|
||||
variableService.updateVariable(username, aiApp.getId(), name, value);
|
||||
return "变量 " + name + " 已更新为: " + value;
|
||||
} catch (Exception e) {
|
||||
log.error("更新变量失败", e);
|
||||
return "更新变量失败: " + e.getMessage();
|
||||
}
|
||||
};
|
||||
|
||||
params.getTools().put(spec, executor);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,48 @@
|
||||
package org.jeecg.modules.airag.app.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* @Description: ai写作生成实体类
|
||||
*
|
||||
* @author: wangshuai
|
||||
* @date: 2026/1/12 15:59
|
||||
*/
|
||||
@Data
|
||||
public class AiWriteGenerateVo {
|
||||
|
||||
/**
|
||||
* 写作类型
|
||||
*/
|
||||
private String activeMode;
|
||||
|
||||
/**
|
||||
* 写作内容提示
|
||||
*/
|
||||
private String prompt;
|
||||
|
||||
/**
|
||||
* 原文
|
||||
*/
|
||||
private String originalContent;
|
||||
|
||||
/**
|
||||
* 长度
|
||||
*/
|
||||
private String length;
|
||||
|
||||
/**
|
||||
* 格式
|
||||
*/
|
||||
private String format;
|
||||
|
||||
/**
|
||||
* 语气
|
||||
*/
|
||||
private String tone;
|
||||
|
||||
/**
|
||||
* 语言
|
||||
*/
|
||||
private String language;
|
||||
}
|
||||
@ -0,0 +1,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;
|
||||
}
|
||||
@ -47,4 +47,14 @@ public class ChatConversation {
|
||||
* for [issues/8545]新建AI应用的时候只能选择没有自定义参数的AI流程
|
||||
*/
|
||||
private Map<String, Object> flowInputs;
|
||||
|
||||
/**
|
||||
* portal 应用门户
|
||||
*/
|
||||
private String sessionType;
|
||||
|
||||
/**
|
||||
* 是否保存会话
|
||||
*/
|
||||
private Boolean izSaveSession;
|
||||
}
|
||||
|
||||
@ -47,6 +47,11 @@ public class ChatSendParams {
|
||||
*/
|
||||
private List<String> images;
|
||||
|
||||
/**
|
||||
* 文件列表
|
||||
*/
|
||||
private List<String> files;
|
||||
|
||||
/**
|
||||
* 工作流额外入参配置
|
||||
* key: 参数field, value: 参数值
|
||||
@ -59,4 +64,39 @@ public class ChatSendParams {
|
||||
*/
|
||||
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;
|
||||
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
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;
|
||||
|
||||
@ -11,12 +12,15 @@ import java.util.Map;
|
||||
* @Author: chenrui
|
||||
* @Date: 2025/3/6 11:42
|
||||
*/
|
||||
@Slf4j
|
||||
@Component("testAiragEnhance")
|
||||
public class TestAiragEnhance implements IAiRagEnhanceJava {
|
||||
@Override
|
||||
public Map<String, Object> process(Map<String, Object> inputParams) {
|
||||
Object arg1 = inputParams.get("arg1");
|
||||
Object arg2 = inputParams.get("arg2");
|
||||
Object index = inputParams.get("index");
|
||||
log.info("arg1={}, arg2={}, index={}", arg1, arg2, index);
|
||||
return Collections.singletonMap("result",arg1.toString()+"java拼接"+arg2.toString());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,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";
|
||||
}
|
||||
@ -1,5 +1,8 @@
|
||||
package org.jeecg.modules.airag.llm.consts;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
@ -35,6 +38,11 @@ public class LLMConsts {
|
||||
*/
|
||||
public static final String MODEL_TYPE_LLM = "LLM";
|
||||
|
||||
/**
|
||||
* 模型类型: 图像生成
|
||||
*/
|
||||
public static final String MODEL_TYPE_IMAGE = "IMAGE";
|
||||
|
||||
/**
|
||||
* 向量模型:默认维度
|
||||
*/
|
||||
@ -85,4 +93,29 @@ public class LLMConsts {
|
||||
*/
|
||||
public static final String DEEPSEEK_REASONER = "deepseek-reasoner";
|
||||
|
||||
/**
|
||||
* 知识库类型:知识库
|
||||
*/
|
||||
public static final String KNOWLEDGE_TYPE_KNOWLEDGE = "knowledge";
|
||||
|
||||
/**
|
||||
* 知识库类型:记忆库
|
||||
*/
|
||||
public static final String KNOWLEDGE_TYPE_MEMORY = "memory";
|
||||
|
||||
/**
|
||||
* 支持文件的后缀
|
||||
*/
|
||||
public static final Set<String> CHAT_FILE_EXT_WHITELIST = new HashSet<>(Arrays.asList("txt", "pdf", "docx", "doc", "pptx", "ppt", "xlsx", "xls", "md"));
|
||||
|
||||
/**
|
||||
* 文件内容最大长度
|
||||
*/
|
||||
public static final int CHAT_FILE_TEXT_MAX_LENGTH = 20000;
|
||||
|
||||
/**
|
||||
* 上传文件对打数量
|
||||
*/
|
||||
public static final int CHAT_FILE_MAX_COUNT = 3;
|
||||
|
||||
}
|
||||
|
||||
@ -0,0 +1,31 @@
|
||||
package org.jeecg.modules.airag.llm.controller;
|
||||
|
||||
import org.jeecg.common.airag.api.IAiragBaseApi;
|
||||
import org.jeecg.modules.airag.llm.service.impl.AiragBaseApiImpl;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* airag baseAPI Controller
|
||||
*
|
||||
* @author sjlei
|
||||
* @date 2025-12-30
|
||||
*/
|
||||
@RestController("airagBaseApiController")
|
||||
public class AiragBaseApiController implements IAiragBaseApi {
|
||||
|
||||
@Autowired
|
||||
AiragBaseApiImpl airagBaseApi;
|
||||
|
||||
@PostMapping("/airag/api/knowledgeWriteTextDocument")
|
||||
public String knowledgeWriteTextDocument(
|
||||
@RequestParam("knowledgeId") String knowledgeId,
|
||||
@RequestParam("title") String title,
|
||||
@RequestParam("content") String content
|
||||
) {
|
||||
return airagBaseApi.knowledgeWriteTextDocument(knowledgeId, title, content);
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,14 +1,18 @@
|
||||
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;
|
||||
@ -22,7 +26,6 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
@ -80,6 +83,9 @@ public class AiragKnowledgeController {
|
||||
@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("添加成功!");
|
||||
}
|
||||
@ -101,6 +107,9 @@ public class AiragKnowledgeController {
|
||||
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())) {
|
||||
// 更新了模型,重建文档
|
||||
@ -357,5 +366,62 @@ public class AiragKnowledgeController {
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -17,6 +17,7 @@ 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;
|
||||
@ -172,11 +173,16 @@ public class AiragModelController extends JeecgController<AiragModel, IAiragMode
|
||||
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{
|
||||
}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("测试模型连接失败,请检查模型配置是否正确!");
|
||||
|
||||
@ -7,6 +7,7 @@ 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;
|
||||
@ -30,7 +31,10 @@ import org.xml.sax.ContentHandler;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.file.Files;
|
||||
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;
|
||||
|
||||
@ -51,6 +55,8 @@ public class TikaDocumentParser {
|
||||
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);
|
||||
@ -71,22 +77,16 @@ public class TikaDocumentParser {
|
||||
InputStream isForParsing = Files.newInputStream(file.toPath());
|
||||
// 使用 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);
|
||||
} else if (fileName.endsWith(".docx")) {
|
||||
return extractTextFromDocx(isForParsing);
|
||||
} else if (fileName.endsWith(".doc")) {
|
||||
return extractTextFromDoc(isForParsing);
|
||||
} else if (fileName.endsWith(".xlsx")) {
|
||||
return extractTextFromExcel(isForParsing);
|
||||
} else if (fileName.endsWith(".xls")) {
|
||||
return extractTextFromExcel(isForParsing);
|
||||
} else if (fileName.endsWith(".pptx")) {
|
||||
return extractTextFromPptx(isForParsing);
|
||||
} else if (fileName.endsWith(".ppt")) {
|
||||
return extractTextFromPpt(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));
|
||||
}
|
||||
@ -95,6 +95,27 @@ public class TikaDocumentParser {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* langchain4j 内部解析器
|
||||
* @param file
|
||||
* @return
|
||||
*/
|
||||
public Document parseDocExcelPdfUsingApachePoi(File file) {
|
||||
AssertUtils.assertNotEmpty("请选择文件", file);
|
||||
try (InputStream inputStream = Files.newInputStream(file.toPath())) {
|
||||
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 格式)
|
||||
|
||||
@ -102,4 +102,11 @@ public class AiragKnowledge implements Serializable {
|
||||
@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;
|
||||
}
|
||||
|
||||
@ -12,13 +12,16 @@ import org.jeecg.ai.handler.LLMHandler;
|
||||
import org.jeecg.common.exception.JeecgBootException;
|
||||
import org.jeecg.common.util.AssertUtils;
|
||||
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;
|
||||
@ -54,6 +57,8 @@ public class AIChatHandler implements IAIChatHandler {
|
||||
@Autowired
|
||||
LLMHandler llmHandler;
|
||||
|
||||
@Autowired
|
||||
AiRagConfigBean aiRagConfigBean;
|
||||
|
||||
@Value(value = "${jeecg.path.upload:}")
|
||||
private String uploadpath;
|
||||
@ -285,7 +290,7 @@ public class AIChatHandler implements IAIChatHandler {
|
||||
|
||||
// 默认超时时间
|
||||
if(oConvertUtils.isObjectEmpty(params.getTimeout())){
|
||||
params.setTimeout(60);
|
||||
params.setTimeout(AiragConsts.DEFAULT_TIMEOUT);
|
||||
}
|
||||
|
||||
//deepseek-reasoner 推理模型不支持插件tool
|
||||
@ -301,6 +306,7 @@ public class AIChatHandler implements IAIChatHandler {
|
||||
/**
|
||||
* 构造插件和MCP工具
|
||||
* for [QQYUN-12453]【AI】支持插件
|
||||
* for [QQYUN-9234] MCP服务连接关闭 - 使用包装器保存连接引用
|
||||
* @param params
|
||||
* @author chenrui
|
||||
* @date 2025/10/31 14:04
|
||||
@ -310,6 +316,7 @@ public class AIChatHandler implements IAIChatHandler {
|
||||
|
||||
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())) {
|
||||
@ -325,15 +332,18 @@ public class AIChatHandler implements IAIChatHandler {
|
||||
}
|
||||
|
||||
if ("mcp".equalsIgnoreCase(category)) {
|
||||
// MCP类型:构建McpToolProvider
|
||||
McpToolProvider mcpToolProvider = buildMcpToolProvider(
|
||||
// MCP类型:构建McpToolProviderWrapper(包含连接引用用于后续关闭)
|
||||
// for [QQYUN-9234] MCP服务连接关闭
|
||||
McpToolProviderWrapper wrapper = buildMcpToolProviderWrapper(
|
||||
airagMcp.getName(),
|
||||
airagMcp.getType(),
|
||||
airagMcp.getEndpoint(),
|
||||
airagMcp.getHeaders()
|
||||
airagMcp.getHeaders(),
|
||||
aiRagConfigBean.getAllowSensitiveNodes()
|
||||
);
|
||||
if (mcpToolProvider != null) {
|
||||
mcpToolProviders.add(mcpToolProvider);
|
||||
if (wrapper != null) {
|
||||
mcpToolProviders.add(wrapper.getMcpToolProvider());
|
||||
mcpToolProviderWrappers.add(wrapper);
|
||||
}
|
||||
} else if ("plugin".equalsIgnoreCase(category)) {
|
||||
// 插件类型:构建ToolSpecification和ToolExecutor
|
||||
@ -348,6 +358,12 @@ public class AIChatHandler implements IAIChatHandler {
|
||||
if (!mcpToolProviders.isEmpty()) {
|
||||
params.setMcpToolProviders(mcpToolProviders);
|
||||
}
|
||||
|
||||
// 保存MCP连接包装器,用于后续关闭
|
||||
// for [QQYUN-9234] MCP服务连接关闭
|
||||
if (!mcpToolProviderWrappers.isEmpty()) {
|
||||
params.setMcpToolProviderWrappers(mcpToolProviderWrappers);
|
||||
}
|
||||
|
||||
// 设置插件工具
|
||||
if (!pluginTools.isEmpty()) {
|
||||
@ -401,5 +417,129 @@ public class AIChatHandler implements IAIChatHandler {
|
||||
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 (errMsg.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;
|
||||
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 绘画创作 ========================================
|
||||
}
|
||||
|
||||
@ -17,13 +17,17 @@ 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;
|
||||
@ -94,11 +98,21 @@ public class EmbeddingHandler implements IEmbeddingHandler {
|
||||
*/
|
||||
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
|
||||
*/
|
||||
@ -109,6 +123,11 @@ public class EmbeddingHandler implements IEmbeddingHandler {
|
||||
*/
|
||||
public static final String EMBED_STORE_METADATA_DOCNAME = "docName";
|
||||
|
||||
/**
|
||||
* 向量存储元数据:创建时间
|
||||
*/
|
||||
public static final String EMBED_STORE_CREATE_TIME = "createTime";
|
||||
|
||||
/**
|
||||
* 向量存储缓存
|
||||
*/
|
||||
@ -175,7 +194,26 @@ public class EmbeddingHandler implements IEmbeddingHandler {
|
||||
.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_METADATA_DOCNAME, FilenameUtils.getName(doc.getTitle()))
|
||||
//初始化记忆库的时候添加创建时间选项
|
||||
.put(EMBED_STORE_CREATE_TIME, String.valueOf(doc.getCreateTime() != null ? doc.getCreateTime().getTime() : System.currentTimeMillis()));
|
||||
//update-begin---author:wangshuai---date:2025-12-26---for:【QQYUN-14265】【AI】支持记忆---
|
||||
//添加用户名字到元数据里面,用于记忆库中数据隔离
|
||||
String username = doc.getCreateBy();
|
||||
if (oConvertUtils.isEmpty(username)) {
|
||||
try {
|
||||
HttpServletRequest request = SpringContextUtils.getHttpServletRequest();
|
||||
String token = TokenUtils.getTokenByRequest(request);
|
||||
username = JwtUtil.getUsername(token);
|
||||
} catch (Exception e) {
|
||||
// ignore:token获取不到默认为admin
|
||||
username = "admin";
|
||||
}
|
||||
}
|
||||
if (oConvertUtils.isNotEmpty(username)) {
|
||||
metadata.put(EMBED_STORE_METADATA_USER_NAME, username);
|
||||
}
|
||||
//update-end---author:wangshuai---date:2025-12-26---for:【QQYUN-14265】【AI】支持记忆---
|
||||
Document from = Document.from(content, metadata);
|
||||
ingestor.ingest(from);
|
||||
return metadata.toMap();
|
||||
@ -208,16 +246,47 @@ public class EmbeddingHandler implements IEmbeddingHandler {
|
||||
}
|
||||
}
|
||||
|
||||
//命中的文档内容
|
||||
StringBuilder data = new StringBuilder();
|
||||
// 对documents按score降序排序并取前topNumber个
|
||||
List<Map<String, Object>> sortedDocuments = documents.stream()
|
||||
.sorted(Comparator.comparingDouble((Map<String, Object> doc) -> (Double) doc.get("score")).reversed())
|
||||
.limit(topNumber)
|
||||
.peek(doc -> data.append(doc.get("content")).append("\n"))
|
||||
//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());
|
||||
|
||||
return new KnowledgeSearchResult(data.toString(), sortedDocuments);
|
||||
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的时候需要限制几个字---
|
||||
}
|
||||
|
||||
/**
|
||||
@ -244,11 +313,31 @@ public class EmbeddingHandler implements IEmbeddingHandler {
|
||||
|
||||
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(metadataKey(EMBED_STORE_METADATA_KNOWLEDGEID).isEqualTo(knowId))
|
||||
.filter(filter)
|
||||
.build();
|
||||
|
||||
EmbeddingStore<TextSegment> embeddingStore = getEmbedStore(model);
|
||||
@ -262,6 +351,9 @@ public class EmbeddingHandler implements IEmbeddingHandler {
|
||||
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());
|
||||
}
|
||||
@ -295,13 +387,32 @@ public class EmbeddingHandler implements IEmbeddingHandler {
|
||||
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(metadataKey(EMBED_STORE_METADATA_KNOWLEDGEID).isEqualTo(knowId))
|
||||
.filter(filter)
|
||||
.build();
|
||||
retrievers.add(contentRetriever);
|
||||
}
|
||||
|
||||
@ -12,6 +12,8 @@ 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;
|
||||
@ -51,8 +53,9 @@ public class PluginToolBuilder {
|
||||
}
|
||||
|
||||
String baseUrl = airagMcp.getEndpoint();
|
||||
boolean isEmptyBaseUrl = oConvertUtils.isEmpty(baseUrl);
|
||||
// 如果baseUrl为空,使用当前系统地址
|
||||
if (oConvertUtils.isEmpty(baseUrl)) {
|
||||
if (isEmptyBaseUrl) {
|
||||
if (currentHttpRequest != null) {
|
||||
baseUrl = CommonUtils.getBaseUrl(currentHttpRequest);
|
||||
log.info("插件[{}]的BaseURL为空,使用系统地址: {}", airagMcp.getName(), baseUrl);
|
||||
@ -64,7 +67,10 @@ public class PluginToolBuilder {
|
||||
|
||||
// 解析headers
|
||||
Map<String, String> headersMap = parseHeaders(airagMcp.getHeaders());
|
||||
|
||||
|
||||
// 判断是否需要加签
|
||||
boolean isNeedSign = isEmptyBaseUrl && ToolsNode.Helper.checkNeedSign(headersMap);
|
||||
|
||||
// 解析并应用授权配置(从metadata中读取)
|
||||
applyAuthConfig(headersMap, airagMcp.getMetadata(), currentHttpRequest);
|
||||
|
||||
@ -76,7 +82,7 @@ public class PluginToolBuilder {
|
||||
|
||||
try {
|
||||
ToolSpecification spec = buildToolSpecification(toolConfig);
|
||||
ToolExecutor executor = buildToolExecutor(toolConfig, baseUrl, headersMap);
|
||||
ToolExecutor executor = buildToolExecutor(toolConfig, baseUrl, headersMap, isNeedSign);
|
||||
if (spec != null && executor != null) {
|
||||
tools.put(spec, executor);
|
||||
}
|
||||
@ -187,7 +193,7 @@ public class PluginToolBuilder {
|
||||
/**
|
||||
* 构建ToolExecutor
|
||||
*/
|
||||
private static ToolExecutor buildToolExecutor(JSONObject toolConfig, String baseUrl, Map<String, String> defaultHeaders) {
|
||||
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");
|
||||
@ -215,8 +221,13 @@ public class PluginToolBuilder {
|
||||
JSONObject urlVariables = buildUrlVariables(parameters, args);
|
||||
Object body = buildRequestBody(parameters, args, httpHeaders);
|
||||
|
||||
// 发送HTTP请求
|
||||
ResponseEntity<String> response = RestUtil.request(url, httpMethod, httpHeaders, urlVariables, body, String.class);
|
||||
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() : "";
|
||||
@ -335,7 +346,13 @@ public class PluginToolBuilder {
|
||||
if (isQueryParam || !isOtherType) {
|
||||
Object value = args.get(paramName);
|
||||
if (value != null) {
|
||||
urlVariables.put(paramName, value);
|
||||
//如果是知识库的id赋值默认值
|
||||
if ("knowledgeId".equalsIgnoreCase(paramName)) {
|
||||
String defaultValue = param.getString("defaultValue");
|
||||
urlVariables.put(paramName, defaultValue);
|
||||
} else {
|
||||
urlVariables.put(paramName, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -392,7 +409,13 @@ public class PluginToolBuilder {
|
||||
|
||||
Object value = args.get(paramName);
|
||||
if (value != null) {
|
||||
body.put(paramName, value);
|
||||
//如果是知识库的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");
|
||||
|
||||
@ -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);
|
||||
}
|
||||
@ -3,6 +3,8 @@ 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知识库
|
||||
*
|
||||
@ -11,4 +13,12 @@ import org.jeecg.modules.airag.llm.entity.AiragKnowledge;
|
||||
* @Version: V1.0
|
||||
*/
|
||||
public interface IAiragKnowledgeService extends IService<AiragKnowledge> {
|
||||
|
||||
/**
|
||||
* 构建知识库的工具
|
||||
*
|
||||
* @param memoryId
|
||||
* @return Map<String, Object>
|
||||
*/
|
||||
Map<String, Object> getPluginMemory(String memoryId);
|
||||
}
|
||||
|
||||
@ -0,0 +1,45 @@
|
||||
package org.jeecg.modules.airag.llm.service.impl;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.common.airag.api.IAiragBaseApi;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.exception.JeecgBootBizTipException;
|
||||
import org.jeecg.common.util.AssertUtils;
|
||||
import org.jeecg.modules.airag.llm.consts.LLMConsts;
|
||||
import org.jeecg.modules.airag.llm.entity.AiragKnowledgeDoc;
|
||||
import org.jeecg.modules.airag.llm.service.IAiragKnowledgeDocService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* airag baseAPI 实现类
|
||||
*/
|
||||
@Slf4j
|
||||
@Primary
|
||||
@Service("airagBaseApiImpl")
|
||||
public class AiragBaseApiImpl implements IAiragBaseApi {
|
||||
|
||||
@Autowired
|
||||
private IAiragKnowledgeDocService airagKnowledgeDocService;
|
||||
|
||||
@Override
|
||||
public String knowledgeWriteTextDocument(String knowledgeId, String title, String content) {
|
||||
AssertUtils.assertNotEmpty("知识库ID不能为空", knowledgeId);
|
||||
AssertUtils.assertNotEmpty("写入内容不能为空", content);
|
||||
AiragKnowledgeDoc knowledgeDoc = new AiragKnowledgeDoc();
|
||||
knowledgeDoc.setKnowledgeId(knowledgeId);
|
||||
knowledgeDoc.setTitle(title);
|
||||
knowledgeDoc.setType(LLMConsts.KNOWLEDGE_DOC_TYPE_TEXT);
|
||||
knowledgeDoc.setContent(content);
|
||||
Result<?> result = airagKnowledgeDocService.editDocument(knowledgeDoc);
|
||||
if (!result.isSuccess()) {
|
||||
throw new JeecgBootBizTipException(result.getMessage());
|
||||
}
|
||||
if (knowledgeDoc.getId() == null) {
|
||||
throw new JeecgBootBizTipException("知识库文档ID为空");
|
||||
}
|
||||
log.info("[AI-KNOWLEDGE] 文档写入完成,知识库:{}, 文档ID:{}", knowledgeId, knowledgeDoc.getId());
|
||||
return knowledgeDoc.getId();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,231 @@
|
||||
package org.jeecg.modules.airag.llm.service.impl;
|
||||
|
||||
import com.alibaba.fastjson.JSONArray;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import dev.langchain4j.agent.tool.ToolSpecification;
|
||||
import dev.langchain4j.service.tool.ToolExecutor;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.common.constant.CommonConstant;
|
||||
import org.jeecg.common.constant.SymbolConstant;
|
||||
import org.jeecg.common.util.SpringContextUtils;
|
||||
import org.jeecg.common.util.TokenUtils;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.modules.airag.flow.consts.FlowConsts;
|
||||
import org.jeecg.modules.airag.flow.entity.AiragFlow;
|
||||
import org.jeecg.modules.airag.flow.service.IAiragFlowService;
|
||||
import org.jeecg.modules.airag.flow.vo.api.SubFlowResult;
|
||||
import org.jeecg.modules.airag.flow.vo.flow.config.FlowNodeConfig;
|
||||
import org.jeecg.modules.airag.llm.consts.FlowPluginContent;
|
||||
import org.jeecg.modules.airag.llm.entity.AiragMcp;
|
||||
import org.jeecg.modules.airag.llm.handler.PluginToolBuilder;
|
||||
import org.jeecg.modules.airag.llm.service.IAiragFlowPluginService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* @Description: 流程同步到MCP服务实现类
|
||||
* @Author: wangshuai
|
||||
* @Date: 2025-12-22
|
||||
* @Version: V1.0
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class AiragFlowPluginServiceImpl implements IAiragFlowPluginService {
|
||||
|
||||
@Autowired
|
||||
private IAiragFlowService airagFlowService;
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getFlowsToPlugin(String flowIds) {
|
||||
log.info("开始构建流程插件");
|
||||
// 1. 查询所有启用的流程
|
||||
LambdaQueryWrapper<AiragFlow> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(AiragFlow::getStatus, FlowConsts.FLOW_STATUS_ENABLE);
|
||||
queryWrapper.in(AiragFlow::getId, Arrays.asList(flowIds.split(SymbolConstant.COMMA)));
|
||||
List<AiragFlow> flows = airagFlowService.list(queryWrapper);
|
||||
HttpServletRequest httpServletRequest = SpringContextUtils.getHttpServletRequest();
|
||||
if (flows.isEmpty()) {
|
||||
log.info("当前应用所选流程没有启用的流程");
|
||||
return null;
|
||||
}
|
||||
//返回数据
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
//插件
|
||||
//插件id
|
||||
AiragMcp tool = new AiragMcp();
|
||||
// 2. 构建插件
|
||||
String id = UUID.randomUUID().toString().replace("-", "");
|
||||
tool.setId(id);
|
||||
// 插件名称
|
||||
tool.setName(FlowPluginContent.PLUGIN_NAME);
|
||||
// 描述
|
||||
tool.setDescr(FlowPluginContent.PLUGIN_DESC);
|
||||
tool.setStatus(FlowConsts.FLOW_STATUS_ENABLE);
|
||||
tool.setSynced(CommonConstant.STATUS_1_INT);
|
||||
tool.setCategory("plugin");
|
||||
tool.setEndpoint("");
|
||||
int toolCount = 0;
|
||||
//构建拆件工具
|
||||
for (AiragFlow flow : flows) {
|
||||
try {
|
||||
|
||||
SubFlowResult subFlow = new SubFlowResult(flow);
|
||||
// 获取入参参数
|
||||
JSONArray parameter = getInputParameter(flow, subFlow);
|
||||
// 获取出参参数
|
||||
JSONArray outParams = getOutputParameter(flow, subFlow);
|
||||
// name必须符合 ^[a-zA-Z0-9_-]+$
|
||||
String validToolName = "flow_" + flow.getId();
|
||||
// 将原始名称拼接到描述中
|
||||
String description = flow.getName();
|
||||
if (oConvertUtils.isNotEmpty(flow.getDescr())) {
|
||||
description += " : " + flow.getDescr();
|
||||
}
|
||||
//构造工具参数
|
||||
String flowTool = buildParameter(parameter, outParams, flow.getId(), tool.getTools(), validToolName, description);
|
||||
tool.setTools(flowTool);
|
||||
toolCount++;
|
||||
} catch (Exception e) {
|
||||
log.error("处理流程[{}]转换插件失败: {}", flow.getName(), e.getMessage());
|
||||
}
|
||||
}
|
||||
String tenantId = TokenUtils.getTenantIdByRequest(httpServletRequest);
|
||||
//构建元数据(请求头)
|
||||
String meataData = buildMetadata(toolCount, tenantId);
|
||||
tool.setMetadata(meataData);
|
||||
Map<ToolSpecification, ToolExecutor> tools = PluginToolBuilder.buildTools(tool, httpServletRequest);
|
||||
result.put("pluginTool", tools);
|
||||
result.put("pluginId", id);
|
||||
log.info("构建流程插件结束");
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建元数据
|
||||
*
|
||||
* @param toolCount
|
||||
* @param tenantId
|
||||
*/
|
||||
private String buildMetadata(int toolCount, String tenantId) {
|
||||
JSONObject jsonObject = new JSONObject();
|
||||
jsonObject.put(FlowPluginContent.TOKEN_PARAM_NAME, FlowPluginContent.X_ACCESS_TOKEN);
|
||||
jsonObject.put(FlowPluginContent.TOOL_COUNT, toolCount);
|
||||
jsonObject.put(FlowPluginContent.AUTH_TYPE, FlowPluginContent.TOKEN);
|
||||
jsonObject.put(FlowPluginContent.TOKEN_PARAM_VALUE, "");
|
||||
jsonObject.put(CommonConstant.TENANT_ID, oConvertUtils.getInt(tenantId, 0));
|
||||
return jsonObject.toJSONString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建参数
|
||||
*
|
||||
* @param parameter
|
||||
* @param outParams
|
||||
* @param flowId
|
||||
* @param tools
|
||||
* @param description
|
||||
* @param name
|
||||
*/
|
||||
private String buildParameter(JSONArray parameter, JSONArray outParams, String flowId, String tools, String name, String description) {
|
||||
JSONArray paramArray = new JSONArray();
|
||||
JSONObject parameterObject = new JSONObject();
|
||||
parameterObject.put(FlowPluginContent.NAME, name);
|
||||
parameterObject.put(FlowPluginContent.DESCRIPTION, description);
|
||||
parameterObject.put(FlowPluginContent.PATH, FlowPluginContent.PLUGIN_REQUEST_URL + flowId);
|
||||
parameterObject.put(FlowPluginContent.METHOD, FlowPluginContent.POST);
|
||||
parameterObject.put(FlowPluginContent.ENABLED, true);
|
||||
parameterObject.put(FlowPluginContent.PARAMETERS, parameter);
|
||||
parameterObject.put(FlowPluginContent.RESPONSES, outParams);
|
||||
if (oConvertUtils.isNotEmpty(tools)) {
|
||||
paramArray = JSONArray.parseArray(tools);
|
||||
paramArray.add(parameterObject);
|
||||
} else {
|
||||
paramArray.add(parameterObject);
|
||||
}
|
||||
return paramArray.toJSONString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取参数
|
||||
*
|
||||
* @param flow
|
||||
* @param subFlow
|
||||
*/
|
||||
private JSONArray getInputParameter(AiragFlow flow, SubFlowResult subFlow) {
|
||||
JSONArray parameters = new JSONArray();
|
||||
String metadata = flow.getMetadata();
|
||||
if (oConvertUtils.isNotEmpty(metadata)) {
|
||||
JSONObject jsonObject = JSONObject.parseObject(metadata);
|
||||
if (jsonObject.containsKey(FlowPluginContent.INPUTS)) {
|
||||
JSONArray jsonArray = jsonObject.getJSONArray(FlowPluginContent.INPUTS);
|
||||
jsonArray.forEach(item -> {
|
||||
if (oConvertUtils.isNotEmpty(item.toString())) {
|
||||
JSONObject json = JSONObject.parseObject(item.toString());
|
||||
json.put(FlowPluginContent.LOCATION, FlowPluginContent.LOCATION_BODY);
|
||||
}
|
||||
});
|
||||
parameters.addAll(jsonArray);
|
||||
}
|
||||
}
|
||||
//需要获取子流程的参数,子流程的参数是单独封装的,否则在流程执行的时候会报错缺少参数
|
||||
List<FlowNodeConfig.NodeParam> inputParams = subFlow.getInputParams();
|
||||
if (inputParams != null) {
|
||||
for (FlowNodeConfig.NodeParam param : inputParams) {
|
||||
JSONObject p = new JSONObject();
|
||||
// 参数名
|
||||
p.put(FlowPluginContent.NAME, param.getField());
|
||||
String paramDesc = param.getName();
|
||||
if (oConvertUtils.isEmpty(paramDesc)) {
|
||||
paramDesc = param.getField();
|
||||
}
|
||||
// 参数描述
|
||||
p.put(FlowPluginContent.DESCRIPTION, paramDesc);
|
||||
// 类型
|
||||
p.put(FlowPluginContent.TYPE, oConvertUtils.getString(param.getType(), FlowPluginContent.TYPE_STRING));
|
||||
// 所有参数都在Body中
|
||||
p.put(FlowPluginContent.LOCATION, FlowPluginContent.LOCATION_BODY);
|
||||
boolean required = param.getRequired() != null && param.getRequired();
|
||||
p.put(FlowPluginContent.REQUIRED, required);
|
||||
parameters.add(p);
|
||||
}
|
||||
}
|
||||
return parameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建返回值
|
||||
*/
|
||||
private JSONArray getOutputParameter(AiragFlow flow, SubFlowResult subFlow) {
|
||||
JSONArray parameters = new JSONArray();
|
||||
String metadata = flow.getMetadata();
|
||||
if (oConvertUtils.isNotEmpty(metadata)) {
|
||||
JSONObject jsonObject = JSONObject.parseObject(metadata);
|
||||
if (jsonObject.containsKey(FlowPluginContent.OUTPUTS)) {
|
||||
JSONArray jsonArray = jsonObject.getJSONArray(FlowPluginContent.OUTPUTS);
|
||||
parameters.addAll(jsonArray);
|
||||
}
|
||||
}
|
||||
// List<FlowNodeConfig.NodeParam> outputParams = subFlow.getOutputParams();
|
||||
// if (outputParams != null) {
|
||||
// for (FlowNodeConfig.NodeParam param : outputParams) {
|
||||
// JSONObject p = new JSONObject();
|
||||
// // 参数名
|
||||
// p.put("name", param.getField());
|
||||
// String paramDesc = param.getName();
|
||||
// if (oConvertUtils.isEmpty(paramDesc)) {
|
||||
// paramDesc = param.getField();
|
||||
// }
|
||||
// // 参数描述
|
||||
// p.put("description", paramDesc);
|
||||
// // 类型
|
||||
// p.put("type", oConvertUtils.getString(param.getType(), "String"));
|
||||
// parameters.add(p);
|
||||
// }
|
||||
// }
|
||||
return parameters;
|
||||
}
|
||||
}
|
||||
@ -131,7 +131,6 @@ public class AiragKnowledgeDocServiceImpl extends ServiceImpl<AiragKnowledgeDocM
|
||||
List<AiragKnowledgeDoc> docList = airagKnowledgeDocMapper.selectBatchIds(docIdList);
|
||||
AssertUtils.assertNotEmpty("文档不存在", docList);
|
||||
|
||||
HttpServletRequest request = SpringContextUtils.getHttpServletRequest();
|
||||
// 检查状态
|
||||
List<AiragKnowledgeDoc> knowledgeDocs = docList.stream()
|
||||
.filter(doc -> {
|
||||
@ -330,6 +329,7 @@ public class AiragKnowledgeDocServiceImpl extends ServiceImpl<AiragKnowledgeDocM
|
||||
if (File.separator.equals("\\")) {
|
||||
// Windows path handling
|
||||
String escapedPath = uploadpath.replace("//", "\\\\");
|
||||
escapedPath = escapedPath.replace("/", "\\\\");
|
||||
relativePath = uploadedFile.getPath().replaceFirst("^" + escapedPath, "");
|
||||
} else {
|
||||
// Unix path handling
|
||||
|
||||
@ -1,18 +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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -10,11 +10,14 @@ 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;
|
||||
@ -22,6 +25,7 @@ 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;
|
||||
|
||||
@ -31,12 +35,15 @@ import java.util.stream.Collectors;
|
||||
* @Date: 2025-10-20
|
||||
* @Version: V1.0
|
||||
*/
|
||||
@Service("airagMcpServiceImpl")
|
||||
@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
|
||||
@ -125,7 +132,7 @@ public class AiragMcpServiceImpl extends ServiceImpl<AiragMcpMapper, AiragMcp> i
|
||||
Map<String, String> headers = null;
|
||||
if (oConvertUtils.isNotEmpty(mcp.getHeaders())) {
|
||||
try {
|
||||
headers = JSONObject.parseObject(mcp.getHeaders(), Map.class);
|
||||
headers = JSONObject.parseObject(mcp.getHeaders(), new com.alibaba.fastjson.TypeReference<Map<String, String>>() {});
|
||||
} catch (JSONException e) {
|
||||
headers = null;
|
||||
}
|
||||
@ -136,19 +143,53 @@ public class AiragMcpServiceImpl extends ServiceImpl<AiragMcpMapper, AiragMcp> i
|
||||
McpClient mcpClient = null;
|
||||
try {
|
||||
if ("sse".equalsIgnoreCase(type)) {
|
||||
HttpMcpTransport.Builder builder = new HttpMcpTransport.Builder()
|
||||
//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)) {
|
||||
// stdio 类型:endpoint 可能是一个命令行,需要拆分为命令列表
|
||||
// List<String> cmdParts = Arrays.asList(endpoint.trim().split("\\s+"));
|
||||
// StdioMcpTransport.Builder builder = new StdioMcpTransport.Builder()
|
||||
// .command(cmdParts)
|
||||
// .environment(headers);
|
||||
// mcpClient = new DefaultMcpClient.Builder().transport(builder.build()).build();
|
||||
return Result.error("不支持的MCP类型:" + 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);
|
||||
}
|
||||
@ -204,6 +245,29 @@ public class AiragMcpServiceImpl extends ServiceImpl<AiragMcpMapper, AiragMcp> i
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
|
||||
@ -0,0 +1,30 @@
|
||||
package org.jeecg.modules.airag.prompts.consts;
|
||||
|
||||
/**
|
||||
* AI提示词常量类
|
||||
*
|
||||
*/
|
||||
public class AiPromptsConsts {
|
||||
|
||||
/**
|
||||
* 状态:进行中
|
||||
*/
|
||||
public static final String STATUS_RUNNING = "run";
|
||||
/**
|
||||
* 状态:完成
|
||||
*/
|
||||
public static final String STATUS_COMPLETED = "completed";
|
||||
/**
|
||||
* 状态:失败
|
||||
*/
|
||||
public static final String STATUS_FAILED = "failed";
|
||||
/**
|
||||
* 业务类型:评估器
|
||||
*/
|
||||
public static final String BIZ_TYPE_EVALUATOR = "evaluator";
|
||||
/**
|
||||
* 业务类型:轨迹
|
||||
*/
|
||||
public static final String BIZ_TYPE_TRACK = "track";
|
||||
|
||||
}
|
||||
@ -0,0 +1,213 @@
|
||||
package org.jeecg.modules.airag.prompts.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.aspect.annotation.AutoLog;
|
||||
import org.jeecg.common.system.base.controller.JeecgController;
|
||||
import org.jeecg.common.system.query.QueryGenerator;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.modules.airag.prompts.consts.AiPromptsConsts;
|
||||
import org.jeecg.modules.airag.prompts.entity.AiragExtData;
|
||||
import org.jeecg.modules.airag.prompts.service.IAiragExtDataService;
|
||||
import org.jeecg.modules.airag.prompts.vo.AiragDebugVo;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @Description: airag_ext_data
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-12-24
|
||||
* @Version: V1.0
|
||||
*/
|
||||
@Tag(name="airag_ext_data")
|
||||
@RestController
|
||||
@RequestMapping("/airag/extData")
|
||||
@Slf4j
|
||||
public class AiragExtDataController extends JeecgController<AiragExtData, IAiragExtDataService> {
|
||||
@Autowired
|
||||
private IAiragExtDataService airagExtDataService;
|
||||
|
||||
/**
|
||||
* 分页列表查询
|
||||
*
|
||||
* @param airagExtData
|
||||
* @param pageNo
|
||||
* @param pageSize
|
||||
* @param req
|
||||
* @return
|
||||
*/
|
||||
//@AutoLog(value = "airag_ext_data-分页列表查询")
|
||||
@Operation(summary="airag_ext_data-分页列表查询")
|
||||
@GetMapping(value = "/list")
|
||||
public Result<IPage<AiragExtData>> queryPageList(AiragExtData airagExtData,
|
||||
@RequestParam(name="pageNo", defaultValue="1") Integer pageNo,
|
||||
@RequestParam(name="pageSize", defaultValue="10") Integer pageSize,
|
||||
HttpServletRequest req) {
|
||||
QueryWrapper<AiragExtData> queryWrapper = QueryGenerator.initQueryWrapper(airagExtData, req.getParameterMap());
|
||||
Page<AiragExtData> page = new Page<AiragExtData>(pageNo, pageSize);
|
||||
queryWrapper.eq("biz_type", AiPromptsConsts.BIZ_TYPE_EVALUATOR);
|
||||
IPage<AiragExtData> pageList = airagExtDataService.page(page, queryWrapper);
|
||||
return Result.OK(pageList);
|
||||
}
|
||||
/**
|
||||
* 调用轨迹列表查询
|
||||
*
|
||||
* @param airagExtData
|
||||
* @param pageNo
|
||||
* @param pageSize
|
||||
* @param req
|
||||
* @return
|
||||
*/
|
||||
@Operation(summary="airag_ext_data-分页列表查询")
|
||||
@GetMapping(value = "/getTrackList")
|
||||
public Result<IPage<AiragExtData>> getTrackList(AiragExtData airagExtData,
|
||||
@RequestParam(name="pageNo", defaultValue="1") Integer pageNo,
|
||||
@RequestParam(name="pageSize", defaultValue="10") Integer pageSize,
|
||||
HttpServletRequest req) {
|
||||
QueryWrapper<AiragExtData> queryWrapper = QueryGenerator.initQueryWrapper(airagExtData, req.getParameterMap());
|
||||
Page<AiragExtData> page = new Page<AiragExtData>(pageNo, pageSize);
|
||||
queryWrapper.eq("biz_type", AiPromptsConsts.BIZ_TYPE_TRACK);
|
||||
String metadata = airagExtData.getMetadata();
|
||||
if(oConvertUtils.isEmpty(metadata)){
|
||||
return Result.OK();
|
||||
}
|
||||
IPage<AiragExtData> pageList = airagExtDataService.page(page, queryWrapper);
|
||||
return Result.OK(pageList);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加
|
||||
*
|
||||
* @param airagExtData
|
||||
* @return
|
||||
*/
|
||||
@AutoLog(value = "airag_ext_data-添加")
|
||||
@Operation(summary="airag_ext_data-添加")
|
||||
@PostMapping(value = "/add")
|
||||
public Result<String> add(@RequestBody AiragExtData airagExtData) {
|
||||
airagExtData.setBizType(AiPromptsConsts.BIZ_TYPE_EVALUATOR);
|
||||
airagExtDataService.save(airagExtData);
|
||||
return Result.OK("添加成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑
|
||||
*
|
||||
* @param airagExtData
|
||||
* @return
|
||||
*/
|
||||
@AutoLog(value = "airag_ext_data-编辑")
|
||||
@Operation(summary="airag_ext_data-编辑")
|
||||
@RequestMapping(value = "/edit", method = {RequestMethod.PUT,RequestMethod.POST})
|
||||
public Result<String> edit(@RequestBody AiragExtData airagExtData) {
|
||||
airagExtDataService.updateById(airagExtData);
|
||||
return Result.OK("编辑成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过id删除
|
||||
*
|
||||
* @param id
|
||||
* @return
|
||||
*/
|
||||
@AutoLog(value = "airag_ext_data-通过id删除")
|
||||
@Operation(summary="airag_ext_data-通过id删除")
|
||||
@DeleteMapping(value = "/delete")
|
||||
public Result<String> delete(@RequestParam(name="id",required=true) String id) {
|
||||
airagExtDataService.removeById(id);
|
||||
return Result.OK("删除成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除
|
||||
*
|
||||
* @param ids
|
||||
* @return
|
||||
*/
|
||||
@AutoLog(value = "airag_ext_data-批量删除")
|
||||
@Operation(summary="airag_ext_data-批量删除")
|
||||
@DeleteMapping(value = "/deleteBatch")
|
||||
public Result<String> deleteBatch(@RequestParam(name="ids",required=true) String ids) {
|
||||
this.airagExtDataService.removeByIds(Arrays.asList(ids.split(",")));
|
||||
return Result.OK("批量删除成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过id查询
|
||||
*
|
||||
* @param id
|
||||
* @return
|
||||
*/
|
||||
//@AutoLog(value = "airag_ext_data-通过id查询")
|
||||
@Operation(summary="airag_ext_data-通过id查询")
|
||||
@GetMapping(value = "/queryById")
|
||||
public Result<AiragExtData> queryById(@RequestParam(name="id",required=true) String id) {
|
||||
AiragExtData airagExtData = airagExtDataService.getById(id);
|
||||
if(airagExtData==null) {
|
||||
return Result.error("未找到对应数据");
|
||||
}
|
||||
return Result.OK(airagExtData);
|
||||
}
|
||||
/**
|
||||
* 通过id查询
|
||||
*
|
||||
* @param id
|
||||
* @return
|
||||
*/
|
||||
//@AutoLog(value = "airag_ext_data-通过id查询")
|
||||
@Operation(summary="airag_ext_data-通过id查询")
|
||||
@GetMapping(value = "/queryTrackById")
|
||||
public Result<List<AiragExtData>> queryTrackById(@RequestParam(name="id",required=true) String id) {
|
||||
AiragExtData airagExtData = airagExtDataService.getById(id);
|
||||
String status = airagExtData.getStatus();
|
||||
if(AiPromptsConsts.STATUS_RUNNING.equals(status)) {
|
||||
return Result.error("处理中,请稍后刷新");
|
||||
}
|
||||
List<AiragExtData> trackList = airagExtDataService.queryTrackById(id);
|
||||
return Result.OK(trackList);
|
||||
}
|
||||
/**
|
||||
* 构造器调试
|
||||
*
|
||||
* @param debugVo
|
||||
* @return
|
||||
*/
|
||||
@PostMapping(value = "/evaluator/debug")
|
||||
public Result<?> debugEvaluator(@RequestBody AiragDebugVo debugVo) {
|
||||
return airagExtDataService.debugEvaluator(debugVo);
|
||||
}
|
||||
/**
|
||||
* 导出excel
|
||||
*
|
||||
* @param request
|
||||
* @param airagExtData
|
||||
*/
|
||||
@RequestMapping(value = "/exportXls")
|
||||
public ModelAndView exportXls(HttpServletRequest request, AiragExtData airagExtData) {
|
||||
return super.exportXls(request, airagExtData, AiragExtData.class, "airag_ext_data");
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过excel导入数据
|
||||
*
|
||||
* @param request
|
||||
* @param response
|
||||
* @return
|
||||
*/
|
||||
@RequestMapping(value = "/importExcel", method = RequestMethod.POST)
|
||||
public Result<?> importExcel(HttpServletRequest request, HttpServletResponse response) {
|
||||
return super.importExcel(request, response, AiragExtData.class);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,167 @@
|
||||
package org.jeecg.modules.airag.prompts.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.aspect.annotation.AutoLog;
|
||||
import org.jeecg.common.constant.CommonConstant;
|
||||
import org.jeecg.common.system.base.controller.JeecgController;
|
||||
import org.jeecg.common.system.query.QueryGenerator;
|
||||
import org.jeecg.modules.airag.prompts.entity.AiragPrompts;
|
||||
import org.jeecg.modules.airag.prompts.service.IAiragPromptsService;
|
||||
import org.jeecg.modules.airag.prompts.vo.AiragExperimentVo;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
||||
import java.util.Arrays;
|
||||
/**
|
||||
* @Description: airag_prompts
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-12-24
|
||||
* @Version: V1.0
|
||||
*/
|
||||
@Tag(name="airag_prompts")
|
||||
@RestController
|
||||
@RequestMapping("/airag/prompts")
|
||||
@Slf4j
|
||||
public class AiragPromptsController extends JeecgController<AiragPrompts, IAiragPromptsService> {
|
||||
@Autowired
|
||||
private IAiragPromptsService airagPromptsService;
|
||||
|
||||
/**
|
||||
* 分页列表查询
|
||||
*
|
||||
* @param airagPrompts
|
||||
* @param pageNo
|
||||
* @param pageSize
|
||||
* @param req
|
||||
* @return
|
||||
*/
|
||||
//@AutoLog(value = "airag_prompts-分页列表查询")
|
||||
@Operation(summary="airag_prompts-分页列表查询")
|
||||
@GetMapping(value = "/list")
|
||||
public Result<IPage<AiragPrompts>> queryPageList(AiragPrompts airagPrompts,
|
||||
@RequestParam(name="pageNo", defaultValue="1") Integer pageNo,
|
||||
@RequestParam(name="pageSize", defaultValue="10") Integer pageSize,
|
||||
HttpServletRequest req) {
|
||||
QueryWrapper<AiragPrompts> queryWrapper = QueryGenerator.initQueryWrapper(airagPrompts, req.getParameterMap());
|
||||
Page<AiragPrompts> page = new Page<AiragPrompts>(pageNo, pageSize);
|
||||
IPage<AiragPrompts> pageList = airagPromptsService.page(page, queryWrapper);
|
||||
return Result.OK(pageList);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加
|
||||
*
|
||||
* @param airagPrompts
|
||||
* @return
|
||||
*/
|
||||
@AutoLog(value = "airag_prompts-添加")
|
||||
@Operation(summary="airag_prompts-添加")
|
||||
@PostMapping(value = "/add")
|
||||
public Result<String> add(@RequestBody AiragPrompts airagPrompts) {
|
||||
airagPrompts.setDelFlag(CommonConstant.DEL_FLAG_0);
|
||||
airagPrompts.setStatus("0");
|
||||
airagPromptsService.save(airagPrompts);
|
||||
return Result.OK("添加成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑
|
||||
*
|
||||
* @param airagPrompts
|
||||
* @return
|
||||
*/
|
||||
@AutoLog(value = "airag_prompts-编辑")
|
||||
@Operation(summary="airag_prompts-编辑")
|
||||
@RequestMapping(value = "/edit", method = {RequestMethod.PUT,RequestMethod.POST})
|
||||
public Result<String> edit(@RequestBody AiragPrompts airagPrompts) {
|
||||
airagPromptsService.updateById(airagPrompts);
|
||||
return Result.OK("编辑成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过id删除
|
||||
*
|
||||
* @param id
|
||||
* @return
|
||||
*/
|
||||
@AutoLog(value = "airag_prompts-通过id删除")
|
||||
@Operation(summary="airag_prompts-通过id删除")
|
||||
@DeleteMapping(value = "/delete")
|
||||
public Result<String> delete(@RequestParam(name="id",required=true) String id) {
|
||||
airagPromptsService.removeById(id);
|
||||
return Result.OK("删除成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除
|
||||
*
|
||||
* @param ids
|
||||
* @return
|
||||
*/
|
||||
@AutoLog(value = "airag_prompts-批量删除")
|
||||
@Operation(summary="airag_prompts-批量删除")
|
||||
@DeleteMapping(value = "/deleteBatch")
|
||||
public Result<String> deleteBatch(@RequestParam(name="ids",required=true) String ids) {
|
||||
this.airagPromptsService.removeByIds(Arrays.asList(ids.split(",")));
|
||||
return Result.OK("批量删除成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过id查询
|
||||
*
|
||||
* @param id
|
||||
* @return
|
||||
*/
|
||||
//@AutoLog(value = "airag_prompts-通过id查询")
|
||||
@Operation(summary="airag_prompts-通过id查询")
|
||||
@GetMapping(value = "/queryById")
|
||||
public Result<AiragPrompts> queryById(@RequestParam(name="id",required=true) String id) {
|
||||
AiragPrompts airagPrompts = airagPromptsService.getById(id);
|
||||
if(airagPrompts==null) {
|
||||
return Result.error("未找到对应数据");
|
||||
}
|
||||
return Result.OK(airagPrompts);
|
||||
}
|
||||
/**
|
||||
* 构造器调试
|
||||
*
|
||||
* @param experimentVo
|
||||
* @return
|
||||
*/
|
||||
@PostMapping(value = "/experiment")
|
||||
public Result<?> promptExperiment(@RequestBody AiragExperimentVo experimentVo, HttpServletRequest request) {
|
||||
return airagPromptsService.promptExperiment(experimentVo,request);
|
||||
}
|
||||
/**
|
||||
* 导出excel
|
||||
*
|
||||
* @param request
|
||||
* @param airagPrompts
|
||||
*/
|
||||
@RequestMapping(value = "/exportXls")
|
||||
public ModelAndView exportXls(HttpServletRequest request, AiragPrompts airagPrompts) {
|
||||
return super.exportXls(request, airagPrompts, AiragPrompts.class, "airag_prompts");
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过excel导入数据
|
||||
*
|
||||
* @param request
|
||||
* @param response
|
||||
* @return
|
||||
*/
|
||||
@RequestMapping(value = "/importExcel", method = RequestMethod.POST)
|
||||
public Result<?> importExcel(HttpServletRequest request, HttpServletResponse response) {
|
||||
return super.importExcel(request, response, AiragPrompts.class);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,99 @@
|
||||
package org.jeecg.modules.airag.prompts.entity;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.util.Date;
|
||||
import java.math.BigDecimal;
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||
import org.jeecg.common.constant.ProvinceCityArea;
|
||||
import org.jeecg.common.util.SpringContextUtils;
|
||||
import lombok.Data;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
import org.jeecgframework.poi.excel.annotation.Excel;
|
||||
import org.jeecg.common.aspect.annotation.Dict;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
/**
|
||||
* @Description: airag_ext_data
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-12-12
|
||||
* @Version: V1.0
|
||||
*/
|
||||
@Data
|
||||
@TableName("airag_ext_data")
|
||||
@Accessors(chain = true)
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@Schema(description="airag_ext_data")
|
||||
public class AiragExtData implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**主键ID*/
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
@Schema(description = "主键ID")
|
||||
private java.lang.String id;
|
||||
/**业务类型标识(evaluator:评估器;track:测试追踪)*/
|
||||
@Excel(name = "业务类型标识(evaluator:评估器;track:测试追踪)", width = 15)
|
||||
@Schema(description = "业务类型标识(evaluator:评估器;track:测试追踪)")
|
||||
private java.lang.String bizType;
|
||||
/**名称*/
|
||||
@Excel(name = "名称", width = 15)
|
||||
@Schema(description = "名称")
|
||||
private java.lang.String name;
|
||||
/**描述信息*/
|
||||
@Excel(name = "描述信息", width = 15)
|
||||
@Schema(description = "描述信息")
|
||||
private java.lang.String descr;
|
||||
/**标签,多个用逗号分隔*/
|
||||
@Excel(name = "标签,多个用逗号分隔", width = 15)
|
||||
@Schema(description = "标签,多个用逗号分隔")
|
||||
private java.lang.String tags;
|
||||
/**实际存储内容,json*/
|
||||
@Excel(name = "实际存储内容,json", width = 15)
|
||||
@Schema(description = "实际存储内容,json")
|
||||
private java.lang.String dataValue;
|
||||
/**元数据,用于存储补充业务数据信息*/
|
||||
@Excel(name = "元数据,用于存储补充业务数据信息", width = 15)
|
||||
@Schema(description = "元数据,用于存储补充业务数据信息")
|
||||
private java.lang.String metadata;
|
||||
/**评测集数据*/
|
||||
@Excel(name = "评测集数据", width = 15)
|
||||
@Schema(description = "评测集数据")
|
||||
private java.lang.String datasetValue;
|
||||
/**创建人*/
|
||||
@Schema(description = "创建人")
|
||||
private java.lang.String createBy;
|
||||
/**创建时间*/
|
||||
@JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss")
|
||||
@Schema(description = "创建时间")
|
||||
private java.util.Date createTime;
|
||||
/**修改人*/
|
||||
@Schema(description = "修改人")
|
||||
private java.lang.String updateBy;
|
||||
/**修改时间*/
|
||||
@JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss")
|
||||
@Schema(description = "修改时间")
|
||||
private java.util.Date updateTime;
|
||||
/**所属部门*/
|
||||
@Schema(description = "所属部门")
|
||||
private java.lang.String sysOrgCode;
|
||||
/**租户id*/
|
||||
@Excel(name = "租户id", width = 15)
|
||||
@Schema(description = "租户id")
|
||||
private java.lang.String tenantId;
|
||||
/**状态*/
|
||||
@Excel(name = "状态(run:进行中 completed:已完成)", width = 15)
|
||||
@Schema(description = "状态(run:进行中 completed:已完成)")
|
||||
private java.lang.String status;
|
||||
/**版本*/
|
||||
@Excel(name = "版本", width = 15)
|
||||
@Schema(description = "版本")
|
||||
private java.lang.Integer version;
|
||||
}
|
||||
@ -0,0 +1,108 @@
|
||||
package org.jeecg.modules.airag.prompts.entity;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.util.Date;
|
||||
import java.math.BigDecimal;
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||
import org.jeecg.common.constant.ProvinceCityArea;
|
||||
import org.jeecg.common.util.SpringContextUtils;
|
||||
import lombok.Data;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
import org.jeecgframework.poi.excel.annotation.Excel;
|
||||
import org.jeecg.common.aspect.annotation.Dict;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
/**
|
||||
* @Description: airag_prompts
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-12-12
|
||||
* @Version: V1.0
|
||||
*/
|
||||
@Data
|
||||
@TableName("airag_prompts")
|
||||
@Accessors(chain = true)
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@Schema(description="airag_prompts")
|
||||
public class AiragPrompts implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**主键ID*/
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
@Schema(description = "主键ID")
|
||||
private java.lang.String id;
|
||||
/**提示词名称*/
|
||||
@Excel(name = "提示词名称", width = 15)
|
||||
@Schema(description = "提示词名称")
|
||||
private java.lang.String name;
|
||||
/**提示词名称*/
|
||||
@Excel(name = "提示key", width = 15)
|
||||
@Schema(description = "提示key")
|
||||
private java.lang.String promptKey;
|
||||
/**提示词功能描述*/
|
||||
@Excel(name = "提示词功能描述", width = 15)
|
||||
@Schema(description = "提示词功能描述")
|
||||
private java.lang.String description;
|
||||
/**提示词模板内容,支持变量占位符如 {{variable}}*/
|
||||
@Excel(name = "提示词模板内容,支持变量占位符如 {{variable}}", width = 15)
|
||||
@Schema(description = "提示词模板内容,支持变量占位符如 {{variable}}")
|
||||
private java.lang.String content;
|
||||
/**提示词分类*/
|
||||
@Excel(name = "提示词分类", width = 15)
|
||||
@Schema(description = "提示词分类")
|
||||
private java.lang.String category;
|
||||
/**标签,多个逗号分割*/
|
||||
@Excel(name = "标签,多个逗号分割", width = 15)
|
||||
@Schema(description = "标签,多个逗号分割")
|
||||
private java.lang.String tags;
|
||||
/**适配的大模型ID*/
|
||||
@Excel(name = "适配的大模型ID", width = 15)
|
||||
@Schema(description = "适配的大模型ID")
|
||||
private java.lang.String modelId;
|
||||
/**大模型的参数配置*/
|
||||
@Excel(name = "大模型的参数配置", width = 15)
|
||||
@Schema(description = "大模型的参数配置")
|
||||
private java.lang.String modelParam;
|
||||
/**状态(0:未发布 1:已发布)*/
|
||||
@Excel(name = "状态(0:未发布 1:已发布)", width = 15)
|
||||
@Schema(description = "状态(0:未发布 1:已发布)")
|
||||
private java.lang.String status;
|
||||
/**版本号(格式 0.0.1)*/
|
||||
@Excel(name = "版本号(格式 0.0.1)", width = 15)
|
||||
@Schema(description = "版本号(格式 0.0.1)")
|
||||
private java.lang.String version;
|
||||
/**删除状态(0未删除 1已删除)*/
|
||||
@Excel(name = "删除状态(0未删除 1已删除)", width = 15)
|
||||
@Schema(description = "删除状态(0未删除 1已删除)")
|
||||
@TableLogic
|
||||
private java.lang.Integer delFlag;
|
||||
/**创建人*/
|
||||
@Schema(description = "创建人")
|
||||
private java.lang.String createBy;
|
||||
/**创建日期*/
|
||||
@JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss")
|
||||
@Schema(description = "创建日期")
|
||||
private java.util.Date createTime;
|
||||
/**更新人*/
|
||||
@Schema(description = "更新人")
|
||||
private java.lang.String updateBy;
|
||||
/**更新日期*/
|
||||
@JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss")
|
||||
@Schema(description = "更新日期")
|
||||
private java.util.Date updateTime;
|
||||
/**所属部门*/
|
||||
@Schema(description = "所属部门")
|
||||
private java.lang.String sysOrgCode;
|
||||
/**租户id*/
|
||||
@Excel(name = "租户id", width = 15)
|
||||
@Schema(description = "租户id")
|
||||
private java.lang.String tenantId;
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
package org.jeecg.modules.airag.prompts.mapper;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.jeecg.modules.airag.prompts.entity.AiragExtData;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
|
||||
/**
|
||||
* @Description: airag_ext_data
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-12-12
|
||||
* @Version: V1.0
|
||||
*/
|
||||
public interface AiragExtDataMapper extends BaseMapper<AiragExtData> {
|
||||
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
package org.jeecg.modules.airag.prompts.mapper;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.jeecg.modules.airag.prompts.entity.AiragPrompts;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
|
||||
/**
|
||||
* @Description: airag_prompts
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-12-12
|
||||
* @Version: V1.0
|
||||
*/
|
||||
public interface AiragPromptsMapper extends BaseMapper<AiragPrompts> {
|
||||
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="org.jeecg.modules.airag.prompts.mapper.AiragExtDataMapper">
|
||||
|
||||
</mapper>
|
||||
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="org.jeecg.modules.airag.prompts.mapper.AiragPromptsMapper">
|
||||
|
||||
</mapper>
|
||||
@ -0,0 +1,21 @@
|
||||
package org.jeecg.modules.airag.prompts.service;
|
||||
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.modules.airag.prompts.entity.AiragExtData;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import org.jeecg.modules.airag.prompts.vo.AiragDebugVo;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @Description: airag_ext_data
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-12-12
|
||||
* @Version: V1.0
|
||||
*/
|
||||
public interface IAiragExtDataService extends IService<AiragExtData> {
|
||||
|
||||
Result debugEvaluator(AiragDebugVo debugVo);
|
||||
|
||||
List<AiragExtData> queryTrackById(String id);
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
package org.jeecg.modules.airag.prompts.service;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.modules.airag.prompts.entity.AiragPrompts;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import org.jeecg.modules.airag.prompts.vo.AiragExperimentVo;
|
||||
|
||||
/**
|
||||
* @Description: airag_prompts
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-12-12
|
||||
* @Version: V1.0
|
||||
*/
|
||||
public interface IAiragPromptsService extends IService<AiragPrompts> {
|
||||
|
||||
Result<?> promptExperiment(AiragExperimentVo experimentVo, HttpServletRequest request);
|
||||
}
|
||||
@ -0,0 +1,98 @@
|
||||
package org.jeecg.modules.airag.prompts.service.impl;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import dev.langchain4j.data.message.ChatMessage;
|
||||
import dev.langchain4j.data.message.SystemMessage;
|
||||
import dev.langchain4j.data.message.UserMessage;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.util.AssertUtils;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.modules.airag.common.handler.AIChatParams;
|
||||
import org.jeecg.modules.airag.common.handler.IAIChatHandler;
|
||||
import org.jeecg.modules.airag.prompts.entity.AiragExtData;
|
||||
import org.jeecg.modules.airag.prompts.mapper.AiragExtDataMapper;
|
||||
import org.jeecg.modules.airag.prompts.service.IAiragExtDataService;
|
||||
import org.jeecg.modules.airag.prompts.vo.AiragDebugVo;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @Description: airag_ext_data
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-12-12
|
||||
* @Version: V1.0
|
||||
*/
|
||||
@Service("airagExtDataServiceImpl")
|
||||
public class AiragExtDataServiceImpl extends ServiceImpl<AiragExtDataMapper, AiragExtData> implements IAiragExtDataService {
|
||||
|
||||
@Autowired
|
||||
IAIChatHandler aiChatHandler;
|
||||
|
||||
@Override
|
||||
public Result debugEvaluator(AiragDebugVo debugVo) {
|
||||
//1.提示词
|
||||
String prompt = debugVo.getPrompts();
|
||||
AssertUtils.assertNotEmpty("请输入提示词", prompt);
|
||||
|
||||
//2.测试内容
|
||||
String content = debugVo.getContent();
|
||||
AssertUtils.assertNotEmpty("请输入测试内容", content);
|
||||
List<ChatMessage> messages = Arrays.asList(new SystemMessage(prompt), new UserMessage(content));
|
||||
|
||||
//3.模型数据
|
||||
String modelId = debugVo.getModelId();
|
||||
AssertUtils.assertNotEmpty("请选择模型", modelId);
|
||||
|
||||
//4.模型参数
|
||||
String modelParam = debugVo.getModelParam();
|
||||
// 默认大模型参数
|
||||
AIChatParams params = new AIChatParams();
|
||||
params.setTemperature(0.8);
|
||||
params.setTopP(0.9);
|
||||
params.setPresencePenalty(0.1);
|
||||
params.setFrequencyPenalty(0.1);
|
||||
|
||||
if(oConvertUtils.isNotEmpty(modelParam)){
|
||||
JSONObject param = JSON.parseObject(modelParam);
|
||||
if(param.containsKey("temperature")){
|
||||
params.setTemperature(param.getDoubleValue("temperature"));
|
||||
}
|
||||
if(param.containsKey("topP")){
|
||||
params.setTemperature(param.getDoubleValue("topP"));
|
||||
}
|
||||
if(param.containsKey("presencePenalty")){
|
||||
params.setTemperature(param.getDoubleValue("presencePenalty"));
|
||||
}
|
||||
if(param.containsKey("frequencyPenalty")){
|
||||
params.setTemperature(param.getDoubleValue("frequencyPenalty"));
|
||||
}
|
||||
}
|
||||
//5.AI问答
|
||||
String promptValue = aiChatHandler.completions(modelId,messages, params);
|
||||
if (promptValue == null || promptValue.isEmpty()) {
|
||||
return Result.error("生成失败");
|
||||
}
|
||||
return Result.OK("success", promptValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询AI问答记录
|
||||
* @param id
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public List<AiragExtData> queryTrackById(String id) {
|
||||
LambdaQueryWrapper<AiragExtData> lqw = new LambdaQueryWrapper<AiragExtData>()
|
||||
.eq(AiragExtData::getMetadata, id)
|
||||
.orderByDesc(AiragExtData::getVersion)
|
||||
.orderByDesc(AiragExtData::getCreateTime);
|
||||
List<AiragExtData> list = this.baseMapper.selectList(lqw);
|
||||
return list;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,394 @@
|
||||
package org.jeecg.modules.airag.prompts.service.impl;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONArray;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import dev.langchain4j.data.message.ChatMessage;
|
||||
import dev.langchain4j.data.message.SystemMessage;
|
||||
import dev.langchain4j.data.message.UserMessage;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.util.AssertUtils;
|
||||
import org.jeecg.common.util.CommonUtils;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.config.JeecgBaseConfig;
|
||||
import org.jeecg.modules.airag.common.handler.AIChatParams;
|
||||
import org.jeecg.modules.airag.common.handler.IAIChatHandler;
|
||||
import org.jeecg.modules.airag.prompts.consts.AiPromptsConsts;
|
||||
import org.jeecg.modules.airag.prompts.entity.AiragExtData;
|
||||
import org.jeecg.modules.airag.prompts.entity.AiragPrompts;
|
||||
import org.jeecg.modules.airag.prompts.mapper.AiragPromptsMapper;
|
||||
import org.jeecg.modules.airag.prompts.service.IAiragExtDataService;
|
||||
import org.jeecg.modules.airag.prompts.service.IAiragPromptsService;
|
||||
import org.jeecg.modules.airag.prompts.vo.AiragExperimentVo;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
/**
|
||||
* @Description: airag_prompts
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-12-12
|
||||
* @Version: V1.0
|
||||
*/
|
||||
@Slf4j
|
||||
@Service("airagPromptsServiceImpl")
|
||||
public class AiragPromptsServiceImpl extends ServiceImpl<AiragPromptsMapper, AiragPrompts> implements IAiragPromptsService {
|
||||
@Autowired
|
||||
IAIChatHandler aiChatHandler;
|
||||
|
||||
@Autowired
|
||||
IAiragExtDataService airagExtDataService;
|
||||
|
||||
@Autowired
|
||||
private JeecgBaseConfig jeecgBaseConfig;
|
||||
// 创建静态线程池,确保整个应用生命周期中只有一个实例
|
||||
private static final ExecutorService executor = new ThreadPoolExecutor(
|
||||
4, // 核心线程数
|
||||
8, // 最大线程数
|
||||
60L, TimeUnit.SECONDS,
|
||||
new ArrayBlockingQueue<>(100), // 防止内存溢出
|
||||
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
|
||||
);
|
||||
|
||||
/**
|
||||
* 提示词实验
|
||||
* @param experimentVo
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public Result<?> promptExperiment(AiragExperimentVo experimentVo, HttpServletRequest request) {
|
||||
log.info("开始执行提示词实验,参数:{}", JSON.toJSONString(experimentVo));
|
||||
|
||||
// 参数验证
|
||||
String promptKey = experimentVo.getPromptKey();
|
||||
AssertUtils.assertNotEmpty("请选择提示词", promptKey);
|
||||
String dataId = experimentVo.getExtDataId();
|
||||
AssertUtils.assertNotEmpty("请选择数据集", dataId);
|
||||
|
||||
Map<String, String> fieldMappings = experimentVo.getMappings();
|
||||
AssertUtils.assertNotEmpty("请配置字段映射", fieldMappings);
|
||||
|
||||
try {
|
||||
//1.查询提示词
|
||||
AiragPrompts airagPrompts = this.baseMapper.selectOne(new LambdaQueryWrapper<AiragPrompts>().eq(AiragPrompts::getPromptKey, promptKey));
|
||||
AssertUtils.assertNotEmpty("未找到指定的提示词", airagPrompts);
|
||||
String modelParam = airagPrompts.getModelParam();
|
||||
// 过滤提示词变量
|
||||
JSONArray promptVariables;
|
||||
if(oConvertUtils.isNotEmpty(modelParam)){
|
||||
JSONObject airagPromptsParams = JSON.parseObject(modelParam);
|
||||
if(airagPromptsParams.containsKey("promptVariables")){
|
||||
promptVariables = airagPromptsParams.getJSONArray("promptVariables");
|
||||
} else {
|
||||
promptVariables = null;
|
||||
}
|
||||
} else {
|
||||
promptVariables = null;
|
||||
}
|
||||
//2.查询数据集
|
||||
AiragExtData airagExtData = airagExtDataService.getById(dataId);
|
||||
AssertUtils.assertNotEmpty("未找到指定的数据集", airagExtData);
|
||||
String datasetValue = airagExtData.getDatasetValue();
|
||||
if(oConvertUtils.isEmpty(datasetValue)){
|
||||
return Result.error("评测集不能为空!");
|
||||
}
|
||||
|
||||
//3.异步调用 根据映射字段,调用评估器测评
|
||||
JSONObject datasetObj = JSONObject.parseObject(datasetValue);
|
||||
//评测列配置
|
||||
JSONArray columns = datasetObj.getJSONArray("columns");
|
||||
//评测题库
|
||||
JSONArray datasetArray = datasetObj.getJSONArray("dataSource");
|
||||
AssertUtils.assertNotEmpty("数据集中没有找到数据源", datasetArray);
|
||||
AssertUtils.assertTrue("数据源为空", datasetArray.size() > 0);
|
||||
|
||||
//测评结果集 - 使用线程安全的CopyOnWriteArrayList
|
||||
List<JSONObject> scoreResult = new CopyOnWriteArrayList<>();
|
||||
|
||||
// 批量提交任务
|
||||
List<CompletableFuture<Void>> futures = IntStream.range(0, datasetArray.size())
|
||||
.mapToObj(i -> CompletableFuture.runAsync(() -> {
|
||||
try {
|
||||
log.info("开始处理第{}条数据", i + 1);
|
||||
//定义返回结果
|
||||
JSONObject result = new JSONObject();
|
||||
//评测数据
|
||||
JSONObject dataset = datasetArray.getJSONObject(i);
|
||||
result.putAll(dataset);
|
||||
//用户问题
|
||||
String userQuery = dataset.getString(fieldMappings.get("user_query"));
|
||||
result.put("userQuery", userQuery);
|
||||
//变量处理
|
||||
if(!CollectionUtils.isEmpty(promptVariables)){
|
||||
String content = airagPrompts.getContent();
|
||||
for (Object var : promptVariables){
|
||||
JSONObject variable = JSONObject.parseObject(var.toString());
|
||||
String name = dataset.getString(fieldMappings.get(variable.getString("name")));
|
||||
//提示词默认变量值
|
||||
String defaultValue = variable.getString("value");
|
||||
// 获取目标类型
|
||||
String dataType = findDataType(columns, variable);
|
||||
if("FILE".equals(dataType)){
|
||||
defaultValue = getFileAccessHttpUrl(request, defaultValue);
|
||||
name = getFileAccessHttpUrl(request, name);
|
||||
}
|
||||
if(oConvertUtils.isNotEmpty(name)){
|
||||
//提示词 评估集变量值替换
|
||||
content = content.replaceAll(variable.getString("name"), name);
|
||||
}else if(oConvertUtils.isNotEmpty(defaultValue)){
|
||||
content = content.replaceAll(variable.getString("name"), defaultValue);
|
||||
}
|
||||
}
|
||||
airagPrompts.setContent(content);
|
||||
}
|
||||
|
||||
//提示词答案
|
||||
String promptAnswer = getPromptAnswer(airagPrompts, dataset, fieldMappings);
|
||||
result.put("promptAnswer", promptAnswer);
|
||||
|
||||
//评估器答案
|
||||
String answerScore = getAnswerScore(promptAnswer, dataset, fieldMappings, airagExtData);
|
||||
result.put("answerScore", answerScore);
|
||||
|
||||
scoreResult.add(result);
|
||||
log.info("第{}条数据处理完成", i + 1);
|
||||
} catch (Exception e) {
|
||||
log.error("处理第{}条数据时发生异常", i + 1, e);
|
||||
// 重新抛出异常,让CompletableFuture捕获
|
||||
throw new CompletionException(e);
|
||||
}
|
||||
}, executor))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// 非阻塞方式处理完成
|
||||
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
|
||||
.whenComplete((result, ex) -> {
|
||||
if (ex != null) {
|
||||
log.error("批量处理失败", ex);
|
||||
// 更新状态为失败
|
||||
airagExtData.setStatus(AiPromptsConsts.STATUS_FAILED);
|
||||
} else {
|
||||
log.info("所有数据处理完成,共处理{}条数据", scoreResult.size());
|
||||
// 查询已存在的评测记录
|
||||
List<AiragExtData> existingTracks = airagExtDataService.queryTrackById(dataId);
|
||||
Integer version = 1;
|
||||
if(!CollectionUtils.isEmpty(existingTracks)) {
|
||||
version = existingTracks.stream()
|
||||
.map(AiragExtData::getVersion)
|
||||
.max(Integer::compareTo)
|
||||
.orElse(0) + 1;
|
||||
}
|
||||
for (JSONObject item : scoreResult) {
|
||||
// 保存结果
|
||||
AiragExtData track = new AiragExtData();
|
||||
//关联评估器ID
|
||||
track.setMetadata(dataId);
|
||||
//定义类型
|
||||
track.setBizType(AiPromptsConsts.BIZ_TYPE_TRACK);
|
||||
//定义版本
|
||||
track.setVersion(version);
|
||||
//定义状态
|
||||
track.setStatus(AiPromptsConsts.STATUS_COMPLETED);
|
||||
//定义评测结果
|
||||
track.setDataValue(item.toJSONString());
|
||||
airagExtDataService.save(track);
|
||||
}
|
||||
// 更新状态为完成
|
||||
airagExtData.setStatus(AiPromptsConsts.STATUS_COMPLETED);
|
||||
}
|
||||
airagExtDataService.updateById(airagExtData);
|
||||
});
|
||||
|
||||
//4.修改状态进行中
|
||||
airagExtData.setStatus(AiPromptsConsts.STATUS_RUNNING);
|
||||
airagExtDataService.updateById(airagExtData);
|
||||
|
||||
log.info("提示词实验已提交,共{}条数据待处理", datasetArray.size());
|
||||
return Result.OK("实验已开始,正在处理数据");
|
||||
} catch (Exception e) {
|
||||
log.error("提示词实验执行失败", e);
|
||||
return Result.error("实验执行失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 提示词回答的结果
|
||||
* @param airagPrompts
|
||||
* @param questions
|
||||
* @param fieldMappings
|
||||
* @return
|
||||
*/
|
||||
public String getPromptAnswer(AiragPrompts airagPrompts, JSONObject questions, Map<String, String> fieldMappings) {
|
||||
try {
|
||||
//0.判断是否配置了判断fieldMappings的value值 是否包含actual_output
|
||||
if (!fieldMappings.containsValue("actual_output")) {
|
||||
log.warn("字段映射中没有配置actual_output");
|
||||
return null;
|
||||
}
|
||||
|
||||
//1.提示词
|
||||
String prompt = airagPrompts.getContent();
|
||||
AssertUtils.assertNotEmpty("请输入提示词", prompt);
|
||||
|
||||
String userQuery = questions.getString(fieldMappings.get("user_query"));
|
||||
AssertUtils.assertNotEmpty("请输入测试内容", userQuery);
|
||||
|
||||
//2.ai问题组装
|
||||
List<ChatMessage> messages = Arrays.asList(new SystemMessage(prompt), new UserMessage(userQuery));
|
||||
|
||||
//3.模型数据
|
||||
String modelId = airagPrompts.getModelId();
|
||||
AssertUtils.assertNotEmpty("请选择模型", modelId);
|
||||
|
||||
//4.模型参数
|
||||
String modelParam = airagPrompts.getModelParam();
|
||||
// 默认大模型参数
|
||||
AIChatParams params = new AIChatParams();
|
||||
params.setTemperature(0.8);
|
||||
params.setTopP(0.9);
|
||||
params.setPresencePenalty(0.1);
|
||||
params.setFrequencyPenalty(0.1);
|
||||
|
||||
if(oConvertUtils.isNotEmpty(modelParam)){
|
||||
JSONObject param = JSON.parseObject(modelParam);
|
||||
if(param.containsKey("temperature")){
|
||||
params.setTemperature(param.getDoubleValue("temperature"));
|
||||
}
|
||||
if(param.containsKey("topP")){
|
||||
params.setTopP(param.getDoubleValue("topP")); // 修复:设置到正确的字段
|
||||
}
|
||||
if(param.containsKey("presencePenalty")){
|
||||
params.setPresencePenalty(param.getDoubleValue("presencePenalty")); // 修复:设置到正确的字段
|
||||
}
|
||||
if(param.containsKey("frequencyPenalty")){
|
||||
params.setFrequencyPenalty(param.getDoubleValue("frequencyPenalty")); // 修复:设置到正确的字段
|
||||
}
|
||||
}
|
||||
|
||||
log.debug("调用AI模型,模型ID:{},参数:{}", modelId, JSON.toJSONString(params));
|
||||
//5.AI问答
|
||||
String promptAnswer = aiChatHandler.completions(modelId, messages, params);
|
||||
log.debug("AI模型返回结果:{}", promptAnswer);
|
||||
|
||||
return promptAnswer;
|
||||
} catch (Exception e) {
|
||||
log.error("获取提示词回答失败", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 评测答案分数
|
||||
* @return
|
||||
*/
|
||||
public String getAnswerScore(String promptAnswer, JSONObject questions, Map<String, String> fieldMappings, AiragExtData airagExtData) {
|
||||
try {
|
||||
//1.提示词
|
||||
String prompt = airagExtData.getDataValue();
|
||||
AssertUtils.assertNotEmpty("请输入提示词", prompt);
|
||||
prompt += "定义返回格式: 得分:最终的得分,必须输出一个数字,表示满足Prompt中评分标准的程度。得分范围从 0.0 到 1.0,1.0 表示完全满足评分标准,0.0 表示完全不满足评分标准。\n" +
|
||||
"原因:{对得分的可读性的解释,说明打分原因}。最后,必须用一句话结束理由,该句话为:因此,应该给出的分数是<你评测的的得分>。请勿返回提问的问题、添加分析过程、解释说明等内容,只返回要求的格式内容";
|
||||
|
||||
String userQuery = "输入的内容:";
|
||||
//2.拼接测试内容
|
||||
for (Map.Entry<String, String> entry : fieldMappings.entrySet()) {
|
||||
// 评估器中的key
|
||||
String key = entry.getKey();
|
||||
// 评估器中的映射的key
|
||||
String value = entry.getValue();
|
||||
String valueData;
|
||||
if("actual_output".equalsIgnoreCase(value)){
|
||||
valueData = promptAnswer;
|
||||
}else{
|
||||
valueData = questions.getString(value);
|
||||
}
|
||||
userQuery += (key + ":" + valueData + " ");
|
||||
}
|
||||
List<ChatMessage> messages = Arrays.asList(new SystemMessage(prompt), new UserMessage(userQuery));
|
||||
|
||||
//3.模型数据
|
||||
String metadata = airagExtData.getMetadata();
|
||||
if(oConvertUtils.isNotEmpty(metadata)){
|
||||
JSONObject modelParam = JSONObject.parseObject(metadata);
|
||||
String modelId = modelParam.getString("modelId");
|
||||
AssertUtils.assertNotEmpty("评估器模型ID不能为空", modelId);
|
||||
|
||||
// 默认大模型参数
|
||||
AIChatParams params = new AIChatParams();
|
||||
params.setTemperature(0.8);
|
||||
params.setTopP(0.9);
|
||||
params.setPresencePenalty(0.1);
|
||||
params.setFrequencyPenalty(0.1);
|
||||
|
||||
if(oConvertUtils.isNotEmpty(modelParam)){
|
||||
if(modelParam.containsKey("temperature")){
|
||||
params.setTemperature(modelParam.getDoubleValue("temperature"));
|
||||
}
|
||||
if(modelParam.containsKey("topP")){
|
||||
params.setTopP(modelParam.getDoubleValue("topP")); // 修复:设置到正确的字段
|
||||
}
|
||||
if(modelParam.containsKey("presencePenalty")){
|
||||
params.setPresencePenalty(modelParam.getDoubleValue("presencePenalty")); // 修复:设置到正确的字段
|
||||
}
|
||||
if(modelParam.containsKey("frequencyPenalty")){
|
||||
params.setFrequencyPenalty(modelParam.getDoubleValue("frequencyPenalty")); // 修复:设置到正确的字段
|
||||
}
|
||||
}
|
||||
|
||||
log.debug("调用评估器模型,模型ID:{},参数:{}", modelId, JSON.toJSONString(params));
|
||||
//5.AI问答
|
||||
String answerScore = aiChatHandler.completions(modelId, messages, params);
|
||||
log.debug("评估器模型返回结果:{}", answerScore);
|
||||
|
||||
return answerScore;
|
||||
}
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
log.error("获取答案评分失败", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param columns
|
||||
* @param variable
|
||||
* @return
|
||||
*/
|
||||
public static String findDataType(JSONArray columns, JSONObject variable) {
|
||||
// 获取目标字段值
|
||||
String targetName = variable.getString("name");
|
||||
|
||||
// 使用 Stream API 查找并获取 dataType
|
||||
return columns.stream()
|
||||
.map(obj -> JSONObject.parseObject(obj.toString()))
|
||||
.filter(column -> targetName.equals(column.getString("name")))
|
||||
.findFirst()
|
||||
.map(column -> column.getString("dataType"))
|
||||
.orElse(null); // 如果没有找到,返回 null
|
||||
}
|
||||
/**
|
||||
* 获取图片地址
|
||||
* @param request
|
||||
* @param url
|
||||
* @return
|
||||
*/
|
||||
private String getFileAccessHttpUrl(HttpServletRequest request,String url){
|
||||
if(oConvertUtils.isNotEmpty(url) && url.startsWith("http")){
|
||||
return url;
|
||||
}else{
|
||||
return CommonUtils.getBaseUrl(request) + "/sys/common/static/" + url;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,39 @@
|
||||
package org.jeecg.modules.airag.prompts.vo;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
import org.jeecgframework.poi.excel.annotation.Excel;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* @Description: AiragDebugVo
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-12-12
|
||||
* @Version: V1.0
|
||||
*/
|
||||
@Data
|
||||
public class AiragDebugVo implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
/**
|
||||
* 提示词
|
||||
*/
|
||||
private String prompts;
|
||||
/**
|
||||
* 输入内容
|
||||
*/
|
||||
private String content;
|
||||
/**适配的大模型ID*/
|
||||
private String modelId;
|
||||
/**大模型的参数配置*/
|
||||
private String modelParam;
|
||||
}
|
||||
@ -0,0 +1,30 @@
|
||||
package org.jeecg.modules.airag.prompts.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @Description: AiragExperimentVo
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-12-12
|
||||
* @Version: V1.0
|
||||
*/
|
||||
@Data
|
||||
public class AiragExperimentVo implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
/**
|
||||
* 提示词
|
||||
*/
|
||||
private String promptKey;
|
||||
/**
|
||||
* 输入内容
|
||||
*/
|
||||
private String extDataId;
|
||||
/**
|
||||
* 映射关系
|
||||
*/
|
||||
private Map<String,String> mappings;
|
||||
}
|
||||
@ -0,0 +1,30 @@
|
||||
package org.jeecg.modules.airag.wordtpl.consts;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* @author chenrui
|
||||
* @ClassName: TitleLevelEnum
|
||||
* @Description: 标题级别
|
||||
* @date 2024年5月4日07:38:30
|
||||
*/
|
||||
@Getter
|
||||
public enum WordTitleEnum {
|
||||
|
||||
FIRST("first", "标题1"),
|
||||
SECOND("second", "标题2"),
|
||||
THIRD("third", "标题3"),
|
||||
FOURTH("fourth", "标题4"),
|
||||
FIFTH("fifth", "标题5"),
|
||||
SIXTH("sixth", "标题6");
|
||||
|
||||
WordTitleEnum(String code, String name) {
|
||||
this.code = code;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
final String code;
|
||||
|
||||
final String name;
|
||||
|
||||
}
|
||||
@ -0,0 +1,244 @@
|
||||
package org.jeecg.modules.airag.wordtpl.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.aspect.annotation.AutoLog;
|
||||
import org.jeecg.common.exception.JeecgBootException;
|
||||
import org.jeecg.common.system.base.controller.JeecgController;
|
||||
import org.jeecg.common.system.query.QueryGenerator;
|
||||
import org.jeecg.common.util.AssertUtils;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.modules.airag.wordtpl.dto.WordTplGenDTO;
|
||||
import org.jeecg.modules.airag.wordtpl.entity.EoaWordTemplate;
|
||||
import org.jeecg.modules.airag.wordtpl.service.IEoaWordTemplateService;
|
||||
import org.jeecg.modules.airag.wordtpl.utils.WordTplUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.net.URLEncoder;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* @Description: word模版管理
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-07-04
|
||||
* @Version: V1.0
|
||||
*/
|
||||
@Tag(name = "word模版管理")
|
||||
@RestController("eoaWordTemplateController")
|
||||
@RequestMapping("/airag/word")
|
||||
@Slf4j
|
||||
public class EoaWordTemplateController extends JeecgController<EoaWordTemplate, IEoaWordTemplateService> {
|
||||
@Autowired
|
||||
private IEoaWordTemplateService eoaWordTemplateService;
|
||||
|
||||
@Autowired
|
||||
WordTplUtils wordTplUtils;
|
||||
|
||||
/**
|
||||
* 分页列表查询
|
||||
*
|
||||
* @param eoaWordTemplate
|
||||
* @param pageNo
|
||||
* @param pageSize
|
||||
* @param req
|
||||
* @return
|
||||
*/
|
||||
@Operation(summary = "word模版管理-分页列表查询")
|
||||
@GetMapping(value = "/list")
|
||||
public Result<IPage<EoaWordTemplate>> queryPageList(EoaWordTemplate eoaWordTemplate,
|
||||
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
|
||||
@RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize,
|
||||
HttpServletRequest req) {
|
||||
QueryWrapper<EoaWordTemplate> queryWrapper = QueryGenerator.initQueryWrapper(eoaWordTemplate, req.getParameterMap());
|
||||
Page<EoaWordTemplate> page = new Page<EoaWordTemplate>(pageNo, pageSize);
|
||||
IPage<EoaWordTemplate> pageList = eoaWordTemplateService.page(page, queryWrapper);
|
||||
return Result.OK(pageList);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加
|
||||
*
|
||||
* @param eoaWordTemplate
|
||||
* @return
|
||||
*/
|
||||
@AutoLog(value = "word模版管理-添加")
|
||||
@Operation(summary = "word模版管理-添加")
|
||||
// @RequiresPermissions("wordtpl:template:add")
|
||||
@PostMapping(value = "/add")
|
||||
public Result<String> add(@RequestBody EoaWordTemplate eoaWordTemplate) {
|
||||
AssertUtils.assertNotEmpty("参数异常", eoaWordTemplate);
|
||||
AssertUtils.assertNotEmpty("模版名称不能为空", eoaWordTemplate.getName());
|
||||
boolean isCodeExists = eoaWordTemplateService.exists(Wrappers.lambdaQuery(EoaWordTemplate.class).eq(EoaWordTemplate::getCode, eoaWordTemplate.getCode()));
|
||||
AssertUtils.assertFalse("模版编码已存在", isCodeExists);
|
||||
eoaWordTemplateService.save(eoaWordTemplate);
|
||||
return Result.OK("添加成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑
|
||||
*
|
||||
* @param eoaWordTemplate
|
||||
* @return
|
||||
*/
|
||||
@AutoLog(value = "word模版管理-编辑")
|
||||
@Operation(summary = "word模版管理-编辑")
|
||||
// @RequiresPermissions("wordtpl:template:edit")
|
||||
@RequestMapping(value = "/edit", method = {RequestMethod.PUT, RequestMethod.POST})
|
||||
public Result<String> edit(@RequestBody EoaWordTemplate eoaWordTemplate) {
|
||||
AssertUtils.assertNotEmpty("参数异常", eoaWordTemplate);
|
||||
AssertUtils.assertNotEmpty("模版名称不能为空", eoaWordTemplate.getName());
|
||||
// 避免编辑时修改编码
|
||||
eoaWordTemplate.setCode(null);
|
||||
eoaWordTemplateService.updateById(eoaWordTemplate);
|
||||
return Result.OK("编辑成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过id删除
|
||||
*
|
||||
* @param id
|
||||
* @return
|
||||
*/
|
||||
@AutoLog(value = "word模版管理-通过id删除")
|
||||
@Operation(summary = "word模版管理-通过id删除")
|
||||
// @RequiresPermissions("wordtpl:template:delete")
|
||||
@DeleteMapping(value = "/delete")
|
||||
public Result<String> delete(@RequestParam(name = "id", required = true) String id) {
|
||||
eoaWordTemplateService.removeById(id);
|
||||
return Result.OK("删除成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除
|
||||
*
|
||||
* @param ids
|
||||
* @return
|
||||
*/
|
||||
@AutoLog(value = "word模版管理-批量删除")
|
||||
@Operation(summary = "word模版管理-批量删除")
|
||||
// @RequiresPermissions("wordtpl:template:deleteBatch")
|
||||
@DeleteMapping(value = "/deleteBatch")
|
||||
public Result<String> deleteBatch(@RequestParam(name = "ids", required = true) String ids) {
|
||||
this.eoaWordTemplateService.removeByIds(Arrays.asList(ids.split(",")));
|
||||
return Result.OK("批量删除成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过id查询
|
||||
*
|
||||
* @param id
|
||||
* @return
|
||||
*/
|
||||
//@AutoLog(value = "word模版管理-通过id查询")
|
||||
@Operation(summary = "word模版管理-通过id查询")
|
||||
@GetMapping(value = "/queryById")
|
||||
public Result<EoaWordTemplate> queryById(@RequestParam(name = "id", required = true) String id) {
|
||||
EoaWordTemplate eoaWordTemplate = eoaWordTemplateService.getById(id);
|
||||
if (eoaWordTemplate == null) {
|
||||
return Result.error("未找到对应数据");
|
||||
}
|
||||
return Result.OK(eoaWordTemplate);
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载word模版
|
||||
* @param id
|
||||
* @param response
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/7/9 14:38
|
||||
*/
|
||||
@GetMapping(value = "/download")
|
||||
public void downloadTemplate(@RequestParam(name = "id", required = true) String id, HttpServletResponse response) {
|
||||
AssertUtils.assertNotEmpty("请先选择模版", id);
|
||||
EoaWordTemplate template = eoaWordTemplateService.getById(id);
|
||||
try (ByteArrayOutputStream wordTemplateOut = new ByteArrayOutputStream();
|
||||
BufferedOutputStream bos = new BufferedOutputStream(response.getOutputStream());) {
|
||||
wordTplUtils.generateWordTemplate(template, wordTemplateOut);
|
||||
String fileName = template.getName();
|
||||
String encodedFileName = URLEncoder.encode(fileName, "UTF-8");
|
||||
response.setContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document");
|
||||
response.addHeader("Content-Disposition", "attachment;filename=" + encodedFileName + ".docx");
|
||||
response.addHeader("filename", encodedFileName + ".docx");
|
||||
byte[] bytes = wordTemplateOut.toByteArray();
|
||||
response.setHeader("Content-Length", String.valueOf(bytes.length));
|
||||
bos.write(bytes);
|
||||
} catch (Exception e) {
|
||||
log.error(e.getMessage(), e);
|
||||
throw new JeecgBootException("下载word模版失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 解析word模版文件
|
||||
* @param file
|
||||
* @param id
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/7/9 14:38
|
||||
*/
|
||||
@PostMapping(value = "/parse/file")
|
||||
public Result<?> parseWOrdFile(@RequestParam("file") MultipartFile file) {
|
||||
try {
|
||||
InputStream inputStream = file.getInputStream();
|
||||
EoaWordTemplate eoaWordTemplate = wordTplUtils.parseWordFile(inputStream);
|
||||
log.info("解析的模版信息: {}", eoaWordTemplate);
|
||||
return Result.OK("解析成功", eoaWordTemplate);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("解析word模版失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成word文档
|
||||
*
|
||||
* @param wordTplGenDTO
|
||||
* @param response
|
||||
* @author chenrui
|
||||
* @date 2025/7/10 15:39
|
||||
*/
|
||||
@PostMapping(value = "/generate/word")
|
||||
public void generateWord(@RequestBody WordTplGenDTO wordTplGenDTO, HttpServletResponse response) {
|
||||
AssertUtils.assertNotEmpty("参数异常", wordTplGenDTO);
|
||||
EoaWordTemplate template ;
|
||||
if (oConvertUtils.isNotEmpty(wordTplGenDTO.getTemplateId())) {
|
||||
template = eoaWordTemplateService.getById(wordTplGenDTO.getTemplateId());
|
||||
}else{
|
||||
AssertUtils.assertNotEmpty("请先选择模版", wordTplGenDTO.getTemplateCode());
|
||||
template = eoaWordTemplateService.getOne(Wrappers.lambdaQuery(EoaWordTemplate.class)
|
||||
.eq(EoaWordTemplate::getCode, wordTplGenDTO.getTemplateCode()));
|
||||
}
|
||||
AssertUtils.assertNotEmpty("未找到对应的模版", template);
|
||||
|
||||
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
BufferedOutputStream bos = new BufferedOutputStream(response.getOutputStream());) {
|
||||
eoaWordTemplateService.generateWordFromTpl(wordTplGenDTO, outputStream);
|
||||
String fileName = template.getName();
|
||||
String encodedFileName = URLEncoder.encode(fileName, "UTF-8");
|
||||
response.setContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document");
|
||||
response.addHeader("Content-Disposition", "attachment;filename=" + encodedFileName + ".docx");
|
||||
response.addHeader("filename", encodedFileName + ".docx");
|
||||
byte[] bytes = outputStream.toByteArray();
|
||||
response.setHeader("Content-Length", String.valueOf(bytes.length));
|
||||
bos.write(bytes);
|
||||
} catch (Exception e) {
|
||||
log.error(e.getMessage(), e);
|
||||
throw new JeecgBootException("生成word文档失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
package org.jeecg.modules.airag.wordtpl.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 合并列DTO
|
||||
* @author chenrui
|
||||
* @date 2025/7/4 18:36
|
||||
*/
|
||||
@Data
|
||||
public class MergeColDTO {
|
||||
|
||||
/**
|
||||
* 合并列的行号
|
||||
*/
|
||||
private int row;
|
||||
|
||||
/**
|
||||
* 合并列的起始列号
|
||||
*/
|
||||
private int from;
|
||||
|
||||
/**
|
||||
* 合并列的结束列号
|
||||
*/
|
||||
private int to;
|
||||
}
|
||||
@ -0,0 +1,47 @@
|
||||
package org.jeecg.modules.airag.wordtpl.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* @ClassName: DocImageDto
|
||||
* @Description: word文档图片用实体类
|
||||
* @author chenrui
|
||||
* @date 2024-10-02 09:17:59
|
||||
*/
|
||||
@Data
|
||||
public class WordImageDTO {
|
||||
|
||||
/**
|
||||
* @Fields type : 类型
|
||||
* @author chenrui
|
||||
* @date 2024-09-29 08:53:27
|
||||
*/
|
||||
private String type = "image";
|
||||
/**
|
||||
* @Fields value : 内容
|
||||
* @author chenrui
|
||||
* @date 2024-09-24 10:20:12
|
||||
*/
|
||||
private String value = "";
|
||||
|
||||
/**
|
||||
* @Fields width : 图片宽度
|
||||
* @author chenrui
|
||||
* @date 2024-10-02 09:22:33
|
||||
*/
|
||||
private double width;
|
||||
|
||||
/**
|
||||
* @Fields height : 图片高度
|
||||
* @author chenrui
|
||||
* @date 2024-10-02 09:22:40
|
||||
*/
|
||||
private double height;
|
||||
|
||||
/**
|
||||
* @Fields rowFlex : 水平对齐方式,默认left
|
||||
* @author chenrui
|
||||
* @date 2024-09-27 09:12:18
|
||||
*/
|
||||
private String rowFlex = "left";
|
||||
}
|
||||
@ -0,0 +1,45 @@
|
||||
package org.jeecg.modules.airag.wordtpl.dto;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class WordTableCellDTO {
|
||||
|
||||
/**
|
||||
* @Fields colspan : 合并列数
|
||||
* @author chenrui
|
||||
* @date 2024-09-26 09:37:27
|
||||
*/
|
||||
private int colspan;
|
||||
|
||||
/**
|
||||
* @Fields rowspan : 合并行数
|
||||
* @author chenrui
|
||||
* @date 2024-09-26 09:38:22
|
||||
*/
|
||||
private int rowspan;
|
||||
|
||||
/**
|
||||
* @Fields value : 单元格数据
|
||||
* @author chenrui
|
||||
* @date 2024-09-26 09:42:14
|
||||
*/
|
||||
private List<Object> value = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* @Fields verticalAlign : 垂直对齐方式,默认top
|
||||
* @author chenrui
|
||||
* @date 2024-09-27 09:16:56
|
||||
*/
|
||||
private String verticalAlign = "top";
|
||||
|
||||
/**
|
||||
* @Fields backgroundColor : 背景颜色
|
||||
* @author chenrui
|
||||
* @date 2024-11-18 09:56:28
|
||||
*/
|
||||
private String backgroundColor;
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
package org.jeecg.modules.airag.wordtpl.dto;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class WordTableDTO {
|
||||
|
||||
private String value = "";
|
||||
|
||||
private String type = "table";
|
||||
|
||||
private List<WordTableRowDTO> trList;
|
||||
|
||||
private int width;
|
||||
|
||||
private int height;
|
||||
|
||||
private List<JSONObject> colgroup = new ArrayList<>();
|
||||
}
|
||||
@ -0,0 +1,30 @@
|
||||
package org.jeecg.modules.airag.wordtpl.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class WordTableRowDTO {
|
||||
|
||||
/**
|
||||
* @Fields height : 行高
|
||||
* @author chenrui
|
||||
* @date 2024-09-26 09:45:30
|
||||
*/
|
||||
private Integer height;
|
||||
|
||||
/**
|
||||
* @Fields minHeight : 行最小高度
|
||||
* @author chenrui
|
||||
* @date 2024-09-26 09:47:28
|
||||
*/
|
||||
private int minHeight = 42;
|
||||
|
||||
/**
|
||||
* @Fields tdList : 行数据
|
||||
* @author chenrui
|
||||
* @date 2024-09-26 09:46:02
|
||||
*/
|
||||
private List<WordTableCellDTO> tdList;
|
||||
}
|
||||
@ -0,0 +1,94 @@
|
||||
package org.jeecg.modules.airag.wordtpl.dto;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* @author chenrui
|
||||
* @ClassName: DocTextDto
|
||||
* @Description: word文本实体类
|
||||
* @date 2024-09-24 10:19:57
|
||||
*/
|
||||
@Data
|
||||
public class WordTextDTO {
|
||||
|
||||
/**
|
||||
* @Fields type : 类型
|
||||
* @author chenrui
|
||||
* @date 2024-09-29 08:53:27
|
||||
*/
|
||||
private String type;
|
||||
/**
|
||||
* @Fields value : 内容
|
||||
* @author chenrui
|
||||
* @date 2024-09-24 10:20:12
|
||||
*/
|
||||
private String value = "";
|
||||
|
||||
/**
|
||||
* @Fields bold : 是否加粗 默认false
|
||||
* @author chenrui
|
||||
* @date 2024-09-24 10:20:33
|
||||
*/
|
||||
private boolean bold = false;
|
||||
|
||||
/**
|
||||
* @Fields color : 字体颜色
|
||||
* @author chenrui
|
||||
* @date 2024-09-24 10:21:08
|
||||
*/
|
||||
private String color;
|
||||
|
||||
/**
|
||||
* @Fields italic : 是否斜体 默认false
|
||||
* @author chenrui
|
||||
* @date 2024-09-24 10:21:25
|
||||
*/
|
||||
private boolean italic = false;
|
||||
|
||||
/**
|
||||
* @Fields underline : 是否下划线 默认false
|
||||
* @author chenrui
|
||||
* @date 2024-09-24 10:21:47
|
||||
*/
|
||||
private boolean underline = false;
|
||||
|
||||
/**
|
||||
* @Fields strikeout : 删除线 默认false
|
||||
* @author chenrui
|
||||
* @date 2024-09-24 10:22:06
|
||||
*/
|
||||
private boolean strikeout = false;
|
||||
|
||||
/**
|
||||
* @Fields size : 字号大小
|
||||
* @author chenrui
|
||||
* @date 2024-09-24 10:44:42
|
||||
*/
|
||||
private int size;
|
||||
|
||||
/**
|
||||
* @Fields font : 字体,默认微软雅黑
|
||||
* @author chenrui
|
||||
* @date 2024-09-24 10:45:31
|
||||
*/
|
||||
private String font = "微软雅黑";
|
||||
|
||||
/**
|
||||
* @Fields highlight : 高亮颜色
|
||||
* @author chenrui
|
||||
* @date 2024-09-25 11:20:23
|
||||
*/
|
||||
private String highlight;
|
||||
|
||||
/**
|
||||
* @Fields rowFlex : 水平对齐方式,默认left
|
||||
* @author chenrui
|
||||
* @date 2024-09-27 09:12:18
|
||||
*/
|
||||
private String rowFlex = "left";
|
||||
|
||||
private List<Object> dashArray = new ArrayList<>();
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
package org.jeecg.modules.airag.wordtpl.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* word模版生成入参
|
||||
* @author chenrui
|
||||
* @date 2025/7/10 14:38
|
||||
*/
|
||||
@Data
|
||||
public class WordTplGenDTO {
|
||||
|
||||
/**
|
||||
* 模版id
|
||||
*/
|
||||
String templateId;
|
||||
|
||||
|
||||
/**
|
||||
* 模版code
|
||||
*/
|
||||
String templateCode;
|
||||
|
||||
/**
|
||||
* 数据
|
||||
*/
|
||||
Map<String,Object> data;
|
||||
|
||||
}
|
||||
@ -0,0 +1,126 @@
|
||||
package org.jeecg.modules.airag.wordtpl.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
import org.jeecgframework.poi.excel.annotation.Excel;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* @Description: word模版管理
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-07-04
|
||||
* @Version: V1.0
|
||||
*/
|
||||
@Data
|
||||
@TableName("aigc_word_template")
|
||||
@Accessors(chain = true)
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@Schema(description = "word模版管理")
|
||||
public class EoaWordTemplate implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 主键
|
||||
*/
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
@Schema(description = "主键")
|
||||
private String id;
|
||||
/**
|
||||
* 创建人
|
||||
*/
|
||||
@Schema(description = "创建人")
|
||||
private String createBy;
|
||||
/**
|
||||
* 创建日期
|
||||
*/
|
||||
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@Schema(description = "创建日期")
|
||||
private Date createTime;
|
||||
/**
|
||||
* 更新人
|
||||
*/
|
||||
@Schema(description = "更新人")
|
||||
private String updateBy;
|
||||
/**
|
||||
* 更新日期
|
||||
*/
|
||||
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@Schema(description = "更新日期")
|
||||
private Date updateTime;
|
||||
/**
|
||||
* 所属部门
|
||||
*/
|
||||
@Schema(description = "所属部门")
|
||||
private String sysOrgCode;
|
||||
/**
|
||||
* 模版名称
|
||||
*/
|
||||
@Excel(name = "模版名称", width = 15)
|
||||
@Schema(description = "模版名称")
|
||||
private String name;
|
||||
/**
|
||||
* 模版编码
|
||||
*/
|
||||
@Excel(name = "模版编码", width = 15)
|
||||
@Schema(description = "模版编码")
|
||||
private String code;
|
||||
/**
|
||||
* 页眉
|
||||
*/
|
||||
@Excel(name = "页眉", width = 15)
|
||||
@Schema(description = "页眉")
|
||||
private String header;
|
||||
/**
|
||||
* 页脚
|
||||
*/
|
||||
@Excel(name = "页脚", width = 15)
|
||||
@Schema(description = "页脚")
|
||||
private String footer;
|
||||
/**
|
||||
* 主体内容
|
||||
*/
|
||||
@Excel(name = "主体内容", width = 15)
|
||||
@Schema(description = "主体内容")
|
||||
private String main;
|
||||
/**
|
||||
* 页边距
|
||||
*/
|
||||
@Excel(name = "页边距", width = 15)
|
||||
@Schema(description = "页边距")
|
||||
private String margins;
|
||||
/**
|
||||
* 宽度
|
||||
*/
|
||||
@Excel(name = "宽度", width = 15)
|
||||
@Schema(description = "宽度")
|
||||
private Integer width;
|
||||
/**
|
||||
* 高度
|
||||
*/
|
||||
@Excel(name = "高度", width = 15)
|
||||
@Schema(description = "高度")
|
||||
private Integer height;
|
||||
/**
|
||||
* 纸张方向 vertical纵向 horizontal横向
|
||||
*/
|
||||
@Excel(name = "纸张方向 vertical纵向 horizontal横向", width = 15)
|
||||
@Schema(description = "纸张方向 vertical纵向 horizontal横向")
|
||||
private String paperDirection;
|
||||
/**
|
||||
* 水印
|
||||
*/
|
||||
@Excel(name = "水印", width = 15)
|
||||
@Schema(description = "水印")
|
||||
private String watermark;
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
package org.jeecg.modules.airag.wordtpl.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.jeecg.modules.airag.wordtpl.entity.EoaWordTemplate;
|
||||
|
||||
/**
|
||||
* @Description: word模版管理
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-07-04
|
||||
* @Version: V1.0
|
||||
*/
|
||||
public interface EoaWordTemplateMapper extends BaseMapper<EoaWordTemplate> {
|
||||
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="org.jeecg.modules.airag.wordtpl.mapper.EoaWordTemplateMapper">
|
||||
|
||||
</mapper>
|
||||
@ -0,0 +1,26 @@
|
||||
package org.jeecg.modules.airag.wordtpl.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import org.jeecg.modules.airag.wordtpl.dto.WordTplGenDTO;
|
||||
import org.jeecg.modules.airag.wordtpl.entity.EoaWordTemplate;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
|
||||
/**
|
||||
* @Description: word模版管理
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-07-04
|
||||
* @Version: V1.0
|
||||
*/
|
||||
public interface IEoaWordTemplateService extends IService<EoaWordTemplate> {
|
||||
|
||||
/**
|
||||
* 通过模版生成word文档
|
||||
*
|
||||
* @param wordTplGenDTO
|
||||
* @return
|
||||
* @author chenrui
|
||||
* @date 2025/7/10 14:40
|
||||
*/
|
||||
void generateWordFromTpl(WordTplGenDTO wordTplGenDTO, ByteArrayOutputStream wordOutputStream);
|
||||
}
|
||||
@ -0,0 +1,85 @@
|
||||
package org.jeecg.modules.airag.wordtpl.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.deepoove.poi.XWPFTemplate;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.common.constant.DataBaseConstant;
|
||||
import org.jeecg.common.exception.JeecgBootException;
|
||||
import org.jeecg.common.system.util.JwtUtil;
|
||||
import org.jeecg.common.util.AssertUtils;
|
||||
import org.jeecg.modules.airag.wordtpl.dto.WordTplGenDTO;
|
||||
import org.jeecg.modules.airag.wordtpl.entity.EoaWordTemplate;
|
||||
import org.jeecg.modules.airag.wordtpl.mapper.EoaWordTemplateMapper;
|
||||
import org.jeecg.modules.airag.wordtpl.service.IEoaWordTemplateService;
|
||||
import org.jeecg.modules.airag.wordtpl.utils.WordTplUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @Description: word模版管理
|
||||
* @Author: jeecg-boot
|
||||
* @Date: 2025-07-04
|
||||
* @Version: V1.0
|
||||
*/
|
||||
@Slf4j
|
||||
@Service("eoaWordTemplateService")
|
||||
public class EoaWordTemplateServiceImpl extends ServiceImpl<EoaWordTemplateMapper, EoaWordTemplate> implements IEoaWordTemplateService {
|
||||
|
||||
/**
|
||||
* 内置的系统变量键列表
|
||||
*/
|
||||
private static final String[] SYSTEM_KEYS = {
|
||||
DataBaseConstant.SYS_ORG_CODE, DataBaseConstant.SYS_ORG_CODE_TABLE, DataBaseConstant.SYS_MULTI_ORG_CODE,
|
||||
DataBaseConstant.SYS_MULTI_ORG_CODE_TABLE, DataBaseConstant.SYS_ORG_ID, DataBaseConstant.SYS_ORG_ID_TABLE,
|
||||
DataBaseConstant.SYS_ROLE_CODE, DataBaseConstant.SYS_ROLE_CODE_TABLE, DataBaseConstant.SYS_USER_CODE,
|
||||
DataBaseConstant.SYS_USER_CODE_TABLE, DataBaseConstant.SYS_USER_ID, DataBaseConstant.SYS_USER_ID_TABLE,
|
||||
DataBaseConstant.SYS_USER_NAME, DataBaseConstant.SYS_USER_NAME_TABLE, DataBaseConstant.SYS_DATE,
|
||||
DataBaseConstant.SYS_DATE_TABLE, DataBaseConstant.SYS_TIME, DataBaseConstant.SYS_TIME_TABLE,
|
||||
DataBaseConstant.SYS_BASE_PATH
|
||||
};
|
||||
|
||||
@Autowired
|
||||
WordTplUtils wordTplUtils;
|
||||
|
||||
@Override
|
||||
public void generateWordFromTpl(WordTplGenDTO wordTplGenDTO, ByteArrayOutputStream wordOutputStream) {
|
||||
AssertUtils.assertNotEmpty("参数异常", wordTplGenDTO);
|
||||
AssertUtils.assertNotEmpty("模版ID不能为空", wordTplGenDTO.getTemplateId());
|
||||
String templateId = wordTplGenDTO.getTemplateId();
|
||||
// 生成word模版 date:2025/7/10
|
||||
EoaWordTemplate template = getById(templateId);
|
||||
ByteArrayOutputStream wordTemplateOut = new ByteArrayOutputStream();
|
||||
wordTplUtils.generateWordTemplate(template, wordTemplateOut);
|
||||
//根据word模版和数据生成word文件
|
||||
Map<String, Object> data = wordTplGenDTO.getData();
|
||||
mergeSystemVarsToData(data);
|
||||
try {
|
||||
XWPFTemplate.compile(new ByteArrayInputStream(wordTemplateOut.toByteArray())).render(data).write(wordOutputStream);
|
||||
}catch (Exception e){
|
||||
log.error(e.getMessage(), e);
|
||||
throw new JeecgBootException("生成word文档失败,请检查模版和数据是否正确");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 将系统变量合并到数据中
|
||||
*
|
||||
* @param data
|
||||
* @author chenrui
|
||||
* @date 2025/7/3 17:43
|
||||
*/
|
||||
private static void mergeSystemVarsToData(Map<String, Object> data) {
|
||||
for (String key : SYSTEM_KEYS) {
|
||||
if (!data.containsKey(key)) {
|
||||
String value = JwtUtil.getUserSystemData(key, null);
|
||||
if (value != null) {
|
||||
data.put(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,326 @@
|
||||
package org.jeecg.modules.demo.mcp;
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
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.config.shiro.IgnoreAuth;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* MCP Server 示例 (Model Context Protocol)
|
||||
*
|
||||
* 这是一个符合 MCP 协议的服务端实现,支持 SSE 传输。
|
||||
*
|
||||
* 连接地址: http://你的服务器:8080/jeecg-boot/demo/mcp/sse
|
||||
*
|
||||
* 提供的工具:
|
||||
* - hello: 打招呼工具
|
||||
* - get_time: 获取当前时间
|
||||
* - calculate: 简单计算器
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/demo/mcp")
|
||||
@Tag(name = "MCP Server 示例")
|
||||
public class McpDemoController {
|
||||
|
||||
// 存储 SSE 连接
|
||||
private final Map<String, SseEmitter> sseEmitters = new ConcurrentHashMap<>();
|
||||
|
||||
// 定义工具列表
|
||||
private final List<Map<String, Object>> TOOLS = List.of(
|
||||
Map.of(
|
||||
"name", "hello",
|
||||
"description", "打招呼工具,返回问候语",
|
||||
"inputSchema", Map.of(
|
||||
"type", "object",
|
||||
"properties", Map.of(
|
||||
"name", Map.of("type", "string", "description", "你的名字")
|
||||
),
|
||||
"required", List.of("name")
|
||||
)
|
||||
),
|
||||
Map.of(
|
||||
"name", "get_time",
|
||||
"description", "获取当前服务器时间",
|
||||
"inputSchema", Map.of(
|
||||
"type", "object",
|
||||
"properties", Map.of()
|
||||
)
|
||||
),
|
||||
Map.of(
|
||||
"name", "calculate",
|
||||
"description", "简单计算器,支持加减乘除",
|
||||
"inputSchema", Map.of(
|
||||
"type", "object",
|
||||
"properties", Map.of(
|
||||
"a", Map.of("type", "number", "description", "第一个数"),
|
||||
"b", Map.of("type", "number", "description", "第二个数"),
|
||||
"operator", Map.of("type", "string", "description", "运算符: +, -, *, /")
|
||||
),
|
||||
"required", List.of("a", "b", "operator")
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
/**
|
||||
* MCP SSE 端点 - 客户端通过此接口建立 SSE 连接
|
||||
*/
|
||||
@IgnoreAuth
|
||||
@GetMapping(value = "/sse", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
|
||||
@Operation(summary = "MCP SSE 连接端点")
|
||||
public SseEmitter sse(HttpServletRequest request) {
|
||||
String clientId = UUID.randomUUID().toString();
|
||||
log.info("[MCP Server] 新客户端 SSE 连接: {}", clientId);
|
||||
|
||||
SseEmitter emitter = new SseEmitter(0L); // 不超时
|
||||
sseEmitters.put(clientId, emitter);
|
||||
|
||||
emitter.onCompletion(() -> {
|
||||
log.info("[MCP Server] 客户端断开: {}", clientId);
|
||||
sseEmitters.remove(clientId);
|
||||
});
|
||||
emitter.onTimeout(() -> {
|
||||
log.info("[MCP Server] 客户端超时: {}", clientId);
|
||||
sseEmitters.remove(clientId);
|
||||
});
|
||||
emitter.onError(e -> {
|
||||
log.error("[MCP Server] SSE 错误: {}", e.getMessage());
|
||||
sseEmitters.remove(clientId);
|
||||
});
|
||||
|
||||
// 发送 endpoint 事件,告诉客户端消息端点地址
|
||||
try {
|
||||
String baseUrl = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort();
|
||||
String messageEndpoint = baseUrl + request.getContextPath() + "/demo/mcp/message?sessionId=" + clientId;
|
||||
emitter.send(SseEmitter.event()
|
||||
.name("endpoint")
|
||||
.data(messageEndpoint));
|
||||
log.info("[MCP Server] 发送 endpoint 事件: {}", messageEndpoint);
|
||||
} catch (IOException e) {
|
||||
log.error("[MCP Server] 发送 endpoint 事件失败", e);
|
||||
}
|
||||
|
||||
return emitter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Streamable HTTP 端点 - 同时支持 POST 到 /sse 的 JSON-RPC 请求
|
||||
* Cursor 客户端会先尝试这种方式
|
||||
*/
|
||||
@IgnoreAuth
|
||||
@PostMapping(value = "/sse")
|
||||
@Operation(summary = "MCP Streamable HTTP 端点")
|
||||
public void ssePost(@RequestBody String body, HttpServletResponse response) throws IOException {
|
||||
log.info("[MCP Server] Streamable HTTP 请求: {}", body);
|
||||
handleJsonRpcRequest(body, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* MCP 消息处理端点 - 处理 JSON-RPC 请求
|
||||
* 直接写入原始 JSON-RPC 响应,避免框架包装
|
||||
*/
|
||||
@IgnoreAuth
|
||||
@PostMapping(value = "/message")
|
||||
@Operation(summary = "MCP 消息处理")
|
||||
public void handleMessage(@RequestParam(required = false) String sessionId,
|
||||
@RequestBody String body,
|
||||
HttpServletResponse response) throws IOException {
|
||||
log.info("[MCP Server] 收到消息, sessionId: {}, body: {}", sessionId, body);
|
||||
handleJsonRpcRequest(body, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 JSON-RPC 请求的公共方法
|
||||
*/
|
||||
private void handleJsonRpcRequest(String body, HttpServletResponse response) throws IOException {
|
||||
response.setContentType("application/json;charset=UTF-8");
|
||||
PrintWriter writer = response.getWriter();
|
||||
|
||||
try {
|
||||
JSONObject request = JSON.parseObject(body);
|
||||
String method = request.getString("method");
|
||||
Object id = request.get("id");
|
||||
JSONObject params = request.getJSONObject("params");
|
||||
|
||||
// 通知类消息(没有id)不需要响应
|
||||
if (id == null) {
|
||||
log.info("[MCP Server] 收到通知: {}", method);
|
||||
writer.write("{}");
|
||||
writer.flush();
|
||||
return;
|
||||
}
|
||||
|
||||
// 构建 JSON-RPC 2.0 响应
|
||||
Map<String, Object> jsonRpcResponse = new LinkedHashMap<>();
|
||||
jsonRpcResponse.put("jsonrpc", "2.0");
|
||||
jsonRpcResponse.put("id", id);
|
||||
|
||||
try {
|
||||
Object result = switch (method) {
|
||||
case "initialize" -> handleInitialize(params);
|
||||
case "initialized", "notifications/initialized" -> handleInitialized();
|
||||
case "tools/list" -> handleToolsList();
|
||||
case "tools/call" -> handleToolsCall(params);
|
||||
case "ping" -> handlePing();
|
||||
case "notifications/cancelled" -> handleCancelled(params);
|
||||
default -> {
|
||||
if (method != null && method.startsWith("notifications/")) {
|
||||
log.info("[MCP Server] 忽略未知通知: {}", method);
|
||||
yield Map.of();
|
||||
}
|
||||
throw new RuntimeException("未知方法: " + method);
|
||||
}
|
||||
};
|
||||
jsonRpcResponse.put("result", result);
|
||||
} catch (Exception e) {
|
||||
log.error("[MCP Server] 处理请求失败", e);
|
||||
jsonRpcResponse.put("error", Map.of(
|
||||
"code", -32603,
|
||||
"message", e.getMessage()
|
||||
));
|
||||
}
|
||||
|
||||
String responseJson = JSON.toJSONString(jsonRpcResponse);
|
||||
log.info("[MCP Server] 返回: {}", responseJson);
|
||||
writer.write(responseJson);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("[MCP Server] 解析请求失败", e);
|
||||
writer.write("{\"jsonrpc\":\"2.0\",\"id\":null,\"error\":{\"code\":-32700,\"message\":\"Parse error\"}}");
|
||||
}
|
||||
|
||||
writer.flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 initialize 请求
|
||||
*/
|
||||
private Map<String, Object> handleInitialize(JSONObject params) {
|
||||
log.info("[MCP Server] 初始化请求: {}", params);
|
||||
return Map.of(
|
||||
"protocolVersion", "2024-11-05",
|
||||
"capabilities", Map.of(
|
||||
"tools", Map.of()
|
||||
),
|
||||
"serverInfo", Map.of(
|
||||
"name", "jeecg-mcp-demo",
|
||||
"version", "1.0.0"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 initialized 通知
|
||||
*/
|
||||
private Map<String, Object> handleInitialized() {
|
||||
log.info("[MCP Server] 客户端已初始化完成");
|
||||
return Map.of();
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 ping 请求
|
||||
*/
|
||||
private Map<String, Object> handlePing() {
|
||||
log.info("[MCP Server] Ping");
|
||||
return Map.of();
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 notifications/cancelled 通知
|
||||
*/
|
||||
private Map<String, Object> handleCancelled(JSONObject params) {
|
||||
log.info("[MCP Server] 请求被取消: {}", params);
|
||||
return Map.of();
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 tools/list 请求
|
||||
*/
|
||||
private Map<String, Object> handleToolsList() {
|
||||
log.info("[MCP Server] 获取工具列表");
|
||||
return Map.of("tools", TOOLS);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 tools/call 请求
|
||||
*/
|
||||
private Map<String, Object> handleToolsCall(JSONObject params) {
|
||||
String toolName = params.getString("name");
|
||||
JSONObject arguments = params.getJSONObject("arguments");
|
||||
if (arguments == null) {
|
||||
arguments = new JSONObject();
|
||||
}
|
||||
log.info("[MCP Server] 调用工具: {}, 参数: {}", toolName, arguments);
|
||||
|
||||
String result = switch (toolName) {
|
||||
case "hello" -> {
|
||||
String name = arguments.getString("name");
|
||||
if (name == null || name.isEmpty()) {
|
||||
name = "World";
|
||||
}
|
||||
yield "你好, " + name + "! 欢迎使用 JeecgBoot MCP 服务!";
|
||||
}
|
||||
case "get_time" -> {
|
||||
yield "当前时间: " + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
|
||||
}
|
||||
case "calculate" -> {
|
||||
double a = arguments.getDoubleValue("a");
|
||||
double b = arguments.getDoubleValue("b");
|
||||
String op = arguments.getString("operator");
|
||||
if (op == null) op = "+";
|
||||
double res = switch (op) {
|
||||
case "+" -> a + b;
|
||||
case "-" -> a - b;
|
||||
case "*" -> a * b;
|
||||
case "/" -> b != 0 ? a / b : Double.NaN;
|
||||
default -> throw new RuntimeException("不支持的运算符: " + op);
|
||||
};
|
||||
yield String.format("%.2f %s %.2f = %.2f", a, op, b, res);
|
||||
}
|
||||
default -> throw new RuntimeException("未知工具: " + toolName);
|
||||
};
|
||||
|
||||
return Map.of(
|
||||
"content", List.of(Map.of(
|
||||
"type", "text",
|
||||
"text", result
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用说明页面
|
||||
*/
|
||||
@IgnoreAuth
|
||||
@GetMapping("/info")
|
||||
@Operation(summary = "MCP Server 使用说明")
|
||||
public Map<String, Object> info(HttpServletRequest request) {
|
||||
log.info("[MCP Server] Hello 接口被访问");
|
||||
String baseUrl = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + request.getContextPath();
|
||||
return Map.of(
|
||||
"success", true,
|
||||
"message", "JeecgBoot MCP Server 示例",
|
||||
"sseUrl", baseUrl + "/demo/mcp/sse",
|
||||
"tools", List.of(
|
||||
Map.of("name", "hello", "description", "打招呼工具", "params", "name: 你的名字"),
|
||||
Map.of("name", "get_time", "description", "获取当前时间", "params", "无"),
|
||||
Map.of("name", "calculate", "description", "简单计算器", "params", "a, b, operator(+,-,*,/)")
|
||||
),
|
||||
"usage", "在 Cursor/Claude 等 MCP 客户端中配置 SSE URL: " + baseUrl + "/demo/mcp/sse",
|
||||
"example", "请调用 hello 工具,参数 name 填 \"测试用户\""
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -72,10 +72,10 @@ public class JeecgDemoController extends JeecgController<JeecgDemo, IJeecgDemoSe
|
||||
Page<JeecgDemo> page = new Page<JeecgDemo>(pageNo, pageSize);
|
||||
|
||||
IPage<JeecgDemo> pageList = jeecgDemoService.page(page, queryWrapper);
|
||||
log.info("查询当前页:" + pageList.getCurrent());
|
||||
log.info("查询当前页数量:" + pageList.getSize());
|
||||
log.info("查询结果数量:" + pageList.getRecords().size());
|
||||
log.info("数据总数:" + pageList.getTotal());
|
||||
log.debug("查询当前页:" + pageList.getCurrent());
|
||||
log.debug("查询当前页数量:" + pageList.getSize());
|
||||
log.debug("查询结果数量:" + pageList.getRecords().size());
|
||||
log.debug("数据总数:" + pageList.getTotal());
|
||||
return Result.OK(pageList);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user