mirror of
https://gitcode.com/ageerle/ruoyi-ai.git
synced 2026-04-10 02:07:33 +00:00
Compare commits
33 Commits
13800dc389
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf7b5eac72 | ||
|
|
d602b805bd | ||
|
|
9cf18904bb | ||
|
|
2f39fa0f53 | ||
|
|
d2005cfa48 | ||
|
|
4e38f853f3 | ||
|
|
3cfb185dde | ||
|
|
ef99c540bb | ||
|
|
3071bfd0f9 | ||
|
|
7bb938c145 | ||
|
|
75b21d3633 | ||
|
|
7ed9d8def4 | ||
|
|
63ed7ddb02 | ||
|
|
11696a016d | ||
|
|
1a10104751 | ||
|
|
f95cb17933 | ||
|
|
0687b49542 | ||
|
|
27ad00ac3a | ||
|
|
c84d6247b0 | ||
|
|
f582f38570 | ||
|
|
a8bd4b47a0 | ||
|
|
a59ddf6070 | ||
|
|
797ecbb054 | ||
|
|
b6b78afea9 | ||
|
|
02240f3fd0 | ||
|
|
a916f14efc | ||
|
|
523628ade6 | ||
|
|
2259a2f717 | ||
|
|
8df37274da | ||
|
|
393057ab24 | ||
|
|
ee8c882b6f | ||
|
|
69ec2a33a4 | ||
|
|
1cd8ae1cd9 |
53
README.md
53
README.md
@@ -19,7 +19,7 @@
|
||||
|
||||
### 企业级AI助手平台
|
||||
|
||||
*开箱即用的全栈AI平台,支持多智能体协同、Supervisor模式编排、多种决策模型,提供先进的RAG技术和可视化流程编排能力*
|
||||
*开箱即用的全栈AI平台,支持多智能体协同、Supervisor模式编排、多种决策模式、RAG技术和流程编排能力*
|
||||
|
||||
**[English](README_EN.md)** | **[📖 使用文档](https://doc.pandarobot.chat)** |
|
||||
**[🚀 在线体验](https://web.pandarobot.chat)** | **[🐛 问题反馈](https://github.com/ageerle/ruoyi-ai/issues)** | **[💡 功能建议](https://github.com/ageerle/ruoyi-ai/issues)**
|
||||
@@ -27,17 +27,15 @@
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
## ✨ 核心亮点
|
||||
|
||||
| 模块 | 现有能力 | 扩展方向 |
|
||||
|:----------:|---|------------------------|
|
||||
| **模型管理** | 多模型接入(OpenAI/DeepSeek/通义/智谱)、多模态理解、Coze/DIFY/FastGPT平台集成 | 自动模式、容错机制、计费管理 |
|
||||
| **知识管理** | 本地RAG + 向量库(Milvus/Weaviate) + 文档解析 | 多模态、知识出处、知识图谱、重排序 |
|
||||
| **工具管理** | Mcp协议集成、Skills能力 + 可扩展工具生态 | 工具插件市场、 |
|
||||
| **流程编排** | 可视化工作流设计器、节点拖拽编排、SSE流式执行,目前已经支持模型调用,邮件发送,人工审核等节点 | 更多节点类型 |
|
||||
| **多智能体** | 基于Langchain4j的Agent框架、Supervisor模式编排,支持多种决策模型 | 智能体可配置 |
|
||||
| 模块 | 现有能力
|
||||
|:----------:|---
|
||||
| **模型管理** | 多模型接入(OpenAI/DeepSeek/通义/智谱)、多模态理解、Coze/DIFY/FastGPT平台集成
|
||||
| **知识管理** | 本地RAG + 向量库(Milvus/Weaviate/Qdrant) + 文档解析
|
||||
| **工具管理** | Mcp协议集成、Skills能力 + 可扩展工具生态
|
||||
| **流程编排** | 可视化工作流设计器、节点拖拽编排、SSE流式执行,目前已经支持模型调用,邮件发送,人工审核等节点
|
||||
| **多智能体** | 基于Langchain4j的Agent框架、Supervisor模式编排,支持多种决策模型
|
||||
|
||||
## 🚀 快速体验
|
||||
|
||||
@@ -64,12 +62,16 @@
|
||||
## 🛠️ 技术架构
|
||||
|
||||
### 核心框架
|
||||
- **后端架构**:Spring Boot 4.0 + Spring ai 2.0 + Langchain4j
|
||||
- **数据存储**:MySQL 8.0 + Redis + 向量数据库(Milvus/Weaviate)
|
||||
- **后端架构**:Spring Boot 3.5.8 + Langchain4j
|
||||
- **数据存储**:MySQL 8.0 + Redis + 向量数据库(Milvus/Weaviate/Qdrant)
|
||||
- **前端技术**:Vue 3 + Vben Admin + element-plus-x
|
||||
- **安全认证**:Sa-Token + JWT 双重保障
|
||||
|
||||
|
||||
- **文档处理**:PDF、Word、Excel 解析,图像智能分析
|
||||
- **实时通信**:WebSocket 实时通信,SSE 流式响应
|
||||
- **系统监控**:完善的日志体系、性能监控、服务健康检查
|
||||
|
||||
## 🐳 Docker 部署
|
||||
|
||||
本项目提供两种 Docker 部署方式:
|
||||
@@ -220,9 +222,6 @@ docker-compose -f docker-compose-all.yaml restart [服务名]
|
||||
算力和模型 API 服务
|
||||
- [优云智算](https://www.compshare.cn/?ytag=GPU_YY-gh_ruoyi) - 万卡RTX40系GPU+海内外主流模型API服务,秒级响应,按量计费,新客免费用。
|
||||
|
||||
## 优秀开源项目及社区推荐
|
||||
- [imaiwork](https://gitee.com/tsinghua-open/imaiwork) - AI手机开源版,AI获客手机项目,基于无障碍模式,RPA,比豆包AI手机更强大。
|
||||
|
||||
## 💬 社区交流
|
||||
|
||||
<div align="center">
|
||||
@@ -246,30 +245,6 @@ docker-compose -f docker-compose-all.yaml restart [服务名]
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
|
||||
## 📺 视频教程
|
||||
|
||||
<div align="center">
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<img src="docs/image/dy.png" alt="微信二维码" width="200" height="200"><br>
|
||||
<strong>打开抖音扫一扫</strong><br>
|
||||
<em>获取免费视频教程</em>
|
||||
</td>
|
||||
<td align="center">
|
||||
<img src="docs/image/bibi.png" alt="QQ群二维码" width="200" height="200"><br>
|
||||
<strong>打开B站扫一扫</strong><br>
|
||||
<em>获取免费视频教程</em>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
|
||||
**[⭐ 点个Star支持一下](https://github.com/ageerle/ruoyi-ai)** • **[ Fork 开始贡献](https://github.com/ageerle/ruoyi-ai/fork)** • **[📚 English](README_EN.md)** • **[📖 查看完整文档](https://doc.pandarobot.chat)**
|
||||
|
||||
141
README_EN.md
141
README_EN.md
@@ -32,14 +32,13 @@
|
||||
|
||||
## ✨ Core Features
|
||||
|
||||
| Module | Current Capabilities | Extension Direction |
|
||||
|:---:|---|---|
|
||||
| **Model Management** | Multi-model integration (OpenAI/DeepSeek/Tongyi/Zhipu), multi-modal understanding, Coze/DIFY/FastGPT platform integration | Auto mode, fault tolerance |
|
||||
| **Knowledge Base** | Local RAG + Vector DB (Milvus/Weaviate) + Knowledge Graph + Document parsing + Reranking | Audio/video parsing, knowledge source |
|
||||
| **Tool Management** | MCP protocol integration, Skills capability + Extensible tool ecosystem | Tool plugin marketplace, toolAgent auto-loading |
|
||||
| **Workflow Orchestration** | Visual workflow designer, drag-and-drop node orchestration, SSE streaming execution, currently supports model (with RAG) calls, email sending, manual review nodes | More node types |
|
||||
| **Multi-Agent** | Agent framework based on Langchain4j, Supervisor mode orchestration, supports multiple decision models | Configurable agents |
|
||||
| **AI Coding** | Intelligent code analysis, project scaffolding generation, Copilot assistant | Code generation optimization |
|
||||
| Module | Current Capabilities |
|
||||
|:---:|---|
|
||||
| **Model Management** | Multi-model integration (OpenAI/DeepSeek/Tongyi/Zhipu), multi-modal understanding, Coze/DIFY/FastGPT platform integration |
|
||||
| **Knowledge Base** | Local RAG + Vector DB (Milvus/Weaviate/Qdrant) + Document parsing |
|
||||
| **Tool Management** | MCP protocol integration, Skills capability + Extensible tool ecosystem |
|
||||
| **Workflow Orchestration** | Visual workflow designer, drag-and-drop node orchestration, SSE streaming execution, currently supports model calls, email sending, manual review nodes |
|
||||
| **Multi-Agent** | Agent framework based on Langchain4j, Supervisor mode orchestration, supports multiple decision models |
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
@@ -59,19 +58,134 @@
|
||||
| 🛠️ Admin Panel | [ruoyi-admin](https://github.com/ageerle/ruoyi-admin) | [ruoyi-admin](https://gitee.com/ageerle/ruoyi-admin) | [ruoyi-admin](https://gitcode.com/ageerle/ruoyi-admin) |
|
||||
|
||||
### Partner Projects
|
||||
| Project Name | GitHub Repository | Gitee Repository
|
||||
| Project Name | GitHub Repository | Gitee Repository |
|
||||
|----------------|-------------------------------------------------------|------------------------------------------------------|
|
||||
| element-plus-x | [element-plus-x](https://github.com/element-plus-x/Element-Plus-X) | [element-plus-x](https://gitee.com/he-jiayue/element-plus-x) |
|
||||
|
||||
## 🛠️ Technical Architecture
|
||||
|
||||
### Core Framework
|
||||
- **Backend**: Spring Boot 4.0 + Spring AI 2.0 + Langchain4j
|
||||
- **Data Storage**: MySQL 8.0 + Redis + Vector Databases (Milvus/Weaviate)
|
||||
- **Backend**: Spring Boot 3.5.8 + Langchain4j
|
||||
- **Data Storage**: MySQL 8.0 + Redis + Vector Databases (Milvus/Weaviate/Qdrant)
|
||||
- **Frontend**: Vue 3 + Vben Admin + element-plus-x
|
||||
- **Security**: Sa-Token + JWT dual-layer security
|
||||
|
||||
|
||||
- **Document Processing**: PDF, Word, Excel parsing, intelligent image analysis
|
||||
- **Real-time Communication**: WebSocket real-time communication, SSE streaming response
|
||||
- **System Monitoring**: Comprehensive logging system, performance monitoring, service health checks
|
||||
|
||||
## 🐳 Docker Deployment
|
||||
|
||||
This project provides two Docker deployment methods:
|
||||
|
||||
### Method 1: One-click Start All Services (Recommended)
|
||||
|
||||
Use `docker-compose-all.yaml` to start all services at once (including backend, admin panel, user frontend, and dependencies):
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://github.com/ageerle/ruoyi-ai.git
|
||||
cd ruoyi-ai
|
||||
|
||||
# Start all services (pull pre-built images from registry)
|
||||
docker-compose -f docker-compose-all.yaml up -d
|
||||
|
||||
# Check service status
|
||||
docker-compose -f docker-compose-all.yaml ps
|
||||
|
||||
# Access services
|
||||
# Admin Panel: http://localhost:25666 (admin / admin123)
|
||||
# User Frontend: http://localhost:25137
|
||||
# Backend API: http://localhost:26039
|
||||
```
|
||||
|
||||
### Method 2: Step-by-step Deployment (Source Build)
|
||||
|
||||
If you need to build backend services from source, follow these steps:
|
||||
|
||||
#### Step 1: Deploy Backend Service
|
||||
|
||||
```bash
|
||||
# Enter backend project directory
|
||||
cd ruoyi-ai
|
||||
|
||||
# Start backend service (build from source)
|
||||
docker-compose up -d --build
|
||||
|
||||
# Wait for backend service to start
|
||||
docker-compose logs -f backend
|
||||
```
|
||||
|
||||
#### Step 2: Deploy Admin Panel
|
||||
|
||||
```bash
|
||||
# Enter admin panel project directory
|
||||
cd ruoyi-admin
|
||||
|
||||
# Build and start admin panel
|
||||
docker-compose up -d --build
|
||||
|
||||
# Access admin panel
|
||||
# URL: http://localhost:5666
|
||||
```
|
||||
|
||||
#### Step 3: Deploy User Frontend (Optional)
|
||||
|
||||
```bash
|
||||
# Enter user frontend project directory
|
||||
cd ruoyi-web
|
||||
|
||||
# Build and start user frontend
|
||||
docker-compose up -d --build
|
||||
|
||||
# Access user frontend
|
||||
# URL: http://localhost:5137
|
||||
```
|
||||
|
||||
### Service Ports
|
||||
|
||||
| Service | One-click Port | Step-by-step Port | Description |
|
||||
|------|-------------|-------------|------|
|
||||
| Admin Panel | 25666 | 5666 | Admin backend access |
|
||||
| User Frontend | 25137 | 5137 | User frontend access |
|
||||
| Backend Service | 26039 | 6039 | Backend API service |
|
||||
| MySQL | 23306 | 23306 | Database service |
|
||||
| Redis | 26379 | 6379 | Cache service |
|
||||
| Weaviate | 28080 | 28080 | Vector database |
|
||||
| MinIO API | 29000 | 9000 | Object storage API |
|
||||
| MinIO Console | 29090 | 9090 | Object storage console |
|
||||
|
||||
### Image Registry
|
||||
|
||||
All images are hosted on Alibaba Cloud Container Registry:
|
||||
|
||||
```
|
||||
crpi-31mraxd99y2gqdgr.cn-beijing.personal.cr.aliyuncs.com/ruoyi_ai
|
||||
```
|
||||
|
||||
Available images:
|
||||
- `mysql:v3` - MySQL database (includes initialization SQL)
|
||||
- `redis:6.2` - Redis cache
|
||||
- `weaviate:1.30.0` - Vector database
|
||||
- `minio:latest` - Object storage
|
||||
- `ruoyi-ai-backend:latest` - Backend service
|
||||
- `ruoyi-ai-admin:latest` - Admin frontend
|
||||
- `ruoyi-ai-web:latest` - User frontend
|
||||
|
||||
### Common Commands
|
||||
|
||||
```bash
|
||||
# Stop all services
|
||||
docker-compose -f docker-compose-all.yaml down
|
||||
|
||||
# View service logs
|
||||
docker-compose -f docker-compose-all.yaml logs -f [service-name]
|
||||
|
||||
# Restart a service
|
||||
docker-compose -f docker-compose-all.yaml restart [service-name]
|
||||
```
|
||||
|
||||
## 📚 Documentation
|
||||
|
||||
Want to learn more about installation, deployment, configuration, and secondary development?
|
||||
@@ -109,14 +223,13 @@ Thanks to the following excellent open-source projects for their support:
|
||||
- [PPIO Cloud](https://ppinfra.com/user/register?invited_by=P8QTUY&utm_source=github_ruoyi-ai) - Provides cost-effective GPU computing and model API services
|
||||
- [Youyun Intelligent Computing](https://www.compshare.cn/?ytag=GPU_YY-gh_ruoyi) - Thousands of RTX40 series GPUs + mainstream models API services, second-level response, pay-per-use, free for new customers.
|
||||
|
||||
## Outstanding Open-Source Projects and Community Recommendations
|
||||
- [imaiwork](https://gitee.com/tsinghua-open/imaiwork) - Open-source AI phone, AI customer acquisition phone project, based on accessibility mode and RPA, more powerful than Doubao AI phone.
|
||||
|
||||
## 💬 Community Chat
|
||||
|
||||
<div align="center">
|
||||
|
||||
**[📱 Join Telegram Group](https://t.me/+LqooQAc5HxRmYmE1)**
|
||||
**[📱 Join Telegram Group](
|
||||
https://t.me/+LqooQAc5HxRmYmE1)**
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
12
docs/docker/qdrant/docker-compose.yml
Normal file
12
docs/docker/qdrant/docker-compose.yml
Normal file
@@ -0,0 +1,12 @@
|
||||
---
|
||||
services:
|
||||
qdrant:
|
||||
image: qdrant/qdrant:latest
|
||||
ports:
|
||||
- 6333:6333
|
||||
- 6334:6334
|
||||
volumes:
|
||||
- qdrant_data:/qdrant/storage
|
||||
volumes:
|
||||
qdrant_data:
|
||||
...
|
||||
@@ -4,6 +4,9 @@ import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.ServerSocket;
|
||||
|
||||
/**
|
||||
* 启动程序
|
||||
*
|
||||
@@ -13,10 +16,66 @@ import org.springframework.boot.context.metrics.buffering.BufferingApplicationSt
|
||||
public class RuoYiAIApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
// killPortProcess(6039);
|
||||
SpringApplication application = new SpringApplication(RuoYiAIApplication.class);
|
||||
application.setApplicationStartup(new BufferingApplicationStartup(2048));
|
||||
application.run(args);
|
||||
System.out.println("(♥◠‿◠)ノ゙ RuoYi-AI启动成功 ლ(´ڡ`ლ)゙");
|
||||
System.out.println("(♥◠‿◠)ノ゙ RuoYi-AI启动成功 ლ(´ڡ`ლ)冢");
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查并终止占用指定端口的进程
|
||||
*
|
||||
* @param port 端口号
|
||||
*/
|
||||
private static void killPortProcess(int port) {
|
||||
try {
|
||||
if (!isPortInUse(port)) {
|
||||
return;
|
||||
}
|
||||
System.out.println("端口 " + port + " 已被占用,正在查找并终止进程...");
|
||||
|
||||
ProcessBuilder pb = new ProcessBuilder("netstat", "-ano");
|
||||
Process process = pb.start();
|
||||
java.io.BufferedReader reader = new java.io.BufferedReader(
|
||||
new java.io.InputStreamReader(process.getInputStream()));
|
||||
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
if (line.contains(":" + port + " ") && line.contains("LISTENING")) {
|
||||
String[] parts = line.trim().split("\\s+");
|
||||
String pid = parts[parts.length - 1];
|
||||
System.out.println("找到占用端口 " + port + " 的进程 PID: " + pid + ",正在终止...");
|
||||
|
||||
ProcessBuilder killPb = new ProcessBuilder("taskkill", "/F", "/PID", pid);
|
||||
Process killProcess = killPb.start();
|
||||
int exitCode = killProcess.waitFor();
|
||||
if (exitCode == 0) {
|
||||
System.out.println("进程 " + pid + " 已成功终止");
|
||||
} else {
|
||||
System.out.println("终止进程 " + pid + " 失败,exitCode: " + exitCode);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 等待一小段时间确保端口释放
|
||||
Thread.sleep(500);
|
||||
} catch (Exception e) {
|
||||
System.out.println("检查/终止端口进程时发生异常: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查端口是否被占用
|
||||
*/
|
||||
private static boolean isPortInUse(int port) {
|
||||
try (ServerSocket socket = new ServerSocket()) {
|
||||
socket.bind(new InetSocketAddress(port));
|
||||
return false;
|
||||
} catch (Exception e) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ spring:
|
||||
driverClassName: com.mysql.cj.jdbc.Driver
|
||||
# jdbc 所有参数配置参考 https://lionli.blog.csdn.net/article/details/122018562
|
||||
# rewriteBatchedStatements=true 批处理优化 大幅提升批量插入更新删除性能(对数据库有性能损耗 使用批量操作应考虑性能问题)
|
||||
url: jdbc:mysql://127.0.0.1:3306/ruoyi-ai-agent?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
|
||||
url: jdbc:mysql://127.0.0.1:3306/ruoyi-ai?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
|
||||
username: root
|
||||
password: root
|
||||
# agent:
|
||||
|
||||
@@ -275,7 +275,7 @@ warm-flow:
|
||||
|
||||
# 向量库配置
|
||||
vector-store:
|
||||
# 向量存储类型 可选(weaviate/milvus)
|
||||
# 向量存储类型 可选(weaviate/milvus/qdrant)
|
||||
# 如需修改向量库类型,请修改此配置值!
|
||||
type: milvus
|
||||
# Weaviate配置
|
||||
@@ -287,3 +287,10 @@ vector-store:
|
||||
milvus:
|
||||
url: http://localhost:19530
|
||||
collectionname: LocalKnowledge
|
||||
# Qdrant配置
|
||||
qdrant:
|
||||
host: localhost
|
||||
port: 6334
|
||||
collectionname: LocalKnowledge
|
||||
api-key:
|
||||
use-tls: false
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
package org.ruoyi.common.chat.domain.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 聊天消息DTO - 用于上下文传递
|
||||
*
|
||||
* @author ageerle@163.com
|
||||
* @date 2025/12/13
|
||||
*/
|
||||
@Data
|
||||
public class ChatMessageDTO {
|
||||
|
||||
/**
|
||||
* 消息角色: system/user/assistant
|
||||
*/
|
||||
private String role;
|
||||
|
||||
/**
|
||||
* 消息内容
|
||||
*/
|
||||
private String content;
|
||||
|
||||
public static ChatMessageDTO system(String content) {
|
||||
ChatMessageDTO msg = new ChatMessageDTO();
|
||||
msg.role = "system";
|
||||
msg.content = content;
|
||||
return msg;
|
||||
}
|
||||
|
||||
public static ChatMessageDTO user(String content) {
|
||||
ChatMessageDTO msg = new ChatMessageDTO();
|
||||
msg.role = "user";
|
||||
msg.content = content;
|
||||
return msg;
|
||||
}
|
||||
|
||||
public static ChatMessageDTO assistant(String content) {
|
||||
ChatMessageDTO msg = new ChatMessageDTO();
|
||||
msg.role = "assistant";
|
||||
msg.content = content;
|
||||
return msg;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
package org.ruoyi.common.chat.domain.dto.request;
|
||||
|
||||
import dev.langchain4j.data.message.ChatMessage;
|
||||
import com.alibaba.fastjson.annotation.JSONField;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import lombok.Data;
|
||||
import org.ruoyi.common.chat.domain.dto.ChatMessageDTO;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 对话请求对象
|
||||
@@ -16,11 +16,15 @@ import java.util.List;
|
||||
@Data
|
||||
public class ChatRequest {
|
||||
|
||||
@NotEmpty(message = "对话消息不能为空")
|
||||
private List<ChatMessageDTO> messages;
|
||||
@NotEmpty(message = "传入的模型不能为空")
|
||||
private String model;
|
||||
|
||||
/**
|
||||
* 对话消息
|
||||
*/
|
||||
@NotEmpty(message = "对话消息不能为空")
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* 工作流请求体
|
||||
*/
|
||||
@@ -31,59 +35,49 @@ public class ChatRequest {
|
||||
*/
|
||||
private ReSumeRunner reSumeRunner;
|
||||
|
||||
/**
|
||||
* 是否为人机交互用户继续输入
|
||||
*/
|
||||
private Boolean isResume = false;
|
||||
|
||||
/**
|
||||
* 是否启用工作流
|
||||
*/
|
||||
private Boolean enableWorkFlow;
|
||||
private Boolean enableWorkFlow = false;
|
||||
|
||||
/**
|
||||
* 会话id
|
||||
*/
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
@JSONField(serializeUsing = String.class)
|
||||
private Long sessionId;
|
||||
|
||||
/**
|
||||
* 应用ID
|
||||
*/
|
||||
private String appId;
|
||||
|
||||
/**
|
||||
* 知识库id
|
||||
*/
|
||||
private String knowledgeId;
|
||||
|
||||
/**
|
||||
* 对话id(每个聊天窗口都不一样)
|
||||
* 应用ID
|
||||
*/
|
||||
private Long uuid;
|
||||
private String appId;
|
||||
|
||||
|
||||
/**
|
||||
* 是否为人机交互用户继续输入
|
||||
* 对话id(每个聊天窗口都不一样)
|
||||
*/
|
||||
private Boolean isResume;
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
@JSONField(serializeUsing = String.class)
|
||||
private Long uuid;
|
||||
|
||||
/**
|
||||
* 是否启用深度思考
|
||||
*/
|
||||
private Boolean enableThinking;
|
||||
|
||||
/**
|
||||
* 是否自动切换模型
|
||||
*/
|
||||
private Boolean autoSelectModel;
|
||||
private Boolean enableThinking = false;
|
||||
|
||||
/**
|
||||
* 是否支持联网
|
||||
*/
|
||||
private Boolean enableInternet;
|
||||
|
||||
/**
|
||||
* 会话令牌(为避免在非Web线程中获取Request,入口处注入)
|
||||
*/
|
||||
private String token;
|
||||
|
||||
/**
|
||||
* 原生对话对象
|
||||
*/
|
||||
private List<ChatMessage> chatMessages;
|
||||
|
||||
}
|
||||
|
||||
@@ -5,8 +5,6 @@ import cn.idev.excel.annotation.ExcelProperty;
|
||||
import io.github.linpeilie.annotations.AutoMapper;
|
||||
import lombok.Data;
|
||||
import org.ruoyi.common.chat.entity.chat.ChatMessage;
|
||||
import org.ruoyi.common.excel.annotation.ExcelDictFormat;
|
||||
import org.ruoyi.common.excel.convert.ExcelDictConvert;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
@@ -6,12 +6,14 @@ import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
public class BaseEntity implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
package org.ruoyi.common.chat.entity.chat;
|
||||
|
||||
import dev.langchain4j.model.chat.response.StreamingChatResponseHandler;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
|
||||
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
|
||||
import org.ruoyi.common.chat.service.chat.IChatService;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
/**
|
||||
* 聊天对话上下文对象
|
||||
*
|
||||
* @author zengxb
|
||||
* @date 2026-02-14
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@Builder
|
||||
public class ChatContext {
|
||||
|
||||
/**
|
||||
* 模型管理视图对象
|
||||
*/
|
||||
@NotNull(message = "模型管理视图对象不能为空")
|
||||
private ChatModelVo chatModelVo;
|
||||
|
||||
/**
|
||||
* 对话请求对象
|
||||
*/
|
||||
@NotNull(message = "对话请求对象不能为空")
|
||||
private ChatRequest chatRequest;
|
||||
|
||||
/**
|
||||
* SSe连接对象
|
||||
*/
|
||||
@NotNull(message = "SSe连接对象不能为空")
|
||||
private SseEmitter emitter;
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
@NotNull(message = "用户ID不能为空")
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* Token
|
||||
*/
|
||||
@NotNull(message = "Token不能为空")
|
||||
private String tokenValue;
|
||||
|
||||
/**
|
||||
* 响应处理器
|
||||
*/
|
||||
private StreamingChatResponseHandler handler;
|
||||
|
||||
/**
|
||||
* 聊天服务实例
|
||||
*/
|
||||
private IChatService chatService;
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
package org.ruoyi.common.chat.service.chat;
|
||||
|
||||
import dev.langchain4j.model.chat.response.StreamingChatResponseHandler;
|
||||
import jakarta.validation.Valid;
|
||||
import org.ruoyi.common.chat.entity.chat.ChatContext;
|
||||
import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
/**
|
||||
@@ -12,10 +13,15 @@ public interface IChatService {
|
||||
/**
|
||||
* 客户端发送对话消息到服务端
|
||||
*/
|
||||
SseEmitter chat(@Valid ChatContext chatContext);
|
||||
SseEmitter chat(@Valid ChatRequest chatRequest);
|
||||
|
||||
/**
|
||||
* 获取服务提供商名称
|
||||
* 支持外部 handler 的对话接口(跨模块调用)
|
||||
* 同时发送到 SSE 和外部 handler
|
||||
*
|
||||
* @param chatRequest 聊天请求
|
||||
* @param externalHandler 外部响应处理器(可为 null)
|
||||
*/
|
||||
String getProviderName();
|
||||
void chat(@Valid ChatRequest chatRequest, StreamingChatResponseHandler externalHandler);
|
||||
|
||||
}
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
package org.ruoyi.common.chat.service.chatMessage;
|
||||
|
||||
import org.ruoyi.common.chat.domain.bo.chat.ChatMessageBo;
|
||||
import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
|
||||
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
/**
|
||||
* 聊天信息抽象基类 - 保存聊天信息
|
||||
*
|
||||
* @author Zengxb
|
||||
* @date 2026-02-24
|
||||
*/
|
||||
public abstract class AbstractChatMessageService {
|
||||
|
||||
/**
|
||||
* 创建日志对象
|
||||
*/
|
||||
Logger log = LoggerFactory.getLogger(AbstractChatMessageService.class);
|
||||
|
||||
@Autowired
|
||||
private IChatMessageService chatMessageService;
|
||||
|
||||
/**
|
||||
* 保存聊天信息
|
||||
*/
|
||||
public void saveChatMessage(ChatRequest chatRequest, Long userId, String content, String role, ChatModelVo chatModelVo){
|
||||
try {
|
||||
// 验证必要的上下文信息
|
||||
if (chatRequest == null || userId == null) {
|
||||
log.warn("缺少必要的聊天上下文信息,无法保存消息");
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建ChatMessageBo对象
|
||||
ChatMessageBo messageBO = new ChatMessageBo();
|
||||
messageBO.setUserId(userId);
|
||||
messageBO.setSessionId(chatRequest.getSessionId());
|
||||
messageBO.setContent(content);
|
||||
messageBO.setRole(role);
|
||||
messageBO.setModelName(chatRequest.getModel());
|
||||
messageBO.setRemark(null);
|
||||
|
||||
chatMessageService.insertByBo(messageBO);
|
||||
} catch (Exception e) {
|
||||
log.error("保存{}聊天消息时出错: {}", getProviderName(), e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取服务提供商名称
|
||||
*/
|
||||
protected String getProviderName(){
|
||||
return "默认工作流大模型";
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,11 @@ package org.ruoyi.common.sse.core;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.ruoyi.common.core.utils.SpringUtils;
|
||||
import org.ruoyi.common.redis.utils.RedisUtils;
|
||||
import org.ruoyi.common.sse.dto.SseEventDto;
|
||||
import org.ruoyi.common.sse.dto.SseMessageDto;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
@@ -65,7 +67,7 @@ public class SseEmitterManager {
|
||||
emitter.onCompletion(() -> {
|
||||
SseEmitter remove = emitters.remove(token);
|
||||
if (remove != null) {
|
||||
// remove.complete();
|
||||
remove.complete();
|
||||
}
|
||||
});
|
||||
emitter.onTimeout(() -> {
|
||||
@@ -174,9 +176,11 @@ public class SseEmitterManager {
|
||||
if (MapUtil.isNotEmpty(emitters)) {
|
||||
for (Map.Entry<String, SseEmitter> entry : emitters.entrySet()) {
|
||||
try {
|
||||
// 格式化为标准SSE JSON格式
|
||||
SseEventDto eventDto = SseEventDto.content(message);
|
||||
entry.getValue().send(SseEmitter.event()
|
||||
.name("message")
|
||||
.data(message));
|
||||
.data(JSONUtil.toJsonStr(eventDto)));
|
||||
} catch (Exception e) {
|
||||
SseEmitter remove = emitters.remove(entry.getKey());
|
||||
if (remove != null) {
|
||||
@@ -189,6 +193,33 @@ public class SseEmitterManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 向指定的用户会话发送结构化事件
|
||||
*
|
||||
* @param userId 要发送消息的用户id
|
||||
* @param eventDto SSE事件对象
|
||||
*/
|
||||
public void sendEvent(Long userId, SseEventDto eventDto) {
|
||||
Map<String, SseEmitter> emitters = USER_TOKEN_EMITTERS.get(userId);
|
||||
if (MapUtil.isNotEmpty(emitters)) {
|
||||
for (Map.Entry<String, SseEmitter> entry : emitters.entrySet()) {
|
||||
try {
|
||||
entry.getValue().send(SseEmitter.event()
|
||||
.name(eventDto.getEvent())
|
||||
.data(JSONUtil.toJsonStr(eventDto)));
|
||||
} catch (Exception e) {
|
||||
SseEmitter remove = emitters.remove(entry.getKey());
|
||||
if (remove != null) {
|
||||
remove.complete();
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
USER_TOKEN_EMITTERS.remove(userId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 本机全用户会话发送消息
|
||||
*
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
package org.ruoyi.common.sse.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* SSE 事件数据传输对象
|
||||
* <p>
|
||||
* 标准的 SSE 消息格式,支持不同事件类型
|
||||
*
|
||||
* @author ageerle@163.com
|
||||
* @date 2025/03/19
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class SseEventDto implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 事件类型
|
||||
*/
|
||||
private String event;
|
||||
|
||||
/**
|
||||
* 消息内容
|
||||
*/
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* 推理内容(深度思考模式)
|
||||
*/
|
||||
private String reasoningContent;
|
||||
|
||||
/**
|
||||
* 错误信息
|
||||
*/
|
||||
private String error;
|
||||
|
||||
/**
|
||||
* 是否完成
|
||||
*/
|
||||
private Boolean done;
|
||||
|
||||
/**
|
||||
* 创建内容事件
|
||||
*/
|
||||
public static SseEventDto content(String content) {
|
||||
return SseEventDto.builder()
|
||||
.event("content")
|
||||
.content(content)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建推理内容事件
|
||||
*/
|
||||
public static SseEventDto reasoning(String reasoningContent) {
|
||||
return SseEventDto.builder()
|
||||
.event("reasoning")
|
||||
.reasoningContent(reasoningContent)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建完成事件
|
||||
*/
|
||||
public static SseEventDto done() {
|
||||
return SseEventDto.builder()
|
||||
.event("done")
|
||||
.done(true)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建错误事件
|
||||
*/
|
||||
public static SseEventDto error(String error) {
|
||||
return SseEventDto.builder()
|
||||
.event("error")
|
||||
.error(error)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
package org.ruoyi.common.sse.utils;
|
||||
|
||||
import java.util.Collections;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.ruoyi.common.core.utils.SpringUtils;
|
||||
import org.ruoyi.common.sse.core.SseEmitterManager;
|
||||
import org.ruoyi.common.sse.dto.SseEventDto;
|
||||
import org.ruoyi.common.sse.dto.SseMessageDto;
|
||||
|
||||
/**
|
||||
@@ -27,6 +29,7 @@ public class SseMessageUtils {
|
||||
|
||||
/**
|
||||
* 向指定的SSE会话发送消息
|
||||
* 通过 Redis Pub/Sub 广播,确保跨模块消息可达
|
||||
*
|
||||
* @param userId 要发送消息的用户id
|
||||
* @param message 要发送的消息内容
|
||||
@@ -35,7 +38,11 @@ public class SseMessageUtils {
|
||||
if (!isEnable()) {
|
||||
return;
|
||||
}
|
||||
MANAGER.sendMessage(userId, message);
|
||||
// 通过 Redis 广播,让所有模块的 SseTopicListener 接收并转发到本地 SSE 连接
|
||||
SseMessageDto dto = new SseMessageDto();
|
||||
dto.setMessage(message);
|
||||
dto.setUserIds(Collections.singletonList(userId));
|
||||
MANAGER.publishMessage(dto);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -86,6 +93,58 @@ public class SseMessageUtils {
|
||||
MANAGER.disconnect(userId, tokenValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* 向指定的SSE会话发送结构化事件
|
||||
*
|
||||
* @param userId 要发送消息的用户id
|
||||
* @param eventDto SSE事件对象
|
||||
*/
|
||||
public static void sendEvent(Long userId, SseEventDto eventDto) {
|
||||
if (!isEnable()) {
|
||||
return;
|
||||
}
|
||||
MANAGER.sendEvent(userId, eventDto);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送内容事件
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param content 内容
|
||||
*/
|
||||
public static void sendContent(Long userId, String content) {
|
||||
sendEvent(userId, SseEventDto.content(content));
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送推理内容事件
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param reasoningContent 推理内容
|
||||
*/
|
||||
public static void sendReasoning(Long userId, String reasoningContent) {
|
||||
sendEvent(userId, SseEventDto.reasoning(reasoningContent));
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送完成事件
|
||||
*
|
||||
* @param userId 用户ID
|
||||
*/
|
||||
public static void sendDone(Long userId) {
|
||||
sendEvent(userId, SseEventDto.done());
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送错误事件
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param error 错误信息
|
||||
*/
|
||||
public static void sendError(Long userId, String error) {
|
||||
sendEvent(userId, SseEventDto.error(error));
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否开启
|
||||
*/
|
||||
|
||||
@@ -98,7 +98,7 @@ public class WorkflowComponentService extends ServiceImpl<WorkflowComponentMappe
|
||||
return baseMapper.selectPage(new Page<>(currentPage, pageSize), wrapper);
|
||||
}
|
||||
|
||||
@Cacheable(cacheNames = WORKFLOW_COMPONENTS)
|
||||
// @Cacheable(cacheNames = WORKFLOW_COMPONENTS)
|
||||
public List<WorkflowComponent> getAllEnable() {
|
||||
return ChainWrappers.lambdaQueryChain(baseMapper)
|
||||
.eq(WorkflowComponent::getIsEnable, true)
|
||||
|
||||
@@ -64,7 +64,8 @@ public class WorkflowMessageUtil {
|
||||
ChatRequest chatRequest = new ChatRequest();
|
||||
chatRequest.setSessionId(sessionId);
|
||||
WorkflowUtil workflowUtil = SpringUtils.getBean(WorkflowUtil.class);
|
||||
workflowUtil.saveChatMessage(chatRequest, userId, message, RoleType.WORKFLOW.getName(), new ChatModelVo());
|
||||
// todo 保存消息
|
||||
//workflowUtil.saveChatMessage(chatRequest, userId, message, RoleType.WORKFLOW.getName(), new ChatModelVo());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,23 +4,18 @@ import cn.hutool.core.collection.CollStreamUtil;
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import dev.langchain4j.data.message.ChatMessage;
|
||||
import dev.langchain4j.data.message.SystemMessage;
|
||||
import dev.langchain4j.data.message.UserMessage;
|
||||
import dev.langchain4j.model.chat.response.StreamingChatResponseHandler;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.bsc.langgraph4j.langchain4j.generators.StreamingChatGenerator;
|
||||
import org.bsc.langgraph4j.state.AgentState;
|
||||
import org.ruoyi.common.chat.enums.RoleType;
|
||||
import org.ruoyi.common.chat.service.chat.IChatModelService;
|
||||
import org.ruoyi.common.chat.service.chat.IChatService;
|
||||
import org.ruoyi.common.chat.service.chatMessage.AbstractChatMessageService;
|
||||
import org.ruoyi.common.chat.service.image.IImageGenerationService;
|
||||
import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
|
||||
import org.ruoyi.common.chat.entity.chat.ChatContext;
|
||||
import org.ruoyi.common.chat.entity.image.ImageContext;
|
||||
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
|
||||
import org.ruoyi.common.chat.factory.ChatServiceFactory;
|
||||
import org.ruoyi.common.chat.factory.ImageServiceFactory;
|
||||
import org.ruoyi.workflow.base.NodeInputConfigTypeHandler;
|
||||
import org.ruoyi.workflow.entity.WorkflowNode;
|
||||
@@ -29,9 +24,7 @@ import org.ruoyi.workflow.util.JsonUtil;
|
||||
import org.ruoyi.workflow.workflow.data.NodeIOData;
|
||||
import org.ruoyi.workflow.workflow.data.NodeIODataContent;
|
||||
import org.ruoyi.workflow.workflow.def.WfNodeParamRef;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@@ -39,10 +32,7 @@ import static org.ruoyi.workflow.cosntant.AdiConstant.WorkflowConstant.DEFAULT_O
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class WorkflowUtil extends AbstractChatMessageService {
|
||||
|
||||
@Resource
|
||||
private ChatServiceFactory chatServiceFactory;
|
||||
public class WorkflowUtil{
|
||||
|
||||
@Resource
|
||||
private ImageServiceFactory imageServiceFactory;
|
||||
@@ -50,6 +40,9 @@ public class WorkflowUtil extends AbstractChatMessageService {
|
||||
@Resource
|
||||
private IChatModelService chatModelService;
|
||||
|
||||
@Resource
|
||||
private IChatService chatService;
|
||||
|
||||
public static String renderTemplate(String template, List<NodeIOData> values) {
|
||||
// 🔒 关键修复:如果 template 为 null,直接返回 null 或空字符串
|
||||
if (template == null) {
|
||||
@@ -112,54 +105,23 @@ public class WorkflowUtil extends AbstractChatMessageService {
|
||||
}
|
||||
|
||||
public void streamingInvokeLLM(WfState wfState, WfNodeState state, WorkflowNode node, String modelName,
|
||||
List<SystemMessage> systemMessage, String nodeMessageTemplate) {
|
||||
String prompt, String nodeMessageTemplate) {
|
||||
log.info("stream invoke, modelName: {}", modelName);
|
||||
|
||||
// 根据模型名称查询模型信息
|
||||
ChatModelVo chatModelVo = chatModelService.selectModelByName(modelName);
|
||||
if (chatModelVo == null) {
|
||||
throw new IllegalArgumentException("模型不存在: " + modelName);
|
||||
}
|
||||
|
||||
// 路由服务提供商
|
||||
String category = chatModelVo.getProviderCode();
|
||||
// 根据 category 获取对应的 ChatService(不使用计费代理,工作流场景单独计费)
|
||||
IChatService chatService = chatServiceFactory.getOriginalService(category);
|
||||
|
||||
// 获取用户信息和Token以及SSe连接对象(对话接口需要使用)
|
||||
Long sessionId = wfState.getSessionId();
|
||||
Long userId = wfState.getUserId();
|
||||
String tokenValue = wfState.getTokenValue();
|
||||
SseEmitter sseEmitter = wfState.getSseEmitter();
|
||||
|
||||
// 构建 ruoyi-ai 的 ChatRequest
|
||||
List<ChatMessage> chatMessages = new ArrayList<>();
|
||||
addUserMessage(node, state.getInputs(), chatMessages);
|
||||
chatMessages.addAll(systemMessage);
|
||||
|
||||
// 定义模型调用对象
|
||||
ChatRequest chatRequest = new ChatRequest();
|
||||
// 目前工作流深度思考成员变量只能写死
|
||||
chatRequest.setSessionId(sessionId);
|
||||
chatRequest.setEnableThinking(false);
|
||||
chatRequest.setModel(modelName);
|
||||
chatRequest.setChatMessages(chatMessages);
|
||||
chatRequest.setContent(prompt);
|
||||
|
||||
// 构建流式生成器
|
||||
StreamingChatGenerator<AgentState> streamingGenerator = StreamingChatGenerator.builder()
|
||||
.mapResult(response -> {
|
||||
String responseTxt = response.aiMessage().text();
|
||||
log.info("llm response:{}", responseTxt);
|
||||
|
||||
// 会话ID不为空时插入数据库
|
||||
if (sessionId != null){
|
||||
// 获取模板消息拼接信息体
|
||||
String message = nodeMessageTemplate + responseTxt;
|
||||
// 保存助手回复消息
|
||||
saveChatMessage(chatRequest, userId, message, RoleType.ASSISTANT.getName(), chatModelVo);
|
||||
log.info("{}消息结束,已保存到数据库", getProviderName());
|
||||
}
|
||||
|
||||
// 传递所有输入数据 + 添加 LLM 输出
|
||||
wfState.getNodeStateByNodeUuid(node.getUuid()).ifPresent(item -> {
|
||||
List<NodeIOData> outputs = new ArrayList<>(item.getInputs());
|
||||
@@ -174,21 +136,13 @@ public class WorkflowUtil extends AbstractChatMessageService {
|
||||
.startingState(state)
|
||||
.build();
|
||||
|
||||
// 构建流式回调响应器
|
||||
StreamingChatResponseHandler handler = streamingGenerator.handler();
|
||||
// 获取 StreamingChatGenerator 的 handler,用于处理流式响应
|
||||
StreamingChatResponseHandler workflowHandler = streamingGenerator.handler();
|
||||
|
||||
//构建聊天对话上下文参数
|
||||
ChatContext chatContext = ChatContext.builder()
|
||||
.chatModelVo(chatModelVo)
|
||||
.chatRequest(chatRequest)
|
||||
.emitter(sseEmitter)
|
||||
.userId(userId)
|
||||
.tokenValue(tokenValue)
|
||||
.handler(handler)
|
||||
.build();
|
||||
// 调用 Chat 服务,传入 workflow 的 handler
|
||||
// 消息会同时发送到 SSE(前端)和 workflowHandler(工作流处理)
|
||||
chatService.chat(chatRequest, workflowHandler);
|
||||
|
||||
// 使用工作流专用方法
|
||||
chatService.chat(chatContext);
|
||||
wfState.getNodeToStreamingGenerator().put(node.getUuid(), streamingGenerator);
|
||||
}
|
||||
|
||||
|
||||
@@ -46,13 +46,11 @@ public class LLMAnswerNode extends AbstractWfNode {
|
||||
// 调用LLM
|
||||
WorkflowUtil workflowUtil = SpringUtil.getBean(WorkflowUtil.class);
|
||||
String modelName = nodeConfigObj.getModelName();
|
||||
// 转换系统信息结构
|
||||
List<SystemMessage> systemMessage = List.of(new SystemMessage(prompt));
|
||||
// 获取节点模板提示词信息
|
||||
String nodeMessageTemplate = WorkflowMessageUtil.getNodeMessageTemplate(NodeMessageTemplateEnum.LLM_RESPONSE.getValue());
|
||||
// 发送SSE驱动事件消息
|
||||
WorkflowMessageUtil.sendEmitterMessage(wfState.getSseEmitter(), node, nodeMessageTemplate);
|
||||
workflowUtil.streamingInvokeLLM(wfState, state, node, modelName, systemMessage, nodeMessageTemplate);
|
||||
workflowUtil.streamingInvokeLLM(wfState, state, node, modelName, prompt, nodeMessageTemplate);
|
||||
return new NodeProcessResult();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,13 +67,12 @@ public class KeywordExtractorNode extends AbstractWfNode {
|
||||
// 调用 LLM 进行关键词提取
|
||||
WorkflowUtil workflowUtil = SpringUtil.getBean(WorkflowUtil.class);
|
||||
String modelName = config.getModelName();
|
||||
List<SystemMessage> systemMessage = List.of(new SystemMessage(prompt));
|
||||
// 获取节点模板提示词信息
|
||||
String nodeMessageTemplate = WorkflowMessageUtil.getNodeMessageTemplate(NodeMessageTemplateEnum.KEYWORD_EXTRACTOR.getValue());
|
||||
// 发送SSE事件消息
|
||||
WorkflowMessageUtil.sendEmitterMessage(wfState.getSseEmitter(), node, nodeMessageTemplate);
|
||||
// 使用流式调用
|
||||
workflowUtil.streamingInvokeLLM(wfState, state, node, modelName, systemMessage, nodeMessageTemplate);
|
||||
workflowUtil.streamingInvokeLLM(wfState, state, node, modelName, prompt, nodeMessageTemplate);
|
||||
return new NodeProcessResult();
|
||||
}
|
||||
|
||||
|
||||
@@ -151,7 +151,6 @@ public class KnowledgeRetrievalNode extends AbstractWfNode {
|
||||
|
||||
// 使用WorkflowUtil调用LLM(流式)
|
||||
WorkflowUtil workflowUtil = SpringUtil.getBean(WorkflowUtil.class);
|
||||
List<SystemMessage> systemMessage = List.of(new SystemMessage(prompt));
|
||||
|
||||
// 调用流式LLM
|
||||
String modelName = StringUtils.isNotBlank(config.getModelName()) ? config.getModelName() : "deepseek-chat";
|
||||
@@ -161,7 +160,7 @@ public class KnowledgeRetrievalNode extends AbstractWfNode {
|
||||
tempState,
|
||||
tempNode,
|
||||
modelName,
|
||||
systemMessage,
|
||||
prompt,
|
||||
""
|
||||
);
|
||||
|
||||
|
||||
425
ruoyi-modules/ruoyi-aiflow/流程编排模块说明.md
Normal file
425
ruoyi-modules/ruoyi-aiflow/流程编排模块说明.md
Normal file
@@ -0,0 +1,425 @@
|
||||
# Ruoyi-AI 流程编排模块详细说明文档
|
||||
|
||||
## 概述
|
||||
|
||||
Ruoyi-AI 工作流模块是一个基于 LangGraph4j 的智能工作流引擎,支持可视化工作流设计、AI 模型集成、条件分支、人机交互等高级功能。该模块采用微服务架构,提供完整的
|
||||
RESTful API 和流式响应支持。
|
||||
|
||||
## 模块架构
|
||||
|
||||
### 1. 核心依赖
|
||||
|
||||
- **LangGraph4j**: 1.5.3 - 工作流图执行引擎
|
||||
- **LangChain4j**: 1.11.0 - AI 模型集成框架
|
||||
- **Spring Boot**: 3.5.8 - 应用框架
|
||||
- **MyBatis Plus**: 数据访问层
|
||||
- **Redis**: 缓存和状态管理
|
||||
- **OpenAPI**: API 文档
|
||||
|
||||
## 核心功能
|
||||
|
||||
### 1. 工作流管理
|
||||
|
||||
#### 1.1 工作流定义
|
||||
|
||||
- **创建工作流**: 支持自定义标题、描述、公开性设置
|
||||
- **编辑工作流**: 可视化节点编辑、连接线配置
|
||||
- **版本控制**: 支持工作流的版本管理和回滚
|
||||
- **权限管理**: 支持公开/私有工作流设置
|
||||
|
||||
#### 1.2 工作流执行
|
||||
|
||||
- **流式执行**: 基于 SSE 的实时流式响应
|
||||
- **状态管理**: 完整的执行状态跟踪
|
||||
- **错误处理**: 详细的错误信息和异常处理
|
||||
- **中断恢复**: 支持工作流中断和恢复执行
|
||||
|
||||
### 2. 节点类型
|
||||
|
||||
#### 2.1 基础节点
|
||||
|
||||
- **Start**: 开始节点,定义工作流入口
|
||||
- **End**: 结束节点,定义工作流出口
|
||||
|
||||
|
||||
#### 2.2 AI 模型节点
|
||||
|
||||
- **Answer**: 大语言模型问答节点
|
||||
- **Dalle3**: DALL-E 3 图像生成
|
||||
- **Tongyiwanx**: 通义万相图像生成
|
||||
- **Classifier**: 内容分类节点
|
||||
|
||||
#### 2.3 数据处理节点
|
||||
|
||||
- **DocumentExtractor**: 文档信息提取
|
||||
- **KeywordExtractor**: 关键词提取
|
||||
- **FaqExtractor**: 常见问题提取
|
||||
- **KnowledgeRetrieval**: 知识库检索
|
||||
|
||||
#### 2.4 控制流节点
|
||||
|
||||
- **Switcher**: 条件分支节点
|
||||
- **HumanFeedback**: 人机交互节点
|
||||
|
||||
#### 2.5 外部集成节点
|
||||
|
||||
- **Google**: Google 搜索集成
|
||||
- **MailSend**: 邮件发送
|
||||
- **HttpRequest**: HTTP 请求
|
||||
- **Template**: 模板转换
|
||||
|
||||
### 3. 数据流管理
|
||||
|
||||
#### 3.1 输入输出定义
|
||||
|
||||
```java
|
||||
// 节点输入输出数据结构
|
||||
public class NodeIOData {
|
||||
private String name; // 参数名称
|
||||
private NodeIODataContent content; // 参数内容
|
||||
}
|
||||
|
||||
// 支持的数据类型
|
||||
public enum WfIODataTypeEnum {
|
||||
TEXT, // 文本
|
||||
NUMBER, // 数字
|
||||
BOOLEAN, // 布尔值
|
||||
FILES, // 文件
|
||||
OPTIONS // 选项
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.2 参数引用
|
||||
|
||||
- **节点间引用**: 支持上游节点输出作为下游节点输入
|
||||
- **参数映射**: 自动处理参数名称映射
|
||||
- **类型转换**: 自动进行数据类型转换
|
||||
|
||||
## 数据库设计
|
||||
|
||||
### 1. 核心表结构
|
||||
|
||||
#### 1.1 工作流定义表 (t_workflow)
|
||||
|
||||
```sql
|
||||
CREATE TABLE t_workflow (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
uuid VARCHAR(32) NOT NULL DEFAULT '',
|
||||
title VARCHAR(100) NOT NULL DEFAULT '',
|
||||
remark TEXT NOT NULL DEFAULT '',
|
||||
user_id BIGINT NOT NULL DEFAULT 0,
|
||||
is_public TINYINT(1) NOT NULL DEFAULT 0,
|
||||
is_enable TINYINT(1) NOT NULL DEFAULT 1,
|
||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
is_deleted TINYINT(1) NOT NULL DEFAULT 0
|
||||
);
|
||||
```
|
||||
|
||||
#### 1.2 工作流节点表 (t_workflow_node)
|
||||
|
||||
```sql
|
||||
CREATE TABLE t_workflow_node (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
uuid VARCHAR(32) NOT NULL DEFAULT '',
|
||||
workflow_id BIGINT NOT NULL DEFAULT 0,
|
||||
workflow_component_id BIGINT NOT NULL DEFAULT 0,
|
||||
user_id BIGINT NOT NULL DEFAULT 0,
|
||||
title VARCHAR(100) NOT NULL DEFAULT '',
|
||||
remark VARCHAR(500) NOT NULL DEFAULT '',
|
||||
input_config JSON NOT NULL DEFAULT ('{}'),
|
||||
node_config JSON NOT NULL DEFAULT ('{}'),
|
||||
position_x DOUBLE NOT NULL DEFAULT 0,
|
||||
position_y DOUBLE NOT NULL DEFAULT 0,
|
||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
is_deleted TINYINT(1) NOT NULL DEFAULT 0
|
||||
);
|
||||
```
|
||||
|
||||
#### 1.3 工作流边表 (t_workflow_edge)
|
||||
|
||||
```sql
|
||||
CREATE TABLE t_workflow_edge (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
uuid VARCHAR(32) NOT NULL DEFAULT '',
|
||||
workflow_id BIGINT NOT NULL DEFAULT 0,
|
||||
source_node_uuid VARCHAR(32) NOT NULL DEFAULT '',
|
||||
source_handle VARCHAR(32) NOT NULL DEFAULT '',
|
||||
target_node_uuid VARCHAR(32) NOT NULL DEFAULT '',
|
||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
is_deleted TINYINT(1) NOT NULL DEFAULT 0
|
||||
);
|
||||
```
|
||||
|
||||
#### 1.4 工作流运行时表 (t_workflow_runtime)
|
||||
|
||||
```sql
|
||||
CREATE TABLE t_workflow_runtime (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
uuid VARCHAR(32) NOT NULL DEFAULT '',
|
||||
user_id BIGINT NOT NULL DEFAULT 0,
|
||||
workflow_id BIGINT NOT NULL DEFAULT 0,
|
||||
input JSON NOT NULL DEFAULT ('{}'),
|
||||
output JSON NOT NULL DEFAULT ('{}'),
|
||||
status SMALLINT NOT NULL DEFAULT 1,
|
||||
status_remark VARCHAR(250) NOT NULL DEFAULT '',
|
||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
is_deleted TINYINT(1) NOT NULL DEFAULT 0
|
||||
);
|
||||
```
|
||||
|
||||
#### 1.5 工作流组件表 (t_workflow_component)
|
||||
|
||||
```sql
|
||||
CREATE TABLE t_workflow_component (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
uuid VARCHAR(32) DEFAULT '' NOT NULL,
|
||||
name VARCHAR(32) DEFAULT '' NOT NULL,
|
||||
title VARCHAR(100) DEFAULT '' NOT NULL,
|
||||
remark TEXT NOT NULL,
|
||||
display_order INT DEFAULT 0 NOT NULL,
|
||||
is_enable TINYINT(1) DEFAULT 0 NOT NULL,
|
||||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
update_time DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
is_deleted TINYINT(1) DEFAULT 0 NOT NULL
|
||||
);
|
||||
```
|
||||
|
||||
## API 接口
|
||||
|
||||
### 1. 工作流管理接口
|
||||
|
||||
#### 1.1 基础操作
|
||||
|
||||
```http
|
||||
# 创建工作流
|
||||
POST /workflow/add
|
||||
Content-Type: application/json
|
||||
{
|
||||
"title": "工作流标题",
|
||||
"remark": "工作流描述",
|
||||
"isPublic": false
|
||||
}
|
||||
|
||||
# 更新工作流
|
||||
POST /workflow/update
|
||||
Content-Type: application/json
|
||||
{
|
||||
"uuid": "工作流UUID",
|
||||
"title": "新标题",
|
||||
"remark": "新描述"
|
||||
}
|
||||
|
||||
# 删除工作流
|
||||
POST /workflow/del/{uuid}
|
||||
|
||||
# 启用/禁用工作流
|
||||
POST /workflow/enable/{uuid}?enable=true
|
||||
```
|
||||
|
||||
#### 1.2 搜索和查询
|
||||
|
||||
```http
|
||||
# 搜索我的工作流
|
||||
GET /workflow/mine/search?keyword=关键词&isPublic=true¤tPage=1&pageSize=10
|
||||
|
||||
# 搜索公开工作流
|
||||
GET /workflow/public/search?keyword=关键词¤tPage=1&pageSize=10
|
||||
|
||||
# 获取工作流组件列表
|
||||
GET /workflow/public/component/list
|
||||
```
|
||||
|
||||
### 2. 工作流执行接口
|
||||
|
||||
#### 2.1 流式执行
|
||||
|
||||
```http
|
||||
# 流式执行工作流
|
||||
POST /workflow/run
|
||||
Content-Type: application/json
|
||||
Accept: text/event-stream
|
||||
{
|
||||
"uuid": "工作流UUID",
|
||||
"inputs": [
|
||||
{
|
||||
"name": "input",
|
||||
"content": {
|
||||
"type": 1,
|
||||
"textContent": "用户输入内容"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### 2.2 运行时管理
|
||||
|
||||
```http
|
||||
# 恢复中断的工作流
|
||||
POST /workflow/runtime/resume/{runtimeUuid}
|
||||
Content-Type: application/json
|
||||
{
|
||||
"feedbackContent": "用户反馈内容"
|
||||
}
|
||||
|
||||
# 查询工作流执行历史
|
||||
GET /workflow/runtime/page?wfUuid=工作流UUID¤tPage=1&pageSize=10
|
||||
|
||||
# 查询运行时节点详情
|
||||
GET /workflow/runtime/nodes/{runtimeUuid}
|
||||
|
||||
# 清理运行时数据
|
||||
POST /workflow/runtime/clear?wfUuid=工作流UUID
|
||||
```
|
||||
|
||||
### 3. 管理端接口
|
||||
|
||||
#### 3.1 工作流管理
|
||||
|
||||
```http
|
||||
# 搜索所有工作流
|
||||
POST /admin/workflow/search
|
||||
Content-Type: application/json
|
||||
{
|
||||
"title": "搜索关键词",
|
||||
"isPublic": true,
|
||||
"isEnable": true
|
||||
}
|
||||
|
||||
# 启用/禁用工作流
|
||||
POST /admin/workflow/enable?uuid=工作流UUID&isEnable=true
|
||||
```
|
||||
|
||||
## 核心实现
|
||||
|
||||
### 1. 工作流引擎 (WorkflowEngine)
|
||||
|
||||
工作流引擎是整个模块的核心,负责:
|
||||
|
||||
- 工作流图的构建和编译
|
||||
- 节点执行调度
|
||||
- 状态管理和持久化
|
||||
- 流式输出处理
|
||||
|
||||
```java
|
||||
public class WorkflowEngine {
|
||||
// 核心执行方法
|
||||
public void run(User user, List<ObjectNode> userInputs, SseEmitter sseEmitter) {
|
||||
// 1. 验证工作流状态
|
||||
// 2. 创建运行时实例
|
||||
// 3. 构建状态图
|
||||
// 4. 执行工作流
|
||||
// 5. 处理流式输出
|
||||
}
|
||||
|
||||
// 恢复执行方法
|
||||
public void resume(String userInput) {
|
||||
// 1. 更新状态
|
||||
// 2. 继续执行
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 节点工厂 (WfNodeFactory)
|
||||
|
||||
节点工厂负责根据组件类型创建对应的节点实例:
|
||||
|
||||
```java
|
||||
public class WfNodeFactory {
|
||||
public static AbstractWfNode create(WorkflowComponent component,
|
||||
WorkflowNode node,
|
||||
WfState wfState,
|
||||
WfNodeState nodeState) {
|
||||
// 根据组件类型创建对应的节点实例
|
||||
switch (component.getName()) {
|
||||
case "Answer":
|
||||
return new LLMAnswerNode(component, node, wfState, nodeState);
|
||||
case "Switcher":
|
||||
return new SwitcherNode(component, node, wfState, nodeState);
|
||||
// ... 其他节点类型
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 图构建器 (WorkflowGraphBuilder)
|
||||
|
||||
图构建器负责将工作流定义转换为可执行的状态图:
|
||||
|
||||
```java
|
||||
public class WorkflowGraphBuilder {
|
||||
public StateGraph<WfNodeState> build(WorkflowNode startNode) {
|
||||
// 1. 构建编译节点树
|
||||
// 2. 转换为状态图
|
||||
// 3. 添加节点和边
|
||||
// 4. 处理条件分支
|
||||
// 5. 处理并行执行
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 流式响应机制
|
||||
|
||||
### 1. SSE 事件类型
|
||||
|
||||
工作流执行过程中会发送多种类型的 SSE 事件:
|
||||
|
||||
```javascript
|
||||
// 节点开始执行
|
||||
[NODE_RUN_节点UUID] - 节点执行开始事件
|
||||
|
||||
// 节点输入数据
|
||||
[NODE_INPUT_节点UUID] - 节点输入数据事件
|
||||
|
||||
// 节点输出数据
|
||||
[NODE_OUTPUT_节点UUID] - 节点输出数据事件
|
||||
|
||||
// 流式内容块
|
||||
[NODE_CHUNK_节点UUID] - 流式内容块事件
|
||||
|
||||
// 等待用户输入
|
||||
[NODE_WAIT_FEEDBACK_BY_节点UUID] - 等待用户输入事件
|
||||
```
|
||||
|
||||
### 2. 流式处理流程
|
||||
|
||||
1. **初始化**: 创建工作流运行时实例
|
||||
2. **节点执行**: 逐个执行工作流节点
|
||||
3. **实时输出**: 通过 SSE 实时推送执行结果
|
||||
4. **状态更新**: 实时更新节点和工作流状态
|
||||
5. **错误处理**: 捕获并处理执行过程中的错误
|
||||
|
||||
## 扩展开发
|
||||
|
||||
### 1. 自定义节点开发
|
||||
|
||||
要开发自定义工作流节点,需要:
|
||||
|
||||
1. **创建节点类**:继承 `AbstractWfNode`
|
||||
2. **实现处理逻辑**:重写 `onProcess()` 方法
|
||||
3. **定义配置类**:创建节点配置类
|
||||
4. **注册组件**:在组件表中注册新组件
|
||||
|
||||
```java
|
||||
public class CustomNode extends AbstractWfNode {
|
||||
@Override
|
||||
protected NodeProcessResult onProcess() {
|
||||
// 实现自定义处理逻辑
|
||||
List<NodeIOData> outputs = new ArrayList<>();
|
||||
// ... 处理逻辑
|
||||
return NodeProcessResult.success(outputs);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 自定义组件注册
|
||||
|
||||
```sql
|
||||
-- 在 t_workflow_component 表中添加新组件
|
||||
INSERT INTO t_workflow_component (uuid, name, title, remark, is_enable)
|
||||
VALUES (REPLACE(UUID(), '-', ''), 'CustomNode', '自定义节点', '自定义节点描述', true);
|
||||
```
|
||||
@@ -19,6 +19,11 @@
|
||||
<artifactId>ruoyi-common-chat</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.ruoyi</groupId>
|
||||
<artifactId>ruoyi-common-sse</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.ruoyi</groupId>
|
||||
<artifactId>ruoyi-common-sensitive</artifactId>
|
||||
@@ -86,6 +91,12 @@
|
||||
<version>${langchain4j.community.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>dev.langchain4j</groupId>
|
||||
<artifactId>langchain4j-qdrant</artifactId>
|
||||
<version>${langchain4j.community.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>dev.langchain4j</groupId>
|
||||
<artifactId>langchain4j-mcp</artifactId>
|
||||
|
||||
@@ -6,7 +6,7 @@ import dev.langchain4j.service.UserMessage;
|
||||
import dev.langchain4j.service.V;
|
||||
|
||||
|
||||
public interface ChartGenerationAgent extends Agent {
|
||||
public interface ChartGenerationAgent {
|
||||
|
||||
@SystemMessage("""
|
||||
You are a chart generation specialist. Your only task is to generate Apache ECharts
|
||||
|
||||
@@ -14,7 +14,7 @@ public interface EchartsAgent {
|
||||
|
||||
@SystemMessage("""
|
||||
You are a data visualization assistant that generates Echarts chart configurations.
|
||||
|
||||
|
||||
CRITICAL OUTPUT REQUIREMENTS:
|
||||
- Return Echarts JSON wrapped in markdown code block
|
||||
- Use this exact format: ```json\n{...}\n```
|
||||
@@ -81,7 +81,7 @@ public interface EchartsAgent {
|
||||
""")
|
||||
@UserMessage("""
|
||||
Generate an Echarts chart for: {{query}}
|
||||
|
||||
|
||||
IMPORTANT: Return the Echarts configuration JSON wrapped in markdown code block (```json...```).
|
||||
""")
|
||||
@Agent("Data visualization assistant that returns Echarts JSON configurations for frontend rendering")
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
package org.ruoyi.agent;
|
||||
|
||||
import dev.langchain4j.agentic.Agent;
|
||||
import dev.langchain4j.service.SystemMessage;
|
||||
import dev.langchain4j.service.UserMessage;
|
||||
import dev.langchain4j.service.V;
|
||||
|
||||
/**
|
||||
* User Name Retrieval Agent
|
||||
* A simple assistant that retrieves user names using the get_name tool.
|
||||
*/
|
||||
public interface GetNameInfo {
|
||||
|
||||
@SystemMessage("""
|
||||
You are a user identity assistant. You MUST always use tools to get information.
|
||||
|
||||
MANDATORY REQUIREMENTS:
|
||||
- You MUST call the get_user_name_by_id tool for ANY question about names or identity
|
||||
- NEVER respond without calling the get_user_name_by_id tool first
|
||||
- Return ONLY the exact string returned by the get_user_name_by_id tool
|
||||
- Do not make up names like "John Doe" or any other default names
|
||||
- Do not use your knowledge to answer - ALWAYS use the tool
|
||||
|
||||
Your workflow:
|
||||
1. Extract userId from the query (if mentioned), or use "1" as default
|
||||
2. ALWAYS call the get_user_name_by_id tool with the userId parameter
|
||||
3. Return the exact result as plain text with no additions
|
||||
|
||||
CRITICAL: If you don't call the get_user_name_by_id tool, your response is wrong.
|
||||
""")
|
||||
@UserMessage("""
|
||||
Get the user name using the get_user_name_by_id tool. Query: {{query}}
|
||||
|
||||
IMPORTANT: Return only the exact result from the tool.
|
||||
""")
|
||||
@Agent("User identity assistant that returns user name from get_name tool")
|
||||
String search(@V("query") String query);
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
package org.ruoyi.agent;
|
||||
|
||||
import dev.langchain4j.agentic.Agent;
|
||||
import dev.langchain4j.service.SystemMessage;
|
||||
import dev.langchain4j.service.UserMessage;
|
||||
import dev.langchain4j.service.V;
|
||||
|
||||
public interface McpAgent extends Agent {
|
||||
|
||||
/**
|
||||
* 系统提示词:通用工具调用智能体
|
||||
* 不限定具体工具类型,让 LangChain4j 自动传递工具描述给 LLM
|
||||
*/
|
||||
@SystemMessage("""
|
||||
你是一个AI助手,可以通过调用各种工具来帮助用户完成不同的任务。
|
||||
|
||||
【工具使用规则】
|
||||
1. 根据用户的请求,判断需要使用哪些工具
|
||||
2. 仔细阅读每个工具的描述,确保理解工具的功能和参数要求
|
||||
3. 使用正确的参数调用工具
|
||||
4. 如果工具执行失败,向用户友好地说明错误原因,并尝试提供替代方案
|
||||
5. 对于复杂任务,可以分步骤使用多个工具完成
|
||||
6. 将工具执行结果以清晰易懂的方式呈现给用户
|
||||
|
||||
【响应格式】
|
||||
- 直接回答用户的问题
|
||||
- 如果使用了工具,说明使用了什么工具以及结果
|
||||
- 如果遇到错误,提供友好的错误信息和解决建议
|
||||
""")
|
||||
|
||||
@UserMessage("""
|
||||
{{query}}
|
||||
""")
|
||||
|
||||
@Agent("通用工具调用智能体")
|
||||
/**
|
||||
* 智能体对外调用入口
|
||||
* @param query 用户的自然语言请求
|
||||
* @return 处理结果
|
||||
*/
|
||||
String callMcpTool(@V("query") String query);
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import dev.langchain4j.service.V;
|
||||
* and returning relevant data and analysis results.
|
||||
*
|
||||
*/
|
||||
public interface SqlAgent extends Agent {
|
||||
public interface SqlAgent {
|
||||
|
||||
@SystemMessage("""
|
||||
This agent is designed for MySQL 5.7
|
||||
|
||||
@@ -10,7 +10,7 @@ import dev.langchain4j.service.V;
|
||||
* A web search assistant that answers natural language questions by searching the internet
|
||||
* and returning relevant information from web pages.
|
||||
*/
|
||||
public interface WebSearchAgent extends Agent {
|
||||
public interface WebSearchAgent {
|
||||
|
||||
@SystemMessage("""
|
||||
You are a web search assistant. Answer questions by searching and retrieving web content.
|
||||
|
||||
@@ -43,7 +43,7 @@ public class ExecuteSqlQueryTool implements BuiltinToolProvider {
|
||||
@Tool("Execute a SELECT SQL query and return the results. Example: SELECT * FROM sys_user")
|
||||
public String executeSql(String sql) {
|
||||
// 2. 手动推入数据源上下文
|
||||
DynamicDataSourceContextHolder.push("agent");
|
||||
// DynamicDataSourceContextHolder.push("agent");
|
||||
if (sql == null || sql.trim().isEmpty()) {
|
||||
return "Error: SQL query cannot be empty";
|
||||
}
|
||||
|
||||
@@ -59,4 +59,37 @@ public class VectorStoreProperties {
|
||||
*/
|
||||
private String collectionname;
|
||||
}
|
||||
|
||||
/**
|
||||
* Qdrant配置
|
||||
*/
|
||||
private Qdrant qdrant = new Qdrant();
|
||||
|
||||
@Data
|
||||
public static class Qdrant {
|
||||
/**
|
||||
* 主机地址
|
||||
*/
|
||||
private String host = "localhost";
|
||||
|
||||
/**
|
||||
* gRPC端口
|
||||
*/
|
||||
private int port = 6334;
|
||||
|
||||
/**
|
||||
* 集合名称
|
||||
*/
|
||||
private String collectionname = "LocalKnowledge";
|
||||
|
||||
/**
|
||||
* API密钥(可选)
|
||||
*/
|
||||
private String apiKey;
|
||||
|
||||
/**
|
||||
* 是否启用TLS
|
||||
*/
|
||||
private boolean useTls = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,8 +30,8 @@ public class ChatController {
|
||||
*/
|
||||
@PostMapping("/send")
|
||||
@ResponseBody
|
||||
public SseEmitter sseChat(@RequestBody @Valid ChatRequest chatRequest, HttpServletRequest request) {
|
||||
return chatService.sseChat(chatRequest,request);
|
||||
public SseEmitter sseChat(@RequestBody @Valid ChatRequest chatRequest) {
|
||||
return chatService.sseChat(chatRequest);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import jakarta.validation.constraints.*;
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import org.ruoyi.common.chat.domain.bo.chat.ChatMessageBo;
|
||||
import org.ruoyi.common.chat.domain.vo.chat.ChatMessageVo;
|
||||
import org.ruoyi.common.chat.service.chatMessage.IChatMessageService;
|
||||
import org.ruoyi.service.chat.IChatMessageService;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.ruoyi.common.idempotent.annotation.RepeatSubmit;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.ruoyi.common.chat.factory;
|
||||
package org.ruoyi.factory;
|
||||
|
||||
import org.ruoyi.common.chat.service.chat.IChatService;
|
||||
import org.ruoyi.service.chat.AbstractChatService;
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationContextAware;
|
||||
@@ -18,13 +19,13 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
@Component
|
||||
public class ChatServiceFactory implements ApplicationContextAware {
|
||||
|
||||
private final Map<String, IChatService> chatServiceMap = new ConcurrentHashMap<>();
|
||||
private final Map<String, AbstractChatService> chatServiceMap = new ConcurrentHashMap<>();
|
||||
|
||||
@Override
|
||||
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
|
||||
// 初始化时收集所有IChatService的实现
|
||||
Map<String, IChatService> serviceMap = applicationContext.getBeansOfType(IChatService.class);
|
||||
for (IChatService service : serviceMap.values()) {
|
||||
Map<String, AbstractChatService> serviceMap = applicationContext.getBeansOfType(AbstractChatService.class);
|
||||
for (AbstractChatService service : serviceMap.values()) {
|
||||
if (service != null ) {
|
||||
chatServiceMap.put(service.getProviderName(), service);
|
||||
}
|
||||
@@ -35,8 +36,8 @@ public class ChatServiceFactory implements ApplicationContextAware {
|
||||
/**
|
||||
* 获取原始服务(不包装代理)
|
||||
*/
|
||||
public IChatService getOriginalService(String category) {
|
||||
IChatService service = chatServiceMap.get(category);
|
||||
public AbstractChatService getOriginalService(String category) {
|
||||
AbstractChatService service = chatServiceMap.get(category);
|
||||
if (service == null) {
|
||||
throw new IllegalArgumentException("不支持的模型类别: " + category);
|
||||
}
|
||||
@@ -2,8 +2,9 @@ package org.ruoyi.factory;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.ruoyi.common.chat.service.chat.IChatModelService;
|
||||
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
|
||||
import org.ruoyi.common.chat.service.chat.IChatModelService;
|
||||
import org.ruoyi.observability.EmbeddingModelListenerProvider;
|
||||
import org.ruoyi.service.embed.BaseEmbedModelService;
|
||||
import org.ruoyi.service.embed.MultiModalEmbedModelService;
|
||||
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
|
||||
@@ -27,6 +28,7 @@ public class EmbeddingModelFactory {
|
||||
private final ApplicationContext applicationContext;
|
||||
|
||||
private final IChatModelService chatModelService;
|
||||
private final EmbeddingModelListenerProvider embeddingModelListenerProvider;
|
||||
|
||||
// 模型缓存,使用ConcurrentHashMap保证线程安全
|
||||
private final Map<String, BaseEmbedModelService> modelCache = new ConcurrentHashMap<>();
|
||||
@@ -109,6 +111,8 @@ public class EmbeddingModelFactory {
|
||||
BaseEmbedModelService model = applicationContext.getBean(factory, BaseEmbedModelService.class);
|
||||
// 配置模型参数
|
||||
model.configure(config);
|
||||
// 增加嵌入模型监听器
|
||||
model.addListeners(embeddingModelListenerProvider.getEmbeddingModelListeners());
|
||||
log.info("成功创建嵌入模型: factory={}, modelId={}", config.getProviderCode(), config.getId());
|
||||
return model;
|
||||
} catch (NoSuchBeanDefinitionException e) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.ruoyi.factory;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.ruoyi.constant.FileTypeConstants;
|
||||
import org.ruoyi.service.knowledge.ResourceLoader;
|
||||
import org.ruoyi.service.knowledge.impl.loader.*;
|
||||
@@ -16,6 +17,7 @@ public class ResourceLoaderFactory {
|
||||
private final ExcelTextSplitter excelTextSplitter;
|
||||
|
||||
public ResourceLoader getLoaderByFileType(String fileType) {
|
||||
fileType = StringUtils.removeStart(fileType, ".");
|
||||
if (FileTypeConstants.isTextFile(fileType)) {
|
||||
return new TextFileLoader(characterTextSplitter);
|
||||
} else if (FileTypeConstants.isWord(fileType)) {
|
||||
|
||||
@@ -7,6 +7,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import org.ruoyi.config.VectorStoreProperties;
|
||||
import org.ruoyi.service.vector.VectorStoreService;
|
||||
import org.ruoyi.service.vector.impl.MilvusVectorStoreStrategy;
|
||||
import org.ruoyi.service.vector.impl.QdrantVectorStoreStrategy;
|
||||
import org.ruoyi.service.vector.impl.WeaviateVectorStoreStrategy;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@@ -27,6 +28,7 @@ public class VectorStoreStrategyFactory {
|
||||
private final VectorStoreProperties vectorStoreProperties;
|
||||
private final WeaviateVectorStoreStrategy weaviateStrategy;
|
||||
private final MilvusVectorStoreStrategy milvusStrategy;
|
||||
private final QdrantVectorStoreStrategy qdrantStrategy;
|
||||
|
||||
private Map<String, VectorStoreService> strategies;
|
||||
|
||||
@@ -35,6 +37,7 @@ public class VectorStoreStrategyFactory {
|
||||
strategies = new HashMap<>();
|
||||
strategies.put("weaviate", weaviateStrategy);
|
||||
strategies.put("milvus", milvusStrategy);
|
||||
strategies.put("qdrant", qdrantStrategy);
|
||||
log.info("向量库策略工厂初始化完成,支持的策略: {}", strategies.keySet());
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package org.ruoyi.observability;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import dev.langchain4j.model.chat.listener.ChatModelListener;
|
||||
import dev.langchain4j.model.embedding.listener.EmbeddingModelListener;
|
||||
import lombok.Getter;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* LangChain4j 监听器共享提供者。
|
||||
* <p>
|
||||
* 供所有 {@link dev.langchain4j.model.chat.StreamingChatModel} 构建器使用,
|
||||
* 将可观测性监听器注入到模型实例中。
|
||||
*
|
||||
* @author evo
|
||||
*/
|
||||
@Component
|
||||
@Getter
|
||||
@Lazy
|
||||
public class ChatModelListenerProvider {
|
||||
|
||||
private final List<ChatModelListener> chatModelListeners;
|
||||
private final List<EmbeddingModelListener> embeddingModelListeners;
|
||||
|
||||
public ChatModelListenerProvider(@Nullable List<ChatModelListener> chatModelListeners,
|
||||
@Nullable List<EmbeddingModelListener> embeddingModelListeners) {
|
||||
if (CollUtil.isEmpty(chatModelListeners)) {
|
||||
chatModelListeners = Collections.emptyList();
|
||||
}
|
||||
if (CollUtil.isEmpty(embeddingModelListeners)) {
|
||||
embeddingModelListeners = Collections.emptyList();
|
||||
}
|
||||
this.chatModelListeners = chatModelListeners;
|
||||
this.embeddingModelListeners = embeddingModelListeners;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package org.ruoyi.observability;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import dev.langchain4j.model.embedding.listener.EmbeddingModelListener;
|
||||
import lombok.Getter;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* EmbeddingModel 监听器共享提供者。
|
||||
* <p>
|
||||
* 供所有 {@link dev.langchain4j.model.embedding.EmbeddingModel} 构建器使用,
|
||||
* 将可观测性监听器注入到模型实例中。
|
||||
*
|
||||
* @author evo
|
||||
*/
|
||||
@Component
|
||||
@Getter
|
||||
@Lazy
|
||||
public class EmbeddingModelListenerProvider {
|
||||
|
||||
private final List<EmbeddingModelListener> embeddingModelListeners;
|
||||
|
||||
public EmbeddingModelListenerProvider(@Nullable List<EmbeddingModelListener> embeddingModelListeners) {
|
||||
if (CollUtil.isEmpty(embeddingModelListeners)) {
|
||||
embeddingModelListeners = Collections.emptyList();
|
||||
}
|
||||
this.embeddingModelListeners = embeddingModelListeners;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
package org.ruoyi.observability;
|
||||
|
||||
import dev.langchain4j.Experimental;
|
||||
import dev.langchain4j.mcp.client.McpClientListener;
|
||||
import dev.langchain4j.model.chat.listener.ChatModelListener;
|
||||
import dev.langchain4j.model.embedding.listener.EmbeddingModelListener;
|
||||
import dev.langchain4j.observability.api.AiServiceListenerRegistrar;
|
||||
import dev.langchain4j.observability.api.listener.*;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* LangChain4j 可观测性配置类。
|
||||
* <p>
|
||||
* 负责注册所有 langchain4j 的监听器:
|
||||
* <ul>
|
||||
* <li>{@link AiServiceListener} - AI服务级别的事件监听器(通过 AiServiceListenerRegistrar 注册)</li>
|
||||
* <li>{@link ChatModelListener} - ChatModel 级别的监听器(注入到模型构建器)</li>
|
||||
* <li>{@link EmbeddingModelListener} - EmbeddingModel 级别的监听器(注入到模型构建器)</li>
|
||||
* </ul>
|
||||
*
|
||||
* @author evo
|
||||
*/
|
||||
@Configuration
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class LangChain4jObservabilityConfig {
|
||||
|
||||
private final AiServiceListenerRegistrar registrar = AiServiceListenerRegistrar.newInstance();
|
||||
|
||||
/**
|
||||
* 注册 AI 服务级别的事件监听器
|
||||
*/
|
||||
@PostConstruct
|
||||
public void registerAiServiceListeners() {
|
||||
log.info("正在注册 LangChain4j AI Service 事件监听器...");
|
||||
registrar.register(
|
||||
new MyAiServiceStartedListener(),
|
||||
new MyAiServiceRequestIssuedListener(),
|
||||
new MyAiServiceResponseReceivedListener(),
|
||||
new MyAiServiceCompletedListener(),
|
||||
new MyAiServiceErrorListener(),
|
||||
new MyInputGuardrailExecutedListener(),
|
||||
new MyOutputGuardrailExecutedListener(),
|
||||
new MyToolExecutedEventListener()
|
||||
);
|
||||
log.info("LangChain4j AI Service 事件监听器注册完成");
|
||||
}
|
||||
|
||||
// ==================== AI Service 监听器 Beans ====================
|
||||
|
||||
@Bean
|
||||
public AiServiceStartedListener aiServiceStartedListener() {
|
||||
return new MyAiServiceStartedListener();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public AiServiceRequestIssuedListener aiServiceRequestIssuedListener() {
|
||||
return new MyAiServiceRequestIssuedListener();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public AiServiceResponseReceivedListener aiServiceResponseReceivedListener() {
|
||||
return new MyAiServiceResponseReceivedListener();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public AiServiceCompletedListener aiServiceCompletedListener() {
|
||||
return new MyAiServiceCompletedListener();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public AiServiceErrorListener aiServiceErrorListener() {
|
||||
return new MyAiServiceErrorListener();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public InputGuardrailExecutedListener inputGuardrailExecutedListener() {
|
||||
return new MyInputGuardrailExecutedListener();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public OutputGuardrailExecutedListener outputGuardrailExecutedListener() {
|
||||
return new MyOutputGuardrailExecutedListener();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ToolExecutedEventListener toolExecutedEventListener() {
|
||||
return new MyToolExecutedEventListener();
|
||||
}
|
||||
|
||||
// ==================== ChatModel 监听器 ====================
|
||||
|
||||
@Bean
|
||||
public ChatModelListener chatModelListener() {
|
||||
return new MyChatModelListener();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public List<ChatModelListener> chatModelListeners() {
|
||||
return List.of(new MyChatModelListener());
|
||||
}
|
||||
|
||||
// ==================== EmbeddingModel 监听器 ====================
|
||||
|
||||
@Bean
|
||||
@Experimental
|
||||
public EmbeddingModelListener embeddingModelListener() {
|
||||
return new MyEmbeddingModelListener();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Experimental
|
||||
public List<EmbeddingModelListener> embeddingModelListeners() {
|
||||
return List.of(new MyEmbeddingModelListener());
|
||||
}
|
||||
|
||||
// ==================== MCP Client 监听器 ====================
|
||||
|
||||
@Bean
|
||||
public McpClientListener mcpClientListener() {
|
||||
return new MyMcpClientListener();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
package org.ruoyi.observability;
|
||||
|
||||
import dev.langchain4j.agentic.observability.AgentInvocationError;
|
||||
import dev.langchain4j.agentic.observability.AgentRequest;
|
||||
import dev.langchain4j.agentic.observability.AgentResponse;
|
||||
import dev.langchain4j.agentic.planner.AgentInstance;
|
||||
import dev.langchain4j.agentic.scope.AgenticScope;
|
||||
import dev.langchain4j.service.tool.BeforeToolExecution;
|
||||
import dev.langchain4j.service.tool.ToolExecution;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
/**
|
||||
* 自定义的 AgentListener 的监听器。
|
||||
* 监听 Agent 相关的所有可观测性事件,包括:
|
||||
* <ul>
|
||||
* <li>Agent 调用前/后的生命周期事件</li>
|
||||
* <li>Agent 执行错误事件</li>
|
||||
* <li>AgenticScope 的创建/销毁事件</li>
|
||||
* <li>工具执行前/后的生命周期事件</li>
|
||||
* </ul>
|
||||
*
|
||||
* @author evo
|
||||
*/
|
||||
@Slf4j
|
||||
public class MyAgentListener implements dev.langchain4j.agentic.observability.AgentListener {
|
||||
|
||||
/** 最终捕获到的思考结果(主 Agent 完成后写入,供外部获取) */
|
||||
private final AtomicReference<String> sharedOutputRef = new AtomicReference<>();
|
||||
|
||||
public String getCapturedResult() {
|
||||
return sharedOutputRef.get();
|
||||
}
|
||||
|
||||
// ==================== Agent 调用生命周期 ====================
|
||||
|
||||
@Override
|
||||
public void beforeAgentInvocation(AgentRequest agentRequest) {
|
||||
AgentInstance agent = agentRequest.agent();
|
||||
AgenticScope scope = agentRequest.agenticScope();
|
||||
Map<String, Object> inputs = agentRequest.inputs();
|
||||
|
||||
log.info("【Agent调用前】Agent名称: {}", agent.name());
|
||||
log.info("【Agent调用前】Agent ID: {}", agent.agentId());
|
||||
log.info("【Agent调用前】Agent类型: {}", agent.type().getName());
|
||||
log.info("【Agent调用前】Agent描述: {}", agent.description());
|
||||
log.info("【Agent调用前】Planner类型: {}", agent.plannerType());
|
||||
log.info("【Agent调用前】输出类型: {}", agent.outputType());
|
||||
log.info("【Agent调用前】输出Key: {}", agent.outputKey());
|
||||
log.info("【Agent调用前】是否为异步: {}", agent.async());
|
||||
log.info("【Agent调用前】是否为叶子节点: {}", agent.leaf());
|
||||
log.info("【Agent调用前】Agent参数列表:");
|
||||
for (var arg : agent.arguments()) {
|
||||
log.info(" - 参数名: {}, 类型: {}, 默认值: {}",
|
||||
arg.name(), arg.rawType().getName(), arg.defaultValue());
|
||||
}
|
||||
log.info("【Agent调用前】Agent输入参数: {}", inputs);
|
||||
log.info("【Agent调用前】AgenticScope memoryId: {}", scope.memoryId());
|
||||
log.info("【Agent调用前】AgenticScope当前状态: {}", scope.state());
|
||||
log.info("【Agent调用前】Agent调用历史记录数: {}", scope.agentInvocations().size());
|
||||
|
||||
// 打印嵌套的子Agent信息
|
||||
if (!agent.subagents().isEmpty()) {
|
||||
log.info("【Agent调用前】子Agent列表:");
|
||||
for (AgentInstance sub : agent.subagents()) {
|
||||
log.info(" - 子Agent: {} ({})", sub.name(), sub.type().getName());
|
||||
}
|
||||
}
|
||||
|
||||
// 打印父Agent信息
|
||||
if (agent.parent() != null) {
|
||||
log.info("【Agent调用前】父Agent: {}", agent.parent().name());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterAgentInvocation(AgentResponse agentResponse) {
|
||||
AgentInstance agent = agentResponse.agent();
|
||||
Map<String, Object> inputs = agentResponse.inputs();
|
||||
Object output = agentResponse.output();
|
||||
String outputStr = output != null ? output.toString() : "";
|
||||
|
||||
log.info("【Agent调用后】Agent名称: {}", agent.name());
|
||||
log.info("【Agent调用后】Agent ID: {}", agent.agentId());
|
||||
log.info("【Agent调用后】Agent输入参数: {}", inputs);
|
||||
log.info("【Agent调用后】Agent输出结果: {}", output);
|
||||
log.info("【Agent调用后】是否为叶子节点: {}", agent.leaf());
|
||||
|
||||
// 捕获主 Agent 的最终输出,供外部获取
|
||||
if ("invoke".equals(agent.agentId()) && !outputStr.isEmpty()) {
|
||||
sharedOutputRef.set(outputStr);
|
||||
log.info("【Agent调用后】已捕获主Agent输出: {}", outputStr);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAgentInvocationError(AgentInvocationError error) {
|
||||
AgentInstance agent = error.agent();
|
||||
Map<String, Object> inputs = error.inputs();
|
||||
Throwable throwable = error.error();
|
||||
|
||||
log.error("【Agent执行错误】Agent名称: {}", agent.name());
|
||||
log.error("【Agent执行错误】Agent ID: {}", agent.agentId());
|
||||
log.error("【Agent执行错误】Agent类型: {}", agent.type().getName());
|
||||
log.error("【Agent执行错误】Agent输入参数: {}", inputs);
|
||||
log.error("【Agent执行错误】错误类型: {}", throwable.getClass().getName());
|
||||
log.error("【Agent执行错误】错误信息: {}", throwable.getMessage(), throwable);
|
||||
}
|
||||
|
||||
// ==================== AgenticScope 生命周期 ====================
|
||||
|
||||
@Override
|
||||
public void afterAgenticScopeCreated(AgenticScope agenticScope) {
|
||||
log.info("【AgenticScope已创建】memoryId: {}", agenticScope.memoryId());
|
||||
log.info("【AgenticScope已创建】初始状态: {}", agenticScope.state());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeAgenticScopeDestroyed(AgenticScope agenticScope) {
|
||||
log.info("【AgenticScope即将销毁】memoryId: {}", agenticScope.memoryId());
|
||||
log.info("【AgenticScope即将销毁】最终状态: {}", agenticScope.state());
|
||||
log.info("【AgenticScope即将销毁】总调用次数: {}", agenticScope.agentInvocations().size());
|
||||
}
|
||||
|
||||
// ==================== 工具执行生命周期 ====================
|
||||
|
||||
@Override
|
||||
public void beforeToolExecution(BeforeToolExecution beforeToolExecution) {
|
||||
var toolRequest = beforeToolExecution.request();
|
||||
log.info("【工具执行前】工具请求ID: {}", toolRequest.id());
|
||||
log.info("【工具执行前】工具名称: {}", toolRequest.name());
|
||||
log.info("【工具执行前】工具参数: {}", toolRequest.arguments());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterToolExecution(ToolExecution toolExecution) {
|
||||
var toolRequest = toolExecution.request();
|
||||
log.info("【工具执行后】工具请求ID: {}", toolRequest.id());
|
||||
log.info("【工具执行后】工具名称: {}", toolRequest.name());
|
||||
log.info("【工具执行后】工具执行结果: {}", toolExecution.result());
|
||||
log.info("【工具执行后】工具执行是否失败: {}", toolExecution.hasFailed());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package org.ruoyi.observability;
|
||||
|
||||
import dev.langchain4j.invocation.InvocationContext;
|
||||
import dev.langchain4j.observability.api.event.AiServiceCompletedEvent;
|
||||
import dev.langchain4j.observability.api.listener.AiServiceCompletedListener;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 自定义的 AiServiceCompletedEvent 的监听器。
|
||||
* 它表示在 AI 服务调用完成时发生的事件。
|
||||
*
|
||||
* @author evo
|
||||
*/
|
||||
@Slf4j
|
||||
public class MyAiServiceCompletedListener implements AiServiceCompletedListener {
|
||||
|
||||
@Override
|
||||
public void onEvent(AiServiceCompletedEvent event) {
|
||||
InvocationContext invocationContext = event.invocationContext();
|
||||
Optional<Object> result = event.result();
|
||||
UUID invocationId = invocationContext.invocationId();
|
||||
String aiServiceInterfaceName = invocationContext.interfaceName();
|
||||
String aiServiceMethodName = invocationContext.methodName();
|
||||
List<Object> aiServiceMethodArgs = invocationContext.methodArguments();
|
||||
Object chatMemoryId = invocationContext.chatMemoryId();
|
||||
Instant eventTimestamp = invocationContext.timestamp();
|
||||
|
||||
log.info("【AI服务完成】调用唯一标识符: {}", invocationId);
|
||||
log.info("【AI服务完成】AI服务接口名: {}", aiServiceInterfaceName);
|
||||
log.info("【AI服务完成】调用的方法名: {}", aiServiceMethodName);
|
||||
log.info("【AI服务完成】AI服务方法参数: {}", aiServiceMethodArgs);
|
||||
log.info("【AI服务完成】聊天记忆ID: {}", chatMemoryId);
|
||||
log.info("【AI服务完成】调用发生的时间: {}", eventTimestamp);
|
||||
log.info("【AI服务完成】调用结果: {}", result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package org.ruoyi.observability;
|
||||
|
||||
import dev.langchain4j.invocation.InvocationContext;
|
||||
import dev.langchain4j.observability.api.event.AiServiceErrorEvent;
|
||||
import dev.langchain4j.observability.api.listener.AiServiceErrorListener;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 自定义的 AiServiceErrorEvent 的监听器。
|
||||
* 它表示在 AI 服务调用失败时发生的事件。
|
||||
*
|
||||
* @author evo
|
||||
*/
|
||||
@Slf4j
|
||||
public class MyAiServiceErrorListener implements AiServiceErrorListener {
|
||||
|
||||
@Override
|
||||
public void onEvent(AiServiceErrorEvent event) {
|
||||
InvocationContext invocationContext = event.invocationContext();
|
||||
UUID invocationId = invocationContext.invocationId();
|
||||
String aiServiceInterfaceName = invocationContext.interfaceName();
|
||||
String aiServiceMethodName = invocationContext.methodName();
|
||||
Throwable error = event.error();
|
||||
|
||||
log.error("【AI服务错误】调用唯一标识符: {}", invocationId);
|
||||
log.error("【AI服务错误】AI服务接口名: {}", aiServiceInterfaceName);
|
||||
log.error("【AI服务错误】调用的方法名: {}", aiServiceMethodName);
|
||||
log.error("【AI服务错误】错误类型: {}", error.getClass().getName());
|
||||
log.error("【AI服务错误】错误信息: {}", error.getMessage(), error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package org.ruoyi.observability;
|
||||
|
||||
import dev.langchain4j.invocation.InvocationContext;
|
||||
import dev.langchain4j.model.chat.request.ChatRequest;
|
||||
import dev.langchain4j.observability.api.event.AiServiceRequestIssuedEvent;
|
||||
import dev.langchain4j.observability.api.listener.AiServiceRequestIssuedListener;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 自定义的 AiServiceRequestIssuedEvent 的监听器。
|
||||
* 它表示在向 LLM 发送请求之前发生的事件。
|
||||
*
|
||||
* @author evo
|
||||
*/
|
||||
@Slf4j
|
||||
public class MyAiServiceRequestIssuedListener implements AiServiceRequestIssuedListener {
|
||||
|
||||
@Override
|
||||
public void onEvent(AiServiceRequestIssuedEvent event) {
|
||||
InvocationContext invocationContext = event.invocationContext();
|
||||
UUID invocationId = invocationContext.invocationId();
|
||||
String aiServiceInterfaceName = invocationContext.interfaceName();
|
||||
String aiServiceMethodName = invocationContext.methodName();
|
||||
ChatRequest request = event.request();
|
||||
|
||||
log.info("【请求已发出】调用唯一标识符: {}", invocationId);
|
||||
log.info("【请求已发出】AI服务接口名: {}", aiServiceInterfaceName);
|
||||
log.info("【请求已发出】调用的方法名: {}", aiServiceMethodName);
|
||||
log.info("【请求已发出】发送给LLM的请求: {}", request);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user