aiflowy-commons/aiflowy-common-web/src/main/java/tech/aiflowy/common/web/controller/BaseCurdController.java
@@ -135,13 +135,6 @@ * * @return 所有数据 */ @GetMapping("sysDept") public Result sysDept(M entity, Boolean asTree, String sortKey, String sortType) { QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity)); queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy())); List<M> list = Tree.tryToTree(service.list(queryWrapper), asTree); return Result.success(list); } @GetMapping("list") public Result list(M entity, Boolean asTree, String sortKey, String sortType) { QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity)); aiflowy-modules/aiflowy-module-ai/pom.xml
@@ -64,5 +64,11 @@ <groupId>tech.aiflowy</groupId> <artifactId>aiflowy-module-system</artifactId> </dependency> <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> <version>4.5.14</version> <scope>compile</scope> </dependency> </dependencies> </project> aiflowy-modules/aiflowy-module-ai/src/main/java/tech/aiflowy/ai/config/RestTemplateConfig.java
New file @@ -0,0 +1,14 @@ package tech.aiflowy.ai.config; import cn.hutool.json.ObjectMapper; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.client.RestTemplate; @Configuration public class RestTemplateConfig { @Bean public RestTemplate restTemplate() { return new RestTemplate(); } } aiflowy-modules/aiflowy-module-ai/src/main/java/tech/aiflowy/ai/controller/AiBotController.java
@@ -2,6 +2,8 @@ import cn.dev33.satoken.annotation.SaIgnore; import cn.hutool.core.util.ObjectUtil; import cn.hutool.json.JSONArray; import cn.hutool.json.JSONObject; import com.agentsflex.core.llm.ChatContext; import com.agentsflex.core.llm.Llm; import com.agentsflex.core.llm.StreamResponseListener; @@ -16,24 +18,31 @@ import com.agentsflex.core.util.CollectionUtil; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.serializer.SerializeConfig; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.jfinal.template.stat.ast.Break; import com.mybatisflex.core.paginate.Page; import com.mybatisflex.core.query.QueryWrapper; import com.mybatisflex.core.table.TableInfo; import com.mybatisflex.core.table.TableInfoFactory; import io.milvus.param.R; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.*; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import tech.aiflowy.ai.entity.*; import tech.aiflowy.ai.mapper.AiBotConversationMessageMapper; import tech.aiflowy.ai.service.*; import tech.aiflowy.ai.vo.ModelConfig; import tech.aiflowy.common.ai.ChatManager; import tech.aiflowy.common.ai.MySseEmitter; import tech.aiflowy.common.domain.Result; @@ -48,7 +57,9 @@ import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.math.BigInteger; import java.time.Duration; import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicBoolean; @@ -77,6 +88,8 @@ private AiBotConversationMessageMapper aiBotConversationMessageMapper; private static final Logger logger = LoggerFactory.getLogger(AiBotController.class); @Autowired private RestTemplate restTemplate; public AiBotController(AiBotService service, AiLlmService aiLlmService, AiBotWorkflowService aiBotWorkflowService, AiBotKnowledgeService aiBotKnowledgeService, AiBotMessageService aiBotMessageService) { super(service); @@ -132,6 +145,10 @@ * @param response * @return */ @Autowired private ObjectMapper objectMapper; @PostMapping("chat") public SseEmitter chat(@JsonBody(value = "prompt", required = true) String prompt, @JsonBody(value = "botId", required = true) BigInteger botId, @@ -143,86 +160,77 @@ if (aiBot == null) { return ChatManager.getInstance().sseEmitterForContent("机器人不存在"); } if (StringUtil.hasText(aiBot.getApiEndpoint())){ // 情况1:aiBot自带大模型信息 try { // 从aiBot构建自定义LLM实现 Llm llm = null; if (llm == null) { return ChatManager.getInstance().sseEmitterForContent("LLM获取为空"); } if (StringUtil.hasText(aiBot.getModelAPI())){ // 使用Dify模型的逻辑 String apiUrl = aiBot.getModelAPI(); String bearerToken = aiBot.getModelKey(); AiBotMessageMemory memory = new AiBotMessageMemory(botId, SaTokenUtil.getLoginAccount().getId(), sessionId, isExternalMsg, aiBotMessageService, aiBotConversationMessageMapper, aiBotConversationMessageService); // 构建请求JSON JSONObject jsonBody = new JSONObject(); jsonBody.put("inputs", new JSONObject()); jsonBody.put("query", prompt); jsonBody.put("response_mode", "blocking"); jsonBody.put("conversation_id", ""); jsonBody.put("user", userId.toString()); final HistoriesPrompt historiesPrompt = new HistoriesPrompt(); JSONArray files = new JSONArray(); JSONObject fileObj = new JSONObject(); fileObj.put("type", ""); fileObj.put("transfer_method", ""); fileObj.put("url", ""); files.put(fileObj); jsonBody.put("files", files); historiesPrompt.setMemory(memory); // 发送HTTP请求 HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(30)) .build(); HumanMessage humanMessage = new HumanMessage(prompt); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(apiUrl)) .header("Content-Type", "application/json") .header("Authorization", "Bearer " + bearerToken) .POST(HttpRequest.BodyPublishers.ofString(jsonBody.toString())) .build(); // 添加插件相关的function calling appendPluginToolFunction(botId, humanMessage); HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString()); //添加工作流相关的 Function Calling appendWorkflowFunctions(botId, humanMessage); // 处理成功响应 JSONObject responseJson = new JSONObject(response.body()); //添加知识库相关的 Function Calling appendKnowledgeFunctions(botId, humanMessage); // 保存用户消息到历史记录 ChatHistory userChat = new ChatHistory(); userChat.setContent(prompt); userChat.setRole("user"); userChat.setUserId(userId); userChat.setBotId(botId.intValue()); userChat.setModel("dify"); userChat.setChatId(responseJson.optString("conversation_id", "")); Integer userHistoryId = chatHistoryService.saveChatHistory(userChat); historiesPrompt.addMessage(humanMessage); // 保存AI回复到历史记录 ChatHistory assistantChat = new ChatHistory(); assistantChat.setContent(responseJson.getString("answer")); assistantChat.setRole("assistant"); assistantChat.setUserId(userId); assistantChat.setBotId(botId.intValue()); assistantChat.setModel("dify"); assistantChat.setChatId(userChat.getChatId()); Integer assistantHistoryId = chatHistoryService.saveChatHistory(assistantChat); MySseEmitter emitter = new MySseEmitter((long) (1000 * 60 * 2)); // 构建响应数据 int count = chatHistoryService.getChatHistoryCount(userId, botId.intValue()); JSONObject result = new JSONObject(); result.put("Count", count); result.put("Data", responseJson.getString("answer")); result.put("UserId", userHistoryId); result.put("AssistantId", assistantHistoryId); final Boolean[] needClose = {true}; ServletRequestAttributes sra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); // 统一使用流式处理,无论是否有 Function Calling llm.chatStream(historiesPrompt, new StreamResponseListener() { @Override public void onMessage(ChatContext context, AiMessageResponse response) { try { RequestContextHolder.setRequestAttributes(sra, true); if (response != null) { // 检查是否需要触发 Function Calling if (response.getFunctionCallers() != null && CollectionUtil.hasItems(response.getFunctionCallers())) { needClose[0] = false; function_call(response, emitter, needClose, historiesPrompt, llm, prompt, false); } else { // 强制流式返回,即使有 Function Calling 也先返回部分结果 if (response.getMessage() != null) { String content = response.getMessage().getContent(); if (StringUtil.hasText(content)) { emitter.send(JSON.toJSONString(response.getMessage())); } } } } } catch (Exception e) { emitter.completeWithError(e); } } @Override public void onStop(ChatContext context) { if (needClose[0]) { emitter.complete(); } } @Override public void onFailure(ChatContext context, Throwable throwable) { emitter.completeWithError(throwable); } }); return emitter; } catch (Exception e) { return ChatManager.getInstance().sseEmitterForContent("自定义LLM配置错误"); } // 发送SSE响应 emitter.send(SseEmitter.event() .name("message") .data(result.toString())); emitter.complete(); }else{ Map<String, Object> llmOptions = aiBot.getLlmOptions(); String systemPrompt = llmOptions != null ? (String) llmOptions.get("systemPrompt") : null; aiflowy-modules/aiflowy-module-ai/src/main/java/tech/aiflowy/ai/controller/AiFirstMenuController.java
@@ -28,4 +28,12 @@ AiFirstMenuService aiFirstMenuService; public Result list(AiFirstMenu entity, Boolean asTree, String sortKey, String sortType) { QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity)); queryWrapper.orderBy(AiFirstMenu::getId); List<AiFirstMenu> list = Tree.tryToTree(super.service.list(queryWrapper), asTree); return Result.success(list); } } aiflowy-modules/aiflowy-module-ai/src/main/java/tech/aiflowy/ai/controller/AiSecondMenuController.java
@@ -1,11 +1,23 @@ package tech.aiflowy.ai.controller; import com.mybatisflex.core.paginate.Page; import com.mybatisflex.core.query.QueryWrapper; import com.mybatisflex.core.table.TableInfo; import com.mybatisflex.core.table.TableInfoFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.*; import tech.aiflowy.ai.entity.AiSecondMenu; import tech.aiflowy.ai.service.AiSecondMenuService; import tech.aiflowy.common.domain.Result; import tech.aiflowy.common.entity.LoginAccount; import tech.aiflowy.common.satoken.util.SaTokenUtil; import tech.aiflowy.common.tree.Tree; import tech.aiflowy.common.web.controller.BaseCurdController; import tech.aiflowy.common.web.jsonbody.JsonBody; import javax.servlet.http.HttpServletRequest; import java.util.List; @RestController @RequestMapping("/api/v1/aiMenu/SecondMenu") public class AiSecondMenuController extends BaseCurdController<AiSecondMenuService, AiSecondMenu> { @@ -14,5 +26,16 @@ } @Autowired AiSecondMenuService AiSecondMenuService; AiSecondMenuService aiSecondMenuService; @Override public Result list(AiSecondMenu entity, Boolean asTree, String sortKey, String sortType) { QueryWrapper queryWrapper = QueryWrapper.create(entity, buildOperators(entity)); queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy())); List<AiSecondMenu> list = Tree.tryToTree(aiSecondMenuService.findAll(queryWrapper), asTree); return Result.success(list); } protected Page<AiSecondMenu> queryPage(Page<AiSecondMenu> page, QueryWrapper queryWrapper) { return service.page(page, queryWrapper); } } aiflowy-modules/aiflowy-module-ai/src/main/java/tech/aiflowy/ai/entity/AiSecondMenu.java
@@ -1,7 +1,25 @@ package tech.aiflowy.ai.entity; import com.fasterxml.jackson.annotation.JsonIgnore; import com.mybatisflex.annotation.Column; import com.mybatisflex.annotation.Table; import tech.aiflowy.ai.entity.base.AiSecondMenuBase; import java.math.BigInteger; @Table(value ="ai_second_menu") public class AiSecondMenu extends AiSecondMenuBase { @Column(ignore = true) public String firstMenuName; public String getFirstMenuName() { return firstMenuName; } public void setFirstMenuName(String firstMenuName) { this.firstMenuName = firstMenuName; } } aiflowy-modules/aiflowy-module-ai/src/main/java/tech/aiflowy/ai/entity/base/AiBotBase.java
@@ -94,17 +94,12 @@ @Column(comment = "修改者ID") private BigInteger modifiedBy; @Column(comment = "API品牌") private String apiProvider; @Column(comment = "API") private String modelAPI; @Column(comment = "API地址") private String apiEndpoint; @Column(comment = "KEY") private String modelKey; @Column(comment = "API密钥") private String apiKey; @Column(comment = "API额外配置") private String apiVersion; @Column(comment = "一级菜单编号") private Integer firstMenuId; @@ -120,37 +115,20 @@ this.firstMenuId = firstMenuId; } public String getApiProvider() { return apiProvider; public String getModelAPI() { return modelAPI; } public void setApiProvider(String apiProvider) { this.apiProvider = apiProvider; public void setModelAPI(String modelAPI) { this.modelAPI = modelAPI; } public String getApiEndpoint() { return apiEndpoint; public String getModelKey() { return modelKey; } public void setApiEndpoint(String apiEndpoint) { this.apiEndpoint = apiEndpoint; } public String getApiKey() { return apiKey; } public void setApiKey(String apiKey) { this.apiKey = apiKey; } public String getApiVersion() { return apiVersion; } public void setApiVersion(String apiVersion) { this.apiVersion = apiVersion; public void setModelKey(String modelKey) { this.modelKey = modelKey; } public Integer getSecondMenuId() { aiflowy-modules/aiflowy-module-ai/src/main/java/tech/aiflowy/ai/entity/base/AiSecondMenuBase.java
@@ -22,11 +22,20 @@ @Id(keyType = KeyType.Auto, value = "secondMenuId", comment = "二级菜单编号") private BigInteger id; /** * 一级菜单编号 */ @Column(comment = "一级菜单编号") @Column("first_menu_id") private BigInteger firstMenuId; public BigInteger getFirstMenuId() { return firstMenuId; } public void setFirstMenuId(BigInteger firstMenuId) { this.firstMenuId = firstMenuId; } /** * 二级菜单名称 @@ -40,14 +49,6 @@ public void setId(BigInteger id) { this.id = id; } public BigInteger getFirstMenuId() { return firstMenuId; } public void setFirstMenuId(BigInteger firstMenuId) { this.firstMenuId = firstMenuId; } public String getSecondMenuName() { aiflowy-modules/aiflowy-module-ai/src/main/java/tech/aiflowy/ai/mapper/AiSecondMenuMapper.java
@@ -1,8 +1,12 @@ package tech.aiflowy.ai.mapper; import com.mybatisflex.core.BaseMapper; import com.mybatisflex.core.query.QueryWrapper; import org.apache.ibatis.annotations.Select; import tech.aiflowy.ai.entity.AiSecondMenu; import tech.aiflowy.ai.entity.base.AiSecondMenuBase; import java.math.BigInteger; import java.util.List; /** * @author admin @@ -11,7 +15,8 @@ * @Entity tech.aiflowy.ai.entity.base.AiSecondMenu */ public interface AiSecondMenuMapper extends BaseMapper<AiSecondMenu> { @Select("select first_menu_name from ai_first_menu where id = #{firstMenuId}") String getFMN(BigInteger firstMenuId); } aiflowy-modules/aiflowy-module-ai/src/main/java/tech/aiflowy/ai/service/AiSecondMenuService.java
@@ -1,8 +1,12 @@ package tech.aiflowy.ai.service; import com.mybatisflex.core.paginate.Page; import com.mybatisflex.core.query.QueryWrapper; import com.mybatisflex.core.service.IService; import tech.aiflowy.ai.entity.AiSecondMenu; import tech.aiflowy.ai.entity.base.AiSecondMenuBase; import java.util.List; /** * @author admin @@ -10,5 +14,8 @@ * @createDate 2025-05-28 15:26:35 */ public interface AiSecondMenuService extends IService<AiSecondMenu> { public List<AiSecondMenu> findAll(QueryWrapper query); Page<AiSecondMenu> page(Page<AiSecondMenu> page, QueryWrapper query) ; } aiflowy-modules/aiflowy-module-ai/src/main/java/tech/aiflowy/ai/service/impl/AiSecondMenuServiceImpl.java
@@ -1,10 +1,17 @@ package tech.aiflowy.ai.service.impl; import com.mybatisflex.core.paginate.Page; import com.mybatisflex.core.query.QueryWrapper; import org.checkerframework.checker.units.qual.A; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import tech.aiflowy.ai.entity.AiSecondMenu; import tech.aiflowy.ai.mapper.AiSecondMenuMapper; import tech.aiflowy.ai.service.AiSecondMenuService; import com.mybatisflex.spring.service.impl.ServiceImpl; import java.util.List; /** * @author admin * @description 针对表【ai_second_menu(ai机器人二级菜单表)】的数据库操作Service实现 @@ -13,7 +20,24 @@ @Service public class AiSecondMenuServiceImpl extends ServiceImpl<AiSecondMenuMapper, AiSecondMenu> implements AiSecondMenuService { @Autowired private AiSecondMenuMapper aiSecondMenuMapper; @Override public List<AiSecondMenu> findAll(QueryWrapper query) { List<AiSecondMenu> list = this.list(query); for (AiSecondMenu aiSecondMenu : list) { aiSecondMenu.setFirstMenuName(aiSecondMenuMapper.getFMN(aiSecondMenu.getFirstMenuId())); } return list; } public Page<AiSecondMenu> page(Page<AiSecondMenu> page, QueryWrapper query){ Page page1 = this.<AiSecondMenu>pageAs(page, query, (Class) null); List<AiSecondMenu> records = page1.getRecords(); for (AiSecondMenu record : records) { record.setFirstMenuName(aiSecondMenuMapper.getFMN(record.getFirstMenuId())); } return page1; } } aiflowy-modules/aiflowy-module-ai/src/main/java/tech/aiflowy/ai/vo/ModelConfig.java
New file @@ -0,0 +1,24 @@ package tech.aiflowy.ai.vo; public class ModelConfig { private String modelApi; private String modelKey; // getters and setters public String getModelApi() { return modelApi; } public void setModelApi(String modelApi) { this.modelApi = modelApi; } public String getModelKey() { return modelKey; } public void setModelKey(String modelKey) { this.modelKey = modelKey; } }