diff --git a/erupt-ai-web/src/App.vue b/erupt-ai-web/src/App.vue index e889abb86..73ff20a8f 100644 --- a/erupt-ai-web/src/App.vue +++ b/erupt-ai-web/src/App.vue @@ -92,6 +92,14 @@ const fetchMessages = (chatId: number, toBottom: boolean, after?: () => void) => bubbles.value.scrollTop = bubbles.value.scrollHeight; // 滚动到最新消息 }, 100); } + }, err => { + messages.value.push({ + content: err.message || err.data || String(err), + senderType: 'MODEL', + id: 0, + createTime: "", + loading: false + }) }).finally(() => { loadingMoreMessages.value = false; // 加载完成 }); diff --git a/erupt-ai/pom.xml b/erupt-ai/pom.xml index fec8f8eaf..faf8efd8b 100644 --- a/erupt-ai/pom.xml +++ b/erupt-ai/pom.xml @@ -14,6 +14,10 @@ ../pom.xml + + 1.11.0 + + ${erupt.groupId} @@ -37,9 +41,29 @@ true - com.squareup.okhttp3 - okhttp - 4.12.0 + dev.langchain4j + langchain4j-open-ai + ${langchain4j.version} + + + dev.langchain4j + langchain4j-anthropic + ${langchain4j.version} + + + dev.langchain4j + langchain4j-google-ai-gemini + ${langchain4j.version} + + + dev.langchain4j + langchain4j-ollama + ${langchain4j.version} + + + dev.langchain4j + langchain4j + ${langchain4j.version} org.freemarker diff --git a/erupt-ai/src/main/java/xyz/erupt/ai/annotation/AiParam.java b/erupt-ai/src/main/java/xyz/erupt/ai/annotation/AiToolbox.java similarity index 52% rename from erupt-ai/src/main/java/xyz/erupt/ai/annotation/AiParam.java rename to erupt-ai/src/main/java/xyz/erupt/ai/annotation/AiToolbox.java index 40a144f73..f9cee4683 100644 --- a/erupt-ai/src/main/java/xyz/erupt/ai/annotation/AiParam.java +++ b/erupt-ai/src/main/java/xyz/erupt/ai/annotation/AiToolbox.java @@ -3,13 +3,11 @@ import java.lang.annotation.*; @Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.FIELD) +@Target(ElementType.TYPE) @Documented @Inherited -public @interface AiParam { +public @interface AiToolbox { - String description(); - - boolean required() default true; + String value() default ""; } diff --git a/erupt-ai/src/main/java/xyz/erupt/ai/call/AiFunctionCall.java b/erupt-ai/src/main/java/xyz/erupt/ai/call/AiFunctionCall.java deleted file mode 100644 index 7e3b89882..000000000 --- a/erupt-ai/src/main/java/xyz/erupt/ai/call/AiFunctionCall.java +++ /dev/null @@ -1,21 +0,0 @@ -package xyz.erupt.ai.call; - -/** - * @author YuePeng - * date 2025/3/14 23:30 - */ -public interface AiFunctionCall { - - default boolean mcpCall() { - return true; - } - - default String name() { - return this.getClass().getSimpleName(); - } - - String description(); - - String call(String userPrompt); - -} diff --git a/erupt-ai/src/main/java/xyz/erupt/ai/call/AiFunctionManager.java b/erupt-ai/src/main/java/xyz/erupt/ai/call/AiFunctionManager.java deleted file mode 100644 index 552eda8c6..000000000 --- a/erupt-ai/src/main/java/xyz/erupt/ai/call/AiFunctionManager.java +++ /dev/null @@ -1,134 +0,0 @@ -package xyz.erupt.ai.call; - -import com.google.gson.reflect.TypeToken; -import lombok.Getter; -import lombok.SneakyThrows; -import lombok.extern.slf4j.Slf4j; -import org.springframework.boot.ApplicationArguments; -import org.springframework.boot.ApplicationRunner; -import org.springframework.core.annotation.Order; -import org.springframework.core.type.filter.AssignableTypeFilter; -import org.springframework.core.type.filter.TypeFilter; -import org.springframework.stereotype.Component; -import xyz.erupt.ai.annotation.AiParam; -import xyz.erupt.ai.constants.ResponseFormat; -import xyz.erupt.ai.core.LlmCore; -import xyz.erupt.ai.core.LlmRequest; -import xyz.erupt.ai.model.LLM; -import xyz.erupt.ai.pojo.ChatCompletionMessage; -import xyz.erupt.ai.util.MarkDownUtil; -import xyz.erupt.core.config.GsonFactory; -import xyz.erupt.core.exception.EruptWebApiRuntimeException; -import xyz.erupt.core.service.EruptApplication; -import xyz.erupt.core.util.EruptSpringUtil; - -import java.lang.reflect.Field; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; - -/** - * @author YuePeng - * date 2025/3/14 23:42 - */ -@Component -@Order(100) -@Slf4j -public class AiFunctionManager implements ApplicationRunner { - - @Getter - private static final Map aiFunctions = new HashMap<>(); - - @Override - public void run(ApplicationArguments args) { - EruptSpringUtil.scannerPackage(EruptApplication.getScanPackage(), - new TypeFilter[]{new AssignableTypeFilter(AiFunctionCall.class)}, clazz -> { - AiFunctionCall functionCall = (AiFunctionCall) EruptSpringUtil.getBean(clazz); - aiFunctions.put(functionCall.name(), functionCall); - }); - } - - public String getFunctionCallPrompt() { - StringBuilder sb = new StringBuilder(""" - Below is a mapping of available Function Calls. - - Please decide whether a function is clearly required after understanding the user's question. - - Rules: - 1. Call a function IF AND ONLY IF the user's intent clearly and directly matches the function description, - and calling the function will significantly improve the accuracy of the response. - 2. If a function is triggered, STRICTLY output ONLY the function name. - Do NOT output explanations, symbols, punctuation, or any extra text. - 3. If no function is required, IGNORE this entire instruction block - and respond to the user normally. - - Available functions: - """); - for (Map.Entry entry : aiFunctions.entrySet()) { - sb.append("- ") - .append(entry.getKey()) - .append(": ") - .append(entry.getValue().description()) - .append("\n"); - } - - return sb.toString(); - } - - public boolean exist(String key) { - return aiFunctions.containsKey(key); - } - - @SneakyThrows - public String call(String key, LLM llm, String userMessage, List userContext) { - AiFunctionCall aiFunctionCall = aiFunctions.get(key); - Map params = new HashMap<>(); - for (Field field : aiFunctionCall.getClass().getDeclaredFields()) { - Optional.ofNullable(field.getAnnotation(AiParam.class)).ifPresent(it -> { - params.put(field.getName(), field); - }); - } - if (params.isEmpty()) { - return aiFunctionCall.call(userMessage); - } else { - Map promptTemplateMap = getStringParamPromptTemplateMap(params); - StringBuilder prompt = new StringBuilder(); - prompt.append(userMessage).append("\n\n"); - prompt.append("根据上面的内容,自动识别并填充下面JSON的val字段,此JSON中的每个value都是具体的生成要求,将不同key的识别结果放到对应val字段内\n"); - prompt.append("请严格按照以下JSON格式返回,不要返回其他任何多余的内容或解释,请确保只返回纯JSON:\n\n"); - prompt.append(GsonFactory.getGson().toJson(promptTemplateMap)); - LlmRequest llmRequest = llm.toLlmRequest(); - llmRequest.setResponseFormat(ResponseFormat.json_object); - String llmRes = LlmCore.getLLM(llm).chat(llm.toLlmRequest(), prompt.toString(), userContext).getMessageStr(); - llmRes = MarkDownUtil.extractCodeBlock(llmRes); - try { - Map res = GsonFactory.getGson().fromJson(llmRes, new TypeToken>() { - }.getType()); - for (Map.Entry entry : res.entrySet()) { - Field field = aiFunctionCall.getClass().getDeclaredField(entry.getKey()); - field.setAccessible(true); - field.set(aiFunctionCall, entry.getValue().getVal()); - field.setAccessible(false); - } - } catch (Exception e) { - throw new EruptWebApiRuntimeException("Function Call param error: " + e.getMessage() + "→ \n\n" + llmRes); - } - return aiFunctionCall.call(prompt.toString()); - } - } - - private static Map getStringParamPromptTemplateMap(Map params) { - Map promptTemplateMap = new HashMap<>(); - for (Map.Entry entry : params.entrySet()) { - AiParam aiFuncParam = entry.getValue().getAnnotation(AiParam.class); - ParamPromptTemplate promptTemplate = new ParamPromptTemplate(); - promptTemplate.setDescription(aiFuncParam.description()); - promptTemplate.setRequired(aiFuncParam.required()); - promptTemplate.setType(entry.getValue().getType().getSimpleName()); - promptTemplateMap.put(entry.getKey(), promptTemplate); - } - return promptTemplateMap; - } - -} diff --git a/erupt-ai/src/main/java/xyz/erupt/ai/call/ParamPromptTemplate.java b/erupt-ai/src/main/java/xyz/erupt/ai/call/ParamPromptTemplate.java deleted file mode 100644 index 2c664da66..000000000 --- a/erupt-ai/src/main/java/xyz/erupt/ai/call/ParamPromptTemplate.java +++ /dev/null @@ -1,22 +0,0 @@ -package xyz.erupt.ai.call; - -import lombok.Getter; -import lombok.Setter; - -/** - * @author YuePeng - * date 2025/4/9 22:31 - */ -@Getter -@Setter -public class ParamPromptTemplate { - - private boolean required; - - private String type; - - private String description; - - private Object val; - -} diff --git a/erupt-ai/src/main/java/xyz/erupt/ai/call/impl/EruptDataQuery.java b/erupt-ai/src/main/java/xyz/erupt/ai/call/impl/EruptDataQuery.java deleted file mode 100644 index 870179927..000000000 --- a/erupt-ai/src/main/java/xyz/erupt/ai/call/impl/EruptDataQuery.java +++ /dev/null @@ -1,39 +0,0 @@ -package xyz.erupt.ai.call.impl; - -import jakarta.annotation.Resource; -import org.springframework.context.annotation.Scope; -import org.springframework.stereotype.Component; -import xyz.erupt.ai.annotation.AiParam; -import xyz.erupt.ai.call.AiFunctionCall; -import xyz.erupt.core.config.GsonFactory; -import xyz.erupt.core.service.EruptCoreService; -import xyz.erupt.core.view.EruptModel; -import xyz.erupt.jpa.dao.EruptDao; - -import java.util.List; - -/** - * @author YuePeng - * date 2025/3/14 23:25 - */ -@Component -@Scope("prototype") -public class EruptDataQuery implements AiFunctionCall { - - @AiParam(description = "HQL (Hibernate Query Language)") - private String hql; - - @Resource - private EruptDao eruptDao; - - @Override - public String description() { - return "Query erupt model data"; - } - - @Override - public String call(String prompt) { - List result = eruptDao.getEntityManager().createQuery(hql).getResultList(); - return GsonFactory.getGson().toJson(result); - } -} diff --git a/erupt-ai/src/main/java/xyz/erupt/ai/call/impl/EruptList.java b/erupt-ai/src/main/java/xyz/erupt/ai/call/impl/EruptList.java deleted file mode 100644 index a234ed677..000000000 --- a/erupt-ai/src/main/java/xyz/erupt/ai/call/impl/EruptList.java +++ /dev/null @@ -1,31 +0,0 @@ -package xyz.erupt.ai.call.impl; - -import org.springframework.context.annotation.Scope; -import org.springframework.stereotype.Component; -import xyz.erupt.ai.call.AiFunctionCall; -import xyz.erupt.core.module.EruptModuleInvoke; -import xyz.erupt.core.service.EruptCoreService; -import xyz.erupt.core.view.EruptModel; - -/** - * @author YuePeng - * date 2025/3/14 23:25 - */ -@Component -@Scope("prototype") -public class EruptList implements AiFunctionCall { - - @Override - public String description() { - return "Erupt model list"; - } - - @Override - public String call(String prompt) { - StringBuilder sb = new StringBuilder(); - for (EruptModel erupt : EruptCoreService.getErupts()) { - sb.append(erupt.getEruptName()).append(": ").append(erupt.getErupt().name()); - } - return sb.toString(); - } -} diff --git a/erupt-ai/src/main/java/xyz/erupt/ai/call/impl/EruptModuleInfo.java b/erupt-ai/src/main/java/xyz/erupt/ai/call/impl/EruptModuleInfo.java deleted file mode 100644 index 5687700c6..000000000 --- a/erupt-ai/src/main/java/xyz/erupt/ai/call/impl/EruptModuleInfo.java +++ /dev/null @@ -1,29 +0,0 @@ -package xyz.erupt.ai.call.impl; - -import org.springframework.context.annotation.Scope; -import org.springframework.stereotype.Component; -import xyz.erupt.ai.call.AiFunctionCall; -import xyz.erupt.core.module.EruptModuleInvoke; - -/** - * @author YuePeng - * date 2025/3/14 23:25 - */ -@Component -@Scope("prototype") -public class EruptModuleInfo implements AiFunctionCall { - - @Override - public String description() { - return "Erupt Module list"; - } - - @Override - public String call(String prompt) { - StringBuilder sb = new StringBuilder(); - EruptModuleInvoke.invoke(it -> { - sb.append(it.info().getName()).append(": ").append(it.info().getDescription()).append("\n"); - }); - return sb.toString(); - } -} diff --git a/erupt-ai/src/main/java/xyz/erupt/ai/call/impl/EruptSchema.java b/erupt-ai/src/main/java/xyz/erupt/ai/call/impl/EruptSchema.java deleted file mode 100644 index 4ee9b1ab9..000000000 --- a/erupt-ai/src/main/java/xyz/erupt/ai/call/impl/EruptSchema.java +++ /dev/null @@ -1,33 +0,0 @@ -package xyz.erupt.ai.call.impl; - -import org.springframework.context.annotation.Scope; -import org.springframework.stereotype.Component; -import xyz.erupt.ai.annotation.AiParam; -import xyz.erupt.ai.call.AiFunctionCall; -import xyz.erupt.core.config.GsonFactory; -import xyz.erupt.core.module.EruptModuleInvoke; -import xyz.erupt.core.service.EruptCoreService; -import xyz.erupt.core.view.EruptModel; - -/** - * @author YuePeng - * date 2025/3/14 23:25 - */ -@Component -@Scope("prototype") -public class EruptSchema implements AiFunctionCall { - - @AiParam(description = "Erupt Name") - private String eruptName; - - @Override - public String description() { - return "Erupt model schema"; - } - - @Override - public String call(String prompt) { - EruptModel erupt = EruptCoreService.getEruptView(eruptName); - return GsonFactory.getGson().toJson(erupt); - } -} diff --git a/erupt-ai/src/main/java/xyz/erupt/ai/call/impl/EruptUserInfo.java b/erupt-ai/src/main/java/xyz/erupt/ai/call/impl/EruptUserInfo.java deleted file mode 100644 index 39b76ce60..000000000 --- a/erupt-ai/src/main/java/xyz/erupt/ai/call/impl/EruptUserInfo.java +++ /dev/null @@ -1,33 +0,0 @@ -package xyz.erupt.ai.call.impl; - -import jakarta.annotation.Resource; -import org.springframework.context.annotation.Scope; -import org.springframework.stereotype.Component; -import xyz.erupt.ai.call.AiFunctionCall; -import xyz.erupt.core.config.GsonFactory; -import xyz.erupt.core.context.MetaContext; -import xyz.erupt.jpa.dao.EruptDao; -import xyz.erupt.upms.model.EruptUser; - -/** - * @author YuePeng - * date 2025/3/14 23:25 - */ -@Component -@Scope("prototype") -public class EruptUserInfo implements AiFunctionCall { - - @Resource - private EruptDao eruptDao; - - @Override - public String description() { - return "Ask the current system logged-in user"; - } - - @Override - public String call(String prompt) { - return GsonFactory.getGson().toJson(eruptDao.find(EruptUser.class, MetaContext.getUser().getUid())); - } - -} diff --git a/erupt-ai/src/main/java/xyz/erupt/ai/config/AiProp.java b/erupt-ai/src/main/java/xyz/erupt/ai/config/AiProp.java index f4f457261..075fe330b 100644 --- a/erupt-ai/src/main/java/xyz/erupt/ai/config/AiProp.java +++ b/erupt-ai/src/main/java/xyz/erupt/ai/config/AiProp.java @@ -21,4 +21,8 @@ public class AiProp { private boolean enableFunctionCall = true; + private int messageChunkSize = 20; + + private int messageDelay = 30; + } diff --git a/erupt-ai/src/main/java/xyz/erupt/ai/constants/MessageRole.java b/erupt-ai/src/main/java/xyz/erupt/ai/constants/MessageRole.java deleted file mode 100644 index f845f6488..000000000 --- a/erupt-ai/src/main/java/xyz/erupt/ai/constants/MessageRole.java +++ /dev/null @@ -1,11 +0,0 @@ -package xyz.erupt.ai.constants; - -/** - * @author YuePeng - * date 2025/2/25 21:51 - */ -public enum MessageRole { - system, - user, - assistant -} diff --git a/erupt-ai/src/main/java/xyz/erupt/ai/controller/ChatController.java b/erupt-ai/src/main/java/xyz/erupt/ai/controller/ChatController.java index cbedaa9d3..b5c15c0de 100644 --- a/erupt-ai/src/main/java/xyz/erupt/ai/controller/ChatController.java +++ b/erupt-ai/src/main/java/xyz/erupt/ai/controller/ChatController.java @@ -8,8 +8,8 @@ import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import xyz.erupt.ai.constants.ChatSenderType; import xyz.erupt.ai.core.LlmCore; -import xyz.erupt.ai.model.Chat; -import xyz.erupt.ai.model.ChatMessage; +import xyz.erupt.ai.model.AiChat; +import xyz.erupt.ai.model.AiChatMessage; import xyz.erupt.ai.model.LLM; import xyz.erupt.ai.model.LLMAgent; import xyz.erupt.ai.service.LLMService; @@ -65,9 +65,9 @@ public SseEmitter send(@RequestParam Long chatId, emitter.complete(); } else { LlmCore llm = LlmCore.getLLM(llmModel.getLlm()); - ChatMessage chatMessage = ChatMessage.create(chatId, llmModel.getLlm(), llmModel.getModel(), ChatSenderType.USER, message, 0L); + AiChatMessage chatMessage = AiChatMessage.create(chatId, llmModel.getLlm(), llmModel.getModel(), ChatSenderType.USER, message, 0); eruptDao.persist(chatMessage); - Chat chat = eruptDao.find(Chat.class, chatId); + AiChat chat = eruptDao.find(AiChat.class, chatId); LLMAgent llmAgent = null; if (null != agentId) { llmAgent = eruptDao.find(LLMAgent.class, agentId); @@ -82,7 +82,7 @@ public SseEmitter send(@RequestParam Long chatId, @PostMapping("/create_chat") @Transactional public R createChat(@RequestParam String title) { - Chat chat = new Chat(); + AiChat chat = new AiChat(); if (title.length() > 100) title = title.substring(0, 100); chat.setTitle(title); chat.setCreatedTime(LocalDateTime.now()); @@ -95,27 +95,27 @@ public R createChat(@RequestParam String title) { @GetMapping("/delete_chat") @Transactional public R deleteChat(@RequestParam Long chatId) { - Chat chat = eruptDao.find(Chat.class, chatId); + AiChat chat = eruptDao.find(AiChat.class, chatId); chat.setDeleted(true); return R.ok(); } @EruptLoginAuth @GetMapping("/chats") - public R> chats() { - return R.ok(eruptDao.lambdaQuery(Chat.class) - .with(Chat::getEruptUser).eq(EruptUserVo::getId, eruptUserService.getCurrentUid()).with() - .orderByDesc(Chat::getCreatedTime) + public R> chats() { + return R.ok(eruptDao.lambdaQuery(AiChat.class) + .with(AiChat::getEruptUser).eq(EruptUserVo::getId, eruptUserService.getCurrentUid()).with() + .orderByDesc(AiChat::getCreatedTime) .list()); } @EruptLoginAuth @GetMapping("/messages") - public R> messages(@RequestParam Long chatId, @RequestParam Integer size, - @RequestParam(defaultValue = "1") Integer index) { - return R.ok(eruptDao.lambdaQuery(ChatMessage.class) - .eq(ChatMessage::getChatId, chatId) - .orderByDesc(ChatMessage::getCreatedAt) + public R> messages(@RequestParam Long chatId, @RequestParam Integer size, + @RequestParam(defaultValue = "1") Integer index) { + return R.ok(eruptDao.lambdaQuery(AiChatMessage.class) + .eq(AiChatMessage::getChatId, chatId) + .orderByDesc(AiChatMessage::getCreatedAt) .offset((index - 1) * size) .limit(size) .list()); diff --git a/erupt-ai/src/main/java/xyz/erupt/ai/controller/McpController.java b/erupt-ai/src/main/java/xyz/erupt/ai/controller/McpController.java index af28890be..f430b3939 100644 --- a/erupt-ai/src/main/java/xyz/erupt/ai/controller/McpController.java +++ b/erupt-ai/src/main/java/xyz/erupt/ai/controller/McpController.java @@ -1,5 +1,8 @@ package xyz.erupt.ai.controller; +import dev.langchain4j.agent.tool.P; +import dev.langchain4j.agent.tool.Tool; +import dev.langchain4j.agent.tool.ToolExecutionRequest; import jakarta.annotation.Resource; import jakarta.servlet.http.HttpServletRequest; import lombok.SneakyThrows; @@ -9,20 +12,19 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; -import xyz.erupt.ai.annotation.AiParam; -import xyz.erupt.ai.call.AiFunctionCall; -import xyz.erupt.ai.call.AiFunctionManager; +import xyz.erupt.ai.tool.AiToolboxManager; import xyz.erupt.ai.config.AiMCPProp; import xyz.erupt.ai.util.McpUtil; import xyz.erupt.ai.vo.mcp.*; +import xyz.erupt.core.config.GsonFactory; import xyz.erupt.core.context.MetaContext; import xyz.erupt.core.util.EruptInformation; -import xyz.erupt.core.util.ReflectUtil; import xyz.erupt.jpa.dao.EruptDao; import xyz.erupt.upms.model.EruptOpenApi; import java.io.IOException; -import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -136,27 +138,29 @@ private McpInfo mcpInfo() { private List mcpTools() { List mcpTools = new ArrayList<>(); - for (Map.Entry entry : AiFunctionManager.getAiFunctions().entrySet()) { - if (entry.getValue().mcpCall()) { + for (Map.Entry entry : AiToolboxManager.getAiMethodMap().entrySet()) { + Method method = entry.getValue(); + Tool tool = method.getAnnotation(Tool.class); + if (null != tool) { McpTool mcpTool = new McpTool(); mcpTools.add(mcpTool); - mcpTool.setName(entry.getValue().name()); - mcpTool.setDescription(entry.getValue().description()); + mcpTool.setName(entry.getKey()); + mcpTool.setDescription(tool.value().length > 0 ? tool.value()[0] : ""); { McpTool.InputSchema inputSchema = new McpTool.InputSchema(); mcpTool.setInputSchema(inputSchema); List required = new ArrayList<>(); - for (Field field : entry.getValue().getClass().getDeclaredFields()) { - AiParam aiParam = field.getDeclaredAnnotation(AiParam.class); - if (null != aiParam) { - if (aiParam.required()) { - required.add(field.getName()); - } - McpTool.SchemaProperties schema = new McpTool.SchemaProperties(); - schema.setType(McpUtil.toMcp(field.getType())); - schema.setDescription(aiParam.description()); - mcpTool.getInputSchema().getProperties().put(field.getName(), schema); + for (Parameter parameter : method.getParameters()) { + P p = parameter.getAnnotation(P.class); + String description = ""; + if (null != p) { + description = p.value(); + required.add(parameter.getName()); } + McpTool.SchemaProperties schema = new McpTool.SchemaProperties(); + schema.setType(McpUtil.toMcp(parameter.getType())); + schema.setDescription(description); + mcpTool.getInputSchema().getProperties().put(parameter.getName(), schema); } mcpTool.getInputSchema().setRequired(required); } @@ -167,16 +171,11 @@ private List mcpTools() { @SneakyThrows private String mcpCall(String code, Map params) { - AiFunctionCall aiFunctionCall = AiFunctionManager.getAiFunctions().get(code); - if (null != params) { - for (Map.Entry entry : params.entrySet()) { - Field field = ReflectUtil.findClassField(aiFunctionCall.getClass(), entry.getKey()); - field.setAccessible(true); - field.set(aiFunctionCall, entry.getValue()); - field.setAccessible(false); - } - } - return aiFunctionCall.call(null); + ToolExecutionRequest request = ToolExecutionRequest.builder() + .name(code) + .arguments(GsonFactory.getGson().toJson(params)) + .build(); + return (String) AiToolboxManager.invoke(request); } } diff --git a/erupt-ai/src/main/java/xyz/erupt/ai/core/LlmConfig.java b/erupt-ai/src/main/java/xyz/erupt/ai/core/LlmConfig.java index 8ecd1e46a..25da7d072 100644 --- a/erupt-ai/src/main/java/xyz/erupt/ai/core/LlmConfig.java +++ b/erupt-ai/src/main/java/xyz/erupt/ai/core/LlmConfig.java @@ -13,9 +13,9 @@ @NoArgsConstructor public class LlmConfig { - private Float top_p; + private Double top_p; - private Float temperature; + private Double temperature; public LlmRequest toLlmRequest() { LlmRequest llmRequest = new LlmRequest(); diff --git a/erupt-ai/src/main/java/xyz/erupt/ai/core/LlmCore.java b/erupt-ai/src/main/java/xyz/erupt/ai/core/LlmCore.java index f3adb3a74..fe08a6ff3 100644 --- a/erupt-ai/src/main/java/xyz/erupt/ai/core/LlmCore.java +++ b/erupt-ai/src/main/java/xyz/erupt/ai/core/LlmCore.java @@ -1,18 +1,30 @@ package xyz.erupt.ai.core; +import dev.langchain4j.agent.tool.ToolSpecification; +import dev.langchain4j.agent.tool.ToolSpecifications; +import dev.langchain4j.data.message.ChatMessage; +import dev.langchain4j.data.message.SystemMessage; +import dev.langchain4j.data.message.UserMessage; +import dev.langchain4j.model.chat.ChatModel; +import dev.langchain4j.model.chat.StreamingChatModel; +import dev.langchain4j.model.chat.request.ChatRequest; +import dev.langchain4j.model.chat.response.ChatResponse; +import dev.langchain4j.model.chat.response.StreamingChatResponseHandler; +import lombok.extern.slf4j.Slf4j; +import xyz.erupt.ai.config.AiProp; import xyz.erupt.ai.model.LLM; -import xyz.erupt.ai.pojo.ChatCompletionMessage; -import xyz.erupt.ai.pojo.ChatCompletionResponse; +import xyz.erupt.ai.tool.AiToolboxManager; import xyz.erupt.annotation.fun.ChoiceFetchHandler; import xyz.erupt.annotation.fun.VLModel; +import xyz.erupt.core.context.MetaContext; +import xyz.erupt.core.util.EruptSpringUtil; -import java.util.Comparator; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.lang.reflect.Method; +import java.util.*; import java.util.function.Consumer; import java.util.stream.Collectors; +@Slf4j public abstract class LlmCore { private static final Map llms = new HashMap<>(); @@ -35,11 +47,52 @@ public static LlmCore getLLM(LLM llm) { public abstract String api(); - public abstract LlmConfig config(); + public LlmConfig config() { + return new LlmConfig(); + } + + public abstract ChatModel buildChatModel(LlmRequest llmRequest, List chatMessages); + + public abstract StreamingChatModel buildStreamingChatModel(LlmRequest llmRequest, List chatMessages, Consumer listener); + + public String chat(LlmRequest llmRequest, List chatMessages) { + ChatModel chatModel = this.buildChatModel(llmRequest, chatMessages); + chatMessages.add(0, SystemMessage.from(EruptSpringUtil.getBean(AiProp.class).getSystemPrompt())); + return chatModel.chat(chatMessages).aiMessage().text(); + } + + public void chatSse(LlmRequest llmRequest, List chatMessages, Consumer listener) { + StreamingChatModel streamingChatModel = this.buildStreamingChatModel(llmRequest, chatMessages, listener); + chatMessages.add(0, SystemMessage.from(EruptSpringUtil.getBean(AiProp.class).getSystemPrompt())); + List specs = new ArrayList<>(); + for (Method method : AiToolboxManager.getAiMethodMap().values()) { + specs.add(ToolSpecifications.toolSpecificationFrom(method)); + } + ChatRequest request = ChatRequest.builder().messages(chatMessages).toolSpecifications(specs).build(); + MetaContext metaContext = MetaContext.get(); + streamingChatModel.chat(request, new StreamingChatResponseHandler() { + @Override + public void onPartialResponse(String partialResponse) { + MetaContext.set(metaContext); + listener.accept(SseListener.builder().currMessage(partialResponse).build()); + } - public abstract ChatCompletionResponse chat(LlmRequest llmRequest, String userPrompt, List assistantPrompt); + @Override + public void onCompleteResponse(ChatResponse chatResponse) { + MetaContext.set(metaContext); + listener.accept(SseListener.builder() + .isFinish(true) + .usage(chatResponse.tokenUsage()) + .aiMessage(chatResponse.aiMessage()).build()); + } - public abstract void chatSse(LlmRequest llmRequest, String userPrompt, List assistantPrompt, Consumer listener); + @Override + public void onError(Throwable e) { + log.error("Failed to get response from server", e); + listener.accept(SseListener.builder().throwable(e).build()); + } + }); + } public static class H implements ChoiceFetchHandler { @Override diff --git a/erupt-ai/src/main/java/xyz/erupt/ai/core/LlmRequest.java b/erupt-ai/src/main/java/xyz/erupt/ai/core/LlmRequest.java index fd80d5c6d..2abce6ec9 100644 --- a/erupt-ai/src/main/java/xyz/erupt/ai/core/LlmRequest.java +++ b/erupt-ai/src/main/java/xyz/erupt/ai/core/LlmRequest.java @@ -18,9 +18,9 @@ public class LlmRequest { private String model; - private Float temperature; + private Double temperature; - private Float top_p; + private Double top_p; private ResponseFormat responseFormat = ResponseFormat.text; diff --git a/erupt-ai/src/main/java/xyz/erupt/ai/core/OpenAi.java b/erupt-ai/src/main/java/xyz/erupt/ai/core/OpenAi.java index 8403ef566..fccb7b8a7 100644 --- a/erupt-ai/src/main/java/xyz/erupt/ai/core/OpenAi.java +++ b/erupt-ai/src/main/java/xyz/erupt/ai/core/OpenAi.java @@ -1,26 +1,15 @@ package xyz.erupt.ai.core; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; +import dev.langchain4j.data.message.ChatMessage; +import dev.langchain4j.model.chat.ChatModel; +import dev.langchain4j.model.chat.StreamingChatModel; +import dev.langchain4j.model.openai.OpenAiChatModel; +import dev.langchain4j.model.openai.OpenAiStreamingChatModel; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; -import okhttp3.*; -import okio.BufferedSource; -import org.apache.commons.lang3.StringUtils; -import org.jetbrains.annotations.NotNull; import org.springframework.stereotype.Component; -import xyz.erupt.ai.constants.MessageRole; -import xyz.erupt.ai.pojo.ChatCompletion; -import xyz.erupt.ai.pojo.ChatCompletionMessage; -import xyz.erupt.ai.pojo.ChatCompletionResponse; -import xyz.erupt.ai.pojo.ChatCompletionStreamResponse; -import xyz.erupt.core.config.GsonFactory; -import xyz.erupt.core.context.MetaContext; -import java.io.IOException; -import java.util.HashMap; import java.util.List; -import java.util.concurrent.TimeUnit; import java.util.function.Consumer; /** @@ -31,18 +20,8 @@ @Slf4j public abstract class OpenAi extends LlmCore { - private final OkHttpClient client = new OkHttpClient().newBuilder() - .connectTimeout(30, TimeUnit.SECONDS) - .readTimeout(5, TimeUnit.MINUTES) // 流式需要很长! - .writeTimeout(30, TimeUnit.SECONDS) - .pingInterval(10, TimeUnit.SECONDS) // 保持连接心跳 - .build(); - ; - - private final Gson gson = new GsonBuilder().create(); - - public String chatApiPath() { - return "/v1/chat/completions"; + public String chatApiPoint() { + return "/v1"; } @Override @@ -51,120 +30,26 @@ public String code() { } @Override - public LlmConfig config() { - return new LlmConfig(); - } - - @Override - public ChatCompletionResponse chat(LlmRequest llmRequest, String userPrompt, List assistantPrompt) { - assistantPrompt.add(new ChatCompletionMessage(MessageRole.user, userPrompt)); - ChatCompletion completion = ChatCompletion.builder().model(llmRequest.getModel()).stream(false).messages(assistantPrompt).build(); - RequestBody body = RequestBody.create( - gson.toJson(completion), - MediaType.parse("application/json; charset=utf-8") - ); - Request request = new Request.Builder() - .url(llmRequest.getUrl() + chatApiPath()) - .post(body) - .addHeader("Authorization", "Bearer " + llmRequest.getApiKey()) + public ChatModel buildChatModel(LlmRequest llmRequest, List chatMessages) { + return OpenAiChatModel.builder() + .baseUrl(llmRequest.getUrl() + chatApiPoint()) + .apiKey(llmRequest.getApiKey()) + .modelName(llmRequest.getModel()) + .topP(llmRequest.getTop_p()) + .temperature(llmRequest.getTemperature()) .build(); - - // Synchronous execution request - try (Response response = client.newCall(request).execute()) { - if (!response.isSuccessful()) { - throw new RuntimeException("Failed to get response from server: " + response.body()); - } - - // Parsing the response body as ChatCompletionResponse - return GsonFactory.getGson().fromJson(null == response.body() ? null : response.body().string(), ChatCompletionResponse.class); - } catch (IOException e) { - throw new RuntimeException("Failed to execute HTTP request", e); - } } @Override @SneakyThrows - public void chatSse(LlmRequest llmRequest, String userPrompt, List assistantPrompt, Consumer listener) { - assistantPrompt.removeIf(it -> StringUtils.isBlank(it.getContent())); - ChatCompletion completion = ChatCompletion.builder().model(llmRequest.getModel()).messages(assistantPrompt).stream(true).build(); - completion.setResponse_format(new HashMap<>() {{ - this.put("type", String.valueOf(llmRequest.getResponseFormat())); - }}); - completion.setTopP(llmRequest.getTop_p()); - completion.setTemperature(llmRequest.getTemperature()); - OkHttpClient client = new OkHttpClient(); - RequestBody body = RequestBody.create( - gson.toJson(completion), - MediaType.parse("application/json; charset=utf-8") - ); - Request request = new Request.Builder() - .url(llmRequest.getUrl() + chatApiPath()) - .post(body) - .addHeader("Accept", "text/event-stream") - .addHeader("Authorization", "Bearer " + llmRequest.getApiKey()) + public StreamingChatModel buildStreamingChatModel(LlmRequest llmRequest, List chatMessages, Consumer listener) { + return OpenAiStreamingChatModel.builder() + .baseUrl(llmRequest.getUrl() + chatApiPoint()) + .apiKey(llmRequest.getApiKey()) + .modelName(llmRequest.getModel()) + .topP(llmRequest.getTop_p()) + .temperature(llmRequest.getTemperature()) .build(); - MetaContext metaContext = MetaContext.get(); - client.newCall(request).enqueue(new Callback() { - @Override - public void onFailure(@NotNull Call call, @NotNull IOException e) { - log.error("Failed to get response from server", e); - SseListener sseListener = new SseListener(); - sseListener.setError(true); - sseListener.setFinish(true); - sseListener.getOutput().append(e.getMessage()); - sseListener.setCurrMessage(e.getMessage()); - listener.accept(sseListener); - } - - @Override - public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException { - MetaContext.set(metaContext); - try (ResponseBody responseBody = response.body()) { - if (responseBody != null) { - BufferedSource source = responseBody.source(); - SseListener sseListener = new SseListener(); - while (!source.exhausted()) { - String line = source.readUtf8Line(); - if (StringUtils.isNotBlank(line)) { - if (!response.isSuccessful()) { - this.onFailure(call, new IOException(response.body().string() + line)); - log.error("Failed to get llm response from server: {}", response.body() + " → " + line); - return; - } else { - try { - if (line.startsWith("data: ")) { - line = line.substring(6); - } - if ("[DONE]".equalsIgnoreCase(line)) { - sseListener.setFinish(true); - listener.accept(sseListener); - } else { - ChatCompletionStreamResponse chatCompletionStreamResponse = GsonFactory.getGson().fromJson(line, ChatCompletionStreamResponse.class); - sseListener.setCurrData(line); - StringBuilder sb = new StringBuilder(); - for (ChatCompletionStreamResponse.Choice choice : chatCompletionStreamResponse.getChoices()) { - if (null != choice.getUsage()) { - sseListener.setUsage(sseListener.getUsage().plus(choice.getUsage())); - } - if (choice.getDelta() != null && choice.getDelta().getContent() != null) { - sseListener.getOutput().append(choice.getDelta().getContent()); - sb.append(choice.getDelta().getContent()); - } - } - sseListener.setCurrMessage(sb.toString()); - listener.accept(sseListener); - } - } catch (Exception e) { - this.onFailure(call, new IOException(e + "→" + line)); - break; - } - } - } - } - } - } - } - }); } } diff --git a/erupt-ai/src/main/java/xyz/erupt/ai/core/SseListener.java b/erupt-ai/src/main/java/xyz/erupt/ai/core/SseListener.java index 34d12cad0..b8b9d8ac3 100644 --- a/erupt-ai/src/main/java/xyz/erupt/ai/core/SseListener.java +++ b/erupt-ai/src/main/java/xyz/erupt/ai/core/SseListener.java @@ -1,8 +1,11 @@ package xyz.erupt.ai.core; +import dev.langchain4j.data.message.AiMessage; +import dev.langchain4j.model.chat.StreamingChatModel; +import dev.langchain4j.model.output.TokenUsage; +import lombok.Builder; import lombok.Getter; import lombok.Setter; -import xyz.erupt.ai.pojo.ChatUsage; /** * @author YuePeng @@ -10,23 +13,20 @@ */ @Getter @Setter +@Builder public class SseListener { - // Full output content - private final StringBuilder output = new StringBuilder(); - - private boolean pending = false; - // Stream output, the content of the current message private String currMessage; - // Stream output, the entire message object - private String currData; + private boolean isFinish = false; // Spending tokens - private ChatUsage usage = new ChatUsage(); + private TokenUsage usage; - private boolean isFinish = false; + private AiMessage aiMessage; + + private StreamingChatModel streamingChatModel; - private boolean error = false; + private Throwable throwable; } diff --git a/erupt-ai/src/main/java/xyz/erupt/ai/llm/Claude.java b/erupt-ai/src/main/java/xyz/erupt/ai/llm/Claude.java index 6bad04f3d..c595de2b9 100644 --- a/erupt-ai/src/main/java/xyz/erupt/ai/llm/Claude.java +++ b/erupt-ai/src/main/java/xyz/erupt/ai/llm/Claude.java @@ -1,15 +1,27 @@ package xyz.erupt.ai.llm; +import dev.langchain4j.data.message.ChatMessage; +import dev.langchain4j.model.anthropic.AnthropicChatModel; +import dev.langchain4j.model.anthropic.AnthropicStreamingChatModel; +import dev.langchain4j.model.chat.ChatModel; +import dev.langchain4j.model.chat.StreamingChatModel; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; -import xyz.erupt.ai.core.LlmConfig; -import xyz.erupt.ai.core.OpenAi; +import xyz.erupt.ai.core.LlmCore; +import xyz.erupt.ai.core.LlmRequest; +import xyz.erupt.ai.core.SseListener; + +import java.util.List; +import java.util.function.Consumer; /** * @author YuePeng * date 2025/2/22 16:37 */ @Component -public class Claude extends OpenAi { +@Slf4j +public class Claude extends LlmCore { @Override public String model() { @@ -21,4 +33,32 @@ public String api() { return "https://api.anthropic.com"; } + @Override + public String code() { + return getClass().getSimpleName(); + } + + @Override + public ChatModel buildChatModel(LlmRequest llmRequest, List chatMessages) { + return AnthropicChatModel.builder() + .baseUrl(llmRequest.getUrl()) + .apiKey(llmRequest.getApiKey()) + .modelName(llmRequest.getModel()) + .topP(llmRequest.getTop_p()) + .temperature(llmRequest.getTemperature()) + .build(); + } + + @Override + @SneakyThrows + public StreamingChatModel buildStreamingChatModel(LlmRequest llmRequest, List chatMessages, Consumer listener) { + return AnthropicStreamingChatModel.builder() + .baseUrl(llmRequest.getUrl()) + .apiKey(llmRequest.getApiKey()) + .modelName(llmRequest.getModel()) + .topP(llmRequest.getTop_p()) + .temperature(llmRequest.getTemperature()) + .build(); + } + } diff --git a/erupt-ai/src/main/java/xyz/erupt/ai/llm/Doubao.java b/erupt-ai/src/main/java/xyz/erupt/ai/llm/Doubao.java new file mode 100644 index 000000000..c96350b9f --- /dev/null +++ b/erupt-ai/src/main/java/xyz/erupt/ai/llm/Doubao.java @@ -0,0 +1,24 @@ +package xyz.erupt.ai.llm; + +import org.springframework.stereotype.Component; +import xyz.erupt.ai.core.OpenAi; + +@Component +public class Doubao extends OpenAi { + + @Override + public String model() { + return "doubao-pro-32k"; + } + + @Override + public String api() { + return "https://ark.cn-beijing.volces.com"; + } + + @Override + public String chatApiPoint() { + return "/api/v3"; + } + +} diff --git a/erupt-ai/src/main/java/xyz/erupt/ai/llm/GLM.java b/erupt-ai/src/main/java/xyz/erupt/ai/llm/GLM.java index 056e0890c..c27a04aff 100644 --- a/erupt-ai/src/main/java/xyz/erupt/ai/llm/GLM.java +++ b/erupt-ai/src/main/java/xyz/erupt/ai/llm/GLM.java @@ -22,8 +22,8 @@ public String api() { } @Override - public String chatApiPath() { - return "/api/paas/v4/chat/completions"; + public String chatApiPoint() { + return "/api/paas/v4"; } } diff --git a/erupt-ai/src/main/java/xyz/erupt/ai/llm/Gemini.java b/erupt-ai/src/main/java/xyz/erupt/ai/llm/Gemini.java index 3b4cf5945..2ff3b1e3d 100644 --- a/erupt-ai/src/main/java/xyz/erupt/ai/llm/Gemini.java +++ b/erupt-ai/src/main/java/xyz/erupt/ai/llm/Gemini.java @@ -1,15 +1,27 @@ package xyz.erupt.ai.llm; +import dev.langchain4j.data.message.ChatMessage; +import dev.langchain4j.model.chat.ChatModel; +import dev.langchain4j.model.chat.StreamingChatModel; +import dev.langchain4j.model.googleai.GoogleAiGeminiChatModel; +import dev.langchain4j.model.googleai.GoogleAiGeminiStreamingChatModel; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; -import xyz.erupt.ai.core.LlmConfig; -import xyz.erupt.ai.core.OpenAi; +import xyz.erupt.ai.core.LlmCore; +import xyz.erupt.ai.core.LlmRequest; +import xyz.erupt.ai.core.SseListener; + +import java.util.List; +import java.util.function.Consumer; /** * @author YuePeng * date 2025/2/22 16:37 */ @Component -public class Gemini extends OpenAi { +@Slf4j +public class Gemini extends LlmCore { @Override public String model() { @@ -21,4 +33,30 @@ public String api() { return "https://generativelanguage.googleapis.com"; } + @Override + public String code() { + return getClass().getSimpleName(); + } + + @Override + public ChatModel buildChatModel(LlmRequest llmRequest, List chatMessages) { + return GoogleAiGeminiChatModel.builder() + .apiKey(llmRequest.getApiKey()) + .modelName(llmRequest.getModel()) + .topP(llmRequest.getTop_p()) + .temperature(llmRequest.getTemperature()) + .build(); + } + + @Override + @SneakyThrows + public StreamingChatModel buildStreamingChatModel(LlmRequest llmRequest, List chatMessages, Consumer listener) { + return GoogleAiGeminiStreamingChatModel.builder() + .apiKey(llmRequest.getApiKey()) + .modelName(llmRequest.getModel()) + .topP(llmRequest.getTop_p()) + .temperature(llmRequest.getTemperature()) + .build(); + } + } diff --git a/erupt-ai/src/main/java/xyz/erupt/ai/llm/Ollama.java b/erupt-ai/src/main/java/xyz/erupt/ai/llm/Ollama.java index 2d4503b37..247586db5 100644 --- a/erupt-ai/src/main/java/xyz/erupt/ai/llm/Ollama.java +++ b/erupt-ai/src/main/java/xyz/erupt/ai/llm/Ollama.java @@ -1,15 +1,29 @@ package xyz.erupt.ai.llm; +import dev.langchain4j.data.message.ChatMessage; +import dev.langchain4j.model.chat.ChatModel; +import dev.langchain4j.model.chat.StreamingChatModel; +import dev.langchain4j.model.ollama.OllamaChatModel; +import dev.langchain4j.model.ollama.OllamaStreamingChatModel; import org.springframework.stereotype.Component; -import xyz.erupt.ai.core.LlmConfig; -import xyz.erupt.ai.core.OpenAi; +import xyz.erupt.ai.core.LlmCore; +import xyz.erupt.ai.core.LlmRequest; +import xyz.erupt.ai.core.SseListener; + +import java.util.List; +import java.util.function.Consumer; /** * @author YuePeng * date 2025/2/26 22:58 */ @Component -public class Ollama extends OpenAi { +public class Ollama extends LlmCore { + + @Override + public String code() { + return Ollama.class.getSimpleName(); + } @Override public String model() { @@ -21,4 +35,22 @@ public String api() { return "http://localhost:11434"; } + @Override + public ChatModel buildChatModel(LlmRequest llmRequest, List chatMessages) { + return OllamaChatModel.builder() + .modelName(llmRequest.getModel()) + .topP(llmRequest.getTop_p()) + .temperature(llmRequest.getTemperature()) + .build(); + } + + @Override + public StreamingChatModel buildStreamingChatModel(LlmRequest llmRequest, List chatMessages, Consumer listener) { + return OllamaStreamingChatModel.builder() + .modelName(llmRequest.getModel()) + .topP(llmRequest.getTop_p()) + .temperature(llmRequest.getTemperature()) + .build(); + } + } diff --git a/erupt-ai/src/main/java/xyz/erupt/ai/llm/OpenRouter.java b/erupt-ai/src/main/java/xyz/erupt/ai/llm/OpenRouter.java index 0edf9fb11..f422d1acd 100644 --- a/erupt-ai/src/main/java/xyz/erupt/ai/llm/OpenRouter.java +++ b/erupt-ai/src/main/java/xyz/erupt/ai/llm/OpenRouter.java @@ -17,7 +17,7 @@ public String api() { } @Override - public String chatApiPath() { - return "/api/v1/chat/completions"; + public String chatApiPoint() { + return "/api/v1"; } } \ No newline at end of file diff --git a/erupt-ai/src/main/java/xyz/erupt/ai/llm/Qwen.java b/erupt-ai/src/main/java/xyz/erupt/ai/llm/Qwen.java index aa92acd6a..f403f36af 100644 --- a/erupt-ai/src/main/java/xyz/erupt/ai/llm/Qwen.java +++ b/erupt-ai/src/main/java/xyz/erupt/ai/llm/Qwen.java @@ -11,8 +11,8 @@ public class Qwen extends OpenAi { @Override - public String chatApiPath() { - return "/compatible-mode/v1/chat/completions"; + public String chatApiPoint() { + return "/compatible-mode/v1"; } @Override diff --git a/erupt-ai/src/main/java/xyz/erupt/ai/model/Chat.java b/erupt-ai/src/main/java/xyz/erupt/ai/model/AiChat.java similarity index 94% rename from erupt-ai/src/main/java/xyz/erupt/ai/model/Chat.java rename to erupt-ai/src/main/java/xyz/erupt/ai/model/AiChat.java index 6a2dc0c6d..cadae4b0c 100644 --- a/erupt-ai/src/main/java/xyz/erupt/ai/model/Chat.java +++ b/erupt-ai/src/main/java/xyz/erupt/ai/model/AiChat.java @@ -28,12 +28,12 @@ @Erupt( name = "会话管理", power = @Power(add = false, edit = false, viewDetails = false), orderBy = "createdTime desc", - drills = @Drill(title = "会话记录", link = @Link(linkErupt = ChatMessage.class, joinColumn = "chatId")) + drills = @Drill(title = "会话记录", link = @Link(linkErupt = AiChatMessage.class, joinColumn = "chatId")) ) @Entity @EruptI18n @Where(clause = "deleted = false or deleted is null") -public class Chat extends BaseModel { +public class AiChat extends BaseModel { @EruptField( views = @View(title = "会话标题"), diff --git a/erupt-ai/src/main/java/xyz/erupt/ai/model/ChatMessage.java b/erupt-ai/src/main/java/xyz/erupt/ai/model/AiChatMessage.java similarity index 81% rename from erupt-ai/src/main/java/xyz/erupt/ai/model/ChatMessage.java rename to erupt-ai/src/main/java/xyz/erupt/ai/model/AiChatMessage.java index 88b62257c..41f7151e7 100644 --- a/erupt-ai/src/main/java/xyz/erupt/ai/model/ChatMessage.java +++ b/erupt-ai/src/main/java/xyz/erupt/ai/model/AiChatMessage.java @@ -24,14 +24,14 @@ * @author YuePeng * date 2025/2/27 22:57 */ -@Erupt(name = "会话管理", dataProxy = ChatMessage.class) +@Erupt(name = "会话管理", dataProxy = AiChatMessage.class) @Getter @Setter @Table(name = "e_ai_chat_message") @Entity @EruptI18n @NoArgsConstructor -public class ChatMessage extends BaseModel implements DataProxy { +public class AiChatMessage extends BaseModel implements DataProxy { private Long chatId; @@ -56,10 +56,10 @@ public class ChatMessage extends BaseModel implements DataProxy { ) private LocalDateTime createdAt; - private Long tokens; + private Integer tokens; - public static ChatMessage create(Long chatId, String llm, String model, ChatSenderType senderType, String content, Long tokens) { - ChatMessage chatMessage = new ChatMessage(); + public static AiChatMessage create(Long chatId, String llm, String model, ChatSenderType senderType, String content, Integer tokens) { + AiChatMessage chatMessage = new AiChatMessage(); chatMessage.setChatId(chatId); chatMessage.setLlm(llm); chatMessage.setModel(model); @@ -72,7 +72,7 @@ public static ChatMessage create(Long chatId, String llm, String model, ChatSend @Override public void afterFetch(Collection> list) { - String senderType = LambdaSee.field(ChatMessage::getSenderType); + String senderType = LambdaSee.field(AiChatMessage::getSenderType); for (Map map : list) { if (map.get(senderType) == ChatSenderType.MODEL) { EruptTableStyle.cellColor(map, senderType, "#09f"); diff --git a/erupt-ai/src/main/java/xyz/erupt/ai/pojo/ChatCompletion.java b/erupt-ai/src/main/java/xyz/erupt/ai/pojo/ChatCompletion.java deleted file mode 100644 index fa404155d..000000000 --- a/erupt-ai/src/main/java/xyz/erupt/ai/pojo/ChatCompletion.java +++ /dev/null @@ -1,74 +0,0 @@ -package xyz.erupt.ai.pojo; - -import lombok.Builder; -import lombok.Getter; -import lombok.Setter; - -import java.util.List; -import java.util.Map; - -/** - * @author YuePeng - * date 2025/2/25 21:45 - */ -@Getter -@Setter -@Builder -public class ChatCompletion { - - private String model; - - private List messages; - - @Builder.Default - private boolean stream = true; - - /** - * Sampling temperature to use, between 0 and 1. - * Higher values (e.g., 0.7) make the output more random, - * while lower values (e.g., 0.2) make it more focused and deterministic. - */ - private Float temperature; - - /** - * An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass. - * So 0.1 means only the tokens comprising the top 10% probability mass are considered. - * We generally recommend altering this or temperature, but not both. - */ - private Float topP; - - /** - * Maximum number of tokens to generate in the completion. - * The total length of input tokens and generated tokens is limited by the model's context length. - */ - private Integer maxTokens; - - /** - * Number between -2.0 and 2.0. - * Positive values penalize new tokens based on their existing frequency in the text so far, - * decreasing the model's likelihood to repeat the same line verbatim. - */ - private Float frequency_penalty; - - /** - * Number between -2.0 and 2.0. - * Positive values penalize new tokens if they have already appeared in the text, - * increasing the model's likelihood to talk about new topics. - */ - private Float presence_penalty; - - /** - * An object specifying the format that the model must output. - * Set to { "type": "json_object" } to enable JSON mode, which guarantees the generated message is valid JSON. - * Note: When using JSON mode, you must still instruct the model to generate JSON via system or user messages, - * otherwise it may produce endless whitespace until the token limit is reached, making the request appear "stuck". - * Additionally, if finish_reason="length", the message content may be partially truncated because generation exceeded max_tokens or the conversation exceeded the maximum context length. - */ - private Map response_format; - - /** - * List of stop sequences; the API will stop generating further tokens when any of these is encountered. - */ - private List stop; - -} diff --git a/erupt-ai/src/main/java/xyz/erupt/ai/pojo/ChatCompletionMessage.java b/erupt-ai/src/main/java/xyz/erupt/ai/pojo/ChatCompletionMessage.java deleted file mode 100644 index 3bf6d9bc7..000000000 --- a/erupt-ai/src/main/java/xyz/erupt/ai/pojo/ChatCompletionMessage.java +++ /dev/null @@ -1,25 +0,0 @@ -package xyz.erupt.ai.pojo; - -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import xyz.erupt.ai.constants.MessageRole; - -/** - * @author YuePeng - * date 2025/2/25 21:52 - */ -@Getter -@Setter -@NoArgsConstructor -public class ChatCompletionMessage { - - private MessageRole role; - - private String content; - - public ChatCompletionMessage(MessageRole role, String content) { - this.role = role; - this.content = content; - } -} diff --git a/erupt-ai/src/main/java/xyz/erupt/ai/pojo/ChatCompletionResponse.java b/erupt-ai/src/main/java/xyz/erupt/ai/pojo/ChatCompletionResponse.java deleted file mode 100644 index a0f80a6a6..000000000 --- a/erupt-ai/src/main/java/xyz/erupt/ai/pojo/ChatCompletionResponse.java +++ /dev/null @@ -1,54 +0,0 @@ -package xyz.erupt.ai.pojo; - -import lombok.Data; - -import java.util.List; - -/** - * @author YuePeng - * date 2025/2/25 23:02 - */ -@Data -public class ChatCompletionResponse { - - private String id; - - private String object; - - private long created; - - private String model; - - private List choices; - - private ChatUsage usage; - - @Data - public static class Choice { - - private int index; - - private Message message; - - private String finishReason; - - } - - @Data - public static class Message { - - private String role; - - private String content; - - } - - public String getMessageStr() { - StringBuilder sb = new StringBuilder(); - for (Choice choice : this.choices) { - sb.append(choice.getMessage().getContent()); - } - return sb.toString(); - } - -} diff --git a/erupt-ai/src/main/java/xyz/erupt/ai/pojo/ChatCompletionStreamResponse.java b/erupt-ai/src/main/java/xyz/erupt/ai/pojo/ChatCompletionStreamResponse.java deleted file mode 100644 index 6508b34b9..000000000 --- a/erupt-ai/src/main/java/xyz/erupt/ai/pojo/ChatCompletionStreamResponse.java +++ /dev/null @@ -1,44 +0,0 @@ -package xyz.erupt.ai.pojo; - -import lombok.Data; - -import java.util.List; - -/** - * @author YuePeng - * date 2025/2/25 23:02 - */ -@Data -public class ChatCompletionStreamResponse { - - private String id; - - private String object; - - private long created; - - private String model; - - private List choices; - - private String systemFingerprint; - - @Data - public static class Choice { - private int index; - - private Delta delta; - - private String finishReason; - - private ChatUsage usage; - } - - @Data - public static class Delta { - - private String content; - - } - -} diff --git a/erupt-ai/src/main/java/xyz/erupt/ai/pojo/ChatUsage.java b/erupt-ai/src/main/java/xyz/erupt/ai/pojo/ChatUsage.java deleted file mode 100644 index c31706a88..000000000 --- a/erupt-ai/src/main/java/xyz/erupt/ai/pojo/ChatUsage.java +++ /dev/null @@ -1,25 +0,0 @@ -package xyz.erupt.ai.pojo; - -import lombok.Data; - -/** - * @author YuePeng - * date 2025/2/25 23:15 - */ -@Data -public class ChatUsage { - - private int prompt_tokens = 0; - - private int completion_tokens = 0; - - private int total_tokens = 0; - - public ChatUsage plus(ChatUsage chatUsage) { - this.completion_tokens += chatUsage.completion_tokens; - this.prompt_tokens += chatUsage.prompt_tokens; - this.total_tokens += chatUsage.total_tokens; - return this; - } - -} diff --git a/erupt-ai/src/main/java/xyz/erupt/ai/service/LLMService.java b/erupt-ai/src/main/java/xyz/erupt/ai/service/LLMService.java index 954121a62..9154fbf02 100644 --- a/erupt-ai/src/main/java/xyz/erupt/ai/service/LLMService.java +++ b/erupt-ai/src/main/java/xyz/erupt/ai/service/LLMService.java @@ -1,5 +1,10 @@ package xyz.erupt.ai.service; +import dev.langchain4j.agent.tool.ToolExecutionRequest; +import dev.langchain4j.data.message.AiMessage; +import dev.langchain4j.data.message.ChatMessage; +import dev.langchain4j.data.message.SystemMessage; +import dev.langchain4j.data.message.UserMessage; import jakarta.annotation.Resource; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; @@ -9,18 +14,16 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; -import xyz.erupt.ai.call.AiFunctionManager; import xyz.erupt.ai.config.AiProp; import xyz.erupt.ai.constants.ChatSenderType; -import xyz.erupt.ai.constants.MessageRole; import xyz.erupt.ai.core.LlmCore; import xyz.erupt.ai.core.LlmRequest; import xyz.erupt.ai.handler.EruptPromptHandler; -import xyz.erupt.ai.model.Chat; -import xyz.erupt.ai.model.ChatMessage; +import xyz.erupt.ai.model.AiChat; +import xyz.erupt.ai.model.AiChatMessage; import xyz.erupt.ai.model.LLM; import xyz.erupt.ai.model.LLMAgent; -import xyz.erupt.ai.pojo.ChatCompletionMessage; +import xyz.erupt.ai.tool.AiToolboxManager; import xyz.erupt.ai.vo.SseBody; import xyz.erupt.core.config.GsonFactory; import xyz.erupt.core.context.MetaContext; @@ -46,124 +49,131 @@ public class LLMService { @Resource private EruptDao eruptDao; - @Resource - private AiFunctionManager aiFunctionManager; - - private static final int MESSAGE_TS = 100; - public String send(String prompt) { - return send(prompt, Collections.emptyList()); + return this.send(List.of(UserMessage.from(prompt))); } - public String send(String prompt, List assistantPrompt) { - return send(eruptDao.lambdaQuery(LLM.class).eq(LLM::getDefaultLLM, true).eq(LLM::getEnable, true).limit(1).one(), prompt, assistantPrompt); + public String send(List chatMessages) { + return this.send(eruptDao.lambdaQuery(LLM.class).eq(LLM::getDefaultLLM, true).eq(LLM::getEnable, true).limit(1).one(), chatMessages); } public String send(LLM llm, String prompt) { - return send(llm, prompt, Collections.emptyList()); + return send(llm, List.of(UserMessage.from(prompt))); } - public String send(LLM llmConfig, String prompt, List assistantPrompt) { + public String send(LLM llmConfig, List chatMessages) { if (null == llmConfig) { throw new EruptWebApiRuntimeException("Not found LLM config"); } - return LlmCore.getLLM(llmConfig.getLlm()).chat(llmConfig.toLlmRequest(), prompt, assistantPrompt).getMessageStr(); + return LlmCore.getLLM(llmConfig.getLlm()).chat(llmConfig.toLlmRequest(), chatMessages); } @SneakyThrows - public List geneCompletionPrompt(Chat chat, LLMAgent llmAgent, Integer contextTurn) { - List chatCompletionMessages = new ArrayList<>(); - chatCompletionMessages.add(new ChatCompletionMessage(MessageRole.system, aiProp.getSystemPrompt())); + public List geneCompletionPrompt(AiChat chat, LLMAgent llmAgent, Integer contextTurn) { + List messages = new ArrayList<>(); if (null != llmAgent) { // Agent if (null == llmAgent.getPromptHandler()) { - chatCompletionMessages.add(new ChatCompletionMessage(MessageRole.system, llmAgent.getPrompt())); + messages.add(SystemMessage.from(llmAgent.getPrompt())); } else { - chatCompletionMessages.add(new ChatCompletionMessage(MessageRole.system, - EruptSpringUtil.getBeanByPath(llmAgent.getPromptHandler(), EruptPromptHandler.class).handle(llmAgent.getPrompt()))); - } - } else { - // Function Call - if (aiProp.isEnableFunctionCall()) { - chatCompletionMessages.add(new ChatCompletionMessage(MessageRole.system, aiFunctionManager.getFunctionCallPrompt())); + messages.add(SystemMessage.from(EruptSpringUtil.getBeanByPath(llmAgent.getPromptHandler(), EruptPromptHandler.class).handle(llmAgent.getPrompt()))); } } - List chatMessages = eruptDao.lambdaQuery(ChatMessage.class) - .eq(ChatMessage::getChatId, chat.getId()) - .isNotNull(ChatMessage::getContent) - .orderByDesc(ChatMessage::getCreatedAt) + List chatMessages = eruptDao.lambdaQuery(AiChatMessage.class) + .eq(AiChatMessage::getChatId, chat.getId()) + .isNotNull(AiChatMessage::getContent) + .orderByDesc(AiChatMessage::getCreatedAt) .limit(contextTurn + 1).list(); Collections.reverse(chatMessages); - chatMessages.forEach(it -> chatCompletionMessages.add( - new ChatCompletionMessage(it.getSenderType() == ChatSenderType.USER ? MessageRole.user : MessageRole.assistant, it.getContent())) - ); - return chatCompletionMessages; + for (AiChatMessage message : chatMessages) { + if (message.getSenderType() == ChatSenderType.USER) { + messages.add(UserMessage.from(message.getContent())); + } else if (message.getSenderType() == ChatSenderType.MODEL) { + messages.add(AiMessage.from(message.getContent())); + } + } + return messages; } @Async @Transactional @SneakyThrows - public void sendSse(MetaContext metaContext, LLMAgent llmAgent, SseEmitter emitter, LlmCore llm, LLM llmModal, ChatMessage chatMessage, List completionMessage) { + public void sendSse(MetaContext metaContext, LLMAgent llmAgent, SseEmitter emitter, LlmCore llm, LLM llmModal, AiChatMessage chatMessage, List chatMessages) { try { MetaContext.set(metaContext); LlmRequest llmRequest = llmModal.toLlmRequest(); if (null != llmAgent) { llmAgent.mergeToLLmRequest(llmModal); } - llm.chatSse(llmRequest, chatMessage.getContent(), completionMessage, it -> { - if (it.isFinish()) { - String msg = it.getOutput().toString(); - // finish 时,无论之前发送了多少,都要确保最后的增量被发送 - if (msg.length() > MESSAGE_TS && !it.isError()) { - // 只发送最后的增量部分 - if (!StringUtils.isEmpty(it.getCurrMessage())) { - this.sendMessage(emitter, it.getCurrMessage(), llmModal, chatMessage, completionMessage); + llm.chatSse(llmRequest, chatMessages, it -> { + if (null != it.getThrowable()) { + String message = it.getThrowable().getMessage(); + this.sendSseMessage(emitter, message); + eruptDao.persistAndFlush(AiChatMessage.create(chatMessage.getChatId(), llmModal.getLlm(), llmModal.getModel(), ChatSenderType.MODEL, message, 0)); + emitter.complete(); + } else if (it.isFinish()) { + String message = it.getAiMessage().text(); + if (aiProp.isEnableFunctionCall()) { + if (it.getAiMessage().hasToolExecutionRequests()) { + List functionCallRtn = new ArrayList<>(); + for (ToolExecutionRequest request : it.getAiMessage().toolExecutionRequests()) { + try { + Object rtn = AiToolboxManager.invoke(request); + if (null != rtn) { + functionCallRtn.add(rtn.toString()); + } + } catch (Exception e) { + log.error("Function call error", e); + this.stopSse(emitter, chatMessage, llmModal, e.toString()); + } + } + if (functionCallRtn.isEmpty()) { + message = "Completed !"; + } else { + for (String s : functionCallRtn) { + chatMessages.add(AiMessage.from(s)); + } + message = llm.chat(llmRequest, chatMessages); + this.sendSseMessage(emitter, message); + } } - } else { - // 短消息直接发送完整内容 - msg = this.sendMessage(emitter, msg, llmModal, chatMessage, completionMessage); } - - // 保存到数据库时使用完整消息 - chatMessage.setTokens((long) it.getUsage().getPrompt_tokens()); + chatMessage.setTokens(it.getUsage().inputTokenCount()); eruptDao.mergeAndFlush(chatMessage); - eruptDao.persistAndFlush(ChatMessage.create(chatMessage.getChatId(), llmModal.getLlm(), llmModal.getModel(), ChatSenderType.MODEL, msg, (long) it.getUsage().getCompletion_tokens())); + eruptDao.persistAndFlush(AiChatMessage.create(chatMessage.getChatId(), llmModal.getLlm(), llmModal.getModel(), ChatSenderType.MODEL, message, it.getUsage().outputTokenCount())); emitter.complete(); } else { - if (it.getOutput().toString().length() > MESSAGE_TS) { - if (it.isPending()) { - this.sendMessage(emitter, it.getOutput().toString(), llmModal, chatMessage, completionMessage); - it.setPending(false); - } else { - this.sendMessage(emitter, it.getCurrMessage(), llmModal, chatMessage, completionMessage); - } - } else { - it.setPending(true); - } + // streaming + this.sendSseMessage(emitter, it.getCurrMessage()); } }); } catch (Exception e) { - eruptDao.persistAndFlush(ChatMessage.create(chatMessage.getChatId(), llmModal.getLlm(), llmModal.getModel(), ChatSenderType.MODEL, e.getMessage(), 0L)); - emitter.send(GsonFactory.getGson().toJson(new SseBody(e.getMessage())), MediaType.TEXT_EVENT_STREAM); - emitter.complete(); + this.stopSse(emitter, chatMessage, llmModal, e.toString()); } } - private String sendMessage(SseEmitter emitter, String userMessage, LLM llm, ChatMessage chatMessage, List userContext) { - if (aiProp.isEnableFunctionCall() && aiFunctionManager.exist(userMessage.trim())) { - String functionMessage = aiFunctionManager.call(userMessage, llm, chatMessage.getContent(), userContext); - try { - emitter.send(GsonFactory.getGson().toJson(new SseBody(functionMessage)), MediaType.TEXT_EVENT_STREAM); - } catch (Exception ignore) { - } - return functionMessage; - } else { - try { - emitter.send(GsonFactory.getGson().toJson(new SseBody(userMessage)), MediaType.TEXT_EVENT_STREAM); - } catch (Exception ignore) { + @SneakyThrows + private void stopSse(SseEmitter emitter, AiChatMessage chatMessage, LLM llmModal, String reason) { + eruptDao.persistAndFlush(AiChatMessage.create(chatMessage.getChatId(), llmModal.getLlm(), llmModal.getModel(), ChatSenderType.MODEL, reason, 0)); + emitter.send(GsonFactory.getGson().toJson(new SseBody(reason)), MediaType.TEXT_EVENT_STREAM); + emitter.complete(); + } + + private void sendSseMessage(SseEmitter emitter, String llmMessage) { + try { + if (StringUtils.isNotBlank(llmMessage) && llmMessage.length() > aiProp.getMessageChunkSize()) { + for (int i = 0; i < llmMessage.length(); i += aiProp.getMessageChunkSize()) { + int end = Math.min(i + aiProp.getMessageChunkSize(), llmMessage.length()); + emitter.send(GsonFactory.getGson().toJson(new SseBody(llmMessage.substring(i, end))), MediaType.TEXT_EVENT_STREAM); + if (aiProp.getMessageDelay() > 0) { + Thread.sleep(aiProp.getMessageDelay()); + } + } + } else { + emitter.send(GsonFactory.getGson().toJson(new SseBody(llmMessage)), MediaType.TEXT_EVENT_STREAM); } + } catch (Exception ignore) { } - return userMessage; } } diff --git a/erupt-ai/src/main/java/xyz/erupt/ai/tool/AiToolboxManager.java b/erupt-ai/src/main/java/xyz/erupt/ai/tool/AiToolboxManager.java new file mode 100644 index 000000000..6d1a51228 --- /dev/null +++ b/erupt-ai/src/main/java/xyz/erupt/ai/tool/AiToolboxManager.java @@ -0,0 +1,64 @@ +package xyz.erupt.ai.tool; + +import com.google.gson.JsonObject; +import dev.langchain4j.agent.tool.Tool; +import dev.langchain4j.agent.tool.ToolExecutionRequest; +import lombok.Getter; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.core.annotation.Order; +import org.springframework.core.type.filter.AnnotationTypeFilter; +import org.springframework.core.type.filter.TypeFilter; +import org.springframework.stereotype.Service; +import xyz.erupt.ai.annotation.AiToolbox; +import xyz.erupt.core.config.GsonFactory; +import xyz.erupt.core.service.EruptApplication; +import xyz.erupt.core.util.EruptSpringUtil; + +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; + +@Order(100) +@Service +@Slf4j +public class AiToolboxManager implements ApplicationRunner { + + @Getter + private static final Map aiMethodMap = new HashMap<>(); + + @SneakyThrows + public static Object invoke(ToolExecutionRequest request) { + Method method = aiMethodMap.get(request.name()); + if (null != method) { + Object[] args = new Object[method.getParameterCount()]; + if (StringUtils.isNotBlank(request.arguments())) { + JsonObject jsonObject = GsonFactory.getGson().fromJson(request.arguments(), JsonObject.class); + for (int i = 0; i < method.getParameters().length; i++) { + String paramName = method.getParameters()[i].getName(); + if (jsonObject.has(paramName)) { + args[i] = GsonFactory.getGson().fromJson(jsonObject.get(paramName), method.getGenericParameterTypes()[i]); + } + } + } + return method.invoke(EruptSpringUtil.getBean(method.getDeclaringClass()), args); + } + return null; + } + + @Override + public void run(ApplicationArguments args) { + EruptSpringUtil.scannerPackage(EruptApplication.getScanPackage(), new TypeFilter[]{ + new AnnotationTypeFilter(AiToolbox.class) + }, clazz -> { + for (Method method : clazz.getDeclaredMethods()) { + if (method.isAnnotationPresent(Tool.class)) { + aiMethodMap.put(method.getName(), method); + } + } + }); + } +} diff --git a/erupt-ai/src/main/java/xyz/erupt/ai/tool/EruptAiToolbox.java b/erupt-ai/src/main/java/xyz/erupt/ai/tool/EruptAiToolbox.java new file mode 100644 index 000000000..66b3dcf5a --- /dev/null +++ b/erupt-ai/src/main/java/xyz/erupt/ai/tool/EruptAiToolbox.java @@ -0,0 +1,62 @@ +package xyz.erupt.ai.tool; + +import dev.langchain4j.agent.tool.P; +import dev.langchain4j.agent.tool.Tool; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Component; +import xyz.erupt.ai.annotation.AiToolbox; +import xyz.erupt.core.config.GsonFactory; +import xyz.erupt.core.context.MetaContext; +import xyz.erupt.core.module.EruptModuleInvoke; +import xyz.erupt.core.service.EruptCoreService; +import xyz.erupt.core.view.EruptModel; +import xyz.erupt.jpa.dao.EruptDao; +import xyz.erupt.upms.model.EruptUser; + +import java.util.List; + +@AiToolbox +@Component +public class EruptAiToolbox { + + @Resource + private EruptDao eruptDao; + + @Tool("Erupt model list") + public String eruptModelList() { + StringBuilder sb = new StringBuilder(); + for (EruptModel erupt : EruptCoreService.getErupts()) { + sb.append(erupt.getEruptName()).append(": ").append(erupt.getErupt().name()); + } + return sb.toString(); + } + + @Tool("Erupt model schema") + public String eruptSchema(@P("Erupt Name") String eruptName) { + EruptModel erupt = EruptCoreService.getEruptView(eruptName); + return GsonFactory.getGson().toJson(erupt); + } + + @Tool("Ask the current system logged-in user") + public String eruptUserInfo() { + EruptUser eruptUser = eruptDao.find(EruptUser.class, MetaContext.getUser().getUid()); + eruptUser.setRoles(null); + eruptUser.setHeadOrg(null); + eruptUser.setPassword(null); + return GsonFactory.getGson().toJson(eruptUser); + } + + @Tool("Query erupt model data") + public String eruptDataQuery(@P("HQL (Hibernate Query Language)") String hql) { + List result = eruptDao.getEntityManager().createQuery(hql).getResultList(); + return GsonFactory.getGson().toJson(result); + } + + @Tool("Erupt Module list") + public String eruptModuleList() { + StringBuilder sb = new StringBuilder(); + EruptModuleInvoke.invoke(it -> sb.append(GsonFactory.getGson().toJson(it.info())).append("\n")); + return sb.toString(); + } + +} diff --git a/erupt-core/src/main/java/xyz/erupt/core/controller/EruptDataController.java b/erupt-core/src/main/java/xyz/erupt/core/controller/EruptDataController.java index fff4f0672..8db10ba0c 100644 --- a/erupt-core/src/main/java/xyz/erupt/core/controller/EruptDataController.java +++ b/erupt-core/src/main/java/xyz/erupt/core/controller/EruptDataController.java @@ -1 +1 @@ -package xyz.erupt.core.controller; import com.google.gson.Gson; import com.google.gson.JsonObject; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.web.bind.annotation.*; import xyz.erupt.annotation.config.QueryExpression; import xyz.erupt.annotation.constant.AnnotationConst; import xyz.erupt.annotation.fun.OperationHandler; import xyz.erupt.annotation.fun.PowerObject; import xyz.erupt.annotation.model.Row; import xyz.erupt.annotation.query.Condition; import xyz.erupt.annotation.query.Sort; import xyz.erupt.annotation.sub_erupt.Filter; import xyz.erupt.annotation.sub_erupt.RowOperation; import xyz.erupt.annotation.sub_erupt.Tree; import xyz.erupt.annotation.sub_field.Edit; import xyz.erupt.annotation.sub_field.sub_edit.CheckboxType; import xyz.erupt.annotation.sub_field.sub_edit.OnChange; import xyz.erupt.annotation.sub_field.sub_edit.ReferenceTableType; import xyz.erupt.annotation.sub_field.sub_edit.ReferenceTreeType; import xyz.erupt.core.annotation.EruptRecordOperate; import xyz.erupt.core.annotation.EruptRouter; import xyz.erupt.core.config.GsonFactory; import xyz.erupt.core.constant.EruptConst; import xyz.erupt.core.constant.EruptRestPath; import xyz.erupt.core.exception.EruptNoLegalPowerException; import xyz.erupt.core.i18n.I18nTranslate; import xyz.erupt.core.invoke.DataProcessorManager; import xyz.erupt.core.invoke.DataProxyInvoke; import xyz.erupt.core.invoke.ExprInvoke; import xyz.erupt.core.naming.EruptRowOperationNaming; import xyz.erupt.core.query.Column; import xyz.erupt.core.query.EruptQuery; import xyz.erupt.core.service.EruptCoreService; import xyz.erupt.core.service.EruptService; import xyz.erupt.core.service.PreEruptDataService; import xyz.erupt.core.util.EruptSpringUtil; import xyz.erupt.core.util.EruptUtil; import xyz.erupt.core.util.Erupts; import xyz.erupt.core.util.TypeUtil; import xyz.erupt.core.view.*; import java.io.Serializable; import java.util.*; import java.util.stream.Collectors; /** * @author YuePeng * date 9/28/18. */ @RestController @RequestMapping(EruptRestPath.ERUPT_DATA) @RequiredArgsConstructor @Slf4j public class EruptDataController { private final EruptService eruptService; private final PreEruptDataService preEruptDataService; public static final int MAX_PAGE_SIZE = 50000; private final Gson gson = GsonFactory.getGson(); @PostMapping({"/table/{erupt}"}) @EruptRouter(authIndex = 2, verifyType = EruptRouter.VerifyType.ERUPT) public Page getEruptData(@PathVariable("erupt") String eruptName, @RequestBody TableQuery tableQuery) { if (tableQuery.getPageSize() > MAX_PAGE_SIZE) { tableQuery.setPageSize(MAX_PAGE_SIZE); } return eruptService.getEruptData(EruptCoreService.getErupt(eruptName), tableQuery, null); } @GetMapping("/tree/{erupt}") @EruptRouter(authIndex = 2, verifyType = EruptRouter.VerifyType.ERUPT) public Collection getEruptTreeData(@PathVariable("erupt") String eruptName) { EruptModel eruptModel = EruptCoreService.getErupt(eruptName); Erupts.powerLegal(eruptModel, PowerObject::isQuery); Tree tree = eruptModel.getErupt().tree(); return preEruptDataService.geneTree(eruptModel, tree.id(), tree.label(), tree.pid(), tree.rootPid(), EruptQuery.builder().build()); } //获取初始化数据 @GetMapping("/init-value/{erupt}") @SneakyThrows @EruptRouter(authIndex = 2, verifyType = EruptRouter.VerifyType.ERUPT) public Map initEruptValue(@PathVariable("erupt") String eruptName) { EruptModel eruptModel = EruptCoreService.getErupt(eruptName); Object obj = eruptModel.getClazz().getDeclaredConstructor().newInstance(); DataProxyInvoke.invoke(eruptModel, (dataProxy -> dataProxy.addBehavior(obj))); return EruptUtil.generateEruptDataMap(eruptModel, obj, false); } @GetMapping("/{erupt}/{id}") @EruptRouter(authIndex = 1, verifyType = EruptRouter.VerifyType.ERUPT) public Map getEruptDataById(@PathVariable("erupt") String eruptName, @PathVariable("id") String id) { EruptModel eruptModel = EruptCoreService.getErupt(eruptName); Erupts.powerLegal(eruptModel, powerObject -> powerObject.isEdit() || powerObject.isViewDetails() || powerObject.isPrint()); eruptService.verifyIdPermissions(eruptModel, id); return preEruptDataService.getEruptData(eruptModel, id, false); } public static final String OPERATOR_PATH_STR = "/operator"; /** * Custom button form initialization values * * @param eruptName eruptName * @param code btn code * @param ids link ids * @return form value */ @PostMapping("/{erupt}" + OPERATOR_PATH_STR + "/{code}/form-value") @EruptRouter(authIndex = 1, verifyType = EruptRouter.VerifyType.ERUPT) @SneakyThrows public Object eruptOperatorFormValue(@PathVariable("erupt") String eruptName, @PathVariable("code") String code, @RequestParam(required = false) List ids) { if (null == ids) ids = new ArrayList<>(); EruptModel eruptModel = EruptCoreService.getErupt(eruptName); RowOperation rowOperation = Arrays.stream(eruptModel.getErupt().rowOperation()).filter(it -> code.equals(it.code())).findFirst().orElseThrow(EruptNoLegalPowerException::new); EruptModel operatorErupt = EruptCoreService.getErupt(rowOperation.eruptClass().getSimpleName()); if (rowOperation.operationHandler().isInterface()) return null; OperationHandler operationHandler = EruptSpringUtil.getBean(rowOperation.operationHandler()); Object eruptForm = rowOperation.eruptClass().getDeclaredConstructor().newInstance(); DataProxyInvoke.invoke(operatorErupt, (dataProxy -> dataProxy.addBehavior(eruptForm))); try { operationHandler.getClass().getDeclaredMethod("eruptFormValue", List.class, operatorErupt.getClazz(), String[].class); return operationHandler.eruptFormValue(ids.stream().map(id -> DataProcessorManager.getEruptDataProcessor(eruptModel.getClazz()) .findDataById(eruptModel, EruptUtil.toEruptId(eruptModel, id.toString()))).collect(Collectors.toList()), eruptForm, rowOperation.operationParam()); } catch (NoSuchMethodException ignored) { } return eruptForm; } @PostMapping("/{erupt}" + OPERATOR_PATH_STR + "/{code}") @EruptRouter(authIndex = 1, verifyType = EruptRouter.VerifyType.ERUPT) @EruptRecordOperate(value = "", dynamicConfig = EruptRowOperationNaming.class) public EruptApiModel eruptOperatorExec(@PathVariable("erupt") String eruptName, @PathVariable("code") String code, @RequestBody JsonObject body) { EruptModel eruptModel = EruptCoreService.getErupt(eruptName); RowOperation rowOperation = Arrays.stream(eruptModel.getErupt().rowOperation()).filter(it -> code.equals(it.code())).findFirst().orElseThrow(EruptNoLegalPowerException::new); Erupts.powerLegal(ExprInvoke.getExpr(rowOperation.show())); if (rowOperation.eruptClass() != void.class) { EruptModel erupt = EruptCoreService.getErupt(rowOperation.eruptClass().getSimpleName()); EruptApiModel eruptApiModel = EruptUtil.validateEruptValue(erupt, body.getAsJsonObject("param")); if (eruptApiModel.getStatus() == EruptApiModel.Status.ERROR) return eruptApiModel; } if (rowOperation.operationHandler().isInterface()) { return EruptApiModel.errorApi("Please implement the 'OperationHandler' interface for " + rowOperation.title()); } OperationHandler operationHandler = EruptSpringUtil.getBean(rowOperation.operationHandler()); Object param = null; if (!body.get("param").isJsonNull()) { param = gson.fromJson(body.getAsJsonObject("param"), rowOperation.eruptClass()); } if (rowOperation.mode() == RowOperation.Mode.BUTTON) { String eval = operationHandler.exec(null, param, rowOperation.operationParam()); if (StringUtils.isNotBlank(eval)) { return EruptApiModel.successApi(eval); } else { return EruptApiModel.successApi(I18nTranslate.$translate("erupt.exec_success"), null); } } if (body.get("ids").isJsonArray() && !body.getAsJsonArray("ids").isEmpty()) { List list = new ArrayList<>(); body.getAsJsonArray("ids").forEach(id -> list.add(DataProcessorManager.getEruptDataProcessor(eruptModel.getClazz()) .findDataById(eruptModel, EruptUtil.toEruptId(eruptModel, id.getAsString())))); String eval = operationHandler.exec(list, param, rowOperation.operationParam()); if (StringUtils.isNotBlank(eval)) { return EruptApiModel.successApi(eval); } } return EruptApiModel.successApi(I18nTranslate.$translate("erupt.exec_success"), null); } @GetMapping("/tab/tree/{erupt}/{tabFieldName}") @EruptRouter(authIndex = 3, verifyType = EruptRouter.VerifyType.ERUPT) public Collection findTabTree(@PathVariable("erupt") String eruptName, @PathVariable("tabFieldName") String tabFieldName) { EruptModel eruptModel = EruptCoreService.getErupt(eruptName); // Erupts.powerLegal(eruptModel, powerObject -> powerObject.isViewDetails() || powerObject.isEdit()); EruptModel tabEruptModel = EruptCoreService.getErupt(eruptModel.getEruptFieldMap().get(tabFieldName).getFieldReturnName()); Tree tree = tabEruptModel.getErupt().tree(); EruptFieldModel eruptFieldModel = eruptModel.getEruptFieldMap().get(tabFieldName); EruptQuery eruptQuery = EruptQuery.builder().conditionStrings( Arrays.stream(eruptFieldModel.getEruptField().edit().filter()).map(Filter::value).collect(Collectors.toList()) ).build(); return preEruptDataService.geneTree(tabEruptModel, tree.id(), tree.label(), tree.pid(), tree.rootPid(), eruptQuery); } @GetMapping("/{erupt}/checkbox/{fieldName}") @EruptRouter(authIndex = 1, verifyType = EruptRouter.VerifyType.ERUPT) public Collection> findCheckbox(@PathVariable("erupt") String eruptName, @PathVariable("fieldName") String fieldName) { EruptModel eruptModel = EruptCoreService.getErupt(eruptName); EruptFieldModel eruptFieldModel = eruptModel.getEruptFieldMap().get(fieldName); EruptModel tabEruptModel = EruptCoreService.getErupt(eruptFieldModel.getFieldReturnName()); CheckboxType checkboxType = eruptFieldModel.getEruptField().edit().checkboxType(); List columns = new ArrayList<>(); columns.add(new Column(checkboxType.id(), AnnotationConst.ID)); columns.add(new Column(checkboxType.label(), AnnotationConst.LABEL)); if (!AnnotationConst.EMPTY_STR.equals(checkboxType.remark())) { columns.add(new Column(checkboxType.remark(), AnnotationConst.REMARK)); } EruptQuery eruptQuery = EruptQuery.builder().conditionStrings( Arrays.stream(eruptFieldModel.getEruptField().edit().filter()).map(Filter::value).collect(Collectors.toList()) ).build(); Collection> collection = preEruptDataService.createColumnQuery(tabEruptModel, columns, eruptQuery); Collection> checkboxModels = new ArrayList<>(collection.size()); collection.forEach(map -> checkboxModels.add(new CheckboxModel<>(map.get(AnnotationConst.ID), map.get(AnnotationConst.LABEL), map.get(AnnotationConst.REMARK)))); return checkboxModels; } // REFERENCE API @PostMapping("/{erupt}/reference-table/{fieldName}") @EruptRouter(authIndex = 1, verifyType = EruptRouter.VerifyType.ERUPT) public Page getReferenceTable(@PathVariable("erupt") String eruptName, @PathVariable("fieldName") String fieldName, @RequestParam(value = "dependValue", required = false) Serializable dependValue, @RequestParam(value = "tabRef", required = false) Boolean tabRef, @RequestBody TableQuery tableQuery) { EruptModel eruptModel = EruptCoreService.getErupt(eruptName); EruptFieldModel eruptFieldModel = eruptModel.getEruptFieldMap().get(fieldName); Edit edit = eruptFieldModel.getEruptField().edit(); String dependField = edit.referenceTableType().dependField(); List serverConditions = new ArrayList<>(); List conditions = Arrays.stream(edit.filter()).map(Filter::value).toList(); if (!AnnotationConst.EMPTY_STR.equals(dependField)) { Erupts.requireNonNull(dependValue, I18nTranslate.$translate("erupt.select") + " " + eruptModel.getEruptFieldMap().get(dependField).getEruptField().edit().title()); EruptModel refErupt = EruptCoreService.getErupt(eruptFieldModel.getFieldReturnName()); serverConditions.add(new Condition( eruptFieldModel.getFieldReturnName() + EruptConst.DOT + edit.referenceTableType().dependColumn(), TypeUtil.typeStrConvertObject(dependValue, refErupt.getEruptFieldMap().get(refErupt.getErupt().primaryKeyCol()).getField().getType()), QueryExpression.EQ )); } EruptModel eruptReferenceModel = EruptCoreService.getErupt(eruptFieldModel.getFieldReturnName()); if (!tabRef) { //由于类加载顺序问题,并未选择在启动时检测 ReferenceTableType referenceTableType = eruptFieldModel.getEruptField().edit().referenceTableType(); Erupts.requireTrue(eruptReferenceModel.getEruptFieldMap().containsKey(referenceTableType.label().split("\\.")[0]) , eruptReferenceModel.getEruptName() + " not found '" + referenceTableType.label() + "' field,please use @ReferenceTableType annotation 'label' config"); } return eruptService.getEruptData(eruptReferenceModel, tableQuery, serverConditions, conditions.toArray(new String[0])); } @SneakyThrows @GetMapping("/depend-tree/{erupt}") @EruptRouter(authIndex = 2, verifyType = EruptRouter.VerifyType.ERUPT) public Collection getDependTree(@PathVariable("erupt") String erupt) { EruptModel eruptModel = EruptCoreService.getErupt(erupt); String field = eruptModel.getErupt().linkTree().field(); if (null == eruptModel.getEruptFieldMap().get(field)) { String treeErupt = eruptModel.getClazz().getDeclaredField(field).getType().getSimpleName(); return this.getEruptTreeData(treeErupt); } return this.getReferenceTree(eruptModel.getEruptName(), field, null); } @GetMapping("/{erupt}/reference-tree/{fieldName}") @EruptRouter(authIndex = 1, verifyType = EruptRouter.VerifyType.ERUPT) public Collection getReferenceTree(@PathVariable("erupt") String erupt, @PathVariable("fieldName") String fieldName, @RequestParam(value = "dependValue", required = false) Serializable dependValue) { EruptModel eruptModel = EruptCoreService.getErupt(erupt); EruptFieldModel eruptFieldModel = eruptModel.getEruptFieldMap().get(fieldName); String dependField = eruptFieldModel.getEruptField().edit().referenceTreeType().dependField(); if (!AnnotationConst.EMPTY_STR.equals(dependField)) { Erupts.requireNonNull(dependValue, I18nTranslate.$translate("erupt.select") + " " + eruptModel.getEruptFieldMap().get(dependField).getEruptField().edit().title()); } Edit edit = eruptFieldModel.getEruptField().edit(); ReferenceTreeType treeType = edit.referenceTreeType(); EruptModel referenceEruptModel = EruptCoreService.getErupt(eruptFieldModel.getFieldReturnName()); Erupts.requireTrue(referenceEruptModel.getEruptFieldMap().containsKey(treeType.label().split("\\.")[0]), referenceEruptModel.getEruptName() + " not found " + treeType.label() + " field, please use @ReferenceTreeType annotation config"); List conditions = new ArrayList<>(); // process depend if (StringUtils.isNotBlank(treeType.dependField()) && null != dependValue) { conditions.add(new Condition(edit.referenceTreeType().dependColumn(), dependValue, QueryExpression.EQ)); } List conditionStrings = Arrays.stream(edit.filter()).map(Filter::value).collect(Collectors.toList()); return preEruptDataService.geneTree(referenceEruptModel, treeType.id(), treeType.label(), treeType.pid(), treeType.rootPid(), EruptQuery.builder().sort(Sort.toSortList(edit.orderBy())).conditionStrings(conditionStrings).conditions(conditions).build()); } //自定义行 @PostMapping("/extra-row/{erupt}") @EruptRouter(authIndex = 2, verifyType = EruptRouter.VerifyType.ERUPT) public List extraRow(@PathVariable("erupt") String erupt, @RequestBody TableQuery tableQuery) { List rows = new ArrayList<>(); DataProxyInvoke.invoke(EruptCoreService.getErupt(erupt), dataProxy -> Optional.ofNullable(dataProxy.extraRow(tableQuery.getCondition())).ifPresent(rows::addAll)); return rows; } @PostMapping("/onchange/{erupt}/{field}") @EruptRouter(authIndex = 2, verifyType = EruptRouter.VerifyType.ERUPT) public R onChange(@PathVariable("erupt") String eruptName, @PathVariable("field") String field, @RequestBody JsonObject data) { EruptModel eruptModel = EruptCoreService.getErupt(eruptName); Edit edit = eruptModel.getEruptFieldMap().get(field).getEruptField().edit(); Object o = gson.fromJson(data.toString(), eruptModel.getClazz()); OnChange onChange = EruptSpringUtil.getBean(edit.onchange()); OnChangeVo onChangeVo = new OnChangeVo(); onChangeVo.setFormData(onChange.populateForm(o, edit.onchangeParams())); onChangeVo.setEditExpr(onChange.buildEditExpr(o, edit.onchangeParams())); return R.ok(onChangeVo); } } \ No newline at end of file +package xyz.erupt.core.controller; import com.google.gson.Gson; import com.google.gson.JsonObject; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.web.bind.annotation.*; import xyz.erupt.annotation.config.QueryExpression; import xyz.erupt.annotation.constant.AnnotationConst; import xyz.erupt.annotation.fun.OperationHandler; import xyz.erupt.annotation.fun.PowerObject; import xyz.erupt.annotation.model.Row; import xyz.erupt.annotation.query.Condition; import xyz.erupt.annotation.query.Sort; import xyz.erupt.annotation.sub_erupt.Filter; import xyz.erupt.annotation.sub_erupt.RowOperation; import xyz.erupt.annotation.sub_erupt.Tree; import xyz.erupt.annotation.sub_field.Edit; import xyz.erupt.annotation.sub_field.sub_edit.CheckboxType; import xyz.erupt.annotation.sub_field.sub_edit.OnChange; import xyz.erupt.annotation.sub_field.sub_edit.ReferenceTableType; import xyz.erupt.annotation.sub_field.sub_edit.ReferenceTreeType; import xyz.erupt.core.annotation.EruptRecordOperate; import xyz.erupt.core.annotation.EruptRouter; import xyz.erupt.core.config.GsonFactory; import xyz.erupt.core.constant.EruptConst; import xyz.erupt.core.constant.EruptRestPath; import xyz.erupt.core.exception.EruptNoLegalPowerException; import xyz.erupt.core.i18n.I18nTranslate; import xyz.erupt.core.invoke.DataProcessorManager; import xyz.erupt.core.invoke.DataProxyInvoke; import xyz.erupt.core.invoke.ExprInvoke; import xyz.erupt.core.naming.EruptRowOperationNaming; import xyz.erupt.core.query.Column; import xyz.erupt.core.query.EruptQuery; import xyz.erupt.core.service.EruptCoreService; import xyz.erupt.core.service.EruptService; import xyz.erupt.core.service.PreEruptDataService; import xyz.erupt.core.util.EruptSpringUtil; import xyz.erupt.core.util.EruptUtil; import xyz.erupt.core.util.Erupts; import xyz.erupt.core.util.TypeUtil; import xyz.erupt.core.view.*; import java.io.Serializable; import java.util.*; import java.util.stream.Collectors; /** * @author YuePeng * date 9/28/18. */ @RestController @RequestMapping(EruptRestPath.ERUPT_DATA) @RequiredArgsConstructor @Slf4j public class EruptDataController { private final EruptService eruptService; private final PreEruptDataService preEruptDataService; public static final int MAX_PAGE_SIZE = 50000; private final Gson gson = GsonFactory.getGson(); @PostMapping({"/table/{erupt}"}) @EruptRouter(authIndex = 2, verifyType = EruptRouter.VerifyType.ERUPT) public Page getEruptData(@PathVariable("erupt") String eruptName, @RequestBody TableQuery tableQuery) { if (tableQuery.getPageSize() > MAX_PAGE_SIZE) { tableQuery.setPageSize(MAX_PAGE_SIZE); } return eruptService.getEruptData(EruptCoreService.getErupt(eruptName), tableQuery, null); } @GetMapping("/tree/{erupt}") @EruptRouter(authIndex = 2, verifyType = EruptRouter.VerifyType.ERUPT) public Collection getEruptTreeData(@PathVariable("erupt") String eruptName) { EruptModel eruptModel = EruptCoreService.getErupt(eruptName); Erupts.powerLegal(eruptModel, PowerObject::isQuery); Tree tree = eruptModel.getErupt().tree(); return preEruptDataService.geneTree(eruptModel, tree.id(), tree.label(), tree.pid(), tree.rootPid(), EruptQuery.builder().build()); } //获取初始化数据 @GetMapping("/init-value/{erupt}") @SneakyThrows @EruptRouter(authIndex = 2, verifyType = EruptRouter.VerifyType.ERUPT) public Map initEruptValue(@PathVariable("erupt") String eruptName) { EruptModel eruptModel = EruptCoreService.getErupt(eruptName); Object obj = eruptModel.getClazz().getDeclaredConstructor().newInstance(); DataProxyInvoke.invoke(eruptModel, (dataProxy -> dataProxy.addBehavior(obj))); return EruptUtil.generateEruptDataMap(eruptModel, obj, false); } @GetMapping("/{erupt}/{id}") @EruptRouter(authIndex = 1, verifyType = EruptRouter.VerifyType.ERUPT) public Map getEruptDataById(@PathVariable("erupt") String eruptName, @PathVariable("id") String id) { EruptModel eruptModel = EruptCoreService.getErupt(eruptName); Erupts.powerLegal(eruptModel, powerObject -> powerObject.isEdit() || powerObject.isViewDetails() || powerObject.isPrint()); eruptService.verifyIdPermissions(eruptModel, id); return preEruptDataService.getEruptData(eruptModel, id, false); } public static final String OPERATOR_PATH_STR = "/operator"; /** * Custom button form initialization values * * @param eruptName eruptName * @param code btn code * @param ids link ids * @return form value */ @PostMapping("/{erupt}" + OPERATOR_PATH_STR + "/{code}/form-value") @EruptRouter(authIndex = 1, verifyType = EruptRouter.VerifyType.ERUPT) @SneakyThrows public Object eruptOperatorFormValue(@PathVariable("erupt") String eruptName, @PathVariable("code") String code, @RequestParam(required = false) List ids) { if (null == ids) ids = new ArrayList<>(); EruptModel eruptModel = EruptCoreService.getErupt(eruptName); RowOperation rowOperation = Arrays.stream(eruptModel.getErupt().rowOperation()).filter(it -> code.equals(it.code())).findFirst().orElseThrow(EruptNoLegalPowerException::new); EruptModel operatorErupt = EruptCoreService.getErupt(rowOperation.eruptClass().getSimpleName()); if (rowOperation.operationHandler().isInterface()) return null; OperationHandler operationHandler = EruptSpringUtil.getBean(rowOperation.operationHandler()); Object eruptForm = rowOperation.eruptClass().getDeclaredConstructor().newInstance(); DataProxyInvoke.invoke(operatorErupt, (dataProxy -> dataProxy.addBehavior(eruptForm))); try { operationHandler.getClass().getDeclaredMethod("eruptFormValue", List.class, operatorErupt.getClazz(), String[].class); return operationHandler.eruptFormValue(ids.stream().map(id -> DataProcessorManager.getEruptDataProcessor(eruptModel.getClazz()) .findDataById(eruptModel, EruptUtil.toEruptId(eruptModel, id.toString()))).collect(Collectors.toList()), eruptForm, rowOperation.operationParam()); } catch (NoSuchMethodException ignored) { } return eruptForm; } @PostMapping("/{erupt}" + OPERATOR_PATH_STR + "/{code}") @EruptRouter(authIndex = 1, verifyType = EruptRouter.VerifyType.ERUPT) @EruptRecordOperate(value = "", dynamicConfig = EruptRowOperationNaming.class) public EruptApiModel eruptOperatorExec(@PathVariable("erupt") String eruptName, @PathVariable("code") String code, @RequestBody JsonObject body) { EruptModel eruptModel = EruptCoreService.getErupt(eruptName); RowOperation rowOperation = Arrays.stream(eruptModel.getErupt().rowOperation()).filter(it -> code.equals(it.code())).findFirst().orElseThrow(EruptNoLegalPowerException::new); Erupts.powerLegal(ExprInvoke.getExpr(rowOperation.show())); if (rowOperation.eruptClass() != void.class) { EruptModel erupt = EruptCoreService.getErupt(rowOperation.eruptClass().getSimpleName()); EruptApiModel eruptApiModel = EruptUtil.validateEruptValue(erupt, body.getAsJsonObject("param")); if (eruptApiModel.getStatus() == EruptApiModel.Status.ERROR) return eruptApiModel; } if (rowOperation.operationHandler().isInterface()) { return EruptApiModel.errorApi("Please implement the 'OperationHandler' interface for " + rowOperation.title()); } OperationHandler operationHandler = EruptSpringUtil.getBean(rowOperation.operationHandler()); Object param = null; if (!body.get("param").isJsonNull()) { param = gson.fromJson(body.getAsJsonObject("param"), rowOperation.eruptClass()); } if (rowOperation.mode() == RowOperation.Mode.BUTTON) { String eval = operationHandler.exec(null, param, rowOperation.operationParam()); if (StringUtils.isNotBlank(eval)) { return EruptApiModel.successApi(eval); } else { return EruptApiModel.successApi(I18nTranslate.$translate("erupt.exec_success"), null); } } if (body.get("ids").isJsonArray() && !body.getAsJsonArray("ids").isEmpty()) { List list = new ArrayList<>(); body.getAsJsonArray("ids").forEach(id -> list.add(DataProcessorManager.getEruptDataProcessor(eruptModel.getClazz()) .findDataById(eruptModel, EruptUtil.toEruptId(eruptModel, id.getAsString())))); String eval = operationHandler.exec(list, param, rowOperation.operationParam()); if (StringUtils.isNotBlank(eval)) { return EruptApiModel.successApi(eval); } } return EruptApiModel.successApi(I18nTranslate.$translate("erupt.exec_success"), null); } @GetMapping("/tab/tree/{erupt}/{tabFieldName}") @EruptRouter(authIndex = 3, verifyType = EruptRouter.VerifyType.ERUPT) public Collection findTabTree(@PathVariable("erupt") String eruptName, @PathVariable("tabFieldName") String tabFieldName) { EruptModel eruptModel = EruptCoreService.getErupt(eruptName); // Erupts.powerLegal(eruptModel, powerObject -> powerObject.isViewDetails() || powerObject.isEdit()); EruptModel tabEruptModel = EruptCoreService.getErupt(eruptModel.getEruptFieldMap().get(tabFieldName).getFieldReturnName()); Tree tree = tabEruptModel.getErupt().tree(); EruptFieldModel eruptFieldModel = eruptModel.getEruptFieldMap().get(tabFieldName); EruptQuery eruptQuery = EruptQuery.builder().conditionStrings( Arrays.stream(eruptFieldModel.getEruptField().edit().filter()).map(Filter::value).collect(Collectors.toList()) ).build(); return preEruptDataService.geneTree(tabEruptModel, tree.id(), tree.label(), tree.pid(), tree.rootPid(), eruptQuery); } @GetMapping("/{erupt}/checkbox/{fieldName}") @EruptRouter(authIndex = 1, verifyType = EruptRouter.VerifyType.ERUPT) public Collection> findCheckbox(@PathVariable("erupt") String eruptName, @PathVariable("fieldName") String fieldName) { EruptModel eruptModel = EruptCoreService.getErupt(eruptName); EruptFieldModel eruptFieldModel = eruptModel.getEruptFieldMap().get(fieldName); EruptModel tabEruptModel = EruptCoreService.getErupt(eruptFieldModel.getFieldReturnName()); CheckboxType checkboxType = eruptFieldModel.getEruptField().edit().checkboxType(); List columns = new ArrayList<>(); columns.add(new Column(checkboxType.id(), AnnotationConst.ID)); columns.add(new Column(checkboxType.label(), AnnotationConst.LABEL)); if (!AnnotationConst.EMPTY_STR.equals(checkboxType.remark())) { columns.add(new Column(checkboxType.remark(), AnnotationConst.REMARK)); } EruptQuery eruptQuery = EruptQuery.builder().conditionStrings( Arrays.stream(eruptFieldModel.getEruptField().edit().filter()).map(Filter::value).collect(Collectors.toList()) ).build(); Collection> collection = preEruptDataService.createColumnQuery(tabEruptModel, columns, eruptQuery); Collection> checkboxModels = new ArrayList<>(collection.size()); collection.forEach(map -> checkboxModels.add(new CheckboxModel<>(map.get(AnnotationConst.ID), map.get(AnnotationConst.LABEL), map.get(AnnotationConst.REMARK)))); return checkboxModels; } // REFERENCE API @PostMapping("/{erupt}/reference-table/{fieldName}") @EruptRouter(authIndex = 1, verifyType = EruptRouter.VerifyType.ERUPT) public Page getReferenceTable(@PathVariable("erupt") String eruptName, @PathVariable("fieldName") String fieldName, @RequestParam(value = "dependValue", required = false) Serializable dependValue, @RequestParam(value = "tabRef", required = false) Boolean tabRef, @RequestBody TableQuery tableQuery) { EruptModel eruptModel = EruptCoreService.getErupt(eruptName); EruptFieldModel eruptFieldModel = eruptModel.getEruptFieldMap().get(fieldName); Edit edit = eruptFieldModel.getEruptField().edit(); String dependField = edit.referenceTableType().dependField(); List serverConditions = new ArrayList<>(); List conditions = Arrays.stream(edit.filter()).map(Filter::value).toList(); if (!AnnotationConst.EMPTY_STR.equals(dependField)) { Erupts.requireNonNull(dependValue, I18nTranslate.$translate("erupt.select") + " " + eruptModel.getEruptFieldMap().get(dependField).getEruptField().edit().title()); EruptModel refErupt = EruptCoreService.getErupt(eruptFieldModel.getFieldReturnName()); serverConditions.add(new Condition( eruptFieldModel.getFieldReturnName() + EruptConst.DOT + edit.referenceTableType().dependColumn(), TypeUtil.typeStrConvertObject(dependValue, refErupt.getEruptFieldMap().get(refErupt.getErupt().primaryKeyCol()).getField().getType()), QueryExpression.EQ )); } EruptModel eruptReferenceModel = EruptCoreService.getErupt(eruptFieldModel.getFieldReturnName()); if (!tabRef) { //由于类加载顺序问题,并未选择在启动时检测 ReferenceTableType referenceTableType = eruptFieldModel.getEruptField().edit().referenceTableType(); Erupts.requireTrue(eruptReferenceModel.getEruptFieldMap().containsKey(referenceTableType.label().split("\\.")[0]) , eruptReferenceModel.getEruptName() + " not found '" + referenceTableType.label() + "' field,please use @ReferenceTableType annotation 'label' config"); } return eruptService.getEruptData(eruptReferenceModel, tableQuery, serverConditions, conditions.toArray(new String[0])); } @SneakyThrows @GetMapping("/depend-tree/{erupt}") @EruptRouter(authIndex = 2, verifyType = EruptRouter.VerifyType.ERUPT) public Collection getDependTree(@PathVariable("erupt") String erupt) { EruptModel eruptModel = EruptCoreService.getErupt(erupt); String field = eruptModel.getErupt().linkTree().field(); if (null == eruptModel.getEruptFieldMap().get(field)) { String treeErupt = eruptModel.getClazz().getDeclaredField(field).getType().getSimpleName(); return this.getEruptTreeData(treeErupt); } return this.getReferenceTree(eruptModel.getEruptName(), field, null); } @GetMapping("/{erupt}/reference-tree/{fieldName}") @EruptRouter(authIndex = 1, verifyType = EruptRouter.VerifyType.ERUPT) public Collection getReferenceTree(@PathVariable("erupt") String erupt, @PathVariable("fieldName") String fieldName, @RequestParam(value = "dependValue", required = false) Serializable dependValue) { EruptModel eruptModel = EruptCoreService.getErupt(erupt); EruptFieldModel eruptFieldModel = eruptModel.getEruptFieldMap().get(fieldName); String dependField = eruptFieldModel.getEruptField().edit().referenceTreeType().dependField(); if (!AnnotationConst.EMPTY_STR.equals(dependField)) { Erupts.requireNonNull(dependValue, I18nTranslate.$translate("erupt.select") + " " + eruptModel.getEruptFieldMap().get(dependField).getEruptField().edit().title()); } Edit edit = eruptFieldModel.getEruptField().edit(); ReferenceTreeType treeType = edit.referenceTreeType(); EruptModel referenceEruptModel = EruptCoreService.getErupt(eruptFieldModel.getFieldReturnName()); Erupts.requireTrue(referenceEruptModel.getEruptFieldMap().containsKey(treeType.label().split("\\.")[0]), referenceEruptModel.getEruptName() + " not found " + treeType.label() + " field, please use @ReferenceTreeType annotation config"); List conditions = new ArrayList<>(); // process depend if (StringUtils.isNotBlank(treeType.dependField()) && null != dependValue) { conditions.add(new Condition(edit.referenceTreeType().dependColumn(), dependValue, QueryExpression.EQ)); } List conditionStrings = Arrays.stream(edit.filter()).map(Filter::value).collect(Collectors.toList()); return preEruptDataService.geneTree(referenceEruptModel, treeType.id(), treeType.label(), treeType.pid(), treeType.rootPid(), EruptQuery.builder().sort(Sort.toSortList(edit.orderBy())).conditionStrings(conditionStrings).conditions(conditions).build()); } //自定义行 @PostMapping("/extra-row/{erupt}") @EruptRouter(authIndex = 2, verifyType = EruptRouter.VerifyType.ERUPT) public List extraRow(@PathVariable("erupt") String erupt, @RequestBody TableQuery tableQuery) { List rows = new ArrayList<>(); DataProxyInvoke.invoke(EruptCoreService.getErupt(erupt), dataProxy -> Optional.ofNullable(dataProxy.extraRow(tableQuery.getCondition())).ifPresent(rows::addAll)); return rows; } @PostMapping("/onchange/{erupt}/{field}") @EruptRouter(authIndex = 2, verifyType = EruptRouter.VerifyType.ERUPT) public R onchange(@PathVariable("erupt") String eruptName, @PathVariable("field") String field, @RequestBody JsonObject data) { EruptModel eruptModel = EruptCoreService.getErupt(eruptName); Edit edit = eruptModel.getEruptFieldMap().get(field).getEruptField().edit(); Object o = gson.fromJson(data.toString(), eruptModel.getClazz()); OnChange onChange = EruptSpringUtil.getBean(edit.onchange()); OnChangeVo onChangeVo = new OnChangeVo(); onChangeVo.setFormData(onChange.populateForm(o, edit.onchangeParams())); onChangeVo.setEditExpr(onChange.buildEditExpr(o, edit.onchangeParams())); return R.ok(onChangeVo); } } \ No newline at end of file diff --git a/erupt-core/src/main/java/xyz/erupt/core/service/EruptCoreService.java b/erupt-core/src/main/java/xyz/erupt/core/service/EruptCoreService.java index 13c8d93c6..3e3a75c43 100644 --- a/erupt-core/src/main/java/xyz/erupt/core/service/EruptCoreService.java +++ b/erupt-core/src/main/java/xyz/erupt/core/service/EruptCoreService.java @@ -64,7 +64,7 @@ public static EruptModel getErupt(String eruptName) { } } - //动态注册erupt类 + // 动态注册 erupt 类 public static void registerErupt(Class eruptClazz) { if (ERUPTS.containsKey(eruptClazz.getSimpleName())) { throw new RuntimeException(eruptClazz.getSimpleName() + " conflict !"); @@ -74,7 +74,7 @@ public static void registerErupt(Class eruptClazz) { ERUPT_LIST.add(eruptModel); } - //移除容器内所维护的Erupt + // 移除容器内所维护的 Erupt public static void unregisterErupt(Class eruptClazz) { ERUPTS.remove(eruptClazz.getSimpleName()); ERUPT_LIST.removeIf(model -> model.getEruptName().equals(eruptClazz.getSimpleName())); diff --git a/erupt-core/src/main/java/xyz/erupt/core/util/DateUtil.java b/erupt-core/src/main/java/xyz/erupt/core/util/DateUtil.java index 0929c159e..29e82f5d8 100644 --- a/erupt-core/src/main/java/xyz/erupt/core/util/DateUtil.java +++ b/erupt-core/src/main/java/xyz/erupt/core/util/DateUtil.java @@ -21,7 +21,7 @@ public class DateUtil { public static final String DATE_TIME = "yyyy-MM-dd HH:mm:ss"; - public static final String ISO_8601 = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"; + public static final String ISO_8601 = "yyyy-MM-dd'T'HH:mm:ss.SSS"; private static final String[] PATTERNS = { "yyyy-MM-dd'T'HH:mm:ss", // ISO 8601 specification,T segmentation diff --git a/erupt-tpl-frame/angular/README.md b/erupt-tpl-frame/angular/README.md new file mode 100644 index 000000000..29965d7ee --- /dev/null +++ b/erupt-tpl-frame/angular/README.md @@ -0,0 +1,7 @@ +# erupt-tpl use Angular render + +The front-end part of erupt is written in Angular. +If you need to add custom pages, simply modify the source code in the erupt-web project and add routes. +There is no need to start a new project. + +Reference: https://github.com/erupts/erupt-web diff --git a/erupt-tpl-frame/react/README.md b/erupt-tpl-frame/react/README.md new file mode 100644 index 000000000..956076f4d --- /dev/null +++ b/erupt-tpl-frame/react/README.md @@ -0,0 +1 @@ +# erupt-tpl use react render \ No newline at end of file diff --git a/erupt-tpl-frame/vue/README.md b/erupt-tpl-frame/vue/README.md new file mode 100644 index 000000000..ea182865c --- /dev/null +++ b/erupt-tpl-frame/vue/README.md @@ -0,0 +1 @@ +# erupt-tpl use vue render \ No newline at end of file diff --git a/erupt-tpl/src/main/resources/tpl/jump.tpl b/erupt-tpl/src/main/resources/tpl/jump.tpl new file mode 100644 index 000000000..e69de29bb