3 Commits

Author SHA1 Message Date
v-zhangjc9
c2af2d6365 feat(chat): 尝试在对话中加入知识库 2025-06-03 20:23:52 +08:00
v-zhangjc9
536c4e9cab feat(bin): 增加forest发布脚本 2025-06-03 20:22:29 +08:00
v-zhangjc9
c9a1ea2be5 feat(web): 用markdown显示思考过程 2025-06-03 16:12:23 +08:00
18 changed files with 325 additions and 66 deletions

13
bin/build-forest.js Normal file
View File

@@ -0,0 +1,13 @@
import {cd, path} from 'zx'
import {trim} from "licia";
import {run_deploy_batch, run_deploy_root} from "./library.js";
// 切换目录
cd(trim(path.dirname(import.meta.dirname)))
// 执行流程
try {
await run_deploy_root()
await run_deploy_batch(['service-common', 'service-dependencies', 'service-configuration', 'service-forest'])
} catch (e) {
console.error(e)
}

View File

@@ -19,6 +19,11 @@
<dependency> <dependency>
<groupId>org.springframework.ai</groupId> <groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId> <artifactId>spring-ai-starter-model-openai</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-deepseek</artifactId>
</dependency> </dependency>
</dependencies> </dependencies>

View File

@@ -1,9 +1,12 @@
package com.lanyuanxiaoyao.service.ai.chat.controller; package com.lanyuanxiaoyao.service.ai.chat.controller;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.lanyuanxiaoyao.service.ai.chat.entity.MessageVO; import com.lanyuanxiaoyao.service.ai.chat.entity.MessageVO;
import com.lanyuanxiaoyao.service.ai.chat.tools.DatetimeTools; import com.lanyuanxiaoyao.service.ai.chat.tools.DatetimeTools;
import com.lanyuanxiaoyao.service.forest.service.KnowledgeService;
import java.io.IOException; import java.io.IOException;
import java.util.Optional;
import org.eclipse.collections.api.list.ImmutableList; import org.eclipse.collections.api.list.ImmutableList;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -11,10 +14,14 @@ import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.messages.AssistantMessage; import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.Message; import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.model.Generation;
import org.springframework.ai.deepseek.DeepSeekAssistantMessage;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
@@ -28,26 +35,42 @@ import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
@RequestMapping("chat") @RequestMapping("chat")
public class ChatController { public class ChatController {
private static final Logger logger = LoggerFactory.getLogger(ChatController.class); private static final Logger logger = LoggerFactory.getLogger(ChatController.class);
private static final String ROLE_ASSISTANT = "assistant";
private static final String ROLE_USER = "user";
private final ChatClient chatClient; private final ChatClient chatClient;
private final KnowledgeService knowledgeService;
public ChatController(ChatClient.Builder builder) { @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
public ChatController(ChatClient.Builder builder, KnowledgeService knowledgeService) {
this.chatClient = builder.build(); this.chatClient = builder.build();
this.knowledgeService = knowledgeService;
} }
private ChatClient.ChatClientRequestSpec buildRequest(ImmutableList<MessageVO> messages) { private ChatClient.ChatClientRequestSpec buildRequest(Long knowledgeId, ImmutableList<MessageVO> messages) {
return chatClient.prompt() var systemPromptBuilder = new StringBuilder();
.system(""" systemPromptBuilder.append("""
你是一名专业的AI运维助手负责“Hudi数据同步服务平台”的运维工作 你是一名专业的AI运维助手负责“Hudi数据同步服务平台”的运维工作
你将会友好地帮助用户解答关于该平台运维工作的问题,你会尽可能通过各种方式获取知识和数据来解答; 你将会友好地帮助用户解答关于该平台运维工作的问题,你会尽可能通过各种方式获取知识和数据来解答;
对于无法通过已有知识回答的问题,你会提示用户你无法解答该问题,而不是虚构不存在的数据或答案; 对于无法通过已有知识回答的问题,你会提示用户你无法解答该问题,而不是虚构不存在的数据或答案;
对于与该平台无关的问题,你会委婉地拒绝用户,并提示无法回答; 对于与该平台无关的问题,你会委婉地拒绝用户,并提示无法回答;
你将始终在中文语境下进行对话。 你将始终在中文语境下进行对话。
""") """);
if (ObjectUtil.isNotNull(knowledgeId)) {
var vo = messages.select(message -> StrUtil.equals(message.getRole(), "user")).getLastOptional().orElseThrow();
var documents = knowledgeService.query(knowledgeId, vo.getContent());
logger.info("Knowledge id:{}, content:{}", knowledgeId, vo.getContent());
if (ObjectUtil.isNotEmpty(documents)) {
systemPromptBuilder.append("以下是与用户问题有关的外部知识,优先利用该知识回答用户的提问:\n");
systemPromptBuilder.append(documents.makeString("\n"));
}
}
return chatClient.prompt()
.system(systemPromptBuilder.toString())
.tools(new DatetimeTools()) .tools(new DatetimeTools())
.messages( .messages(
messages messages
.collect(message -> StrUtil.equals(message.getRole(), "assistant") .collect(message -> StrUtil.equals(message.getRole(), ROLE_ASSISTANT)
? new AssistantMessage(message.getContent()) ? new AssistantMessage(message.getContent())
: new UserMessage(message.getContent())) : new UserMessage(message.getContent()))
.collect(message -> (Message) message) .collect(message -> (Message) message)
@@ -57,23 +80,29 @@ public class ChatController {
@PostMapping("sync") @PostMapping("sync")
@ResponseBody @ResponseBody
public String chatSync(@RequestBody ImmutableList<MessageVO> messages) { public MessageVO chatSync(
String content = buildRequest(messages) @RequestParam(value = "knowledge_id", required = false) Long knowledgeId,
@RequestBody ImmutableList<MessageVO> messages
) {
ChatResponse response = buildRequest(knowledgeId, messages)
.call() .call()
.content(); .chatResponse();
return StrUtil.trimToEmpty(content); return toMessage(response);
} }
@PostMapping("async") @PostMapping("async")
public SseEmitter chatAsync(@RequestBody ImmutableList<MessageVO> messages) { public SseEmitter chatAsync(
@RequestParam(value = "knowledge_id", required = false) Long knowledgeId,
@RequestBody ImmutableList<MessageVO> messages
) {
SseEmitter emitter = new SseEmitter(); SseEmitter emitter = new SseEmitter();
buildRequest(messages) buildRequest(knowledgeId, messages)
.stream() .stream()
.content() .chatResponse()
.subscribe( .subscribe(
content -> { response -> {
try { try {
emitter.send(content); emitter.send(toMessage(response));
} catch (IOException e) { } catch (IOException e) {
emitter.completeWithError(e); emitter.completeWithError(e);
throw new RuntimeException(e); throw new RuntimeException(e);
@@ -84,4 +113,18 @@ public class ChatController {
); );
return emitter; return emitter;
} }
private MessageVO toMessage(ChatResponse response) {
AssistantMessage message = Optional.ofNullable(response)
.map(ChatResponse::getResult)
.map(Generation::getOutput)
.orElseThrow(() -> new RuntimeException("ChatResponse is null"));
MessageVO vo = new MessageVO();
vo.setRole(ROLE_ASSISTANT);
vo.setContent(message.getText());
if (message instanceof DeepSeekAssistantMessage deepseekMessage) {
vo.setReason(deepseekMessage.getReasoningContent());
}
return vo;
}
} }

View File

@@ -7,6 +7,7 @@ package com.lanyuanxiaoyao.service.ai.chat.entity;
public class MessageVO { public class MessageVO {
private String role; private String role;
private String content; private String content;
private String reason;
public String getRole() { public String getRole() {
return role; return role;
@@ -24,11 +25,20 @@ public class MessageVO {
this.content = content; this.content = content;
} }
public String getReason() {
return reason;
}
public void setReason(String reason) {
this.reason = reason;
}
@Override @Override
public String toString() { public String toString() {
return "MessageVO{" + return "MessageVO{" +
"role='" + role + '\'' + "role='" + role + '\'' +
", content='" + content + '\'' + ", content='" + content + '\'' +
", reason='" + reason + '\'' +
'}'; '}';
} }
} }

View File

@@ -4,8 +4,8 @@ spring:
profiles: profiles:
include: random-port,common,discovery,metrics,forest include: random-port,common,discovery,metrics,forest
ai: ai:
openai: deepseek:
base-url: http://132.121.206.65:10086 base-url: http://132.121.206.65:10086/v1
api-key: ENC(K+Hff9QGC+fcyi510VIDd9CaeK/IN5WBJ9rlkUsHEdDgIidW+stHHJlsK0lLPUXXREha+ToQZqqDXJrqSE+GUKCXklFhelD8bRHFXBIeP/ZzT2cxhzgKUXgjw3S0Qw2R) api-key: ENC(K+Hff9QGC+fcyi510VIDd9CaeK/IN5WBJ9rlkUsHEdDgIidW+stHHJlsK0lLPUXXREha+ToQZqqDXJrqSE+GUKCXklFhelD8bRHFXBIeP/ZzT2cxhzgKUXgjw3S0Qw2R)
chat: chat:
options: options:

View File

@@ -4,7 +4,6 @@ import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.openai.OpenAiChatModel; import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.openai.OpenAiChatOptions; import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.ai.openai.api.OpenAiApi; import org.springframework.ai.openai.api.OpenAiApi;
import reactor.core.Disposable;
/** /**
* @author lanyuanxiaoyao * @author lanyuanxiaoyao

View File

@@ -8,10 +8,12 @@ import com.lanyuanxiaoyao.service.ai.knowledge.service.EmbeddingService;
import com.lanyuanxiaoyao.service.ai.knowledge.service.KnowledgeService; import com.lanyuanxiaoyao.service.ai.knowledge.service.KnowledgeService;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import org.eclipse.collections.api.factory.Lists; import org.eclipse.collections.api.factory.Lists;
import org.eclipse.collections.api.list.ImmutableList;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
@@ -105,4 +107,14 @@ public class KnowledgeController {
throw new IllegalArgumentException("Unsupported type: " + type); throw new IllegalArgumentException("Unsupported type: " + type);
} }
} }
@PostMapping("query")
public ImmutableList<String> query(
@RequestParam("id") Long id,
@RequestParam(value = "limit", defaultValue = "5") Integer limit,
@RequestParam(value = "threshold", defaultValue = "0.6") Double threshold,
@RequestBody String text
) throws ExecutionException, InterruptedException {
return knowledgeService.query(id, text, limit, threshold);
}
} }

View File

@@ -6,6 +6,7 @@ package com.lanyuanxiaoyao.service.ai.knowledge.entity.vo;
*/ */
public class KnowledgeVO { public class KnowledgeVO {
private String id; private String id;
private String vectorSourceId;
private String name; private String name;
private String strategy; private String strategy;
private Long size; private Long size;
@@ -23,6 +24,14 @@ public class KnowledgeVO {
this.id = id; this.id = id;
} }
public String getVectorSourceId() {
return vectorSourceId;
}
public void setVectorSourceId(String vectorSourceId) {
this.vectorSourceId = vectorSourceId;
}
public String getName() { public String getName() {
return name; return name;
} }
@@ -91,6 +100,7 @@ public class KnowledgeVO {
public String toString() { public String toString() {
return "KnowledgeVO{" + return "KnowledgeVO{" +
"id='" + id + '\'' + "id='" + id + '\'' +
", vectorSourceId='" + vectorSourceId + '\'' +
", name='" + name + '\'' + ", name='" + name + '\'' +
", strategy='" + strategy + '\'' + ", strategy='" + strategy + '\'' +
", size=" + size + ", size=" + size +

View File

@@ -109,7 +109,6 @@ public class GroupService {
Long.class, Long.class,
groupId groupId
); );
logger.info("Delete {} {}", vectorSourceId, groupId);
client.deleteAsync( client.deleteAsync(
String.valueOf(vectorSourceId), String.valueOf(vectorSourceId),
Points.Filter.newBuilder() Points.Filter.newBuilder()

View File

@@ -9,14 +9,18 @@ import com.lanyuanxiaoyao.service.ai.knowledge.entity.vo.KnowledgeVO;
import com.lanyuanxiaoyao.service.common.Constants; import com.lanyuanxiaoyao.service.common.Constants;
import io.qdrant.client.QdrantClient; import io.qdrant.client.QdrantClient;
import io.qdrant.client.grpc.Collections; import io.qdrant.client.grpc.Collections;
import java.util.List;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.eclipse.collections.api.factory.Lists; import org.eclipse.collections.api.factory.Lists;
import org.eclipse.collections.api.list.ImmutableList; import org.eclipse.collections.api.list.ImmutableList;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.ai.document.Document;
import org.springframework.ai.embedding.EmbeddingModel; import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore; import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.ai.vectorstore.qdrant.QdrantVectorStore;
import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -41,13 +45,13 @@ public class KnowledgeService {
return knowledge; return knowledge;
}; };
private final JdbcTemplate template; private final JdbcTemplate template;
private final EmbeddingModel embeddingModel; private final EmbeddingModel model;
private final QdrantClient client; private final QdrantClient client;
private final GroupService groupService; private final GroupService groupService;
public KnowledgeService(JdbcTemplate template, EmbeddingModel embeddingModel, VectorStore vectorStore, GroupService groupService) { public KnowledgeService(JdbcTemplate template, EmbeddingModel model, VectorStore vectorStore, GroupService groupService) {
this.template = template; this.template = template;
this.embeddingModel = embeddingModel; this.model = model;
this.client = (QdrantClient) vectorStore.getNativeClient().orElseThrow(); this.client = (QdrantClient) vectorStore.getNativeClient().orElseThrow();
this.groupService = groupService; this.groupService = groupService;
} }
@@ -93,7 +97,7 @@ public class KnowledgeService {
String.valueOf(vectorSourceId), String.valueOf(vectorSourceId),
Collections.VectorParams.newBuilder() Collections.VectorParams.newBuilder()
.setDistance(Collections.Distance.valueOf(strategy)) .setDistance(Collections.Distance.valueOf(strategy))
.setSize(embeddingModel.dimensions()) .setSize(model.dimensions())
.build() .build()
).get(); ).get();
} }
@@ -123,6 +127,7 @@ public class KnowledgeService {
Collections.CollectionInfo info = client.getCollectionInfoAsync(String.valueOf(knowledge.getVectorSourceId())).get(); Collections.CollectionInfo info = client.getCollectionInfoAsync(String.valueOf(knowledge.getVectorSourceId())).get();
KnowledgeVO vo = new KnowledgeVO(); KnowledgeVO vo = new KnowledgeVO();
vo.setId(String.valueOf(knowledge.getId())); vo.setId(String.valueOf(knowledge.getId()));
vo.setVectorSourceId(String.valueOf(knowledge.getVectorSourceId()));
vo.setName(knowledge.getName()); vo.setName(knowledge.getName());
vo.setPoints(info.getPointsCount()); vo.setPoints(info.getPointsCount());
vo.setSegments(info.getSegmentsCount()); vo.setSegments(info.getSegmentsCount());
@@ -156,4 +161,29 @@ public class KnowledgeService {
groupService.removeByKnowledgeId(knowledge.getId()); groupService.removeByKnowledgeId(knowledge.getId());
client.deleteCollectionAsync(String.valueOf(knowledge.getVectorSourceId())).get(); client.deleteCollectionAsync(String.valueOf(knowledge.getVectorSourceId())).get();
} }
public ImmutableList<String> query(
Long id,
String text,
Integer limit,
Double threshold) throws ExecutionException, InterruptedException {
Knowledge knowledge = get(id);
Boolean exists = client.collectionExistsAsync(String.valueOf(knowledge.getVectorSourceId())).get();
if (!exists) {
throw new RuntimeException(StrUtil.format("{} not exists", id));
}
VectorStore vs = QdrantVectorStore.builder(client, model)
.collectionName(String.valueOf(knowledge.getVectorSourceId()))
.initializeSchema(false)
.build();
List<Document> documents = vs.similaritySearch(
SearchRequest.builder()
.query(text)
.topK(limit)
.similarityThreshold(threshold)
.build()
);
return Lists.immutable.ofAll(documents)
.collect(Document::getText);
}
} }

View File

@@ -215,7 +215,9 @@ public class EmbeddingNodes {
.build(); .build();
for (Document document : context.getDocuments()) { for (Document document : context.getDocuments()) {
Map<String, Object> metadata = document.getMetadata(); Map<String, Object> metadata = document.getMetadata();
if (StrUtil.isNotBlank(context.getFileFormat()))
metadata.put("filename", context.getFileFormat()); metadata.put("filename", context.getFileFormat());
if (StrUtil.isNotBlank(context.getFile()))
metadata.put("filepath", context.getFile()); metadata.put("filepath", context.getFile());
metadata.put("group_id", String.valueOf(context.getGroupId())); metadata.put("group_id", String.valueOf(context.getGroupId()));
metadata.put("vector_source_id", String.valueOf(context.getVectorSourceId())); metadata.put("vector_source_id", String.valueOf(context.getVectorSourceId()));

View File

@@ -2,7 +2,6 @@ package com.lanyuanxiaoyao.service.ai.knowledge;
import io.qdrant.client.QdrantClient; import io.qdrant.client.QdrantClient;
import io.qdrant.client.QdrantGrpcClient; import io.qdrant.client.QdrantGrpcClient;
import io.qdrant.client.grpc.Collections;
import java.net.http.HttpClient; import java.net.http.HttpClient;
import java.util.List; import java.util.List;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
@@ -12,6 +11,7 @@ import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.openai.OpenAiEmbeddingModel; import org.springframework.ai.openai.OpenAiEmbeddingModel;
import org.springframework.ai.openai.OpenAiEmbeddingOptions; import org.springframework.ai.openai.OpenAiEmbeddingOptions;
import org.springframework.ai.openai.api.OpenAiApi; import org.springframework.ai.openai.api.OpenAiApi;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore; import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.ai.vectorstore.qdrant.QdrantVectorStore; import org.springframework.ai.vectorstore.qdrant.QdrantVectorStore;
import org.springframework.http.client.JdkClientHttpRequestFactory; import org.springframework.http.client.JdkClientHttpRequestFactory;
@@ -41,19 +41,30 @@ public class TestEmbedding {
.build() .build()
); );
QdrantClient client = new QdrantClient( QdrantClient client = new QdrantClient(
QdrantGrpcClient.newBuilder("localhost", 6334, false).build() QdrantGrpcClient.newBuilder("132.121.206.65", 29463, false)
.withApiKey("jdHyKdp9qxNqCK3c")
.build()
); );
client.createCollectionAsync( /* client.createCollectionAsync(
"test", "1927659521614176256",
Collections.VectorParams.newBuilder() Collections.VectorParams.newBuilder()
.setDistance(Collections.Distance.Cosine) .setDistance(Collections.Distance.Cosine)
.setSize(1024) .setSize(1024)
.build() .build()
).get(); ).get(); */
VectorStore store = QdrantVectorStore.builder(client, model) VectorStore store = QdrantVectorStore.builder(client, model)
.initializeSchema(true) .initializeSchema(false)
.collectionName("test") .collectionName("1929833382422159361")
.build(); .build();
store.add(List.of(new Document("hello world"))); List<Document> documents = store.similaritySearch(
SearchRequest.builder()
.query("Hudi是什么")
.topK(5)
.similarityThreshold(0.2)
.build()
);
for (Document document : documents) {
System.out.println(document.getText());
}
} }
} }

View File

@@ -0,0 +1,22 @@
package com.lanyuanxiaoyao.service.forest.service;
import com.dtflys.forest.annotation.BaseRequest;
import com.dtflys.forest.annotation.Body;
import com.dtflys.forest.annotation.Post;
import com.dtflys.forest.annotation.Query;
import org.eclipse.collections.api.list.ImmutableList;
/**
* 队列查询
*
* @author lanyuanxiaoyao
* @date 2023-05-07
*/
@BaseRequest(baseURL = "http://service-ai-knowledge/knowledge")
public interface KnowledgeService {
@Post(value = "/query", contentType = "plain/text")
ImmutableList<String> query(@Query("id") Long id, @Body String text);
@Post(value = "/query", contentType = "plain/text")
ImmutableList<String> query(@Query("id") Long id, @Query("limit") Integer limit, @Query("threshold") Double threshold, @Body String text);
}

View File

@@ -15,6 +15,7 @@
"@echofly/fetch-event-source": "^3.0.2", "@echofly/fetch-event-source": "^3.0.2",
"@fortawesome/fontawesome-free": "^6.7.2", "@fortawesome/fontawesome-free": "^6.7.2",
"@tinyflow-ai/react": "^0.1.10", "@tinyflow-ai/react": "^0.1.10",
"ahooks": "^3.8.5",
"amis": "^6.12.0", "amis": "^6.12.0",
"antd": "^5.25.3", "antd": "^5.25.3",
"axios": "^1.9.0", "axios": "^1.9.0",

View File

@@ -26,6 +26,9 @@ importers:
'@tinyflow-ai/react': '@tinyflow-ai/react':
specifier: ^0.1.10 specifier: ^0.1.10
version: 0.1.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(svelte@5.28.2) version: 0.1.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(svelte@5.28.2)
ahooks:
specifier: ^3.8.5
version: 3.8.5(react@18.3.1)
amis: amis:
specifier: ^6.12.0 specifier: ^6.12.0
version: 6.12.0(@types/react@18.3.23)(amis-core@6.12.0(@types/react@18.3.23)(amis-formula@6.12.0)(react-dom@18.3.1(react@18.3.1))(react-is@18.3.1)(react@18.3.1))(amis-ui@6.12.0(@types/react@18.3.23)(amis-core@6.12.0(@types/react@18.3.23)(amis-formula@6.12.0)(react-dom@18.3.1(react@18.3.1))(react-is@18.3.1)(react@18.3.1))(amis-formula@6.12.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(office-viewer@0.3.14(echarts@5.5.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) version: 6.12.0(@types/react@18.3.23)(amis-core@6.12.0(@types/react@18.3.23)(amis-formula@6.12.0)(react-dom@18.3.1(react@18.3.1))(react-is@18.3.1)(react@18.3.1))(amis-ui@6.12.0(@types/react@18.3.23)(amis-core@6.12.0(@types/react@18.3.23)(amis-formula@6.12.0)(react-dom@18.3.1(react@18.3.1))(react-is@18.3.1)(react@18.3.1))(amis-formula@6.12.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(office-viewer@0.3.14(echarts@5.5.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -1077,6 +1080,12 @@ packages:
resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==}
engines: {node: '>= 6.0.0'} engines: {node: '>= 6.0.0'}
ahooks@3.8.5:
resolution: {integrity: sha512-Y+MLoJpBXVdjsnnBjE5rOSPkQ4DK+8i5aPDzLJdIOsCpo/fiAeXcBY1Y7oWgtOK0TpOz0gFa/XcyO1UGdoqLcw==}
engines: {node: '>=8.0.0'}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
ajv@6.12.6: ajv@6.12.6:
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
@@ -1863,6 +1872,9 @@ packages:
inherits@2.0.4: inherits@2.0.4:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
intersection-observer@0.12.2:
resolution: {integrity: sha512-7m1vEcPCxXYI8HqnL8CKI6siDyD+eIWSwgB3DZA+ZTogxk9I4CDnj4wilt9x/+/QbHI4YG5YZNmC6458/e9Ktg==}
invariant@2.2.4: invariant@2.2.4:
resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==}
@@ -1898,6 +1910,10 @@ packages:
isexe@2.0.0: isexe@2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
js-cookie@3.0.5:
resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==}
engines: {node: '>=14'}
js-tokens@4.0.0: js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
@@ -2659,6 +2675,9 @@ packages:
peerDependencies: peerDependencies:
react: '>= 16.8' react: '>= 16.8'
react-fast-compare@3.2.2:
resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==}
react-hook-form@7.39.0: react-hook-form@7.39.0:
resolution: {integrity: sha512-rekW5NMBVG0nslE2choOKThy0zxLWQeoew87yTLwb3C9F91LaXwu/dhfFL/D3hdnSMnrTG60gVN/v6rvCrSOTw==} resolution: {integrity: sha512-rekW5NMBVG0nslE2choOKThy0zxLWQeoew87yTLwb3C9F91LaXwu/dhfFL/D3hdnSMnrTG60gVN/v6rvCrSOTw==}
engines: {node: '>=12.22.0'} engines: {node: '>=12.22.0'}
@@ -2825,6 +2844,10 @@ packages:
scheduler@0.23.2: scheduler@0.23.2:
resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==}
screenfull@5.2.0:
resolution: {integrity: sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA==}
engines: {node: '>=0.10.0'}
scroll-into-view-if-needed@3.1.0: scroll-into-view-if-needed@3.1.0:
resolution: {integrity: sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==} resolution: {integrity: sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==}
@@ -4262,6 +4285,19 @@ snapshots:
- supports-color - supports-color
optional: true optional: true
ahooks@3.8.5(react@18.3.1):
dependencies:
'@babel/runtime': 7.27.3
dayjs: 1.11.13
intersection-observer: 0.12.2
js-cookie: 3.0.5
lodash: 4.17.21
react: 18.3.1
react-fast-compare: 3.2.2
resize-observer-polyfill: 1.5.1
screenfull: 5.2.0
tslib: 2.8.1
ajv@6.12.6: ajv@6.12.6:
dependencies: dependencies:
fast-deep-equal: 3.1.3 fast-deep-equal: 3.1.3
@@ -5299,6 +5335,8 @@ snapshots:
inherits@2.0.4: {} inherits@2.0.4: {}
intersection-observer@0.12.2: {}
invariant@2.2.4: invariant@2.2.4:
dependencies: dependencies:
loose-envify: 1.4.0 loose-envify: 1.4.0
@@ -5326,6 +5364,8 @@ snapshots:
isexe@2.0.0: {} isexe@2.0.0: {}
js-cookie@3.0.5: {}
js-tokens@4.0.0: {} js-tokens@4.0.0: {}
js-yaml@4.1.0: js-yaml@4.1.0:
@@ -5828,7 +5868,7 @@ snapshots:
dependencies: dependencies:
'@babel/runtime': 7.27.3 '@babel/runtime': 7.27.3
'@rc-component/mini-decimal': 1.1.0 '@rc-component/mini-decimal': 1.1.0
classnames: 2.3.2 classnames: 2.5.1
rc-util: 5.44.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) rc-util: 5.44.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react: 18.3.1 react: 18.3.1
react-dom: 18.3.1(react@18.3.1) react-dom: 18.3.1(react@18.3.1)
@@ -5925,7 +5965,7 @@ snapshots:
rc-progress@3.4.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1): rc-progress@3.4.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies: dependencies:
'@babel/runtime': 7.27.3 '@babel/runtime': 7.27.3
classnames: 2.3.2 classnames: 2.5.1
rc-util: 5.44.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) rc-util: 5.44.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react: 18.3.1 react: 18.3.1
react-dom: 18.3.1(react@18.3.1) react-dom: 18.3.1(react@18.3.1)
@@ -6146,6 +6186,8 @@ snapshots:
prop-types: 15.8.1 prop-types: 15.8.1
react: 18.3.1 react: 18.3.1
react-fast-compare@3.2.2: {}
react-hook-form@7.39.0(react@18.3.1): react-hook-form@7.39.0(react@18.3.1):
dependencies: dependencies:
react: 18.3.1 react: 18.3.1
@@ -6362,6 +6404,8 @@ snapshots:
dependencies: dependencies:
loose-envify: 1.4.0 loose-envify: 1.4.0
screenfull@5.2.0: {}
scroll-into-view-if-needed@3.1.0: scroll-into-view-if-needed@3.1.0:
dependencies: dependencies:
compute-scroll-into-view: 3.1.1 compute-scroll-into-view: 3.1.1

View File

@@ -1,9 +1,11 @@
import {ClearOutlined, FileOutlined, UserOutlined} from '@ant-design/icons' import {ClearOutlined, FileOutlined, UserOutlined} from '@ant-design/icons'
import {Bubble, Sender, useXAgent, useXChat, Welcome} from '@ant-design/x' import {Bubble, Sender, useXAgent, useXChat, Welcome} from '@ant-design/x'
import {fetchEventSource} from '@echofly/fetch-event-source' import {fetchEventSource} from '@echofly/fetch-event-source'
import {Button, Divider, Flex, Popover, Radio, Switch, Tooltip, Typography} from 'antd' import {useMount} from 'ahooks'
import {Button, Collapse, Divider, Flex, Popover, Radio, Switch, Tooltip, Typography} from 'antd'
import {isEqual, isStrBlank, trim} from 'licia'
import markdownIt from 'markdown-it' import markdownIt from 'markdown-it'
import {useRef, useState} from 'react' import {useMemo, useRef, useState} from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import {commonInfo} from '../../util/amis.tsx' import {commonInfo} from '../../util/amis.tsx'
@@ -29,10 +31,9 @@ const ConversationDiv = styled.div`
think { think {
color: gray; color: gray;
display: block; display: block;
border-left: 3px solid; border-left: 3px solid lightgray;
padding-left: 5px; padding-left: 10px;
margin-bottom: 10px; margin-bottom: 10px;
white-space: pre-line;
} }
} }
@@ -43,23 +44,38 @@ const ConversationDiv = styled.div`
} }
` `
type ChatMessage = { role: string, content?: string, reason?: string }
function Conversation() { function Conversation() {
const abortController = useRef<AbortController | null>(null) const abortController = useRef<AbortController | null>(null)
const [input, setInput] = useState<string>('') const [input, setInput] = useState<string>('')
const [think, setThink] = useState<boolean>(true) const [think, setThink] = useState<boolean>(true)
const [knowledge, setKnowledge] = useState<string>('0')
const [knowledgeList, setKnowledgeList] = useState<{ id: string, name: string }[]>([])
const requestUrl = useMemo(() => {
let url = `${commonInfo.baseAiChatUrl}/chat/async`
if (!isEqual('0', knowledge)) {
url = `${url}?knowledge_id=${knowledge}`
}
return url
}, [knowledge])
const [agent] = useXAgent<{ role: string, content: string }>({ useMount(async () => {
let response = await fetch(`${commonInfo.baseAiKnowledgeUrl}/knowledge/list`, {
headers: commonInfo.authorizationHeaders,
})
let items = (await response.json()).data.items
setKnowledgeList(items.map((item: { id: string, name: string }) => ({id: item.id, name: item.name})))
})
const [agent] = useXAgent<ChatMessage>({
request: async (info, callbacks) => { request: async (info, callbacks) => {
await fetchEventSource(`${commonInfo.baseAiChatUrl}/chat/async`, { await fetchEventSource(requestUrl, {
method: 'POST', method: 'POST',
headers: { headers: commonInfo.authorizationHeaders,
'Authorization': 'Basic QXhoRWJzY3dzSkRiWU1IMjpjWXhnM2I0UHRXb1ZENVNqRmF5V3h0blNWc2p6UnNnNA==',
'Content-Type': 'application/json',
},
body: JSON.stringify(info.messages), body: JSON.stringify(info.messages),
signal: abortController.current?.signal, signal: abortController.current?.signal,
onmessage: ev => { onmessage: ev => {
console.log(ev)
callbacks.onUpdate({ callbacks.onUpdate({
id: ev.id, id: ev.id,
event: 'delta', event: 'delta',
@@ -73,17 +89,22 @@ function Conversation() {
const {onRequest, messages, setMessages} = useXChat({ const {onRequest, messages, setMessages} = useXChat({
agent, agent,
transformMessage: ({originMessage, chunk}) => { transformMessage: ({originMessage, chunk}) => {
let text = '' let content = '', reason = ''
try { try {
if (chunk?.data) { if (chunk?.data) {
text = chunk.data let map = JSON.parse(chunk.data)
if (map['content'])
content = map['content']
if (map['reason'])
reason = map['reason']
} }
} catch (error) { } catch (error) {
console.error(error) console.error(error)
} }
return { return {
content: (originMessage?.content || '') + text,
role: 'assistant', role: 'assistant',
content: (originMessage?.content || '') + content,
reason: (originMessage?.reason || '') + reason,
} }
}, },
resolveAbortController: controller => { resolveAbortController: controller => {
@@ -105,11 +126,34 @@ function Conversation() {
background: 'transparent', background: 'transparent',
}, },
}, },
messageRender: content => { messageRender: item => {
let content = '', reason = ''
if (!isStrBlank(item['reason'])) {
reason = `${trim(md.render(item['reason']))}`
}
content = trim(md.render(item['content']))
return ( return (
<div>
{isStrBlank(reason)
? <span/>
: <Collapse
size="small"
items={[
{
key: 0,
label: '思考链',
children: (
<Typography> <Typography>
<div dangerouslySetInnerHTML={{__html: md.render(content)}}/> <div dangerouslySetInnerHTML={{__html: reason}}/>
</Typography> </Typography>
),
},
]}
/>}
<Typography>
<div dangerouslySetInnerHTML={{__html: content}}/>
</Typography>
</div>
) )
}, },
}, },
@@ -118,12 +162,20 @@ function Conversation() {
avatar: { avatar: {
icon: <UserOutlined/>, icon: <UserOutlined/>,
}, },
messageRender: item => {
return (
<Typography>{trim(item['content'])}</Typography>
)
},
}, },
}} }}
items={messages.map(({id, message}) => ({ items={messages.map(({id, message}) => {
return {
key: id, key: id,
...message, role: message.role,
}))} content: message,
}
})}
/>) />)
: (<div className="conversation-welcome"> : (<div className="conversation-welcome">
<Welcome <Welcome
@@ -170,10 +222,12 @@ function Conversation() {
flexDirection: 'column', flexDirection: 'column',
gap: 10, gap: 10,
}} }}
disabled={agent.isRequesting()}
value={knowledge}
onChange={event => setKnowledge(event.target.value)}
options={[ options={[
{value: 1, label: '测试'}, {value: '0', label: ''},
{value: 2, label: 'Hudi'}, ...knowledgeList.map(k => ({label: k.name, value: k.id})),
{value: 3, label: 'Apache Hudi'},
]} ]}
/>} />}
> >

View File

@@ -146,7 +146,7 @@ const DataDetail: React.FC = () => {
level: 'link', level: 'link',
size: 'sm', size: 'sm',
actionType: 'ajax', actionType: 'ajax',
api: `${commonInfo.baseAiKnowledgeUrl}/group/delete?id=\${id}`, api: `get:${commonInfo.baseAiKnowledgeUrl}/group/delete?id=\${id}`,
confirmText: '确认删除', confirmText: '确认删除',
confirmTitle: '删除', confirmTitle: '删除',
}, },

View File

@@ -13,6 +13,10 @@ export const commonInfo = {
baseAiChatUrl: 'http://132.126.207.130:35690/hudi_services/ai_chat', baseAiChatUrl: 'http://132.126.207.130:35690/hudi_services/ai_chat',
baseAiKnowledgeUrl: 'http://132.126.207.130:35690/hudi_services/ai_knowledge', baseAiKnowledgeUrl: 'http://132.126.207.130:35690/hudi_services/ai_knowledge',
// baseUrl: '/hudi_services/service_web', // baseUrl: '/hudi_services/service_web',
authorizationHeaders: {
'Authorization': 'Basic QXhoRWJzY3dzSkRiWU1IMjpjWXhnM2I0UHRXb1ZENVNqRmF5V3h0blNWc2p6UnNnNA==',
'Content-Type': 'application/json',
},
clusters: { clusters: {
// hudi同步运行集群和yarn队列名称 // hudi同步运行集群和yarn队列名称
sync: { sync: {