diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 3cea655c09a004ef8fcbd89534ac4334c0b08902..8470d759e504410655a1bd046f90cb66f6deb9db 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -20,7 +20,7 @@ services: retries: 10 backend: - image: crpi-35xwxmfks2ouwnqn.cn-shanghai.personal.cr.aliyuncs.com/sparkx/sparkx-backend:1.1.1 + image: crpi-35xwxmfks2ouwnqn.cn-shanghai.personal.cr.aliyuncs.com/sparkx/sparkx-backend:1.1.2 container_name: sparkx-backend depends_on: postgres: @@ -33,7 +33,7 @@ services: - "8989:8989" frontend: - image: crpi-35xwxmfks2ouwnqn.cn-shanghai.personal.cr.aliyuncs.com/sparkx/sparkx-frontend:1.1.1 + image: crpi-35xwxmfks2ouwnqn.cn-shanghai.personal.cr.aliyuncs.com/sparkx/sparkx-frontend:1.1.2 container_name: sparkx-frontend ports: - "8189:80" diff --git a/docker/sql/sparkx.sql b/docker/sql/sparkx.sql index 1360072ac2089299c76bc8dd1182e677eb98a91f..f7cd7c1f9507359cd8c85f73d3d5b1ea4261b027 100644 --- a/docker/sql/sparkx.sql +++ b/docker/sql/sparkx.sql @@ -404,6 +404,8 @@ INSERT INTO "public"."models" VALUES ('6f4f2e21-df9d-419d-a65b-ed272b6cf5c8', ' INSERT INTO "public"."models" VALUES ('6f6f2e81-eg9b-429d-a57b-ed371g6cf8c9', 'Ollama', 'ollama', 1, '[{"field":"apiKey","value":"SparkX"}]', '[{"field":"temperature","name":"温度","range":[0.01,1],"value":0.95},{"field":"url","name":"模型地址","value": "http://localhost:11434"}]', 1, '', '/icons/ollama.png', '', NULL, '2025-07-29 12:05:50'); INSERT INTO "public"."models" VALUES ('6f6f2e88-eg9c-429e-a58b-ed381g6cf9c9', 'Ollama', 'ollama', 2, '[{"field":"apiKey","value":"SparkX"}]', '[{"field":"temperature","name":"温度","range":[0.01,1],"value":0.95},{"field":"url","name":"模型地址","value": "http://localhost:11434"}]', 1, '', '/icons/ollama.png', '', NULL, '2025-07-29 12:05:50'); INSERT INTO "public"."models" VALUES ('6f6f2e89-eg9f-439e-a68b-ed382g7cf9e9', 'Ollama', 'ollama', 3, '[{"field":"apiKey","value":"SparkX"}]', '[{"field":"temperature","name":"温度","range":[0.01,1],"value":0.95},{"field":"url","name":"模型地址","value": "http://localhost:11434"}]', 1, '', '/icons/ollama.png', '', NULL, '2025-07-29 12:05:50'); +INSERT INTO "public"."models" VALUES ('5f6f2e81-ef9b-419d-a56b-ed271g6cf6c9', 'DeepSeek', 'deepseek', 1, '[{"field":"apiKey","value":""}]', '[{"field":"temperature","name":"温度","range":[0.01,1],"value":0.95},{"field":"url","name":"模型地址","value": "https://api.deepseek.com/v1"}]', 1, 'deepseek-chat,deepseek-reasoner', '/icons/deepseek.png', 'deepseek-chat', NULL, '2025-07-29 12:05:50'); +INSERT INTO "public"."models" VALUES ('4f6f2e82-ef9c-439d-a57b-ed271g6cf8c9', '模力方舟', 'gitee', 1, '[{"field":"apiKey","value":""}]', '[{"field":"temperature","name":"温度","range":[0.01,1],"value":0.95},{"field":"url","name":"模型地址","value":"https://ai.gitee.com/v1"}]', 1, 'kimi-k2-instruct,internlm3-8b-instruct,Qwen3-235B-A22B', '/icons/gitee.png', '', NULL, '2025-07-30 16:26:00'); CREATE TABLE "public"."application_workflow" ( "id" INT8 NOT NULL GENERATED ALWAYS AS IDENTITY (INCREMENT 1 MINVALUE 1 MAXVALUE 9223372036854775807 START 1 CACHE 1), diff --git a/server/pom.xml b/server/pom.xml index 54f1b474362a3e81f1cf97fdcb6884f8a850ae36..085d1845f57320dfca7fb39998a7f905b2063add 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -5,7 +5,7 @@ cn.sparkshop.ai cn.sparkshop - 1.1.1 + 1.1.2 pom SparkX 基于大语言模型和 RAG 的知识库问答系统。开箱即用、模型中立、灵活编排,支持快速嵌入到第三方业务系统。 @@ -22,7 +22,7 @@ true 17 5.8.26 - 1.1.1 + 1.1.2 1.18.24 3.4.2 3.5.10.1 @@ -31,13 +31,13 @@ 3.5.10.1 9.0.0.CR1 2.18.2 - 1.1.0 + 1.2.0 1.1.0-beta7 0.64.8 portable-1.8.6 3.1.0 2.9.0 - 1.1.0-rc1 + 1.2.0 1.21.3 @@ -126,6 +126,13 @@ ${langchain4j.version} + + dev.langchain4j + langchain4j-core + ${langchain4j.version} + compile + + dev.langchain4j langchain4j-document-parser-apache-pdfbox diff --git a/server/sparkx-common/pom.xml b/server/sparkx-common/pom.xml index a9e425dfbaea266446cc5412d3713df8d1cde65a..134dde65a98c5b2e965b5f9ddb655a3c7b71b88c 100644 --- a/server/sparkx-common/pom.xml +++ b/server/sparkx-common/pom.xml @@ -5,11 +5,11 @@ cn.sparkshop.ai cn.sparkshop - 1.1.1 + 1.1.2 sparkx-common - 1.1.1 + 1.1.2 jar diff --git a/server/sparkx-service/pom.xml b/server/sparkx-service/pom.xml index 356454f800a12d955d0e54a66a9d53d3816fb5dd..8d83081abdcb479a4ea44af3bacafa5c90855194 100644 --- a/server/sparkx-service/pom.xml +++ b/server/sparkx-service/pom.xml @@ -5,11 +5,11 @@ cn.sparkshop.ai cn.sparkshop - 1.1.1 + 1.1.2 sparkx-service - 1.1.1 + 1.1.2 jar @@ -126,6 +126,11 @@ com.squareup.retrofit2 converter-jackson + + + dev.langchain4j + langchain4j-core + diff --git a/server/sparkx-service/src/main/java/sparkx/service/helper/SseEmitterHelper.java b/server/sparkx-service/src/main/java/sparkx/service/helper/SseEmitterHelper.java index bf74be2240084e310fac0d48ee91cf9ffccd1005..2fdb9c0e00f1efe02da50f006bfa2d382023bf2c 100644 --- a/server/sparkx-service/src/main/java/sparkx/service/helper/SseEmitterHelper.java +++ b/server/sparkx-service/src/main/java/sparkx/service/helper/SseEmitterHelper.java @@ -12,6 +12,7 @@ package sparkx.service.helper; import cn.hutool.core.date.TimeInterval; import cn.hutool.json.JSONObject; import cn.hutool.json.JSONUtil; +import dev.langchain4j.model.chat.response.PartialThinking; import dev.langchain4j.service.TokenStream; import dev.langchain4j.service.tool.ToolExecution; import lombok.extern.slf4j.Slf4j; @@ -25,6 +26,7 @@ import sparkx.service.extend.workflow.SendEndCallback; import java.io.IOException; import java.util.*; +import java.util.concurrent.atomic.AtomicBoolean; @Slf4j @Component @@ -45,11 +47,14 @@ public class SseEmitterHelper { // 消息开始 sendStartSse(emitter); + AtomicBoolean hasReasoningContent = new AtomicBoolean(false); // 是否有思考过程 + AtomicBoolean hasSendStart = new AtomicBoolean(false); // 是有发送了思考开始标识 + AtomicBoolean hasSendEnd = new AtomicBoolean(false); // 是否发送了思考结束标识 final TimeInterval timer = new TimeInterval(); tokenStream + // 整理并转换召回的片段数据,返回前端 .onRetrieved((retrievedList) -> { - // 整理并转换召回的片段数据,返回前端 List> retiredMapList = new ArrayList<>(); retrievedList.forEach(item -> { Map map = new HashMap<>(); @@ -64,35 +69,35 @@ public class SseEmitterHelper { // 召回知识库片段 sendMetaSse(emitter, retiredMapList); }) - .onToolExecuted((ToolExecution toolExecution) -> sendToolSse(emitter, toolExecution.request().name())) - .onPartialResponse((content) -> { - // 加空格配合前端的fetchEventSource进行解析, - // 见https://github.com/Azure/fetch-event-source/blob/45ac3cfffd30b05b79fbf95c21e67d4ef59aa56a/src/parse.ts#L129-L133 + // 思考过程 + .onPartialThinking((PartialThinking reasoningContent) -> { try { + hasReasoningContent.set(true); - String[] lines = content.split("[\\n]", -1); - if (lines.length > 1) { - - emitter.send(Tool.buildSendData(runtimeId, nodeId, lines[0])); - - for (int i = 1; i < lines.length; i++) { - /** - * 当响应结果的content中包含有多行文本时, - * 前端的fetch-event-source框架的BUG会将包含有换行符的那一行内容替换为空字符串, - * 故需要先将换行符与后面的内容拆分并转成,前端碰到换行标志时转成换行符处理 - */ - emitter.send(Tool.buildSendData(runtimeId, nodeId, "-_-_wrap_-_-")); - emitter.send(Tool.buildSendData(runtimeId, nodeId, lines[i])); - } - } else { + if (!hasSendStart.get()) { + emitter.send(Tool.buildSendData(runtimeId, nodeId, "")); + hasSendStart.set(true); + } - emitter.send(Tool.buildSendData(runtimeId, nodeId, content)); + sendSseData(reasoningContent.text(), emitter, runtimeId, nodeId); + } catch (Exception e) { + emitter.completeWithError(e); + } + }) + // 工具调用 + .onToolExecuted((ToolExecution toolExecution) -> { + sendToolSse(emitter, toolExecution.request().name()); + }) + .onPartialResponse((content) -> { + try { + if (hasReasoningContent.get() && !hasSendEnd.get()) { + emitter.send(Tool.buildSendData(runtimeId, nodeId, "")); + hasSendEnd.set(true); } - } catch (IOException e) { - //log.error("拆解AI返回信息失败:", e); - sendErrorSse(emitter, e.getMessage()); - //emitter.complete(); + sendSseData(content, emitter, runtimeId, nodeId); + } catch (Exception e) { + emitter.completeWithError(e); } }) .onCompleteResponse((response) -> { @@ -131,37 +136,46 @@ public class SseEmitterHelper { public void asyncSend2Client(TokenStream tokenStream, SseEmitter emitter, long runtimeId, String nodeId, boolean needSend, SendEndCallback sendEndCallback) { + AtomicBoolean hasReasoningContent = new AtomicBoolean(false); // 是否有思考过程 + AtomicBoolean hasSendStart = new AtomicBoolean(false); // 是有发送了思考开始标识 + AtomicBoolean hasSendEnd = new AtomicBoolean(false); // 是否发送了思考结束标识 + tokenStream - .onPartialResponse((content) -> { - // 加空格配合前端的fetchEventSource进行解析, - // 见https://github.com/Azure/fetch-event-source/blob/45ac3cfffd30b05b79fbf95c21e67d4ef59aa56a/src/parse.ts#L129-L133 + // 思考过程 + .onPartialThinking((PartialThinking reasoningContent) -> { if (needSend) { try { + hasReasoningContent.set(true); - String[] lines = content.split("[\\n]", -1); - if (lines.length > 1) { - - emitter.send(Tool.buildSendData(runtimeId, nodeId, lines[0])); - for (int i = 1; i < lines.length; i++) { - /** - * 当响应结果的content中包含有多行文本时, - * 前端的fetch-event-source框架的BUG会将包含有换行符的那一行内容替换为空字符串, - * 故需要先将换行符与后面的内容拆分并转成,前端碰到换行标志时转成换行符处理 - */ - emitter.send(Tool.buildSendData(runtimeId, nodeId, "-_-_wrap_-_-")); - emitter.send(Tool.buildSendData(runtimeId, nodeId, lines[i])); - } - } else { - emitter.send(Tool.buildSendData(runtimeId, nodeId, content)); + if (!hasSendStart.get()) { + emitter.send(Tool.buildSendData(runtimeId, nodeId, "")); + hasSendStart.set(true); } - } catch (IOException e) { - //log.error("拆解AI返回信息失败:", e); - sendErrorSse(emitter, e.getMessage()); + sendSseData(reasoningContent.text(), emitter, runtimeId, nodeId); + } catch (Exception e) { + emitter.completeWithError(e); + } + } + }) + // 工具调用 + .onToolExecuted((ToolExecution toolExecution) -> { + sendToolSse(emitter, toolExecution.request().name()); + }) + .onPartialResponse((content) -> { + if (needSend) { + try { + if (hasReasoningContent.get() && !hasSendEnd.get()) { + emitter.send(Tool.buildSendData(runtimeId, nodeId, "")); + hasSendStart.set(true); + } + + sendSseData(content, emitter, runtimeId, nodeId); + } catch (Exception e) { + emitter.completeWithError(e); } } }) - .onToolExecuted((ToolExecution toolExecution) -> sendToolSse(emitter, toolExecution.request().name())) .onCompleteResponse((response) -> { // 输入的token int inputTokenCount = response.tokenUsage().totalTokenCount(); @@ -262,4 +276,42 @@ public class SseEmitterHelper { sseEmitter.completeWithError(e); } } + + /** + * 发送模型返回数据 + * @param content String + * @param emitter SseEmitter + * @param runtimeId Long + * @param nodeId String + */ + private void sendSseData(String content, SseEmitter emitter, Long runtimeId, String nodeId) { + // 加空格配合前端的fetchEventSource进行解析, + // 见https://github.com/Azure/fetch-event-source/blob/45ac3cfffd30b05b79fbf95c21e67d4ef59aa56a/src/parse.ts#L129-L133 + try { + + String[] lines = content.split("[\\n]", -1); + if (lines.length > 1) { + + emitter.send(Tool.buildSendData(runtimeId, nodeId, lines[0])); + + for (int i = 1; i < lines.length; i++) { + /** + * 当响应结果的content中包含有多行文本时, + * 前端的fetch-event-source框架的BUG会将包含有换行符的那一行内容替换为空字符串, + * 故需要先将换行符与后面的内容拆分并转成,前端碰到换行标志时转成换行符处理 + */ + emitter.send(Tool.buildSendData(runtimeId, nodeId, "-_-_wrap_-_-")); + emitter.send(Tool.buildSendData(runtimeId, nodeId, lines[i])); + } + } else { + + emitter.send(Tool.buildSendData(runtimeId, nodeId, content)); + } + + } catch (IOException e) { + //log.error("拆解AI返回信息失败:", e); + sendErrorSse(emitter, e.getMessage()); + //emitter.complete(); + } + } } \ No newline at end of file diff --git a/server/sparkx-service/src/main/java/sparkx/service/helper/StreamChatModelBuildHelper.java b/server/sparkx-service/src/main/java/sparkx/service/helper/StreamChatModelBuildHelper.java index 2e010a816516a45ca78abf51dd7e4899b04b4a8c..c57ffdbc45f2f1864544607b9be752c72abe5088 100644 --- a/server/sparkx-service/src/main/java/sparkx/service/helper/StreamChatModelBuildHelper.java +++ b/server/sparkx-service/src/main/java/sparkx/service/helper/StreamChatModelBuildHelper.java @@ -106,6 +106,7 @@ public class StreamChatModelBuildHelper { return OpenAiStreamingChatModel.builder() .baseUrl(url) .apiKey(key) + .returnThinking(true) .modelName(applicationInfo.getModelName()) .listeners(List.of(applicationHelper.observability())) .build(); diff --git a/server/sparkx-web/pom.xml b/server/sparkx-web/pom.xml index 718a5ee8d1ef7e5f76c03c4b32bf73229a818a65..032350e07dce676ddf26db5b0673b7d2293301e4 100644 --- a/server/sparkx-web/pom.xml +++ b/server/sparkx-web/pom.xml @@ -5,11 +5,11 @@ cn.sparkshop.ai cn.sparkshop - 1.1.1 + 1.1.2 sparkx-web - 1.1.1 + 1.1.2 jar diff --git a/server/sparkx-web/src/main/resources/application.yml b/server/sparkx-web/src/main/resources/application.yml index 56e4ce6298d7b020ee47accc9d91d780048df6f3..c1700154714f0963e84187b0d2a24a73f6aa42df 100644 --- a/server/sparkx-web/src/main/resources/application.yml +++ b/server/sparkx-web/src/main/resources/application.yml @@ -4,7 +4,7 @@ server: spring: datasource: driver-class-name: org.postgresql.Driver - url: jdbc:postgresql://127.0.0.1:5432/sparkx + url: jdbc:postgresql://127.0.0.1:6432/sparkx username: sparkx password: 123123 jpa: diff --git a/server/sparkx-web/src/main/resources/license b/server/sparkx-web/src/main/resources/license index 201a4ff6920d8c199e468fa62a8a196f860ebccf..e9514cce05bb9f91b1dca2058674ea210406cf10 100644 --- a/server/sparkx-web/src/main/resources/license +++ b/server/sparkx-web/src/main/resources/license @@ -2,5 +2,5 @@ APP_NUM:999999 DATASET_NUM:999999 USER_NUM:999999 COMPANY_NAME:\u793e\u533a\u7248 -VERSION:1.1.1 +VERSION:1.1.2 LICENSE_ID:2b96f5aa \ No newline at end of file diff --git a/server/sparkx-web/src/main/resources/static/icons/gitee.png b/server/sparkx-web/src/main/resources/static/icons/gitee.png index eda812da52caff6a1825af690780c9d2f5b15298..d6135149805d34b87eb7d2b085be7f48a25e6721 100644 Binary files a/server/sparkx-web/src/main/resources/static/icons/gitee.png and b/server/sparkx-web/src/main/resources/static/icons/gitee.png differ diff --git a/web/src/components/tools/index.vue b/web/src/components/tools/index.vue index 815bf31751697f54087642f6996b5960bf6cf327..03c82f425fcb848398309b50c295ff6b5e011817 100644 --- a/web/src/components/tools/index.vue +++ b/web/src/components/tools/index.vue @@ -115,4 +115,4 @@ export default { left: 10px; color: #E6A23C; } - + \ No newline at end of file