正在显示
17 个修改的文件
包含
677 行增加
和
1312 行删除
| @@ -63,9 +63,9 @@ public class EmbeddingsController { | @@ -63,9 +63,9 @@ public class EmbeddingsController { | ||
| 63 | @RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo, | 63 | @RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo, |
| 64 | @RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize, | 64 | @RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize, |
| 65 | HttpServletRequest req) throws NoSuchFieldException, IllegalAccessException, SQLException { | 65 | HttpServletRequest req) throws NoSuchFieldException, IllegalAccessException, SQLException { |
| 66 | - //Response<Embedding> embedding = aiModelUtils.getEmbedding("1925730210204721154", "33333"); | ||
| 67 | 66 | ||
| 68 | - Page<Embeddings> records = embeddingsService.findAll(embeddings,pageNo,pageSize); | 67 | + Page<Embeddings> page = new Page<>(pageNo, pageSize); |
| 68 | + Page<Embeddings> records = embeddingsService.findAll(page,embeddings); | ||
| 69 | return Result.OK(records); | 69 | return Result.OK(records); |
| 70 | } | 70 | } |
| 71 | /** | 71 | /** |
| @@ -94,19 +94,13 @@ public class EmbeddingsController { | @@ -94,19 +94,13 @@ public class EmbeddingsController { | ||
| 94 | @RequiresPermissions("embeddings:embeddings:add") | 94 | @RequiresPermissions("embeddings:embeddings:add") |
| 95 | @PostMapping(value = "/add") | 95 | @PostMapping(value = "/add") |
| 96 | public Result<String> add(@RequestBody Embeddings embeddings) { | 96 | public Result<String> add(@RequestBody Embeddings embeddings) { |
| 97 | - // 1. 构建完整的metadata | ||
| 98 | Map<String, Object> metadata = embeddings.getMetadata(); | 97 | Map<String, Object> metadata = embeddings.getMetadata(); |
| 99 | SnowflakeGenerator snowflakeGenerator = new SnowflakeGenerator(); | 98 | SnowflakeGenerator snowflakeGenerator = new SnowflakeGenerator(); |
| 100 | metadata.put("docName", embeddings.getDocName()); | 99 | metadata.put("docName", embeddings.getDocName()); |
| 101 | String docId = String.valueOf(snowflakeGenerator.next()); | 100 | String docId = String.valueOf(snowflakeGenerator.next()); |
| 102 | - metadata.put("docId", docId); // 自动生成唯一文档ID | ||
| 103 | - metadata.put("index", "0"); // 默认索引位置为0 | ||
| 104 | - // 2. 设置到embeddings对象 | 101 | + metadata.put("docId", docId); |
| 102 | + metadata.put("index", "0"); | ||
| 105 | embeddings.setMetadata(metadata); | 103 | embeddings.setMetadata(metadata); |
| 106 | - | ||
| 107 | - | ||
| 108 | - System.out.println(new SnowflakeGenerator().next()); | ||
| 109 | - | ||
| 110 | embeddingsService.insert(embeddings); | 104 | embeddingsService.insert(embeddings); |
| 111 | return Result.OK("添加成功!"); | 105 | return Result.OK("添加成功!"); |
| 112 | } | 106 | } |
| @@ -122,7 +116,11 @@ public class EmbeddingsController { | @@ -122,7 +116,11 @@ public class EmbeddingsController { | ||
| 122 | @RequiresPermissions("embeddings:embeddings:edit") | 116 | @RequiresPermissions("embeddings:embeddings:edit") |
| 123 | @RequestMapping(value = "/edit", method = {RequestMethod.PUT, RequestMethod.POST}) | 117 | @RequestMapping(value = "/edit", method = {RequestMethod.PUT, RequestMethod.POST}) |
| 124 | public Result<String> edit(@RequestBody Embeddings embeddings) { | 118 | public Result<String> edit(@RequestBody Embeddings embeddings) { |
| 125 | - embeddingsService.update(embeddings); | 119 | + try { |
| 120 | + embeddingsService.update(embeddings); | ||
| 121 | + } catch (SQLException e) { | ||
| 122 | + throw new RuntimeException(e); | ||
| 123 | + } | ||
| 126 | return Result.OK("编辑成功!"); | 124 | return Result.OK("编辑成功!"); |
| 127 | } | 125 | } |
| 128 | 126 | ||
| @@ -137,6 +135,7 @@ public class EmbeddingsController { | @@ -137,6 +135,7 @@ public class EmbeddingsController { | ||
| 137 | @RequiresPermissions("embeddings:embeddings:delete") | 135 | @RequiresPermissions("embeddings:embeddings:delete") |
| 138 | @DeleteMapping(value = "/delete") | 136 | @DeleteMapping(value = "/delete") |
| 139 | public Result<String> delete(@RequestParam(name = "id", required = true) String id) { | 137 | public Result<String> delete(@RequestParam(name = "id", required = true) String id) { |
| 138 | + //embeddingsService.removeById(id); | ||
| 140 | embeddingsService.deleteById(id); | 139 | embeddingsService.deleteById(id); |
| 141 | return Result.OK("删除成功!"); | 140 | return Result.OK("删除成功!"); |
| 142 | } | 141 | } |
| @@ -166,6 +165,10 @@ public class EmbeddingsController { | @@ -166,6 +165,10 @@ public class EmbeddingsController { | ||
| 166 | @Operation(summary = "Embeddings-通过id查询") | 165 | @Operation(summary = "Embeddings-通过id查询") |
| 167 | @GetMapping(value = "/queryById") | 166 | @GetMapping(value = "/queryById") |
| 168 | public Result<Embeddings> queryById(@RequestParam(name = "id", required = true) String id) { | 167 | public Result<Embeddings> queryById(@RequestParam(name = "id", required = true) String id) { |
| 168 | +// Embeddings Embeddings = embeddingsService.getById(id); | ||
| 169 | +// if(Embeddings==null) { | ||
| 170 | +// return Result.error("未找到对应数据"); | ||
| 171 | +// } | ||
| 169 | embeddingsService.findById(id); | 172 | embeddingsService.findById(id); |
| 170 | return Result.OK(); | 173 | return Result.OK(); |
| 171 | } | 174 | } |
| @@ -82,7 +82,7 @@ public class AiragLog implements Serializable { | @@ -82,7 +82,7 @@ public class AiragLog implements Serializable { | ||
| 82 | */ | 82 | */ |
| 83 | @Excel(name = "回答方式", width = 15) | 83 | @Excel(name = "回答方式", width = 15) |
| 84 | @TableField("answer_type") | 84 | @TableField("answer_type") |
| 85 | - @Schema(description = "回答方式:1:问题库回答 2:模型回答 3:未命中") | 85 | + @Schema(description = "回答方式:1:问题库回答 2:模型回答 3:未命中 4:发生异常") |
| 86 | private int answerType; | 86 | private int answerType; |
| 87 | /** | 87 | /** |
| 88 | * 提问方式 | 88 | * 提问方式 |
| 1 | +package org.jeecg.modules.airag.app.handler; | ||
| 2 | + | ||
| 3 | +import org.apache.ibatis.type.BaseTypeHandler; | ||
| 4 | +import org.apache.ibatis.type.JdbcType; | ||
| 5 | +import org.postgresql.util.PGobject; | ||
| 6 | +import com.fasterxml.jackson.core.JsonProcessingException; | ||
| 7 | +import com.fasterxml.jackson.core.type.TypeReference; | ||
| 8 | +import com.fasterxml.jackson.databind.ObjectMapper; | ||
| 9 | + | ||
| 10 | +import java.sql.*; | ||
| 11 | +import java.util.Map; | ||
| 12 | + | ||
| 13 | +public class JsonbMapTypeHandler extends BaseTypeHandler<Map<String, Object>> { | ||
| 14 | + private static final ObjectMapper objectMapper = new ObjectMapper(); | ||
| 15 | + | ||
| 16 | + @Override | ||
| 17 | + public void setNonNullParameter(PreparedStatement ps, int i, | ||
| 18 | + Map<String, Object> parameter, JdbcType jdbcType) throws SQLException { | ||
| 19 | + PGobject pgObject = new PGobject(); | ||
| 20 | + pgObject.setType("jsonb"); | ||
| 21 | + try { | ||
| 22 | + pgObject.setValue(objectMapper.writeValueAsString(parameter)); | ||
| 23 | + ps.setObject(i, pgObject); | ||
| 24 | + } catch (JsonProcessingException e) { | ||
| 25 | + throw new SQLException("Failed to convert Map to JSON", e); | ||
| 26 | + } | ||
| 27 | + } | ||
| 28 | + | ||
| 29 | + @Override | ||
| 30 | + public Map<String, Object> getNullableResult(ResultSet rs, String columnName) throws SQLException { | ||
| 31 | + return parseJson(rs.getString(columnName)); | ||
| 32 | + } | ||
| 33 | + | ||
| 34 | + @Override | ||
| 35 | + public Map<String, Object> getNullableResult(ResultSet rs, int columnIndex) throws SQLException { | ||
| 36 | + return parseJson(rs.getString(columnIndex)); | ||
| 37 | + } | ||
| 38 | + | ||
| 39 | + @Override | ||
| 40 | + public Map<String, Object> getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { | ||
| 41 | + return parseJson(cs.getString(columnIndex)); | ||
| 42 | + } | ||
| 43 | + | ||
| 44 | + private Map<String, Object> parseJson(String json) throws SQLException { | ||
| 45 | + if (json == null) return null; | ||
| 46 | + try { | ||
| 47 | + return objectMapper.readValue(json, new TypeReference<Map<String, Object>>() {}); | ||
| 48 | + } catch (JsonProcessingException e) { | ||
| 49 | + throw new SQLException("Failed to parse JSON", e); | ||
| 50 | + } | ||
| 51 | + } | ||
| 52 | +} |
| 1 | +package org.jeecg.modules.airag.app.handler; | ||
| 2 | + | ||
| 3 | +import org.apache.ibatis.type.BaseTypeHandler; | ||
| 4 | +import org.apache.ibatis.type.JdbcType; | ||
| 5 | +import com.pgvector.PGvector; | ||
| 6 | +import java.sql.*; | ||
| 7 | + | ||
| 8 | +public class PgVectorTypeHandler extends BaseTypeHandler<float[]> { | ||
| 9 | + @Override | ||
| 10 | + public void setNonNullParameter(PreparedStatement ps, int i, | ||
| 11 | + float[] parameter, JdbcType jdbcType) throws SQLException { | ||
| 12 | + ps.setObject(i, new PGvector(parameter)); | ||
| 13 | + } | ||
| 14 | + | ||
| 15 | + @Override | ||
| 16 | + public float[] getNullableResult(ResultSet rs, String columnName) throws SQLException { | ||
| 17 | + PGvector pgVector = (PGvector) rs.getObject(columnName); | ||
| 18 | + return pgVector != null ? pgVector.toArray() : null; | ||
| 19 | + } | ||
| 20 | + | ||
| 21 | + @Override | ||
| 22 | + public float[] getNullableResult(ResultSet rs, int columnIndex) throws SQLException { | ||
| 23 | + PGvector pgVector = (PGvector) rs.getObject(columnIndex); | ||
| 24 | + return pgVector != null ? pgVector.toArray() : null; | ||
| 25 | + } | ||
| 26 | + | ||
| 27 | + @Override | ||
| 28 | + public float[] getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { | ||
| 29 | + PGvector pgVector = (PGvector) cs.getObject(columnIndex); | ||
| 30 | + return pgVector != null ? pgVector.toArray() : null; | ||
| 31 | + } | ||
| 32 | +} |
| 1 | package org.jeecg.modules.airag.app.mapper; | 1 | package org.jeecg.modules.airag.app.mapper; |
| 2 | 2 | ||
| 3 | -import ch.qos.logback.core.net.SyslogOutputStream; | ||
| 4 | -import cn.hutool.core.lang.generator.SnowflakeGenerator; | ||
| 5 | -import com.alibaba.fastjson2.JSONObject; | 3 | +import com.baomidou.dynamic.datasource.annotation.DS; |
| 4 | +import com.baomidou.mybatisplus.core.metadata.IPage; | ||
| 6 | import com.baomidou.mybatisplus.extension.plugins.pagination.Page; | 5 | import com.baomidou.mybatisplus.extension.plugins.pagination.Page; |
| 7 | -import com.fasterxml.jackson.core.JsonProcessingException; | ||
| 8 | -import com.fasterxml.jackson.core.type.TypeReference; | ||
| 9 | -import com.fasterxml.jackson.databind.ObjectMapper; | ||
| 10 | -import com.pgvector.PGvector; | ||
| 11 | -import dev.langchain4j.data.embedding.Embedding; | ||
| 12 | -import dev.langchain4j.model.output.Response; | ||
| 13 | -import lombok.extern.slf4j.Slf4j; | ||
| 14 | -import org.apache.commons.lang3.StringUtils; | 6 | +import org.apache.ibatis.annotations.Mapper; |
| 7 | +import org.apache.ibatis.annotations.Param; | ||
| 15 | import org.jeecg.modules.airag.app.entity.Embeddings; | 8 | import org.jeecg.modules.airag.app.entity.Embeddings; |
| 16 | -import org.jeecg.modules.airag.app.entity.QuestionEmbedding; | ||
| 17 | -import org.jeecg.modules.airag.app.utils.AiModelUtils; | ||
| 18 | -import org.postgresql.util.PGobject; | ||
| 19 | -import org.springframework.beans.factory.annotation.Autowired; | ||
| 20 | -import org.springframework.beans.factory.annotation.Value; | ||
| 21 | -import org.springframework.stereotype.Component; | ||
| 22 | 9 | ||
| 23 | -import java.sql.*; | ||
| 24 | -import java.util.*; | ||
| 25 | -import java.util.stream.Collectors; | ||
| 26 | - | ||
| 27 | -@Component | ||
| 28 | -@Slf4j | ||
| 29 | -public class PgVectorMapper { | ||
| 30 | - @Autowired | ||
| 31 | - private AiModelUtils aiModelUtils; | ||
| 32 | - | ||
| 33 | - // PostgreSQL连接参数(实际项目中应从配置读取) | ||
| 34 | - private static final String URL = "jdbc:postgresql://192.168.100.104:5432/postgres"; | ||
| 35 | - private static final String USER = "postgres"; | ||
| 36 | - private static final String PASSWORD = "postgres"; | ||
| 37 | - | ||
| 38 | - @Value("${jeecg.ai-chat.embedId}") | ||
| 39 | - private String embedId; | ||
| 40 | - | ||
| 41 | - | ||
| 42 | - // 获取数据库连接 | ||
| 43 | - private Connection getConnection() throws SQLException { | ||
| 44 | - return DriverManager.getConnection(URL, USER, PASSWORD); | ||
| 45 | - } | ||
| 46 | - | ||
| 47 | - // 查询所有向量记录 | ||
| 48 | - public Page<Embeddings> findAll(Embeddings embeddings,int pageNo,int pageSize) { | ||
| 49 | - List<Embeddings> results = new ArrayList<>(); | ||
| 50 | - StringBuilder sql = new StringBuilder("SELECT * FROM embeddings WHERE 1=1"); | ||
| 51 | - StringBuilder countSql = new StringBuilder("SELECT COUNT(1) FROM embeddings WHERE 1=1"); | ||
| 52 | - List<Object> params = new ArrayList<>(); // 存储参数值 | ||
| 53 | - List<Object> countParams = new ArrayList<>(); // 存储参数值 | ||
| 54 | - | ||
| 55 | - // 动态构建查询条件 | ||
| 56 | - if (StringUtils.isNotBlank(embeddings.getDocName())) { | ||
| 57 | - sql.append(" AND metadata ->> 'docName' LIKE ?"); | ||
| 58 | - countSql.append(" AND metadata ->> 'docName' LIKE ?"); | ||
| 59 | - params.add("%" + embeddings.getDocName() + "%"); | ||
| 60 | - countParams.add("%" + embeddings.getDocName() + "%"); | ||
| 61 | - } | ||
| 62 | - | ||
| 63 | - if (StringUtils.isNotBlank(embeddings.getKnowledgeId())) { | ||
| 64 | - sql.append(" AND metadata ->> 'knowledgeId' = ?"); | ||
| 65 | - countSql.append(" AND metadata ->> 'knowledgeId' = ?"); | ||
| 66 | - params.add(embeddings.getKnowledgeId()); | ||
| 67 | - countParams.add(embeddings.getKnowledgeId()); | ||
| 68 | - } | ||
| 69 | - | ||
| 70 | - if (StringUtils.isNotBlank(embeddings.getText())) { | ||
| 71 | - sql.append(" AND text ILIKE ?"); // 使用 ILIKE 进行不区分大小写的模糊匹配 | ||
| 72 | - countSql.append(" AND text ILIKE ?"); // 使用 ILIKE 进行不区分大小写的模糊匹配 | ||
| 73 | - params.add("%" + embeddings.getText() + "%"); | ||
| 74 | - countParams.add("%" + embeddings.getText() + "%"); | ||
| 75 | - } | ||
| 76 | - | ||
| 77 | - sql.append(" ORDER BY (metadata->>'knowledgeId') ASC NULLS LAST, (metadata->>'docName') ASC"); | ||
| 78 | - | ||
| 79 | - // 添加分页 | ||
| 80 | - sql.append(" LIMIT ? OFFSET ?"); | ||
| 81 | - params.add(pageSize); | ||
| 82 | - params.add((pageNo - 1) * pageSize); | ||
| 83 | - | ||
| 84 | - | ||
| 85 | - try (Connection conn = getConnection(); | ||
| 86 | - PreparedStatement stmt = conn.prepareStatement(sql.toString())) { | ||
| 87 | - | ||
| 88 | - // 设置参数值 | ||
| 89 | - for (int i = 0; i < params.size(); i++) { | ||
| 90 | - stmt.setObject(i + 1, params.get(i)); | ||
| 91 | - } | ||
| 92 | - | ||
| 93 | - try (ResultSet rs = stmt.executeQuery()) { | ||
| 94 | - while (rs.next()) { | ||
| 95 | - results.add(mapRowToEmbeddings(rs)); | ||
| 96 | - } | ||
| 97 | - } | ||
| 98 | - } catch (SQLException e) { | ||
| 99 | - log.error("查询所有向量记录失败", e); | ||
| 100 | - throw new RuntimeException("查询向量数据时发生数据库错误", e); | ||
| 101 | - } | ||
| 102 | - | ||
| 103 | - // 执行计数查询 | ||
| 104 | - int total = 0; | ||
| 105 | - try(Connection conn = getConnection(); | ||
| 106 | - PreparedStatement stmt = conn.prepareStatement(countSql.toString())){ | ||
| 107 | - // 设置参数值 | ||
| 108 | - for (int i = 0; i < countParams.size(); i++) { | ||
| 109 | - stmt.setObject(i + 1, countParams.get(i)); | ||
| 110 | - } | ||
| 111 | - | ||
| 112 | - try (ResultSet rs = stmt.executeQuery()) { | ||
| 113 | - if (rs.next()) { | ||
| 114 | - total = rs.getInt(1); // 直接获取count值 | ||
| 115 | - } | ||
| 116 | - } | ||
| 117 | - } catch (SQLException e) { | ||
| 118 | - log.error("查询记录总数失败", e); | ||
| 119 | - throw new RuntimeException("查询记录总数时发生数据库错误", e); | ||
| 120 | - } | ||
| 121 | - | ||
| 122 | - Page<Embeddings> page = new Page<>(); | ||
| 123 | - page.setRecords(results); | ||
| 124 | - page.setTotal(total); | ||
| 125 | - return page; | ||
| 126 | - } | ||
| 127 | - | ||
| 128 | - // 根据ID查询单个向量记录 | ||
| 129 | - public Embeddings findById(String id) { | ||
| 130 | - String sql = "SELECT * FROM embeddings WHERE embedding_id = ?"; | ||
| 131 | - | ||
| 132 | - try (Connection conn = getConnection(); | ||
| 133 | - PreparedStatement stmt = conn.prepareStatement(sql)) { | ||
| 134 | - | ||
| 135 | - stmt.setString(1, id); | ||
| 136 | - try (ResultSet rs = stmt.executeQuery()) { | ||
| 137 | - if (rs.next()) { | ||
| 138 | - return mapRowToEmbeddings(rs); | ||
| 139 | - } | ||
| 140 | - } | ||
| 141 | - } catch (SQLException e) { | ||
| 142 | - log.error("根据ID查询向量记录失败, ID: {}", id, e); | ||
| 143 | - throw new RuntimeException("根据ID查询向量时发生数据库错误", e); | ||
| 144 | - } | ||
| 145 | - return null; | ||
| 146 | - } | ||
| 147 | - // 查询所有记录 | ||
| 148 | - public Integer findEmbeddingCount(Embeddings embeddings) { | ||
| 149 | - | ||
| 150 | - StringBuilder sql = new StringBuilder("select COUNT(1) AS total_count from embeddings where 1 = 1"); | ||
| 151 | - List<Object> params = new ArrayList<>(); | ||
| 152 | - | ||
| 153 | - if(StringUtils.isNotBlank(embeddings.getText())){ | ||
| 154 | - sql.append(" AND text = ?"); | ||
| 155 | - params.add(embeddings.getText()); | ||
| 156 | - } | ||
| 157 | - | ||
| 158 | - | ||
| 159 | - try(Connection conn = getConnection(); | ||
| 160 | - PreparedStatement stmt = conn.prepareStatement(sql.toString())){ | ||
| 161 | - // 设置参数值 | ||
| 162 | - for (int i = 0; i < params.size(); i++) { | ||
| 163 | - stmt.setObject(i + 1, params.get(i)); | ||
| 164 | - } | ||
| 165 | - | ||
| 166 | - try (ResultSet rs = stmt.executeQuery()) { | ||
| 167 | - while (rs.next()) { | ||
| 168 | - return rs.getInt("total_count"); | ||
| 169 | - } | ||
| 170 | - return 0; | ||
| 171 | - } | ||
| 172 | - } catch (SQLException e) { | ||
| 173 | - log.error("查询所有记录失败", e); | ||
| 174 | - throw new RuntimeException("查询数据时发生数据库错误", e); | ||
| 175 | - } | ||
| 176 | - | ||
| 177 | - } | ||
| 178 | - | ||
| 179 | - // 插入新向量记录 | ||
| 180 | - public int insert(Embeddings record) { | ||
| 181 | - | ||
| 182 | - String sql = "INSERT INTO embeddings (embedding_id, embedding, text, metadata) VALUES (?, ?, ?, ?::jsonb)"; | ||
| 183 | - | ||
| 184 | - try (Connection conn = getConnection(); | ||
| 185 | - PreparedStatement stmt = conn.prepareStatement(sql)) { | ||
| 186 | - | ||
| 187 | - stmt.setString(1, UUID.randomUUID().toString()); | ||
| 188 | - Response<Embedding> embedding = aiModelUtils.getEmbedding(embedId, record.getText()); | ||
| 189 | - stmt.setObject(2, embedding.content().vector()); | ||
| 190 | - stmt.setObject(3, record.getText()); | ||
| 191 | - stmt.setObject(4, toJson(record.getMetadata())); | ||
| 192 | - | ||
| 193 | - return stmt.executeUpdate(); | ||
| 194 | - } catch (SQLException e) { | ||
| 195 | - log.error("插入向量记录失败: {}", record, e); | ||
| 196 | - throw new RuntimeException("插入向量数据时发生数据库错误", e); | ||
| 197 | - } | ||
| 198 | - } | ||
| 199 | - // 更新向量记录 | ||
| 200 | - public int update(Embeddings record) { | ||
| 201 | - String sql = "UPDATE embeddings SET embedding = ?, metadata = ?::jsonb, text = ? WHERE embedding_id = ?"; | ||
| 202 | - | ||
| 203 | - try (Connection conn = getConnection(); | ||
| 204 | - PreparedStatement stmt = conn.prepareStatement(sql)) { | ||
| 205 | - | ||
| 206 | - JSONObject mataData = new JSONObject(); | ||
| 207 | - mataData.put("knowledgeId", record.getKnowledgeId()); // 使用前端传入的知识库ID | ||
| 208 | - mataData.put("docName", record.getDocName()); | ||
| 209 | - | ||
| 210 | - //获取record数据中的docId | ||
| 211 | - Map<String, Object> map = record.getMetadata(); | ||
| 212 | - System.out.println("map = " + map); | ||
| 213 | - mataData.put("docId", record.getDocId()); | ||
| 214 | - mataData.put("index", "0"); | ||
| 215 | - System.out.println("原始数据: " + mataData); | ||
| 216 | - | ||
| 217 | - | ||
| 218 | - PGobject jsonObject = new PGobject(); | ||
| 219 | - jsonObject.setType("json"); | ||
| 220 | - jsonObject.setValue(mataData.toJSONString()); | ||
| 221 | - Response<Embedding> embedding = aiModelUtils.getEmbedding(embedId, record.getText()); | ||
| 222 | - stmt.setObject(1, embedding.content().vector()); | ||
| 223 | - stmt.setObject(2, jsonObject); | ||
| 224 | - stmt.setObject(3, record.getText()); | ||
| 225 | - stmt.setString(4, record.getId()); | ||
| 226 | - | ||
| 227 | - return stmt.executeUpdate(); | ||
| 228 | - } catch (SQLException e) { | ||
| 229 | - log.error("更新向量记录失败: {}", record, e); | ||
| 230 | - throw new RuntimeException("更新向量数据时发生数据库错误", e); | ||
| 231 | - } | ||
| 232 | - } | ||
| 233 | - | ||
| 234 | - // 根据ID删除向量记录 | ||
| 235 | - public int deleteById(String id) { | ||
| 236 | - String sql = "DELETE FROM embeddings WHERE embedding_id = ?"; | ||
| 237 | - | ||
| 238 | - try (Connection conn = getConnection(); | ||
| 239 | - PreparedStatement stmt = conn.prepareStatement(sql)) { | ||
| 240 | - | ||
| 241 | - stmt.setString(1, id); | ||
| 242 | - return stmt.executeUpdate(); | ||
| 243 | - } catch (SQLException e) { | ||
| 244 | - log.error("删除向量记录失败, ID: {}", id, e); | ||
| 245 | - throw new RuntimeException("删除向量数据时发生数据库错误", e); | ||
| 246 | - } | ||
| 247 | - } | ||
| 248 | - | ||
| 249 | - // 批量删除方法 | ||
| 250 | - public int deleteByIds(List<String> ids) { | ||
| 251 | - if (ids == null || ids.isEmpty()) { | ||
| 252 | - return 0; | ||
| 253 | - } | ||
| 254 | - | ||
| 255 | - String sql = "DELETE FROM embeddings WHERE embedding_id IN ("; | ||
| 256 | - StringBuilder placeholders = new StringBuilder(); | ||
| 257 | - for (int i = 0; i < ids.size(); i++) { | ||
| 258 | - placeholders.append("?"); | ||
| 259 | - if (i < ids.size() - 1) { | ||
| 260 | - placeholders.append(","); | ||
| 261 | - } | ||
| 262 | - } | ||
| 263 | - sql += placeholders.toString() + ")"; | ||
| 264 | - | ||
| 265 | - try (Connection conn = getConnection(); | ||
| 266 | - PreparedStatement stmt = conn.prepareStatement(sql)) { | ||
| 267 | - | ||
| 268 | - for (int i = 0; i < ids.size(); i++) { | ||
| 269 | - stmt.setString(i + 1, ids.get(i)); | ||
| 270 | - } | ||
| 271 | - | ||
| 272 | - return stmt.executeUpdate(); | ||
| 273 | - } catch (SQLException e) { | ||
| 274 | - log.error("批量删除向量记录失败, IDs: {}", ids, e); | ||
| 275 | - throw new RuntimeException("批量删除向量数据时发生数据库错误", e); | ||
| 276 | - } | ||
| 277 | - } | ||
| 278 | - | ||
| 279 | - // 向量相似度搜索 | ||
| 280 | - public List<Embeddings> similaritySearch(float[] vector, int limit) { | ||
| 281 | - String sql = "SELECT * FROM embeddings ORDER BY embedding <-> ? LIMIT ?"; | ||
| 282 | - List<Embeddings> results = new ArrayList<>(); | ||
| 283 | - | ||
| 284 | - try (Connection conn = getConnection(); | ||
| 285 | - PreparedStatement stmt = conn.prepareStatement(sql)) { | ||
| 286 | - | ||
| 287 | - stmt.setObject(1, new PGvector(vector)); | ||
| 288 | - stmt.setInt(2, limit); | ||
| 289 | - | ||
| 290 | - try (ResultSet rs = stmt.executeQuery()) { | ||
| 291 | - while (rs.next()) { | ||
| 292 | - results.add(mapRowToEmbeddings(rs)); | ||
| 293 | - } | ||
| 294 | - } | ||
| 295 | - } catch (SQLException e) { | ||
| 296 | - log.error("向量相似度搜索失败", e); | ||
| 297 | - throw new RuntimeException("执行向量相似度搜索时发生数据库错误", e); | ||
| 298 | - } | ||
| 299 | - return results; | ||
| 300 | - } | ||
| 301 | - | ||
| 302 | - // 将ResultSet行映射为VectorRecord对象 | ||
| 303 | - private Embeddings mapRowToEmbeddings(ResultSet rs) throws SQLException { | ||
| 304 | - Embeddings record = new Embeddings(); | ||
| 305 | - record.setId(rs.getString("embedding_id")); | ||
| 306 | - record.setText(rs.getString("text")); | ||
| 307 | - | ||
| 308 | - String metadataJson = rs.getString("metadata"); | ||
| 309 | - if (StringUtils.isNotBlank(metadataJson)) { | ||
| 310 | - record.setMetadata(fromJson(metadataJson)); | ||
| 311 | - } | ||
| 312 | - | ||
| 313 | - return record; | ||
| 314 | - } | ||
| 315 | - | ||
| 316 | - // 将Map转换为JSON字符串 | ||
| 317 | - private String toJson(Map<String, Object> map) { | ||
| 318 | - try { | ||
| 319 | - return new ObjectMapper().writeValueAsString(map); | ||
| 320 | - } catch (JsonProcessingException e) { | ||
| 321 | - log.error("元数据转换为JSON失败", e); | ||
| 322 | - return "{}"; | ||
| 323 | - } | ||
| 324 | - } | ||
| 325 | - | ||
| 326 | - // 将JSON字符串转换为Map | ||
| 327 | - private Map<String, Object> fromJson(String json) { | ||
| 328 | - try { | ||
| 329 | - return new ObjectMapper().readValue(json, new TypeReference<Map<String, Object>>() {}); | ||
| 330 | - } catch (JsonProcessingException e) { | ||
| 331 | - log.error("JSON转换为元数据失败", e); | ||
| 332 | - return Collections.emptyMap(); | ||
| 333 | - } | ||
| 334 | - } | 10 | +import java.util.List; |
| 11 | + | ||
| 12 | +@Mapper | ||
| 13 | +@DS("pgvector") | ||
| 14 | +public interface PgVectorMapper { | ||
| 15 | + Page<Embeddings> findAll(IPage<Embeddings> page, @Param("embeddings") Embeddings embeddings); | ||
| 16 | + | ||
| 17 | + Embeddings findById(@Param("id") String id); | ||
| 18 | + | ||
| 19 | + Integer findEmbeddingCount(@Param("embeddings") Embeddings embeddings); | ||
| 20 | + | ||
| 21 | + int insert(@Param("record") Embeddings record); | ||
| 22 | + | ||
| 23 | + int update(@Param("record") Embeddings record); | ||
| 24 | + | ||
| 25 | + int deleteById(@Param("id") String id); | ||
| 26 | + | ||
| 27 | + int deleteByIds(@Param("ids") List<String> ids); | ||
| 28 | + | ||
| 29 | + List<Embeddings> similaritySearch(@Param("vector") float[] vector, | ||
| 30 | + @Param("limit") int limit); | ||
| 335 | } | 31 | } |
| 1 | +<?xml version="1.0" encoding="UTF-8"?> | ||
| 2 | +<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" | ||
| 3 | + "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> | ||
| 4 | +<mapper namespace="org.jeecg.modules.airag.app.mapper.PgVectorMapper"> | ||
| 5 | + | ||
| 6 | + <resultMap id="embeddingsResultMap" type="org.jeecg.modules.airag.app.entity.Embeddings"> | ||
| 7 | + <id column="embedding_id" property="id" /> | ||
| 8 | + <result column="text" property="text" /> | ||
| 9 | + <result column="metadata" property="metadata" typeHandler="org.jeecg.modules.airag.app.handler.JsonbMapTypeHandler" /> | ||
| 10 | + </resultMap> | ||
| 11 | + | ||
| 12 | + <select id="findAll" resultMap="embeddingsResultMap"> | ||
| 13 | + SELECT * FROM embeddings WHERE 1=1 | ||
| 14 | + <if test="embeddings.docName != null and embeddings.docName != ''"> | ||
| 15 | + AND metadata ->> 'docName' LIKE CONCAT('%', #{embeddings.docName}, '%') | ||
| 16 | + </if> | ||
| 17 | + <if test="embeddings.knowledgeId != null and embeddings.knowledgeId != ''"> | ||
| 18 | + AND metadata ->> 'knowledgeId' = #{embeddings.knowledgeId} | ||
| 19 | + </if> | ||
| 20 | + <if test="embeddings.text != null and embeddings.text != ''"> | ||
| 21 | + AND text ILIKE CONCAT('%', #{embeddings.text}, '%') | ||
| 22 | + </if> | ||
| 23 | + ORDER BY (metadata->>'knowledgeId') ASC NULLS LAST, (metadata->>'docName') ASC | ||
| 24 | + </select> | ||
| 25 | + | ||
| 26 | + <select id="findById" resultMap="embeddingsResultMap"> | ||
| 27 | + SELECT * FROM embeddings WHERE embedding_id = #{id} | ||
| 28 | + </select> | ||
| 29 | + | ||
| 30 | + <select id="findEmbeddingCount" resultType="int"> | ||
| 31 | + SELECT COUNT(1) FROM embeddings WHERE 1=1 | ||
| 32 | + <if test="embeddings.text != null and embeddings.text != ''"> | ||
| 33 | + AND text = #{embeddings.text} | ||
| 34 | + </if> | ||
| 35 | + </select> | ||
| 36 | + | ||
| 37 | + <insert id="insert" parameterType="org.jeecg.modules.airag.app.entity.Embeddings"> | ||
| 38 | + INSERT INTO embeddings (embedding_id, embedding, text, metadata) | ||
| 39 | + VALUES ( | ||
| 40 | + #{record.id, jdbcType=VARCHAR}, | ||
| 41 | + #{record.embedding, jdbcType=ARRAY, typeHandler=org.jeecg.modules.airag.app.handler.PgVectorTypeHandler}, | ||
| 42 | + #{record.text, jdbcType=VARCHAR}, | ||
| 43 | + #{record.metadata, jdbcType=OTHER, typeHandler=org.jeecg.modules.airag.app.handler.JsonbMapTypeHandler}::jsonb | ||
| 44 | + ) | ||
| 45 | + </insert> | ||
| 46 | + | ||
| 47 | + <update id="update" parameterType="org.jeecg.modules.airag.app.entity.Embeddings"> | ||
| 48 | + UPDATE embeddings | ||
| 49 | + SET | ||
| 50 | + embedding = #{record.embedding, jdbcType=ARRAY, typeHandler=org.jeecg.modules.airag.app.handler.PgVectorTypeHandler}, | ||
| 51 | + metadata = #{record.metadata, jdbcType=OTHER, typeHandler=org.jeecg.modules.airag.app.handler.JsonbMapTypeHandler}::jsonb, | ||
| 52 | + text = #{record.text, jdbcType=VARCHAR} | ||
| 53 | + WHERE embedding_id = #{record.id} | ||
| 54 | + </update> | ||
| 55 | + | ||
| 56 | + <delete id="deleteById"> | ||
| 57 | + DELETE FROM embeddings WHERE embedding_id = #{id} | ||
| 58 | + </delete> | ||
| 59 | + | ||
| 60 | + <delete id="deleteByIds"> | ||
| 61 | + DELETE FROM embeddings WHERE embedding_id IN | ||
| 62 | + <foreach collection="ids" item="id" open="(" separator="," close=")"> | ||
| 63 | + #{id} | ||
| 64 | + </foreach> | ||
| 65 | + </delete> | ||
| 66 | + | ||
| 67 | + <select id="similaritySearch" resultMap="embeddingsResultMap"> | ||
| 68 | + | ||
| 69 | + </select> | ||
| 70 | +</mapper> |
| @@ -4,9 +4,8 @@ package org.jeecg.modules.airag.app.service; | @@ -4,9 +4,8 @@ package org.jeecg.modules.airag.app.service; | ||
| 4 | 4 | ||
| 5 | import com.baomidou.mybatisplus.extension.plugins.pagination.Page; | 5 | import com.baomidou.mybatisplus.extension.plugins.pagination.Page; |
| 6 | import org.jeecg.modules.airag.app.entity.Embeddings; | 6 | import org.jeecg.modules.airag.app.entity.Embeddings; |
| 7 | -import org.jeecg.modules.airag.app.entity.QuestionEmbedding; | ||
| 8 | 7 | ||
| 9 | -import java.util.ArrayList; | 8 | +import java.sql.SQLException; |
| 10 | import java.util.List; | 9 | import java.util.List; |
| 11 | 10 | ||
| 12 | /** | 11 | /** |
| @@ -17,10 +16,10 @@ import java.util.List; | @@ -17,10 +16,10 @@ import java.util.List; | ||
| 17 | */ | 16 | */ |
| 18 | public interface IEmbeddingsService { | 17 | public interface IEmbeddingsService { |
| 19 | 18 | ||
| 20 | - Page<Embeddings> findAll(Embeddings embeddings,int pageNo,int pageSize); | 19 | + Page<Embeddings> findAll(Page<Embeddings> page, Embeddings embeddings); |
| 21 | int deleteById(String id); | 20 | int deleteById(String id); |
| 22 | int insert(Embeddings record); | 21 | int insert(Embeddings record); |
| 23 | - int update(Embeddings record); | 22 | + int update(Embeddings record) throws SQLException; |
| 24 | Embeddings findById(String id); | 23 | Embeddings findById(String id); |
| 25 | int removeByIds(List<String> ids); | 24 | int removeByIds(List<String> ids); |
| 26 | 25 |
| 1 | package org.jeecg.modules.airag.app.service.impl; | 1 | package org.jeecg.modules.airag.app.service.impl; |
| 2 | 2 | ||
| 3 | +import com.alibaba.fastjson2.JSONObject; | ||
| 3 | import com.baomidou.mybatisplus.extension.plugins.pagination.Page; | 4 | import com.baomidou.mybatisplus.extension.plugins.pagination.Page; |
| 5 | +import dev.langchain4j.data.embedding.Embedding; | ||
| 6 | +import dev.langchain4j.model.output.Response; | ||
| 4 | import org.jeecg.modules.airag.app.entity.Embeddings; | 7 | import org.jeecg.modules.airag.app.entity.Embeddings; |
| 5 | -import org.jeecg.modules.airag.app.entity.QuestionEmbedding; | ||
| 6 | import org.jeecg.modules.airag.app.mapper.PgVectorMapper; | 8 | import org.jeecg.modules.airag.app.mapper.PgVectorMapper; |
| 7 | import org.jeecg.modules.airag.app.service.IEmbeddingsService; | 9 | import org.jeecg.modules.airag.app.service.IEmbeddingsService; |
| 10 | +import org.jeecg.modules.airag.app.utils.AiModelUtils; | ||
| 11 | +import org.postgresql.util.PGobject; | ||
| 8 | import org.springframework.beans.factory.annotation.Autowired; | 12 | import org.springframework.beans.factory.annotation.Autowired; |
| 13 | +import org.springframework.beans.factory.annotation.Value; | ||
| 9 | import org.springframework.stereotype.Service; | 14 | import org.springframework.stereotype.Service; |
| 10 | 15 | ||
| 16 | +import java.sql.SQLException; | ||
| 11 | import java.util.List; | 17 | import java.util.List; |
| 18 | +import java.util.UUID; | ||
| 12 | 19 | ||
| 13 | /** | 20 | /** |
| 14 | * @Description: test | 21 | * @Description: test |
| @@ -20,10 +27,14 @@ import java.util.List; | @@ -20,10 +27,14 @@ import java.util.List; | ||
| 20 | public class IEmbeddingsServiceImpl implements IEmbeddingsService { | 27 | public class IEmbeddingsServiceImpl implements IEmbeddingsService { |
| 21 | @Autowired | 28 | @Autowired |
| 22 | private PgVectorMapper pgVectorMapper; | 29 | private PgVectorMapper pgVectorMapper; |
| 30 | + @Autowired | ||
| 31 | + private AiModelUtils aiModelUtils; | ||
| 32 | + @Value("${jeecg.ai-chat.embedId}") | ||
| 33 | + private String embedId; | ||
| 23 | 34 | ||
| 24 | @Override | 35 | @Override |
| 25 | - public Page<Embeddings> findAll(Embeddings embeddings, int pageNo, int pageSize) { | ||
| 26 | - return pgVectorMapper.findAll(embeddings,pageNo,pageSize); | 36 | + public Page<Embeddings> findAll(Page<Embeddings> page, Embeddings embeddings) { |
| 37 | + return pgVectorMapper.findAll(page,embeddings); | ||
| 27 | } | 38 | } |
| 28 | 39 | ||
| 29 | @Override | 40 | @Override |
| @@ -36,11 +47,28 @@ public class IEmbeddingsServiceImpl implements IEmbeddingsService { | @@ -36,11 +47,28 @@ public class IEmbeddingsServiceImpl implements IEmbeddingsService { | ||
| 36 | } | 47 | } |
| 37 | 48 | ||
| 38 | public int insert(Embeddings record) { | 49 | public int insert(Embeddings record) { |
| 50 | + record.setId(UUID.randomUUID().toString()); | ||
| 51 | + Response<Embedding> embedding = aiModelUtils.getEmbedding(embedId, record.getText()); | ||
| 52 | + record.setEmbedding(embedding.content().vector()); | ||
| 39 | return pgVectorMapper.insert(record); | 53 | return pgVectorMapper.insert(record); |
| 40 | } | 54 | } |
| 41 | 55 | ||
| 42 | @Override | 56 | @Override |
| 43 | - public int update(Embeddings record) { | 57 | + public int update(Embeddings record) throws SQLException { |
| 58 | + JSONObject mataData = new JSONObject(); | ||
| 59 | + mataData.put("knowledgeId", record.getKnowledgeId()); // 使用前端传入的知识库ID | ||
| 60 | + mataData.put("docName", record.getDocName()); | ||
| 61 | + mataData.put("docId", record.getDocId()); // 自动生成唯一文档ID | ||
| 62 | + mataData.put("index", "0"); | ||
| 63 | + PGobject jsonObject = new PGobject(); | ||
| 64 | + jsonObject.setType("json"); | ||
| 65 | + jsonObject.setValue(mataData.toJSONString()); | ||
| 66 | + | ||
| 67 | + record.setMetadata(mataData); | ||
| 68 | + Response<Embedding> embedding = aiModelUtils.getEmbedding(embedId, record.getText()); | ||
| 69 | + | ||
| 70 | + record.setEmbedding(embedding.content().vector()); | ||
| 71 | + | ||
| 44 | return pgVectorMapper.update(record); | 72 | return pgVectorMapper.update(record); |
| 45 | } | 73 | } |
| 46 | 74 |
| 1 | -package org.jeecg.modules.airag.zdyrag.controller; | ||
| 2 | - | ||
| 3 | -import cn.hutool.core.collection.CollectionUtil; | ||
| 4 | -import com.fasterxml.jackson.databind.ObjectMapper; | ||
| 5 | -import com.hankcs.hanlp.summary.TextRankKeyword; | ||
| 6 | -import dev.langchain4j.data.message.ChatMessage; | ||
| 7 | -import dev.langchain4j.data.message.UserMessage; | ||
| 8 | -import dev.langchain4j.service.TokenStream; | ||
| 9 | -import dev.langchain4j.store.embedding.pgvector.PgVectorEmbeddingStore; | ||
| 10 | -import io.swagger.v3.oas.annotations.Operation; | ||
| 11 | -import lombok.extern.slf4j.Slf4j; | ||
| 12 | -import org.apache.commons.lang3.StringUtils; | ||
| 13 | -import org.apache.shiro.SecurityUtils; | ||
| 14 | -import org.apache.shiro.subject.Subject; | ||
| 15 | -import org.apache.shiro.util.ThreadContext; | ||
| 16 | -import org.jeecg.ai.handler.AIParams; | ||
| 17 | -import org.jeecg.ai.handler.LLMHandler; | ||
| 18 | -import org.jeecg.common.api.vo.Result; | ||
| 19 | -import org.jeecg.common.system.vo.LoginUser; | ||
| 20 | -import org.jeecg.modules.airag.app.entity.AiragLog; | ||
| 21 | -import org.jeecg.modules.airag.app.entity.QuestionEmbedding; | ||
| 22 | -import org.jeecg.modules.airag.app.service.IAiragLogService; | ||
| 23 | -import org.jeecg.modules.airag.app.service.IQuestionEmbeddingService; | ||
| 24 | -import org.jeecg.modules.airag.app.utils.FileToBase64Util; | ||
| 25 | -import org.jeecg.modules.airag.common.handler.IAIChatHandler; | ||
| 26 | -import org.jeecg.modules.airag.llm.handler.EmbeddingHandler; | ||
| 27 | -import org.jeecg.modules.airag.llm.service.IAiragKnowledgeService; | ||
| 28 | -import org.springframework.beans.factory.annotation.Autowired; | ||
| 29 | -import org.springframework.beans.factory.annotation.Value; | ||
| 30 | -import org.springframework.stereotype.Component; | ||
| 31 | -import org.springframework.web.bind.annotation.GetMapping; | ||
| 32 | -import org.springframework.web.bind.annotation.RequestMapping; | ||
| 33 | -import org.springframework.web.bind.annotation.RestController; | ||
| 34 | -import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; | ||
| 35 | - | ||
| 36 | -import java.io.File; | ||
| 37 | -import java.io.IOException; | ||
| 38 | -import java.util.concurrent.ExecutorService; | ||
| 39 | -import java.util.concurrent.Executors; | ||
| 40 | - | ||
| 41 | -import java.util.*; | ||
| 42 | - | ||
| 43 | -/** | ||
| 44 | - * 直接回答llm | ||
| 45 | - */ | ||
| 46 | -@RestController | ||
| 47 | -@RequestMapping("/airag/zdyRag") | ||
| 48 | -@Slf4j | ||
| 49 | -public class KeyRagController { | ||
| 50 | - @Autowired | ||
| 51 | - private EmbeddingHandler embeddingHandler; | ||
| 52 | - @Autowired | ||
| 53 | - IAIChatHandler aiChatHandler; | ||
| 54 | - @Autowired | ||
| 55 | - private IQuestionEmbeddingService questionEmbeddingService; | ||
| 56 | - @Value("${jeecg.upload.path}") | ||
| 57 | - private String uploadPath; | ||
| 58 | - @Autowired | ||
| 59 | - private IAiragLogService airagLogService; | ||
| 60 | - | ||
| 61 | - | ||
| 62 | - // 用于异步处理的线程池 | ||
| 63 | - private final ExecutorService executor = Executors.newCachedThreadPool(); | ||
| 64 | - | ||
| 65 | - @Operation(summary = "sendStream1") | ||
| 66 | - @GetMapping("sendStream1") | ||
| 67 | - public SseEmitter sendStream(String questionText) throws Exception { | ||
| 68 | - SseEmitter emitter = new SseEmitter(300000L); | ||
| 69 | - | ||
| 70 | - // 创建日志对象 | ||
| 71 | - String modelId = "1926875898187878401"; | ||
| 72 | - AiragLog logRecord = new AiragLog() | ||
| 73 | - .setQuestion(questionText) | ||
| 74 | - .setModelId(modelId) | ||
| 75 | - .setCreateTime(new Date()); | ||
| 76 | - | ||
| 77 | - executor.execute(() -> { | ||
| 78 | - String knowId = "1926872137990148098"; | ||
| 79 | - try { | ||
| 80 | - List<Map<String, Object>> maps = embeddingHandler.searchEmbedding(knowId, questionText, 2, 0.78); | ||
| 81 | - // 从知识库搜索 | ||
| 82 | - if (CollectionUtil.isEmpty(maps)) { | ||
| 83 | - Map<String, String> data = new HashMap<>(); | ||
| 84 | - data.put("token", "该问题未记录在知识库中"); | ||
| 85 | - emitter.send(SseEmitter.event().data(new ObjectMapper().writeValueAsString(data))); | ||
| 86 | - | ||
| 87 | - // 准备END事件数据 | ||
| 88 | - Map<String, String> endData = new HashMap<>(); | ||
| 89 | - endData.put("event", "END"); | ||
| 90 | - emitter.send(SseEmitter.event().data(new ObjectMapper().writeValueAsString(endData))); | ||
| 91 | - | ||
| 92 | - // 记录日志 - 未命中任何知识库 | ||
| 93 | - logRecord.setAnswer("该问题未记录在知识库中") | ||
| 94 | - .setAnswerType(3) | ||
| 95 | - .setIsStorage(0); | ||
| 96 | - airagLogService.save(logRecord); | ||
| 97 | - | ||
| 98 | - emitter.complete(); | ||
| 99 | - return; | ||
| 100 | - } | ||
| 101 | - | ||
| 102 | - // 构建知识库内容 | ||
| 103 | - StringBuilder content = new StringBuilder(); | ||
| 104 | - for (Map<String, Object> map : maps) { | ||
| 105 | - if (Double.parseDouble(map.get("score").toString()) > 0.78) { | ||
| 106 | - content.append(map.get("content").toString()).append("\n"); | ||
| 107 | - } | ||
| 108 | - } | ||
| 109 | - TextRankKeyword textRank = new TextRankKeyword(); | ||
| 110 | - | ||
| 111 | - List<String> keyWords = textRank.getKeywords(questionText, 5); | ||
| 112 | - System.out.println("关键词...:" + keyWords); | ||
| 113 | - // 获取第一个匹配的元数据用于日志和文件信息 | ||
| 114 | - Map<String, Object> firstMatch = maps.get(0); | ||
| 115 | - String fileName = generateFileDocName(firstMatch.get("metadata").toString()); | ||
| 116 | - String storedFileName = generateFilePath(firstMatch.get("metadata").toString()); | ||
| 117 | - // 构建更优化的prompt | ||
| 118 | - String prompt = String.format("你是一个严谨的信息处理助手,请严格按照以下要求处理用户问题:" + questionText + "\n\n" + | ||
| 119 | - "处理步骤和要求:\n" + | ||
| 120 | - "1. 严格基于参考内容回答,禁止任何超出参考内容的推断或想象\n" + | ||
| 121 | - "2. 严格基于参考内容回答,禁止使用参考内容中与问题无关的内容\n" + | ||
| 122 | - "3. 回答结构:\n" + | ||
| 123 | - " - 首先用一句话直接回答问题核心(仅限参考内容中明确包含的信息)\n" + | ||
| 124 | - " - 然后列出支持该答案的具体内容(可直接引用参考内容)\n" + | ||
| 125 | - "4. 禁止以下行为:\n" + | ||
| 126 | - " - 添加参考内容中不存在的信息\n" + | ||
| 127 | - " - 在回答中提及‘参考内容’等字样\n" + | ||
| 128 | - " - 在回答中提及其他产品的功能\n" + | ||
| 129 | - " - 进行任何推测性陈述\n" + | ||
| 130 | - " - 使用模糊或不确定的表达\n" + | ||
| 131 | - " - 参考内容为空时应该拒绝回答\n" + | ||
| 132 | - "参考内容(请严格限制回答范围于此):\n" + content); | ||
| 133 | - | ||
| 134 | - List<ChatMessage> messages = new ArrayList<>(); | ||
| 135 | - messages.add(new UserMessage("user", prompt)); | ||
| 136 | - StringBuilder answerBuilder = new StringBuilder(); | ||
| 137 | - | ||
| 138 | - TokenStream tokenStream = aiChatHandler.chat(modelId, messages); | ||
| 139 | - tokenStream.onNext(token -> { | ||
| 140 | - try { | ||
| 141 | - answerBuilder.append(token); | ||
| 142 | - Map<String, String> data = new HashMap<>(); | ||
| 143 | - data.put("token", token); | ||
| 144 | - emitter.send(SseEmitter.event().data(new ObjectMapper().writeValueAsString(data))); | ||
| 145 | - } catch (Exception e) { | ||
| 146 | - log.error("发送token失败", e); | ||
| 147 | - } | ||
| 148 | - }); | ||
| 149 | - | ||
| 150 | - tokenStream.onComplete(response -> { | ||
| 151 | - try { | ||
| 152 | - // 准备END事件数据 | ||
| 153 | - Map<String, String> endData = new HashMap<>(); | ||
| 154 | - endData.put("event", "END"); | ||
| 155 | - endData.put("similarity", firstMatch.get("score").toString()); | ||
| 156 | - endData.put("fileName", fileName); | ||
| 157 | - endData.put("fileBase64", FileToBase64Util.fileToBase64(uploadPath + storedFileName)); | ||
| 158 | - emitter.send(SseEmitter.event().data(new ObjectMapper().writeValueAsString(endData))); | ||
| 159 | - | ||
| 160 | - // 记录日志 - 从知识库生成回答 | ||
| 161 | - logRecord.setAnswer(answerBuilder.toString()) | ||
| 162 | - .setAnswerType(2); | ||
| 163 | - | ||
| 164 | - System.out.println("回答内容 = " + answerBuilder); | ||
| 165 | - airagLogService.save(logRecord); | ||
| 166 | - | ||
| 167 | - emitter.complete(); | ||
| 168 | - } catch (Exception e) { | ||
| 169 | - log.error("流式响应结束时发生错误", e); | ||
| 170 | - } | ||
| 171 | - }); | ||
| 172 | - | ||
| 173 | - tokenStream.onError(error -> { | ||
| 174 | - log.error("生成答案失败", error); | ||
| 175 | - // 记录日志 - 错误情况 | ||
| 176 | - logRecord.setAnswer("生成答案失败: " + error.getMessage()) | ||
| 177 | - .setAnswerType(4); | ||
| 178 | - airagLogService.save(logRecord); | ||
| 179 | - emitter.completeWithError(error); | ||
| 180 | - }); | ||
| 181 | - | ||
| 182 | - tokenStream.start(); | ||
| 183 | - } catch (Exception e) { | ||
| 184 | - log.error("处理请求时发生异常", e); | ||
| 185 | - // 记录日志 - 异常情况 | ||
| 186 | - logRecord.setAnswer("处理请求时发生异常: " + e.getMessage()) | ||
| 187 | - .setAnswerType(4); | ||
| 188 | - airagLogService.save(logRecord); | ||
| 189 | - emitter.completeWithError(e); | ||
| 190 | - } | ||
| 191 | - }); | ||
| 192 | - return emitter; | ||
| 193 | - } | ||
| 194 | - | ||
| 195 | - | ||
| 196 | - private String generateFilePath(String metadataJson) throws Exception { | ||
| 197 | - if (StringUtils.isEmpty(metadataJson)) { | ||
| 198 | - return ""; | ||
| 199 | - } | ||
| 200 | - ObjectMapper objectMapper = new ObjectMapper(); | ||
| 201 | - // 解析JSON字符串 | ||
| 202 | - Map<String, String> metadata = objectMapper.readValue(metadataJson, Map.class); | ||
| 203 | - | ||
| 204 | - // 获取docName和docId | ||
| 205 | - return metadata.get("storedFileName"); | ||
| 206 | - | ||
| 207 | - } | ||
| 208 | - private String generateFileDocName(String metadataJson) throws Exception { | ||
| 209 | - if (StringUtils.isEmpty(metadataJson)) { | ||
| 210 | - return ""; | ||
| 211 | - } | ||
| 212 | - ObjectMapper objectMapper = new ObjectMapper(); | ||
| 213 | - // 解析JSON字符串 | ||
| 214 | - Map<String, String> metadata = objectMapper.readValue(metadataJson, Map.class); | ||
| 215 | - | ||
| 216 | - return metadata.get("docName"); | ||
| 217 | - | ||
| 218 | - } | ||
| 219 | - | ||
| 220 | - | ||
| 221 | - | ||
| 222 | -} |
| 1 | package org.jeecg.modules.airag.zdyrag.controller; | 1 | package org.jeecg.modules.airag.zdyrag.controller; |
| 2 | 2 | ||
| 3 | -import cn.hutool.core.collection.CollectionUtil; | ||
| 4 | -import com.fasterxml.jackson.databind.ObjectMapper; | ||
| 5 | -import dev.langchain4j.data.message.ChatMessage; | ||
| 6 | -import dev.langchain4j.data.message.UserMessage; | ||
| 7 | -import dev.langchain4j.service.TokenStream; | ||
| 8 | -import dev.langchain4j.store.embedding.pgvector.PgVectorEmbeddingStore; | ||
| 9 | import io.swagger.v3.oas.annotations.Operation; | 3 | import io.swagger.v3.oas.annotations.Operation; |
| 10 | import lombok.extern.slf4j.Slf4j; | 4 | import lombok.extern.slf4j.Slf4j; |
| 11 | -import org.apache.commons.lang3.StringUtils; | ||
| 12 | -import org.apache.shiro.SecurityUtils; | ||
| 13 | -import org.apache.shiro.subject.Subject; | ||
| 14 | -import org.apache.shiro.util.ThreadContext; | ||
| 15 | -import org.jeecg.ai.handler.AIParams; | ||
| 16 | -import org.jeecg.ai.handler.LLMHandler; | ||
| 17 | -import org.jeecg.common.api.vo.Result; | ||
| 18 | -import org.jeecg.common.system.vo.LoginUser; | ||
| 19 | -import org.jeecg.modules.airag.app.entity.AiragLog; | ||
| 20 | -import org.jeecg.modules.airag.app.entity.QuestionEmbedding; | ||
| 21 | -import org.jeecg.modules.airag.app.service.IAiragLogService; | ||
| 22 | -import org.jeecg.modules.airag.app.service.IQuestionEmbeddingService; | ||
| 23 | -import org.jeecg.modules.airag.app.utils.FileToBase64Util; | ||
| 24 | -import org.jeecg.modules.airag.common.handler.IAIChatHandler; | ||
| 25 | -import org.jeecg.modules.airag.llm.handler.EmbeddingHandler; | ||
| 26 | -import org.jeecg.modules.airag.llm.service.IAiragKnowledgeService; | 5 | +import org.jeecg.modules.airag.zdyrag.service.AiragResponseService; |
| 27 | import org.springframework.beans.factory.annotation.Autowired; | 6 | import org.springframework.beans.factory.annotation.Autowired; |
| 28 | -import org.springframework.beans.factory.annotation.Value; | ||
| 29 | -import org.springframework.stereotype.Component; | ||
| 30 | import org.springframework.web.bind.annotation.GetMapping; | 7 | import org.springframework.web.bind.annotation.GetMapping; |
| 31 | import org.springframework.web.bind.annotation.RequestMapping; | 8 | import org.springframework.web.bind.annotation.RequestMapping; |
| 32 | import org.springframework.web.bind.annotation.RequestParam; | 9 | import org.springframework.web.bind.annotation.RequestParam; |
| 33 | import org.springframework.web.bind.annotation.RestController; | 10 | import org.springframework.web.bind.annotation.RestController; |
| 34 | import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; | 11 | import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; |
| 35 | 12 | ||
| 36 | -import java.io.File; | ||
| 37 | -import java.io.IOException; | ||
| 38 | -import java.util.concurrent.ExecutorService; | ||
| 39 | -import java.util.concurrent.Executors; | ||
| 40 | - | ||
| 41 | -import java.util.*; | ||
| 42 | - | ||
| 43 | -/** | ||
| 44 | - * 直接回答llm | ||
| 45 | - */ | ||
| 46 | @RestController | 13 | @RestController |
| 47 | @RequestMapping("/airag/zdyRag") | 14 | @RequestMapping("/airag/zdyRag") |
| 48 | @Slf4j | 15 | @Slf4j |
| 49 | public class ZdyRagController { | 16 | public class ZdyRagController { |
| 50 | - @Autowired | ||
| 51 | - private EmbeddingHandler embeddingHandler; | ||
| 52 | - @Autowired | ||
| 53 | - IAIChatHandler aiChatHandler; | ||
| 54 | - @Autowired | ||
| 55 | - private IQuestionEmbeddingService questionEmbeddingService; | ||
| 56 | - @Value("${jeecg.upload.path}") | ||
| 57 | - private String uploadPath; | ||
| 58 | - @Autowired | ||
| 59 | - private IAiragLogService airagLogService; | ||
| 60 | - | ||
| 61 | 17 | ||
| 62 | - // 用于异步处理的线程池 | ||
| 63 | - private final ExecutorService executor = Executors.newCachedThreadPool(); | 18 | + @Autowired |
| 19 | + private AiragResponseService airagResponseService; | ||
| 20 | + | ||
| 21 | + | ||
| 22 | + /** | ||
| 23 | + * @author lixiang | ||
| 24 | + * @param questionText 问题文本 | ||
| 25 | + * @param code 快捷按钮code | ||
| 26 | + * @param codeType 提问方式,用于记录日志,区分输入框提问还是快捷方式 | ||
| 27 | + * @param user 提问人 | ||
| 28 | + * @return 以流式返回回答结果 | ||
| 29 | + * | ||
| 30 | + * 1、将提问文本与问题库匹配,若匹配则回答预设回答结果 | ||
| 31 | + * 2、若问题库中无匹配预设问题,则查询知识库,将查询到的知识提供给llm模型,生成回答结果 | ||
| 32 | + * 3、回答时会将当初上传的资料以参考资料的形式进行返回,可进行预览 | ||
| 33 | + * 4、将本次的问答结果记录日志 | ||
| 34 | + * todo :增加产品推荐功能? | ||
| 35 | + */ | ||
| 64 | 36 | ||
| 65 | @Operation(summary = "sendStream") | 37 | @Operation(summary = "sendStream") |
| 66 | @GetMapping("sendStream") | 38 | @GetMapping("sendStream") |
| 67 | public SseEmitter sendStream(@RequestParam("questionText") String questionText, | 39 | public SseEmitter sendStream(@RequestParam("questionText") String questionText, |
| 68 | @RequestParam("code") String code, | 40 | @RequestParam("code") String code, |
| 69 | @RequestParam("codeType") Integer codeType, | 41 | @RequestParam("codeType") Integer codeType, |
| 70 | - @RequestParam("user") String user | ||
| 71 | - ) throws Exception { | ||
| 72 | - SseEmitter emitter = new SseEmitter(300000L); | ||
| 73 | - | ||
| 74 | - // 创建日志对象 | ||
| 75 | - String modelId = "1926875898187878401"; | ||
| 76 | - AiragLog logRecord = new AiragLog() | ||
| 77 | - .setQuestion(questionText) | ||
| 78 | - .setCode(code) | ||
| 79 | - .setCreateBy(user) | ||
| 80 | - .setCodeType(codeType) | ||
| 81 | - .setModelId(modelId) | ||
| 82 | - .setCreateTime(new Date()); | ||
| 83 | - | ||
| 84 | - executor.execute(() -> { | ||
| 85 | - try { | ||
| 86 | - String knowId = "1926872137990148098"; | ||
| 87 | - List<QuestionEmbedding> questionEmbeddings = questionEmbeddingService.similaritySearchByQuestion(questionText, 1, 0.8); | ||
| 88 | - | ||
| 89 | - // 如果从问题库中找到匹配 | ||
| 90 | - if (!questionEmbeddings.isEmpty()) { | ||
| 91 | - QuestionEmbedding questionEmbedding = questionEmbeddings.get(0); | ||
| 92 | - Map<String, String> data = new HashMap<>(); | ||
| 93 | - data.put("token", questionEmbedding.getAnswer()); | ||
| 94 | - emitter.send(SseEmitter.event().data(new ObjectMapper().writeValueAsString(data))); | ||
| 95 | - | ||
| 96 | - // 解析元数据 | ||
| 97 | - ObjectMapper objectMapper = new ObjectMapper(); | ||
| 98 | - Map<String, String> metadata = objectMapper.readValue(questionEmbedding.getMetadata(), Map.class); | ||
| 99 | - | ||
| 100 | - // 准备END事件数据 | ||
| 101 | - Map<String, String> endData = new HashMap<>(); | ||
| 102 | - endData.put("event", "END"); | ||
| 103 | - endData.put("similarity", String.valueOf(questionEmbedding.getSimilarity())); | ||
| 104 | - if (metadata != null) { | ||
| 105 | - String docName = metadata.get("docName"); | ||
| 106 | - endData.put("fileName", docName); | ||
| 107 | - String fileName = generateFilePath(questionEmbedding.getMetadata()); | ||
| 108 | - if (StringUtils.isNotBlank(fileName)) { | ||
| 109 | - endData.put("fileBase64", FileToBase64Util.fileToBase64(uploadPath + fileName)); | ||
| 110 | - } | ||
| 111 | - } | ||
| 112 | - emitter.send(SseEmitter.event().data(objectMapper.writeValueAsString(endData))); | ||
| 113 | - | ||
| 114 | - // 记录日志 - 从问题库匹配 | ||
| 115 | - logRecord.setAnswer(questionEmbedding.getAnswer()) | ||
| 116 | - .setAnswerType(1); | ||
| 117 | - airagLogService.save(logRecord); | ||
| 118 | - | ||
| 119 | - emitter.complete(); | ||
| 120 | - return; | ||
| 121 | - } | ||
| 122 | - | ||
| 123 | - // 从知识库搜索 | ||
| 124 | - List<Map<String, Object>> maps = embeddingHandler.searchEmbedding(knowId, questionText, 2, 0.78); | ||
| 125 | - if (CollectionUtil.isEmpty(maps)) { | ||
| 126 | - Map<String, String> data = new HashMap<>(); | ||
| 127 | - data.put("token", "该问题未记录在知识库中"); | ||
| 128 | - emitter.send(SseEmitter.event().data(new ObjectMapper().writeValueAsString(data))); | ||
| 129 | - | ||
| 130 | - // 准备END事件数据 | ||
| 131 | - Map<String, String> endData = new HashMap<>(); | ||
| 132 | - endData.put("event", "END"); | ||
| 133 | - emitter.send(SseEmitter.event().data(new ObjectMapper().writeValueAsString(endData))); | ||
| 134 | - | ||
| 135 | - // 记录日志 - 未命中任何知识库 | ||
| 136 | - logRecord.setAnswer("该问题未记录在知识库中") | ||
| 137 | - .setAnswerType(3) | ||
| 138 | - .setIsStorage(0); | ||
| 139 | - airagLogService.save(logRecord); | ||
| 140 | - | ||
| 141 | - emitter.complete(); | ||
| 142 | - return; | ||
| 143 | - } | ||
| 144 | - | ||
| 145 | - // 构建知识库内容 | ||
| 146 | - StringBuilder content = new StringBuilder(); | ||
| 147 | - for (Map<String, Object> map : maps) { | ||
| 148 | - if (Double.parseDouble(map.get("score").toString()) > 0.78) { | ||
| 149 | - content.append(map.get("content").toString()).append("\n"); | ||
| 150 | - } | ||
| 151 | - } | ||
| 152 | - | ||
| 153 | - // 获取第一个匹配的元数据用于日志和文件信息 | ||
| 154 | - Map<String, Object> firstMatch = maps.get(0); | ||
| 155 | - String fileName = generateFileDocName(firstMatch.get("metadata").toString()); | ||
| 156 | - String storedFileName = generateFilePath(firstMatch.get("metadata").toString()); | ||
| 157 | - | ||
| 158 | - // 构建问题提示 | ||
| 159 | - String questin = "你是一个严谨的信息处理助手,请严格按照以下要求处理用户问题:" + questionText + "\n\n" + | ||
| 160 | - "处理步骤和要求:\n" + | ||
| 161 | - "1. 严格基于参考内容回答,禁止任何超出参考内容的推断或想象\n" + | ||
| 162 | - "2. 严格基于参考内容回答,禁止使用参考内容中与问题无关的内容\n" + | ||
| 163 | - "3. 回答结构:\n" + | ||
| 164 | - " - 首先用一句话直接回答问题核心(仅限参考内容中明确包含的信息)\n" + | ||
| 165 | - " - 然后列出支持该答案的具体内容(可直接引用参考内容)\n" + | ||
| 166 | - "4. 禁止以下行为:\n" + | ||
| 167 | - " - 添加参考内容中不存在的信息\n" + | ||
| 168 | - " - 在回答中提及‘参考内容’等字样\n" + | ||
| 169 | - " - 在回答中提及其他产品的功能\n" + | ||
| 170 | - " - 进行任何推测性陈述\n" + | ||
| 171 | - " - 使用模糊或不确定的表达\n" + | ||
| 172 | - " - 参考内容为空时应该拒绝回答\n" + | ||
| 173 | - "参考内容(请严格限制回答范围于此):\n" + content; | ||
| 174 | - | ||
| 175 | - List<ChatMessage> messages = new ArrayList<>(); | ||
| 176 | - messages.add(new UserMessage("user", questin)); | ||
| 177 | - StringBuilder answerBuilder = new StringBuilder(); | ||
| 178 | - | ||
| 179 | - TokenStream tokenStream = aiChatHandler.chat(modelId, messages); | ||
| 180 | - tokenStream.onNext(token -> { | ||
| 181 | - try { | ||
| 182 | - answerBuilder.append(token); | ||
| 183 | - Map<String, String> data = new HashMap<>(); | ||
| 184 | - data.put("token", token); | ||
| 185 | - emitter.send(SseEmitter.event().data(new ObjectMapper().writeValueAsString(data))); | ||
| 186 | - } catch (Exception e) { | ||
| 187 | - log.error("发送token失败", e); | ||
| 188 | - } | ||
| 189 | - }); | ||
| 190 | - | ||
| 191 | - tokenStream.onComplete(response -> { | ||
| 192 | - try { | ||
| 193 | - // 准备END事件数据 | ||
| 194 | - Map<String, String> endData = new HashMap<>(); | ||
| 195 | - endData.put("event", "END"); | ||
| 196 | - endData.put("similarity", firstMatch.get("score").toString()); | ||
| 197 | - endData.put("fileName", fileName); | ||
| 198 | - endData.put("fileBase64", FileToBase64Util.fileToBase64(uploadPath + storedFileName)); | ||
| 199 | - emitter.send(SseEmitter.event().data(new ObjectMapper().writeValueAsString(endData))); | ||
| 200 | - | ||
| 201 | - // 记录日志 - 从知识库生成回答 | ||
| 202 | - logRecord.setAnswer(answerBuilder.toString()) | ||
| 203 | - .setAnswerType(2); | ||
| 204 | - | ||
| 205 | - System.out.println("回答内容 = " + answerBuilder.toString()); | ||
| 206 | - airagLogService.save(logRecord); | ||
| 207 | - | ||
| 208 | - emitter.complete(); | ||
| 209 | - } catch (Exception e) { | ||
| 210 | - log.error("流式响应结束时发生错误", e); | ||
| 211 | - } | ||
| 212 | - }); | ||
| 213 | - | ||
| 214 | - tokenStream.onError(error -> { | ||
| 215 | - log.error("生成答案失败", error); | ||
| 216 | - // 记录日志 - 错误情况 | ||
| 217 | - logRecord.setAnswer("生成答案失败: " + error.getMessage()) | ||
| 218 | - .setAnswerType(4); | ||
| 219 | - airagLogService.save(logRecord); | ||
| 220 | - emitter.completeWithError(error); | ||
| 221 | - }); | ||
| 222 | - | ||
| 223 | - tokenStream.start(); | ||
| 224 | - } catch (Exception e) { | ||
| 225 | - log.error("处理请求时发生异常", e); | ||
| 226 | - // 记录日志 - 异常情况 | ||
| 227 | - logRecord.setAnswer("处理请求时发生异常: " + e.getMessage()) | ||
| 228 | - .setAnswerType(4); | ||
| 229 | - airagLogService.save(logRecord); | ||
| 230 | - emitter.completeWithError(e); | ||
| 231 | - } | ||
| 232 | - }); | ||
| 233 | - return emitter; | 42 | + @RequestParam("user") String user) { |
| 43 | + return airagResponseService.handleStreamRequest(questionText, code, codeType, user); | ||
| 234 | } | 44 | } |
| 235 | - | ||
| 236 | - @Operation(summary = "send") | ||
| 237 | - @GetMapping("send") | ||
| 238 | - public Result<Map<String, Object>> send(String questionText) throws Exception { | ||
| 239 | - String knowId = "1926872137990148098"; | ||
| 240 | - String modelId = "1926875898187878401"; | ||
| 241 | - Integer topNumber = 1; | ||
| 242 | - Double similarity = 0.8; | ||
| 243 | - | ||
| 244 | - // 创建日志对象 | ||
| 245 | - AiragLog logRecord = new AiragLog() | ||
| 246 | - .setQuestion(questionText) | ||
| 247 | - .setModelId(modelId) | ||
| 248 | - .setCreateTime(new Date()); | ||
| 249 | - | ||
| 250 | - | ||
| 251 | - | ||
| 252 | - HashMap<String, Object> resMap = new HashMap<>(); | ||
| 253 | - //根据问题相似度进行查询 | ||
| 254 | - List<QuestionEmbedding> questionEmbeddings = questionEmbeddingService.similaritySearchByQuestion(questionText, 1,0.8); | ||
| 255 | - for (QuestionEmbedding questionEmbedding : questionEmbeddings) { | ||
| 256 | - resMap.put("question", questionText); | ||
| 257 | - resMap.put("answer", questionEmbedding.getAnswer()); | ||
| 258 | - resMap.put("similarity", questionEmbedding.getSimilarity()); | ||
| 259 | - | ||
| 260 | - ObjectMapper objectMapper = new ObjectMapper(); | ||
| 261 | - Map<String, String> metadata = objectMapper.readValue(questionEmbedding.getMetadata(), Map.class); | ||
| 262 | - // 获取docName和docId | ||
| 263 | - if (metadata != null) { | ||
| 264 | - String docName = metadata.get("docName"); | ||
| 265 | - resMap.put("fileName", docName); | ||
| 266 | - String fileName = generateFilePath(questionEmbedding.getMetadata()); | ||
| 267 | - | ||
| 268 | - if (StringUtils.isNotBlank(fileName)) { | ||
| 269 | - resMap.put("fileBase64", FileToBase64Util.fileToBase64(uploadPath + fileName)); | ||
| 270 | - } | ||
| 271 | - } | ||
| 272 | - // 记录日志 - 从问题库匹配 | ||
| 273 | - logRecord.setAnswer(questionEmbedding.getAnswer()); | ||
| 274 | - logRecord.setAnswerType(1); | ||
| 275 | - airagLogService.save(logRecord); | ||
| 276 | - | ||
| 277 | - log.info("questionEmbedding.getMetadata() = " + questionEmbedding.getMetadata()); | ||
| 278 | - log.info("questionEmbedding.getQuestion() = " + questionEmbedding.getQuestion()); | ||
| 279 | - log.info("questionEmbedding.getAnswer() = " + questionEmbedding.getAnswer()); | ||
| 280 | - log.info("questionEmbedding.getSimilarity() = " + questionEmbedding.getSimilarity()); | ||
| 281 | - log.info("-------------------------------------------------------------"); | ||
| 282 | - } | ||
| 283 | - //返回问题库命中的问题 | ||
| 284 | - if (!questionEmbeddings.isEmpty()) { | ||
| 285 | - return Result.OK(resMap); | ||
| 286 | - } | ||
| 287 | - | ||
| 288 | - List<Map<String, Object>> maps = embeddingHandler.searchEmbedding(knowId, questionText, 3, 0.75); | ||
| 289 | - if (CollectionUtil.isEmpty(maps)) { | ||
| 290 | - resMap.put("answer", "该问题未记录在知识库中"); | ||
| 291 | - // 记录日志 - 未命中任何知识库 | ||
| 292 | - logRecord.setAnswer("该问题未记录在知识库中"); | ||
| 293 | - logRecord.setAnswerType(3); | ||
| 294 | - logRecord.setIsStorage(0); | ||
| 295 | - airagLogService.save(logRecord); | ||
| 296 | - | ||
| 297 | - return Result.OK(resMap); | ||
| 298 | - } | ||
| 299 | - | ||
| 300 | - StringBuilder content = new StringBuilder(); | ||
| 301 | - for (Map<String, Object> map : maps) { | ||
| 302 | - if (Double.parseDouble(map.get("score").toString()) > 0.78){ | ||
| 303 | - log.info("score = " + map.get("score").toString()); | ||
| 304 | - log.info("content = " + map.get("content").toString()); | ||
| 305 | - content.append(map.get("content").toString()).append("\n"); | ||
| 306 | - } | ||
| 307 | - } | ||
| 308 | - | ||
| 309 | - | ||
| 310 | - List<ChatMessage> messages = new ArrayList<>(); | ||
| 311 | - | ||
| 312 | - String questin = "你是一个严格遵循指令的信息处理助手,请按照以下规范回答用户问题:\n\n" + | ||
| 313 | - "# 处理规范\n" + | ||
| 314 | - "1. 回答范围:\n" + | ||
| 315 | - " - 仅使用提供的参考内容进行回答\n" + | ||
| 316 | - " - 禁止任何超出参考内容的推断、想象或补充\n" + | ||
| 317 | - " - 当参考内容为空或不相关时,必须拒绝回答\n\n" + | ||
| 318 | - "2. 回答结构要求:\n" + | ||
| 319 | - " - 首行必须用「回答:」开头,给出最直接的事实性回答\n" + | ||
| 320 | - " - 后续每行以「•」开头列出支持证据,每条证据必须:\n" + | ||
| 321 | - " * 直接引用参考内容\n" + | ||
| 322 | - " * 标注具体出处位置(如段落编号/行号)\n" + | ||
| 323 | - " * 保持原句完整性,不得改写\n\n" + | ||
| 324 | - "3. 禁止事项:\n" + | ||
| 325 | - " - 任何形式的推测(包括\"可能\"、\"应该\"等不确定表述)\n" + | ||
| 326 | - " - 回答内容不得提出\"参考内容\"、\"证据\"等字样\n" + | ||
| 327 | - " - 参考内容中未明确出现的数字、事实或结论\n" + | ||
| 328 | - " - 总结性陈述或观点性表达\n" + | ||
| 329 | - " - 多个信息点的合并表述\n\n" + | ||
| 330 | - "4. 特殊情形处理:\n" + | ||
| 331 | - " - 专业术语必须保持原文表述\n" + | ||
| 332 | - " - 数据必须包含原始单位和精度\n\n" + | ||
| 333 | - "# 当前任务\n" + | ||
| 334 | - "问题:「" + questionText + "」\n\n" + | ||
| 335 | - "参考内容(严格限制回答范围):\n" + | ||
| 336 | - content; | ||
| 337 | - | ||
| 338 | - messages.add(new UserMessage("user", questin)); | ||
| 339 | - String chat = aiChatHandler.completions(modelId, messages, null); | ||
| 340 | - resMap.put("question", questionText); | ||
| 341 | - resMap.put("answer", chat); | ||
| 342 | - resMap.put("similarity", maps.get(0).get("score").toString()); | ||
| 343 | - String fileName = generateFileDocName(maps.get(0).get("metadata").toString()); | ||
| 344 | - String storedFileName = generateFilePath(maps.get(0).get("metadata").toString()); | ||
| 345 | - resMap.put("fileName", fileName); | ||
| 346 | - resMap.put("fileBase64",FileToBase64Util.fileToBase64(uploadPath + storedFileName)); | ||
| 347 | - | ||
| 348 | - | ||
| 349 | - // 记录日志 - 从知识库生成回答 | ||
| 350 | - logRecord.setAnswer(chat); | ||
| 351 | - logRecord.setAnswerType(2); | ||
| 352 | - airagLogService.save(logRecord); | ||
| 353 | - | ||
| 354 | - return Result.OK(resMap); | ||
| 355 | - } | ||
| 356 | - | ||
| 357 | - | ||
| 358 | - | ||
| 359 | - private String generateFilePath(String metadataJson) throws Exception { | ||
| 360 | - if (StringUtils.isEmpty(metadataJson)) { | ||
| 361 | - return ""; | ||
| 362 | - } | ||
| 363 | - ObjectMapper objectMapper = new ObjectMapper(); | ||
| 364 | - // 解析JSON字符串 | ||
| 365 | - Map<String, String> metadata = objectMapper.readValue(metadataJson, Map.class); | ||
| 366 | - | ||
| 367 | - // 获取docName和docId | ||
| 368 | - return metadata.get("storedFileName"); | ||
| 369 | - | ||
| 370 | - } | ||
| 371 | - private String generateFileDocName(String metadataJson) throws Exception { | ||
| 372 | - if (StringUtils.isEmpty(metadataJson)) { | ||
| 373 | - return ""; | ||
| 374 | - } | ||
| 375 | - ObjectMapper objectMapper = new ObjectMapper(); | ||
| 376 | - // 解析JSON字符串 | ||
| 377 | - Map<String, String> metadata = objectMapper.readValue(metadataJson, Map.class); | ||
| 378 | - | ||
| 379 | - return metadata.get("docName"); | ||
| 380 | - | ||
| 381 | - } | ||
| 382 | - | ||
| 383 | - public static void main(String[] args) { | ||
| 384 | - String s = "学生户口复印_efde055d-1207-4b6f-8d46-79eb557ca711.docx"; | ||
| 385 | - | ||
| 386 | - String s1 = StringUtils.substringBefore(s, "."); | ||
| 387 | - log.info("s1 = " + s1); | ||
| 388 | - | ||
| 389 | - | ||
| 390 | - String[] split = s.split("_"); | ||
| 391 | - for (String string : split) { | ||
| 392 | - log.info("string = " + string); | ||
| 393 | - } | ||
| 394 | - | ||
| 395 | - } | ||
| 396 | - | ||
| 397 | -} | 45 | +} |
| 1 | -package org.jeecg.modules.airag.zdyrag.controller; | ||
| 2 | - | ||
| 3 | -import cn.hutool.core.collection.CollectionUtil; | ||
| 4 | -import com.fasterxml.jackson.databind.ObjectMapper; | ||
| 5 | -import dev.langchain4j.data.message.ChatMessage; | ||
| 6 | -import dev.langchain4j.data.message.UserMessage; | ||
| 7 | -import dev.langchain4j.service.TokenStream; | ||
| 8 | -import io.swagger.v3.oas.annotations.Operation; | ||
| 9 | -import lombok.extern.slf4j.Slf4j; | ||
| 10 | -import org.apache.commons.lang3.StringUtils; | ||
| 11 | -import org.jeecg.ai.handler.LLMHandler; | ||
| 12 | -import org.jeecg.common.api.vo.Result; | ||
| 13 | -import org.jeecg.modules.airag.app.entity.AiragLog; | ||
| 14 | -import org.jeecg.modules.airag.app.service.IAiragLogService; | ||
| 15 | -import org.jeecg.modules.airag.common.handler.IAIChatHandler; | ||
| 16 | -import org.jeecg.modules.airag.llm.handler.EmbeddingHandler; | ||
| 17 | -import org.jeecg.modules.airag.app.utils.FileToBase64Util; | ||
| 18 | -import org.jeecg.modules.airag.zdyrag.helper.MultiTurnContextHelper; | ||
| 19 | -import org.springframework.beans.factory.annotation.Autowired; | ||
| 20 | -import org.springframework.beans.factory.annotation.Value; | ||
| 21 | -import org.springframework.data.redis.core.RedisTemplate; | ||
| 22 | -import org.springframework.web.bind.annotation.GetMapping; | ||
| 23 | -import org.springframework.web.bind.annotation.RequestMapping; | ||
| 24 | -import org.springframework.web.bind.annotation.RequestParam; | ||
| 25 | -import org.springframework.web.bind.annotation.RestController; | ||
| 26 | -import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; | ||
| 27 | - | ||
| 28 | -import java.util.*; | ||
| 29 | -import java.util.concurrent.*; | ||
| 30 | - | ||
| 31 | -@Slf4j | ||
| 32 | -@RestController | ||
| 33 | -@RequestMapping("/airag/zdyRag") | ||
| 34 | -public class ZdyRagMultiStageController { | ||
| 35 | - | ||
| 36 | - @Autowired | ||
| 37 | - private EmbeddingHandler embeddingHandler; | ||
| 38 | - | ||
| 39 | - @Autowired | ||
| 40 | - private IAIChatHandler aiChatHandler; | ||
| 41 | - | ||
| 42 | - @Autowired | ||
| 43 | - private IAiragLogService airagLogService; | ||
| 44 | - | ||
| 45 | - @Autowired | ||
| 46 | - private RedisTemplate<String, Object> redisTemplate; | ||
| 47 | - | ||
| 48 | - @Value("${jeecg.upload.path}") | ||
| 49 | - private String uploadPath; | ||
| 50 | - | ||
| 51 | - private final ExecutorService executor = Executors.newCachedThreadPool(); | ||
| 52 | - private final ExecutorService asyncLLMExecutor = Executors.newFixedThreadPool(5); | ||
| 53 | - | ||
| 54 | - @Operation(summary = "multiStageStream with Redis context") | ||
| 55 | - @GetMapping("multiStageStream") | ||
| 56 | - public SseEmitter multiStageStream(@RequestParam String questionText, | ||
| 57 | - @RequestParam(required = false) String sessionId) throws Exception { | ||
| 58 | - SseEmitter emitter = new SseEmitter(300000L); | ||
| 59 | - String modelId = "1926875898187878401"; | ||
| 60 | - String knowId = "1926872137990148098"; | ||
| 61 | - | ||
| 62 | - AiragLog logRecord = new AiragLog() | ||
| 63 | - .setQuestion(questionText) | ||
| 64 | - .setModelId(modelId) | ||
| 65 | - .setCreateTime(new Date()); | ||
| 66 | - | ||
| 67 | - executor.execute(() -> { | ||
| 68 | - try { | ||
| 69 | - List<Map<String, Object>> maps = embeddingHandler.searchEmbedding(knowId, questionText, 5, 0.75); | ||
| 70 | - | ||
| 71 | - // ========================== 知识库为空时,尝试使用历史上下文回答 ========================== | ||
| 72 | - if (CollectionUtil.isEmpty(maps)) { | ||
| 73 | - List<ChatMessage> historyContext = MultiTurnContextHelper.loadHistory(sessionId, redisTemplate); | ||
| 74 | - | ||
| 75 | - if (!historyContext.isEmpty()) { | ||
| 76 | - log.info("知识库为空,尝试使用历史上下文回答问题"); | ||
| 77 | - | ||
| 78 | - String prompt = MultiTurnContextHelper.buildPromptFromHistory(historyContext, questionText); | ||
| 79 | - String answer = aiChatHandler.completions(modelId, List.of(new UserMessage("user", prompt)), null); | ||
| 80 | - | ||
| 81 | - if (StringUtils.isBlank(answer) || MultiTurnContextHelper.containsRefusalKeywords(answer)) { | ||
| 82 | - sendSimpleMessage(emitter, "该问题未记录在知识库或历史中,无法回答"); | ||
| 83 | - logRecord.setAnswer("该问题未记录在知识库或历史中,无法回答").setAnswerType(3).setIsStorage(0); | ||
| 84 | - } else { | ||
| 85 | - sendSimpleMessage(emitter, answer); | ||
| 86 | - | ||
| 87 | - Map<String, String> endData = new HashMap<>(); | ||
| 88 | - endData.put("event", "END"); | ||
| 89 | - endData.put("similarity", "0.0"); | ||
| 90 | - endData.put("fileName", "历史上下文"); | ||
| 91 | - emitter.send(SseEmitter.event().data(new ObjectMapper().writeValueAsString(endData))); | ||
| 92 | - | ||
| 93 | - logRecord.setAnswer(answer).setAnswerType(2); | ||
| 94 | - MultiTurnContextHelper.saveHistory(sessionId, redisTemplate, historyContext, questionText, answer); | ||
| 95 | - } | ||
| 96 | - | ||
| 97 | - airagLogService.save(logRecord); | ||
| 98 | - emitter.complete(); | ||
| 99 | - return; | ||
| 100 | - } else { | ||
| 101 | - sendSimpleMessage(emitter, "该问题未记录在知识库中,且无历史内容可参考"); | ||
| 102 | - logRecord.setAnswer("该问题未记录在知识库中,且无历史内容可参考").setAnswerType(3).setIsStorage(0); | ||
| 103 | - airagLogService.save(logRecord); | ||
| 104 | - emitter.complete(); | ||
| 105 | - return; | ||
| 106 | - } | ||
| 107 | - } | ||
| 108 | - | ||
| 109 | - // ========================== 多线程摘要生成 ========================== | ||
| 110 | - List<Future<String>> summaryFutures = new ArrayList<>(); | ||
| 111 | - for (Map<String, Object> map : maps) { | ||
| 112 | - String content = map.get("content").toString(); | ||
| 113 | - String summaryPrompt = buildSummaryPrompt(questionText, content); | ||
| 114 | - summaryFutures.add(asyncLLMExecutor.submit(() -> | ||
| 115 | - aiChatHandler.completions(modelId, List.of(new UserMessage("user", summaryPrompt)), null) | ||
| 116 | - )); | ||
| 117 | - } | ||
| 118 | - | ||
| 119 | - List<String> summaries = new ArrayList<>(); | ||
| 120 | - for (Future<String> future : summaryFutures) { | ||
| 121 | - try { | ||
| 122 | - String summary = future.get(15, TimeUnit.SECONDS); | ||
| 123 | - if (StringUtils.isNotBlank(summary)) summaries.add(summary.trim()); | ||
| 124 | - } catch (Exception e) { | ||
| 125 | - log.warn("摘要生成失败", e); | ||
| 126 | - } | ||
| 127 | - } | ||
| 128 | - | ||
| 129 | - // ========================== 多线程候选答案生成 ========================== | ||
| 130 | - List<Future<String>> answerFutures = new ArrayList<>(); | ||
| 131 | - for (String summary : summaries) { | ||
| 132 | - String answerPrompt = buildAnswerPrompt(questionText, summary); | ||
| 133 | - answerFutures.add(asyncLLMExecutor.submit(() -> | ||
| 134 | - aiChatHandler.completions(modelId, List.of(new UserMessage("user", answerPrompt)), null) | ||
| 135 | - )); | ||
| 136 | - } | ||
| 137 | - | ||
| 138 | - List<String> candidateAnswers = new ArrayList<>(); | ||
| 139 | - for (Future<String> future : answerFutures) { | ||
| 140 | - try { | ||
| 141 | - String answer = future.get(15, TimeUnit.SECONDS); | ||
| 142 | - if (StringUtils.isNotBlank(answer)) candidateAnswers.add(answer); | ||
| 143 | - } catch (Exception e) { | ||
| 144 | - log.warn("候选答案生成失败", e); | ||
| 145 | - } | ||
| 146 | - } | ||
| 147 | - | ||
| 148 | - // ========================== 合并答案生成最终回答 ========================== | ||
| 149 | - String mergePrompt = buildMergePrompt(questionText, candidateAnswers); | ||
| 150 | - List<ChatMessage> mergeMessages = new ArrayList<>(); | ||
| 151 | - | ||
| 152 | - if (StringUtils.isNotBlank(sessionId)) { | ||
| 153 | - Object cached = redisTemplate.opsForValue().get(MultiTurnContextHelper.redisKey(sessionId)); | ||
| 154 | - if (cached instanceof List) { | ||
| 155 | - mergeMessages.addAll((List<ChatMessage>) cached); | ||
| 156 | - } | ||
| 157 | - } | ||
| 158 | - mergeMessages.add(new UserMessage("user", mergePrompt)); | ||
| 159 | - | ||
| 160 | - StringBuilder answerBuilder = new StringBuilder(); | ||
| 161 | - | ||
| 162 | - Map<String, Object> firstMatch = maps.get(0); | ||
| 163 | - String storedFileName = extractFieldFromMetadata(firstMatch.get("metadata"), "storedFileName"); | ||
| 164 | - String docName = extractFieldFromMetadata(firstMatch.get("metadata"), "docName"); | ||
| 165 | - String similarityScore = String.valueOf(firstMatch.get("score")); | ||
| 166 | - | ||
| 167 | - TokenStream tokenStream = aiChatHandler.chat(modelId, mergeMessages); | ||
| 168 | - | ||
| 169 | - tokenStream.onNext(token -> { | ||
| 170 | - try { | ||
| 171 | - answerBuilder.append(token); | ||
| 172 | - Map<String, String> data = new HashMap<>(); | ||
| 173 | - data.put("token", token); | ||
| 174 | - emitter.send(SseEmitter.event().data(new ObjectMapper().writeValueAsString(data))); | ||
| 175 | - } catch (Exception e) { | ||
| 176 | - log.error("发送 token 失败", e); | ||
| 177 | - } | ||
| 178 | - }); | ||
| 179 | - | ||
| 180 | - tokenStream.onComplete(response -> { | ||
| 181 | - try { | ||
| 182 | - Map<String, String> endData = new HashMap<>(); | ||
| 183 | - endData.put("event", "END"); | ||
| 184 | - endData.put("similarity", similarityScore); | ||
| 185 | - endData.put("fileName", docName); | ||
| 186 | - if (StringUtils.isNotBlank(storedFileName)) { | ||
| 187 | - endData.put("fileBase64", FileToBase64Util.fileToBase64(uploadPath + storedFileName)); | ||
| 188 | - } | ||
| 189 | - emitter.send(SseEmitter.event().data(new ObjectMapper().writeValueAsString(endData))); | ||
| 190 | - | ||
| 191 | - logRecord.setAnswer(answerBuilder.toString()).setAnswerType(2); | ||
| 192 | - airagLogService.save(logRecord); | ||
| 193 | - | ||
| 194 | - MultiTurnContextHelper.saveHistory(sessionId, redisTemplate, | ||
| 195 | - MultiTurnContextHelper.loadHistory(sessionId, redisTemplate), | ||
| 196 | - questionText, answerBuilder.toString()); | ||
| 197 | - | ||
| 198 | - emitter.complete(); | ||
| 199 | - } catch (Exception e) { | ||
| 200 | - emitter.completeWithError(e); | ||
| 201 | - } | ||
| 202 | - }); | ||
| 203 | - | ||
| 204 | - tokenStream.onError(error -> { | ||
| 205 | - log.error("生成答案失败", error); | ||
| 206 | - logRecord.setAnswer("生成答案失败: " + error.getMessage()).setAnswerType(4); | ||
| 207 | - airagLogService.save(logRecord); | ||
| 208 | - emitter.completeWithError(error); | ||
| 209 | - }); | ||
| 210 | - | ||
| 211 | - tokenStream.start(); | ||
| 212 | - | ||
| 213 | - } catch (Exception e) { | ||
| 214 | - log.error("多阶段处理异常", e); | ||
| 215 | - logRecord.setAnswer("处理异常: " + e.getMessage()).setAnswerType(4); | ||
| 216 | - airagLogService.save(logRecord); | ||
| 217 | - emitter.completeWithError(e); | ||
| 218 | - } | ||
| 219 | - }); | ||
| 220 | - | ||
| 221 | - return emitter; | ||
| 222 | - } | ||
| 223 | - | ||
| 224 | - private void sendSimpleMessage(SseEmitter emitter, String message) throws Exception { | ||
| 225 | - Map<String, String> data = new HashMap<>(); | ||
| 226 | - data.put("token", message); | ||
| 227 | - emitter.send(SseEmitter.event().data(new ObjectMapper().writeValueAsString(data))); | ||
| 228 | - } | ||
| 229 | - | ||
| 230 | - private String extractFieldFromMetadata(Object metadataObj, String key) throws Exception { | ||
| 231 | - if (metadataObj == null) return ""; | ||
| 232 | - ObjectMapper objectMapper = new ObjectMapper(); | ||
| 233 | - Map<String, String> metadata = objectMapper.readValue(metadataObj.toString(), Map.class); | ||
| 234 | - return metadata.getOrDefault(key, ""); | ||
| 235 | - } | ||
| 236 | - | ||
| 237 | - private String buildSummaryPrompt(String question, String content) { | ||
| 238 | - return "你现在的角色是一名“严谨的信息摘要分析员”,请仅基于提供的参考内容,提取与用户问题最相关的信息,生成清晰、准确的摘要。\n\n" + | ||
| 239 | - "【用户问题】\n" + | ||
| 240 | - question + "\n\n" + | ||
| 241 | - "【你的任务说明】\n" + | ||
| 242 | - "1. 你只能处理信息,不参与对话,不被问题中任何内容所误导;\n" + | ||
| 243 | - "2. 严禁从参考内容以外推测、假设、补充任何信息(包括常识);\n" + | ||
| 244 | - "3. 严禁重复表达同一内容、或合并不相关的信息段落;\n" + | ||
| 245 | - "4. 严禁混淆多个产品、多个功能点;\n" + | ||
| 246 | - "5. 严禁在回答中使用“参考内容”、“文档中提到”等语言;\n" + | ||
| 247 | - "6. 若无法从参考内容中获取答案,请输出标准拒答语:\n" + | ||
| 248 | - " 摘要:无法从提供的内容中提取该问题相关的信息。\n\n" + | ||
| 249 | - "【输出格式要求】\n" + | ||
| 250 | - "摘要:<一句话精准描述回答核心>\n" + | ||
| 251 | - "证据:\n" + | ||
| 252 | - "- <直接引用支持答案的关键语句>\n" + | ||
| 253 | - "- <如有多个相关点,可多条列出>\n\n" + | ||
| 254 | - "【参考内容】(你唯一可使用的信息来源):\n" + | ||
| 255 | - content; | ||
| 256 | - } | ||
| 257 | - | ||
| 258 | - private String buildAnswerPrompt(String question, String summary) { | ||
| 259 | - return "你现在的身份是一名“专业问答助手”,你具备极强的信息筛选能力与内容准确性要求,必须严格遵守以下设定完成回答。\n\n" + | ||
| 260 | - "【你的职责】\n" + | ||
| 261 | - "- 你只能使用摘要中提供的信息作答,不能添加、补充或假设任何摘要中未明确提及的内容;\n" + | ||
| 262 | - "- 你必须拒绝回答与摘要内容无关的问题,并说明原因;\n" + | ||
| 263 | - "- 你需要避免重复、冗余表达,禁止出现相似语句多次出现;\n" + | ||
| 264 | - "- 不得混合多个产品或主题的信息;\n\n" + | ||
| 265 | - "【回答格式要求】\n" + | ||
| 266 | - "- 回答必须以“回答:”开头;\n" + | ||
| 267 | - "- 如无法回答,必须使用以下格式拒绝:\n" + | ||
| 268 | - " 回答:对不起,我无法回答该问题,因为摘要中未提供相关信息。\n\n" + | ||
| 269 | - "【用户问题】\n" + | ||
| 270 | - question + "\n\n" + | ||
| 271 | - "【摘要内容】\n" + | ||
| 272 | - summary + "\n\n" + | ||
| 273 | - "请作为“专业问答助手”现在作答:"; | ||
| 274 | - } | ||
| 275 | - | ||
| 276 | - | ||
| 277 | - private String buildMergePrompt(String question, List<String> answers) { | ||
| 278 | - StringBuilder sb = new StringBuilder("你收到多个候选答案,请从中选择最准确且不交叉混淆产品信息的答案作为最终回答。\n\n"); | ||
| 279 | - sb.append("用户问题:").append(question).append("\n"); | ||
| 280 | - for (int i = 0; i < answers.size(); i++) { | ||
| 281 | - sb.append("候选答案").append(i + 1).append(":\n").append(answers.get(i)).append("\n\n"); | ||
| 282 | - } | ||
| 283 | - sb.append("请直接输出最佳答案,**禁止添加新信息**或跨产品混合。"); | ||
| 284 | - return sb.toString(); | ||
| 285 | - } | ||
| 286 | - | ||
| 287 | -} |
| 1 | -package org.jeecg.modules.airag.zdyrag.helper; | ||
| 2 | - | ||
| 3 | -import com.fasterxml.jackson.databind.ObjectMapper; | ||
| 4 | -import dev.langchain4j.data.message.ChatMessage; | ||
| 5 | -import dev.langchain4j.data.message.UserMessage; | ||
| 6 | -import lombok.extern.slf4j.Slf4j; | ||
| 7 | -import org.apache.commons.lang3.StringUtils; | ||
| 8 | -import org.springframework.data.redis.core.RedisTemplate; | ||
| 9 | - | ||
| 10 | -import java.util.*; | ||
| 11 | -import java.util.concurrent.TimeUnit; | ||
| 12 | - | ||
| 13 | -@Slf4j | ||
| 14 | -public class MultiTurnContextHelper { | ||
| 15 | - | ||
| 16 | - private static final int MAX_CONTEXT_SIZE = 10; | ||
| 17 | - private static final long CONTEXT_TTL_MILLIS = 30 * 60 * 1000; // 30分钟 | ||
| 18 | - | ||
| 19 | - public static String redisKey(String sessionId) { | ||
| 20 | - return "chat:context:" + sessionId; | ||
| 21 | - } | ||
| 22 | - | ||
| 23 | - public static List<ChatMessage> loadHistory(String sessionId, RedisTemplate<String, Object> redisTemplate) { | ||
| 24 | - if (StringUtils.isBlank(sessionId)) return new ArrayList<>(); | ||
| 25 | - Object cached = redisTemplate.opsForValue().get(redisKey(sessionId)); | ||
| 26 | - if (cached instanceof List) { | ||
| 27 | - return new ArrayList<>((List<ChatMessage>) cached); | ||
| 28 | - } | ||
| 29 | - return new ArrayList<>(); | ||
| 30 | - } | ||
| 31 | - | ||
| 32 | - public static String buildPromptFromHistory(List<ChatMessage> history, String currentQuestion) { | ||
| 33 | - StringBuilder sb = new StringBuilder("你是一个对话助手,请根据以下历史对话内容回答用户当前问题:\n\n"); | ||
| 34 | - sb.append("限制要求:\n"); | ||
| 35 | - sb.append("1. 严格只能使用历史对话中明确提到的信息\n"); | ||
| 36 | - sb.append("2. 禁止任何基于常识或主观推断的补充\n"); | ||
| 37 | - sb.append("3. 若无法从历史内容中明确回答,应直接拒绝回答\n"); | ||
| 38 | - sb.append("4. 回答必须以“回答:”开头\n\n"); | ||
| 39 | - sb.append("历史对话如下(最多展示最近5轮):\n"); | ||
| 40 | - | ||
| 41 | - int count = 0; | ||
| 42 | - for (int i = Math.max(0, history.size() - 10); i < history.size(); i++) { | ||
| 43 | - ChatMessage msg = history.get(i); | ||
| 44 | - if (msg instanceof UserMessage) { | ||
| 45 | - sb.append("用户:").append(msg.text()).append("\n"); | ||
| 46 | - } else { | ||
| 47 | - sb.append("助手:").append(msg.text()).append("\n"); | ||
| 48 | - } | ||
| 49 | - count++; | ||
| 50 | - if (count >= 10) break; | ||
| 51 | - } | ||
| 52 | - | ||
| 53 | - sb.append("\n当前用户问题:").append(currentQuestion).append("\n"); | ||
| 54 | - return sb.toString(); | ||
| 55 | - } | ||
| 56 | - | ||
| 57 | - public static void saveHistory(String sessionId, RedisTemplate<String, Object> redisTemplate, | ||
| 58 | - List<ChatMessage> history, String question, String answer) { | ||
| 59 | - if (StringUtils.isBlank(sessionId)) return; | ||
| 60 | - | ||
| 61 | - history.add(new UserMessage("user", question)); | ||
| 62 | - history.add(new UserMessage("assistant", answer)); | ||
| 63 | - | ||
| 64 | - if (history.size() > MAX_CONTEXT_SIZE) { | ||
| 65 | - history = history.subList(history.size() - MAX_CONTEXT_SIZE, history.size()); | ||
| 66 | - } | ||
| 67 | - | ||
| 68 | - redisTemplate.opsForValue().set(redisKey(sessionId), history, CONTEXT_TTL_MILLIS, TimeUnit.MILLISECONDS); | ||
| 69 | - } | ||
| 70 | - | ||
| 71 | - public static boolean containsRefusalKeywords(String answer) { | ||
| 72 | - List<String> refusalKeywords = List.of("无法", "不知道", "未提及", "没有相关信息", "参考内容为空", "不能回答"); | ||
| 73 | - return refusalKeywords.stream().anyMatch(answer::contains); | ||
| 74 | - } | ||
| 75 | -} |
| 1 | +package org.jeecg.modules.airag.zdyrag.service; | ||
| 2 | + | ||
| 3 | +import org.jeecg.modules.airag.app.entity.AiragLog; | ||
| 4 | +import org.jeecg.modules.airag.app.entity.QuestionEmbedding; | ||
| 5 | +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; | ||
| 6 | + | ||
| 7 | +import java.io.IOException; | ||
| 8 | +import java.util.List; | ||
| 9 | +import java.util.Map; | ||
| 10 | + | ||
| 11 | +/** | ||
| 12 | + * Service interface for handling AI-RAG responses | ||
| 13 | + */ | ||
| 14 | +public interface AiragResponseService { | ||
| 15 | + | ||
| 16 | + | ||
| 17 | + SseEmitter handleStreamRequest(String questionText, String code, Integer codeType, String user); | ||
| 18 | + | ||
| 19 | + | ||
| 20 | + String generateFilePath(String metadataJson) throws IOException, IOException; | ||
| 21 | + | ||
| 22 | + | ||
| 23 | + String generateFileDocName(String metadataJson) throws IOException; | ||
| 24 | +} |
| 1 | +package org.jeecg.modules.airag.zdyrag.service.impl; | ||
| 2 | + | ||
| 3 | +import cn.hutool.core.collection.CollectionUtil; | ||
| 4 | +import com.fasterxml.jackson.databind.ObjectMapper; | ||
| 5 | +import dev.langchain4j.data.message.ChatMessage; | ||
| 6 | +import dev.langchain4j.data.message.SystemMessage; | ||
| 7 | +import dev.langchain4j.data.message.UserMessage; | ||
| 8 | +import dev.langchain4j.service.TokenStream; | ||
| 9 | +import lombok.extern.log4j.Log4j2; | ||
| 10 | +import org.apache.commons.lang3.StringUtils; | ||
| 11 | +import org.jeecg.modules.airag.app.entity.AiragLog; | ||
| 12 | +import org.jeecg.modules.airag.app.entity.QuestionEmbedding; | ||
| 13 | +import org.jeecg.modules.airag.app.service.IAiragLogService; | ||
| 14 | +import org.jeecg.modules.airag.app.service.IQuestionEmbeddingService; | ||
| 15 | +import org.jeecg.modules.airag.app.utils.FileToBase64Util; | ||
| 16 | +import org.jeecg.modules.airag.common.handler.IAIChatHandler; | ||
| 17 | +import org.jeecg.modules.airag.llm.handler.EmbeddingHandler; | ||
| 18 | +import org.jeecg.modules.airag.zdyrag.service.AiragResponseService; | ||
| 19 | +import org.springframework.beans.factory.annotation.Autowired; | ||
| 20 | +import org.springframework.beans.factory.annotation.Value; | ||
| 21 | +import org.springframework.stereotype.Service; | ||
| 22 | +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; | ||
| 23 | + | ||
| 24 | +import java.io.IOException; | ||
| 25 | +import java.util.*; | ||
| 26 | +import java.util.regex.Pattern; | ||
| 27 | + | ||
| 28 | +@Service | ||
| 29 | +@Log4j2 | ||
| 30 | +public class AiragResponseServiceImpl implements AiragResponseService { | ||
| 31 | + | ||
| 32 | + @Autowired | ||
| 33 | + private EmbeddingHandler embeddingHandler; | ||
| 34 | + @Autowired | ||
| 35 | + private IAIChatHandler aiChatHandler; | ||
| 36 | + @Autowired | ||
| 37 | + private IQuestionEmbeddingService questionEmbeddingService; | ||
| 38 | + @Value("${jeecg.upload.path}") | ||
| 39 | + private String uploadPath; | ||
| 40 | + @Autowired | ||
| 41 | + private IAiragLogService airagLogService; | ||
| 42 | + | ||
| 43 | + @Override | ||
| 44 | + public SseEmitter handleStreamRequest(String questionText, String code, Integer codeType, String user) { | ||
| 45 | + SseEmitter emitter = new SseEmitter(300000L); | ||
| 46 | + String modelId = "1926875898187878401"; | ||
| 47 | + AiragLog logRecord = createLogRecord(questionText, code, codeType, user, modelId); | ||
| 48 | + String cleanedQuestionText = cleanQuestionText(questionText); | ||
| 49 | + try { | ||
| 50 | + // 处理问题库匹配 | ||
| 51 | + if (handleQuestionEmbeddingMatch(emitter, cleanedQuestionText, logRecord)) { | ||
| 52 | + return emitter; | ||
| 53 | + } | ||
| 54 | + | ||
| 55 | + // 处理知识库搜索 | ||
| 56 | + handleKnowledgeBaseSearch(emitter, cleanedQuestionText, logRecord, modelId); | ||
| 57 | + } catch (Exception e) { | ||
| 58 | + handleError(emitter, logRecord, e); | ||
| 59 | + } | ||
| 60 | + | ||
| 61 | + return emitter; | ||
| 62 | + } | ||
| 63 | + | ||
| 64 | + @Override | ||
| 65 | + public String generateFilePath(String metadataJson) throws IOException { | ||
| 66 | + return extractMetadataValue(metadataJson, "storedFileName"); | ||
| 67 | + } | ||
| 68 | + | ||
| 69 | + @Override | ||
| 70 | + public String generateFileDocName(String metadataJson) throws IOException { | ||
| 71 | + return extractMetadataValue(metadataJson, "docName"); | ||
| 72 | + } | ||
| 73 | + | ||
| 74 | + private String extractMetadataValue(String metadataJson, String key) throws IOException { | ||
| 75 | + if (StringUtils.isEmpty(metadataJson)) { | ||
| 76 | + return ""; | ||
| 77 | + } | ||
| 78 | + ObjectMapper objectMapper = new ObjectMapper(); | ||
| 79 | + Map<String, String> metadata = objectMapper.readValue(metadataJson, Map.class); | ||
| 80 | + return metadata.get(key); | ||
| 81 | + } | ||
| 82 | + | ||
| 83 | + /** | ||
| 84 | + * 创建日志对象 | ||
| 85 | + * @param questionText 问题原文本 | ||
| 86 | + * @param code 快捷按钮code | ||
| 87 | + * @param codeType 提问方式,用于记录日志,区分输入框提问还是快捷方式 | ||
| 88 | + * @param user 提问人 | ||
| 89 | + * @param modelId 模型id | ||
| 90 | + * @return 返回日志对象 | ||
| 91 | + */ | ||
| 92 | + private AiragLog createLogRecord(String questionText, String code, Integer codeType, String user, String modelId) { | ||
| 93 | + return new AiragLog() | ||
| 94 | + .setQuestion(questionText) | ||
| 95 | + .setCode(code) | ||
| 96 | + .setCreateBy(user) | ||
| 97 | + .setCodeType(codeType) | ||
| 98 | + .setModelId(modelId) | ||
| 99 | + .setCreateTime(new Date()); | ||
| 100 | + } | ||
| 101 | + | ||
| 102 | + /** | ||
| 103 | + * 匹配问题库 | ||
| 104 | + * @param emitter 流式返回 | ||
| 105 | + * @param questionText 问题原文本 | ||
| 106 | + * @param logRecord 日志对象 | ||
| 107 | + * @return 返回是否匹配成功 | ||
| 108 | + */ | ||
| 109 | + private boolean handleQuestionEmbeddingMatch(SseEmitter emitter, String questionText, AiragLog logRecord) throws Exception { | ||
| 110 | + List<QuestionEmbedding> questionEmbeddings = questionEmbeddingService.similaritySearchByQuestion(questionText, 1, 0.8); | ||
| 111 | + if (questionEmbeddings.isEmpty()) { | ||
| 112 | + return false; | ||
| 113 | + } | ||
| 114 | + | ||
| 115 | + QuestionEmbedding questionEmbedding = questionEmbeddings.get(0); | ||
| 116 | + sendQuestionEmbeddingResponse(emitter, questionEmbedding); | ||
| 117 | + logRecord.setAnswer(questionEmbedding.getAnswer()).setAnswerType(1); | ||
| 118 | + airagLogService.save(logRecord); | ||
| 119 | + return true; | ||
| 120 | + } | ||
| 121 | + | ||
| 122 | + /** | ||
| 123 | + * 发送token | ||
| 124 | + * @param emitter 流式返回 | ||
| 125 | + * @param questionEmbedding 问题向量 | ||
| 126 | + */ | ||
| 127 | + private void sendQuestionEmbeddingResponse(SseEmitter emitter, QuestionEmbedding questionEmbedding) throws Exception { | ||
| 128 | + ObjectMapper objectMapper = new ObjectMapper(); | ||
| 129 | + | ||
| 130 | + // 发送token | ||
| 131 | + Map<String, String> data = new HashMap<>(); | ||
| 132 | + data.put("token", questionEmbedding.getAnswer()); | ||
| 133 | + emitter.send(SseEmitter.event().data(objectMapper.writeValueAsString(data))); | ||
| 134 | + | ||
| 135 | + // 发送END事件 | ||
| 136 | + Map<String, String> endData = createEndData(questionEmbedding.getMetadata(), String.valueOf(questionEmbedding.getSimilarity())); | ||
| 137 | + emitter.send(SseEmitter.event().data(objectMapper.writeValueAsString(endData))); | ||
| 138 | + emitter.complete(); | ||
| 139 | + } | ||
| 140 | + | ||
| 141 | + /** | ||
| 142 | + * 知识库匹配 | ||
| 143 | + * @param emitter 流式返回 | ||
| 144 | + * @param questionText 问题原文本 | ||
| 145 | + * @param logRecord 日志对象 | ||
| 146 | + * @param modelId 模型id | ||
| 147 | + */ | ||
| 148 | + private void handleKnowledgeBaseSearch(SseEmitter emitter, String questionText, AiragLog logRecord, String modelId) throws Exception { | ||
| 149 | + String knowId = "1926872137990148098"; | ||
| 150 | + List<Map<String, Object>> maps = embeddingHandler.searchEmbedding(knowId, questionText, 2, 0.78); | ||
| 151 | + | ||
| 152 | + if (CollectionUtil.isEmpty(maps)) { | ||
| 153 | + handleNoKnowledgeFound(emitter, logRecord); | ||
| 154 | + return; | ||
| 155 | + } | ||
| 156 | + | ||
| 157 | + String content = buildKnowledgeContent(maps); | ||
| 158 | + Map<String, Object> firstMatch = maps.get(0); | ||
| 159 | + String fileName = generateFileDocName(firstMatch.get("metadata").toString()); | ||
| 160 | + String storedFileName = generateFilePath(firstMatch.get("metadata").toString()); | ||
| 161 | + | ||
| 162 | + String systemPrompt = createSystemPrompt(); | ||
| 163 | + String userPrompt = createUserPrompt(questionText, content); | ||
| 164 | + | ||
| 165 | + processLLMResponse(emitter, logRecord, modelId, systemPrompt, userPrompt, firstMatch, fileName, storedFileName); | ||
| 166 | + } | ||
| 167 | + | ||
| 168 | + /** | ||
| 169 | + * 构建参考内容 | ||
| 170 | + * @param maps 匹配到的参考内容 | ||
| 171 | + * @return 返回参考内容 | ||
| 172 | + */ | ||
| 173 | + private String buildKnowledgeContent(List<Map<String, Object>> maps) { | ||
| 174 | + StringBuilder content = new StringBuilder(); | ||
| 175 | + for (Map<String, Object> map : maps) { | ||
| 176 | + if (Double.parseDouble(map.get("score").toString()) > 0.78) { | ||
| 177 | + content.append(map.get("content").toString()).append("\n"); | ||
| 178 | + } | ||
| 179 | + } | ||
| 180 | + return content.toString(); | ||
| 181 | + } | ||
| 182 | + | ||
| 183 | + /** | ||
| 184 | + * 知识库中未记录 | ||
| 185 | + * @param emitter 流式返回 | ||
| 186 | + * @param logRecord 日志对象 | ||
| 187 | + */ | ||
| 188 | + private void handleNoKnowledgeFound(SseEmitter emitter, AiragLog logRecord) throws Exception { | ||
| 189 | + ObjectMapper objectMapper = new ObjectMapper(); | ||
| 190 | + | ||
| 191 | + Map<String, String> data = new HashMap<>(); | ||
| 192 | + data.put("token", "该问题未记录在知识库中"); | ||
| 193 | + emitter.send(SseEmitter.event().data(objectMapper.writeValueAsString(data))); | ||
| 194 | + | ||
| 195 | + Map<String, String> endData = new HashMap<>(); | ||
| 196 | + endData.put("event", "END"); | ||
| 197 | + emitter.send(SseEmitter.event().data(objectMapper.writeValueAsString(endData))); | ||
| 198 | + | ||
| 199 | + logRecord.setAnswer("该问题未记录在知识库中") | ||
| 200 | + .setAnswerType(3) | ||
| 201 | + .setIsStorage(0); | ||
| 202 | + airagLogService.save(logRecord); | ||
| 203 | + emitter.complete(); | ||
| 204 | + } | ||
| 205 | + | ||
| 206 | + /** | ||
| 207 | + * 构建系统提示词 | ||
| 208 | + * @return 系统提示词 | ||
| 209 | + */ | ||
| 210 | + private String createSystemPrompt() { | ||
| 211 | + return "你是一个严谨的信息处理助手,必须严格遵守以下规则:\n" + | ||
| 212 | + "1. 严格基于参考内容回答,禁止任何超出参考内容的推断或想象\n" + | ||
| 213 | + "2. 严格基于参考内容回答,禁止使用参考内容中与问题无关的内容\n" + | ||
| 214 | + "3. 回答结构:\n" + | ||
| 215 | + " - 首先用一句话直接回答问题核心(仅限参考内容中明确包含的信息)\n" + | ||
| 216 | + " - 然后列出支持该答案的具体内容(可直接引用参考内容)\n" + | ||
| 217 | + "4. 禁止以下行为:\n" + | ||
| 218 | + " - 添加参考内容中不存在的信息\n" + | ||
| 219 | + " - 在回答中提及'参考内容'等字样\n" + | ||
| 220 | + " - 在回答中提及其他产品的功能\n" + | ||
| 221 | + " - 进行任何推测性陈述\n" + | ||
| 222 | + " - 使用模糊或不确定的表达\n" + | ||
| 223 | + " - 参考内容为空时应该拒绝回答"; | ||
| 224 | + } | ||
| 225 | + | ||
| 226 | + /** | ||
| 227 | + * 构建用户提示 | ||
| 228 | + * @param questionText 问题文本 | ||
| 229 | + * @param content 参考内容 | ||
| 230 | + * @return 用户提示词 | ||
| 231 | + */ | ||
| 232 | + private String createUserPrompt(String questionText, String content) { | ||
| 233 | + return "用户问题:\n" + | ||
| 234 | + questionText + "\n\n" + | ||
| 235 | + "参考内容(请严格限制回答范围于此):\n" + | ||
| 236 | + content; | ||
| 237 | + } | ||
| 238 | + | ||
| 239 | + /** | ||
| 240 | + * 对llm模型进行提问 | ||
| 241 | + * @param emitter 流式返回 | ||
| 242 | + * @param logRecord 日志对象 | ||
| 243 | + * @param modelId 模型id | ||
| 244 | + * @param systemPrompt 系统提示词 | ||
| 245 | + * @param userPrompt 用户提示词 | ||
| 246 | + * @param firstMatch 最相似的数据 | ||
| 247 | + * @param fileName 文件名称 | ||
| 248 | + * @param storedFileName 本地存储文件名称 | ||
| 249 | + */ | ||
| 250 | + private void processLLMResponse(SseEmitter emitter, AiragLog logRecord, String modelId, | ||
| 251 | + String systemPrompt, String userPrompt, | ||
| 252 | + Map<String, Object> firstMatch, String fileName, String storedFileName) { | ||
| 253 | + StringBuilder answerBuilder = new StringBuilder(); | ||
| 254 | + List<ChatMessage> messages = new ArrayList<>(); | ||
| 255 | + messages.add(new SystemMessage(systemPrompt)); | ||
| 256 | + messages.add(new UserMessage(userPrompt)); | ||
| 257 | + | ||
| 258 | + TokenStream tokenStream = aiChatHandler.chat(modelId, messages); | ||
| 259 | + | ||
| 260 | + tokenStream.onNext(token -> { | ||
| 261 | + try { | ||
| 262 | + answerBuilder.append(token); | ||
| 263 | + Map<String, String> data = new HashMap<>(); | ||
| 264 | + data.put("token", token); | ||
| 265 | + emitter.send(SseEmitter.event().data(new ObjectMapper().writeValueAsString(data))); | ||
| 266 | + } catch (Exception e) { | ||
| 267 | + log.error("发送token失败", e); | ||
| 268 | + } | ||
| 269 | + }); | ||
| 270 | + | ||
| 271 | + tokenStream.onComplete(response -> { | ||
| 272 | + try { | ||
| 273 | + Map<String, String> endData = createEndData(firstMatch.get("metadata").toString(), | ||
| 274 | + firstMatch.get("score").toString()); | ||
| 275 | + endData.put("fileName", fileName); | ||
| 276 | + endData.put("fileBase64", FileToBase64Util.fileToBase64(uploadPath + storedFileName)); | ||
| 277 | + | ||
| 278 | + emitter.send(SseEmitter.event().data(new ObjectMapper().writeValueAsString(endData))); | ||
| 279 | + logRecord.setAnswer(answerBuilder.toString()).setAnswerType(2); | ||
| 280 | + airagLogService.save(logRecord); | ||
| 281 | + emitter.complete(); | ||
| 282 | + } catch (Exception e) { | ||
| 283 | + log.error("流式响应结束时发生错误", e); | ||
| 284 | + } | ||
| 285 | + }); | ||
| 286 | + | ||
| 287 | + tokenStream.onError(error -> { | ||
| 288 | + log.error("生成答案失败", error); | ||
| 289 | + logRecord.setAnswer("生成答案失败: " + error.getMessage()).setAnswerType(4); | ||
| 290 | + airagLogService.save(logRecord); | ||
| 291 | + emitter.completeWithError(error); | ||
| 292 | + }); | ||
| 293 | + | ||
| 294 | + tokenStream.start(); | ||
| 295 | + } | ||
| 296 | + | ||
| 297 | + /** | ||
| 298 | + * 创建结束标志,发送结束token | ||
| 299 | + * @param metadataJson 元数据 | ||
| 300 | + * @param similarity 相似度 | ||
| 301 | + */ | ||
| 302 | + private Map<String, String> createEndData(String metadataJson, String similarity) throws IOException { | ||
| 303 | + Map<String, String> endData = new HashMap<>(); | ||
| 304 | + endData.put("event", "END"); | ||
| 305 | + endData.put("similarity", similarity); | ||
| 306 | + | ||
| 307 | + if (StringUtils.isNotBlank(metadataJson)) { | ||
| 308 | + ObjectMapper objectMapper = new ObjectMapper(); | ||
| 309 | + Map<String, String> metadata = objectMapper.readValue(metadataJson, Map.class); | ||
| 310 | + String docName = metadata.get("docName"); | ||
| 311 | + endData.put("fileName", docName); | ||
| 312 | + | ||
| 313 | + String fileName = generateFilePath(metadataJson); | ||
| 314 | + if (StringUtils.isNotBlank(fileName)) { | ||
| 315 | + endData.put("fileBase64", FileToBase64Util.fileToBase64(uploadPath + fileName)); | ||
| 316 | + } | ||
| 317 | + } | ||
| 318 | + | ||
| 319 | + return endData; | ||
| 320 | + } | ||
| 321 | + | ||
| 322 | + /** | ||
| 323 | + * 异常日志记录 | ||
| 324 | + * @param emitter 流式返回 | ||
| 325 | + * @param logRecord 日志记录 | ||
| 326 | + * @param e 异常处理 | ||
| 327 | + */ | ||
| 328 | + private void handleError(SseEmitter emitter, AiragLog logRecord, Exception e) { | ||
| 329 | + log.error("处理请求时发生异常", e); | ||
| 330 | + logRecord.setAnswer("处理请求时发生异常: " + e.getMessage()).setAnswerType(4); | ||
| 331 | + airagLogService.save(logRecord); | ||
| 332 | + emitter.completeWithError(e); | ||
| 333 | + } | ||
| 334 | + | ||
| 335 | + | ||
| 336 | + | ||
| 337 | + /** | ||
| 338 | + * 清理问题文本中的标点符号 | ||
| 339 | + * @param questionText 原始问题文本 | ||
| 340 | + * @return 清理后的文本(无标点符号) | ||
| 341 | + */ | ||
| 342 | + private String cleanQuestionText(String questionText) { | ||
| 343 | + if (StringUtils.isBlank(questionText)) { | ||
| 344 | + return questionText; | ||
| 345 | + } | ||
| 346 | + | ||
| 347 | + // 定义要移除的标点符号正则表达式 | ||
| 348 | + Pattern punctuationPattern = Pattern.compile("[,,.。??!!;;、]"); | ||
| 349 | + | ||
| 350 | + // 替换所有匹配的标点符号为空字符串 | ||
| 351 | + return punctuationPattern.matcher(questionText).replaceAll(" "); | ||
| 352 | + } | ||
| 353 | +} |
| 1 | +package org.jeecg.modules.airag.zdyrag.service.impl; | ||
| 2 | + | ||
| 3 | +import dev.langchain4j.data.message.ChatMessage; | ||
| 4 | +import dev.langchain4j.data.message.UserMessage; | ||
| 5 | +import dev.langchain4j.model.openai.OpenAiChatModel; | ||
| 6 | +import org.jeecg.modules.airag.common.handler.IAIChatHandler; | ||
| 7 | +import org.jeecg.modules.airag.zdyrag.service.ProductExtractor; | ||
| 8 | +import org.springframework.beans.factory.annotation.Autowired; | ||
| 9 | +import org.springframework.stereotype.Component; | ||
| 10 | + | ||
| 11 | +import java.util.ArrayList; | ||
| 12 | +import java.util.List; | ||
| 13 | + | ||
| 14 | +/** | ||
| 15 | + * 用于实现产品推荐,该功能暂未实现 | ||
| 16 | + */ | ||
| 17 | + | ||
| 18 | +@Component | ||
| 19 | +public class ProductExtractorImpl implements ProductExtractor { | ||
| 20 | + | ||
| 21 | + @Autowired | ||
| 22 | + IAIChatHandler aiChatHandler; | ||
| 23 | + | ||
| 24 | + @Override | ||
| 25 | + public String extractProduct(String questionText) { | ||
| 26 | + String modelId = "1926875898187878401"; | ||
| 27 | + String prompt = | ||
| 28 | + "请从下列问题中提取涉及的产品名称,返回JSON格式,示例:\n" + | ||
| 29 | + "{ \"products\": [\"产品A\", \"产品B\"] }\n" + | ||
| 30 | + "如果没有产品,请返回:{ \"products\": [] }\n\n" + | ||
| 31 | + "问题:" + questionText; | ||
| 32 | + List<ChatMessage> messages = new ArrayList<>(); | ||
| 33 | + messages.add(new UserMessage("user", prompt)); | ||
| 34 | + return aiChatHandler.completions(modelId,messages); | ||
| 35 | + } | ||
| 36 | +} |
| @@ -162,17 +162,20 @@ spring: | @@ -162,17 +162,20 @@ spring: | ||
| 162 | password: 1234 | 162 | password: 1234 |
| 163 | driver-class-name: com.mysql.cj.jdbc.Driver | 163 | driver-class-name: com.mysql.cj.jdbc.Driver |
| 164 | # 多数据源配置 | 164 | # 多数据源配置 |
| 165 | -# pgvector: | ||
| 166 | -# jdbc-url: jdbc:postgresql://192.168.100.103:5432/postgres | ||
| 167 | -# username: postgres | ||
| 168 | -# password: postgres | ||
| 169 | -# driver-class-name: org.postgresql.Driver | 165 | + pgvector: |
| 166 | + url: jdbc:postgresql://192.168.100.104:5432/postgres | ||
| 167 | + username: postgres | ||
| 168 | + password: postgres | ||
| 169 | + driver-class-name: org.postgresql.Driver | ||
| 170 | #redis 配置 | 170 | #redis 配置 |
| 171 | redis: | 171 | redis: |
| 172 | database: 0 | 172 | database: 0 |
| 173 | host: 127.0.0.1 | 173 | host: 127.0.0.1 |
| 174 | port: 6379 | 174 | port: 6379 |
| 175 | password: | 175 | password: |
| 176 | +mybatis: | ||
| 177 | + type-handlers-package: org.jeecg.modules.airag.app.handler | ||
| 178 | + | ||
| 176 | #mybatis plus 设置 | 179 | #mybatis plus 设置 |
| 177 | mybatis-plus: | 180 | mybatis-plus: |
| 178 | mapper-locations: classpath*:org/jeecg/**/xml/*Mapper.xml | 181 | mapper-locations: classpath*:org/jeecg/**/xml/*Mapper.xml |
-
请 注册 或 登录 后发表评论