mirror of
https://gitcode.com/ageerle/ruoyi-ai.git
synced 2026-04-12 19:17:20 +00:00
Compare commits
31 Commits
c84d6247b0
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf7b5eac72 | ||
|
|
d602b805bd | ||
|
|
9cf18904bb | ||
|
|
2f39fa0f53 | ||
|
|
d2005cfa48 | ||
|
|
4e38f853f3 | ||
|
|
3cfb185dde | ||
|
|
ef99c540bb | ||
|
|
3071bfd0f9 | ||
|
|
7bb938c145 | ||
|
|
75b21d3633 | ||
|
|
7ed9d8def4 | ||
|
|
63ed7ddb02 | ||
|
|
11696a016d | ||
|
|
1a10104751 | ||
|
|
f95cb17933 | ||
|
|
0687b49542 | ||
|
|
27ad00ac3a | ||
|
|
a8bd4b47a0 | ||
|
|
a59ddf6070 | ||
|
|
797ecbb054 | ||
|
|
b6b78afea9 | ||
|
|
02240f3fd0 | ||
|
|
a916f14efc | ||
|
|
523628ade6 | ||
|
|
2259a2f717 | ||
|
|
8df37274da | ||
|
|
393057ab24 | ||
|
|
ee8c882b6f | ||
|
|
69ec2a33a4 | ||
|
|
1cd8ae1cd9 |
13
README.md
13
README.md
@@ -32,7 +32,7 @@
|
|||||||
| 模块 | 现有能力
|
| 模块 | 现有能力
|
||||||
|:----------:|---
|
|:----------:|---
|
||||||
| **模型管理** | 多模型接入(OpenAI/DeepSeek/通义/智谱)、多模态理解、Coze/DIFY/FastGPT平台集成
|
| **模型管理** | 多模型接入(OpenAI/DeepSeek/通义/智谱)、多模态理解、Coze/DIFY/FastGPT平台集成
|
||||||
| **知识管理** | 本地RAG + 向量库(Milvus/Weaviate) + 文档解析
|
| **知识管理** | 本地RAG + 向量库(Milvus/Weaviate/Qdrant) + 文档解析
|
||||||
| **工具管理** | Mcp协议集成、Skills能力 + 可扩展工具生态
|
| **工具管理** | Mcp协议集成、Skills能力 + 可扩展工具生态
|
||||||
| **流程编排** | 可视化工作流设计器、节点拖拽编排、SSE流式执行,目前已经支持模型调用,邮件发送,人工审核等节点
|
| **流程编排** | 可视化工作流设计器、节点拖拽编排、SSE流式执行,目前已经支持模型调用,邮件发送,人工审核等节点
|
||||||
| **多智能体** | 基于Langchain4j的Agent框架、Supervisor模式编排,支持多种决策模型
|
| **多智能体** | 基于Langchain4j的Agent框架、Supervisor模式编排,支持多种决策模型
|
||||||
@@ -62,12 +62,16 @@
|
|||||||
## 🛠️ 技术架构
|
## 🛠️ 技术架构
|
||||||
|
|
||||||
### 核心框架
|
### 核心框架
|
||||||
- **后端架构**:Spring Boot 4.0 + Spring ai 2.0 + Langchain4j
|
- **后端架构**:Spring Boot 3.5.8 + Langchain4j
|
||||||
- **数据存储**:MySQL 8.0 + Redis + 向量数据库(Milvus/Weaviate)
|
- **数据存储**:MySQL 8.0 + Redis + 向量数据库(Milvus/Weaviate/Qdrant)
|
||||||
- **前端技术**:Vue 3 + Vben Admin + element-plus-x
|
- **前端技术**:Vue 3 + Vben Admin + element-plus-x
|
||||||
- **安全认证**:Sa-Token + JWT 双重保障
|
- **安全认证**:Sa-Token + JWT 双重保障
|
||||||
|
|
||||||
|
|
||||||
|
- **文档处理**:PDF、Word、Excel 解析,图像智能分析
|
||||||
|
- **实时通信**:WebSocket 实时通信,SSE 流式响应
|
||||||
|
- **系统监控**:完善的日志体系、性能监控、服务健康检查
|
||||||
|
|
||||||
## 🐳 Docker 部署
|
## 🐳 Docker 部署
|
||||||
|
|
||||||
本项目提供两种 Docker 部署方式:
|
本项目提供两种 Docker 部署方式:
|
||||||
@@ -218,9 +222,6 @@ docker-compose -f docker-compose-all.yaml restart [服务名]
|
|||||||
算力和模型 API 服务
|
算力和模型 API 服务
|
||||||
- [优云智算](https://www.compshare.cn/?ytag=GPU_YY-gh_ruoyi) - 万卡RTX40系GPU+海内外主流模型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">
|
<div align="center">
|
||||||
|
|||||||
141
README_EN.md
141
README_EN.md
@@ -32,14 +32,13 @@
|
|||||||
|
|
||||||
## ✨ Core Features
|
## ✨ Core Features
|
||||||
|
|
||||||
| Module | Current Capabilities | Extension Direction |
|
| Module | Current Capabilities |
|
||||||
|:---:|---|---|
|
|:---:|---|
|
||||||
| **Model Management** | Multi-model integration (OpenAI/DeepSeek/Tongyi/Zhipu), multi-modal understanding, Coze/DIFY/FastGPT platform integration | Auto mode, fault tolerance |
|
| **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) + Knowledge Graph + Document parsing + Reranking | Audio/video parsing, knowledge source |
|
| **Knowledge Base** | Local RAG + Vector DB (Milvus/Weaviate/Qdrant) + Document parsing |
|
||||||
| **Tool Management** | MCP protocol integration, Skills capability + Extensible tool ecosystem | Tool plugin marketplace, toolAgent auto-loading |
|
| **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 (with RAG) calls, email sending, manual review nodes | More node types |
|
| **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 | Configurable agents |
|
| **Multi-Agent** | Agent framework based on Langchain4j, Supervisor mode orchestration, supports multiple decision models |
|
||||||
| **AI Coding** | Intelligent code analysis, project scaffolding generation, Copilot assistant | Code generation optimization |
|
|
||||||
|
|
||||||
## 🚀 Quick Start
|
## 🚀 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) |
|
| 🛠️ 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
|
### 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) |
|
| 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
|
## 🛠️ Technical Architecture
|
||||||
|
|
||||||
### Core Framework
|
### Core Framework
|
||||||
- **Backend**: Spring Boot 4.0 + Spring AI 2.0 + Langchain4j
|
- **Backend**: Spring Boot 3.5.8 + Langchain4j
|
||||||
- **Data Storage**: MySQL 8.0 + Redis + Vector Databases (Milvus/Weaviate)
|
- **Data Storage**: MySQL 8.0 + Redis + Vector Databases (Milvus/Weaviate/Qdrant)
|
||||||
- **Frontend**: Vue 3 + Vben Admin + element-plus-x
|
- **Frontend**: Vue 3 + Vben Admin + element-plus-x
|
||||||
- **Security**: Sa-Token + JWT dual-layer security
|
- **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
|
## 📚 Documentation
|
||||||
|
|
||||||
Want to learn more about installation, deployment, configuration, and secondary development?
|
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
|
- [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.
|
- [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
|
## 💬 Community Chat
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
**[📱 Join Telegram Group](https://t.me/+LqooQAc5HxRmYmE1)**
|
**[📱 Join Telegram Group](
|
||||||
|
https://t.me/+LqooQAc5HxRmYmE1)**
|
||||||
|
|
||||||
</div>
|
</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.autoconfigure.SpringBootApplication;
|
||||||
import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup;
|
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 class RuoYiAIApplication {
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
|
// killPortProcess(6039);
|
||||||
SpringApplication application = new SpringApplication(RuoYiAIApplication.class);
|
SpringApplication application = new SpringApplication(RuoYiAIApplication.class);
|
||||||
application.setApplicationStartup(new BufferingApplicationStartup(2048));
|
application.setApplicationStartup(new BufferingApplicationStartup(2048));
|
||||||
application.run(args);
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -275,7 +275,7 @@ warm-flow:
|
|||||||
|
|
||||||
# 向量库配置
|
# 向量库配置
|
||||||
vector-store:
|
vector-store:
|
||||||
# 向量存储类型 可选(weaviate/milvus)
|
# 向量存储类型 可选(weaviate/milvus/qdrant)
|
||||||
# 如需修改向量库类型,请修改此配置值!
|
# 如需修改向量库类型,请修改此配置值!
|
||||||
type: milvus
|
type: milvus
|
||||||
# Weaviate配置
|
# Weaviate配置
|
||||||
@@ -287,3 +287,10 @@ vector-store:
|
|||||||
milvus:
|
milvus:
|
||||||
url: http://localhost:19530
|
url: http://localhost:19530
|
||||||
collectionname: LocalKnowledge
|
collectionname: LocalKnowledge
|
||||||
|
# Qdrant配置
|
||||||
|
qdrant:
|
||||||
|
host: localhost
|
||||||
|
port: 6334
|
||||||
|
collectionname: LocalKnowledge
|
||||||
|
api-key:
|
||||||
|
use-tls: false
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ public class WorkflowComponentService extends ServiceImpl<WorkflowComponentMappe
|
|||||||
return baseMapper.selectPage(new Page<>(currentPage, pageSize), wrapper);
|
return baseMapper.selectPage(new Page<>(currentPage, pageSize), wrapper);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Cacheable(cacheNames = WORKFLOW_COMPONENTS)
|
// @Cacheable(cacheNames = WORKFLOW_COMPONENTS)
|
||||||
public List<WorkflowComponent> getAllEnable() {
|
public List<WorkflowComponent> getAllEnable() {
|
||||||
return ChainWrappers.lambdaQueryChain(baseMapper)
|
return ChainWrappers.lambdaQueryChain(baseMapper)
|
||||||
.eq(WorkflowComponent::getIsEnable, true)
|
.eq(WorkflowComponent::getIsEnable, true)
|
||||||
|
|||||||
@@ -91,6 +91,12 @@
|
|||||||
<version>${langchain4j.community.version}</version>
|
<version>${langchain4j.community.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>dev.langchain4j</groupId>
|
||||||
|
<artifactId>langchain4j-qdrant</artifactId>
|
||||||
|
<version>${langchain4j.community.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>dev.langchain4j</groupId>
|
<groupId>dev.langchain4j</groupId>
|
||||||
<artifactId>langchain4j-mcp</artifactId>
|
<artifactId>langchain4j-mcp</artifactId>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import dev.langchain4j.service.UserMessage;
|
|||||||
import dev.langchain4j.service.V;
|
import dev.langchain4j.service.V;
|
||||||
|
|
||||||
|
|
||||||
public interface ChartGenerationAgent extends Agent {
|
public interface ChartGenerationAgent {
|
||||||
|
|
||||||
@SystemMessage("""
|
@SystemMessage("""
|
||||||
You are a chart generation specialist. Your only task is to generate Apache ECharts
|
You are a chart generation specialist. Your only task is to generate Apache ECharts
|
||||||
|
|||||||
@@ -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.
|
* and returning relevant data and analysis results.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
public interface SqlAgent extends Agent {
|
public interface SqlAgent {
|
||||||
|
|
||||||
@SystemMessage("""
|
@SystemMessage("""
|
||||||
This agent is designed for MySQL 5.7
|
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
|
* A web search assistant that answers natural language questions by searching the internet
|
||||||
* and returning relevant information from web pages.
|
* and returning relevant information from web pages.
|
||||||
*/
|
*/
|
||||||
public interface WebSearchAgent extends Agent {
|
public interface WebSearchAgent {
|
||||||
|
|
||||||
@SystemMessage("""
|
@SystemMessage("""
|
||||||
You are a web search assistant. Answer questions by searching and retrieving web content.
|
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")
|
@Tool("Execute a SELECT SQL query and return the results. Example: SELECT * FROM sys_user")
|
||||||
public String executeSql(String sql) {
|
public String executeSql(String sql) {
|
||||||
// 2. 手动推入数据源上下文
|
// 2. 手动推入数据源上下文
|
||||||
DynamicDataSourceContextHolder.push("agent");
|
// DynamicDataSourceContextHolder.push("agent");
|
||||||
if (sql == null || sql.trim().isEmpty()) {
|
if (sql == null || sql.trim().isEmpty()) {
|
||||||
return "Error: SQL query cannot be empty";
|
return "Error: SQL query cannot be empty";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,4 +59,37 @@ public class VectorStoreProperties {
|
|||||||
*/
|
*/
|
||||||
private String collectionname;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ package org.ruoyi.factory;
|
|||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
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.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.BaseEmbedModelService;
|
||||||
import org.ruoyi.service.embed.MultiModalEmbedModelService;
|
import org.ruoyi.service.embed.MultiModalEmbedModelService;
|
||||||
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
|
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
|
||||||
@@ -27,6 +28,7 @@ public class EmbeddingModelFactory {
|
|||||||
private final ApplicationContext applicationContext;
|
private final ApplicationContext applicationContext;
|
||||||
|
|
||||||
private final IChatModelService chatModelService;
|
private final IChatModelService chatModelService;
|
||||||
|
private final EmbeddingModelListenerProvider embeddingModelListenerProvider;
|
||||||
|
|
||||||
// 模型缓存,使用ConcurrentHashMap保证线程安全
|
// 模型缓存,使用ConcurrentHashMap保证线程安全
|
||||||
private final Map<String, BaseEmbedModelService> modelCache = new ConcurrentHashMap<>();
|
private final Map<String, BaseEmbedModelService> modelCache = new ConcurrentHashMap<>();
|
||||||
@@ -109,6 +111,8 @@ public class EmbeddingModelFactory {
|
|||||||
BaseEmbedModelService model = applicationContext.getBean(factory, BaseEmbedModelService.class);
|
BaseEmbedModelService model = applicationContext.getBean(factory, BaseEmbedModelService.class);
|
||||||
// 配置模型参数
|
// 配置模型参数
|
||||||
model.configure(config);
|
model.configure(config);
|
||||||
|
// 增加嵌入模型监听器
|
||||||
|
model.addListeners(embeddingModelListenerProvider.getEmbeddingModelListeners());
|
||||||
log.info("成功创建嵌入模型: factory={}, modelId={}", config.getProviderCode(), config.getId());
|
log.info("成功创建嵌入模型: factory={}, modelId={}", config.getProviderCode(), config.getId());
|
||||||
return model;
|
return model;
|
||||||
} catch (NoSuchBeanDefinitionException e) {
|
} catch (NoSuchBeanDefinitionException e) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package org.ruoyi.factory;
|
package org.ruoyi.factory;
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.ruoyi.constant.FileTypeConstants;
|
import org.ruoyi.constant.FileTypeConstants;
|
||||||
import org.ruoyi.service.knowledge.ResourceLoader;
|
import org.ruoyi.service.knowledge.ResourceLoader;
|
||||||
import org.ruoyi.service.knowledge.impl.loader.*;
|
import org.ruoyi.service.knowledge.impl.loader.*;
|
||||||
@@ -16,6 +17,7 @@ public class ResourceLoaderFactory {
|
|||||||
private final ExcelTextSplitter excelTextSplitter;
|
private final ExcelTextSplitter excelTextSplitter;
|
||||||
|
|
||||||
public ResourceLoader getLoaderByFileType(String fileType) {
|
public ResourceLoader getLoaderByFileType(String fileType) {
|
||||||
|
fileType = StringUtils.removeStart(fileType, ".");
|
||||||
if (FileTypeConstants.isTextFile(fileType)) {
|
if (FileTypeConstants.isTextFile(fileType)) {
|
||||||
return new TextFileLoader(characterTextSplitter);
|
return new TextFileLoader(characterTextSplitter);
|
||||||
} else if (FileTypeConstants.isWord(fileType)) {
|
} else if (FileTypeConstants.isWord(fileType)) {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
import org.ruoyi.config.VectorStoreProperties;
|
import org.ruoyi.config.VectorStoreProperties;
|
||||||
import org.ruoyi.service.vector.VectorStoreService;
|
import org.ruoyi.service.vector.VectorStoreService;
|
||||||
import org.ruoyi.service.vector.impl.MilvusVectorStoreStrategy;
|
import org.ruoyi.service.vector.impl.MilvusVectorStoreStrategy;
|
||||||
|
import org.ruoyi.service.vector.impl.QdrantVectorStoreStrategy;
|
||||||
import org.ruoyi.service.vector.impl.WeaviateVectorStoreStrategy;
|
import org.ruoyi.service.vector.impl.WeaviateVectorStoreStrategy;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
@@ -27,6 +28,7 @@ public class VectorStoreStrategyFactory {
|
|||||||
private final VectorStoreProperties vectorStoreProperties;
|
private final VectorStoreProperties vectorStoreProperties;
|
||||||
private final WeaviateVectorStoreStrategy weaviateStrategy;
|
private final WeaviateVectorStoreStrategy weaviateStrategy;
|
||||||
private final MilvusVectorStoreStrategy milvusStrategy;
|
private final MilvusVectorStoreStrategy milvusStrategy;
|
||||||
|
private final QdrantVectorStoreStrategy qdrantStrategy;
|
||||||
|
|
||||||
private Map<String, VectorStoreService> strategies;
|
private Map<String, VectorStoreService> strategies;
|
||||||
|
|
||||||
@@ -35,6 +37,7 @@ public class VectorStoreStrategyFactory {
|
|||||||
strategies = new HashMap<>();
|
strategies = new HashMap<>();
|
||||||
strategies.put("weaviate", weaviateStrategy);
|
strategies.put("weaviate", weaviateStrategy);
|
||||||
strategies.put("milvus", milvusStrategy);
|
strategies.put("milvus", milvusStrategy);
|
||||||
|
strategies.put("qdrant", qdrantStrategy);
|
||||||
log.info("向量库策略工厂初始化完成,支持的策略: {}", strategies.keySet());
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package org.ruoyi.observability;
|
||||||
|
|
||||||
|
import dev.langchain4j.invocation.InvocationContext;
|
||||||
|
import dev.langchain4j.model.chat.request.ChatRequest;
|
||||||
|
import dev.langchain4j.model.chat.response.ChatResponse;
|
||||||
|
import dev.langchain4j.observability.api.event.AiServiceResponseReceivedEvent;
|
||||||
|
import dev.langchain4j.observability.api.listener.AiServiceResponseReceivedListener;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自定义的 AiServiceResponseReceivedEvent 的监听器。
|
||||||
|
* 它表示在从 LLM 接收到响应时发生的事件。
|
||||||
|
* 在涉及工具或 guardrail 的单个 AI 服务调用期间,可能会被调用多次。
|
||||||
|
*
|
||||||
|
* @author evo
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class MyAiServiceResponseReceivedListener implements AiServiceResponseReceivedListener {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onEvent(AiServiceResponseReceivedEvent event) {
|
||||||
|
InvocationContext invocationContext = event.invocationContext();
|
||||||
|
UUID invocationId = invocationContext.invocationId();
|
||||||
|
String aiServiceInterfaceName = invocationContext.interfaceName();
|
||||||
|
String aiServiceMethodName = invocationContext.methodName();
|
||||||
|
ChatRequest request = event.request();
|
||||||
|
ChatResponse response = event.response();
|
||||||
|
|
||||||
|
log.info("【响应已接收】调用唯一标识符: {}", invocationId);
|
||||||
|
log.info("【响应已接收】AI服务接口名: {}", aiServiceInterfaceName);
|
||||||
|
log.info("【响应已接收】调用的方法名: {}", aiServiceMethodName);
|
||||||
|
log.info("【响应已接收】发送给LLM的请求: {}", request);
|
||||||
|
log.info("【响应已接收】从LLM收到的响应: {}", response);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package org.ruoyi.observability;
|
||||||
|
|
||||||
|
import dev.langchain4j.data.message.ChatMessage;
|
||||||
|
import dev.langchain4j.data.message.SystemMessage;
|
||||||
|
import dev.langchain4j.data.message.UserMessage;
|
||||||
|
import dev.langchain4j.invocation.InvocationContext;
|
||||||
|
import dev.langchain4j.observability.api.event.AiServiceStartedEvent;
|
||||||
|
import dev.langchain4j.observability.api.listener.AiServiceStartedListener;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自定义的 AiServiceStartedEvent 的监听器。
|
||||||
|
* 它表示在 AI 服务调用开始时发生的事件。
|
||||||
|
*
|
||||||
|
* @author evo
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class MyAiServiceStartedListener implements AiServiceStartedListener {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onEvent(AiServiceStartedEvent event) {
|
||||||
|
InvocationContext invocationContext = event.invocationContext();
|
||||||
|
UUID invocationId = invocationContext.invocationId();
|
||||||
|
String aiServiceInterfaceName = invocationContext.interfaceName();
|
||||||
|
String aiServiceMethodName = invocationContext.methodName();
|
||||||
|
Optional<SystemMessage> systemMessage = event.systemMessage();
|
||||||
|
UserMessage userMessage = event.userMessage();
|
||||||
|
|
||||||
|
log.info("【AI服务启动】调用唯一标识符: {}", invocationId);
|
||||||
|
log.info("【AI服务启动】AI服务接口名: {}", aiServiceInterfaceName);
|
||||||
|
log.info("【AI服务启动】调用的方法名: {}", aiServiceMethodName);
|
||||||
|
log.info("【AI服务启动】系统消息: {}", systemMessage.orElse(null));
|
||||||
|
log.info("【AI服务启动】用户消息: {}", userMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package org.ruoyi.observability;
|
||||||
|
|
||||||
|
import dev.langchain4j.model.chat.listener.ChatModelErrorContext;
|
||||||
|
import dev.langchain4j.model.chat.listener.ChatModelListener;
|
||||||
|
import dev.langchain4j.model.chat.listener.ChatModelRequestContext;
|
||||||
|
import dev.langchain4j.model.chat.listener.ChatModelResponseContext;
|
||||||
|
import dev.langchain4j.model.chat.request.ChatRequest;
|
||||||
|
import dev.langchain4j.model.chat.response.ChatResponse;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自定义的 ChatModelListener 的监听器。
|
||||||
|
* 它监听 ChatModel 的请求、响应和错误事件。
|
||||||
|
*
|
||||||
|
* @author evo
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class MyChatModelListener implements ChatModelListener {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRequest(ChatModelRequestContext requestContext) {
|
||||||
|
ChatRequest request = requestContext.chatRequest();
|
||||||
|
log.info("【ChatModel请求】发送给模型的请求: {}", request);
|
||||||
|
log.info("【ChatModel请求】模型提供商: {}", requestContext.modelProvider());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onResponse(ChatModelResponseContext responseContext) {
|
||||||
|
ChatRequest request = responseContext.chatRequest();
|
||||||
|
ChatResponse response = responseContext.chatResponse();
|
||||||
|
log.info("【ChatModel响应】原始请求: {}", request);
|
||||||
|
log.info("【ChatModel响应】收到的响应: {}", response);
|
||||||
|
log.info("【ChatModel响应】模型提供商: {}", responseContext.modelProvider());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(ChatModelErrorContext errorContext) {
|
||||||
|
log.error("【ChatModel错误】错误类型: {}", errorContext.error().getClass().getName());
|
||||||
|
log.error("【ChatModel错误】错误信息: {}", errorContext.error().getMessage());
|
||||||
|
log.error("【ChatModel错误】原始请求: {}", errorContext.chatRequest());
|
||||||
|
log.error("【ChatModel错误】模型提供商: {}", errorContext.modelProvider());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package org.ruoyi.observability;
|
||||||
|
|
||||||
|
import dev.langchain4j.Experimental;
|
||||||
|
import dev.langchain4j.data.embedding.Embedding;
|
||||||
|
import dev.langchain4j.model.embedding.listener.EmbeddingModelErrorContext;
|
||||||
|
import dev.langchain4j.model.embedding.listener.EmbeddingModelListener;
|
||||||
|
import dev.langchain4j.model.embedding.listener.EmbeddingModelRequestContext;
|
||||||
|
import dev.langchain4j.model.embedding.listener.EmbeddingModelResponseContext;
|
||||||
|
import dev.langchain4j.model.output.Response;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自定义的 EmbeddingModelListener 的监听器。
|
||||||
|
* 它监听 EmbeddingModel 的请求、响应和错误事件。
|
||||||
|
*
|
||||||
|
* @author evo
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Experimental
|
||||||
|
public class MyEmbeddingModelListener implements EmbeddingModelListener {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRequest(EmbeddingModelRequestContext requestContext) {
|
||||||
|
log.info("【EmbeddingModel请求】输入文本段落数量: {}", requestContext.textSegments().size());
|
||||||
|
log.info("【EmbeddingModel请求】嵌入模型: {}", requestContext.embeddingModel());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onResponse(EmbeddingModelResponseContext responseContext) {
|
||||||
|
Response<List<Embedding>> response = responseContext.response();
|
||||||
|
List<Embedding> embeddings = response.content();
|
||||||
|
log.info("【EmbeddingModel响应】嵌入向量数量: {}", embeddings.size());
|
||||||
|
log.info("【EmbeddingModel响应】嵌入维度: {}", embeddings.isEmpty() ? 0 : embeddings.get(0).dimension());
|
||||||
|
log.info("【EmbeddingModel响应】嵌入模型: {}", responseContext.embeddingModel());
|
||||||
|
log.info("【EmbeddingModel响应】输入文本段落: {}", responseContext.textSegments());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(EmbeddingModelErrorContext errorContext) {
|
||||||
|
log.error("【EmbeddingModel错误】错误类型: {}", errorContext.error().getClass().getName());
|
||||||
|
log.error("【EmbeddingModel错误】错误信息: {}", errorContext.error().getMessage());
|
||||||
|
log.error("【EmbeddingModel错误】输入文本段落数量: {}", errorContext.textSegments().size());
|
||||||
|
log.error("【EmbeddingModel错误】嵌入模型: {}", errorContext.embeddingModel());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package org.ruoyi.observability;
|
||||||
|
|
||||||
|
import dev.langchain4j.data.message.UserMessage;
|
||||||
|
import dev.langchain4j.guardrail.InputGuardrail;
|
||||||
|
import dev.langchain4j.guardrail.InputGuardrailRequest;
|
||||||
|
import dev.langchain4j.guardrail.InputGuardrailResult;
|
||||||
|
import dev.langchain4j.invocation.InvocationContext;
|
||||||
|
import dev.langchain4j.observability.api.event.InputGuardrailExecutedEvent;
|
||||||
|
import dev.langchain4j.observability.api.listener.InputGuardrailExecutedListener;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自定义的 InputGuardrailExecutedEvent 的监听器。
|
||||||
|
* 它表示在输入 guardrail 验证执行时发生的事件。
|
||||||
|
*
|
||||||
|
* @author evo
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class MyInputGuardrailExecutedListener implements InputGuardrailExecutedListener {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onEvent(InputGuardrailExecutedEvent event) {
|
||||||
|
InvocationContext invocationContext = event.invocationContext();
|
||||||
|
UUID invocationId = invocationContext.invocationId();
|
||||||
|
String aiServiceInterfaceName = invocationContext.interfaceName();
|
||||||
|
String aiServiceMethodName = invocationContext.methodName();
|
||||||
|
InputGuardrailRequest request = event.request();
|
||||||
|
InputGuardrailResult result = event.result();
|
||||||
|
Class<InputGuardrail> guardrailClass = event.guardrailClass();
|
||||||
|
Duration duration = event.duration();
|
||||||
|
UserMessage rewrittenUserMessage = event.rewrittenUserMessage();
|
||||||
|
|
||||||
|
log.info("【输入Guardrail已执行】调用唯一标识符: {}", invocationId);
|
||||||
|
log.info("【输入Guardrail已执行】AI服务接口名: {}", aiServiceInterfaceName);
|
||||||
|
log.info("【输入Guardrail已执行】调用的方法名: {}", aiServiceMethodName);
|
||||||
|
log.info("【输入Guardrail已执行】Guardrail类名: {}", guardrailClass.getName());
|
||||||
|
log.info("【输入Guardrail已执行】输入Guardrail请求: {}", request);
|
||||||
|
log.info("【输入Guardrail已执行】输入Guardrail结果: {}", result);
|
||||||
|
log.info("【输入Guardrail已执行】重写后的用户消息: {}", rewrittenUserMessage);
|
||||||
|
log.info("【输入Guardrail已执行】执行耗时: {}ms", duration.toMillis());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
package org.ruoyi.observability;
|
||||||
|
|
||||||
|
import dev.langchain4j.invocation.InvocationContext;
|
||||||
|
import dev.langchain4j.mcp.client.McpCallContext;
|
||||||
|
import dev.langchain4j.mcp.client.McpClientListener;
|
||||||
|
import dev.langchain4j.mcp.client.McpGetPromptResult;
|
||||||
|
import dev.langchain4j.mcp.client.McpReadResourceResult;
|
||||||
|
import dev.langchain4j.mcp.protocol.McpClientMessage;
|
||||||
|
import dev.langchain4j.service.tool.ToolExecutionResult;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自定义的 McpClientListener 的监听器。
|
||||||
|
* 监听 MCP 客户端相关的所有可观测性事件,包括:
|
||||||
|
* <ul>
|
||||||
|
* <li>MCP 工具执行的开始/成功/错误事件</li>
|
||||||
|
* <li>MCP 资源读取的开始/成功/错误事件</li>
|
||||||
|
* <li>MCP 提示词获取的开始/成功/错误事件</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @author evo
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class MyMcpClientListener implements McpClientListener {
|
||||||
|
|
||||||
|
// ==================== 工具执行 ====================
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void beforeExecuteTool(McpCallContext context) {
|
||||||
|
InvocationContext invocationContext = context.invocationContext();
|
||||||
|
McpClientMessage message = context.message();
|
||||||
|
|
||||||
|
log.info("【MCP工具执行前】调用唯一标识符: {}", invocationContext.invocationId());
|
||||||
|
log.info("【MCP工具执行前】MCP消息ID: {}", message.getId());
|
||||||
|
log.info("【MCP工具执行前】MCP方法: {}", message.method);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterExecuteTool(McpCallContext context, ToolExecutionResult result, Map<String, Object> rawResult) {
|
||||||
|
InvocationContext invocationContext = context.invocationContext();
|
||||||
|
McpClientMessage message = context.message();
|
||||||
|
|
||||||
|
log.info("【MCP工具执行后】调用唯一标识符: {}", invocationContext.invocationId());
|
||||||
|
log.info("【MCP工具执行后】MCP消息ID: {}", message.getId());
|
||||||
|
log.info("【MCP工具执行后】MCP方法: {}", message.method);
|
||||||
|
log.info("【MCP工具执行后】工具执行结果: {}", result);
|
||||||
|
log.info("【MCP工具执行后】原始结果: {}", rawResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onExecuteToolError(McpCallContext context, Throwable error) {
|
||||||
|
InvocationContext invocationContext = context.invocationContext();
|
||||||
|
McpClientMessage message = context.message();
|
||||||
|
|
||||||
|
log.error("【MCP工具执行错误】调用唯一标识符: {}", invocationContext.invocationId());
|
||||||
|
log.error("【MCP工具执行错误】MCP消息ID: {}", message.getId());
|
||||||
|
log.error("【MCP工具执行错误】MCP方法: {}", message.method);
|
||||||
|
log.error("【MCP工具执行错误】错误类型: {}", error.getClass().getName());
|
||||||
|
log.error("【MCP工具执行错误】错误信息: {}", error.getMessage(), error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 资源读取 ====================
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void beforeResourceGet(McpCallContext context) {
|
||||||
|
InvocationContext invocationContext = context.invocationContext();
|
||||||
|
McpClientMessage message = context.message();
|
||||||
|
|
||||||
|
log.info("【MCP资源读取前】调用唯一标识符: {}", invocationContext.invocationId());
|
||||||
|
log.info("【MCP资源读取前】MCP消息ID: {}", message.getId());
|
||||||
|
log.info("【MCP资源读取前】MCP方法: {}", message.method);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterResourceGet(McpCallContext context, McpReadResourceResult result, Map<String, Object> rawResult) {
|
||||||
|
InvocationContext invocationContext = context.invocationContext();
|
||||||
|
McpClientMessage message = context.message();
|
||||||
|
|
||||||
|
log.info("【MCP资源读取后】调用唯一标识符: {}", invocationContext.invocationId());
|
||||||
|
log.info("【MCP资源读取后】MCP消息ID: {}", message.getId());
|
||||||
|
log.info("【MCP资源读取后】MCP方法: {}", message.method);
|
||||||
|
log.info("【MCP资源读取后】资源内容数量: {}", result.contents() != null ? result.contents().size() : 0);
|
||||||
|
log.info("【MCP资源读取后】原始结果: {}", rawResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onResourceGetError(McpCallContext context, Throwable error) {
|
||||||
|
InvocationContext invocationContext = context.invocationContext();
|
||||||
|
McpClientMessage message = context.message();
|
||||||
|
|
||||||
|
log.error("【MCP资源读取错误】调用唯一标识符: {}", invocationContext.invocationId());
|
||||||
|
log.error("【MCP资源读取错误】MCP消息ID: {}", message.getId());
|
||||||
|
log.error("【MCP资源读取错误】MCP方法: {}", message.method);
|
||||||
|
log.error("【MCP资源读取错误】错误类型: {}", error.getClass().getName());
|
||||||
|
log.error("【MCP资源读取错误】错误信息: {}", error.getMessage(), error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 提示词获取 ====================
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void beforePromptGet(McpCallContext context) {
|
||||||
|
InvocationContext invocationContext = context.invocationContext();
|
||||||
|
McpClientMessage message = context.message();
|
||||||
|
|
||||||
|
log.info("【MCP提示词获取前】调用唯一标识符: {}", invocationContext.invocationId());
|
||||||
|
log.info("【MCP提示词获取前】MCP消息ID: {}", message.getId());
|
||||||
|
log.info("【MCP提示词获取前】MCP方法: {}", message.method);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterPromptGet(McpCallContext context, McpGetPromptResult result, Map<String, Object> rawResult) {
|
||||||
|
InvocationContext invocationContext = context.invocationContext();
|
||||||
|
McpClientMessage message = context.message();
|
||||||
|
|
||||||
|
log.info("【MCP提示词获取后】调用唯一标识符: {}", invocationContext.invocationId());
|
||||||
|
log.info("【MCP提示词获取后】MCP消息ID: {}", message.getId());
|
||||||
|
log.info("【MCP提示词获取后】MCP方法: {}", message.method);
|
||||||
|
log.info("【MCP提示词获取后】提示词描述: {}", result.description());
|
||||||
|
log.info("【MCP提示词获取后】提示词消息数量: {}", result.messages() != null ? result.messages().size() : 0);
|
||||||
|
log.info("【MCP提示词获取后】原始结果: {}", rawResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPromptGetError(McpCallContext context, Throwable error) {
|
||||||
|
InvocationContext invocationContext = context.invocationContext();
|
||||||
|
McpClientMessage message = context.message();
|
||||||
|
|
||||||
|
log.error("【MCP提示词获取错误】调用唯一标识符: {}", invocationContext.invocationId());
|
||||||
|
log.error("【MCP提示词获取错误】MCP消息ID: {}", message.getId());
|
||||||
|
log.error("【MCP提示词获取错误】MCP方法: {}", message.method);
|
||||||
|
log.error("【MCP提示词获取错误】错误类型: {}", error.getClass().getName());
|
||||||
|
log.error("【MCP提示词获取错误】错误信息: {}", error.getMessage(), error);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package org.ruoyi.observability;
|
||||||
|
|
||||||
|
import dev.langchain4j.guardrail.OutputGuardrail;
|
||||||
|
import dev.langchain4j.guardrail.OutputGuardrailRequest;
|
||||||
|
import dev.langchain4j.guardrail.OutputGuardrailResult;
|
||||||
|
import dev.langchain4j.invocation.InvocationContext;
|
||||||
|
import dev.langchain4j.observability.api.event.OutputGuardrailExecutedEvent;
|
||||||
|
import dev.langchain4j.observability.api.listener.OutputGuardrailExecutedListener;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自定义的 OutputGuardrailExecutedEvent 的监听器。
|
||||||
|
* 它表示在输出 guardrail 验证执行时发生的事件。
|
||||||
|
*
|
||||||
|
* @author evo
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class MyOutputGuardrailExecutedListener implements OutputGuardrailExecutedListener {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onEvent(OutputGuardrailExecutedEvent event) {
|
||||||
|
InvocationContext invocationContext = event.invocationContext();
|
||||||
|
UUID invocationId = invocationContext.invocationId();
|
||||||
|
String aiServiceInterfaceName = invocationContext.interfaceName();
|
||||||
|
String aiServiceMethodName = invocationContext.methodName();
|
||||||
|
OutputGuardrailRequest request = event.request();
|
||||||
|
OutputGuardrailResult result = event.result();
|
||||||
|
Class<OutputGuardrail> guardrailClass = event.guardrailClass();
|
||||||
|
Duration duration = event.duration();
|
||||||
|
|
||||||
|
log.info("【输出Guardrail已执行】调用唯一标识符: {}", invocationId);
|
||||||
|
log.info("【输出Guardrail已执行】AI服务接口名: {}", aiServiceInterfaceName);
|
||||||
|
log.info("【输出Guardrail已执行】调用的方法名: {}", aiServiceMethodName);
|
||||||
|
log.info("【输出Guardrail已执行】Guardrail类名: {}", guardrailClass.getName());
|
||||||
|
log.info("【输出Guardrail已执行】输出Guardrail请求: {}", request);
|
||||||
|
log.info("【输出Guardrail已执行】输出Guardrail结果: {}", result);
|
||||||
|
log.info("【输出Guardrail已执行】执行耗时: {}ms", duration.toMillis());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package org.ruoyi.observability;
|
||||||
|
|
||||||
|
import dev.langchain4j.agent.tool.ToolExecutionRequest;
|
||||||
|
import dev.langchain4j.invocation.InvocationContext;
|
||||||
|
import dev.langchain4j.observability.api.event.ToolExecutedEvent;
|
||||||
|
import dev.langchain4j.observability.api.listener.ToolExecutedEventListener;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自定义的 ToolExecutedEvent 的监听器。
|
||||||
|
* 它表示在工具执行完成后发生的事件。
|
||||||
|
* 在单个 AI 服务调用期间,可能会被调用多次。
|
||||||
|
*
|
||||||
|
* @author evo
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class MyToolExecutedEventListener implements ToolExecutedEventListener {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onEvent(ToolExecutedEvent event) {
|
||||||
|
InvocationContext invocationContext = event.invocationContext();
|
||||||
|
UUID invocationId = invocationContext.invocationId();
|
||||||
|
String aiServiceInterfaceName = invocationContext.interfaceName();
|
||||||
|
String aiServiceMethodName = invocationContext.methodName();
|
||||||
|
ToolExecutionRequest request = event.request();
|
||||||
|
String resultText = event.resultText();
|
||||||
|
|
||||||
|
log.info("【工具已执行】调用唯一标识符: {}", invocationId);
|
||||||
|
log.info("【工具已执行】AI服务接口名: {}", aiServiceInterfaceName);
|
||||||
|
log.info("【工具已执行】调用的方法名: {}", aiServiceMethodName);
|
||||||
|
log.info("【工具已执行】工具执行请求 ID: {}", request.id());
|
||||||
|
log.info("【工具已执行】工具名称: {}", request.name());
|
||||||
|
log.info("【工具已执行】工具参数: {}", request.arguments());
|
||||||
|
log.info("【工具已执行】工具执行结果: {}", resultText);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
//package org.ruoyi.service.chat.handler;
|
|
||||||
//
|
|
||||||
//import dev.langchain4j.agentic.AgenticServices;
|
|
||||||
//import dev.langchain4j.community.model.dashscope.QwenChatModel;
|
|
||||||
//import dev.langchain4j.service.tool.ToolProvider;
|
|
||||||
//import lombok.RequiredArgsConstructor;
|
|
||||||
//import lombok.extern.slf4j.Slf4j;
|
|
||||||
//import org.ruoyi.agent.McpAgent;
|
|
||||||
//import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
|
|
||||||
//import org.ruoyi.common.chat.entity.chat.ChatContext;
|
|
||||||
//import org.ruoyi.common.chat.service.chatMessage.IChatMessageService;
|
|
||||||
//import org.ruoyi.common.sse.utils.SseMessageUtils;
|
|
||||||
//import org.ruoyi.mcp.service.core.ToolProviderFactory;
|
|
||||||
//import org.springframework.core.annotation.Order;
|
|
||||||
//import org.springframework.stereotype.Component;
|
|
||||||
//import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
|
||||||
//
|
|
||||||
//import java.util.ArrayList;
|
|
||||||
//import java.util.List;
|
|
||||||
//
|
|
||||||
///**
|
|
||||||
// * Agent 深度思考处理器
|
|
||||||
// * <p>
|
|
||||||
// * 处理 enableThinking=true 的场景,使用 Agent 进行深度思考和工具调用
|
|
||||||
// *
|
|
||||||
// * @author ageerle@163.com
|
|
||||||
// * @date 2025/12/13
|
|
||||||
// */
|
|
||||||
//@Slf4j
|
|
||||||
//@Component
|
|
||||||
//@Order(3)
|
|
||||||
//@RequiredArgsConstructor
|
|
||||||
//public class AgentChatHandler implements ChatHandler {
|
|
||||||
//
|
|
||||||
// private final ToolProviderFactory toolProviderFactory;
|
|
||||||
//
|
|
||||||
// @Override
|
|
||||||
// public boolean supports(ChatContext context) {
|
|
||||||
// Boolean enableThinking = context.getChatRequest().getEnableThinking();
|
|
||||||
// return enableThinking != null && enableThinking;
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// @Override
|
|
||||||
// public SseEmitter handle(ChatContext context) {
|
|
||||||
// log.info("处理 Agent 深度思考,用户: {}", context.getUserId());
|
|
||||||
//
|
|
||||||
// Long userId = context.getUserId();
|
|
||||||
// String tokenValue = context.getTokenValue();
|
|
||||||
// ChatModelVo chatModelVo = context.getChatModelVo();
|
|
||||||
//
|
|
||||||
// try {
|
|
||||||
// // 1. 保存用户消息
|
|
||||||
// String content = extractUserContent(context);
|
|
||||||
//// saveChatMessage(context.getChatRequest(), userId, content,
|
|
||||||
//// RoleType.USER.getName(), chatModelVo);
|
|
||||||
//
|
|
||||||
// // 2. 执行 Agent 任务
|
|
||||||
// String result = doAgent(content, chatModelVo);
|
|
||||||
//
|
|
||||||
// // 3. 发送结果并保存
|
|
||||||
// SseMessageUtils.sendMessage(userId, result);
|
|
||||||
// SseMessageUtils.completeConnection(userId, tokenValue);
|
|
||||||
//
|
|
||||||
//// saveChatMessage(context.getChatRequest(), userId, result,
|
|
||||||
//// RoleType.ASSISTANT.getName(), chatModelVo);
|
|
||||||
// // todo 保存消息
|
|
||||||
// } catch (Exception e) {
|
|
||||||
// log.error("Agent 执行失败: {}", e.getMessage(), e);
|
|
||||||
// SseMessageUtils.sendMessage(userId, "Agent 执行失败:" + e.getMessage());
|
|
||||||
// SseMessageUtils.completeConnection(userId, tokenValue);
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// return context.getEmitter();
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// /**
|
|
||||||
// * 执行 Agent 任务
|
|
||||||
// */
|
|
||||||
// private String doAgent(String userMessage, ChatModelVo chatModelVo) {
|
|
||||||
// log.info("执行 Agent 任务,消息: {}", userMessage);
|
|
||||||
//
|
|
||||||
// try {
|
|
||||||
// // 1. 加载 LLM 模型
|
|
||||||
// QwenChatModel qwenChatModel = QwenChatModel.builder()
|
|
||||||
// .apiKey(chatModelVo.getApiKey())
|
|
||||||
// .modelName(chatModelVo.getModelName())
|
|
||||||
// .build();
|
|
||||||
//
|
|
||||||
// // 2. 获取内置工具
|
|
||||||
// List<Object> builtinTools = toolProviderFactory.getAllBuiltinToolObjects();
|
|
||||||
// List<Object> allTools = new ArrayList<>(builtinTools);
|
|
||||||
// log.debug("加载 {} 个内置工具", builtinTools.size());
|
|
||||||
//
|
|
||||||
// // 3. 获取 MCP 工具提供者
|
|
||||||
// ToolProvider mcpToolProvider = toolProviderFactory.getAllEnabledMcpToolsProvider();
|
|
||||||
//
|
|
||||||
// // 4. 创建 MCP Agent
|
|
||||||
// var agentBuilder = AgenticServices.agentBuilder(McpAgent.class)
|
|
||||||
// .chatModel(qwenChatModel);
|
|
||||||
//
|
|
||||||
// if (!allTools.isEmpty()) {
|
|
||||||
// agentBuilder.tools(allTools.toArray(new Object[0]));
|
|
||||||
// }
|
|
||||||
// if (mcpToolProvider != null) {
|
|
||||||
// agentBuilder.toolProvider(mcpToolProvider);
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// McpAgent mcpAgent = agentBuilder.build();
|
|
||||||
//
|
|
||||||
// // 5. 调用 Agent
|
|
||||||
// String result = mcpAgent.callMcpTool(userMessage);
|
|
||||||
// log.info("Agent 执行完成,结果长度: {}", result.length());
|
|
||||||
// return result;
|
|
||||||
//
|
|
||||||
// } catch (Exception e) {
|
|
||||||
// log.error("Agent 模式执行失败: {}", e.getMessage(), e);
|
|
||||||
// return "Agent 执行失败: " + e.getMessage();
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// /**
|
|
||||||
// * 提取用户消息内容
|
|
||||||
// */
|
|
||||||
// private String extractUserContent(ChatContext context) {
|
|
||||||
// var messages = context.getChatRequest().getMessages();
|
|
||||||
// if (messages != null && !messages.isEmpty()) {
|
|
||||||
// return messages.get(0).getContent();
|
|
||||||
// }
|
|
||||||
// return "";
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
//}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
//package org.ruoyi.service.chat.handler;
|
|
||||||
//
|
|
||||||
//import lombok.RequiredArgsConstructor;
|
|
||||||
//import lombok.extern.slf4j.Slf4j;
|
|
||||||
//import org.ruoyi.common.chat.domain.dto.request.ReSumeRunner;
|
|
||||||
//import org.ruoyi.common.chat.entity.chat.ChatContext;
|
|
||||||
//import org.ruoyi.common.chat.service.workFlow.IWorkFlowStarterService;
|
|
||||||
//import org.ruoyi.common.core.utils.ObjectUtils;
|
|
||||||
//import org.springframework.core.annotation.Order;
|
|
||||||
//import org.springframework.stereotype.Component;
|
|
||||||
//import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
|
||||||
//
|
|
||||||
///**
|
|
||||||
// * 人机交互恢复处理器
|
|
||||||
// * <p>
|
|
||||||
// * 处理 isResume=true 的场景,恢复工作流的人机交互
|
|
||||||
// *
|
|
||||||
// * @author ageerle@163.com
|
|
||||||
// * @date 2025/12/13
|
|
||||||
// */
|
|
||||||
//@Slf4j
|
|
||||||
//@Component
|
|
||||||
//@Order(1)
|
|
||||||
//@RequiredArgsConstructor
|
|
||||||
//public class ResumeChatHandler implements ChatHandler {
|
|
||||||
//
|
|
||||||
// private final IWorkFlowStarterService workFlowStarterService;
|
|
||||||
//
|
|
||||||
// @Override
|
|
||||||
// public boolean supports(ChatContext context) {
|
|
||||||
// Boolean isResume = context.getChatRequest().getIsResume();
|
|
||||||
// return isResume != null && isResume;
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// @Override
|
|
||||||
// public SseEmitter handle(ChatContext context) {
|
|
||||||
// log.info("处理人机交互恢复,用户: {}", context.getUserId());
|
|
||||||
//
|
|
||||||
// ReSumeRunner reSumeRunner = context.getChatRequest().getReSumeRunner();
|
|
||||||
// if (ObjectUtils.isEmpty(reSumeRunner)) {
|
|
||||||
// log.warn("人机交互恢复参数为空");
|
|
||||||
// return context.getEmitter();
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// workFlowStarterService.resumeFlow(
|
|
||||||
// reSumeRunner.getRuntimeUuid(),
|
|
||||||
// reSumeRunner.getFeedbackContent(),
|
|
||||||
// context.getEmitter()
|
|
||||||
// );
|
|
||||||
//
|
|
||||||
// return context.getEmitter();
|
|
||||||
// }
|
|
||||||
//}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
//package org.ruoyi.service.chat.handler;
|
|
||||||
//
|
|
||||||
//import lombok.RequiredArgsConstructor;
|
|
||||||
//import lombok.extern.slf4j.Slf4j;
|
|
||||||
//import org.ruoyi.common.chat.base.ThreadContext;
|
|
||||||
//import org.ruoyi.common.chat.domain.dto.request.WorkFlowRunner;
|
|
||||||
//import org.ruoyi.common.chat.entity.chat.ChatContext;
|
|
||||||
//import org.ruoyi.common.chat.service.workFlow.IWorkFlowStarterService;
|
|
||||||
//import org.ruoyi.common.core.utils.ObjectUtils;
|
|
||||||
//import org.springframework.core.annotation.Order;
|
|
||||||
//import org.springframework.stereotype.Component;
|
|
||||||
//import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
|
||||||
//
|
|
||||||
///**
|
|
||||||
// * 工作流对话处理器
|
|
||||||
// * <p>
|
|
||||||
// * 处理 enableWorkFlow=true 的场景,启动工作流对话
|
|
||||||
// *
|
|
||||||
// * @author ageerle@163.com
|
|
||||||
// * @date 2025/12/13
|
|
||||||
// */
|
|
||||||
//@Slf4j
|
|
||||||
//@Component
|
|
||||||
//@Order(2)
|
|
||||||
//@RequiredArgsConstructor
|
|
||||||
//public class WorkflowChatHandler implements ChatHandler {
|
|
||||||
//
|
|
||||||
// private final IWorkFlowStarterService workFlowStarterService;
|
|
||||||
//
|
|
||||||
// @Override
|
|
||||||
// public boolean supports(ChatContext context) {
|
|
||||||
// Boolean enableWorkFlow = context.getChatRequest().getEnableWorkFlow();
|
|
||||||
// return enableWorkFlow != null && enableWorkFlow;
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// @Override
|
|
||||||
// public SseEmitter handle(ChatContext context) {
|
|
||||||
// log.info("处理工作流对话,用户: {}, 会话: {}",
|
|
||||||
// context.getUserId(), context.getChatRequest().getSessionId());
|
|
||||||
//
|
|
||||||
// WorkFlowRunner runner = context.getChatRequest().getWorkFlowRunner();
|
|
||||||
// if (ObjectUtils.isEmpty(runner)) {
|
|
||||||
// log.warn("工作流参数为空");
|
|
||||||
// return context.getEmitter();
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// return workFlowStarterService.streaming(
|
|
||||||
// ThreadContext.getCurrentUser(),
|
|
||||||
// runner.getUuid(),
|
|
||||||
// runner.getInputs(),
|
|
||||||
// context.getChatRequest().getSessionId()
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
//}
|
|
||||||
@@ -1,25 +1,53 @@
|
|||||||
package org.ruoyi.service.chat.impl;
|
package org.ruoyi.service.chat.impl;
|
||||||
|
|
||||||
import cn.dev33.satoken.stp.StpUtil;
|
import cn.dev33.satoken.stp.StpUtil;
|
||||||
import dev.langchain4j.data.message.*;
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import dev.langchain4j.agentic.AgenticServices;
|
||||||
|
import dev.langchain4j.agentic.supervisor.SupervisorAgent;
|
||||||
|
import dev.langchain4j.agentic.supervisor.SupervisorResponseStrategy;
|
||||||
|
import dev.langchain4j.data.message.AiMessage;
|
||||||
|
import dev.langchain4j.data.message.ChatMessage;
|
||||||
|
import dev.langchain4j.data.message.UserMessage;
|
||||||
|
import dev.langchain4j.mcp.McpToolProvider;
|
||||||
|
import dev.langchain4j.mcp.client.DefaultMcpClient;
|
||||||
|
import dev.langchain4j.mcp.client.McpClient;
|
||||||
|
import dev.langchain4j.mcp.client.transport.McpTransport;
|
||||||
|
import dev.langchain4j.mcp.client.transport.stdio.StdioMcpTransport;
|
||||||
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
|
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
|
||||||
import dev.langchain4j.model.chat.StreamingChatModel;
|
import dev.langchain4j.model.chat.StreamingChatModel;
|
||||||
import dev.langchain4j.model.chat.response.ChatResponse;
|
import dev.langchain4j.model.chat.response.ChatResponse;
|
||||||
import dev.langchain4j.model.chat.response.StreamingChatResponseHandler;
|
import dev.langchain4j.model.chat.response.StreamingChatResponseHandler;
|
||||||
|
import dev.langchain4j.model.openai.OpenAiChatModel;
|
||||||
|
import dev.langchain4j.service.tool.ToolProvider;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.SneakyThrows;
|
import lombok.SneakyThrows;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.ruoyi.agent.ChartGenerationAgent;
|
||||||
|
import org.ruoyi.agent.SqlAgent;
|
||||||
|
import org.ruoyi.agent.WebSearchAgent;
|
||||||
|
import org.ruoyi.agent.tool.ExecuteSqlQueryTool;
|
||||||
|
import org.ruoyi.agent.tool.QueryAllTablesTool;
|
||||||
|
import org.ruoyi.agent.tool.QueryTableSchemaTool;
|
||||||
|
import org.ruoyi.common.chat.base.ThreadContext;
|
||||||
import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
|
import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
|
||||||
|
import org.ruoyi.common.chat.domain.dto.request.ReSumeRunner;
|
||||||
|
import org.ruoyi.common.chat.domain.dto.request.WorkFlowRunner;
|
||||||
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
|
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
|
||||||
import org.ruoyi.common.chat.enums.RoleType;
|
import org.ruoyi.common.chat.enums.RoleType;
|
||||||
import org.ruoyi.common.chat.service.chat.IChatModelService;
|
import org.ruoyi.common.chat.service.chat.IChatModelService;
|
||||||
import org.ruoyi.common.chat.service.chat.IChatService;
|
import org.ruoyi.common.chat.service.chat.IChatService;
|
||||||
|
import org.ruoyi.common.chat.service.workFlow.IWorkFlowStarterService;
|
||||||
|
import org.ruoyi.common.core.utils.ObjectUtils;
|
||||||
import org.ruoyi.common.satoken.utils.LoginHelper;
|
import org.ruoyi.common.satoken.utils.LoginHelper;
|
||||||
import org.ruoyi.common.sse.core.SseEmitterManager;
|
import org.ruoyi.common.sse.core.SseEmitterManager;
|
||||||
import org.ruoyi.common.sse.utils.SseMessageUtils;
|
import org.ruoyi.common.sse.utils.SseMessageUtils;
|
||||||
import org.ruoyi.domain.bo.vector.QueryVectorBo;
|
import org.ruoyi.domain.bo.vector.QueryVectorBo;
|
||||||
import org.ruoyi.domain.vo.knowledge.KnowledgeInfoVo;
|
import org.ruoyi.domain.vo.knowledge.KnowledgeInfoVo;
|
||||||
import org.ruoyi.factory.ChatServiceFactory;
|
import org.ruoyi.factory.ChatServiceFactory;
|
||||||
|
import org.ruoyi.mcp.service.core.ToolProviderFactory;
|
||||||
|
import org.ruoyi.observability.MyAgentListener;
|
||||||
|
import org.ruoyi.observability.MyChatModelListener;
|
||||||
|
import org.ruoyi.observability.MyMcpClientListener;
|
||||||
import org.ruoyi.service.chat.AbstractChatService;
|
import org.ruoyi.service.chat.AbstractChatService;
|
||||||
import org.ruoyi.service.chat.IChatMessageService;
|
import org.ruoyi.service.chat.IChatMessageService;
|
||||||
import org.ruoyi.service.chat.impl.memory.PersistentChatMemoryStore;
|
import org.ruoyi.service.chat.impl.memory.PersistentChatMemoryStore;
|
||||||
@@ -62,12 +90,18 @@ public class ChatServiceFacade implements IChatService {
|
|||||||
|
|
||||||
private final IChatMessageService chatMessageService;
|
private final IChatMessageService chatMessageService;
|
||||||
|
|
||||||
|
private final IWorkFlowStarterService workFlowStarterService;
|
||||||
|
|
||||||
|
private final ToolProviderFactory toolProviderFactory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 内存实例缓存,避免同一会话重复创建
|
* 内存实例缓存,避免同一会话重复创建
|
||||||
* Key: sessionId, Value: MessageWindowChatMemory实例
|
* Key: sessionId, Value: MessageWindowChatMemory实例
|
||||||
*/
|
*/
|
||||||
private static final Map<Object, MessageWindowChatMemory> memoryCache = new ConcurrentHashMap<>();
|
private static final Map<Object, MessageWindowChatMemory> memoryCache = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 统一聊天入口 - SSE流式响应
|
* 统一聊天入口 - SSE流式响应
|
||||||
*
|
*
|
||||||
@@ -76,6 +110,11 @@ public class ChatServiceFacade implements IChatService {
|
|||||||
*/
|
*/
|
||||||
public SseEmitter sseChat(ChatRequest chatRequest) {
|
public SseEmitter sseChat(ChatRequest chatRequest) {
|
||||||
|
|
||||||
|
// 4. 具体的服务实现
|
||||||
|
Long userId = LoginHelper.getUserId();
|
||||||
|
String tokenValue = StpUtil.getTokenValue();
|
||||||
|
SseEmitter emitter = sseEmitterManager.connect(userId, tokenValue);
|
||||||
|
|
||||||
// 1. 根据模型名称查询完整配置
|
// 1. 根据模型名称查询完整配置
|
||||||
ChatModelVo chatModelVo = chatModelService.selectModelByName(chatRequest.getModel());
|
ChatModelVo chatModelVo = chatModelService.selectModelByName(chatRequest.getModel());
|
||||||
if (chatModelVo == null) {
|
if (chatModelVo == null) {
|
||||||
@@ -85,14 +124,17 @@ public class ChatServiceFacade implements IChatService {
|
|||||||
// 2. 构建上下文消息列表
|
// 2. 构建上下文消息列表
|
||||||
List<ChatMessage> contextMessages = buildContextMessages(chatRequest);
|
List<ChatMessage> contextMessages = buildContextMessages(chatRequest);
|
||||||
|
|
||||||
// 3. 路由服务提供商
|
// 3. 处理特殊聊天模式(工作流、人机交互恢复、思考模式)
|
||||||
|
SseEmitter specialResult = handleSpecialChatModes(chatRequest, contextMessages, chatModelVo, emitter, userId, tokenValue);
|
||||||
|
if (specialResult != null) {
|
||||||
|
return specialResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 路由服务提供商
|
||||||
String providerCode = chatModelVo.getProviderCode();
|
String providerCode = chatModelVo.getProviderCode();
|
||||||
log.info("路由到服务提供商: {}, 模型: {}", providerCode, chatRequest.getModel());
|
log.info("路由到服务提供商: {}, 模型: {}", providerCode, chatRequest.getModel());
|
||||||
AbstractChatService chatService = chatServiceFactory.getOriginalService(providerCode);
|
AbstractChatService chatService = chatServiceFactory.getOriginalService(providerCode);
|
||||||
// 4. 具体的服务实现
|
|
||||||
Long userId = LoginHelper.getUserId();
|
|
||||||
String tokenValue = StpUtil.getTokenValue();
|
|
||||||
SseEmitter emitter = sseEmitterManager.connect(userId, tokenValue);
|
|
||||||
|
|
||||||
StreamingChatResponseHandler handler = createResponseHandler(userId, tokenValue,chatRequest);
|
StreamingChatResponseHandler handler = createResponseHandler(userId, tokenValue,chatRequest);
|
||||||
|
|
||||||
@@ -105,6 +147,144 @@ public class ChatServiceFacade implements IChatService {
|
|||||||
return emitter;
|
return emitter;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理特殊聊天模式(工作流、人机交互恢复、思考模式)
|
||||||
|
*
|
||||||
|
* @param chatRequest 聊天请求
|
||||||
|
* @param contextMessages 上下文消息列表(可能被修改)
|
||||||
|
* @param chatModelVo 聊天模型配置
|
||||||
|
* @param emitter SSE发射器
|
||||||
|
* @param userId 用户ID
|
||||||
|
* @param tokenValue 会话令牌
|
||||||
|
* @return 如果需要提前返回则返回SseEmitter,否则返回null
|
||||||
|
*/
|
||||||
|
private SseEmitter handleSpecialChatModes(ChatRequest chatRequest, List<ChatMessage> contextMessages,
|
||||||
|
ChatModelVo chatModelVo, SseEmitter emitter,
|
||||||
|
Long userId, String tokenValue) {
|
||||||
|
// 处理工作流对话
|
||||||
|
if (chatRequest.getEnableWorkFlow()) {
|
||||||
|
log.info("处理工作流对话,会话: {}", chatRequest.getSessionId());
|
||||||
|
|
||||||
|
WorkFlowRunner runner = chatRequest.getWorkFlowRunner();
|
||||||
|
if (ObjectUtils.isEmpty(runner)) {
|
||||||
|
log.warn("工作流参数为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
return workFlowStarterService.streaming(
|
||||||
|
ThreadContext.getCurrentUser(),
|
||||||
|
runner.getUuid(),
|
||||||
|
runner.getInputs(),
|
||||||
|
chatRequest.getSessionId()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理人机交互恢复
|
||||||
|
if (chatRequest.getIsResume()) {
|
||||||
|
log.info("处理人机交互恢复");
|
||||||
|
|
||||||
|
ReSumeRunner reSumeRunner = chatRequest.getReSumeRunner();
|
||||||
|
if (ObjectUtils.isEmpty(reSumeRunner)) {
|
||||||
|
log.warn("人机交互恢复参数为空");
|
||||||
|
return emitter;
|
||||||
|
}
|
||||||
|
|
||||||
|
workFlowStarterService.resumeFlow(
|
||||||
|
reSumeRunner.getRuntimeUuid(),
|
||||||
|
reSumeRunner.getFeedbackContent(),
|
||||||
|
emitter
|
||||||
|
);
|
||||||
|
|
||||||
|
return emitter;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理思考模式
|
||||||
|
if (chatRequest.getEnableThinking()) {
|
||||||
|
handleThinkingMode(chatRequest, contextMessages, chatModelVo, userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理思考模式
|
||||||
|
*
|
||||||
|
* @param chatRequest 聊天请求
|
||||||
|
* @param contextMessages 上下文消息列表
|
||||||
|
* @param chatModelVo 聊天模型配置
|
||||||
|
* @param userId 用户ID
|
||||||
|
*/
|
||||||
|
private void handleThinkingMode(ChatRequest chatRequest, List<ChatMessage> contextMessages,
|
||||||
|
ChatModelVo chatModelVo, Long userId) {
|
||||||
|
// 步骤1: 配置MCP传输层 - 连接到bing-cn-mcp服务器
|
||||||
|
McpTransport transport = new StdioMcpTransport.Builder()
|
||||||
|
.command(List.of("C:\\Program Files\\nodejs\\npx.cmd", "-y", "bing-cn-mcp"))
|
||||||
|
.logEvents(true)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
McpClient mcpClient = new DefaultMcpClient.Builder()
|
||||||
|
.transport(transport)
|
||||||
|
.listener(new MyMcpClientListener())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
ToolProvider toolProvider = McpToolProvider.builder()
|
||||||
|
.mcpClients(List.of(mcpClient))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// 配置echarts MCP
|
||||||
|
McpTransport transport1 = new StdioMcpTransport.Builder()
|
||||||
|
.command(List.of("C:\\Program Files\\nodejs\\npx.cmd", "-y", "mcp-echarts"))
|
||||||
|
.logEvents(true)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
McpClient mcpClient1 = new DefaultMcpClient.Builder()
|
||||||
|
.transport(transport1)
|
||||||
|
.listener(new MyMcpClientListener())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
ToolProvider toolProvider1 = McpToolProvider.builder()
|
||||||
|
.mcpClients(List.of(mcpClient1))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// 配置模型
|
||||||
|
OpenAiChatModel plannerModel = OpenAiChatModel.builder()
|
||||||
|
.baseUrl(chatModelVo.getApiHost())
|
||||||
|
.apiKey(chatModelVo.getApiKey())
|
||||||
|
.listeners(List.of(new MyChatModelListener()))
|
||||||
|
.modelName(chatModelVo.getModelName())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// 构建各Agent
|
||||||
|
SqlAgent sqlAgent = AgenticServices.agentBuilder(SqlAgent.class)
|
||||||
|
.chatModel(plannerModel)
|
||||||
|
.listener(new MyAgentListener())
|
||||||
|
.tools(new QueryAllTablesTool(), new QueryTableSchemaTool(), new ExecuteSqlQueryTool())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
WebSearchAgent searchAgent = AgenticServices.agentBuilder(WebSearchAgent.class)
|
||||||
|
.chatModel(plannerModel)
|
||||||
|
.listener(new MyAgentListener())
|
||||||
|
.toolProvider(toolProvider)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
ChartGenerationAgent chartGenerationAgent = AgenticServices.agentBuilder(ChartGenerationAgent.class)
|
||||||
|
.chatModel(plannerModel)
|
||||||
|
.listener(new MyAgentListener())
|
||||||
|
.toolProvider(toolProvider1)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// 构建监督者Agent
|
||||||
|
SupervisorAgent supervisor = AgenticServices.supervisorBuilder()
|
||||||
|
.chatModel(plannerModel)
|
||||||
|
.listener(new MyAgentListener())
|
||||||
|
.subAgents(sqlAgent, searchAgent, chartGenerationAgent)
|
||||||
|
.responseStrategy(SupervisorResponseStrategy.LAST)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// 调用 supervisor
|
||||||
|
String invoke = supervisor.invoke(chatRequest.getContent());
|
||||||
|
log.info("supervisor.invoke() 返回: {}", invoke);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 支持外部 handler 的对话接口(跨模块调用)
|
* 支持外部 handler 的对话接口(跨模块调用)
|
||||||
* 同时发送到 SSE 和外部 handler
|
* 同时发送到 SSE 和外部 handler
|
||||||
@@ -179,44 +359,16 @@ public class ChatServiceFacade implements IChatService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 构建上下文消息列表
|
* 构建上下文消息列表
|
||||||
|
|
||||||
|
* 消息顺序:历史消息 → 当前用户消息(确保 AI 正确理解对话上下文)
|
||||||
*
|
*
|
||||||
* @param chatRequest 聊天请求
|
* @param chatRequest 聊天请求
|
||||||
* @return 上下文消息列表
|
* @return 上下文消息列表
|
||||||
*/
|
*/
|
||||||
private List<ChatMessage> buildContextMessages(ChatRequest chatRequest) {
|
private List<ChatMessage> buildContextMessages(ChatRequest chatRequest) {
|
||||||
List<ChatMessage> messages = new ArrayList<>();
|
List<ChatMessage> messages = new ArrayList<>();
|
||||||
// 构建用户消息
|
|
||||||
UserMessage userMessage = UserMessage.userMessage(chatRequest.getContent());
|
|
||||||
messages.add(userMessage);
|
|
||||||
|
|
||||||
// 从向量库查询相关历史消息
|
// 从数据库查询历史对话消息(放在前面)
|
||||||
if (chatRequest.getKnowledgeId() != null) {
|
|
||||||
// 查询知识库信息
|
|
||||||
KnowledgeInfoVo knowledgeInfoVo = knowledgeInfoService.queryById(Long.valueOf(chatRequest.getKnowledgeId()));
|
|
||||||
if (knowledgeInfoVo == null) {
|
|
||||||
log.warn("知识库信息不存在,kid: {}", chatRequest.getKnowledgeId());
|
|
||||||
return messages;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 查询向量模型配置信息
|
|
||||||
ChatModelVo chatModel = chatModelService.selectModelByName(knowledgeInfoVo.getEmbeddingModel());
|
|
||||||
if (chatModel == null) {
|
|
||||||
log.warn("向量模型配置不存在,模型名称: {}", knowledgeInfoVo.getEmbeddingModel());
|
|
||||||
return messages;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 构建向量查询参数
|
|
||||||
QueryVectorBo queryVectorBo = buildQueryVectorBo(chatRequest, knowledgeInfoVo, chatModel);
|
|
||||||
|
|
||||||
// 获取向量查询结果
|
|
||||||
List<String> nearestList = vectorStoreService.getQueryVector(queryVectorBo);
|
|
||||||
for (String prompt : nearestList) {
|
|
||||||
// 知识库内容作为系统上下文添加
|
|
||||||
messages.add( new AiMessage(prompt));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 从数据库查询历史对话消息
|
|
||||||
if (chatRequest.getSessionId() != null) {
|
if (chatRequest.getSessionId() != null) {
|
||||||
MessageWindowChatMemory memory = createChatMemory(chatRequest.getSessionId());
|
MessageWindowChatMemory memory = createChatMemory(chatRequest.getSessionId());
|
||||||
if (memory != null) {
|
if (memory != null) {
|
||||||
@@ -228,6 +380,40 @@ public class ChatServiceFacade implements IChatService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 从向量库查询相关历史消息(知识库内容作为上下文)
|
||||||
|
if (chatRequest.getKnowledgeId() != null) {
|
||||||
|
// 查询知识库信息
|
||||||
|
KnowledgeInfoVo knowledgeInfoVo = knowledgeInfoService.queryById(Long.valueOf(chatRequest.getKnowledgeId()));
|
||||||
|
if (knowledgeInfoVo == null) {
|
||||||
|
log.warn("知识库信息不存在,kid: {}", chatRequest.getKnowledgeId());
|
||||||
|
// 继续添加当前用户消息
|
||||||
|
messages.add(UserMessage.userMessage(chatRequest.getContent()));
|
||||||
|
return messages;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询向量模型配置信息
|
||||||
|
ChatModelVo chatModel = chatModelService.selectModelByName(knowledgeInfoVo.getEmbeddingModel());
|
||||||
|
if (chatModel == null) {
|
||||||
|
log.warn("向量模型配置不存在,模型名称: {}", knowledgeInfoVo.getEmbeddingModel());
|
||||||
|
messages.add(UserMessage.userMessage(chatRequest.getContent()));
|
||||||
|
return messages;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建向量查询参数
|
||||||
|
QueryVectorBo queryVectorBo = buildQueryVectorBo(chatRequest, knowledgeInfoVo, chatModel);
|
||||||
|
|
||||||
|
// 获取向量查询结果(知识库内容作为系统上下文,放在历史消息之后)
|
||||||
|
List<String> nearestList = vectorStoreService.getQueryVector(queryVectorBo);
|
||||||
|
for (String prompt : nearestList) {
|
||||||
|
// 知识库内容作为系统上下文添加
|
||||||
|
messages.add(new AiMessage(prompt));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建当前用户消息(放在最后)
|
||||||
|
UserMessage userMessage = UserMessage.userMessage(chatRequest.getContent());
|
||||||
|
messages.add(userMessage);
|
||||||
|
|
||||||
return messages;
|
return messages;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +1,30 @@
|
|||||||
package org.ruoyi.service.chat.impl.provider;
|
package org.ruoyi.service.chat.impl.provider;
|
||||||
|
|
||||||
|
|
||||||
import dev.langchain4j.model.chat.StreamingChatModel;
|
import dev.langchain4j.model.chat.StreamingChatModel;
|
||||||
import dev.langchain4j.model.openai.OpenAiStreamingChatModel;
|
import dev.langchain4j.model.openai.OpenAiStreamingChatModel;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
|
import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
|
||||||
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
|
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
|
||||||
import org.ruoyi.enums.ChatModeType;
|
import org.ruoyi.enums.ChatModeType;
|
||||||
|
import org.ruoyi.observability.ChatModelListenerProvider;
|
||||||
|
import org.ruoyi.observability.MyChatModelListener;
|
||||||
import org.ruoyi.service.chat.AbstractChatService;
|
import org.ruoyi.service.chat.AbstractChatService;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Author: xiaoen
|
* Deepseek服务调用
|
||||||
* @Description: deepseek 服务调用
|
*
|
||||||
* @Date: Created in 19:12 2026/3/17
|
* @author xiaoen
|
||||||
|
* @date 2026/3/17
|
||||||
*/
|
*/
|
||||||
@Service
|
@Service
|
||||||
@Slf4j
|
@Slf4j
|
||||||
|
@RequiredArgsConstructor
|
||||||
public class DeepseekServiceImpl implements AbstractChatService {
|
public class DeepseekServiceImpl implements AbstractChatService {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -25,6 +33,7 @@ public class DeepseekServiceImpl implements AbstractChatService {
|
|||||||
.baseUrl(chatModelVo.getApiHost())
|
.baseUrl(chatModelVo.getApiHost())
|
||||||
.apiKey(chatModelVo.getApiKey())
|
.apiKey(chatModelVo.getApiKey())
|
||||||
.modelName(chatModelVo.getModelName())
|
.modelName(chatModelVo.getModelName())
|
||||||
|
.listeners(List.of(new MyChatModelListener()))
|
||||||
.returnThinking(chatRequest.getEnableThinking())
|
.returnThinking(chatRequest.getEnableThinking())
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,19 @@
|
|||||||
package org.ruoyi.service.chat.impl.provider;
|
package org.ruoyi.service.chat.impl.provider;
|
||||||
|
|
||||||
|
|
||||||
import dev.langchain4j.model.chat.StreamingChatModel;
|
import dev.langchain4j.model.chat.StreamingChatModel;
|
||||||
import dev.langchain4j.model.ollama.OllamaStreamingChatModel;
|
import dev.langchain4j.model.ollama.OllamaStreamingChatModel;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.ruoyi.enums.ChatModeType;
|
|
||||||
import org.ruoyi.service.chat.AbstractChatService;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
|
import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
|
||||||
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
|
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
|
||||||
|
import org.ruoyi.enums.ChatModeType;
|
||||||
|
import org.ruoyi.observability.ChatModelListenerProvider;
|
||||||
|
import org.ruoyi.observability.MyChatModelListener;
|
||||||
|
import org.ruoyi.service.chat.AbstractChatService;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* OllamaAI服务调用
|
* OllamaAI服务调用
|
||||||
@@ -17,13 +23,17 @@ import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
|
|||||||
*/
|
*/
|
||||||
@Service
|
@Service
|
||||||
@Slf4j
|
@Slf4j
|
||||||
|
@RequiredArgsConstructor
|
||||||
public class OllamaServiceImpl implements AbstractChatService {
|
public class OllamaServiceImpl implements AbstractChatService {
|
||||||
|
|
||||||
|
private final ChatModelListenerProvider listenerProvider;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public StreamingChatModel buildStreamingChatModel(ChatModelVo chatModelVo, ChatRequest chatRequest) {
|
public StreamingChatModel buildStreamingChatModel(ChatModelVo chatModelVo, ChatRequest chatRequest) {
|
||||||
return OllamaStreamingChatModel.builder()
|
return OllamaStreamingChatModel.builder()
|
||||||
.baseUrl(chatModelVo.getApiHost())
|
.baseUrl(chatModelVo.getApiHost())
|
||||||
.modelName(chatModelVo.getModelName())
|
.modelName(chatModelVo.getModelName())
|
||||||
|
.listeners(List.of(new MyChatModelListener()))
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,13 +3,18 @@ package org.ruoyi.service.chat.impl.provider;
|
|||||||
|
|
||||||
import dev.langchain4j.model.chat.StreamingChatModel;
|
import dev.langchain4j.model.chat.StreamingChatModel;
|
||||||
import dev.langchain4j.model.openai.OpenAiStreamingChatModel;
|
import dev.langchain4j.model.openai.OpenAiStreamingChatModel;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
|
import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
|
||||||
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
|
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
|
||||||
import org.ruoyi.enums.ChatModeType;
|
import org.ruoyi.enums.ChatModeType;
|
||||||
|
import org.ruoyi.observability.ChatModelListenerProvider;
|
||||||
|
import org.ruoyi.observability.MyChatModelListener;
|
||||||
import org.ruoyi.service.chat.AbstractChatService;
|
import org.ruoyi.service.chat.AbstractChatService;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* OPENAI服务调用
|
* OPENAI服务调用
|
||||||
@@ -19,14 +24,16 @@ import org.springframework.stereotype.Service;
|
|||||||
*/
|
*/
|
||||||
@Service
|
@Service
|
||||||
@Slf4j
|
@Slf4j
|
||||||
|
@RequiredArgsConstructor
|
||||||
public class OpenAIServiceImpl implements AbstractChatService {
|
public class OpenAIServiceImpl implements AbstractChatService {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public StreamingChatModel buildStreamingChatModel(ChatModelVo chatModelVo, ChatRequest chatRequest) {
|
public StreamingChatModel buildStreamingChatModel(ChatModelVo chatModelVo,ChatRequest chatRequest) {
|
||||||
return OpenAiStreamingChatModel.builder()
|
return OpenAiStreamingChatModel.builder()
|
||||||
.baseUrl(chatModelVo.getApiHost())
|
.baseUrl(chatModelVo.getApiHost())
|
||||||
.apiKey(chatModelVo.getApiKey())
|
.apiKey(chatModelVo.getApiKey())
|
||||||
.modelName(chatModelVo.getModelName())
|
.modelName(chatModelVo.getModelName())
|
||||||
|
.listeners(List.of(new MyChatModelListener()))
|
||||||
.returnThinking(chatRequest.getEnableThinking())
|
.returnThinking(chatRequest.getEnableThinking())
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,28 @@
|
|||||||
package org.ruoyi.service.chat.impl.provider;
|
package org.ruoyi.service.chat.impl.provider;
|
||||||
|
|
||||||
|
|
||||||
import dev.langchain4j.model.chat.StreamingChatModel;
|
import dev.langchain4j.model.chat.StreamingChatModel;
|
||||||
import dev.langchain4j.model.openai.OpenAiStreamingChatModel;
|
import dev.langchain4j.model.openai.OpenAiStreamingChatModel;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
|
import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
|
||||||
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
|
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
|
||||||
import org.ruoyi.enums.ChatModeType;
|
import org.ruoyi.enums.ChatModeType;
|
||||||
|
import org.ruoyi.observability.MyChatModelListener;
|
||||||
import org.ruoyi.service.chat.AbstractChatService;
|
import org.ruoyi.service.chat.AbstractChatService;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* OPENAI服务调用
|
* PPIO服务调用
|
||||||
*
|
*
|
||||||
* @author ageerle@163.com
|
* @author ageerle@163.com
|
||||||
* @date 2025/12/13
|
* @date 2025/12/13
|
||||||
*/
|
*/
|
||||||
@Service
|
@Service
|
||||||
@Slf4j
|
@Slf4j
|
||||||
|
@RequiredArgsConstructor
|
||||||
public class PPIOServiceImpl implements AbstractChatService {
|
public class PPIOServiceImpl implements AbstractChatService {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -25,6 +31,7 @@ public class PPIOServiceImpl implements AbstractChatService {
|
|||||||
.baseUrl(chatModelVo.getApiHost())
|
.baseUrl(chatModelVo.getApiHost())
|
||||||
.apiKey(chatModelVo.getApiKey())
|
.apiKey(chatModelVo.getApiKey())
|
||||||
.modelName(chatModelVo.getModelName())
|
.modelName(chatModelVo.getModelName())
|
||||||
|
.listeners(List.of(new MyChatModelListener()))
|
||||||
.returnThinking(chatRequest.getEnableThinking())
|
.returnThinking(chatRequest.getEnableThinking())
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,20 @@
|
|||||||
package org.ruoyi.service.chat.impl.provider;
|
package org.ruoyi.service.chat.impl.provider;
|
||||||
|
|
||||||
|
|
||||||
import dev.langchain4j.community.model.dashscope.QwenStreamingChatModel;
|
import dev.langchain4j.community.model.dashscope.QwenStreamingChatModel;
|
||||||
import dev.langchain4j.model.chat.StreamingChatModel;
|
import dev.langchain4j.model.chat.StreamingChatModel;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
|
import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
|
||||||
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
|
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
|
||||||
import org.ruoyi.enums.ChatModeType;
|
import org.ruoyi.enums.ChatModeType;
|
||||||
|
import org.ruoyi.observability.ChatModelListenerProvider;
|
||||||
|
import org.ruoyi.observability.MyChatModelListener;
|
||||||
import org.ruoyi.service.chat.AbstractChatService;
|
import org.ruoyi.service.chat.AbstractChatService;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* qianWenAI服务调用
|
* qianWenAI服务调用
|
||||||
@@ -18,14 +24,17 @@ import org.springframework.stereotype.Service;
|
|||||||
*/
|
*/
|
||||||
@Service
|
@Service
|
||||||
@Slf4j
|
@Slf4j
|
||||||
|
@RequiredArgsConstructor
|
||||||
public class QianWenChatServiceImpl implements AbstractChatService {
|
public class QianWenChatServiceImpl implements AbstractChatService {
|
||||||
|
|
||||||
// 添加文档解析的前缀字段
|
private final ChatModelListenerProvider listenerProvider;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public StreamingChatModel buildStreamingChatModel(ChatModelVo chatModelVo,ChatRequest chatRequest) {
|
public StreamingChatModel buildStreamingChatModel(ChatModelVo chatModelVo,ChatRequest chatRequest) {
|
||||||
return QwenStreamingChatModel.builder()
|
return QwenStreamingChatModel.builder()
|
||||||
.apiKey(chatModelVo.getApiKey())
|
.apiKey(chatModelVo.getApiKey())
|
||||||
.modelName(chatModelVo.getModelName())
|
.modelName(chatModelVo.getModelName())
|
||||||
|
.listeners(List.of(new MyChatModelListener()))
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,19 @@
|
|||||||
package org.ruoyi.service.chat.impl.provider;
|
package org.ruoyi.service.chat.impl.provider;
|
||||||
|
|
||||||
|
|
||||||
import dev.langchain4j.community.model.zhipu.ZhipuAiStreamingChatModel;
|
import dev.langchain4j.community.model.zhipu.ZhipuAiStreamingChatModel;
|
||||||
import dev.langchain4j.model.chat.StreamingChatModel;
|
import dev.langchain4j.model.chat.StreamingChatModel;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
|
import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
|
||||||
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
|
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
|
||||||
import org.ruoyi.enums.ChatModeType;
|
import org.ruoyi.enums.ChatModeType;
|
||||||
|
import org.ruoyi.observability.MyChatModelListener;
|
||||||
import org.ruoyi.service.chat.AbstractChatService;
|
import org.ruoyi.service.chat.AbstractChatService;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 智谱AI服务调用
|
* 智谱AI服务调用
|
||||||
@@ -18,6 +23,7 @@ import org.springframework.stereotype.Service;
|
|||||||
*/
|
*/
|
||||||
@Service
|
@Service
|
||||||
@Slf4j
|
@Slf4j
|
||||||
|
@RequiredArgsConstructor
|
||||||
public class ZhiPuChatServiceImpl implements AbstractChatService {
|
public class ZhiPuChatServiceImpl implements AbstractChatService {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -25,6 +31,7 @@ public class ZhiPuChatServiceImpl implements AbstractChatService {
|
|||||||
return ZhipuAiStreamingChatModel.builder()
|
return ZhipuAiStreamingChatModel.builder()
|
||||||
.apiKey(chatModelVo.getApiKey())
|
.apiKey(chatModelVo.getApiKey())
|
||||||
.model(chatModelVo.getModelName())
|
.model(chatModelVo.getModelName())
|
||||||
|
.listeners(List.of(new MyChatModelListener()))
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,10 +4,11 @@ package org.ruoyi.service.embed.impl;
|
|||||||
import dev.langchain4j.community.model.dashscope.QwenEmbeddingModel;
|
import dev.langchain4j.community.model.dashscope.QwenEmbeddingModel;
|
||||||
import dev.langchain4j.data.embedding.Embedding;
|
import dev.langchain4j.data.embedding.Embedding;
|
||||||
import dev.langchain4j.data.segment.TextSegment;
|
import dev.langchain4j.data.segment.TextSegment;
|
||||||
|
import dev.langchain4j.model.embedding.EmbeddingModel;
|
||||||
import dev.langchain4j.model.output.Response;
|
import dev.langchain4j.model.output.Response;
|
||||||
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
|
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
import org.ruoyi.enums.ModalityType;
|
import org.ruoyi.enums.ModalityType;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
@@ -20,7 +21,6 @@ import java.util.Set;
|
|||||||
@Component("alibailian")
|
@Component("alibailian")
|
||||||
public class AliBaiLianBaseEmbedProvider extends OpenAiEmbeddingProvider {
|
public class AliBaiLianBaseEmbedProvider extends OpenAiEmbeddingProvider {
|
||||||
|
|
||||||
|
|
||||||
private ChatModelVo chatModelVo;
|
private ChatModelVo chatModelVo;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -35,12 +35,13 @@ public class AliBaiLianBaseEmbedProvider extends OpenAiEmbeddingProvider {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Response<List<Embedding>> embedAll(List<TextSegment> textSegments) {
|
public Response<List<Embedding>> embedAll(List<TextSegment> textSegments) {
|
||||||
return QwenEmbeddingModel.builder()
|
EmbeddingModel model = QwenEmbeddingModel.builder()
|
||||||
.apiKey(chatModelVo.getApiKey())
|
.apiKey(chatModelVo.getApiKey())
|
||||||
.modelName(chatModelVo.getModelName())
|
.modelName(chatModelVo.getModelName())
|
||||||
.dimension(chatModelVo.getModelDimension())
|
.dimension(chatModelVo.getModelDimension())
|
||||||
.build()
|
.build();
|
||||||
.embedAll(textSegments);
|
|
||||||
|
return model.embedAll(textSegments);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package org.ruoyi.service.embed.impl;
|
|||||||
|
|
||||||
import dev.langchain4j.data.embedding.Embedding;
|
import dev.langchain4j.data.embedding.Embedding;
|
||||||
import dev.langchain4j.data.segment.TextSegment;
|
import dev.langchain4j.data.segment.TextSegment;
|
||||||
|
import dev.langchain4j.model.embedding.EmbeddingModel;
|
||||||
import dev.langchain4j.model.ollama.OllamaEmbeddingModel;
|
import dev.langchain4j.model.ollama.OllamaEmbeddingModel;
|
||||||
import dev.langchain4j.model.output.Response;
|
import dev.langchain4j.model.output.Response;
|
||||||
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
|
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
|
||||||
@@ -34,10 +35,11 @@ public class OllamaEmbeddingProvider implements BaseEmbedModelService {
|
|||||||
// ollama不能设置embedding维度,使用milvus时请注意!!创建向量表时需要先设定维度大小
|
// ollama不能设置embedding维度,使用milvus时请注意!!创建向量表时需要先设定维度大小
|
||||||
@Override
|
@Override
|
||||||
public Response<List<Embedding>> embedAll(List<TextSegment> textSegments) {
|
public Response<List<Embedding>> embedAll(List<TextSegment> textSegments) {
|
||||||
return OllamaEmbeddingModel.builder()
|
EmbeddingModel model = OllamaEmbeddingModel.builder()
|
||||||
.baseUrl(chatModelVo.getApiHost())
|
.baseUrl(chatModelVo.getApiHost())
|
||||||
.modelName(chatModelVo.getModelName())
|
.modelName(chatModelVo.getModelName())
|
||||||
.build()
|
.build();
|
||||||
.embedAll(textSegments);
|
|
||||||
|
return model.embedAll(textSegments);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package org.ruoyi.service.embed.impl;
|
|||||||
|
|
||||||
import dev.langchain4j.data.embedding.Embedding;
|
import dev.langchain4j.data.embedding.Embedding;
|
||||||
import dev.langchain4j.data.segment.TextSegment;
|
import dev.langchain4j.data.segment.TextSegment;
|
||||||
|
import dev.langchain4j.model.embedding.EmbeddingModel;
|
||||||
import dev.langchain4j.model.openai.OpenAiEmbeddingModel;
|
import dev.langchain4j.model.openai.OpenAiEmbeddingModel;
|
||||||
import dev.langchain4j.model.output.Response;
|
import dev.langchain4j.model.output.Response;
|
||||||
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
|
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
|
||||||
@@ -33,12 +34,13 @@ public class OpenAiEmbeddingProvider implements BaseEmbedModelService {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Response<List<Embedding>> embedAll(List<TextSegment> textSegments) {
|
public Response<List<Embedding>> embedAll(List<TextSegment> textSegments) {
|
||||||
return OpenAiEmbeddingModel.builder()
|
EmbeddingModel model = OpenAiEmbeddingModel.builder()
|
||||||
.baseUrl(chatModelVo.getApiHost())
|
.baseUrl(chatModelVo.getApiHost())
|
||||||
.apiKey(chatModelVo.getApiKey())
|
.apiKey(chatModelVo.getApiKey())
|
||||||
.modelName(chatModelVo.getModelName())
|
.modelName(chatModelVo.getModelName())
|
||||||
.dimensions(chatModelVo.getModelDimension())
|
.dimensions(chatModelVo.getModelDimension())
|
||||||
.build()
|
.build();
|
||||||
.embedAll(textSegments);
|
|
||||||
|
return model.embedAll(textSegments);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
package org.ruoyi.service.knowledge.impl.split;
|
package org.ruoyi.service.knowledge.impl.split;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.ruoyi.common.core.utils.StringUtils;
|
import org.ruoyi.common.core.utils.StringUtils;
|
||||||
|
import org.ruoyi.domain.vo.knowledge.KnowledgeInfoVo;
|
||||||
|
import org.ruoyi.service.knowledge.IKnowledgeInfoService;
|
||||||
import org.ruoyi.service.knowledge.TextSplitter;
|
import org.ruoyi.service.knowledge.TextSplitter;
|
||||||
import org.springframework.context.annotation.Primary;
|
import org.springframework.context.annotation.Primary;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
@@ -13,14 +16,37 @@ import java.util.List;
|
|||||||
@Component
|
@Component
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Primary
|
@Primary
|
||||||
|
@AllArgsConstructor
|
||||||
public class CharacterTextSplitter implements TextSplitter {
|
public class CharacterTextSplitter implements TextSplitter {
|
||||||
|
|
||||||
|
private final IKnowledgeInfoService knowledgeInfoService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<String> split(String content, String kid) {
|
public List<String> split(String content, String kid) {
|
||||||
// 使用默认配置
|
// 默认配置值
|
||||||
String knowledgeSeparator = "#";
|
String knowledgeSeparator = "#";
|
||||||
int textBlockSize = 10000;
|
int textBlockSize = 1000;
|
||||||
int overlapChar = 500;
|
int overlapChar = 50;
|
||||||
|
|
||||||
|
// 根据知识库ID查询配置,覆盖默认值
|
||||||
|
if (StringUtils.isNotBlank(kid)) {
|
||||||
|
try {
|
||||||
|
KnowledgeInfoVo info = knowledgeInfoService.queryById(Long.parseLong(kid));
|
||||||
|
if (info != null) {
|
||||||
|
if (StringUtils.isNotBlank(info.getSeparator())) {
|
||||||
|
knowledgeSeparator = info.getSeparator();
|
||||||
|
}
|
||||||
|
if (info.getTextBlockSize() != null && info.getTextBlockSize() > 0) {
|
||||||
|
textBlockSize = info.getTextBlockSize().intValue();
|
||||||
|
}
|
||||||
|
if (info.getOverlapChar() != null && info.getOverlapChar() > 0) {
|
||||||
|
overlapChar = info.getOverlapChar().intValue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("查询知识库配置失败,使用默认配置, kid={}", kid, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
List<String> chunkList = new ArrayList<>();
|
List<String> chunkList = new ArrayList<>();
|
||||||
if (content.contains(knowledgeSeparator) && StringUtils.isNotBlank(knowledgeSeparator)) {
|
if (content.contains(knowledgeSeparator) && StringUtils.isNotBlank(knowledgeSeparator)) {
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package org.ruoyi.service.knowledge.impl.split;
|
|||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.ruoyi.common.core.utils.StringUtils;
|
import org.ruoyi.common.core.utils.StringUtils;
|
||||||
|
import org.ruoyi.domain.vo.knowledge.KnowledgeInfoVo;
|
||||||
|
import org.ruoyi.service.knowledge.IKnowledgeInfoService;
|
||||||
import org.ruoyi.service.knowledge.TextSplitter;
|
import org.ruoyi.service.knowledge.TextSplitter;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
@@ -15,13 +17,34 @@ import java.util.List;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
public class ExcelTextSplitter implements TextSplitter {
|
public class ExcelTextSplitter implements TextSplitter {
|
||||||
|
|
||||||
|
private final IKnowledgeInfoService knowledgeInfoService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<String> split(String content, String kid) {
|
public List<String> split(String content, String kid) {
|
||||||
// 使用默认配置
|
// 默认配置
|
||||||
String knowledgeSeparator = "#";
|
String knowledgeSeparator = "#";
|
||||||
int textBlockSize = 10000;
|
int textBlockSize = 1000;
|
||||||
int overlapChar = 500;
|
int overlapChar = 50;
|
||||||
|
|
||||||
|
// 根据知识库ID查询配置,覆盖默认值
|
||||||
|
if (StringUtils.isNotBlank(kid)) {
|
||||||
|
try {
|
||||||
|
KnowledgeInfoVo info = knowledgeInfoService.queryById(Long.parseLong(kid));
|
||||||
|
if (info != null) {
|
||||||
|
if (StringUtils.isNotBlank(info.getSeparator())) {
|
||||||
|
knowledgeSeparator = info.getSeparator();
|
||||||
|
}
|
||||||
|
if (info.getTextBlockSize() != null && info.getTextBlockSize() > 0) {
|
||||||
|
textBlockSize = info.getTextBlockSize().intValue();
|
||||||
|
}
|
||||||
|
if (info.getOverlapChar() != null && info.getOverlapChar() > 0) {
|
||||||
|
overlapChar = info.getOverlapChar().intValue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("查询知识库配置失败,使用默认配置, kid={}", kid, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
List<String> chunkList = new ArrayList<>();
|
List<String> chunkList = new ArrayList<>();
|
||||||
if (content.contains(knowledgeSeparator) && StringUtils.isNotBlank(knowledgeSeparator)) {
|
if (content.contains(knowledgeSeparator) && StringUtils.isNotBlank(knowledgeSeparator)) {
|
||||||
// 按自定义分隔符切分
|
// 按自定义分隔符切分
|
||||||
|
|||||||
@@ -0,0 +1,204 @@
|
|||||||
|
package org.ruoyi.service.vector.impl;
|
||||||
|
|
||||||
|
import dev.langchain4j.data.document.Metadata;
|
||||||
|
import dev.langchain4j.data.embedding.Embedding;
|
||||||
|
import dev.langchain4j.data.segment.TextSegment;
|
||||||
|
import dev.langchain4j.model.embedding.EmbeddingModel;
|
||||||
|
import dev.langchain4j.store.embedding.EmbeddingStore;
|
||||||
|
import dev.langchain4j.store.embedding.filter.Filter;
|
||||||
|
import dev.langchain4j.store.embedding.filter.MetadataFilterBuilder;
|
||||||
|
import dev.langchain4j.store.embedding.qdrant.QdrantEmbeddingStore;
|
||||||
|
import io.qdrant.client.QdrantClient;
|
||||||
|
import io.qdrant.client.QdrantGrpcClient;
|
||||||
|
import io.qdrant.client.grpc.Collections.Distance;
|
||||||
|
import io.qdrant.client.grpc.Collections.VectorParams;
|
||||||
|
import io.qdrant.client.grpc.JsonWithInt;
|
||||||
|
import io.qdrant.client.grpc.Points.DenseVector;
|
||||||
|
import io.qdrant.client.grpc.Points.Query;
|
||||||
|
import io.qdrant.client.grpc.Points.QueryPoints;
|
||||||
|
import io.qdrant.client.grpc.Points.ScoredPoint;
|
||||||
|
import io.qdrant.client.grpc.Points.VectorInput;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.ruoyi.common.chat.service.chat.IChatModelService;
|
||||||
|
import org.ruoyi.common.core.exception.ServiceException;
|
||||||
|
import org.ruoyi.config.VectorStoreProperties;
|
||||||
|
import org.ruoyi.domain.bo.vector.QueryVectorBo;
|
||||||
|
import org.ruoyi.domain.bo.vector.StoreEmbeddingBo;
|
||||||
|
import org.ruoyi.factory.EmbeddingModelFactory;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import static io.qdrant.client.VectorInputFactory.vectorInput;
|
||||||
|
import static io.qdrant.client.WithPayloadSelectorFactory.enable;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.IntStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Qdrant向量库策略实现
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class QdrantVectorStoreStrategy extends AbstractVectorStoreStrategy {
|
||||||
|
|
||||||
|
private static final String VECTOR_STORE_TYPE = "qdrant";
|
||||||
|
private static final String TEXT_SEGMENT_KEY = "text_segment";
|
||||||
|
private static final String METADATA_FID_KEY = "fid";
|
||||||
|
private static final String METADATA_KID_KEY = "kid";
|
||||||
|
private static final String METADATA_DOC_ID_KEY = "doc_id";
|
||||||
|
|
||||||
|
public QdrantVectorStoreStrategy(VectorStoreProperties vectorStoreProperties,
|
||||||
|
IChatModelService chatModelService,
|
||||||
|
EmbeddingModelFactory embeddingModelFactory) {
|
||||||
|
super(vectorStoreProperties, embeddingModelFactory, chatModelService);
|
||||||
|
}
|
||||||
|
|
||||||
|
private EmbeddingStore<TextSegment> getQdrantStore(String collectionName) {
|
||||||
|
VectorStoreProperties.Qdrant cfg = vectorStoreProperties.getQdrant();
|
||||||
|
QdrantEmbeddingStore.Builder builder = QdrantEmbeddingStore.builder()
|
||||||
|
.host(cfg.getHost())
|
||||||
|
.port(cfg.getPort())
|
||||||
|
.collectionName(collectionName)
|
||||||
|
.useTls(cfg.isUseTls());
|
||||||
|
if (cfg.getApiKey() != null && !cfg.getApiKey().isEmpty()) {
|
||||||
|
builder.apiKey(cfg.getApiKey());
|
||||||
|
}
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private QdrantClient buildQdrantClient() {
|
||||||
|
VectorStoreProperties.Qdrant cfg = vectorStoreProperties.getQdrant();
|
||||||
|
QdrantGrpcClient.Builder grpcBuilder = QdrantGrpcClient.newBuilder(cfg.getHost(), cfg.getPort(), cfg.isUseTls());
|
||||||
|
if (cfg.getApiKey() != null && !cfg.getApiKey().isEmpty()) {
|
||||||
|
grpcBuilder.withApiKey(cfg.getApiKey());
|
||||||
|
}
|
||||||
|
return new QdrantClient(grpcBuilder.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getModelDimension(String modelName) {
|
||||||
|
return chatModelService.selectModelByName(modelName).getModelDimension();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getVectorStoreType() {
|
||||||
|
return VECTOR_STORE_TYPE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void createSchema(String kid, String modelName) {
|
||||||
|
String collectionName = vectorStoreProperties.getQdrant().getCollectionname() + kid;
|
||||||
|
int dimension = getModelDimension(modelName);
|
||||||
|
try (QdrantClient client = buildQdrantClient()) {
|
||||||
|
Boolean exists = client.collectionExistsAsync(collectionName).get();
|
||||||
|
if (!exists) {
|
||||||
|
VectorParams params = VectorParams.newBuilder()
|
||||||
|
.setSize(dimension)
|
||||||
|
.setDistance(Distance.Cosine)
|
||||||
|
.build();
|
||||||
|
client.createCollectionAsync(collectionName, params).get();
|
||||||
|
log.info("Qdrant集合创建成功: {}, dimension: {}", collectionName, dimension);
|
||||||
|
} else {
|
||||||
|
log.info("Qdrant集合已存在: {}", collectionName);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Qdrant集合创建失败: {}", collectionName, e);
|
||||||
|
throw new ServiceException("Qdrant集合创建失败: " + collectionName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void storeEmbeddings(StoreEmbeddingBo storeEmbeddingBo) {
|
||||||
|
EmbeddingModel embeddingModel = getEmbeddingModel(storeEmbeddingBo.getEmbeddingModelName());
|
||||||
|
List<String> chunkList = storeEmbeddingBo.getChunkList();
|
||||||
|
List<String> fidList = storeEmbeddingBo.getFids();
|
||||||
|
String kid = storeEmbeddingBo.getKid();
|
||||||
|
String docId = storeEmbeddingBo.getDocId();
|
||||||
|
String collectionName = vectorStoreProperties.getQdrant().getCollectionname() + kid;
|
||||||
|
|
||||||
|
EmbeddingStore<TextSegment> embeddingStore = getQdrantStore(collectionName);
|
||||||
|
|
||||||
|
log.info("Qdrant向量存储条数记录: {}", chunkList.size());
|
||||||
|
long startTime = System.currentTimeMillis();
|
||||||
|
|
||||||
|
IntStream.range(0, chunkList.size()).forEach(i -> {
|
||||||
|
String text = chunkList.get(i);
|
||||||
|
String fid = fidList.get(i);
|
||||||
|
Metadata metadata = new Metadata();
|
||||||
|
metadata.put(METADATA_FID_KEY, fid);
|
||||||
|
metadata.put(METADATA_KID_KEY, kid);
|
||||||
|
metadata.put(METADATA_DOC_ID_KEY, docId);
|
||||||
|
TextSegment textSegment = TextSegment.from(text, metadata);
|
||||||
|
Embedding embedding = embeddingModel.embed(text).content();
|
||||||
|
embeddingStore.add(embedding, textSegment);
|
||||||
|
});
|
||||||
|
|
||||||
|
long endTime = System.currentTimeMillis();
|
||||||
|
log.info("Qdrant向量存储完成消耗时间:{}秒", (endTime - startTime) / 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> getQueryVector(QueryVectorBo queryVectorBo) {
|
||||||
|
EmbeddingModel embeddingModel = getEmbeddingModel(queryVectorBo.getEmbeddingModelName());
|
||||||
|
Embedding queryEmbedding = embeddingModel.embed(queryVectorBo.getQuery()).content();
|
||||||
|
String collectionName = vectorStoreProperties.getQdrant().getCollectionname() + queryVectorBo.getKid();
|
||||||
|
|
||||||
|
List<Float> vector = new ArrayList<>();
|
||||||
|
for (float f : queryEmbedding.vector()) {
|
||||||
|
vector.add(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
try (QdrantClient client = buildQdrantClient()) {
|
||||||
|
QueryPoints request = QueryPoints.newBuilder()
|
||||||
|
.setCollectionName(collectionName)
|
||||||
|
.setQuery(Query.newBuilder()
|
||||||
|
.setNearest(vectorInput(vector))
|
||||||
|
.build())
|
||||||
|
.setLimit(queryVectorBo.getMaxResults())
|
||||||
|
.setWithPayload(enable(true))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
List<ScoredPoint> results = client.queryAsync(request).get();
|
||||||
|
List<String> resultList = new ArrayList<>();
|
||||||
|
for (ScoredPoint point : results) {
|
||||||
|
JsonWithInt.Value textValue = point.getPayloadMap().get(TEXT_SEGMENT_KEY);
|
||||||
|
if (textValue != null && textValue.hasStringValue()) {
|
||||||
|
resultList.add(textValue.getStringValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return resultList;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Qdrant查询失败: {}", collectionName, e);
|
||||||
|
throw new ServiceException("Qdrant向量查询失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void removeById(String id, String modelName) {
|
||||||
|
String collectionName = vectorStoreProperties.getQdrant().getCollectionname() + id;
|
||||||
|
try (QdrantClient client = buildQdrantClient()) {
|
||||||
|
client.deleteCollectionAsync(collectionName).get();
|
||||||
|
log.info("Qdrant成功删除集合: {}", collectionName);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Qdrant删除集合失败: {}", collectionName, e);
|
||||||
|
throw new ServiceException("失败删除向量数据!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void removeByDocId(String docId, String kid) {
|
||||||
|
String collectionName = vectorStoreProperties.getQdrant().getCollectionname() + kid;
|
||||||
|
EmbeddingStore<TextSegment> embeddingStore = getQdrantStore(collectionName);
|
||||||
|
Filter filter = MetadataFilterBuilder.metadataKey(METADATA_DOC_ID_KEY).isEqualTo(docId);
|
||||||
|
embeddingStore.removeAll(filter);
|
||||||
|
log.info("Qdrant成功删除 docId={} 的所有向量数据", docId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void removeByFid(String fid, String kid) {
|
||||||
|
String collectionName = vectorStoreProperties.getQdrant().getCollectionname() + kid;
|
||||||
|
EmbeddingStore<TextSegment> embeddingStore = getQdrantStore(collectionName);
|
||||||
|
Filter filter = MetadataFilterBuilder.metadataKey(METADATA_FID_KEY).isEqualTo(fid);
|
||||||
|
embeddingStore.removeAll(filter);
|
||||||
|
log.info("Qdrant成功删除 fid={} 的所有向量数据", fid);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user