mirror of
https://gitcode.com/ageerle/ruoyi-ai.git
synced 2026-04-27 18:46:41 +00:00
Merge remote-tracking branch 'origin/main'
# Conflicts: # ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/bo/knowledge/KnowledgeInfoBo.java # ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/entity/knowledge/KnowledgeInfo.java # ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/vo/knowledge/KnowledgeInfoVo.java # ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/ChatServiceFacade.java
This commit is contained in:
@@ -103,6 +103,21 @@
|
||||
<version>${langchain4j.community.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- LangChain4j Skills - 技能模块 -->
|
||||
<dependency>
|
||||
<groupId>dev.langchain4j</groupId>
|
||||
<artifactId>langchain4j-skills</artifactId>
|
||||
<version>${langchain4j.community.version}</version>
|
||||
</dependency>
|
||||
|
||||
|
||||
<dependency>
|
||||
<groupId>dev.langchain4j</groupId>
|
||||
<artifactId>langchain4j-experimental-skills-shell</artifactId>
|
||||
<version>${langchain4j.community.version}</version>
|
||||
</dependency>
|
||||
|
||||
|
||||
<dependency>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>weaviate</artifactId>
|
||||
@@ -152,6 +167,30 @@
|
||||
<artifactId>mysql-connector-j</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 测试依赖 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Test dependencies -->
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-core</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-junit-jupiter</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
|
||||
@@ -6,7 +6,7 @@ import dev.langchain4j.service.UserMessage;
|
||||
import dev.langchain4j.service.V;
|
||||
|
||||
|
||||
public interface ChartGenerationAgent extends Agent {
|
||||
public interface ChartGenerationAgent {
|
||||
|
||||
@SystemMessage("""
|
||||
You are a chart generation specialist. Your only task is to generate Apache ECharts
|
||||
|
||||
@@ -14,7 +14,7 @@ public interface EchartsAgent {
|
||||
|
||||
@SystemMessage("""
|
||||
You are a data visualization assistant that generates Echarts chart configurations.
|
||||
|
||||
|
||||
CRITICAL OUTPUT REQUIREMENTS:
|
||||
- Return Echarts JSON wrapped in markdown code block
|
||||
- Use this exact format: ```json\n{...}\n```
|
||||
@@ -81,7 +81,7 @@ public interface EchartsAgent {
|
||||
""")
|
||||
@UserMessage("""
|
||||
Generate an Echarts chart for: {{query}}
|
||||
|
||||
|
||||
IMPORTANT: Return the Echarts configuration JSON wrapped in markdown code block (```json...```).
|
||||
""")
|
||||
@Agent("Data visualization assistant that returns Echarts JSON configurations for frontend rendering")
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package org.ruoyi.agent;
|
||||
|
||||
import dev.langchain4j.agentic.Agent;
|
||||
import dev.langchain4j.service.SystemMessage;
|
||||
import dev.langchain4j.service.UserMessage;
|
||||
import dev.langchain4j.service.V;
|
||||
|
||||
/**
|
||||
* 技能管理 Agent
|
||||
* 管理 docx、pdf、xlsx 等文档处理技能
|
||||
*
|
||||
* <p>可用技能:
|
||||
* <ul>
|
||||
* <li>docx - Word 文档创建、编辑和分析</li>
|
||||
* <li>pdf - PDF 文档处理、提取文本和表格</li>
|
||||
* <li>xlsx - Excel 电子表格创建、编辑和分析</li>
|
||||
* </ul>
|
||||
*
|
||||
* @author ageerle@163.com
|
||||
* @date 2026/04/10
|
||||
*/
|
||||
public interface SkillsAgent {
|
||||
|
||||
@SystemMessage("""
|
||||
你是一个文档处理技能助手,能够使用 activate_skill 工具激活特定技能来处理各种文档任务。
|
||||
使用指南:
|
||||
1. 根据用户请求判断需要哪个技能
|
||||
2. 使用 activate_skill("skill-name") 激活对应技能
|
||||
3. 按照技能指令执行任务
|
||||
4. 如果需要参考文件,使用 read_skill_resource 读取
|
||||
""")
|
||||
@UserMessage("{{query}}")
|
||||
@Agent("文档处理技能助手,支持 Word、PDF、Excel 文档的创建、编辑和分析")
|
||||
String process(@V("query") String query);
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import dev.langchain4j.service.V;
|
||||
* and returning relevant data and analysis results.
|
||||
*
|
||||
*/
|
||||
public interface SqlAgent extends Agent {
|
||||
public interface SqlAgent {
|
||||
|
||||
@SystemMessage("""
|
||||
This agent is designed for MySQL 5.7
|
||||
|
||||
@@ -6,33 +6,26 @@ import dev.langchain4j.service.UserMessage;
|
||||
import dev.langchain4j.service.V;
|
||||
|
||||
/**
|
||||
* Web Search Agent
|
||||
* A web search assistant that answers natural language questions by searching the internet
|
||||
* and returning relevant information from web pages.
|
||||
* 浏览器工具 Agent
|
||||
* 能够操作浏览器相关工具:网络搜索、网页抓取、浏览器自动化等
|
||||
*
|
||||
* @author ageerle@163.com
|
||||
* @date 2025/04/10
|
||||
*/
|
||||
public interface WebSearchAgent extends Agent {
|
||||
public interface WebSearchAgent {
|
||||
|
||||
@SystemMessage("""
|
||||
You are a web search assistant. Answer questions by searching and retrieving web content.
|
||||
你是一个系统工具助手,能够使用工具来帮助用户获取信息和操作浏览器。
|
||||
|
||||
Available tools:
|
||||
1. bing_search: Search the internet with keywords
|
||||
- query (required): search keywords
|
||||
- count (optional): number of results, default 10, max 50
|
||||
- offset (optional): pagination offset, default 0
|
||||
Returns: title, link, and summary for each result
|
||||
|
||||
2. crawl_webpage: Extract text content from a web page
|
||||
- url (required): web page URL
|
||||
Returns: cleaned page title and main content
|
||||
|
||||
Instructions:
|
||||
- Always cite sources in your answers
|
||||
- Only use the two tools listed above
|
||||
【最重要原则】
|
||||
除非用户明确要求使用浏览器查询信息,否则不要主动调用任何搜索或浏览器工具。
|
||||
使用指南:
|
||||
- 搜索信息时使用 bing_search
|
||||
- 需要详细网页内容时使用 crawl_webpage
|
||||
- 需要交互操作(登录、点击、填写表单)时使用 Playwright 工具
|
||||
- 在回答中注明信息来源
|
||||
""")
|
||||
@UserMessage("""
|
||||
Answer the following question by searching the web: {{query}}
|
||||
""")
|
||||
@Agent("Web search assistant using Bing search and web scraping to find and retrieve information")
|
||||
@UserMessage("{{query}}")
|
||||
@Agent("浏览器工具助手,支持网络搜索、网页抓取和浏览器自动化操作")
|
||||
String search(@V("query") String query);
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ public class ExecuteSqlQueryTool implements BuiltinToolProvider {
|
||||
@Tool("Execute a SELECT SQL query and return the results. Example: SELECT * FROM sys_user")
|
||||
public String executeSql(String sql) {
|
||||
// 2. 手动推入数据源上下文
|
||||
DynamicDataSourceContextHolder.push("agent");
|
||||
// DynamicDataSourceContextHolder.push("agent");
|
||||
if (sql == null || sql.trim().isEmpty()) {
|
||||
return "Error: SQL query cannot be empty";
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.ruoyi.common.chat.domain.dto.request.AgentChatRequest;
|
||||
import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
|
||||
import org.ruoyi.service.chat.impl.ChatServiceFacade;
|
||||
import org.springframework.stereotype.Controller;
|
||||
|
||||
@@ -9,6 +9,7 @@ import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import org.ruoyi.common.chat.service.chat.IChatModelService;
|
||||
import org.ruoyi.common.chat.domain.bo.chat.ChatModelBo;
|
||||
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
|
||||
import org.ruoyi.enums.ChatModeType;
|
||||
import org.ruoyi.enums.ModelType;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
@@ -23,6 +24,8 @@ import org.ruoyi.common.log.enums.BusinessType;
|
||||
import org.ruoyi.common.excel.utils.ExcelUtil;
|
||||
import org.ruoyi.common.mybatis.core.page.TableDataInfo;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
|
||||
/**
|
||||
* 模型管理
|
||||
*
|
||||
@@ -55,6 +58,21 @@ public class ChatModelController extends BaseController {
|
||||
return R.ok(chatModelService.queryList(bo));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取模型供应商枚举
|
||||
*/
|
||||
@GetMapping("/providerOptions")
|
||||
public R<List<LinkedHashMap<String, String>>> providerOptions() {
|
||||
List<LinkedHashMap<String, String>> options = new java.util.ArrayList<>();
|
||||
for (ChatModeType type : ChatModeType.values()) {
|
||||
LinkedHashMap<String, String> item = new LinkedHashMap<>();
|
||||
item.put("label", type.getDescription());
|
||||
item.put("value", type.getCode());
|
||||
options.add(item);
|
||||
}
|
||||
return R.ok(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出模型管理列表
|
||||
*/
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
package org.ruoyi.controller.knowledge;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.validation.constraints.*;
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import org.ruoyi.domain.bo.knowledge.KnowledgeGraphInstanceBo;
|
||||
import org.ruoyi.domain.vo.knowledge.KnowledgeGraphInstanceVo;
|
||||
import org.ruoyi.service.knowledge.IKnowledgeGraphInstanceService;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.ruoyi.common.idempotent.annotation.RepeatSubmit;
|
||||
import org.ruoyi.common.log.annotation.Log;
|
||||
import org.ruoyi.common.web.core.BaseController;
|
||||
import org.ruoyi.common.mybatis.core.page.PageQuery;
|
||||
import org.ruoyi.common.core.domain.R;
|
||||
import org.ruoyi.common.core.validate.AddGroup;
|
||||
import org.ruoyi.common.core.validate.EditGroup;
|
||||
import org.ruoyi.common.log.enums.BusinessType;
|
||||
import org.ruoyi.common.excel.utils.ExcelUtil;
|
||||
import org.ruoyi.common.mybatis.core.page.TableDataInfo;
|
||||
|
||||
/**
|
||||
* 知识图谱实例
|
||||
*
|
||||
* @author ageerle
|
||||
* @date 2025-12-17
|
||||
*/
|
||||
@Validated
|
||||
@RequiredArgsConstructor
|
||||
@RestController
|
||||
@RequestMapping("/system/graphInstance")
|
||||
public class KnowledgeGraphInstanceController extends BaseController {
|
||||
|
||||
private final IKnowledgeGraphInstanceService knowledgeGraphInstanceService;
|
||||
|
||||
/**
|
||||
* 查询知识图谱实例列表
|
||||
*/
|
||||
@SaCheckPermission("system:graphInstance:list")
|
||||
@GetMapping("/list")
|
||||
public TableDataInfo<KnowledgeGraphInstanceVo> list(KnowledgeGraphInstanceBo bo, PageQuery pageQuery) {
|
||||
return knowledgeGraphInstanceService.queryPageList(bo, pageQuery);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出知识图谱实例列表
|
||||
*/
|
||||
@SaCheckPermission("system:graphInstance:export")
|
||||
@Log(title = "知识图谱实例", businessType = BusinessType.EXPORT)
|
||||
@PostMapping("/export")
|
||||
public void export(KnowledgeGraphInstanceBo bo, HttpServletResponse response) {
|
||||
List<KnowledgeGraphInstanceVo> list = knowledgeGraphInstanceService.queryList(bo);
|
||||
ExcelUtil.exportExcel(list, "知识图谱实例", KnowledgeGraphInstanceVo.class, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取知识图谱实例详细信息
|
||||
*
|
||||
* @param id 主键
|
||||
*/
|
||||
@SaCheckPermission("system:graphInstance:query")
|
||||
@GetMapping("/{id}")
|
||||
public R<KnowledgeGraphInstanceVo> getInfo(@NotNull(message = "主键不能为空")
|
||||
@PathVariable Long id) {
|
||||
return R.ok(knowledgeGraphInstanceService.queryById(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增知识图谱实例
|
||||
*/
|
||||
@SaCheckPermission("system:graphInstance:add")
|
||||
@Log(title = "知识图谱实例", businessType = BusinessType.INSERT)
|
||||
@RepeatSubmit()
|
||||
@PostMapping()
|
||||
public R<Void> add(@Validated(AddGroup.class) @RequestBody KnowledgeGraphInstanceBo bo) {
|
||||
return toAjax(knowledgeGraphInstanceService.insertByBo(bo));
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改知识图谱实例
|
||||
*/
|
||||
@SaCheckPermission("system:graphInstance:edit")
|
||||
@Log(title = "知识图谱实例", businessType = BusinessType.UPDATE)
|
||||
@RepeatSubmit()
|
||||
@PutMapping()
|
||||
public R<Void> edit(@Validated(EditGroup.class) @RequestBody KnowledgeGraphInstanceBo bo) {
|
||||
return toAjax(knowledgeGraphInstanceService.updateByBo(bo));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除知识图谱实例
|
||||
*
|
||||
* @param ids 主键串
|
||||
*/
|
||||
@SaCheckPermission("system:graphInstance:remove")
|
||||
@Log(title = "知识图谱实例", businessType = BusinessType.DELETE)
|
||||
@DeleteMapping("/{ids}")
|
||||
public R<Void> remove(@NotEmpty(message = "主键不能为空")
|
||||
@PathVariable Long[] ids) {
|
||||
return toAjax(knowledgeGraphInstanceService.deleteWithValidByIds(List.of(ids), true));
|
||||
}
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
package org.ruoyi.controller.knowledge;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.validation.constraints.*;
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import org.ruoyi.domain.bo.knowledge.KnowledgeGraphSegmentBo;
|
||||
import org.ruoyi.domain.vo.knowledge.KnowledgeGraphSegmentVo;
|
||||
import org.ruoyi.service.knowledge.IKnowledgeGraphSegmentService;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.ruoyi.common.idempotent.annotation.RepeatSubmit;
|
||||
import org.ruoyi.common.log.annotation.Log;
|
||||
import org.ruoyi.common.web.core.BaseController;
|
||||
import org.ruoyi.common.mybatis.core.page.PageQuery;
|
||||
import org.ruoyi.common.core.domain.R;
|
||||
import org.ruoyi.common.core.validate.AddGroup;
|
||||
import org.ruoyi.common.core.validate.EditGroup;
|
||||
import org.ruoyi.common.log.enums.BusinessType;
|
||||
import org.ruoyi.common.excel.utils.ExcelUtil;
|
||||
import org.ruoyi.common.mybatis.core.page.TableDataInfo;
|
||||
|
||||
/**
|
||||
* 知识图谱片段
|
||||
*
|
||||
* @author ageerle
|
||||
* @date 2025-12-17
|
||||
*/
|
||||
@Validated
|
||||
@RequiredArgsConstructor
|
||||
@RestController
|
||||
@RequestMapping("/system/graphSegment")
|
||||
public class KnowledgeGraphSegmentController extends BaseController {
|
||||
|
||||
private final IKnowledgeGraphSegmentService knowledgeGraphSegmentService;
|
||||
|
||||
/**
|
||||
* 查询知识图谱片段列表
|
||||
*/
|
||||
@SaCheckPermission("system:graphSegment:list")
|
||||
@GetMapping("/list")
|
||||
public TableDataInfo<KnowledgeGraphSegmentVo> list(KnowledgeGraphSegmentBo bo, PageQuery pageQuery) {
|
||||
return knowledgeGraphSegmentService.queryPageList(bo, pageQuery);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出知识图谱片段列表
|
||||
*/
|
||||
@SaCheckPermission("system:graphSegment:export")
|
||||
@Log(title = "知识图谱片段", businessType = BusinessType.EXPORT)
|
||||
@PostMapping("/export")
|
||||
public void export(KnowledgeGraphSegmentBo bo, HttpServletResponse response) {
|
||||
List<KnowledgeGraphSegmentVo> list = knowledgeGraphSegmentService.queryList(bo);
|
||||
ExcelUtil.exportExcel(list, "知识图谱片段", KnowledgeGraphSegmentVo.class, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取知识图谱片段详细信息
|
||||
*
|
||||
* @param id 主键
|
||||
*/
|
||||
@SaCheckPermission("system:graphSegment:query")
|
||||
@GetMapping("/{id}")
|
||||
public R<KnowledgeGraphSegmentVo> getInfo(@NotNull(message = "主键不能为空")
|
||||
@PathVariable Long id) {
|
||||
return R.ok(knowledgeGraphSegmentService.queryById(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增知识图谱片段
|
||||
*/
|
||||
@SaCheckPermission("system:graphSegment:add")
|
||||
@Log(title = "知识图谱片段", businessType = BusinessType.INSERT)
|
||||
@RepeatSubmit()
|
||||
@PostMapping()
|
||||
public R<Void> add(@Validated(AddGroup.class) @RequestBody KnowledgeGraphSegmentBo bo) {
|
||||
return toAjax(knowledgeGraphSegmentService.insertByBo(bo));
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改知识图谱片段
|
||||
*/
|
||||
@SaCheckPermission("system:graphSegment:edit")
|
||||
@Log(title = "知识图谱片段", businessType = BusinessType.UPDATE)
|
||||
@RepeatSubmit()
|
||||
@PutMapping()
|
||||
public R<Void> edit(@Validated(EditGroup.class) @RequestBody KnowledgeGraphSegmentBo bo) {
|
||||
return toAjax(knowledgeGraphSegmentService.updateByBo(bo));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除知识图谱片段
|
||||
*
|
||||
* @param ids 主键串
|
||||
*/
|
||||
@SaCheckPermission("system:graphSegment:remove")
|
||||
@Log(title = "知识图谱片段", businessType = BusinessType.DELETE)
|
||||
@DeleteMapping("/{ids}")
|
||||
public R<Void> remove(@NotEmpty(message = "主键不能为空")
|
||||
@PathVariable Long[] ids) {
|
||||
return toAjax(knowledgeGraphSegmentService.deleteWithValidByIds(List.of(ids), true));
|
||||
}
|
||||
}
|
||||
@@ -78,29 +78,32 @@ public class KnowledgeInfoBo extends BaseEntity {
|
||||
private String embeddingModel;
|
||||
|
||||
/**
|
||||
* 重排模型
|
||||
* 是否启用重排序(0 否 1是)
|
||||
*/
|
||||
private Integer enableRerank;
|
||||
|
||||
/**
|
||||
* 重排序模型名称
|
||||
*/
|
||||
private String rerankModel;
|
||||
|
||||
/**
|
||||
* 是否启用重排(0 否 1 是)
|
||||
* 重排序后返回的文档数量
|
||||
*/
|
||||
private Integer enableRerank;
|
||||
private Integer rerankTopN;
|
||||
|
||||
/**
|
||||
* 重排序相关性分数阈值
|
||||
*/
|
||||
private Double rerankScoreThreshold;
|
||||
|
||||
|
||||
/**
|
||||
* 备注
|
||||
*/
|
||||
private String remark;
|
||||
|
||||
/**
|
||||
* 是否启用混合检索(0 否 1 是)
|
||||
*/
|
||||
private Integer enableHybrid;
|
||||
|
||||
/**
|
||||
* 混合检索权重比例 (0.0-1.0)
|
||||
*/
|
||||
private Double hybridAlpha;
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package org.ruoyi.domain.bo.rerank;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 重排序请求参数
|
||||
*
|
||||
* @author yang
|
||||
* @date 2026-04-19
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class RerankRequest {
|
||||
|
||||
/**
|
||||
* 查询文本
|
||||
*/
|
||||
private String query;
|
||||
|
||||
/**
|
||||
* 候选文档列表
|
||||
*/
|
||||
private List<String> documents;
|
||||
|
||||
/**
|
||||
* 返回的文档数量(topN)
|
||||
* 如果不指定,默认返回所有文档
|
||||
*/
|
||||
private Integer topN;
|
||||
|
||||
/**
|
||||
* 是否返回原始文档内容
|
||||
* 默认为 true
|
||||
*/
|
||||
@Builder.Default
|
||||
private Boolean returnDocuments = true;
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package org.ruoyi.domain.bo.rerank;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 重排序结果
|
||||
*
|
||||
* @author yang
|
||||
* @date 2026-04-19
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class RerankResult {
|
||||
|
||||
/**
|
||||
* 重排序后的文档结果列表
|
||||
*/
|
||||
private List<RerankDocument> documents;
|
||||
|
||||
/**
|
||||
* 原始请求中的文档总数
|
||||
*/
|
||||
private Integer totalDocuments;
|
||||
|
||||
/**
|
||||
* 重排序耗时(毫秒)
|
||||
*/
|
||||
private Long durationMs;
|
||||
|
||||
/**
|
||||
* 单个重排序文档结果
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class RerankDocument {
|
||||
|
||||
/**
|
||||
* 文档在原始列表中的索引位置
|
||||
*/
|
||||
private Integer index;
|
||||
|
||||
/**
|
||||
* 相关性分数(通常 0-1 之间,越高越相关)
|
||||
*/
|
||||
private Double relevanceScore;
|
||||
|
||||
/**
|
||||
* 文档内容
|
||||
*/
|
||||
private String document;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建空结果
|
||||
*/
|
||||
public static RerankResult empty() {
|
||||
return RerankResult.builder()
|
||||
.documents(List.of())
|
||||
.totalDocuments(0)
|
||||
.durationMs(0L)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -51,4 +51,30 @@ public class QueryVectorBo {
|
||||
*/
|
||||
private String baseUrl;
|
||||
|
||||
|
||||
// ========== 重排序相关参数 ==========
|
||||
|
||||
/**
|
||||
* 是否启用重排序
|
||||
* 默认为 false
|
||||
*/
|
||||
private Boolean enableRerank = false;
|
||||
|
||||
/**
|
||||
* 重排序模型名称
|
||||
*/
|
||||
private String rerankModelName;
|
||||
|
||||
/**
|
||||
* 重排序后返回的文档数量(topN)
|
||||
* 如果不指定,默认与 maxResults 相同
|
||||
*/
|
||||
private Integer rerankTopN;
|
||||
|
||||
/**
|
||||
* 重排序相关性分数阈值
|
||||
* 低于此阈值的文档将被过滤
|
||||
*/
|
||||
private Double rerankScoreThreshold;
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package org.ruoyi.domain.dto.request;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 阿里百炼重排序请求DTO(OpenAI兼容格式)
|
||||
*
|
||||
* @author yang
|
||||
* @date 2026-04-20
|
||||
*/
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public record AliBaiLianRerankRequest(
|
||||
String model,
|
||||
List<String> documents,
|
||||
String query,
|
||||
@JsonProperty("top_n")
|
||||
Integer topN,
|
||||
String instruct,
|
||||
@JsonProperty("return_documents")
|
||||
Boolean returnDocuments
|
||||
) {
|
||||
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
||||
|
||||
/**
|
||||
* 创建文本重排序请求
|
||||
*/
|
||||
public static AliBaiLianRerankRequest create(String modelName, String query,
|
||||
List<String> documents, Integer topN,
|
||||
Boolean returnDocuments) {
|
||||
return new AliBaiLianRerankRequest(
|
||||
modelName,
|
||||
documents,
|
||||
query,
|
||||
topN != null ? topN : documents.size(),
|
||||
null,
|
||||
returnDocuments != null ? returnDocuments : true
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为JSON字符串
|
||||
*/
|
||||
public String toJson() {
|
||||
try {
|
||||
return OBJECT_MAPPER.writeValueAsString(this);
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new IllegalArgumentException("序列化阿里百炼重排序请求失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package org.ruoyi.domain.dto.request;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 智谱AI重排序请求DTO
|
||||
*
|
||||
* @author yang
|
||||
* @date 2026-04-19
|
||||
*/
|
||||
public record ZhipuRerankRequest(
|
||||
String model,
|
||||
String query,
|
||||
List<String> documents,
|
||||
Integer top_n,
|
||||
Boolean return_documents
|
||||
) {
|
||||
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
||||
|
||||
/**
|
||||
* 创建智谱重排序请求
|
||||
*/
|
||||
public static ZhipuRerankRequest create(String modelName, String query,
|
||||
List<String> documents, Integer topN,
|
||||
Boolean returnDocuments) {
|
||||
return new ZhipuRerankRequest(
|
||||
modelName,
|
||||
query,
|
||||
documents,
|
||||
topN != null ? topN : documents.size(),
|
||||
returnDocuments != null ? returnDocuments : true
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为JSON字符串
|
||||
*/
|
||||
public String toJson() {
|
||||
try {
|
||||
return OBJECT_MAPPER.writeValueAsString(this);
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new IllegalArgumentException("序列化智谱重排序请求失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package org.ruoyi.domain.dto.response;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import org.ruoyi.domain.bo.rerank.RerankResult;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 阿里百炼重排序响应DTO(OpenAI兼容格式)
|
||||
*
|
||||
* @author yang
|
||||
* @date 2026-04-20
|
||||
*/
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public record AliBaiLianRerankResponse(
|
||||
String id,
|
||||
String object,
|
||||
List<ResultItem> results,
|
||||
UsageInfo usage
|
||||
) {
|
||||
/**
|
||||
* 单个重排序结果项
|
||||
*/
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public record ResultItem(
|
||||
Integer index,
|
||||
@JsonProperty("relevance_score")
|
||||
Double relevanceScore,
|
||||
Object document
|
||||
) {
|
||||
/**
|
||||
* 获取文档文本内容
|
||||
*/
|
||||
public String getDocumentText() {
|
||||
if (document == null) return null;
|
||||
if (document instanceof String) return (String) document;
|
||||
if (document instanceof Map) {
|
||||
Object text = ((Map<?, ?>) document).get("text");
|
||||
return text != null ? text.toString() : null;
|
||||
}
|
||||
return document.toString();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Token使用信息
|
||||
*/
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public record UsageInfo(
|
||||
@JsonProperty("total_tokens")
|
||||
Integer totalTokens,
|
||||
@JsonProperty("prompt_tokens")
|
||||
Integer promptTokens
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 转换为通用RerankResult
|
||||
*/
|
||||
public RerankResult toRerankResult(int totalDocs, long durationMs) {
|
||||
if (results == null || results.isEmpty()) {
|
||||
return RerankResult.empty();
|
||||
}
|
||||
|
||||
List<RerankResult.RerankDocument> documents = results.stream()
|
||||
.map(item -> RerankResult.RerankDocument.builder()
|
||||
.index(item.index())
|
||||
.relevanceScore(item.relevanceScore())
|
||||
.document(item.getDocumentText())
|
||||
.build())
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return RerankResult.builder()
|
||||
.documents(documents)
|
||||
.totalDocuments(totalDocs)
|
||||
.durationMs(durationMs)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package org.ruoyi.domain.dto.response;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import org.ruoyi.domain.bo.rerank.RerankResult;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 智谱AI重排序响应DTO
|
||||
*
|
||||
* @author yang
|
||||
* @date 2026-04-19
|
||||
*/
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public record ZhipuRerankResponse(
|
||||
String model,
|
||||
String object,
|
||||
List<ResultItem> results,
|
||||
UsageInfo usage
|
||||
) {
|
||||
/**
|
||||
* 单个重排序结果项
|
||||
*/
|
||||
public record ResultItem(
|
||||
Integer index,
|
||||
@JsonProperty("relevance_score")
|
||||
Double relevanceScore,
|
||||
String document
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Token使用信息
|
||||
*/
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public record UsageInfo(
|
||||
@JsonProperty("total_tokens")
|
||||
Integer totalTokens,
|
||||
@JsonProperty("input_tokens")
|
||||
Integer inputTokens,
|
||||
@JsonProperty("output_tokens")
|
||||
Integer outputTokens
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 转换为通用RerankResult
|
||||
*/
|
||||
public RerankResult toRerankResult(int totalDocs, long durationMs) {
|
||||
if (results == null || results.isEmpty()) {
|
||||
return RerankResult.empty();
|
||||
}
|
||||
|
||||
List<RerankResult.RerankDocument> documents = results.stream()
|
||||
.map(item -> RerankResult.RerankDocument.builder()
|
||||
.index(item.index())
|
||||
.relevanceScore(item.relevanceScore())
|
||||
.document(item.document())
|
||||
.build())
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return RerankResult.builder()
|
||||
.documents(documents)
|
||||
.totalDocuments(totalDocs)
|
||||
.durationMs(durationMs)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -79,29 +79,29 @@ public class KnowledgeInfo extends BaseEntity {
|
||||
private String embeddingModel;
|
||||
|
||||
/**
|
||||
* 重排模型
|
||||
*/
|
||||
private String rerankModel;
|
||||
|
||||
/**
|
||||
* 是否启用重排(0 否 1 是)
|
||||
* 是否启用重排序(0 否 1是)
|
||||
*/
|
||||
private Integer enableRerank;
|
||||
|
||||
/**
|
||||
* 重排序模型名称
|
||||
*/
|
||||
private String rerankModel;
|
||||
|
||||
/**
|
||||
* 重排序后返回的文档数量
|
||||
*/
|
||||
private Integer rerankTopN;
|
||||
|
||||
/**
|
||||
* 重排序相关性分数阈值
|
||||
*/
|
||||
private Double rerankScoreThreshold;
|
||||
|
||||
/**
|
||||
* 备注
|
||||
*/
|
||||
private String remark;
|
||||
|
||||
/**
|
||||
* 是否启用混合检索(0 否 1 是)
|
||||
*/
|
||||
private Integer enableHybrid;
|
||||
|
||||
/**
|
||||
* 混合检索权重比例 (0.0-1.0)
|
||||
*/
|
||||
private Double hybridAlpha;
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -95,17 +95,28 @@ public class KnowledgeInfoVo implements Serializable {
|
||||
private String embeddingModel;
|
||||
|
||||
/**
|
||||
* 重排模型
|
||||
* 是否启用重排序(0 否 1是)
|
||||
*/
|
||||
@ExcelProperty(value = "重排模型")
|
||||
@ExcelProperty(value = "是否启用重排序")
|
||||
private Integer enableRerank;
|
||||
|
||||
/**
|
||||
* 重排序模型名称
|
||||
*/
|
||||
@ExcelProperty(value = "重排序模型")
|
||||
private String rerankModel;
|
||||
|
||||
/**
|
||||
* 是否启用重排(0 否 1 是)
|
||||
* 重排序后返回的文档数量
|
||||
*/
|
||||
@ExcelProperty(value = "是否启用重排", converter = ExcelDictConvert.class)
|
||||
@ExcelDictFormat(readConverterExp = "0=否,1=是")
|
||||
private Integer enableRerank;
|
||||
@ExcelProperty(value = "重排序返回数量")
|
||||
private Integer rerankTopN;
|
||||
|
||||
/**
|
||||
* 重排序相关性分数阈值
|
||||
*/
|
||||
@ExcelProperty(value = "重排序分数阈值")
|
||||
private Double rerankScoreThreshold;
|
||||
|
||||
/**
|
||||
* 备注
|
||||
@@ -113,23 +124,5 @@ public class KnowledgeInfoVo implements Serializable {
|
||||
@ExcelProperty(value = "备注")
|
||||
private String remark;
|
||||
|
||||
/**
|
||||
* 是否启用混合检索(0 否 1 是)
|
||||
*/
|
||||
@ExcelProperty(value = "是否启用混合检索", converter = ExcelDictConvert.class)
|
||||
@ExcelDictFormat(readConverterExp = "0=否,1=是")
|
||||
private Integer enableHybrid;
|
||||
|
||||
/**
|
||||
* 混合检索权重比例 (0.0-1.0)
|
||||
*/
|
||||
@ExcelProperty(value = "混合检索权重比例")
|
||||
private Double hybridAlpha;
|
||||
|
||||
/**
|
||||
* 文档数(统计字段,非数据库列)
|
||||
*/
|
||||
private Integer documentCount;
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -15,7 +15,10 @@ public enum ChatModeType {
|
||||
DEEP_SEEK("deepseek", "深度求索"),
|
||||
QIAN_WEN("qianwen", "通义千问"),
|
||||
OPEN_AI("openai", "openai"),
|
||||
PPIO("ppio", "ppio");
|
||||
PPIO("ppio", "ppio"),
|
||||
CUSTOM_API("custom_api", "自定义API"),
|
||||
MINIMAX("minimax", "MiniMax"),
|
||||
XIAOMI("xiaomi", "小米MiMo");
|
||||
private final String code;
|
||||
private final String description;
|
||||
|
||||
|
||||
@@ -2,8 +2,9 @@ package org.ruoyi.factory;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.ruoyi.common.chat.service.chat.IChatModelService;
|
||||
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
|
||||
import org.ruoyi.common.chat.service.chat.IChatModelService;
|
||||
import org.ruoyi.observability.EmbeddingModelListenerProvider;
|
||||
import org.ruoyi.service.embed.BaseEmbedModelService;
|
||||
import org.ruoyi.service.embed.MultiModalEmbedModelService;
|
||||
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
|
||||
@@ -27,6 +28,7 @@ public class EmbeddingModelFactory {
|
||||
private final ApplicationContext applicationContext;
|
||||
|
||||
private final IChatModelService chatModelService;
|
||||
private final EmbeddingModelListenerProvider embeddingModelListenerProvider;
|
||||
|
||||
// 模型缓存,使用ConcurrentHashMap保证线程安全
|
||||
private final Map<String, BaseEmbedModelService> modelCache = new ConcurrentHashMap<>();
|
||||
@@ -109,6 +111,8 @@ public class EmbeddingModelFactory {
|
||||
BaseEmbedModelService model = applicationContext.getBean(factory, BaseEmbedModelService.class);
|
||||
// 配置模型参数
|
||||
model.configure(config);
|
||||
// 增加嵌入模型监听器
|
||||
model.addListeners(embeddingModelListenerProvider.getEmbeddingModelListeners());
|
||||
log.info("成功创建嵌入模型: factory={}, modelId={}", config.getProviderCode(), config.getId());
|
||||
return model;
|
||||
} catch (NoSuchBeanDefinitionException e) {
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
package org.ruoyi.factory;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
|
||||
import org.ruoyi.common.chat.service.chat.IChatModelService;
|
||||
import org.ruoyi.service.rerank.RerankModelService;
|
||||
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 重排序模型工厂服务类
|
||||
* 参考设计模式:EmbeddingModelFactory
|
||||
* 负责创建和管理重排序模型实例
|
||||
*
|
||||
* @author yang
|
||||
* @date 2026-04-19
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class RerankModelFactory {
|
||||
|
||||
private final ApplicationContext applicationContext;
|
||||
|
||||
private final IChatModelService chatModelService;
|
||||
|
||||
/**
|
||||
* 模型缓存,使用ConcurrentHashMap保证线程安全
|
||||
*/
|
||||
private final Map<String, RerankModelService> modelCache = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 创建重排序模型实例
|
||||
* 如果模型已存在于缓存中,则直接返回;否则创建新的实例
|
||||
*
|
||||
* @param rerankModelName 重排序模型名称
|
||||
*/
|
||||
public RerankModelService createModel(String rerankModelName) {
|
||||
return modelCache.computeIfAbsent(rerankModelName, name -> {
|
||||
ChatModelVo modelConfig = chatModelService.selectModelByName(rerankModelName);
|
||||
|
||||
if (modelConfig == null) {
|
||||
throw new IllegalArgumentException("未找到重排序模型配置,name=" + name);
|
||||
}
|
||||
return createModelInstance(modelConfig.getProviderCode(), modelConfig);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新模型缓存
|
||||
* 根据给定的模型ID从缓存中移除对应的模型
|
||||
*
|
||||
* @param modelId 模型的唯一标识ID
|
||||
*/
|
||||
public void refreshModel(Long modelId) {
|
||||
modelCache.remove(modelId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有支持模型工厂的列表
|
||||
*
|
||||
* @return 支持的模型工厂名称列表
|
||||
*/
|
||||
public List<String> getSupportedFactories() {
|
||||
return new ArrayList<>(applicationContext.getBeansOfType(RerankModelService.class)
|
||||
.keySet());
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建具体的模型实例
|
||||
* 根据提供的工厂名称和配置信息创建并配置模型实例
|
||||
*
|
||||
* @param factory 工厂名称,用于标识模型类型(providerCode)
|
||||
* @param config 模型配置信息
|
||||
* @return RerankModelService 配置好的模型实例
|
||||
* @throws IllegalArgumentException 当无法获取指定的模型实例时抛出
|
||||
*/
|
||||
private RerankModelService createModelInstance(String factory, ChatModelVo config) {
|
||||
try {
|
||||
// 优先尝试使用 providerCode + "Rerank" 作为 Bean 名称
|
||||
// 例如:zhipu -> zhipuRerank,jina -> jinaRerank
|
||||
String rerankBeanName = factory + "Rerank";
|
||||
RerankModelService model = applicationContext.getBean(rerankBeanName, RerankModelService.class);
|
||||
model.configure(config);
|
||||
log.info("成功创建重排序模型: factory={}, modelName={}", rerankBeanName, config.getModelName());
|
||||
return model;
|
||||
} catch (NoSuchBeanDefinitionException e) {
|
||||
// 如果找不到,尝试使用原始的 providerCode
|
||||
try {
|
||||
RerankModelService model = applicationContext.getBean(factory, RerankModelService.class);
|
||||
model.configure(config);
|
||||
log.info("成功创建重排序模型: factory={}, modelName={}", factory, config.getModelName());
|
||||
return model;
|
||||
} catch (NoSuchBeanDefinitionException ex) {
|
||||
throw new IllegalArgumentException("获取不到重排序模型: " + factory + " 或 " + factory + "Rerank", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,173 @@
|
||||
package org.ruoyi.observability;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
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.*;
|
||||
import dev.langchain4j.service.tool.ToolExecutionResult;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.ruoyi.common.sse.dto.SseEventDto;
|
||||
import org.ruoyi.common.sse.utils.SseMessageUtils;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* MCP 客户端监听器
|
||||
* <p>
|
||||
* 监听 MCP 工具执行事件,并通过 SSE 推送到前端
|
||||
* <p>
|
||||
* <b>SSE 推送格式:</b>
|
||||
* <pre>
|
||||
* {
|
||||
* "event": "mcp",
|
||||
* "content": "{\"name\":\"工具名称\",\"status\":\"pending|success|error\",\"result\":\"执行结果\"}"
|
||||
* }
|
||||
* </pre>
|
||||
* <b>前端区分方式:</b>
|
||||
* <ul>
|
||||
* <li>对话内容:event="content"</li>
|
||||
* <li>MCP 事件:event="mcp"</li>
|
||||
* </ul>
|
||||
*
|
||||
* @author evo
|
||||
*/
|
||||
@Slf4j
|
||||
public class MyMcpClientListener implements McpClientListener {
|
||||
|
||||
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
||||
|
||||
private final Long userId;
|
||||
|
||||
public MyMcpClientListener(Long userId) {
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
public MyMcpClientListener() {
|
||||
this.userId = null;
|
||||
}
|
||||
|
||||
// ==================== 工具执行 ====================
|
||||
@Override
|
||||
public void beforeExecuteTool(McpCallContext context) {
|
||||
McpClientRequest message = (McpClientRequest) context.message();
|
||||
McpClientParams params = message.getParams();
|
||||
if (params instanceof McpCallToolParams callToolParams) {
|
||||
String name = callToolParams.getName();
|
||||
log.info("工具调用之前:{}",name);
|
||||
pushMcpEvent(name, "pending", null);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterExecuteTool(McpCallContext context, ToolExecutionResult result, Map<String, Object> rawResult) {
|
||||
McpClientRequest message = (McpClientRequest) context.message();
|
||||
McpClientParams params = message.getParams();
|
||||
if (params instanceof McpCallToolParams callToolParams) {
|
||||
String name = callToolParams.getName();
|
||||
String resultText = result != null ? result.toString() : "";
|
||||
log.info("工具调用之后:{},返回结果{}",name,result);
|
||||
pushMcpEvent(name, "success", truncate(resultText, 500));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExecuteToolError(McpCallContext context, Throwable error) {
|
||||
String toolName = getMethodName(context);
|
||||
log.error("【MCP工具执行错误】工具: {}, 错误: {}", toolName, error.getMessage());
|
||||
pushMcpEvent(toolName, "error", error.getMessage());
|
||||
}
|
||||
|
||||
// ==================== 资源读取 ====================
|
||||
|
||||
@Override
|
||||
public void beforeResourceGet(McpCallContext context) {
|
||||
String name = getMethodName(context);
|
||||
log.info("【MCP资源读取前】资源: {}", name);
|
||||
pushMcpEvent(name, "pending", null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterResourceGet(McpCallContext context, McpReadResourceResult result, Map<String, Object> rawResult) {
|
||||
String name = getMethodName(context);
|
||||
int count = result.contents() != null ? result.contents().size() : 0;
|
||||
log.info("【MCP资源读取后】资源: {}, 数量: {}", name, count);
|
||||
pushMcpEvent(name, "success", "读取 " + count + " 条资源");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResourceGetError(McpCallContext context, Throwable error) {
|
||||
String name = getMethodName(context);
|
||||
log.error("【MCP资源读取错误】资源: {}, 错误: {}", name, error.getMessage());
|
||||
pushMcpEvent(name, "error", error.getMessage());
|
||||
}
|
||||
|
||||
// ==================== 提示词获取 ====================
|
||||
|
||||
@Override
|
||||
public void beforePromptGet(McpCallContext context) {
|
||||
String name = getMethodName(context);
|
||||
log.info("【MCP提示词获取前】提示词: {}", name);
|
||||
pushMcpEvent(name, "pending", null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterPromptGet(McpCallContext context, McpGetPromptResult result, Map<String, Object> rawResult) {
|
||||
String name = getMethodName(context);
|
||||
int count = result.messages() != null ? result.messages().size() : 0;
|
||||
log.info("【MCP提示词获取后】提示词: {}, 消息数: {}", name, count);
|
||||
pushMcpEvent(name, "success", "获取 " + count + " 条消息");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPromptGetError(McpCallContext context, Throwable error) {
|
||||
String name = getMethodName(context);
|
||||
log.error("【MCP提示词获取错误】提示词: {}, 错误: {}", name, error.getMessage());
|
||||
pushMcpEvent(name, "error", error.getMessage());
|
||||
}
|
||||
|
||||
// ==================== 辅助方法 ====================
|
||||
|
||||
private String getMethodName(McpCallContext context) {
|
||||
try {
|
||||
McpClientMessage message = context.message();
|
||||
return message.method != null ? message.method.toString() : "unknown";
|
||||
} catch (Exception e) {
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 推送 MCP 事件到前端
|
||||
*/
|
||||
private void pushMcpEvent(String name, String status, String result) {
|
||||
if (userId == null) {
|
||||
log.warn("userId 为空,无法推送 MCP 事件");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Map<String, Object> content = new HashMap<>();
|
||||
content.put("name", name);
|
||||
content.put("status", status);
|
||||
content.put("result", result);
|
||||
|
||||
String json = OBJECT_MAPPER.writeValueAsString(content);
|
||||
SseMessageUtils.sendEvent(userId, SseEventDto.builder()
|
||||
.event("mcp")
|
||||
.content(json)
|
||||
.build());
|
||||
} catch (JsonProcessingException e) {
|
||||
log.error("序列化 MCP 事件失败: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private String truncate(String str, int maxLen) {
|
||||
if (str == null) return null;
|
||||
return str.length() > maxLen ? str.substring(0, maxLen) + "..." : str;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
package org.ruoyi.observability;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* 跨线程事件总线
|
||||
*
|
||||
* 写入端(异步线程):StreamingOutputWrapper / SupervisorStreamListener
|
||||
* 读取端(SSE 线程):ChatServiceFacade.drain
|
||||
*
|
||||
* 调用链路:
|
||||
* SSE请求 -> 创建 OutputChannel
|
||||
* -> Supervisor.invoke() [同步阻塞调用子Agent]
|
||||
* ├── SupervisorStreamListener -> channel.send()
|
||||
* └── searchAgent.search()
|
||||
* └── StreamingOutputWrapper -> channel.send() [每个token]
|
||||
* -> channel.complete()
|
||||
* drain线程 -> channel.drain() -> SSE实时推送
|
||||
*
|
||||
* @author ageerle@163.com
|
||||
* @date 2025/04/10
|
||||
*/
|
||||
public class OutputChannel {
|
||||
|
||||
private static final String DONE = "__DONE__";
|
||||
private static final Map<String, OutputChannel> REGISTRY = new ConcurrentHashMap<>();
|
||||
|
||||
private final BlockingQueue<String> queue = new LinkedBlockingQueue<>(4096);
|
||||
private final AtomicReference<Throwable> error = new AtomicReference<>();
|
||||
private final CountDownLatch completed = new CountDownLatch(1);
|
||||
|
||||
/**
|
||||
* 创建并注册到全局注册表
|
||||
*/
|
||||
public static OutputChannel create(String requestId) {
|
||||
OutputChannel ch = new OutputChannel();
|
||||
REGISTRY.put(requestId, ch);
|
||||
return ch;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从全局注册表移除
|
||||
*/
|
||||
public static void remove(String requestId) {
|
||||
REGISTRY.remove(requestId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从全局注册表获取
|
||||
*/
|
||||
public static OutputChannel get(String requestId) {
|
||||
return REGISTRY.get(requestId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入:线程安全,非阻塞,队列满时丢弃
|
||||
*/
|
||||
public void send(String text) {
|
||||
if (text == null || text.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (!queue.offer(text, 100, TimeUnit.MILLISECONDS)) {
|
||||
System.err.println("[OutputChannel] 队列满,丢弃消息: " + truncate(text, 100));
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记完成
|
||||
*/
|
||||
public void complete() {
|
||||
queue.offer(DONE);
|
||||
completed.countDown();
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记错误完成
|
||||
*/
|
||||
public void completeWithError(Throwable t) {
|
||||
error.set(t);
|
||||
queue.offer("\n[错误] 致命错误: " + t.getMessage());
|
||||
queue.offer(DONE);
|
||||
completed.countDown();
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取:阻塞迭代,配合 SSE 使用
|
||||
*/
|
||||
public void drain(Consumer<String> emitter) throws InterruptedException {
|
||||
while (true) {
|
||||
String msg = queue.poll(200, TimeUnit.MILLISECONDS);
|
||||
if (msg != null) {
|
||||
if (DONE.equals(msg)) {
|
||||
break;
|
||||
}
|
||||
emitter.accept(msg);
|
||||
} else {
|
||||
if (completed.getCount() == 0 && queue.isEmpty()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Throwable t = error.get();
|
||||
if (t != null && !(t instanceof InterruptedException)) {
|
||||
throw new RuntimeException("Agent 执行出错", t);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否已完成
|
||||
*/
|
||||
public boolean isCompleted() {
|
||||
return completed.getCount() == 0;
|
||||
}
|
||||
|
||||
private String truncate(String s, int maxLen) {
|
||||
if (s == null) {
|
||||
return "null";
|
||||
}
|
||||
return s.length() > maxLen ? s.substring(0, maxLen) + "..." : s;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
package org.ruoyi.observability;
|
||||
|
||||
import dev.langchain4j.model.chat.ChatModel;
|
||||
import dev.langchain4j.model.chat.StreamingChatModel;
|
||||
import dev.langchain4j.model.chat.listener.ChatModelListener;
|
||||
import dev.langchain4j.model.chat.request.ChatRequest;
|
||||
import dev.langchain4j.model.chat.request.ChatRequestParameters;
|
||||
import dev.langchain4j.model.chat.response.ChatResponse;
|
||||
import dev.langchain4j.model.chat.response.PartialThinking;
|
||||
import dev.langchain4j.model.chat.response.PartialThinkingContext;
|
||||
import dev.langchain4j.model.chat.response.PartialToolCall;
|
||||
import dev.langchain4j.model.chat.response.PartialToolCallContext;
|
||||
import dev.langchain4j.model.chat.response.PartialResponse;
|
||||
import dev.langchain4j.model.chat.response.PartialResponseContext;
|
||||
import dev.langchain4j.model.chat.response.StreamingChatResponseHandler;
|
||||
import dev.langchain4j.model.chat.Capability;
|
||||
import dev.langchain4j.model.ModelProvider;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
/**
|
||||
* 包装 StreamingChatModel,同时实现 ChatModel 接口。
|
||||
*
|
||||
* 当 AI Service 方法返回 String 时,LangChain4j 使用 ChatModel.chat()
|
||||
* 当返回 TokenStream 时,使用 StreamingChatModel.chat()
|
||||
*
|
||||
* 此包装器同时实现两个接口,将同步调用转换为流式调用并收集结果,
|
||||
* 同时拦截每个 token 推送到 OutputChannel。
|
||||
*
|
||||
* @author ageerle@163.com
|
||||
* @date 2025/04/10
|
||||
*/
|
||||
@Slf4j
|
||||
public class StreamingOutputWrapper implements StreamingChatModel, ChatModel {
|
||||
|
||||
private final StreamingChatModel streamingDelegate;
|
||||
private final OutputChannel channel;
|
||||
|
||||
/**
|
||||
* 包装 StreamingChatModel
|
||||
*/
|
||||
public StreamingOutputWrapper(StreamingChatModel delegate, OutputChannel channel) {
|
||||
this.streamingDelegate = delegate;
|
||||
this.channel = channel;
|
||||
}
|
||||
|
||||
// ==================== 解决接口默认方法冲突 ====================
|
||||
|
||||
@Override
|
||||
public Set<Capability> supportedCapabilities() {
|
||||
return streamingDelegate.supportedCapabilities();
|
||||
}
|
||||
|
||||
// ==================== ChatModel 接口实现(同步调用) ====================
|
||||
|
||||
@Override
|
||||
public ChatResponse chat(ChatRequest request) {
|
||||
log.info("【StreamingOutputWrapper】chat() 被调用,开始流式处理");
|
||||
// 用于收集完整响应
|
||||
AtomicReference<ChatResponse> responseRef = new AtomicReference<>();
|
||||
CompletableFuture<Void> future = new CompletableFuture<>();
|
||||
|
||||
// 调用流式模型,拦截每个 token
|
||||
streamingDelegate.chat(request, new StreamingChatResponseHandler() {
|
||||
@Override
|
||||
public void onPartialResponse(String token) {
|
||||
// 推送到 channel
|
||||
channel.send(token);
|
||||
log.debug("【流式Token】{}", token);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPartialResponse(PartialResponse pr, PartialResponseContext ctx) {
|
||||
channel.send(pr.text());
|
||||
log.debug("【流式PartialResponse】{}", pr.text());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPartialThinking(PartialThinking thinking) {
|
||||
channel.send("[思考] " + thinking.text());
|
||||
log.debug("【流式思考】{}", thinking.text());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPartialThinking(PartialThinking thinking, PartialThinkingContext ctx) {
|
||||
channel.send("[思考] " + thinking.text());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPartialToolCall(PartialToolCall toolCall) {
|
||||
// channel.send("[工具参数生成中] " + toolCall);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPartialToolCall(PartialToolCall toolCall, PartialToolCallContext ctx) {
|
||||
// channel.send("[工具参数生成中] " + toolCall);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCompleteResponse(ChatResponse response) {
|
||||
responseRef.set(response);
|
||||
if (response.metadata() != null && response.metadata().tokenUsage() != null) {
|
||||
var usage = response.metadata().tokenUsage();
|
||||
// channel.send("\n[Token统计] input=" + usage.inputTokenCount()
|
||||
// + " output=" + usage.outputTokenCount());
|
||||
}
|
||||
log.info("【StreamingOutputWrapper】流式处理完成");
|
||||
future.complete(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable error) {
|
||||
channel.send("\n[错误] " + error.getMessage());
|
||||
channel.completeWithError(error);
|
||||
future.completeExceptionally(error);
|
||||
log.error("【StreamingOutputWrapper】流式处理出错", error);
|
||||
}
|
||||
});
|
||||
|
||||
// 等待流式完成
|
||||
future.join();
|
||||
|
||||
// 返回收集的响应
|
||||
return responseRef.get();
|
||||
}
|
||||
|
||||
// ==================== StreamingChatModel 接口实现(流式调用) ====================
|
||||
|
||||
@Override
|
||||
public void chat(ChatRequest request, StreamingChatResponseHandler handler) {
|
||||
StreamingChatResponseHandler wrapped = wrapHandler(handler);
|
||||
streamingDelegate.chat(request, wrapped);
|
||||
}
|
||||
|
||||
private StreamingChatResponseHandler wrapHandler(StreamingChatResponseHandler original) {
|
||||
return new StreamingChatResponseHandler() {
|
||||
|
||||
@Override
|
||||
public void onPartialResponse(String token) {
|
||||
channel.send(token);
|
||||
original.onPartialResponse(token);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPartialResponse(PartialResponse pr, PartialResponseContext ctx) {
|
||||
channel.send(pr.text());
|
||||
original.onPartialResponse(pr, ctx);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPartialThinking(PartialThinking thinking) {
|
||||
channel.send("[思考] " + thinking.text());
|
||||
original.onPartialThinking(thinking);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPartialThinking(PartialThinking thinking, PartialThinkingContext ctx) {
|
||||
channel.send("[思考] " + thinking.text());
|
||||
original.onPartialThinking(thinking, ctx);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPartialToolCall(PartialToolCall toolCall) {
|
||||
//channel.send("[工具参数生成中] " + toolCall);
|
||||
original.onPartialToolCall(toolCall);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPartialToolCall(PartialToolCall toolCall, PartialToolCallContext ctx) {
|
||||
//channel.send("[工具参数生成中] " + toolCall);
|
||||
original.onPartialToolCall(toolCall, ctx);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCompleteResponse(ChatResponse response) {
|
||||
if (response.metadata() != null && response.metadata().tokenUsage() != null) {
|
||||
var usage = response.metadata().tokenUsage();
|
||||
// channel.send("\n[Token统计] input=" + usage.inputTokenCount()
|
||||
// + " output=" + usage.outputTokenCount());
|
||||
}
|
||||
original.onCompleteResponse(response);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable error) {
|
||||
channel.send("\n[错误] " + error.getMessage());
|
||||
channel.completeWithError(error);
|
||||
original.onError(error);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// ==================== 共用接口方法 ====================
|
||||
|
||||
@Override
|
||||
public ChatRequestParameters defaultRequestParameters() {
|
||||
return streamingDelegate.defaultRequestParameters();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ChatModelListener> listeners() {
|
||||
return streamingDelegate.listeners();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ModelProvider provider() {
|
||||
return streamingDelegate.provider();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
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;
|
||||
|
||||
/**
|
||||
* Supervisor 流式监听器
|
||||
*
|
||||
* 捕获 Agent 生命周期事件、工具执行前后事件,推送到 OutputChannel
|
||||
* inheritedBySubagents() = true -> 注册在 Supervisor 上,自动继承到所有子 Agent
|
||||
*
|
||||
* @author ageerle@163.com
|
||||
* @date 2025/04/10
|
||||
*/
|
||||
@Slf4j
|
||||
public class SupervisorStreamListener implements dev.langchain4j.agentic.observability.AgentListener {
|
||||
|
||||
private final OutputChannel channel;
|
||||
|
||||
/**
|
||||
* 用于在 AgenticScope 中存储 userId 的 key
|
||||
*/
|
||||
public static final String USER_ID_KEY = "userId";
|
||||
|
||||
public SupervisorStreamListener(OutputChannel channel) {
|
||||
this.channel = channel;
|
||||
}
|
||||
|
||||
// ==================== 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.name(), inputs);
|
||||
}
|
||||
|
||||
@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() : "";
|
||||
|
||||
// 只记录日志,不推送输出信息
|
||||
// 流式输出由 StreamingOutputWrapper 处理
|
||||
// 当无子Agent被调用时,由 ChatServiceFacade 用 plannerModel 生成回复
|
||||
log.info("[Agent完成] {} 输出长度: {}", agent.name(), outputStr.length());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAgentInvocationError(AgentInvocationError error) {
|
||||
AgentInstance agent = error.agent();
|
||||
Map<String, Object> inputs = error.inputs();
|
||||
Throwable throwable = error.error();
|
||||
|
||||
channel.send("\n[Agent错误] " + agent.name()
|
||||
+ " 异常: " + throwable.getMessage());
|
||||
log.error("[Agent错误] {} 异常: {}", agent.name(), throwable.getMessage(), throwable);
|
||||
}
|
||||
|
||||
// ==================== AgenticScope 生命周期 ====================
|
||||
|
||||
@Override
|
||||
public void afterAgenticScopeCreated(AgenticScope agenticScope) {
|
||||
log.info("[AgenticScope创建] memoryId: {}", agenticScope.memoryId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeAgenticScopeDestroyed(AgenticScope agenticScope) {
|
||||
log.info("[AgenticScope销毁] memoryId: {}", agenticScope.memoryId());
|
||||
}
|
||||
|
||||
// ==================== 工具执行生命周期 ====================
|
||||
|
||||
// @Override
|
||||
// public void beforeToolExecution(BeforeToolExecution beforeToolExecution) {
|
||||
// var toolRequest = beforeToolExecution.request();
|
||||
//// channel.send("\n[工具即将执行] " + toolRequest.name()
|
||||
//// + " 参数: " + truncate(toolRequest.arguments(), 150));
|
||||
// log.info("[工具即将执行] {} 参数: {}", toolRequest.name(), toolRequest.arguments());
|
||||
// }
|
||||
|
||||
// @Override
|
||||
// public void afterToolExecution(ToolExecution toolExecution) {
|
||||
// var toolRequest = toolExecution.request();
|
||||
//// channel.send("\n[工具执行完成] " + toolRequest.name()
|
||||
//// + " 结果: " + truncate(String.valueOf(toolExecution.result()), 300));
|
||||
// log.info("[工具执行完成] {} 结果: {}", toolRequest.name(), toolExecution.result());
|
||||
// }
|
||||
|
||||
// ==================== 继承机制 ====================
|
||||
|
||||
/**
|
||||
* 返回 true,让此监听器自动继承给所有子 Agent
|
||||
*/
|
||||
@Override
|
||||
public boolean inheritedBySubagents() {
|
||||
return true;
|
||||
}
|
||||
|
||||
// ==================== 辅助方法 ====================
|
||||
|
||||
private String truncate(String s, int maxLen) {
|
||||
if (s == null) {
|
||||
return "null";
|
||||
}
|
||||
return s.length() > maxLen ? s.substring(0, maxLen) + "..." : s;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,13 @@
|
||||
package org.ruoyi.service.chat;
|
||||
|
||||
import dev.langchain4j.model.chat.ChatModel;
|
||||
import dev.langchain4j.model.chat.StreamingChatModel;
|
||||
import dev.langchain4j.model.openai.OpenAiChatModel;
|
||||
import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
|
||||
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
/**
|
||||
* 聊天消息Service接口
|
||||
*
|
||||
@@ -21,6 +25,23 @@ public interface AbstractChatService {
|
||||
*/
|
||||
StreamingChatModel buildStreamingChatModel(ChatModelVo chatModelVo, ChatRequest chatRequest);
|
||||
|
||||
/**
|
||||
* 创建同步聊天模型(供 Agent/SupervisorAgent 使用)
|
||||
* 默认实现使用 OpenAI 兼容协议,适用于 OpenAI、DeepSeek、PPIO 等兼容接口的 provider。
|
||||
* ZhiPu、QianWen、Ollama 等需覆盖此方法使用各自 SDK。
|
||||
*
|
||||
* @param chatModelVo 模型配置
|
||||
* @return 同步聊天模型实例
|
||||
*/
|
||||
default ChatModel buildChatModel(ChatModelVo chatModelVo) {
|
||||
return OpenAiChatModel.builder()
|
||||
.baseUrl(chatModelVo.getApiHost())
|
||||
.apiKey(chatModelVo.getApiKey())
|
||||
.modelName(chatModelVo.getModelName())
|
||||
.timeout(Duration.ofSeconds(120))
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取服务提供商名称
|
||||
*/
|
||||
|
||||
@@ -4,32 +4,28 @@ import cn.dev33.satoken.stp.StpUtil;
|
||||
import dev.langchain4j.agentic.AgenticServices;
|
||||
import dev.langchain4j.agentic.supervisor.SupervisorAgent;
|
||||
import dev.langchain4j.agentic.supervisor.SupervisorResponseStrategy;
|
||||
import dev.langchain4j.community.model.dashscope.QwenChatModel;
|
||||
import dev.langchain4j.data.message.*;
|
||||
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.model.chat.ChatModel;
|
||||
import dev.langchain4j.model.chat.StreamingChatModel;
|
||||
import dev.langchain4j.model.chat.response.ChatResponse;
|
||||
import dev.langchain4j.model.chat.response.StreamingChatResponseHandler;
|
||||
import dev.langchain4j.model.openai.OpenAiChatModel;
|
||||
import dev.langchain4j.rag.AugmentationRequest;
|
||||
import dev.langchain4j.rag.AugmentationResult;
|
||||
import dev.langchain4j.rag.DefaultRetrievalAugmentor;
|
||||
import dev.langchain4j.rag.RetrievalAugmentor;
|
||||
import dev.langchain4j.rag.content.aggregator.ContentAggregator;
|
||||
import dev.langchain4j.rag.content.aggregator.DefaultContentAggregator;
|
||||
import dev.langchain4j.rag.content.aggregator.ReRankingContentAggregator;
|
||||
import dev.langchain4j.model.scoring.ScoringModel;
|
||||
import dev.langchain4j.rag.query.Metadata;
|
||||
import dev.langchain4j.service.tool.ToolProvider;
|
||||
import dev.langchain4j.skills.shell.ShellSkills;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.ruoyi.agent.ChartGenerationAgent;
|
||||
import org.ruoyi.agent.EchartsAgent;
|
||||
import org.ruoyi.agent.SkillsAgent;
|
||||
import org.ruoyi.agent.SqlAgent;
|
||||
import org.ruoyi.agent.WebSearchAgent;
|
||||
import org.ruoyi.agent.tool.ExecuteSqlQueryTool;
|
||||
@@ -45,6 +41,7 @@ import org.ruoyi.common.chat.service.chat.IChatModelService;
|
||||
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.core.utils.StringUtils;
|
||||
import org.ruoyi.common.satoken.utils.LoginHelper;
|
||||
import org.ruoyi.common.sse.core.SseEmitterManager;
|
||||
import org.ruoyi.common.sse.utils.SseMessageUtils;
|
||||
@@ -52,12 +49,12 @@ import org.ruoyi.domain.bo.vector.QueryVectorBo;
|
||||
import org.ruoyi.domain.vo.knowledge.KnowledgeInfoVo;
|
||||
import org.ruoyi.factory.ChatServiceFactory;
|
||||
import org.ruoyi.mcp.service.core.ToolProviderFactory;
|
||||
import org.ruoyi.observability.*;
|
||||
import org.ruoyi.service.chat.AbstractChatService;
|
||||
import org.ruoyi.service.chat.IChatMessageService;
|
||||
import org.ruoyi.service.chat.impl.memory.PersistentChatMemoryStore;
|
||||
import org.ruoyi.service.knowledge.retriever.CustomVectorRetriever;
|
||||
import org.ruoyi.service.knowledge.rerank.ScoringModelFactory;
|
||||
import org.ruoyi.service.knowledge.IKnowledgeInfoService;
|
||||
import org.ruoyi.service.retrieval.KnowledgeRetrievalService;
|
||||
import org.ruoyi.service.vector.VectorStoreService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
@@ -65,6 +62,7 @@ import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
@@ -92,6 +90,8 @@ public class ChatServiceFacade implements IChatService {
|
||||
|
||||
private final VectorStoreService vectorStoreService;
|
||||
|
||||
private final KnowledgeRetrievalService knowledgeRetrievalService;
|
||||
|
||||
private final SseEmitterManager sseEmitterManager;
|
||||
|
||||
private final IChatMessageService chatMessageService;
|
||||
@@ -100,8 +100,6 @@ public class ChatServiceFacade implements IChatService {
|
||||
|
||||
private final ToolProviderFactory toolProviderFactory;
|
||||
|
||||
private final ScoringModelFactory scoringModelFactory;
|
||||
|
||||
/**
|
||||
* 内存实例缓存,避免同一会话重复创建
|
||||
* Key: sessionId, Value: MessageWindowChatMemory实例
|
||||
@@ -132,12 +130,19 @@ public class ChatServiceFacade implements IChatService {
|
||||
// 2. 构建上下文消息列表
|
||||
List<ChatMessage> contextMessages = buildContextMessages(chatRequest);
|
||||
|
||||
// 注意:buildContextMessages() 最后返回的列表中,最新的带有增强知识的 UserMessage 在最后。
|
||||
// 对于有些模型API(非langchain4j的代理),它们可能不识别增强后的复杂文本(取决于供应商适配度)
|
||||
// 但是通过标准流,它被解析为 String。
|
||||
SseEmitter specialResult = handleSpecialChatModes(chatRequest, contextMessages, chatModelVo, emitter);
|
||||
if (specialResult != null) {
|
||||
return specialResult;
|
||||
chatRequest.setEmitter(emitter);
|
||||
chatRequest.setUserId(userId);
|
||||
chatRequest.setTokenValue(tokenValue);
|
||||
chatRequest.setChatModelVo(chatModelVo);
|
||||
chatRequest.setContextMessages(contextMessages);
|
||||
|
||||
// 保存用户消息
|
||||
chatMessageService.saveChatMessage(userId, chatRequest.getSessionId(), chatRequest.getContent(), RoleType.USER.getName(), chatRequest.getModel());
|
||||
|
||||
// 3. 处理特殊聊天模式(工作流、人机交互恢复、思考模式)
|
||||
SseEmitter sseEmitter = handleSpecialChatModes(chatRequest);
|
||||
if (sseEmitter != null) {
|
||||
return sseEmitter;
|
||||
}
|
||||
|
||||
// 4. 路由服务提供商
|
||||
@@ -145,11 +150,8 @@ public class ChatServiceFacade implements IChatService {
|
||||
log.info("路由到服务提供商: {}, 模型: {}", providerCode, chatRequest.getModel());
|
||||
AbstractChatService chatService = chatServiceFactory.getOriginalService(providerCode);
|
||||
|
||||
|
||||
StreamingChatResponseHandler handler = createResponseHandler(userId, tokenValue,chatRequest);
|
||||
|
||||
// 保存用户消息
|
||||
chatMessageService.saveChatMessage(userId, chatRequest.getSessionId(), chatRequest.getContent(), RoleType.USER.getName(), chatRequest.getModel());
|
||||
|
||||
// 5. 发起对话
|
||||
StreamingChatModel streamingChatModel = chatService.buildStreamingChatModel(chatModelVo, chatRequest);
|
||||
@@ -161,13 +163,9 @@ public class ChatServiceFacade implements IChatService {
|
||||
* 处理特殊聊天模式(工作流、人机交互恢复、思考模式)
|
||||
*
|
||||
* @param chatRequest 聊天请求
|
||||
* @param contextMessages 上下文消息列表(可能被修改)
|
||||
* @param chatModelVo 聊天模型配置
|
||||
* @param emitter SSE发射器
|
||||
* @return 如果需要提前返回则返回SseEmitter,否则返回null
|
||||
*/
|
||||
private SseEmitter handleSpecialChatModes(ChatRequest chatRequest, List<ChatMessage> contextMessages,
|
||||
ChatModelVo chatModelVo, SseEmitter emitter) {
|
||||
private SseEmitter handleSpecialChatModes(ChatRequest chatRequest) {
|
||||
// 处理工作流对话
|
||||
if (chatRequest.getEnableWorkFlow()) {
|
||||
log.info("处理工作流对话,会话: {}", chatRequest.getSessionId());
|
||||
@@ -176,7 +174,6 @@ public class ChatServiceFacade implements IChatService {
|
||||
if (ObjectUtils.isEmpty(runner)) {
|
||||
log.warn("工作流参数为空");
|
||||
}
|
||||
|
||||
return workFlowStarterService.streaming(
|
||||
ThreadContext.getCurrentUser(),
|
||||
runner.getUuid(),
|
||||
@@ -188,25 +185,22 @@ public class ChatServiceFacade implements IChatService {
|
||||
// 处理人机交互恢复
|
||||
if (chatRequest.getIsResume()) {
|
||||
log.info("处理人机交互恢复");
|
||||
|
||||
ReSumeRunner reSumeRunner = chatRequest.getReSumeRunner();
|
||||
if (ObjectUtils.isEmpty(reSumeRunner)) {
|
||||
log.warn("人机交互恢复参数为空");
|
||||
return emitter;
|
||||
}
|
||||
|
||||
workFlowStarterService.resumeFlow(
|
||||
reSumeRunner.getRuntimeUuid(),
|
||||
reSumeRunner.getFeedbackContent(),
|
||||
emitter
|
||||
chatRequest.getEmitter()
|
||||
);
|
||||
|
||||
return emitter;
|
||||
}
|
||||
return chatRequest.getEmitter();
|
||||
|
||||
}
|
||||
// 处理思考模式
|
||||
if (chatRequest.getEnableThinking()) {
|
||||
handleThinkingMode(chatRequest, contextMessages, chatModelVo);
|
||||
return handleThinkingMode(chatRequest);
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -215,71 +209,133 @@ public class ChatServiceFacade implements IChatService {
|
||||
/**
|
||||
* 处理思考模式
|
||||
*
|
||||
* @param chatRequest 聊天请求
|
||||
* @param contextMessages 上下文消息列表
|
||||
* @param chatModelVo 聊天模型配置
|
||||
* @param chatRequest 聊天请求
|
||||
|
||||
*/
|
||||
private void handleThinkingMode(ChatRequest chatRequest, List<ChatMessage> contextMessages, ChatModelVo chatModelVo) {
|
||||
// 步骤1: 配置MCP传输层 - 连接到bing-cn-mcp服务器
|
||||
McpTransport transport = new StdioMcpTransport.Builder()
|
||||
private SseEmitter handleThinkingMode(ChatRequest chatRequest) {
|
||||
// 配置监督者模型
|
||||
OpenAiChatModel plannerModel = OpenAiChatModel.builder()
|
||||
.baseUrl(chatRequest.getChatModelVo().getApiHost())
|
||||
.apiKey(chatRequest.getChatModelVo().getApiKey())
|
||||
.modelName(chatRequest.getChatModelVo().getModelName())
|
||||
.build();
|
||||
|
||||
// Bing 搜索 MCP 客户端
|
||||
McpTransport bingTransport = 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)
|
||||
Long userId = chatRequest.getUserId();
|
||||
McpClient bingMcpClient = new DefaultMcpClient.Builder()
|
||||
.transport(bingTransport)
|
||||
.listener(new MyMcpClientListener(userId))
|
||||
.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"))
|
||||
// Playwright MCP 客户端 - 浏览器自动化工具
|
||||
McpTransport playwrightTransport = new StdioMcpTransport.Builder()
|
||||
.command(List.of("C:\\Program Files\\nodejs\\npx.cmd", "-y", "@playwright/mcp@latest"))
|
||||
.logEvents(true)
|
||||
.build();
|
||||
|
||||
McpClient mcpClient1 = new DefaultMcpClient.Builder()
|
||||
.transport(transport1)
|
||||
McpClient playwrightMcpClient = new DefaultMcpClient.Builder()
|
||||
.transport(playwrightTransport)
|
||||
.listener(new MyMcpClientListener(userId))
|
||||
.build();
|
||||
|
||||
ToolProvider toolProvider1 = McpToolProvider.builder()
|
||||
.mcpClients(List.of(mcpClient1))
|
||||
// Filesystem MCP 客户端 - 文件管理工具
|
||||
// 允许 AI 读取、写入、搜索文件(基于当前项目根目录)
|
||||
String userDir = System.getProperty("user.dir");
|
||||
McpTransport filesystemTransport = new StdioMcpTransport.Builder()
|
||||
.command(List.of("C:\\Program Files\\nodejs\\npx.cmd", "-y",
|
||||
"@modelcontextprotocol/server-filesystem", userDir))
|
||||
.logEvents(true)
|
||||
|
||||
.build();
|
||||
|
||||
// 配置模型
|
||||
OpenAiChatModel plannerModel = OpenAiChatModel.builder()
|
||||
.baseUrl(chatModelVo.getApiHost())
|
||||
.apiKey(chatModelVo.getApiKey())
|
||||
.modelName(chatModelVo.getModelName())
|
||||
McpClient filesystemMcpClient = new DefaultMcpClient.Builder()
|
||||
.transport(filesystemTransport)
|
||||
.listener(new MyMcpClientListener(userId))
|
||||
.build();
|
||||
|
||||
// 构建各Agent
|
||||
// 合并三个 MCP 客户端的工具
|
||||
ToolProvider toolProvider = McpToolProvider.builder()
|
||||
// bingMcpClient,
|
||||
.mcpClients(List.of(playwrightMcpClient, filesystemMcpClient))
|
||||
.build();
|
||||
|
||||
// ========== LangChain4j Skills 基本用法 ==========
|
||||
// 通过 SKILL.md 文件定义,LLM 按需通过 activate_skill 工具加载
|
||||
// 加载 Skills - 使用相对路径,基于项目根目录
|
||||
java.nio.file.Path skillsPath = java.nio.file.Path.of(userDir, "ruoyi-admin/src/main/resources/skills");
|
||||
List<dev.langchain4j.skills.FileSystemSkill> skillsList = dev.langchain4j.skills.FileSystemSkillLoader
|
||||
.loadSkills(skillsPath)
|
||||
;
|
||||
|
||||
ShellSkills skills = ShellSkills.from(skillsList);
|
||||
|
||||
// 构建子 Agent
|
||||
WebSearchAgent searchAgent = AgenticServices.agentBuilder(WebSearchAgent.class)
|
||||
.chatModel(plannerModel)
|
||||
.toolProvider(toolProvider)
|
||||
.listener(new MyAgentListener())
|
||||
.build();
|
||||
|
||||
// 构建子 Agent 2: SkillsAgent - 负责文档处理技能(docx、pdf、xlsx)
|
||||
// 独立管理 Skills 工具
|
||||
SkillsAgent skillsAgent = AgenticServices.agentBuilder(SkillsAgent.class)
|
||||
.chatModel(plannerModel)
|
||||
.systemMessage("You have access to the following skills:\n" + skills.formatAvailableSkills()
|
||||
+ "\nWhen the user's request relates to one of these skills, activate it first using the `activate_skill` tool before proceeding.")
|
||||
.toolProvider(skills.toolProvider())
|
||||
.build();
|
||||
|
||||
// 构建子 Agent 3: SqlAgent - 负责数据库查询
|
||||
SqlAgent sqlAgent = AgenticServices.agentBuilder(SqlAgent.class)
|
||||
.chatModel(plannerModel)
|
||||
.tools(new QueryAllTablesTool(), new QueryTableSchemaTool(), new ExecuteSqlQueryTool())
|
||||
.listener(new MyAgentListener())
|
||||
.build();
|
||||
|
||||
WebSearchAgent searchAgent = AgenticServices.agentBuilder(WebSearchAgent.class)
|
||||
.chatModel(plannerModel)
|
||||
.toolProvider(toolProvider)
|
||||
.build();
|
||||
|
||||
// 构建子 Agent 4: ChartGenerationAgent - 负责图表生成
|
||||
ChartGenerationAgent chartGenerationAgent = AgenticServices.agentBuilder(ChartGenerationAgent.class)
|
||||
.chatModel(plannerModel)
|
||||
.toolProvider(toolProvider1)
|
||||
.listener(new MyAgentListener())
|
||||
.build();
|
||||
|
||||
// 构建监督者Agent
|
||||
// 构建子 Agent 5: EchartsAgent - 负责数据可视化(结合 SQL 查询生成 Echarts 图表)
|
||||
EchartsAgent echartsAgent = AgenticServices.agentBuilder(EchartsAgent.class)
|
||||
.chatModel(plannerModel)
|
||||
.tools(new QueryAllTablesTool(), new QueryTableSchemaTool(), new ExecuteSqlQueryTool())
|
||||
.listener(new MyAgentListener())
|
||||
.build();
|
||||
|
||||
// 构建监督者 Agent - 管理多个子 Agent
|
||||
SupervisorAgent supervisor = AgenticServices.supervisorBuilder()
|
||||
.chatModel(plannerModel)
|
||||
.subAgents(sqlAgent, chartGenerationAgent)
|
||||
//.listener(new SupervisorStreamListener(null))
|
||||
.subAgents(skillsAgent,searchAgent, sqlAgent, chartGenerationAgent, echartsAgent)
|
||||
// 加入历史上下文 - 使用 ChatMemoryProvider 提供持久化的聊天内存
|
||||
//.chatMemoryProvider(memoryId -> createChatMemory(chatRequest.getSessionId()))
|
||||
.responseStrategy(SupervisorResponseStrategy.LAST)
|
||||
.build();
|
||||
|
||||
String invoke = supervisor.invoke(chatRequest.getContent());
|
||||
contextMessages.add(AiMessage.from(invoke));
|
||||
String tokenValue = chatRequest.getTokenValue();
|
||||
|
||||
// 异步执行 supervisor,避免阻塞 HTTP 请求线程导致 SSE 事件被缓冲
|
||||
CompletableFuture.runAsync(() -> {
|
||||
try {
|
||||
String result = supervisor.invoke(chatRequest.getContent());
|
||||
SseMessageUtils.sendContent(userId, result);
|
||||
SseMessageUtils.sendDone(userId);
|
||||
} catch (Exception e) {
|
||||
log.error("Supervisor 执行失败", e);
|
||||
SseMessageUtils.sendError(userId, e.getMessage());
|
||||
} finally {
|
||||
SseMessageUtils.completeConnection(userId, tokenValue);
|
||||
}
|
||||
});
|
||||
return chatRequest.getEmitter();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -356,72 +412,16 @@ public class ChatServiceFacade implements IChatService {
|
||||
|
||||
/**
|
||||
* 构建上下文消息列表
|
||||
|
||||
* 消息顺序:历史消息 → 当前用户消息(确保 AI 正确理解对话上下文)
|
||||
*
|
||||
* @param chatRequest 聊天请求
|
||||
* @return 上下文消息列表
|
||||
*/
|
||||
private List<ChatMessage> buildContextMessages(ChatRequest chatRequest) {
|
||||
List<ChatMessage> messages = new ArrayList<>();
|
||||
List<ChatMessage> messages = new ArrayList<>();
|
||||
|
||||
// 初始化用户消息
|
||||
UserMessage userMessage = UserMessage.userMessage(chatRequest.getContent());
|
||||
|
||||
// 使用 LangChain4j 的 RetrievalAugmentor 进行检索增强
|
||||
if (chatRequest.getKnowledgeId() != null) {
|
||||
KnowledgeInfoVo knowledgeInfoVo = knowledgeInfoService.queryById(Long.valueOf(chatRequest.getKnowledgeId()));
|
||||
if (knowledgeInfoVo != null) {
|
||||
ChatModelVo chatModel = chatModelService.selectModelByName(knowledgeInfoVo.getEmbeddingModel());
|
||||
if (chatModel != null) {
|
||||
|
||||
// 1. 构建适配器(Retriever)
|
||||
CustomVectorRetriever retriever = new CustomVectorRetriever(
|
||||
vectorStoreService, knowledgeInfoVo, chatModel);
|
||||
|
||||
// 2. 构建重排聚合器 (Aggregator)
|
||||
ContentAggregator contentAggregator;
|
||||
if (knowledgeInfoVo.getEnableRerank() != null && knowledgeInfoVo.getEnableRerank() == 1
|
||||
&& knowledgeInfoVo.getRerankModel() != null) {
|
||||
|
||||
ChatModelVo scoringModelConfig = chatModelService.selectModelByName(knowledgeInfoVo.getRerankModel());
|
||||
ScoringModel scoringModel = scoringModelFactory.createScoringModel(scoringModelConfig);
|
||||
|
||||
if (scoringModel != null) {
|
||||
contentAggregator = ReRankingContentAggregator.builder()
|
||||
.scoringModel(scoringModel)
|
||||
// 默认重排后只留前 5 条,避免上下文过长
|
||||
.maxResults(5)
|
||||
.build();
|
||||
log.info("启用重排模型: {}", knowledgeInfoVo.getRerankModel());
|
||||
} else {
|
||||
contentAggregator = new DefaultContentAggregator();
|
||||
}
|
||||
} else {
|
||||
contentAggregator = new DefaultContentAggregator();
|
||||
}
|
||||
|
||||
// 3. 构造流水线
|
||||
RetrievalAugmentor augmentor = DefaultRetrievalAugmentor.builder()
|
||||
.contentRetriever(retriever)
|
||||
.contentAggregator(contentAggregator)
|
||||
.build();
|
||||
|
||||
// 4. 执行 Augmentor 增强:将检索到的知识内容编织进 UserMessage 中
|
||||
Metadata ragMetadata = Metadata.from(userMessage, chatRequest.getSessionId(), new ArrayList<>());
|
||||
AugmentationRequest augmentationRequest = new AugmentationRequest(userMessage, ragMetadata);
|
||||
|
||||
AugmentationResult augmentedResult = augmentor.augment(augmentationRequest);
|
||||
|
||||
ChatMessage augmentedMessage = augmentedResult.chatMessage();
|
||||
if (augmentedMessage instanceof UserMessage) {
|
||||
userMessage = (UserMessage) augmentedMessage;
|
||||
}
|
||||
log.info("RAG 增强完成: UserMessage 已重构并附加上下文背景。");
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 从数据库查询历史对话消息(历史消息应放在当前提问前)
|
||||
// 从数据库查询历史对话消息(放在前面)
|
||||
if (chatRequest.getSessionId() != null) {
|
||||
MessageWindowChatMemory memory = createChatMemory(chatRequest.getSessionId());
|
||||
if (memory != null) {
|
||||
@@ -433,7 +433,38 @@ public class ChatServiceFacade implements IChatService {
|
||||
}
|
||||
}
|
||||
|
||||
// 注入本次用户提问(经过 RAG 增强后的 UserMessage)
|
||||
// 从向量库查询相关历史消息(知识库内容作为上下文)
|
||||
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 = knowledgeRetrievalService.retrieveTexts(queryVectorBo);
|
||||
for (String prompt : nearestList) {
|
||||
// 知识库内容作为系统上下文添加
|
||||
messages.add(new AiMessage(prompt));
|
||||
}
|
||||
}
|
||||
|
||||
// 构建当前用户消息(放在最后)
|
||||
UserMessage userMessage = UserMessage.userMessage(chatRequest.getContent());
|
||||
messages.add(userMessage);
|
||||
|
||||
return messages;
|
||||
@@ -452,6 +483,13 @@ public class ChatServiceFacade implements IChatService {
|
||||
queryVectorBo.setVectorModelName(knowledgeInfoVo.getVectorModel());
|
||||
queryVectorBo.setEmbeddingModelName(knowledgeInfoVo.getEmbeddingModel());
|
||||
queryVectorBo.setMaxResults(knowledgeInfoVo.getRetrieveLimit());
|
||||
|
||||
// 设置重排序参数
|
||||
queryVectorBo.setEnableRerank(knowledgeInfoVo.getEnableRerank() != null && knowledgeInfoVo.getEnableRerank() == 1);
|
||||
queryVectorBo.setRerankModelName(knowledgeInfoVo.getRerankModel());
|
||||
queryVectorBo.setRerankTopN(knowledgeInfoVo.getRerankTopN());
|
||||
queryVectorBo.setRerankScoreThreshold(knowledgeInfoVo.getRerankScoreThreshold());
|
||||
|
||||
return queryVectorBo;
|
||||
}
|
||||
|
||||
@@ -571,7 +609,5 @@ public class ChatServiceFacade implements IChatService {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
package org.ruoyi.service.chat.impl.provider;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import dev.langchain4j.model.chat.ChatModel;
|
||||
import dev.langchain4j.model.chat.StreamingChatModel;
|
||||
import dev.langchain4j.model.openai.OpenAiChatModel;
|
||||
import dev.langchain4j.model.openai.OpenAiStreamingChatModel;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
|
||||
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
|
||||
import org.ruoyi.enums.ChatModeType;
|
||||
import org.ruoyi.observability.MyChatModelListener;
|
||||
import org.ruoyi.service.chat.AbstractChatService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 自定义 API 服务调用
|
||||
*
|
||||
* 适用于 OpenAI 兼容接口或仅通过通用 HTTP 协议接入的第三方大模型服务。
|
||||
* 通过模型配置中的 apiHost / apiKey / modelName 即可复用,不需要再写死具体供应商。
|
||||
*
|
||||
* @author better
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class CustomApiServiceImpl implements AbstractChatService {
|
||||
|
||||
private static final Duration DEFAULT_TIMEOUT = Duration.ofSeconds(180);
|
||||
|
||||
@Override
|
||||
public StreamingChatModel buildStreamingChatModel(ChatModelVo chatModelVo, ChatRequest chatRequest) {
|
||||
return OpenAiStreamingChatModel.builder()
|
||||
.baseUrl(normalizeBaseUrl(chatModelVo.getApiHost()))
|
||||
.apiKey(defaultIfBlank(chatModelVo.getApiKey(), "EMPTY"))
|
||||
.modelName(chatModelVo.getModelName())
|
||||
.timeout(DEFAULT_TIMEOUT)
|
||||
.listeners(List.of(new MyChatModelListener()))
|
||||
.returnThinking(chatRequest.getEnableThinking())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChatModel buildChatModel(ChatModelVo chatModelVo) {
|
||||
return OpenAiChatModel.builder()
|
||||
.baseUrl(normalizeBaseUrl(chatModelVo.getApiHost()))
|
||||
.apiKey(defaultIfBlank(chatModelVo.getApiKey(), "EMPTY"))
|
||||
.modelName(chatModelVo.getModelName())
|
||||
.timeout(DEFAULT_TIMEOUT)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getProviderName() {
|
||||
return ChatModeType.CUSTOM_API.getCode();
|
||||
}
|
||||
|
||||
private String normalizeBaseUrl(String baseUrl) {
|
||||
if (StrUtil.isBlank(baseUrl)) {
|
||||
throw new IllegalArgumentException("自定义API的请求地址(apiHost)不能为空");
|
||||
}
|
||||
return baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl;
|
||||
}
|
||||
|
||||
private String defaultIfBlank(String value, String defaultValue) {
|
||||
return StrUtil.isBlank(value) ? defaultValue : value;
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,30 @@
|
||||
package org.ruoyi.service.chat.impl.provider;
|
||||
|
||||
|
||||
import dev.langchain4j.model.chat.StreamingChatModel;
|
||||
import dev.langchain4j.model.openai.OpenAiStreamingChatModel;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
|
||||
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;
|
||||
|
||||
|
||||
/**
|
||||
* @Author: xiaoen
|
||||
* @Description: deepseek 服务调用
|
||||
* @Date: Created in 19:12 2026/3/17
|
||||
* Deepseek服务调用
|
||||
*
|
||||
* @author xiaoen
|
||||
* @date 2026/3/17
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class DeepseekServiceImpl implements AbstractChatService {
|
||||
|
||||
@Override
|
||||
@@ -25,6 +33,7 @@ public class DeepseekServiceImpl implements AbstractChatService {
|
||||
.baseUrl(chatModelVo.getApiHost())
|
||||
.apiKey(chatModelVo.getApiKey())
|
||||
.modelName(chatModelVo.getModelName())
|
||||
.listeners(List.of(new MyChatModelListener()))
|
||||
.returnThinking(chatRequest.getEnableThinking())
|
||||
.build();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package org.ruoyi.service.chat.impl.provider;
|
||||
|
||||
|
||||
import dev.langchain4j.model.chat.StreamingChatModel;
|
||||
import dev.langchain4j.model.openai.OpenAiStreamingChatModel;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
|
||||
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
|
||||
import org.ruoyi.enums.ChatModeType;
|
||||
import org.ruoyi.observability.MyChatModelListener;
|
||||
import org.ruoyi.service.chat.AbstractChatService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
|
||||
/**
|
||||
* 小米MiMo服务调用
|
||||
* <p>
|
||||
* 小米提供OpenAI兼容的API接口,支持MiMo等模型。
|
||||
*
|
||||
* @author ageerle
|
||||
* @date 2026/4/19
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class MiMoServiceImpl implements AbstractChatService {
|
||||
|
||||
@Override
|
||||
public StreamingChatModel buildStreamingChatModel(ChatModelVo chatModelVo, ChatRequest chatRequest) {
|
||||
return OpenAiStreamingChatModel.builder()
|
||||
.baseUrl(chatModelVo.getApiHost())
|
||||
.apiKey(chatModelVo.getApiKey())
|
||||
.modelName(chatModelVo.getModelName())
|
||||
.listeners(List.of(new MyChatModelListener()))
|
||||
.returnThinking(chatRequest.getEnableThinking())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getProviderName() {
|
||||
return ChatModeType.XIAOMI.getCode();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package org.ruoyi.service.chat.impl.provider;
|
||||
|
||||
import dev.langchain4j.model.chat.StreamingChatModel;
|
||||
import dev.langchain4j.model.openai.OpenAiStreamingChatModel;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
|
||||
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
|
||||
import org.ruoyi.enums.ChatModeType;
|
||||
import org.ruoyi.observability.MyChatModelListener;
|
||||
import org.ruoyi.service.chat.AbstractChatService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* MiniMax服务调用
|
||||
* <p>
|
||||
* MiniMax提供OpenAI兼容的API接口,支持MiniMax-M2.7、MiniMax-M2.5等模型。
|
||||
* API地址:https://api.minimax.io/v1
|
||||
*
|
||||
* @author octopus
|
||||
* @date 2026/3/21
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class MinimaxServiceImpl implements AbstractChatService {
|
||||
|
||||
@Override
|
||||
public StreamingChatModel buildStreamingChatModel(ChatModelVo chatModelVo, ChatRequest chatRequest) {
|
||||
return OpenAiStreamingChatModel.builder()
|
||||
.baseUrl(chatModelVo.getApiHost())
|
||||
.apiKey(chatModelVo.getApiKey())
|
||||
.modelName(chatModelVo.getModelName())
|
||||
.listeners(List.of(new MyChatModelListener()))
|
||||
.returnThinking(chatRequest.getEnableThinking())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getProviderName() {
|
||||
return ChatModeType.MINIMAX.getCode();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,13 +1,21 @@
|
||||
package org.ruoyi.service.chat.impl.provider;
|
||||
|
||||
|
||||
import dev.langchain4j.model.chat.ChatModel;
|
||||
import dev.langchain4j.model.chat.StreamingChatModel;
|
||||
import dev.langchain4j.model.ollama.OllamaChatModel;
|
||||
import dev.langchain4j.model.ollama.OllamaStreamingChatModel;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
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.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服务调用
|
||||
@@ -17,16 +25,28 @@ import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class OllamaServiceImpl implements AbstractChatService {
|
||||
|
||||
private final ChatModelListenerProvider listenerProvider;
|
||||
|
||||
@Override
|
||||
public StreamingChatModel buildStreamingChatModel(ChatModelVo chatModelVo, ChatRequest chatRequest) {
|
||||
return OllamaStreamingChatModel.builder()
|
||||
.baseUrl(chatModelVo.getApiHost())
|
||||
.modelName(chatModelVo.getModelName())
|
||||
.listeners(List.of(new MyChatModelListener()))
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChatModel buildChatModel(ChatModelVo chatModelVo) {
|
||||
return OllamaChatModel.builder()
|
||||
.baseUrl(chatModelVo.getApiHost())
|
||||
.modelName(chatModelVo.getModelName())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getProviderName() {
|
||||
return ChatModeType.OLLAMA.getCode();
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user