From c24d628b39d53e3c8da2e0b668583552d845c897 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E4=B8=80=E5=87=A1?= Date: Mon, 10 Jul 2023 00:22:57 +0800 Subject: [PATCH 01/12] add plugin and fix bugs --- Dockerfile | 17 ++ pom.xml | 7 + .../chat/AbstractGptFunctionHandler.java | 1 - .../com/ai/aigenerate/chat/ChatService.java | 18 +- .../chat/FunctionEventSourceListener.java | 22 ++- .../aigenerate/chat/GptFunctionFactory.java | 37 ++++ .../ai/aigenerate/chat/GptStreamContext.java | 1 - .../chat/custom/BaiduGptFunctionHandler.java | 46 +++++ .../custom/BaiduSearchGptFunctionHandler.java | 47 +++++ .../chat/custom/NewsGptFunctionHandler.java | 8 +- .../chat/custom/WeiboGptFunctionHandler.java | 49 ++++++ .../chat/tool/BaiduSearchService.java | 73 ++++++++ .../aigenerate/chat/tool/ProxyIpService.java | 62 +++++++ .../ai/aigenerate/chat/tool/WeiboService.java | 160 ++++++++++++++++++ .../com/ai/aigenerate/config/BaiduYunKey.java | 4 +- .../aigenerate/config/GptFunctionConfig.java | 6 +- .../com/ai/aigenerate/config/MailConfig.java | 10 +- .../ai/aigenerate/config/ProxyIpConfig.java | 16 ++ .../com/ai/aigenerate/facade/ChatFacade.java | 13 +- .../request/baidu/BaiduSearchRequest.java | 11 ++ .../model/request/chat/ChatRequest.java | 4 + .../model/request/chat/FunctionCurl.java | 17 ++ .../request/chat/FunctionDefinition.java | 12 ++ .../model/request/weibo/WeiboRequest.java | 9 + .../model/response/chat/FunctionResponse.java | 11 ++ .../ai/aigenerate/utils/HttpClientUtils.java | 36 +++- src/test/java/com/ai/aigenerate/ApiTest.java | 144 +++++++++++++++- 27 files changed, 807 insertions(+), 34 deletions(-) create mode 100644 Dockerfile create mode 100644 src/main/java/com/ai/aigenerate/chat/custom/BaiduGptFunctionHandler.java create mode 100644 src/main/java/com/ai/aigenerate/chat/custom/BaiduSearchGptFunctionHandler.java create mode 100644 src/main/java/com/ai/aigenerate/chat/custom/WeiboGptFunctionHandler.java create mode 100644 src/main/java/com/ai/aigenerate/chat/tool/BaiduSearchService.java create mode 100644 src/main/java/com/ai/aigenerate/chat/tool/ProxyIpService.java create mode 100644 src/main/java/com/ai/aigenerate/chat/tool/WeiboService.java create mode 100644 src/main/java/com/ai/aigenerate/config/ProxyIpConfig.java create mode 100644 src/main/java/com/ai/aigenerate/model/request/baidu/BaiduSearchRequest.java create mode 100644 src/main/java/com/ai/aigenerate/model/request/chat/FunctionCurl.java create mode 100644 src/main/java/com/ai/aigenerate/model/request/chat/FunctionDefinition.java create mode 100644 src/main/java/com/ai/aigenerate/model/request/weibo/WeiboRequest.java create mode 100644 src/main/java/com/ai/aigenerate/model/response/chat/FunctionResponse.java diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..5c73df9 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +# 使用官方提供的 OpenJDK 17 镜像作为基础镜像 +FROM adoptopenjdk:17-jdk-hotspot + +# 将当前目录下的所有文件复制到镜像的 /app 目录中 +COPY src/main/Dockerfile /app + +# 设置工作目录 +WORKDIR /app + +FROM maven:3.6.3-jdk-8 AS build +COPY src /usr/src/app/src +COPY pom.xml /usr/src/app + +# 构建项目(根据具体情况选择适当的构建工具和命令) +RUN mvn -f /usr/src/app/pom.xml clean package -DskipTests=true + +ENTRYPOINT [ "sh", "-c", "java -jar /app.jar" ] \ No newline at end of file diff --git a/pom.xml b/pom.xml index 20221a6..207f322 100644 --- a/pom.xml +++ b/pom.xml @@ -42,6 +42,13 @@ ${lombok.version} + + + org.jsoup + jsoup + 1.13.1 + + com.google.guava guava diff --git a/src/main/java/com/ai/aigenerate/chat/AbstractGptFunctionHandler.java b/src/main/java/com/ai/aigenerate/chat/AbstractGptFunctionHandler.java index 8f2092c..f025d0d 100644 --- a/src/main/java/com/ai/aigenerate/chat/AbstractGptFunctionHandler.java +++ b/src/main/java/com/ai/aigenerate/chat/AbstractGptFunctionHandler.java @@ -7,7 +7,6 @@ import lombok.extern.slf4j.Slf4j; import java.util.List; -import java.util.concurrent.CountDownLatch; @Slf4j public abstract class AbstractGptFunctionHandler implements GptFunctionService { diff --git a/src/main/java/com/ai/aigenerate/chat/ChatService.java b/src/main/java/com/ai/aigenerate/chat/ChatService.java index 21a1356..ae84483 100644 --- a/src/main/java/com/ai/aigenerate/chat/ChatService.java +++ b/src/main/java/com/ai/aigenerate/chat/ChatService.java @@ -3,6 +3,7 @@ import com.ai.aigenerate.config.GptFunctionConfig; import com.ai.aigenerate.model.request.chat.ChatRequest; import com.ai.aigenerate.model.response.chat.ChatResponse; +import com.ai.aigenerate.model.response.chat.FunctionResponse; import com.ai.aigenerate.utils.MdcUtil; import com.unfbx.chatgpt.OpenAiClient; import com.unfbx.chatgpt.OpenAiStreamClient; @@ -17,10 +18,10 @@ import okhttp3.logging.HttpLoggingInterceptor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import java.io.IOException; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @@ -84,7 +85,7 @@ public ChatResponse chat(ChatRequest chatRequest){ ChatCompletion chatCompletion = ChatCompletion .builder() .messages(messages) - .maxTokens(chatRequest.getMaxTokens() != null?chatRequest.getMaxTokens():2048) + .maxTokens(chatRequest.getMaxTokens() != null?chatRequest.getMaxTokens():8000) .temperature(chatRequest.getTemperature() != null?chatRequest.getTemperature():0.2) .topP(chatRequest.getTopP() != null?chatRequest.getTopP():1.0) .n(chatRequest.getN() != null?chatRequest.getN():1) @@ -160,7 +161,7 @@ public SseEmitter createSse(String requestId) { } ); try { - sseEmitter.send(SseEmitter.event().reconnectTime(5000)); + sseEmitter.send(SseEmitter.event()); } catch (IOException e) { e.printStackTrace(); } @@ -188,7 +189,7 @@ public void chatStream(ChatRequest chatRequest, SseEmitter sseEmitter){ .n(chatRequest.getN() != null?chatRequest.getN():1) .model(chatRequest.getModel() != null?chatRequest.getModel() : ChatCompletion.Model.GPT_3_5_TURBO_16K_0613.getName()) .build(); - if (chatRequest.getIsFunction()) { + if (chatRequest.getIsFunction() && !CollectionUtils.isEmpty(chatRequest.getFunctionNameList())) { chatCompletion.setFunctions(gptFunctionFactory.getFunctionsByFunctionNameList(chatRequest.getFunctionNameList())); chatCompletion.setFunctionCall("auto"); } @@ -232,8 +233,13 @@ public void doStreamFunction(ChatChoice chatChoice){ doStreamFunction(chatChoiceResult); } - public List queryFunctionNameList(){ - return gptFunctionFactory.getFunctions().stream().map(Functions::getName).collect(Collectors.toList()); + public List queryFunctionNameList(){ + return gptFunctionFactory.getFunctions().stream().map(gptFunction -> { + FunctionResponse functionResponse = new FunctionResponse(); + functionResponse.setFunctionName(gptFunction.getName()); + functionResponse.setFunctionDefinition(gptFunction.getDescription()); + return functionResponse; + }).collect(Collectors.toList()); } } \ No newline at end of file diff --git a/src/main/java/com/ai/aigenerate/chat/FunctionEventSourceListener.java b/src/main/java/com/ai/aigenerate/chat/FunctionEventSourceListener.java index 2326d4e..4f12564 100644 --- a/src/main/java/com/ai/aigenerate/chat/FunctionEventSourceListener.java +++ b/src/main/java/com/ai/aigenerate/chat/FunctionEventSourceListener.java @@ -27,10 +27,13 @@ public class FunctionEventSourceListener extends EventSourceListener { private ChatChoice chatChoice; + private Boolean isResponse; + public FunctionEventSourceListener(SseEmitter sseEmitter) { this.countDownLatch = new CountDownLatch(1); this.sseEmitter = sseEmitter; chatChoice = null; + isResponse = false; } @Override @@ -46,10 +49,10 @@ public void onEvent(EventSource eventSource, String id, String type, String data log.info("OpenAI返回数据:{}", data); if (data.equals("[DONE]")) { log.info("OpenAI返回数据结束了"); - sseEmitter.send(SseEmitter.event() - .id("[DONE]") - .data("[DONE]") - .reconnectTime(3000)); + if (isResponse) { + sseEmitter.send(SseEmitter.event() + .data("[DONE]")); + } countDownLatch.countDown(); log.info("OpenAI返回数据结束了"); return; @@ -64,16 +67,17 @@ public void onEvent(EventSource eventSource, String id, String type, String data chatChoice.getDelta().getFunctionCall().setArguments(args); } }else { - try { - sseEmitter.send(SseEmitter.event() - .id(chatCompletionResponse.getId()) - .data(data) - .reconnectTime(3000)); + if (chatCompletionResponse.getChoices().get(0).getDelta().getContent() != null) { + isResponse = true; + try { + sseEmitter.send(SseEmitter.event() + .data(data)); } catch (Exception e) { log.error("sse信息推送失败!"); eventSource.cancel(); e.printStackTrace(); } + } } } diff --git a/src/main/java/com/ai/aigenerate/chat/GptFunctionFactory.java b/src/main/java/com/ai/aigenerate/chat/GptFunctionFactory.java index 89fffd3..763bca8 100644 --- a/src/main/java/com/ai/aigenerate/chat/GptFunctionFactory.java +++ b/src/main/java/com/ai/aigenerate/chat/GptFunctionFactory.java @@ -1,5 +1,9 @@ package com.ai.aigenerate.chat; +import com.ai.aigenerate.model.request.chat.FunctionDefinition; +import com.ai.aigenerate.utils.HttpClientUtils; +import com.ai.aigenerate.utils.MdcUtil; +import com.alibaba.fastjson.JSON; import com.unfbx.chatgpt.entity.chat.Functions; import jakarta.annotation.PostConstruct; import org.springframework.beans.factory.annotation.Autowired; @@ -9,6 +13,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; @Component public class GptFunctionFactory { @@ -18,6 +23,8 @@ public class GptFunctionFactory { private Map gptFunctionServiceMap; + private Map> tempReqFunctionServiceMap = new ConcurrentHashMap<>(); + private List functions; @PostConstruct @@ -45,4 +52,34 @@ public List getFunctionsByFunctionNameList(List functionNameL public GptFunctionService getGptFunctionService(String functionName){ return gptFunctionServiceMap.get(functionName); } + + public List getGptFunctionServices(List functionDefinitions){ + List gptFunctionServices = new ArrayList<>(functionDefinitions.size()); + Map tempFunctionServiceMap = new HashMap<>(functionDefinitions.size()); + for (FunctionDefinition functionDefinition : functionDefinitions) { + GptFunctionService tempService = new AbstractGptFunctionHandler<>() { + @Override + public String doHandle(String paramJson) { + if ("post".equals(functionDefinition.getFunctionCurl().getType())) { + return HttpClientUtils.httpPost(functionDefinition.getFunctionCurl().getUrl(), paramJson).toJSONString(); + }else { + Map param = JSON.parseObject(paramJson, Map.class); + return HttpClientUtils.httpGet(functionDefinition.getFunctionCurl().getUrl(), param).toJSONString(); + } + } + @Override + public Functions getFunction() { + return functionDefinition.getFunctions(); + } + }; + tempFunctionServiceMap.put(functionDefinition.getFunctions().getName(),tempService); + gptFunctionServices.add(tempService); + } + tempReqFunctionServiceMap.put(MdcUtil.getTraceId(),tempFunctionServiceMap); + return gptFunctionServices; + } + + public GptFunctionService getGptFunctionServiceByTraceId(String functionName){ + return tempReqFunctionServiceMap.get(MdcUtil.getTraceId()).get(functionName); + } } diff --git a/src/main/java/com/ai/aigenerate/chat/GptStreamContext.java b/src/main/java/com/ai/aigenerate/chat/GptStreamContext.java index 0e63c86..9ac4abe 100644 --- a/src/main/java/com/ai/aigenerate/chat/GptStreamContext.java +++ b/src/main/java/com/ai/aigenerate/chat/GptStreamContext.java @@ -1,6 +1,5 @@ package com.ai.aigenerate.chat; -import com.unfbx.chatgpt.OpenAiClient; import com.unfbx.chatgpt.OpenAiStreamClient; import com.unfbx.chatgpt.entity.chat.ChatCompletion; import com.unfbx.chatgpt.entity.chat.Message; diff --git a/src/main/java/com/ai/aigenerate/chat/custom/BaiduGptFunctionHandler.java b/src/main/java/com/ai/aigenerate/chat/custom/BaiduGptFunctionHandler.java new file mode 100644 index 0000000..d331f97 --- /dev/null +++ b/src/main/java/com/ai/aigenerate/chat/custom/BaiduGptFunctionHandler.java @@ -0,0 +1,46 @@ +package com.ai.aigenerate.chat.custom; + +import com.ai.aigenerate.chat.AbstractGptFunctionHandler; +import com.ai.aigenerate.model.request.baidu.BaiduSearchRequest; +import com.ai.aigenerate.utils.HttpClientUtils; +import com.alibaba.fastjson.JSONObject; +import com.alibaba.fastjson2.JSON; +import com.unfbx.chatgpt.entity.chat.Functions; +import com.unfbx.chatgpt.entity.chat.Parameters; +import org.springframework.stereotype.Component; + +import java.util.Arrays; + +@Component +public class BaiduGptFunctionHandler extends AbstractGptFunctionHandler { + + + @Override + public String doHandle(String paramJson) { + BaiduSearchRequest baiduSearchRequest = JSON.parseObject(paramJson, BaiduSearchRequest.class); + String key = baiduSearchRequest.getKeyword().replace(" ",""); + JSONObject jsonObject = HttpClientUtils.httpGet("https://baike.baidu.com/api/openapi/BaikeLemmaCardApi?scope=103&format=json&appid=379020&bk_key="+key+"&bk_length=600"); + return jsonObject.toJSONString(); + } + + @Override + public Functions getFunction() { + cn.hutool.json.JSONObject keyword = new cn.hutool.json.JSONObject(); + keyword.putOpt("type", "string"); + keyword.putOpt("description", "查询的关键字,参数中不允许出现空格"); + + //参数 + cn.hutool.json.JSONObject properties = new cn.hutool.json.JSONObject(); + properties.putOpt("keyword", keyword); + Parameters parameters = Parameters.builder() + .type("object") + .properties(properties) + .required(Arrays.asList("keyword")).build(); + Functions functions = Functions.builder() + .name("baiduBaikeSearch") + .description("百度百科搜索,关键字不允许出现空格,搜索结果以json格式返回") + .parameters(parameters) + .build(); + return functions; + } +} diff --git a/src/main/java/com/ai/aigenerate/chat/custom/BaiduSearchGptFunctionHandler.java b/src/main/java/com/ai/aigenerate/chat/custom/BaiduSearchGptFunctionHandler.java new file mode 100644 index 0000000..97095f5 --- /dev/null +++ b/src/main/java/com/ai/aigenerate/chat/custom/BaiduSearchGptFunctionHandler.java @@ -0,0 +1,47 @@ +package com.ai.aigenerate.chat.custom; + + +import cn.hutool.json.JSONObject; +import com.ai.aigenerate.chat.AbstractGptFunctionHandler; +import com.ai.aigenerate.chat.tool.BaiduSearchService; +import com.ai.aigenerate.model.request.baidu.BaiduSearchRequest; +import com.alibaba.fastjson2.JSON; +import com.unfbx.chatgpt.entity.chat.Functions; +import com.unfbx.chatgpt.entity.chat.Parameters; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import java.util.Arrays; + +@Component +public class BaiduSearchGptFunctionHandler extends AbstractGptFunctionHandler { + + @Autowired + private BaiduSearchService baiduSearchService; + + @Override + public String doHandle(String paramJson) { + BaiduSearchRequest baiduSearchRequest = JSON.parseObject(paramJson, BaiduSearchRequest.class); + return baiduSearchService.getBaiduSearchResult(baiduSearchRequest.getKeyword()); + } + + @Override + public Functions getFunction() { + JSONObject keyword = new JSONObject(); + keyword.putOpt("type", "string"); + keyword.putOpt("description", "查询的关键字,参数中不允许出现空格"); + + //参数 + JSONObject properties = new cn.hutool.json.JSONObject(); + properties.putOpt("keyword", keyword); + Parameters parameters = Parameters.builder() + .type("object") + .properties(properties) + .required(Arrays.asList("keyword")).build(); + Functions functions = Functions.builder() + .name("baiduSearch") + .description("通过百度进行搜索,关键字不允许出现空格,搜索结果以json格式返回") + .parameters(parameters) + .build(); + return functions; + } +} diff --git a/src/main/java/com/ai/aigenerate/chat/custom/NewsGptFunctionHandler.java b/src/main/java/com/ai/aigenerate/chat/custom/NewsGptFunctionHandler.java index 39bdf21..5a2cdaf 100644 --- a/src/main/java/com/ai/aigenerate/chat/custom/NewsGptFunctionHandler.java +++ b/src/main/java/com/ai/aigenerate/chat/custom/NewsGptFunctionHandler.java @@ -1,5 +1,6 @@ package com.ai.aigenerate.chat.custom; +import cn.hutool.json.JSONObject; import com.ai.aigenerate.chat.AbstractGptFunctionHandler; import com.ai.aigenerate.model.request.news.NewsRequest; import com.ai.aigenerate.chat.tool.NewsService; @@ -8,7 +9,6 @@ import com.unfbx.chatgpt.entity.chat.Parameters; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; - import java.util.Arrays; @Component @@ -25,18 +25,18 @@ public String doHandle(String paramJson) { @Override public Functions getFunction() { - cn.hutool.json.JSONObject newsType = new cn.hutool.json.JSONObject(); + JSONObject newsType = new JSONObject(); newsType.putOpt("type", "string"); newsType.putOpt("enum",Arrays.asList("top","guonei","guoji","yule","tiyu","junshi","keji","caijing","youxi","qiche","jiankang")); newsType.putOpt("description", "新闻类型, 默认top"); - cn.hutool.json.JSONObject page = new cn.hutool.json.JSONObject(); + JSONObject page = new JSONObject(); page.putOpt("type", "integer"); page.putOpt("description", "当前页数, 默认1, 最大50"); cn.hutool.json.JSONObject size = new cn.hutool.json.JSONObject(); size.putOpt("type", "integer"); size.putOpt("description", "每页返回条数, 默认30 , 最大30"); //参数 - cn.hutool.json.JSONObject properties = new cn.hutool.json.JSONObject(); + JSONObject properties = new JSONObject(); properties.putOpt("type", newsType); properties.putOpt("page", page); properties.putOpt("size", size); diff --git a/src/main/java/com/ai/aigenerate/chat/custom/WeiboGptFunctionHandler.java b/src/main/java/com/ai/aigenerate/chat/custom/WeiboGptFunctionHandler.java new file mode 100644 index 0000000..f5c8d7a --- /dev/null +++ b/src/main/java/com/ai/aigenerate/chat/custom/WeiboGptFunctionHandler.java @@ -0,0 +1,49 @@ +package com.ai.aigenerate.chat.custom; + + +import com.ai.aigenerate.chat.AbstractGptFunctionHandler; +import com.ai.aigenerate.chat.tool.WeiboService; +import com.ai.aigenerate.model.request.weibo.WeiboRequest; +import com.alibaba.fastjson2.JSON; +import com.unfbx.chatgpt.entity.chat.Functions; +import com.unfbx.chatgpt.entity.chat.Parameters; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.Arrays; + +@Component +public class WeiboGptFunctionHandler extends AbstractGptFunctionHandler { + + @Autowired + private WeiboService weiboService; + + @Override + public String doHandle(String paramJson) { + WeiboRequest weiboRequest = JSON.parseObject(paramJson, WeiboRequest.class); + String result = weiboService.getWeiboResult(weiboRequest.getType()); + return result; + } + + @Override + public Functions getFunction() { + cn.hutool.json.JSONObject type = new cn.hutool.json.JSONObject(); + type.putOpt("type", "string"); + type.putOpt("description", "热榜的类型,可选值:hotSearch(实时热搜榜)、topic(话题榜)、importantNews(要闻榜)、movie(电影榜)、entertainment(文娱榜)"); + type.putOpt("enum",Arrays.asList("hotSearch","topic","importantNews","movie","entertainment")); + + //参数 + cn.hutool.json.JSONObject properties = new cn.hutool.json.JSONObject(); + properties.putOpt("type", type); + Parameters parameters = Parameters.builder() + .type("object") + .properties(properties) + .required(Arrays.asList("num")).build(); + Functions functions = Functions.builder() + .name("weiboHotSearch") + .description("根据描述的类型获取微博热榜数据") + .parameters(parameters) + .build(); + return functions; + } +} diff --git a/src/main/java/com/ai/aigenerate/chat/tool/BaiduSearchService.java b/src/main/java/com/ai/aigenerate/chat/tool/BaiduSearchService.java new file mode 100644 index 0000000..ef8e751 --- /dev/null +++ b/src/main/java/com/ai/aigenerate/chat/tool/BaiduSearchService.java @@ -0,0 +1,73 @@ +package com.ai.aigenerate.chat.tool; + +import com.ai.aigenerate.utils.HttpClientUtils; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.select.Elements; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import java.net.Proxy; + + +@Slf4j +@Service +public class BaiduSearchService { + + @Autowired + private ProxyIpService proxyIpService; + + public String getBaiduSearchResult(String keyword) { + String jsonResults = ""; + int getIpCount = 0; + for (int i = 0; i < 3; i++) { + if (StringUtils.isNotBlank(jsonResults) && !jsonResults.equals("[]")) { + break; + } + try { + if (jsonResults.equals("[]")){ + proxyIpService.clearProxyIpCache(); + getIpCount++; + } + Proxy proxyCache = proxyIpService.getProxyIpCache(); + // 发送GET请求 + + Document document = Jsoup.connect("https://www.baidu.com/s?wd=" + keyword).timeout(40000).proxy(proxyCache).get(); + + // 解析返回结果 + Elements results = document.select("div.result"); + + // 创建JSON数组 + JSONArray jsonArray = new JSONArray(); + + // 遍历每个搜索结果 + for (Element result : results) { + // 提取标题和URL + String title = result.select("h3").first().text(); + String url = result.select("h3 a").first().attr("href"); + // 创建JSON对象 + JSONObject jsonObject = new JSONObject(); + jsonObject.put("title", title); + jsonObject.put("url", url); + jsonObject.put("content", result.text()); + // 将JSON对象添加到数组中 + jsonArray.add(jsonObject); + } + + // 将JSON数组转换为字符串 + jsonResults = jsonArray.toJSONString(); + log.info("获取IP次数{},IP信息{},爬取结果{}",getIpCount,proxyCache,jsonResults); + } catch (Exception e) { + jsonResults = "[]"; + log.error("获取百度搜索结果异常",e); + } + + } + log.info("success:",jsonResults); + return jsonResults; + } +} diff --git a/src/main/java/com/ai/aigenerate/chat/tool/ProxyIpService.java b/src/main/java/com/ai/aigenerate/chat/tool/ProxyIpService.java new file mode 100644 index 0000000..4ebd2eb --- /dev/null +++ b/src/main/java/com/ai/aigenerate/chat/tool/ProxyIpService.java @@ -0,0 +1,62 @@ +package com.ai.aigenerate.chat.tool; + +import com.ai.aigenerate.config.ProxyIpConfig; +import com.ai.aigenerate.utils.HttpClientUtils; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import lombok.SneakyThrows; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +@Component +public class ProxyIpService { + + @Autowired + private ProxyIpConfig proxyIpConfig; + + private final LoadingCache proxyIpCache; + + public ProxyIpService() { + // 初始化天气缓存 + proxyIpCache = CacheBuilder.newBuilder() + // 指定缓存最大容量为1000个domain + .maximumSize(2) + // 缓存项在1小时后过期 + .expireAfterWrite(1, TimeUnit.HOURS) + // 指定缓存加载器 + .build(new CacheLoader() { + @Override + public Proxy load(String key) throws Exception { + // 如果缓存未命中,则需要重新创建名单 + return getProxyIp(); + } + }); + } + + public Proxy getProxyIp() { + Map map = new HashMap(); + map.put("signature", proxyIpConfig.getSignature()); + map.put("secret_id", proxyIpConfig.getSecretId()); + map.put("num", 1); + String ipResult = HttpClientUtils.httpGetString("https://dps.kdlapi.com/api/getdps", map); + String[] split = ipResult.split(":"); + Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(split[0], Integer.parseInt(split[1]))); + return proxy; + } + + @SneakyThrows + public synchronized Proxy getProxyIpCache() { + return proxyIpCache.get("国内"); + } + + public synchronized void clearProxyIpCache() { + proxyIpCache.refresh("国内"); + } + +} diff --git a/src/main/java/com/ai/aigenerate/chat/tool/WeiboService.java b/src/main/java/com/ai/aigenerate/chat/tool/WeiboService.java new file mode 100644 index 0000000..3f9aed8 --- /dev/null +++ b/src/main/java/com/ai/aigenerate/chat/tool/WeiboService.java @@ -0,0 +1,160 @@ +package com.ai.aigenerate.chat.tool; + +import com.ai.aigenerate.utils.HttpClientUtils; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.apache.http.HttpEntity; +import org.apache.http.HttpHost; +import org.apache.http.HttpStatus; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.util.EntityUtils; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.select.Elements; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + + +@Slf4j +@Component +public class WeiboService { + + @Autowired + private ProxyIpService proxyIpService; + + private static Map typeMap = new HashMap<>(); + + private final LoadingCache weiboCache; + + public WeiboService() { + // 初始化天气缓存 + weiboCache = CacheBuilder.newBuilder() + // 指定缓存最大容量为1000个domain + .maximumSize(10) + // 缓存项在1小时后过期 + .expireAfterWrite(1, TimeUnit.HOURS) + // 指定缓存加载器 + .build(new CacheLoader() { + @Override + public String load(String type) throws Exception { + // 如果缓存未命中,则需要重新创建名单 + String result = queryWeiboResult(type); + return result; + } + }); + } + + static { + typeMap.put("hotSearch", "https://tophub.today/n/KqndgxeLl9"); + typeMap.put("topic", "https://tophub.today/n/VaobJ98oAj"); + typeMap.put("importantNews", "https://tophub.today/n/Om4ejl3vxE"); + typeMap.put("movie", "https://tophub.today/n/DOvnNXqvEB"); + typeMap.put("entertainment", "https://tophub.today/n/3QeLwJEd7k"); + } + + @SneakyThrows + public String getWeiboResult(String type){ + String result = weiboCache.get(type); + if (StringUtils.isNotBlank(result)) { + return result; + }else { + weiboCache.refresh(type); + return weiboCache.get(type); + } + } + + private String queryWeiboResult(String type) { + JSONArray jsonArray = new JSONArray(); + for (int count = 0; count < 3; count++) { + if (jsonArray.size() > 0) { + break; + } + CloseableHttpClient httpClient = HttpClients.createDefault(); + CloseableHttpResponse response = null; + // 2.创建get请求,相当于在浏览器地址栏输入 网址 + HttpGet request = new HttpGet(typeMap.get(type)); + // 设置请求头,将爬虫伪装成浏览器 + request.setHeader("User-Agent", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36"); + //如果有ip代理,可以加上如下代码 + Proxy proxy = proxyIpService.getProxyIpCache(); + InetSocketAddress inetSocketAddress = (InetSocketAddress) proxy.address(); + HttpHost host = new HttpHost(inetSocketAddress.getHostName(), inetSocketAddress.getPort()); + RequestConfig config = RequestConfig.custom().setProxy(host).setConnectTimeout(5000).build(); + request.setConfig(config); + try { + // 3.执行get请求,相当于在输入地址栏后敲回车键 + response = httpClient.execute(request); + + // 4.判断响应状态为200,进行处理 + if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) { + // 5.获取响应内容 + HttpEntity httpEntity = response.getEntity(); + String html = EntityUtils.toString(httpEntity, "utf-8"); + // 6.Jsoup解析html + Document document = Jsoup.parse(html); + // 像js一样,通过标签获取title + Element item = document.getElementsByTag("tbody").first(); + if (item == null) { + proxyIpService.clearProxyIpCache(); + return "获取失败"; + } + Elements items = item.getElementsByTag("tr"); + int i = 0; + int topCount = 50; + for (Element tmp : items) { + Element rankEle = tmp.getElementsByTag("td").first(); + Elements textEle = tmp.select(".al").select("a"); + JSONObject jsonObject = new JSONObject(); + //String herf = textEle.select("a").attr("href"); + Elements td2 = items.get(i).getElementsByTag("td").next().next(); + String td2Text = td2.text(); + i++; + if (jsonArray.size() >= topCount) { + break; + } + jsonObject.put("序号", rankEle.text()); + String title = textEle.text().replaceAll(" ", "%20"); + jsonObject.put("标题", textEle.text()); + jsonObject.put("链接地址", "https://s.weibo.com/weibo?q=%23" + title + "%23"); + //1. 可以在中括号内加上任何想要删除的字符,实际上是一个正则表达式 + String regExp = "[\n`~!@#$%^&*()+=|{}':;',\\[\\]<>/?~!@#¥%……&*()——+|{}【】‘;:”“’。, 、?\uE652]"; + //2. 这里是将特殊字符换为空字符串,""代表直接去掉 + String replace = ""; + //3. 要处理的字符串 + td2Text = td2Text.replaceAll(regExp, replace); + jsonObject.put("热度", td2Text); + jsonArray.add(jsonObject); + System.out.println(jsonObject); + } + } else { + // 如果返回状态不是200,比如404(页面不存在)等,根据情况做处理,这里略 + System.out.println("返回状态不是200"); + System.out.println(EntityUtils.toString(response.getEntity(), "utf-8")); + proxyIpService.clearProxyIpCache(); + } + log.info("代理ip:{},获取次数:{},获取结果:{}", proxy, count ,jsonArray.toJSONString()); + } catch (Exception e) { + log.error("获取微博热搜失败", e); + proxyIpService.clearProxyIpCache(); + } + } + return jsonArray.toJSONString(); + } +} diff --git a/src/main/java/com/ai/aigenerate/config/BaiduYunKey.java b/src/main/java/com/ai/aigenerate/config/BaiduYunKey.java index 7de035e..8b1bc38 100644 --- a/src/main/java/com/ai/aigenerate/config/BaiduYunKey.java +++ b/src/main/java/com/ai/aigenerate/config/BaiduYunKey.java @@ -8,9 +8,9 @@ @Component public class BaiduYunKey { - @Value("${baidu.weather.accessKey}") + @Value("${baidu.weather.accessKey:}") private String weatherAccessKey; - @Value("${baidu.weather.secretKey}") + @Value("${baidu.weather.secretKey:}") private String weatherSecretKey; } diff --git a/src/main/java/com/ai/aigenerate/config/GptFunctionConfig.java b/src/main/java/com/ai/aigenerate/config/GptFunctionConfig.java index b3d6d4e..99fb7e3 100644 --- a/src/main/java/com/ai/aigenerate/config/GptFunctionConfig.java +++ b/src/main/java/com/ai/aigenerate/config/GptFunctionConfig.java @@ -10,13 +10,13 @@ @Component public class GptFunctionConfig { - @Value("${mj.service.url}") + @Value("${mj.service.url:}") private String mjServiceUrl; - @Value("${mj.service.waitTime:12000}") + @Value("${mj.service.waitTime:90000}") private Integer mjServiceWaitTime; - @Value("${chatgpt.api.key}") + @Value("${chatgpt.api.key:}") private List chatgptApiKey; } diff --git a/src/main/java/com/ai/aigenerate/config/MailConfig.java b/src/main/java/com/ai/aigenerate/config/MailConfig.java index 4dbce4d..3990a54 100644 --- a/src/main/java/com/ai/aigenerate/config/MailConfig.java +++ b/src/main/java/com/ai/aigenerate/config/MailConfig.java @@ -8,18 +8,18 @@ @Component public class MailConfig { - @Value("${mail.password}") + @Value("${mail.password:}") private String password; - @Value("${mail.port}") + @Value("${mail.port:}") private String port; - @Value("${mail.username}") + @Value("${mail.username:}") private String username; - @Value("${mail.host}") + @Value("${mail.host:}") private String host; - @Value("${mail.subject}") + @Value("${mail.subject:}") private String subject; } diff --git a/src/main/java/com/ai/aigenerate/config/ProxyIpConfig.java b/src/main/java/com/ai/aigenerate/config/ProxyIpConfig.java new file mode 100644 index 0000000..53375ab --- /dev/null +++ b/src/main/java/com/ai/aigenerate/config/ProxyIpConfig.java @@ -0,0 +1,16 @@ +package com.ai.aigenerate.config; + +import lombok.Getter; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Getter +@Component +public class ProxyIpConfig { + + @Value("${proxy.ip.signature:}") + private String signature; + + @Value("${proxy.ip.secretId:}") + private String secretId; +} diff --git a/src/main/java/com/ai/aigenerate/facade/ChatFacade.java b/src/main/java/com/ai/aigenerate/facade/ChatFacade.java index 85a9c08..f5decee 100644 --- a/src/main/java/com/ai/aigenerate/facade/ChatFacade.java +++ b/src/main/java/com/ai/aigenerate/facade/ChatFacade.java @@ -3,8 +3,10 @@ import com.ai.aigenerate.model.request.chat.ChatRequest; import com.ai.aigenerate.chat.ChatService; import com.ai.aigenerate.model.response.chat.ChatResponse; +import com.ai.aigenerate.model.response.chat.FunctionResponse; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -25,13 +27,22 @@ public class ChatFacade { @Qualifier("streamThreadPool") private Executor executor; + @Value("${chatgpt.api.token:}") + private String token; + @PostMapping("chat") public ChatResponse chat(@RequestBody ChatRequest chatRequest){ + if (chatRequest.getToken() == null || !chatRequest.getToken().equals(token)){ + throw new RuntimeException("token error"); + } return chatService.chat(chatRequest); } @PostMapping("chatStream") public SseEmitter queryTask(@RequestBody ChatRequest chatRequest){ + if (chatRequest.getToken() == null || !chatRequest.getToken().equals(token)){ + throw new RuntimeException("token error"); + } SseEmitter sseEmitter = chatService.createSse(chatRequest.getRequestId()); executor.execute(() -> { chatService.chatStream(chatRequest,sseEmitter); @@ -40,7 +51,7 @@ public SseEmitter queryTask(@RequestBody ChatRequest chatRequest){ } @GetMapping("queryFunction") - public List queryFunction(){ + public List queryFunction(){ return chatService.queryFunctionNameList(); } } diff --git a/src/main/java/com/ai/aigenerate/model/request/baidu/BaiduSearchRequest.java b/src/main/java/com/ai/aigenerate/model/request/baidu/BaiduSearchRequest.java new file mode 100644 index 0000000..4c2de69 --- /dev/null +++ b/src/main/java/com/ai/aigenerate/model/request/baidu/BaiduSearchRequest.java @@ -0,0 +1,11 @@ +package com.ai.aigenerate.model.request.baidu; + +import lombok.Data; + +@Data +public class BaiduSearchRequest { + + private String keyword; + + private String length; +} diff --git a/src/main/java/com/ai/aigenerate/model/request/chat/ChatRequest.java b/src/main/java/com/ai/aigenerate/model/request/chat/ChatRequest.java index 44241fe..babeec5 100644 --- a/src/main/java/com/ai/aigenerate/model/request/chat/ChatRequest.java +++ b/src/main/java/com/ai/aigenerate/model/request/chat/ChatRequest.java @@ -27,4 +27,8 @@ public class ChatRequest { private List functionNameList; + private List functionDefinitionList; + + private String token; + } diff --git a/src/main/java/com/ai/aigenerate/model/request/chat/FunctionCurl.java b/src/main/java/com/ai/aigenerate/model/request/chat/FunctionCurl.java new file mode 100644 index 0000000..d187fcc --- /dev/null +++ b/src/main/java/com/ai/aigenerate/model/request/chat/FunctionCurl.java @@ -0,0 +1,17 @@ +package com.ai.aigenerate.model.request.chat; + +import lombok.Data; + +@Data +public class FunctionCurl { + + /** + * url + */ + private String url; + + /** + * type: post/get + */ + private String type; +} diff --git a/src/main/java/com/ai/aigenerate/model/request/chat/FunctionDefinition.java b/src/main/java/com/ai/aigenerate/model/request/chat/FunctionDefinition.java new file mode 100644 index 0000000..24aa8eb --- /dev/null +++ b/src/main/java/com/ai/aigenerate/model/request/chat/FunctionDefinition.java @@ -0,0 +1,12 @@ +package com.ai.aigenerate.model.request.chat; + +import com.unfbx.chatgpt.entity.chat.Functions; +import lombok.Data; + +@Data +public class FunctionDefinition { + + private Functions functions; + + private FunctionCurl functionCurl; +} diff --git a/src/main/java/com/ai/aigenerate/model/request/weibo/WeiboRequest.java b/src/main/java/com/ai/aigenerate/model/request/weibo/WeiboRequest.java new file mode 100644 index 0000000..75c2744 --- /dev/null +++ b/src/main/java/com/ai/aigenerate/model/request/weibo/WeiboRequest.java @@ -0,0 +1,9 @@ +package com.ai.aigenerate.model.request.weibo; + +import lombok.Data; + +@Data +public class WeiboRequest { + + private String type; +} diff --git a/src/main/java/com/ai/aigenerate/model/response/chat/FunctionResponse.java b/src/main/java/com/ai/aigenerate/model/response/chat/FunctionResponse.java new file mode 100644 index 0000000..dbad115 --- /dev/null +++ b/src/main/java/com/ai/aigenerate/model/response/chat/FunctionResponse.java @@ -0,0 +1,11 @@ +package com.ai.aigenerate.model.response.chat; + +import lombok.Data; + +@Data +public class FunctionResponse { + + private String functionName; + + private String functionDefinition; +} diff --git a/src/main/java/com/ai/aigenerate/utils/HttpClientUtils.java b/src/main/java/com/ai/aigenerate/utils/HttpClientUtils.java index 3a70947..dba3f8d 100644 --- a/src/main/java/com/ai/aigenerate/utils/HttpClientUtils.java +++ b/src/main/java/com/ai/aigenerate/utils/HttpClientUtils.java @@ -29,7 +29,7 @@ public class HttpClientUtils { static { // 设置请求和传输超时时间 - requestConfig = RequestConfig.custom().setSocketTimeout(2000).setConnectTimeout(2000).build(); + requestConfig = RequestConfig.custom().setSocketTimeout(30000).setConnectTimeout(30000).build(); } /** @@ -123,6 +123,12 @@ public static JSONObject httpGet(String url,Map paramMap){ return httpGet(url+"?"+paramsStr); } + public static String httpGetString(String url,Map paramMap){ + String paramsStr = urlencode(paramMap); + return httpGetString(url+"?"+paramsStr); + } + + /** * 发送get请求 * @@ -157,6 +163,34 @@ public static JSONObject httpGet(String url) { return jsonResult; } + public static String httpGetString(String url) { + String strResult = ""; + // get请求返回结果 + CloseableHttpClient client = HttpClients.createDefault(); + // 发送get请求 + HttpGet request = new HttpGet(url); + request.setConfig(requestConfig); + try { + CloseableHttpResponse response = client.execute(request); + + // 请求发送成功,并得到响应 + if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) { + // 读取服务器返回过来的json字符串数据 + HttpEntity entity = response.getEntity(); + strResult = EntityUtils.toString(entity, "utf-8"); + // 把json字符串转换成json对象 + return strResult; + } else { + logger.error("get请求提交失败:" + url); + } + } catch (IOException e) { + logger.error("get请求提交失败:" + url, e); + } finally { + request.releaseConnection(); + } + return strResult; + } + private static String urlencode(Map data) { StringBuilder sb = new StringBuilder(); for (Map.Entry i : data.entrySet()) { diff --git a/src/test/java/com/ai/aigenerate/ApiTest.java b/src/test/java/com/ai/aigenerate/ApiTest.java index 20f756f..22be51a 100644 --- a/src/test/java/com/ai/aigenerate/ApiTest.java +++ b/src/test/java/com/ai/aigenerate/ApiTest.java @@ -2,17 +2,34 @@ import com.ai.aigenerate.model.request.news.NewsRequest; import com.ai.aigenerate.utils.HttpClientUtils; +import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONObject; import com.baidubce.http.ApiExplorerClient; import com.baidubce.http.AppSigner; import com.baidubce.http.HttpMethodName; import com.baidubce.model.ApiExplorerRequest; import com.baidubce.model.ApiExplorerResponse; +import org.apache.commons.lang3.StringUtils; +import org.apache.http.HttpEntity; +import org.apache.http.HttpHost; +import org.apache.http.HttpStatus; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.util.EntityUtils; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.select.Elements; import org.junit.Test; - +import java.net.InetSocketAddress; +import java.net.Proxy; import java.util.HashMap; import java.util.Map; + public class ApiTest { @Test @@ -51,5 +68,130 @@ public void newsTest(){ map.put("is_filter",1); JSONObject jsonObject = HttpClientUtils.httpGet("http://v.juhe.cn/toutiao/index",map); System.out.println(jsonObject); + + StringBuffer base64 = new StringBuffer("data:image/png;base64,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"); + base64.append("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"); + base64.append("+spUpyIy6ttgpQV9Wz6j3rPjMVxRACSTnoBWnPww2ZVlYcuziigPq8vHTGKn1Bwbjus9VHpjC7JafRSj8WOjIjFuj3r459KiQ38Lncj69ax5cGELXlHOa0f8Ain8QEXSTGtrDgWloHdj1rOBWojIxj6VBpkZjxxuUurPbJknYnPw6ZQzrC1lxO5BfQCCMjqK+h3i/cFueEV3bQMN/s8gkDr8tfPbQ0hTeqratJQkpfQcq246+/FfQLXLrd38KL45tDzC7aQlaShI3Ee3vVXVP+pGf96q/o9eFKD/vCxTpO+StO21BR5ojuK+ZvfkK+1Ga0u8G+J+Tc0SkZ46n0qPXKY7BgMtDG8A5xyKjEuY8s9etEPAZI/xB1QNs72NMfZN10tyGhIcRlXIGMVr/AMGtV6dtXhRY2pTd9W+iJymMlWzdk/lxWSvM3MuhXBI6mvoD4EWiAPBfTDi4bKnDEBUpTYJJyah1GQRxtsXyr+mMdLK7aa4UbZ15pcR0lTWqORn8y+P5UineJOko7a1gap35ACQ4oc1cTCYHlqIjMAdQPLHSipCYW/CYjGQN3+En/aggnaD8v5rS/ZJCPm/JZ31F4k26S+fhUXtDfQeZIXn7jFRZ/Ultef8AMAuSFKGCrzlZ/wD41q42aJcJISmJHJwCR5Se/wBqrC66ZSzr+bFS2hLYAAGxPb7e9XoslhBpqGz4UjXC39VTa7vBU8hS5tz2gKOPNPXHHOK8dfxm20NonXABIwQVj/8A1q1bxBi2nU0Bt1kKDuU8tIwentUrs+mrW4t1ZiRlYT/E0k/2p5ymN/oTW4MjuQ9Z4TrKOt8KEyeonkfN/wBqF/xK46sqTOmpHoTkf0q0rbcEjXkq3LjW8x92UJ+HRgceuKnbFnQ+hbrcWKABylEZBAHrnFccsBwbsXDAcWF/idCs7HU7qdwXOdKSOQptP+1KGdVyVNbRcJK21dCWUn+dWjqPEDUbaQmOGiQQgxEnIKc/5aNg3hDTaSERUbWdwUmGjOef9P0qUztH9Cgbiv8A/NVd+3Zzi+J8gn08pP8AvQze5yXMrnPjjjMYH+9XjaJgnTy4pLavLddQFNRUZwOn8NL48aAhDLn7PS87n5lPMAf/ANNMdksb/Qpm4cjv6185oV4lRHwtDhCh3qSaO1FMGrrU8V7liSg+meRSB/ToQcqXt+1G2qyuIuUZxhRKvMTgDvzRxxY4GllGOIcFvbT/AIguft1Lc1TbcdJbG7G7k/yq2bghl1lx9tO9O3cCB1HWsaW+6yg6Vry0UBOM8nOOe1aFtviCWvD9ckSFPLW2hsEDOCBg0Eg/44JHdaeR/wBpIDuyg/4iLK4fD23z1LyXpqRtwOBtXyTWZF29bgWlYUShOepxV8+K2vJMrStpgvKHlefuSFd+Dg1WURbdzjyApzy0oCgQ0kAe2TRrAidJA5zvUoLnysZO1o9Aq8VbHHG31JBITjknGBVtfhDjvt+Odg3rUGGkSXNucj/BVXtAaSj6qjzWGWnX5G7ASpe3+fpV1eFHh3H8MtTRbpIQ2wtLbqSVHPKkEcGpXY8kzCGjso45GxPDnequPxXcFzuUAqBPlE4B+lRWYrbb1p28EYwaX3nUTF9dbdYUFhKjkj6U1SnfMb27fathpWNtw2gjlZnVMjdluIPCb7IwGfMOxIyrPTrTnJWVoAwKKiM7EHjk0o2buoo1Hjta0BCHzucSuRWvKBVjg1ENSagVBnyUJBUcDKR/EMVLZ0lMWKpQUkHBHNZ91JrsHWEmOt/IR2SnrxQfWmsdiEFFtJe5mU0p20jb48+dNCWyHmnS+Ck9c1pzQjkC5adFwaTvc8txocc9MH+lZI0zNfhvPPRnWkuOIKclBKhj2q4PAy4T2Iz7T+9TKiShTbJxzyevHWvPmObDi0TzdrbAOkyuBxVKDae8P0teIYklrzI61qKwtOQN2c1PLj4pWLQt3VbprLrYaQlQdbTlJB7Y65p+tZjNz5aR+7U2QMrPKufpWZfxCXV9nXS0tOh5hTScIBB55rPhgzZqkR6SV2mY+6H1Wh//AF+03KdaRDekuLJ5AYPFWFCn74iXMDcoAgHr0rBuki98e2txKGsrSMqJPGfrW97Uwh+3sHG5flp6c9qhycdmKAGc2rGn58ueXGSuPRMr9wkueakjA5xioJfLabk4pLnxIB7pc4qzzaVyFOEDkc7cYzTQ9aE+b85ShXHBOBQ9xLaIRraHggqqYXhfFedVs3KK3EqVuTnOKsJGnkQ2XGUo2ktFJPKR2OOD7VNbHY2mnEuFIVg8kYxUb8QtQNWKUo/KVFtfybSOAPWjmnB8kpe89lmtSEUEW1nqp9oq6iywjkk4jlI4+lYI17Phs/iclXWW/wD8t+0/PUo5OBWif/VZEeC4CrjYeM+1Yxvzrt+1vOuDz+xKpCl/bNaYtApwWWLyRXuvo3prWNsudjiPRJjbrWwYUFcfzqOeJOvmrLalbZGCo/mSkE1mHRniWq029FraUByrarGCM0XrTVs6ZDcRLMdQbOUlKVBR+vah02W2FwY7ujmDp0mdE+WPozqtjeGUwai0eZCVbwteNx71IG7BhzgJI9cmof8AhldTJ8KYjyhgLWTVsNsNqSMA+1SnnlU+nCz/AOMEAwmp7zg2IMYoKxz34qmLW6YF2hvjcdrgOftitCfiCjY01OVsDnyAYPfms7xglQCiofKRxQ7KOwbgimEN7gwq0GPEZyfcvJK96UZTsUogcD2NSLQF/Ny1DEaWUnKzwk54/U1njT16ccv7qVLwkrV0A96vLwHiKnTFT15HkrOM9xTmsMjo3HsFE+ZsfjMB6nhX3JiIdCgoAp+4oxuMhCCAABjFJf2gXJi2wkqGzPFKEvKKHDtKcA4zRah1QcOPRVxqHULNu1r8Ct4N7mErCN3U5PaldpTHu7albErStCuVAHpmqD8aNTKt/iuzKflLYbEYDOxZAySO3Hara8LroqVpi3yELC0FK/m9RnrUBABtSBxPVZ31ewlGu7ghttLf/NK5AAI59qmdtYakTGojbocd2845ycU3+Jr0eBqOdIekKS8teUpbaT/M4qO+HzFzufihYCiQlMRx5QWAraMBPf1PtV7UYt7Y/oqWDJtc9L9ZeCr1+ntSnULSQnhQxzz606aV8NUW61CAqNvOxzKlAdSeuMc1oq8WVlbTZCQCMDIpLZdKQxKMhzcVAE9OKp+GNob2Vrcd5d3WWp3gutlDJMZZAJUSAP8AMcc49MVK9P6Ql6f0YY0cgPLdUsYSTtOavm8xIai4G1pXtAyE9RzUJkFPxDgSPlSo9TXOxRkNq+iezIOO6wOqztePBa8arfdluLU47gr/ACYz82MfrRVt8FlwkKZlNJLgQRhY/iBra3h7YIUmDIU815iykAY7ZOaimodGNJvLy1gtBRWoBSe26mPa5w8O6TGgA7yFiy5eEV2tNzRIbbV5IVnKEdBVv3rUs6J4PyrY2XVOrAH+FjAq73LfEch+UG0FWMcpBqPPaXjyYj0NxG5KuTTpcQStbv7JYpjFuDP6liZnTt5uLPmiOvylq2hS0nk+lKG9D3FyM6r4Ze1HVQGQPvW2XdFQLfZIcVtGNi92ehprc0jbVWt9hKClxxSQMH1NPDSOhVfbxysQztNzIjSlqaKQMAnr3revg8tLPhFYEDq3CTx77TVfao8LG1x1lJSUq7qGT9hU90BYLnHsDMRo/wDLIBa4b6ACqGoxOmjZt7FE9LkbDI8u7hJoV7e+CWvduUEp4A/05o5F2eU6ygjCnGEqJPXmneLoldubQyvG5SQT5g7AYp0c01HcUHVISpwNhOWx6DjgUJGI8m1pf4hGGBoRtr3RpQfUschI696ra73xK/Eu5q43I4z9RT9cLNf47anEyGUs5yEhByarORDlu6vuDrjg3FXYe1PgicwOtV8zIbK+MtSDxMvhXqizdMpXnr0zipnZb1+5WFEpVsPGe9VlqOySZ2pIy1LJDYz/ADp6ifESFqCXMFKSkkcVK5hIAUUU4FqORLuTr2UvB6g5z71p7wlDE6LL81CSks55P+oVmRnTz0fVLbxJw4Fc/wA6ubQ92dtMN1KipJ8vqk9RmrUbAJQfZUJZSYHN90zeLqmrdqxjyw0lPlp5JOeh561DGNQpETb+4yGCCec/1p28R5AvVz3+ZlxKQE4QT2+lQuJpCW638khKApOMrbP96R0ZLiuZMGsCtbwkvyJd1cBdSdzj68D7f71azslS3EpJGKzp4VW6RaNRNqUshJUWwAggYJ5NaCaBU+BjilLAkbM5yyhr3wCu7AjLiWt9tOTvdDS1BXPXABxUk8MPBbz4DDk1sJnJUSlKmVDODnnIBrT7OkZ3wakx4zvlrG7zoiyhX2/7V6xQ1W51banZbzm4lXxTinFA+mT0oVLPKG+GbCvwafBvDxRCqy/+EqVR5imG07lDKTnGM0RN8Pr5Y9ElMf8AfILZPl4yc5Ht9auUguqdSoYBOCCM5rt0g+bZHWtruwp/90rYTUWPO+M0eQr+RhxvFt4KwB4pXOfLMKG8na6wQSG1E4+vvTFod2ULuphbrpSs8gqxWpNb+EMXUMhUlpGxSwMpbHOfWqhufhbK03cvPbLhbSrG91G1P/etbi50I8g4Cx+Tp2R85Fq5Pw3R4ca2TluSUOPB85bUhJKccdevTFTjWkRy/LSEKKW0njacHFZ38P2JUGVLcKtn78k7VYrSlhCrnCZVwVEc81vdPigbjiUnqsZmyzvmMIHRM2mbCu2sqaGVFRzkmntTaWjhXUU4MMLZcV8g+XrntTFd5KmPNUnkg+lGoi3dsb0CFSscG73dUqcntxUZI9uKZ52qUIcKGhhQyMqHeo9cL066gDdhQ+bgHp+lRaHPW9MeW64QoPEBDoPPy9egpZsiKEgPcBabDDLMCWNJpKbt4h/85MhuLy4HE7UhPQFJqlpdjuV01g44yw8S8+UpXt2jnoan9mtsKX4iXV1+NKlqZQlxpMdtKvm29ef9qtPRdoZfucd6VaUwlBQcQHVArBHTgAAH7mvNNW1sPBiaLDTwVv8AS9FcS2QmiRymXwv0Yp+JHdl4jvtuKQ8y82SpQHcelXpZLZEtdv2NObmzycZ/pSZyIpuSlUdxpkHlYUkHP8qd429uNs3IKvVIxWFkyHyDk8LfQYrIug5URnvMWtbrzbcp4uDGxLYIHuM1n3WOmzqTUbrriSwlOQQ6javP2BH861JJjuOq2DylknkL6Uj/AOE2/JWtxpoOKJJKSTTGSGO3BQ5uIckBl9FlRrTpt0tltG5zb8ylBWec5PvWxLGENxIwASApAJxx2pjsOj4klx0yGULO7hXOafZKhBmeWk5CUjn2xUwD8qmtFkKDGhbp4cXu6pXGf8p2UHcbQflx1xikri2pKtwIAGPTNQ2brBmPKuTeCAnCirnnIHSkNk1zHmXRmOlQ3KGPm4q3FhmVpeB0TZM4RO2X1VsRShKEhGD05qj/AB1bMlp50BSQ0lzcpB5Ofb+9Wxa7slSykjvjNUf+Iu7mFaZQZJacWVI24UNw7nPSjeK1sDXOd6IHmOdPQas/XDUT8CE9hwqSnjCl7v7VUL9+Wmc6pISdyyeae7reJUhDzZOW+nHSogqKtcgnBAzVoEEIMbCksS6EvrXvCCEZ4q3LpaGrpohm6tvqKlIBIByCapFopbQ7wclOMVb3hldheNB3G1OqAdYwptJPODWa1uKTbFNH/S4X9CvSPgrKgbNk4k9fzIzV+o5Wzvw0fJ4RwgVbkhZwd2at2GvCQB0qlfw0DyvDJKEKyjz1Z7896uGE7xxRq+AseRRVSfiTkGNoW7u54CU59vmFZVtd0U5CDhPPPQ1p38UqifDS8c7BhPOf9Q4rFrl9Vb7XHUFZyDlJPtVTIaXN2juUQwSGSb3HgC0bpu87bvIdUonDmOvfNbI8C5SP+HQUgDPXjBzWIdJObll85AU5nqB3rc/h3Batuno/w5ICmUqB3lQ5GeKvbQwikFDt5JU0aviG78lsrTlTfIJ56083i7NQbc6+sgIS2pR/Ss2eJnikjRHiTCQ8qYphccEpYUkBR3e5FSXxB8VI07w8VPhqUQrKClYwQe+cGpCaFpo5KoHx6u7c3VTeyU5HPwqFAFvdkFSver08DlGJ4dQVjLykMLVx1OVnnFZr8UZcfUFxbuMZXnhDKEJ2K3KB5/pWm/BhsyvDu2slakOKhn5inac7jVcOa88Hup3RvjouFAjhUv4r/ES748opUUpc4UEkVNfBTSbl7cjzEEMLiO78kE5/2oOttGuPSH3GlKcWte5RSM5p58EdQN2ZqTEdU2hzfhZcISRRbIPisaB2pDIB4T3E91eZ3OxxuwVJ4OKDEUUkgUljTG3WFFDiVIVyCDSePcUfElsuAnsPaqddlctKLwnECQvPPA/nVY3FakSlgA4KsmrOvCyq3OEjIyKre5tpEgk/WrMBq1DMLpXZ4b24x7cpS1JJO04A6cU3+IvFxAGOE4p20G8P2cEjphJ/kKZPEJwLuRAx0qn/AF2Va/oTBCQgtAKbCufSgqgNKdztCSfSgxHwlsZPehKlj4jp3qySoUGfBStCQeQO1NKbOApKsAbTnGf0p7flhaBgUjTJKiRtAFIAFxSG52tmRGO8bsp4HOKuLw4tUNOndrcdCQl1XRI9qqSaoJjncrIxjvVw+FDgf06oA52uq/tUWQBsUuP86addRkRLhBUlKQFJWMbSfT0pXpCK3cEP7kglKeoBH9aB4lzf2dcLcC3u3tukEkjGNvoDSvw7kmazKXtSkbR0VnPJ9vaqFeW1evzUi7rpmO8FJKR7cVnW6WZKNa3FCWySHVDOPTNaslN7yeM/WqDmQFHU1xc6bpK87T1wT1qMN4Ke48hVhcbTHVOKlLSlQHfio9Z4vlXN9IKikqPUcVac2zIfcddBCjg8E1GrRpxbk9/onnIwjOaQsTd9KMzbeDdIxQeQFcH/AKTU+tFuaXYSA5seKfl+Rah1/wBIprfsTkGa2pxKMbiBhQz+U9qn2iCBDC2hv4IGTxUrGUbKic6+FVcixT3rqSuOsoKuVeQvGP8A5hVg2jTLJZZSWgFLPQoz/LoKkEpx8SQtTqUBX8J5FOlpLqmEpKwU5xlCeOtSlndR7lAGrCi23stls70rI6ds1JGgfP8AehXRvF+VuUnhXryaNCR5v3qs4WVZZwpL4Fata1hpeQtK9zjDqm1JI27R2+vHegSmlRb3KbCRtCj/ANqzt+GrxXtOmL2yiVGcJlAslbfzEE/lIB5CfX+taHvFzYdur8lC8MLwrd9utQ6iYpclzceyL49VZ0gysxWuyKBA59EKMyoErKjyO9HyWxJh/KsHI7gUzsz4kp9CkSHAB1CTwacFy0JZSATjryKGzY02OdsrC0+6NQzxzjdG4H6KNTYjbTzeGkbckn5U1Hb1brZcoxSLc6pROU7EIA+oJqUzHfOmN/vW20JPI25JFOMuPBXHCU+QrPPzkbvtmom7m8kJ76PAKzBP8MZEq/vyzImQGgvcdkcuJwPdJ5q0tP62hW5tiGZTu8DOZDWwkcjgdakbfh5AlP8AnlKUKPO5GeD6jBpq1DpJ5+YHGn1qZbASorASnH/nrRrCz/APnPAQLKwPFHkHJ7o+NfELuJX8Q0ULyACTmo/Lu7smY4A2pwLJGUgJSce9PDFmt0RTWJrKVDoVuJFRWbZjOuCgn4WQorOzzHilP19xWnk11hLXwHlZxmjPALJRwvXAs7ktygFbujZWTnn260q03ZYu98R23XFFaVbQpLgHTI9Rxmo9dPDq+i4NOfEQ3mcBRRHbIyM9M81bvhzCkW61eSqMG8E8BaQTn2FZzU9SdmEP6fQI/p2nDFGw/mktz0HYm78LohhwOLbSlSG3FBsAf6f96PcVFE9pQU602g/KM8H2p7mtkOKJIA9FHOD6U0LglbaFNqI+fI8sBX9azW6+pWm27aLQpEHBsQfNCPQKo5uQHf3e9O73OKIiJS6yPMBKkj8ucE0guMlq2xXXFNpaOwqC3FA7T6nJq1FEHC39PpaY+SnU39aT+3ZJrpC0EED/AC4P96VAlpghSgD3OQKoS0eJdwmXsR0HzWwrAkISWwPbGean7urlqQhCkFaCnCwT1+9a6P4ZmljbJG4OB+5ZB/xPjxvdHI0tI9f8KX2dXw6nCkKWFKJ49aYtSXQxJ0xbhcb2t7k8ZHTvT5oyeH4GVILYC/lB70k1hIjyzNjutpU2WscpGearNxZMWR7Gt5CnflMyomPLuCqGuV/VKbl5JK3fUYwB0ps0w4+jVEUBaht5URycUikQ3WHJZWpKSVYS2jd0HbkUfYoSX70woYSrjOVckA81fxsKZpEPr2QXIy4zcvor8skghXK1Zz61VvjVYrlqCOpMN87QVBTSv4vbntU4t9xbYbSC5wOgJ6UnmS2pJWFbVg+vNaGHSi5hZI2kLm1NjSHMdaytf/DWc002goQHDyoZBI4qEOaVVDnLbdySkZJAyK1rf7U5KKVMKKilO0pCRVe3fQlwvNxK3EI8sADLqQOPT5VVQl0qeNx2CwnN1CB7bJpZwet5D7qcYpfp5Ygy/mVtyODkjB+1WHcdCOfthuLjlTyUE7cAgkZ5puvGj3oc/wAlBSxscSkFWcck9TQh9csciDLFPatk/haWHPC5YBz/AM2rkD2FXJBQEpGBz71Rf4TbZcrZpC7sTD5rJm/uHUrKkqTtwSnuKvdhtLQIz+pzVOqNIq0221S34oFtJ8Obz5v5NnUt7k5yMZHbnvXz9us5S4oRn5UjgdK+jvjFplrW2lLlbPjWoaXyEF5SQvZjvg1kPxO8BbTojSa7i5qhEyXty2whhttClemVPA/oD9K4C3JxdtaeeyqrSM5ttpSFKIG4YGa+gHh/Ka/4MtDiW1JaMRBG7HpXzw0rKU04Eup2BSxjH9+a3noW5BvSNuDU9l3DCQpt9SlqQcYwAMVYk4KoQ9Ss+/i1nJja4tD7aUndFVyQR0XxUc/43fvWh/JccCEpWE4+bAG33+lPX4yJCP8AiewgJTvEVRKQTkZUMcVBNLx3pOiXUBO5a170gq46Y9aZKQ2IOKkia6SUxtFkomBdFOJYaUAve4E5znHatwaAlQ2NM2gokkkxUHYlaQEZHpmsLKa8ifDBSELDgKkp6Z9q1rdp062+E2lwsxWyWUhK2Xd6yNpxn0PtQ/GiabkHdaXWMh9R4zhW0D7uFKHZLMmS7tAdUpRHJ9/rWadV6hXpHXjxDhYDbyiOMd/Q5pLafFzUNtkDbIbe2klIdbyT82MAgiovru6P6lu65b6QlSjlWE45PJow2YOCy8kBY5XpZvGlubFQhM9KHR/EpBVn36UtT4ormSVeZc2FYOMt/Lj/AM4rN0OOI+wpWpPfPYVdHiPoC9af8PNN3OQHXzOj+c8l9DS0MryNobWkA8pwcHI5xUJyA3kqcYxcaCnMTxj/AGY6ov3pDreOWXTwKVSPFrTV3ebUu4x0EIIOxxPJ+lZfkxTOhea7lJByoDnv70brGwM2uyxrkgFKiygEbAN2R1PbNNGWwPDT1KmOBKYnSgcBfQfw31RAuFuS7ClNymSgfMlYOMCo/rzVkdFwdW6otoCgjPqSKrz8M2lVan8OYzvxqoyXUqWltlvBSUj19aq/xUa1Dp9yVKkXBcqKHP3S8kqAGeDn0qZxY1/J6qiwPe3gdFoK13GPLaCw8Ak85JFEXG7RobxPnoVnnCSCf5Gsn23xGvXltuxJq1gjBCmx1+lGjWN5nTQt9W7KsktqLf6gkipPLfVN5rotRq1EwhCS44G93AJNAN5aQeXAeazvqnV7jljhx23lJdU8lKiDz1FLmtXTWEnc95jmOCAf1xTgE21d0/UCHkKbQvJzzV7eCbpOnnuuN+f5VheLrVQeX5x3E55Oc1s/8Ps34vSKl8JypJIz7VDkDyKbGNvTv4qXyLap1sMjcrcy/gJGem0mlXhVPYucCY/HQQhW3BIx3VSnWVrj3RKC+jeW2nAnCckZAz+uBSnw4itxrUsJHVCc8Y9aGB9ikVLKNp5fGVkVTaGUq1BLATkrfcz3xyauaQ2N59Kp6Fg3xRCU/M44dw47nrT2DqmPNUmVEMOuPFCAeuQe/NIbVDSmY4dgyD2p9jfnmK4yFKGQMd6BpxG66hKk5J749qsNHCrkqMXqP8QlASd370hJA6cGn3SrHw8dtOCBg8gEUmu+74hLO1XDjigMe+KVwnCxBbUSOR0VTiPLabdFOimx5m0pK0j/ADV1KS24nyiEjPTtTWL26VkKQADwCB/OlSJXHbjvUfKfwklzZD953kDco4z613ZskY96U70Oykk43ZHNeklHxijn+LFREKQOpYig3W3LvEMuR2ghp5JU6yVgDH8Kgeo+g7VpeFqqNe4bJhzETGQkIWtjJTuHUc88ViqOhDt5dcVMdS0lZwFLz0OOvGe/bNaK8PLzadKWhqDMlpZfUkvYKcHB5x1Jzxn6Uc0VkbNU8eU8AH0pZvUpZDp5hi6kj1Vw2ieG57O5WGwed3Spv8SypQyVpGB0wQazXavEJ5d/iQ0JS/bX2ifiUoJIcPQfQ81PIurbhGcSN4UBgfMnIxWgztOPxPC3MxTtIsEH+6oabq7Ph2R2JlAuBogj+ylzzDrurT5bqvJQEq2A+/fsPpUxfjMvRwh1pDit2QS2CB9aqqVebpcHEOpdS2rIHyKCSftnmpBYoV4XJS5JmqU0P4Mnmg2Z8P7MdsmRO1paOnKN4nxAJMh0WPjvcHG74rn6qVN2xxtaNjiGmgeEA7QPsK8mGxIffSylrzm8F1TSSpRJHfJ6UN0uOLa8shtCeFjGSfp6Uw2+JNj6kva3AvyHfKLZCs5ASRWOx8VuUH+I8DaLH40tdkZTsZzAxhO40fwtOTVncQsqDiMnkB1nPP60H/hyJKkh+ahnzwf/AHbGc/YUpYIGApJAJ/iPSguzZRfWhMpCGgcf4W4kenBGKrxwNBPmAUskxIB2k/mj58ZlUdtqOwUK6DzEJQTj0HJo+0QpES2yf3LQkhCi02V7Qs44BVz19aazBKHEuokbzjASr8oNOanltx0latykjhXSnSQtjDQ0h1/7SSOV0hJcNtf7aA1hLCFTI6GnVJCnENrKkIJHIycE46ZpnuF4hRXPJS09sP8AEg4H2pjnXh6U+tbrqlEnr0GKRuSUrOSon3Nb7E+FoB58jm+w4AWFy/ieY+SDiu55T+9qa2xghSUvKCR8w8tSifvmq81BcG5rsgMhfkudnUgH6U8vuBSelR24qQ2hZ+XPZPBJNabF0vG05jjCD5vXn9lmMvU8nUXtExHl6Vx+6R6ZiMpmkpI3gntUuKs1EtNJ/feYUkHnqO1SYr/nRTB/6I4QjN/6xtTnSt2iojht1bbak4PzdSaP1ZMYMZx0FLrik7ctn5sVB4zpbXkYpwuD65UFQTlII25B5/nQbJ01njGQHqj+LqTzCIyOiqeW2y3c3V/8wSlRGXV5/wDPrQdPv7r+yUozu3BWEjI9804XOK42QjyXUK55ewQR9jXtN2fZcviDlOzOAFcfpVeOI/aw1nQFLJJ/xi5/UqbBzAxQS7iiiaKWo54rVhoWZLijVu570mOefm/lXiT60Hac+v0qVo7KJzlW8m2u3LXIYDuQZCN3zbUjpkkU5DwnnP3YtFDL6HCFbkfNkZP6U8+HbEaV4whuU24n99wrzEoGccHkc59KvNcGJEvoSpstq+H3FQUVbjuPXHWvG8w/8mT6n9V6tiNBx2H2H6IrwV02rSdjlNvpUguPBeVdMYwKnsychCQcqT6EDFVD4oalYct1rZgOKBEravb+7HA75IGKSSNVSoVoY/fSE5RjKW94Jz1ylX9qqAWr1gClYz09MyK+FKStSXQE78q5x3xg1S34ib5Pg+FyU3OVAQwt1KUttxns7snGP33/APTVreH7yn7budDm51e4lYwoj1+ajfEfR6dX6Kkw48NFyWHAtLUonaCk9cEU4cOSHzN4WKdP6dZuTM51y3WiY0ltKg6oPJeR8o/L175680/x5FzjW9mPHkTYbKAE7VpK0ewAxkfXNXXpjwqv+nLZOS5pyOmO6gKZMNpG4Db6ggk8/XipNZ9NwrnESmfam0PpRh4PtHeD6klVOcbKiaygsH+K864TbnDVcJDkp1CVJDjhUSBnpkk/pS/Rl0W3ZwysDAPyE+lX5+IvQsHR+hzPhW+G+9JmhpzzIhV+6KSchWcpII6gis8ru0GNbGlQ21IdQgKcbJG0HHRI5IH1NR5Vvx9g6nhXtM2w5rZXHhvKcn5qHLytKCMISDkHvitIa/ltf+mWj1uRWQssJBU24nIAbOCcY/nWRG9Q/EXBTv5dw5HAPSr51FfIUvRGkvJlQC4GilSG23UOcIOd2/5VY9R3NJjxGKEN9F2qZYy8t8oNgnhUtMMgyYqo7TrzJSN6y0kbCVZxyeeO9PkxotRGyoBI25BxjPqcUY34aO3NmNKbcEQrCXEqLuQr7YoeobPKtTCEvOIf/hStvPIFdC8O4C7MYWU49ePT0TWy6lLje5WEg846fyrR+sZ0S7/hq0xMjwmnfKC2nksApdK0OYUCgKyUkHdvOOtZoEd9O0FBCiOh9K0/4oz5kzwF02ua88+hyOwE3AkAkH8yS2hPG0gAHPI5IzTMqmxD/wCw/VOwyXyu/wDq79FRtnjIFqBIUd6Co55wc+9Pmt4ibj4bqHlpKylBQsjp6/Soul7/AJNKBhW5OSCSDUmvrxc0IGySQEJ4B/00Gmrxml3qtbjknDkYwf0rR/4atTWvS3hJbkuyHVzZLCti9jjpKsEbQcZAHvxVe+MEx+66GVHdcU8ApSwc4HQ9O9Tz8M8ON/6LWrclCnDHXsWttKz1PqOcelR3WUVL+hJC15cWhwjclAAAGevHej2S2y0rD4jtocPVZc0WtiMyplZ/eKO7rkAVKGohlHDI8sq/iA3YqH2lssajIQ2G9yj3IBqfI8ovo85obkjJKVbf7VQytzXeTuj2mtjkiJl7cUofqeM9D+EStRKjKSN3l7e/15qQzJLsSJvUPMUE4wCR/XNItbRGjcLO2hagHJo+UHIAPcVIr/ZPOgPKjlQ28Yxk0rch4EfPVKMSFxnG35apQmLqBYlDC1J+boecV9Cfw5ubtCpXuKirYTnHB29K+auVQ5+MfOlf5umDX0d/D/Jbj+HsVTeAlxCFHHc7eT/ejE5uNZbG/wCqVZd7YcntKaRJcYKkkbkda74fMLhQ3WlvuOLCUghwYxRDcsuvDjIxTxaUJaK3O6sA4FCxQRh1lOjzQWDkdc5qlIMlLV7SkJUltO/8wI79xVyrl5JGDgVQsKSJuryk5Tuz0V05+lTxd1BLxScIjiBGmuYXk7zynr8x6GhaVAF3yTtTjOT/AOcUUhhDdqkoUsOqyogjqeelNukb20vUKmlLHy53b8jgdRx3qfsq98hI7/L/APbhbBJwleTuHdz2NKrhckW+1oJOCkdhUYvkyO7fG1sSAsqCwUJKiofPxnP9BSjWjq2rC2cAlIzkjJqRo4URPKQq1ywiSgyJEdlJ5CXlJBx9SRipXbtQw57CVMraWhSfzNqCs/oTWFfF68h3VLjSSTtQnKBkbTjoR61O/AK9XNmCsRRvZ3gFKicf+cntTS1IH9lq1+RtmpQleFK7YrsmYlqaUqcwc5I6mqHuHjMzGu4Elt1K0uAEkJGMHHalt48ZrYu5BTWM7crQ5kEg9xxUdAlSlyzIt2JHSVTGkSFI3nyWCUlJPQk/XtXYtzO1sqO1TeOHFf7+lNMdqQyC8rakKOCrdyKOUuG+A0VKVg/MtXJz/aoi2+qH9laXh/rCPamd4eaW8p3etBUQtxGMfQAdgKtfTeqbfd4hdXNjpe3Hc2hWdic8d+frWXIoQ1fG2on7kKUEeYkkjHc81O4GnFb5D1tnhLjDSnBkYD6hyRngdM0Wxtay9MduDrjqg3oB+CH5OmY+cNpb5vXutHMOFaAth3e3nqDwKnGlLq2iYlkOhxaxytxR4+g6VT+gNZx7rp+KXnW0ObvI+ZJSkq7AZ/SrIskQr/5nzFNqQeiEg8Vush2JqWB4kpFkfgT6rN4bsvT84Rwg1f4geim1yvrcBpT23zgOOOKjadaESXXCyNq0pGCehGadPOTKYKVHcPVXp6VEplocakLDZBR25rMaDiae9r48lvm9b7LW69laixzJcU+X0rupC3rZjjdGUf8A5qMY1hG89xbjasH8u0DIqJfs588BIPHY11Fvf3kEAH0zWidoejG//wClnG65rQPT/wDVTB3WbainyWylYIwpeDT43IEhlspcCgoDJI4P2quExHU87Rx6GnW13hdqUNw3N9Sg0H1TQMaaEfYD5m+9396MaXr2TFMRnjynvVUkcxeJTw9FkfzpOVUGQ95r7iwMBSiQKKK+a3MTSGNB60FiJXAvcR0s/qjVqGKan2FOrUD5oGOmBil+84xRZJ78inObuFKJr9ptIbNFEROBuxk8L6inQnaqiGGlBRGSo570aptYPtTIg1jQ0JJi57i4o9pXzdf5UtccxGI3YHrTaykpV1yaXfMWsHGPeoMgtFElW8UOcKpRG9tNOOgEE88q2k/pSqyxPhwVIcyg+o5NPLsULB+QKV+tJko8o8J2/ahGNUuU6S0WyQY8ZrKRp6UBXNcKia5ntmtAs+V7HNRrxB1MzpLTMue84GsAIQSeSo9AB3qSbh3qKeKGnIeqNF3KNKQV7Gy62QQClaRkEE9OlRTb/Dds60nxbd43dLUE/Dr4oKm+JFujJjJU6+6vCn14SjOT1wc/StaS1vNz25alpcKm1ICMhPf0/mK+dPg84lvVEZ0trdW04hQSHNmeelbxuOsbPCmNx5clhp1hKCtgpJU2SMgHGT09q8acD4htesREeGB6Jn8RVN2TT7r3mvIW6l1xKWTznbk89hWTpmr71b3MRbk6lG7IISAofyrUHidqazX/AEZJfYR8W2hp4IDaVApOwfNnPA5HUc81kSVDlynFAjCgRkJIykfSp4hR5Ved3IVvaY8etWQdNRIYuAckGTtEh384TkcZHbmtnN3mRC0XNkPyklSWAVOrG72zXzutLaW7ehhS9+2QTx1HKe1bunsuTtDXpDag8l+Oryh5fUnG08nI+mKFxPL55QexWkzIWw4eM9vO5t/mnTQeqlS7Oyz8W1IaQ3je6kp4x9afGJNvuiJEney62htW5bRCscevrVG6asVyt2nJLXkOKeS2RlTSuuOuMUb4MtTdOWiU1IK8pdWcFBSVHtwelEK7oGHm6Khnj7px6++EtylxxJlOwLky8tLJUralSVJ+ZJAKeoOcVWto8EUxvBGVqbUjU3Tsxc5CW33mioPRSj/4ZI/iwd1XulibcNFazcgzENtueW64XH1JCMKypJSB83yjqQe470861jRtRfh4iWxS3pDRG0GLuWrr1CcAnJ7cUt9ktckj0WZ/HTwZtmkbBYJsK4i4PORQCoBtvIACiSE9Tzjnmqz/AGjczZ7Cw6+78DvUI+5W5KOcHAPStD/iFsrbHh1okRUvPQy0tRJATtXtSCnj3z249aoqfPfnN2qAsILDDoUgBlO/k9Nw5NPIeS3b0vn6KuS0br61wtKeAWk031KbdcpLzbao6VspaKUkLI7g5yO+cVA/xMW6HoHUDFtWUoWtO9PlkncnHXkdc9asn8PsKPJvTS41xDEhtlKnEONA7jwNqc/1x3rPv4j7zM1n4lXdUp5TvwclcZneAAlAPTj6VVwh/L59T+quZ7yX/h+ijzN9iPwnUtuIEgN70uYBwfStCeJ1xhwfAmxMod84yQwVpcuDDqmyE5IS02gbEknJCjnNY9udvet6wpQ2oUrjBzUsu3irc7r4as6RfefcixpKZDa3XCrGEkbUjHA5HrT5oC9oa3pYJToMyiS/qAQFP4C2VWhtOUqVj0xSrUI2aUWG8ODGMY6cf96pGBqifBY8pBGzpuycipAzr142ZUN4Ld8wcObuRzVF+C/xA+75tHo9ZiMJiIry0tw/hwkIh+DlsaUJBSqOdqkhSupPCSBxg8VBvEC9rh6HfjO/K75rm7YoJB69jnpTB4QeJEGwaDaMq3PERmCcwn0EKGO43AgnvVXa88UDqFh5hDZaacWVJSrr/U0RnY59Us7iSMYSXJh0638VfNxA555TU6iRmVPZSopWeMbsj+dV3pu8otjvmpOcnoQcCpzE1Sw9HCipsk//AKlDMpshdwtPpr8dkVPIvqmfWM8RtQ6ebVgqZlpXuQQOvt0p91lqFUC0yCUhaijGeN2c9eKrPXF2Eq+xn0Op/dqSoKQfQ04S7km6RnAX0tkgguEpSP51aix7EZd2QufN2vmEZ6pkjvOXGYFuZKirJ3H+tfSXwqg/sbQlsjhOxXkIJAVntWAPB3TTWrNe221uMl1kul11ZUQkoSPVPvivoxa0hmIy0BgJSEjj0FEMggtoILig7txUgt4JcAJx6mpBEcxkD9cUxWwg8qAp5ilIUrtmhgCLFeewVKwo5wegrPtpf8vXTrYKxgHJcQEY+gx/arzuryIcCe84VFKEKVkDnGOwHU1mi2TXBq2W4JCnEAABDoACR6Af71PF0KrTdQplcZMqDY7i5vUpbRO0EBW7n0AqhnvF6Vo+7LkqY+M3lSFI/wAMpJPpVz3R5BsM5f8AiJUATz3znHHI+tZb18hpicG5CFRSs5Uor3YyepKQc/zqwwg8KpJY5Vq6O1Qzf5zBZS7vS3gl3J6qzx2HJ9O1T3xCkpjacR5pSDtx8qv58VS/g/BQxey4ieqcA2MFlSglIzyCFpBq1vF6a2zp+OFsOOhY+VtjCVDjrkg+vSpeiYOQsWeJEhLurpqmiQkkEAjHarN8BdQphQ1RSUpUpzcFgjd9BVQ6tW09f5S2kKQN+ClRBII47cVMfCZflzkKLbivmwlSE5APFI/5bTG/MnHU/lP36X5pUlYdVkqXkk5NIX3ULXy7kkFGc549KHqu1vJuLshSiUOuKwSMd6bG4yQ6gEk/fFQto8qZ13SYMsocPnupCQnok8n3xTrarciSA4iP/ip3FonKjzxmnB3R/wC1nkKYUvzHMbkOKCS377sc5/rxSiDb1WS5SUuR04QpGfKUVOKHGBkDv3qo+eP5QeVVDSRaZrpFWxcFojJO/ACgg8jH9K4xqOYy2mLG8woVkuN4zn1GfTFTO5Qor60PJYcS687lCDwnpznH9+aZFWllqehLqwnaCVISfy/XtzxSxuZI2imklpsJ78O7y1IU43JkrhQmVIdUWxnBT0O08H61oHSOuVXSAVxGyhpJ27ykEHH/AJ71ma3vIDNwEV1XlICdyGwFb+cd+cAVojSrTTOnbelpsNt+UCEjtWo+G4HSZUrXO8lDy9bQLWJjDEx7PmvgqYuaolOtBIXjHUgYzSEzHVnJWok96RhWBiuhwf8A3r0iLFggsRsAWSlyp568R5KWJlLByFqH3rplOKOStRPrmknm14OVLsbd0ot76rclgkrH8Z/WhF9SiCSTSRLlCC6Ta0GwEu5xFWlIczxmu7vekwWKF5meppyYjyrNHw4zspZDad+OoxSPfTzYXHQcoKUoJ5JUAagncWMJb1U+O1skoa5OEW2OlHzMAY65pHNjKacHGPoKmkGKst5WtKs+hFBftCHVHcnP3rKDKc11la04jXNAUECCg5KaOS4QMcjn0qYmxNK42D9KEjTzOclsH7U5+aHjlMZhFh8qhwKnRgAgfSiXICirp1qwE2RkJH7ofXFB/YrZV+UYqFmUGG2hTuxS8U42q+VblgZ6Civ2c4RVjfsNJ6JH6V5Fh3EYQn71bGouCqu01rlW6ra8ASBupi1FZP2zbnIb7SlNuEZAJT0PqKu5rSrjmB5aADS5GgmHUfvGwr1G2l/irQCJBajOkE8sNLK2m/Dm32qc8hMAspSwnCgQFb8qyQrr07UnGj7lcrpJdceVILq0EEqK1KA45OPStVzPDmFsJUxwewFRY+FNnYU4Y7UmK4ejjTqkmsrkxRzSF8XAK0WPvijDZOSs+TtLamjWPyE259hoocbcU0jlaDjr/wCdqpfU0F203d2O9bnYivlILiCkqz/Fg+vtWyJeh7jbXFrZucx4BWQhawoH61x9uRLZxOgRppBx/wAwyDgChz43xG6VumyirWUtMaKvd7nLTAirkJZmJQcp44CSck8YrfenLFPagKalpb2LQAopczzjkYxVYSLi7CZeTHgmKkjJ8j8n6YqX6U1Mt9YZXMLbZUSrAPT3oe2Onuf6opJO6SGOF39AoKVR7UlouJbaSDjbzzUYusJ6OHSWspSgnA6Gp9AnRI6V+RKDhV0zhX9ajV91a83dhb/2PJu6HWSlwtsDY2Dxk4x+masUVU4WfvDKxXG7I1y1FaeZQ8tCTt7fMeuDz9PSrfhMmy+GswOfM6y0VBC8HafQDrTj4SeHLGh5OonS468zcXUuJY2K3I68fMff1PSlHim23bdDzJEdSYZ/i81O0e2e557Ck7pw+VZu8WLumb4a6VjRcyEtOupSvo4hJydu0diQf0qjShTN2YDjSyQQRhXbPpVtavuDMvS2nIO8CYmY4FNpUQgA55HY+5qDa7s0a0MQnZEgpXkBO1vdn64P86tim0CqBDn2WhXP4eaecuLaZ0S3PuAJJakxnClQXgZCj6Y7ZwKzr4khxvWl2SpSgsS1Ak9Savbw41e1BZi7r2YrElCo7Y+KADilBIHyKxyOec9BUU8WfBvUFi15cxMQh0PJE1t5Z4cbPRQV06jBHtQ/HDYj4QPPKI5O+YeKRx/hUzMQ4l6Md4UQv82M0kvVsYfS2oFXxDjgTwnAyf61K7vZpTSWMs4bUfzAdMCmp+M6l9l1KAUtqynjPPrVsEjqqBCjrkNyPb3I+4Jb8zJG0ZyOOvWkS0JZKAFBR9MVLXbU5KkpJJ+c/NkY6mmq8WYolBLaCdvUhPWnB1ppauw9YXGJCMRqW40zt2hvjGKj7899bhJcJOc5pydskvI2x3FJPAIScU0SYy2nVJUkpIODmnghNopRHu77KSkKOD70adQzMBKHNqRxgCkCQpKeDn2rwHIJ4rqB7JbcOAUe9MXId3r/AEHSrU8DdHjW+pm4jhkeTlKlKZS0pKeRyvzOMfY59KqRWQocHH0q3fB6zToF2iyzBlut+alaHIhG4Y9wenr/AFriaCRvJ5WhdReCyIPiPa5tolLtIbQkqTboYZ80BWVFZaUEn34TjjArQjLwZDY3ZTgbSTmoDI3SpkGc1dI0LegENyY3Ix6FKuv3qwjFUYja3Fl7IHzgYz9KrOdfVEY2hvRPESWjy0nJPqc0vM0N42K3DH0qLobCQB8/tzyKaNXasZsUJHxM1uOkDOHcZPpUG3lWt9BP3iHexH02835oQp0pTuxuIHcgdyKzg/dfhbrJeQ6pSSRlakhCgPU4z/WpLrDxItt7hJjt3PChztbW24Cf+n1+pqp7pcSq4b25LzWDjK05J/8Ap4NSNBApVnuDjasZepWnrM+0p+S0rqVIlBKV49j9OhqifEOGubd9/mKeZWoLSXJO5WCc4yQMVY0KawzHWpdw81SHEr3KZLIyM4/KMkdO/wBqhmqVO3ia69LcS+XDytRUrH8hStsFRv5CdvDG2KbunDynGlH5UHDyd3uUr4P2qQeO+o023TjTUiHJbcfPlh9h1TaEq7bknr3ojw4sq4DQeSWP3ygoPDClYHHTtTf+IOSzKtaWnpS9zSd4GwnkdBnsD9KtjkKA8LMd5CjcXyrqVHntUo8NXVN3Vra95fz9SgqH/aofPkJLuSTmnHTGpBZp6HgneE9t2P6U1wJauaQCrM1m+2vYUS23yVr3EJxj5j71DHXgXU4XkA9jiiLhqNNxXkpSkkk802LmAK4I49KgY0tFKdzgXAhXJHhfs6MiU6+mS4T8uEYbUR1OQcnjgUOw6nmMIuQfShyE9htCQ3lSF78+nJwOPbNM11ukQKZRGcaYfJBAjE7M8569e/PvRipLD9hcbdJbLhKgpkgFQCTkbe2fas8WBw846qJhIHHRN+pL82qElLO0pYcWQGk4UCeTuPv1p4sSmpdlkh5SQtlAWp91tO1gEAhIzyo98CqsjMqb8xashtedqj+VXPvUg0/HcmzGEuhSwoFISjkfUgnH3q/LjMEfBquVC1x3cqyLdbrdbtPRUuRR8P56dygUhbu7nKscpwBwAT71bEItrgR1MqK2igbFK6kds1VabAi67IDSti05KFtIAycc7iBj/wANWVZLd+x7YxE81T3lpAK1d+PStl8GRy7pZAPIe59VmviF8e2NhPmHb2S/OOK4VfpXDQSeK9RpYu0MKGRQkkUVnFd3etIQnWjwqhpVmk4XQwvimUnWjwa6DRAV71zec0tLiUp3/Sl9vlqbARvKUE84FM4Xn60Yl0juRSOYHCika8sduCse235mOlLY3uZHBOP96eYtxTIcQFJWEkdeM1XOm5sVp8mRISwQchSk7qsiDc2QkOMqU723tt4/tWVzcZkTuAtfg5Lpm2SpDCtKXkpVhYSRnKjTgLGxj+I/U0XZp5msgqSpHb5xindKwRyc1l3lzSQtMwAgFNRsDWMDP60AWRCB1Vn0p6zxn+dFKdAzxmmBxTtoTSbYlHqaEiKhAJA6UreWCg8ED2NJWUNBtRQo8nnvk06zSSgndplKAnAGKUsAH0zRQ9e2KBFOXTk9T2queVPVJVJylnITuPpTFIaceB/dYPrUkyFN0ncSOmKaDSQi1DXrW6oqwgH36U0z7PILSgkAH6ZqfqbGSeKRy20lBAFWA89CoSwKmrzpWfNaKS6jcM4KW8f1qpbtYNU2m6u/CzVtIKiQ6jkfcZFanlQkuE8dqZJNnbdWoKSjnjlPNWW48co46qs6RzDyqX0jrW7WCABd5iX3gSSorGcduO1Wv4Z64Zv85bqRIcQsbQso2gEdapXxb0Hfmrgt6225cthRG1xrCjn3HajfBuRqLTstbc6EqI0FZUpeU8+4xihk8LoyeFaikDlrm2S21uLBBzjjJ6VE/F1jzNEy1bVKA+YtjkK7c+w60lsOqrbEbL1xmsR1LGEqfdHJ+nWmrxcucOVoCa9HdZmqIThLTmQRnr6VRqjyrhPHCxbfJcyLNtyFsoQhU5Skp2hJwPf3yff2pZ4jwUvwGnPN35wpG/qBj/wUx62luGVbtisONyCoJScgdgDQ9YajWu1xm3RkJThJ3ZPTtTM1pM0e1W9Lka3FyN3cKt7JII1bbgUZSJjf7tfIPzjgivpD4nwI160pY7h8Ilby4phh11slJzyEq9CSO/HT6V84LLb/ADrrEeCUvN/EoQeSOSc/XtX0l0tdU6h8OYC0LWWkuKG1xOFbkYSCMfw9OvIqGdp/iENDsVHiOvT5rPRw/RZ98U9DN6WjRhIdt62XsqQGozjSs4Gd3Jz9jVL3SelmQhuGttCVjlMdZKT+tW3+MK6PNzrakvpK8ZSts7xjak8gAFJJ/WsvofkSnTj51E9+KKviLhaD+KA6lo3TPgjqPV2kxeYMVC2E/KFLfSlRPsDjioFqLw8uUOctqRAU0sHBcSFeXgfxbhkY+npWsdF6iiaP/DqW5xMZxcdDSGw0FblKBwASSMkd/wCVUs94hxo9lZbKZBQ0R+9Syk4PfKfX3ocNwPKvkNIVeC0XDTFsD8SU38/CUlQcUsfXHAqIXXT8++O+Y5CCF+gTt3Z7+9T/AFBr20vblIbek/NkKfHl4PqABgfSnC06ltU1iMmQwyyk8I/e7h9f/BUnI5URAJpVCvREtaNnwmwp6qTQR4fvFsFlRDvUblf2q+ZEyw29IWW0qbV8vmIcIAz7YTTddxaW0IebU4yncMPFwBPTrkZNJvcu2DuqKtVtucq6IjlkuuZ27XE4B7d61X4FRnmpcO3Qo8i2S4wUqS5HKFMvAHgLwnHc8E5qspVjj3hSXY8l9RAwlQeS4njtkgYpptsi+aRvAet9zn25zJOY7im92fUA4VSl25K0bVuyaf2eGCu0m4OrOf8AlGkKWj3UCBx9zSuPqBqWUtFp2Ms52tSEBOf0zis0s+Nrl/tLcWdcYgfaKfOjXTe6t1QxzubQnAzzgmn+z6p1bcEZtaILLaeiHZLhbXnopCyFYT7HGKZz6KxuC0GhvAOUYz6Goh4u2yM9pKQqU4ppJbPzeelsIA5J5UMjjpSDQPiEqfAbjXW5WxVzZz5rcV8uBSQcFQ3YOfoCKkmsbXL1dpa4sW9xDjjrCm2kOYCSSPUpJH2pAFITY4WL13FhpAbEtLievyDIx7ECm6fqBZKil1fljqC0TnHerrh+A+q1RW2JMG2voRhIK5B3bfb5KVO+AOoI6ZCBFsym3QEqU4s8JHYfKKlDgq5aVQ0PVyEoKlOtkZwCtrAz6dac2b7GdHnOPJWBzgHCT9QasK/+D9zsrKXHo1oS3uwlapGP0yRVU6gtj9okOsBhtSuvmNvpUkj6BRAp3CYbHVWNA1zASlsteW0ogBCfyHOPT0qr/FK5fDuygofE+ak7nW1bgnJ6Dk49KYXlPuKTlJQpJ6AjikL8Zxaz5igrPrTg6uqjPKgjyS+vKWyQexoJiOspBLSk/Wp6m1NKwVNpJ9cV56xxpH528/Tiu8UJu0qCsrI/NwKGp75jg4qYJ0xFaVlCSCffNIZmmACpTeQrsMcU7e0paKUTvJhyY5+IaLTgKkOFPQeuB0PGMe9ekuDLYiuuuOqcGO/B7U66/wBIwtJXCJ5EsSXXklfkqRhKU9O/Oc5prgvCX5bjRDKG1EJyeenJAHOaHsc17A8dE0tINIabYiY05HkPK8oEgJPyhP1+9LFzkQmWUNu7W0DBSDgu54wfYUncjuFxl1twvlKikoBOFJPr3oNxSZMh5tMFLJRgDyj0UD61M2IzV6KIvDeqtvw0tTzl0bmmfvZRHyGkKAwSeQUntVqA4FZkkJlWeYxHVLzIeUnPlZKkEkcehP0zU31Bf77p1hTUqfJfS3tSh1YLalgjOSRkHHoea9C0nK+w4ro3RGmnkgjvysdqGKMrIa8Scu6Aj0VyZ+uKCQT2z9BVDL1xe7h5ahKcWppO7LR25A7qOOlPdu1remnFs3CR56Ukbs9MnkDIOeaIw67BMfLG6vWlUl0eaIeZ4v0VvH3rmcVEU6hlNwm3YcYzAsAqKjsDf/USf9qiV11zPL6AJOVqzvREIKUDB4Bx+tE8vOiw2gyA8qhh4cuY4hhHHCtsL4613fjvVWQdbXNCUssyIbiA2nPnElxJP04/X1pajUl1Wlt5MtfKyPLQwFhQ9gOaFu1/DDxFTi70roro0rJ2l9ivqrG80eteDoI681E7dqQSlIZWHlu5wpRa2j9M8CnlL1aSOpWh7ehWffNseWHqE5lzFdDwpvS6celC8w+tS7E3xU6xZyojyXGykEdCQDUst2tZslryS4SOmUnH9qgAc6c0a2+pB+VRT9DVabGZKPMFagzJIT5Twrds+qG4asSnz0+UlzipfC1HBfZKxMQv2SeR7cVQUa4IQoF1xasc/M3u/vUwga/tcRsIWw+pWOShrA/rWby9LvlgJK1OHqwqpHAD3VxtTWygFKzg89eKCq5sBWFOpB9+M1U48QYLisGCshXQvN0Jd7/aiiuPFgpz2cd2n9NtCf4XID5xQRb+KxEeQ2rPmXeO20sh1B6dFCuQ5MfyRtcRlfz8KFUdexcmyrzY8JlKuARIA/uKSxo9/QhTkVtDgWNn7l5KuPpmrw0ZpZfigfgh51xzZNvhE/itK/FhDYUVfLgd6HBmtuqVhYxWcIc3Wl1fcixnJPmIAKkJeSMDt1NSHSen9YRdRwGLg9LaiuOBThS6FJ289T71Vl0dkTSXTtsc1asxa2+Z7Wsx30eLrhaDSrKBjB+le2biOOKHFipYYCQSR6qOTSgNCsqaWrpJfhgrtTfPjBtJ4xT+EbeKa7qlTgwk05p5TSFHVoGTRDkRKwSPlV7cUsU0vPSupirc6Dj3q015b0Kgc0O4KZVRQgEHJz3xTROXDaDoeT14yGlE/fAqZGH5YOcE0jdigKPBBPoasCcH5gq5hI+UrMWqTIud5X+xJDzEgZUU+U6lBSOvBA/rTRfNR6hRZ0x1Q5c9hXKnyGgAB/DsSonaPU81qRWl4Lj/AJq4rTjh6qWnJNIbxoe03NsJehMHBz/hjj1+lV87w8gBrBQTsaN8dlxsrFGp7VcHkMS5lilNIVnyXFslOfpUbuOj5l5gEs26U4sAr3JycD6VsbUcayackNI+EjtgJzuLJwAD3IFcsfiTo62xpDT67endwEss5Wsnpk9gO/SqbcMYsYk6qbxvFcYrpYWt2npVolNuuKDSEOAuIKsKAr6O+BsCDL8ILY48FyW3W1EsKGNpUokjHB6ED3wDVGahleHepIk34l21x3FJ3JUlpYWlQ6BJScDp0xzUUOuda2CwKh6abkG1MtfunvhStRySMg4PTtmqkzRJlRyAdAfzVrHcMfHlYTd1+ShP4p9SN3rXH7PjW4wW4SltKcWokunCRnGcYG2heCHhhadTt3NFzjKKhDQ61JCQvy1FY7HjoOv1qoLzc5dzuLz05xx6SpR3KeUSc96v/wDDOX7lfJsRKWt/kMpASNpCcnjr7/zq/I4Nj4Q2EbpbPdTnxzeFi0fabeypbDbj5CUp/doKUg44xjjtis93OVITbknaUN8/Nwc/oK19+LbTrUDwbtMsxQ6piQApbik4aKhg85Cs9sDIrGEt/dZgpDJaTvxvbdyCfpmh7JPFFonJEY0zuf8AMoWkqV6gE0+w9Qts2luGpSHEKRtWEtjckg8fMR/Q1G2XkIewQVDB7YoDX5+M1OWhUweVOXbtD8htIfU+rbkJIxg+nI5+tIzqCCt9sPRCVEhKgSAkDPHempyyOSkJWlSnVEZwd5wPumkbDYjy2wrclQWB8yenPvUQaD0UzrHVSb434ZlbMV59WSf3Xm7QffJyKIVKkOhKJDL/AB/ElQUcfWmFy7/CuhKUIBBOCBg04Rro+WFFp5TaSOUHr/vTtibac7fGUHStlp5a/wDOhXlq/nUtsq3ElK0oebfSOXNoKkn/AKhUFgailRmpBS+flTyHAcH6cUrj6/kJASpwA4xwRTCw9kocArMh3F5ySkOyJIWPnD6m04GPTgZq9/C/XE1ZMBcqLcIyuQ7HTtUjjkqGMZ6DisdxNbXCbfGY6m0PNdVJSrYXABkgHPWtD+Gb8RD8CeuNMtoUrcGXCjGSCBwfnV17D04pu0g8qZjr6LRYeS8gKByKRy2icKSgKHRQzg49qTIn/uwU5APYjH8q8uaFoI3bffHSn0pLUR1aw/HSfLt8+elR+VplYCc9wRu71R2sNGtXI/Ft6ZvcKQVKyhbiCgH75OK0dPbMhG1LpUoc4P8A2qrNWREiQr4hm5JIOUrjPPKx68AUpCZ9VnK52WXGcP8AyzyOejgHH6U1rtElSVL+VOP4ScVe7mmoMgqITLWpIJC1echwe5G3mo5cbcxDUEqcub60/wDumVfMk/Qp6VGRSYQqcG5BwSQe9HISVY+Yn2qSuQoU7z3tlzbQhXKilCv1yRTUt5lp1QSQpA4BWAk/emFJSSjIHek8l9SMZJCexNOBdju52qWV+jYzikzkRMhB3fEEjslvikXJr1rqlM1gsSWT8QtYV8Rkc9evHv60msUMRo7SmpDbqHFYStsHc2rqQc9R71NdQ2W0ybU0/EQg3NsFxflKBGMdCkffrUIjqbtsny8hhJTlXm8pA9OBzVWJ4dHtaKUZ4PJSj40W554F5SpAP+InlQOcg59aMecVIYkP5L25QUt8Kx83f9e/FNlytxhNmQXAplwgp2noD6+lPvh/CYl3vzHJqGI0bCnm1qA8wdMYPVPPJ7UYwQ6SRsQPB/L3Q/JIijdLXT/aSO2ovVxkxVtw1eUH0lCmk52kn5ePU4NS9N1lvS3bfcoinlle1a3FHa0f9Qx04p91bfErsy4sMNpY2pKPIWD5SknOQrpj0xzUReu12dbcloliWXx+9U2dqj8uDu47dK0OoQRaW7YZC7uelEf7+SEYE8upt3lgb6c8gpda4ctK33LEBKjyT5RUtCuEg9RkHg5PGB0pxbiyLu4827H8x1l0siStOMHnI56n/wA4pHpfWIas6gtGZEMBsN42jCjgYA754pD+15qXVx0FTYDynXCo5UVKHr34ppzMWDGjbE5xuzQ7A9vxXHEnkyHulaKFAfXurGtN0kQ7Gwh1hBbaHluSZDgaQB0+p9KjMqTZmrjIiw47a5AXvDkcqKMYBwM9vtSG56fm3nR3mOy1KYDiilt14JCQCRuB7gc8HnrSF7Ts1FlZabXFeWknbuUUOLTjpnufbrxUWr6zKWwR7OKsFw5N8cen6p+maXFumkDub5o9KUk1DPhWcBLbEd+S4nctxI2KbJ4HA6/rTzFvEtEdhT8SMqGpKdvlLO8qx0CcACq7hSHVwmW3WihSG/3iCNylY6d8k0slOSXH0bQl1O5JU0p3YCex5/r7UPxtcnhmc50e0kUB0H39yreRokU0LWb7o2Sev3dgp7F1nBS8pBPlhYyFqI/N6U82m7C5jc2lSmxx5vG0n0FU3qaysW27vOIa81xJA3MOFTQXtyQM9ewqw9KzEwZHw65BlPuJSXCUhOxWMgD14PNavR9XzsrKdFkkFo7gAc9ll9W0jExMdsmPYPpfZTYK46ULdRaeAMdK7n1reLIXwhheKGFnpRWa4TS0m7ilhfcY6OAn/Sc4rq7rJKQkunaO2BSHJ7UEK69aUMB6pDK4dE8xdTTYacNuJP8A1JBrjuq7g5ypxB9/LT/tTMTmgk4poxoib2rvtcwFBxpLX7xKeVlTgPtinC1axlWxsoA3ZOd27n+dMBJrnepnY0Urdj28KBmZNE/exxtTiJ4hPw5aZbaNrnlhKtq0knBHtT614v3GWg7Bl5LaQEqWADhY5Ax6VVR+n617GR0Bqi/ScR/JYiEeu5sZoP4WoNMaokXO3sqS8npgjceDsScfqam0GVIc4UvOEg9c5qlvCC5qFmcaCAQh3OVe6Uj+1W3bpy3JCQEpCS2CRXmWoY4gnfGB0K9a0/IOTjslJ6hSNh1Sk5IP3oDw3HOBQWX+MYrzjuTgUH7oskimASTihIY54AowrNGNj5eeadaSk1ykkE+lIVhJ74pzmAlR/Sm9beVVIEwhdQyFAAf1oqTFyk8GlTIAxjFHSlpLZ4xxTSOUt8KIT9OxLkjZKR56cdFHiopdfBHS90Hz2xkKOcLSn5k564qxMJJ6fyoQAAqR1kUUwNF2Fn66/h7hsvqERVzCduQW1pKU/bIyfrRVt8IbhEbUvzpYDiTuLwwvHvtPP0q/XW8nI4NJGUupBC1DqeU9hTPDaeyaRXdY28QfA9yyR/jHJKm2XMlbi2iTu7YB7UZ4OabvFn1G+9GU0hpTaEF5Si2QcjoQCc/atK+IWjrZfozKJnzLCwr689+OlcR5WiLS43FH5XBh0YUcfSkfANhNqJrqkHCR/jKKj+Gi3LBSQmcxvJJO0/N049a+eZmLZScBKx1wpOf6Vrf8TXiBd9Q+GLsOWxIbgIltuMLUvCSRn8yCOevHSsbfFqLh3/Tk4oTFA5gP1Rt+Sx4o+iDOnSFyUOcJG0ABAwK9Fuq1KSlQSkA/mOaIuUwyXeD2xwc0RHUPMBV8vvVwXXKEOLd3lKtgzSu3R0bkkOMqOQjOAPTuagdwnjY984WcjGUYI/nUvgW1V7sC32LpFbLLBBYWoqdc9gAD6eoqt5DEj5woHPvUMPdEc2gGEeic0S0uqSSMk+hp5t+plRGlNITwoYyVVEW0qBKVAcCl8Noqb3FJxmp3dFSYOU4SphmSpGX9iiCdvPp0pnblueW4kOFGBnlJ5PtS9xjM5agopGOKY5M3CNoQd3c9qVnmTJW7eU96TeLt9ieY8tgbx+9TyUe9bI8KIjrPkMPNONsKAcTJddSoOHqlTeO30rHGiFqZvsJ5LAe2OJUUKBI4I7Ait+abmQUwYqG9kZbiQQkZGDjnbuGR/wCdaY7qpIeimCVHaAT2oXQ0l8wnoc0ILI6nFcpkpQEoVkAZ9cUB5SgoKGdv0JNB8w5oQd9j9q5cme5xnpmMTH4/fc2pwKT9MDFRlzRkeVKS49qK4rd3ZSXHFcewGM/zqwEndxRyEpOCUpP2ppS9VR+pvj9GSlLVq5wMrTtQX4CVZ/07igimKdBn6gYjviHAuoUrIdU0yefcJSMitKtw47qAhbDakZ6KSCP0NHptsJtYLUdltzGApKACKbS6rWL9SaTu77YJsUeAlKvzxoqGQT6ZznFRGXBmwFDzUBJ7bljn9DW+JiERkHzXQpJ7OKGB+tRe5Lssr93KXakp6BLvlK/qOKbtSFtr/9k="); + } + + @Test + public void baidu(){ + try { + String jsonResults = ""; + // 设置搜索关键词 + String keyword = "俄乌冲突"; + for (int i = 0; i < 3;i++) { + if (StringUtils.isNotBlank(jsonResults) && !jsonResults.equals("[]")) { + break; + } + Map map = new HashMap(); + map.put("signature", ""); + map.put("secret_id", ""); + map.put("num", 1); + String ipResult = HttpClientUtils.httpGetString("https://dps.kdlapi.com/api/getdps", map); + String[] split = ipResult.split(":"); + Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(split[0], Integer.parseInt(split[1]))); + // 发送GET请求 + Document document = Jsoup.connect("https://www.baidu.com/s?wd=" + keyword).proxy(proxy).get(); + + // 解析返回结果 + Elements results = document.select("div.result"); + + // 创建JSON数组 + JSONArray jsonArray = new JSONArray(); + + // 遍历每个搜索结果 + for (Element result : results) { + // 提取标题和URL + String title = result.select("h3").first().text(); + String url = result.select("h3 a").first().attr("href"); + // 创建JSON对象 + JSONObject jsonObject = new JSONObject(); + jsonObject.put("title", title); + jsonObject.put("url", url); + jsonObject.put("content", result.text()); + // 将JSON对象添加到数组中 + jsonArray.add(jsonObject); + } + + // 将JSON数组转换为字符串 + jsonResults = jsonArray.toJSONString(); + + // 打印JSON字符串 + System.out.println(ipResult+jsonResults); + } + System.out.println("success:"+jsonResults); + } catch (Exception e) { + e.printStackTrace(); + } + } + + @Test + public void weibo(){ + // 1.生成httpclient,相当于该打开一个浏览器 + CloseableHttpClient httpClient = HttpClients.createDefault(); + CloseableHttpResponse response = null; + // 2.创建get请求,相当于在浏览器地址栏输入 网址 + HttpGet request = new HttpGet("https://tophub.today/n/Om4ejl3vxE"); + // 设置请求头,将爬虫伪装成浏览器 + request.setHeader("User-Agent", + "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36"); + //如果有ip代理,可以加上如下代码 + Map map = new HashMap(); + map.put("signature", ""); + map.put("secret_id", ""); + map.put("num", 1); + String ipResult = HttpClientUtils.httpGetString("https://dps.kdlapi.com/api/getdps", map); + String[] split = ipResult.split(":"); + HttpHost proxy = new HttpHost(split[0], Integer.parseInt(split[1])); + RequestConfig config = RequestConfig.custom().setProxy(proxy).build(); + request.setConfig(config); + try { + // 3.执行get请求,相当于在输入地址栏后敲回车键 + response = httpClient.execute(request); + + // 4.判断响应状态为200,进行处理 + if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) { + // 5.获取响应内容 + HttpEntity httpEntity = response.getEntity(); + String html = EntityUtils.toString(httpEntity, "utf-8"); + // 6.Jsoup解析html + Document document = Jsoup.parse(html); + // 像js一样,通过标签获取title + Element item = document.getElementsByTag("tbody").first(); + Elements items = item.getElementsByTag("tr"); + int i =0; + for (Element tmp : items) { + Element rankEle = tmp.getElementsByTag("td").first(); + Elements textEle = tmp.select(".al").select("a"); + JSONObject jsonObject = new JSONObject(); + System.out.println(rankEle.text() + " " + textEle.text()); + String herf = textEle.select("a").attr("href"); +// System.out.println("herf:" + "https://tophub.today" + herf); + Elements td2 = items.get(i).getElementsByTag("td").next().next(); + String td2Text = td2.text(); + i++; + jsonObject.put("top", rankEle.text()); + jsonObject.put("title", textEle.text()); + jsonObject.put("url", "https://tophub.today" + herf); + //1. 可以在中括号内加上任何想要删除的字符,实际上是一个正则表达式 + String regExp="[\n`~!@#$%^&*()+=|{}':;',\\[\\]<>/?~!@#¥%……&*()——+|{}【】‘;:”“’。, 、?\uE652]"; + //2. 这里是将特殊字符换为空字符串,""代表直接去掉 + String replace = ""; + //3. 要处理的字符串 + td2Text = td2Text.replaceAll(regExp,replace); + jsonObject.put("level", td2Text); + System.out.println(jsonObject); + } + } else { + // 如果返回状态不是200,比如404(页面不存在)等,根据情况做处理,这里略 + System.out.println("返回状态不是200"); + System.out.println(EntityUtils.toString(response.getEntity(), "utf-8")); + } + } catch (Exception e) { + e.printStackTrace(); + } finally { + // 6.关闭 + } } } From 763bcbe30b1474483ef1df6a7f789828d051cd80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E4=B8=80=E5=87=A1?= Date: Tue, 21 Nov 2023 16:41:40 +0800 Subject: [PATCH 02/12] =?UTF-8?q?1=E3=80=81Link=20AI=20api=E6=94=AF?= =?UTF-8?q?=E6=8C=81=202=E3=80=81=E8=87=AA=E5=8A=A8=E9=80=89=E6=8B=A9?= =?UTF-8?q?=E6=89=80=E9=9C=80=E6=8F=92=E6=9E=B6=E6=8E=A5=E5=8F=A3=E6=94=AF?= =?UTF-8?q?=E6=8C=81=203=E3=80=81=E7=94=BB=E5=9B=BE=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E6=94=AF=E6=8C=81=EF=BC=8CMJ+SD=EF=BC=8C=E7=94=9F=E6=88=90?= =?UTF-8?q?=E5=90=8E=E8=87=AA=E5=8A=A8=E4=B8=8A=E4=BC=A0=E8=87=B3OSS=204?= =?UTF-8?q?=E3=80=81=E8=AF=AD=E9=9F=B3=E8=81=8A=E5=A4=A9=E6=94=AF=E6=8C=81?= =?UTF-8?q?=EF=BC=8COPEN=20AI=EF=BC=88asr->chat->tts=EF=BC=89=205=E3=80=81?= =?UTF-8?q?fix=20bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 61 ++++- .../chat/AbstractGptFunctionHandler.java | 7 +- .../com/ai/aigenerate/chat/ChatService.java | 226 ++++++++++++++++-- .../aigenerate/chat/GptFunctionFactory.java | 6 +- .../ai/aigenerate/chat/LinkAiChatService.java | 203 ++++++++++++++++ .../custom/BilibiliGtpFunctionHandler.java | 54 +++++ .../chat/tool/AliyunDrawService.java | 71 ++++++ .../aigenerate/chat/tool/BilibiliService.java | 17 ++ .../ai/aigenerate/chat/tool/MjService.java | 12 +- .../chat/tool/StableDiffusionService.java | 31 +++ .../chat/tool/TranslateService.java | 36 +++ .../ai/aigenerate/chat/tool/WeiboService.java | 2 +- .../config/ApiKeyMapProperties.java | 20 ++ .../com/ai/aigenerate/config/GptConfig.java | 40 ++++ .../aigenerate/config/GptFunctionConfig.java | 22 -- .../com/ai/aigenerate/config/JuheKey.java | 2 +- .../ai/aigenerate/constant/LinkAiContent.java | 6 + .../ai/aigenerate/constant/VoiceContent.java | 11 + .../com/ai/aigenerate/facade/ChatFacade.java | 35 ++- .../com/ai/aigenerate/facade/ImageFacade.java | 59 +++++ .../request/Bilibili/BilibiliRequest.java | 9 + .../model/request/chat/ChatVoiceRequest.java | 10 + .../model/request/chat/DrawRequest.java | 9 + .../model/request/chat/LinkAiChatRequest.java | 30 +++ .../stablediffusion/SdTextToImageRequest.java | 11 + .../stablediffusion/TextToImageDTO.java | 34 +++ .../response/bilibili/BilibiliResponse.java | 15 ++ .../response/chat/DrawImageResponse.java | 9 + .../model/response/chat/VoiceResponse.java | 13 + .../model/response/stablediffusion/Meta.java | 37 +++ .../stablediffusion/TextToImageRespDTO.java | 19 ++ .../ai/aigenerate/service/ChatGptService.java | 8 +- .../utils/{MdcUtil.java => MdcUtils.java} | 2 +- .../com/ai/aigenerate/utils/OssUtils.java | 68 ++++++ 34 files changed, 1131 insertions(+), 64 deletions(-) create mode 100644 src/main/java/com/ai/aigenerate/chat/LinkAiChatService.java create mode 100644 src/main/java/com/ai/aigenerate/chat/custom/BilibiliGtpFunctionHandler.java create mode 100644 src/main/java/com/ai/aigenerate/chat/tool/AliyunDrawService.java create mode 100644 src/main/java/com/ai/aigenerate/chat/tool/BilibiliService.java create mode 100644 src/main/java/com/ai/aigenerate/chat/tool/StableDiffusionService.java create mode 100644 src/main/java/com/ai/aigenerate/chat/tool/TranslateService.java create mode 100644 src/main/java/com/ai/aigenerate/config/ApiKeyMapProperties.java create mode 100644 src/main/java/com/ai/aigenerate/config/GptConfig.java delete mode 100644 src/main/java/com/ai/aigenerate/config/GptFunctionConfig.java create mode 100644 src/main/java/com/ai/aigenerate/constant/LinkAiContent.java create mode 100644 src/main/java/com/ai/aigenerate/constant/VoiceContent.java create mode 100644 src/main/java/com/ai/aigenerate/facade/ImageFacade.java create mode 100644 src/main/java/com/ai/aigenerate/model/request/Bilibili/BilibiliRequest.java create mode 100644 src/main/java/com/ai/aigenerate/model/request/chat/ChatVoiceRequest.java create mode 100644 src/main/java/com/ai/aigenerate/model/request/chat/DrawRequest.java create mode 100644 src/main/java/com/ai/aigenerate/model/request/chat/LinkAiChatRequest.java create mode 100644 src/main/java/com/ai/aigenerate/model/request/stablediffusion/SdTextToImageRequest.java create mode 100644 src/main/java/com/ai/aigenerate/model/request/stablediffusion/TextToImageDTO.java create mode 100644 src/main/java/com/ai/aigenerate/model/response/bilibili/BilibiliResponse.java create mode 100644 src/main/java/com/ai/aigenerate/model/response/chat/DrawImageResponse.java create mode 100644 src/main/java/com/ai/aigenerate/model/response/chat/VoiceResponse.java create mode 100644 src/main/java/com/ai/aigenerate/model/response/stablediffusion/Meta.java create mode 100644 src/main/java/com/ai/aigenerate/model/response/stablediffusion/TextToImageRespDTO.java rename src/main/java/com/ai/aigenerate/utils/{MdcUtil.java => MdcUtils.java} (96%) create mode 100644 src/main/java/com/ai/aigenerate/utils/OssUtils.java diff --git a/pom.xml b/pom.xml index 207f322..7d0ae0d 100644 --- a/pom.xml +++ b/pom.xml @@ -10,19 +10,20 @@ com.liyf chatgpt-plus - 0.0.1-SNAPSHOT + 1.0.1-SNAPSHOT chatgpt-plus chatgpt-plus - 17 + 19 4.5 - 1.18.8 + 1.18.26 32.0.0-jre - 1.0.14 + 1.1.3 4.13.2 3.12.0 1.6.2 2.0.31 + 4.3.7.RELEASE @@ -30,18 +31,47 @@ spring-boot-starter-web + + cn.hutool + hutool-all + 5.8.12 + + + + org.springframework.boot + spring-boot-starter-websocket + + org.apache.httpcomponents httpclient ${httpclient.version} + + com.alibaba + dashscope-sdk-java + 2.1.1 + + + + com.aliyun.oss + aliyun-sdk-oss + 3.17.1 + + org.projectlombok lombok ${lombok.version} + + com.twilio.sdk + twilio + 9.13.1 + + org.jsoup @@ -49,6 +79,24 @@ 1.13.1 + + io.github.hamawhitegg + langchain-core + 0.1.9 + + + + dev.langchain4j + langchain4j + 0.11.0 + + + + io.milvus + milvus-sdk-java + 2.2.8 + + com.google.guava guava @@ -114,6 +162,11 @@ org.springframework.boot spring-boot-maven-plugin + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + diff --git a/src/main/java/com/ai/aigenerate/chat/AbstractGptFunctionHandler.java b/src/main/java/com/ai/aigenerate/chat/AbstractGptFunctionHandler.java index f025d0d..e05bc92 100644 --- a/src/main/java/com/ai/aigenerate/chat/AbstractGptFunctionHandler.java +++ b/src/main/java/com/ai/aigenerate/chat/AbstractGptFunctionHandler.java @@ -1,18 +1,17 @@ package com.ai.aigenerate.chat; -import com.ai.aigenerate.utils.MdcUtil; +import com.ai.aigenerate.utils.MdcUtils; import com.unfbx.chatgpt.OpenAiClient; import com.unfbx.chatgpt.OpenAiStreamClient; import com.unfbx.chatgpt.entity.chat.*; import lombok.extern.slf4j.Slf4j; - import java.util.List; @Slf4j public abstract class AbstractGptFunctionHandler implements GptFunctionService { public ChatChoice preHandle(ChatChoice chatChoice){ - String requestId = MdcUtil.getTraceId(); + String requestId = MdcUtils.getTraceId(); Functions functions = getFunction(); GptContext gptContext = ContextMap.get(requestId); OpenAiClient openAiClient = gptContext.getOpenAiClient(); @@ -41,7 +40,7 @@ public ChatChoice preHandle(ChatChoice chatChoice){ } public ChatChoice streamHandle(ChatChoice chatChoice){ - String requestId = MdcUtil.getTraceId(); + String requestId = MdcUtils.getTraceId(); Functions functions = getFunction(); GptStreamContext gptStreamContext = ContextMap.getStreamContext(requestId); ChatCompletion chatCompletion = gptStreamContext.getChatCompletion(); diff --git a/src/main/java/com/ai/aigenerate/chat/ChatService.java b/src/main/java/com/ai/aigenerate/chat/ChatService.java index ae84483..55a59a5 100644 --- a/src/main/java/com/ai/aigenerate/chat/ChatService.java +++ b/src/main/java/com/ai/aigenerate/chat/ChatService.java @@ -1,28 +1,51 @@ package com.ai.aigenerate.chat; -import com.ai.aigenerate.config.GptFunctionConfig; +import cn.hutool.json.JSONArray; +import cn.hutool.json.JSONObject; +import com.ai.aigenerate.config.GptConfig; +import com.ai.aigenerate.constant.VoiceContent; import com.ai.aigenerate.model.request.chat.ChatRequest; import com.ai.aigenerate.model.response.chat.ChatResponse; import com.ai.aigenerate.model.response.chat.FunctionResponse; -import com.ai.aigenerate.utils.MdcUtil; +import com.ai.aigenerate.utils.MdcUtils; +import com.alibaba.fastjson.JSON; import com.unfbx.chatgpt.OpenAiClient; import com.unfbx.chatgpt.OpenAiStreamClient; +import com.unfbx.chatgpt.entity.Tts.TextToSpeech; +import com.unfbx.chatgpt.entity.Tts.TtsFormat; +import com.unfbx.chatgpt.entity.Tts.TtsVoice; import com.unfbx.chatgpt.entity.chat.*; +import com.unfbx.chatgpt.entity.whisper.Translations; +import com.unfbx.chatgpt.entity.whisper.Whisper; +import com.unfbx.chatgpt.entity.whisper.WhisperResponse; import com.unfbx.chatgpt.function.KeyRandomStrategy; import com.unfbx.chatgpt.interceptor.DynamicKeyOpenAiAuthInterceptor; import com.unfbx.chatgpt.interceptor.OpenAILogger; import com.unfbx.chatgpt.interceptor.OpenAiResponseInterceptor; import jakarta.annotation.PostConstruct; +import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import okhttp3.OkHttpClient; +import okhttp3.ResponseBody; import okhttp3.logging.HttpLoggingInterceptor; +import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @@ -34,7 +57,7 @@ public class ChatService { private GptFunctionFactory gptFunctionFactory; @Autowired - private GptFunctionConfig gptFunctionConfig; + private GptConfig gptConfig; private OpenAiClient openAiClient; @@ -50,13 +73,13 @@ public void init(){ .Builder() .addInterceptor(httpLoggingInterceptor) .addInterceptor(new OpenAiResponseInterceptor()) - .connectTimeout(10, TimeUnit.SECONDS) - .writeTimeout(30, TimeUnit.SECONDS) - .readTimeout(30, TimeUnit.SECONDS) + .connectTimeout(100, TimeUnit.SECONDS) + .writeTimeout(300, TimeUnit.SECONDS) + .readTimeout(300, TimeUnit.SECONDS) .build(); openAiClient = OpenAiClient.builder() //支持多key传入,请求时候随机选择 - .apiKey(gptFunctionConfig.getChatgptApiKey()) + .apiKey(gptConfig.getChatgptApiKey()) //自定义key的获取策略:默认KeyRandomStrategy .keyStrategy(new KeyRandomStrategy()) .authInterceptor(new DynamicKeyOpenAiAuthInterceptor()) @@ -64,7 +87,7 @@ public void init(){ .build(); openAiStreamClient = OpenAiStreamClient.builder() //支持多key传入,请求时候随机选择 - .apiKey(gptFunctionConfig.getChatgptApiKey()) + .apiKey(gptConfig.getChatgptApiKey()) //自定义key的获取策略:默认KeyRandomStrategy .keyStrategy(new KeyRandomStrategy()) .authInterceptor(new DynamicKeyOpenAiAuthInterceptor()) @@ -74,8 +97,8 @@ public void init(){ public ChatResponse chat(ChatRequest chatRequest){ ChatResponse chatResponse = ChatResponse.builder().status("200").build(); - String traceId = MdcUtil.generateTraceId(); - MdcUtil.setTraceId(traceId); + String traceId = MdcUtils.generateTraceId(); + MdcUtils.setTraceId(traceId); Message message = Message.builder().role(Message.Role.USER).content(chatRequest.getPrompt()).build(); List messages = chatRequest.getMessages(); if (messages == null) @@ -85,16 +108,71 @@ public ChatResponse chat(ChatRequest chatRequest){ ChatCompletion chatCompletion = ChatCompletion .builder() .messages(messages) - .maxTokens(chatRequest.getMaxTokens() != null?chatRequest.getMaxTokens():8000) + .maxTokens(chatRequest.getMaxTokens() != null?chatRequest.getMaxTokens():4097) .temperature(chatRequest.getTemperature() != null?chatRequest.getTemperature():0.2) .topP(chatRequest.getTopP() != null?chatRequest.getTopP():1.0) .n(chatRequest.getN() != null?chatRequest.getN():1) .model(chatRequest.getModel() != null?chatRequest.getModel() : ChatCompletion.Model.GPT_3_5_TURBO_16K_0613.getName()) .build(); - if (chatRequest.getIsFunction()) { + if (chatRequest.getIsFunction() != null && chatRequest.getIsFunction()) { chatCompletion.setFunctions(gptFunctionFactory.getFunctionsByFunctionNameList(chatRequest.getFunctionNameList())); chatCompletion.setFunctionCall("auto"); } + GptContext gptContext = GptContext.builder() + .gptHandlerHistories(new ArrayList<>()) + .messages(messages) + .openAiClient(openAiClient) + .chatCompletion(chatCompletion) + .requestId(chatRequest.getRequestId()) + .timeout(1200000000l) + .build(); + ContextMap.put(traceId, gptContext); + ChatCompletionResponse chatCompletionResponse = openAiClient.chatCompletion(chatCompletion); + String rs = doHandler(chatCompletionResponse.getChoices().get(0)); + chatResponse.setResult(rs); + log.info("traceId:{},成功获取结果,调用链路:{}", traceId, gptContext.getGptHandlerHistories()); + } catch (Exception e) { + log.error("traceId:{},调用chat接口异常", traceId, e); + chatResponse.setStatus("500"); + } finally { + ContextMap.remove(traceId); + MdcUtils.removeTraceId(); + } + return chatResponse; + } + + public ChatResponse chatDefaultFunction(ChatRequest chatRequest){ + ChatResponse chatResponse = ChatResponse.builder().status("200").build(); + List messages = new ArrayList<>(); + Message systemMessage = Message.builder().role(Message.Role.SYSTEM).content(gptConfig.getSystemPrompt()).build(); + messages.add(systemMessage); + if (org.apache.commons.collections4.CollectionUtils.isNotEmpty(chatRequest.getMessages())){ + for (Message message1:chatRequest.getMessages()){ + if (StringUtils.isNotBlank(message1.getContent())){ + messages.add(message1); + } + } + } + Message message = Message.builder().role(Message.Role.USER).content(chatRequest.getPrompt()).build(); + messages.add(message); + String traceId = MdcUtils.generateTraceId(); + try { + ChatCompletion chatCompletion = ChatCompletion + .builder() + .messages(messages) + .maxTokens(chatRequest.getMaxTokens() != null?chatRequest.getMaxTokens():8000) + .temperature(chatRequest.getTemperature() != null?chatRequest.getTemperature():0.2) + .topP(chatRequest.getTopP() != null?chatRequest.getTopP():1.0) + .n(chatRequest.getN() != null?chatRequest.getN():1) + .model(chatRequest.getModel() != null?chatRequest.getModel() : ChatCompletion.Model.GPT_3_5_TURBO_16K_0613.getName()) + .build(); + if (chatRequest.getIsFunction() != null && chatRequest.getIsFunction()) { + List functionList = autoFindFunction(chatRequest); + if (!CollectionUtils.isEmpty(functionList)){ + chatCompletion.setFunctions(gptFunctionFactory.getFunctionsByFunctionNameList(functionList)); + chatCompletion.setFunctionCall("auto"); + } + } GptContext gptContext = GptContext.builder() .gptHandlerHistories(new ArrayList<>()) .messages(messages) @@ -103,6 +181,7 @@ public ChatResponse chat(ChatRequest chatRequest){ .requestId(chatRequest.getRequestId()) .timeout(120000l) .build(); + MdcUtils.setTraceId(traceId); ContextMap.put(traceId, gptContext); ChatCompletionResponse chatCompletionResponse = openAiClient.chatCompletion(chatCompletion); String rs = doHandler(chatCompletionResponse.getChoices().get(0)); @@ -113,11 +192,46 @@ public ChatResponse chat(ChatRequest chatRequest){ chatResponse.setStatus("500"); } finally { ContextMap.remove(traceId); - MdcUtil.removeTraceId(); + MdcUtils.removeTraceId(); } return chatResponse; } + private List autoFindFunction(ChatRequest chatRequest) { + + ChatRequest completionRequest = new ChatRequest(); + List roleList = new ArrayList<>(); + List functions = gptFunctionFactory.getFunctions(); + JSONArray jsonArray = new JSONArray(); + for (Functions function:functions){ + JSONObject jsonObject = new JSONObject(); + jsonObject.putOpt("函数名",function.getName()); + jsonObject.putOpt("函数描述",function.getDescription()); + jsonArray.add(jsonObject); + } + Message systemMessage = Message.builder().role(Message.Role.SYSTEM).content("你现在是一个函数判断器,这是我的要求\n" + + "1、请根据函数描述返回需要使用的函数\n" + + "2、必须用json返回结果,例如[\"queryWeather\",\"sendMail\"],不要输出额外的内容,没有命中就返回空数组\n" + + "3、这是所有的函数定义:"+jsonArray).build(); + Message userMessage = Message.builder().role(Message.Role.USER).content("将上海天气发送给4198123131@qq.com").build(); + Message assistantMessage = Message.builder().role(Message.Role.ASSISTANT).content("[\"queryWeather\",\"sendMail\"]").build(); + Message userMessage1 = Message.builder().role(Message.Role.USER).content("你是谁").build(); + Message assistantMessage1 = Message.builder().role(Message.Role.ASSISTANT).content("[]").build(); + roleList.add(systemMessage); + roleList.add(userMessage); + roleList.add(assistantMessage); + roleList.add(userMessage1); + roleList.add(assistantMessage1); + completionRequest.setMessages(roleList); + completionRequest.setPrompt(chatRequest.getPrompt()); + completionRequest.setRequestId(chatRequest.getRequestId()); + completionRequest.setIsFunction(false); + completionRequest.setMaxTokens(12000); + completionRequest.setModel(ChatCompletion.Model.GPT_3_5_TURBO_16K.getName()); + String result = chat(completionRequest).getResult(); + return JSON.parseArray(result,String.class); + } + public String doHandler(ChatChoice chatChoice){ String content = chatChoice.getMessage().getContent(); if (null == chatChoice.getMessage().getFunctionCall()){ @@ -171,8 +285,8 @@ public SseEmitter createSse(String requestId) { public void chatStream(ChatRequest chatRequest, SseEmitter sseEmitter){ - String traceId = MdcUtil.generateTraceId(); - MdcUtil.setTraceId(traceId); + String traceId = MdcUtils.generateTraceId(); + MdcUtils.setTraceId(traceId); FunctionEventSourceListener eventSourceListener = new FunctionEventSourceListener(sseEmitter); Message message = Message.builder().role(Message.Role.USER).content(chatRequest.getPrompt()).build(); List messages = chatRequest.getMessages(); @@ -189,7 +303,7 @@ public void chatStream(ChatRequest chatRequest, SseEmitter sseEmitter){ .n(chatRequest.getN() != null?chatRequest.getN():1) .model(chatRequest.getModel() != null?chatRequest.getModel() : ChatCompletion.Model.GPT_3_5_TURBO_16K_0613.getName()) .build(); - if (chatRequest.getIsFunction() && !CollectionUtils.isEmpty(chatRequest.getFunctionNameList())) { + if (chatRequest.getIsFunction() != null && chatRequest.getIsFunction() && !CollectionUtils.isEmpty(chatRequest.getFunctionNameList())) { chatCompletion.setFunctions(gptFunctionFactory.getFunctionsByFunctionNameList(chatRequest.getFunctionNameList())); chatCompletion.setFunctionCall("auto"); } @@ -212,7 +326,7 @@ public void chatStream(ChatRequest chatRequest, SseEmitter sseEmitter){ log.error("traceId:{},异常:{}", traceId, e); }finally { ContextMap.remove(traceId); - MdcUtil.removeTraceId(); + MdcUtils.removeTraceId(); sseEmitter.complete(); } } @@ -242,4 +356,82 @@ public List queryFunctionNameList(){ }).collect(Collectors.toList()); } + public String speechToTextTranslations(File file) { + Translations translations = Translations.builder() + .model(Whisper.Model.WHISPER_1.getName()) + .prompt("请你务必返回中文") + .temperature(0.2) + .responseFormat(Whisper.ResponseFormat.JSON.getName()) + .build(); + //语音转文字+翻译 + WhisperResponse whisperResponse = + openAiClient.speechToTextTranslations(file, translations); + return whisperResponse.getText(); + } + + public File textToSpeed(String text) { + TextToSpeech textToSpeech = TextToSpeech.builder() + .model(TextToSpeech.Model.TTS_1.getName()) + .input(text) + .voice(TtsVoice.NOVA.getName()) + .responseFormat(TtsFormat.MP3.getName()) + .build(); + File file = new File(VoiceContent.TTS_PATH +Math.random()+".mp3"); + CountDownLatch countDownLatch = new CountDownLatch(1); + openAiClient.textToSpeech(textToSpeech, new Callback() { + @SneakyThrows + @Override + public void onResponse(Call call, Response response) { + InputStream inputStream = response.body().byteStream(); + //创建文件 + if (!file.exists()) { + if (!file.getParentFile().exists()) + file.getParentFile().mkdir(); + try { + file.createNewFile(); + } catch (IOException e) { + e.printStackTrace(); + log.error("createNewFile IOException"); + } + } + + OutputStream os = null; + try { + os = new BufferedOutputStream(new FileOutputStream(file)); + byte data[] = new byte[8192]; + int len; + while ((len = inputStream.read(data, 0, 8192)) != -1) { + os.write(data, 0, len); + } + countDownLatch.countDown(); + } catch (IOException e) { + e.printStackTrace(); + } finally { + try { + inputStream.close(); + } catch (IOException e) { + e.printStackTrace(); + } + try { + if (os != null) { + os.close(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + @Override + public void onFailure(Call call, Throwable t) { + + } + }); + try { + countDownLatch.await(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + return file; + } } \ No newline at end of file diff --git a/src/main/java/com/ai/aigenerate/chat/GptFunctionFactory.java b/src/main/java/com/ai/aigenerate/chat/GptFunctionFactory.java index 763bca8..b541891 100644 --- a/src/main/java/com/ai/aigenerate/chat/GptFunctionFactory.java +++ b/src/main/java/com/ai/aigenerate/chat/GptFunctionFactory.java @@ -2,7 +2,7 @@ import com.ai.aigenerate.model.request.chat.FunctionDefinition; import com.ai.aigenerate.utils.HttpClientUtils; -import com.ai.aigenerate.utils.MdcUtil; +import com.ai.aigenerate.utils.MdcUtils; import com.alibaba.fastjson.JSON; import com.unfbx.chatgpt.entity.chat.Functions; import jakarta.annotation.PostConstruct; @@ -75,11 +75,11 @@ public Functions getFunction() { tempFunctionServiceMap.put(functionDefinition.getFunctions().getName(),tempService); gptFunctionServices.add(tempService); } - tempReqFunctionServiceMap.put(MdcUtil.getTraceId(),tempFunctionServiceMap); + tempReqFunctionServiceMap.put(MdcUtils.getTraceId(),tempFunctionServiceMap); return gptFunctionServices; } public GptFunctionService getGptFunctionServiceByTraceId(String functionName){ - return tempReqFunctionServiceMap.get(MdcUtil.getTraceId()).get(functionName); + return tempReqFunctionServiceMap.get(MdcUtils.getTraceId()).get(functionName); } } diff --git a/src/main/java/com/ai/aigenerate/chat/LinkAiChatService.java b/src/main/java/com/ai/aigenerate/chat/LinkAiChatService.java new file mode 100644 index 0000000..400d074 --- /dev/null +++ b/src/main/java/com/ai/aigenerate/chat/LinkAiChatService.java @@ -0,0 +1,203 @@ +package com.ai.aigenerate.chat; + +import com.ai.aigenerate.config.GptConfig; +import com.ai.aigenerate.constant.LinkAiContent; +import com.ai.aigenerate.model.request.chat.LinkAiChatRequest; +import com.ai.aigenerate.model.response.chat.ChatResponse; +import com.ai.aigenerate.utils.MdcUtils; +import com.unfbx.chatgpt.OpenAiClient; +import com.unfbx.chatgpt.OpenAiStreamClient; +import com.unfbx.chatgpt.entity.chat.ChatCompletion; +import com.unfbx.chatgpt.entity.chat.ChatCompletionResponse; +import com.unfbx.chatgpt.entity.chat.Message; +import com.unfbx.chatgpt.function.KeyRandomStrategy; +import com.unfbx.chatgpt.interceptor.DynamicKeyOpenAiAuthInterceptor; +import com.unfbx.chatgpt.interceptor.OpenAILogger; +import com.unfbx.chatgpt.interceptor.OpenAiResponseInterceptor; +import jakarta.annotation.PostConstruct; +import lombok.extern.slf4j.Slf4j; +import okhttp3.OkHttpClient; +import okhttp3.logging.HttpLoggingInterceptor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +@Slf4j +@Service +public class LinkAiChatService { + + @Autowired + private GptFunctionFactory gptFunctionFactory; + + @Autowired + private GptConfig gptConfig; + + private Map linkAiClientMap; + + private Map linkAiStreamClientMap; + + @PostConstruct + public void init(){ + HttpLoggingInterceptor httpLoggingInterceptor = new HttpLoggingInterceptor(new OpenAILogger()); + //!!!!千万别再生产或者测试环境打开BODY级别日志!!!! + //!!!生产或者测试环境建议设置为这三种级别:NONE,BASIC,HEADERS,!!! + httpLoggingInterceptor.setLevel(HttpLoggingInterceptor.Level.HEADERS); + OkHttpClient okHttpClient = new OkHttpClient + .Builder() + .addInterceptor(httpLoggingInterceptor) + .addInterceptor(new OpenAiResponseInterceptor()) + .connectTimeout(10, TimeUnit.SECONDS) + .writeTimeout(30, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .build(); + Map linkAiApiKeyMap = gptConfig.getLinkAiApiKeyMap(); + linkAiClientMap = linkAiApiKeyMap.entrySet().stream().collect(HashMap::new, (m, v) -> m.put(v.getKey(), OpenAiClient.builder() + //支持多key传入,请求时候随机选择 + .apiKey(Collections.singletonList(v.getValue())) + .apiHost(LinkAiContent.LINK_AI_DOMAIN) + //自定义key的获取策略:默认KeyRandomStrategy + .keyStrategy(new KeyRandomStrategy()) + .authInterceptor(new DynamicKeyOpenAiAuthInterceptor()) + .okHttpClient(okHttpClient) + .build()), HashMap::putAll); + linkAiStreamClientMap = linkAiApiKeyMap.entrySet().stream().collect(HashMap::new, (m, v) -> m.put(v.getKey(), OpenAiStreamClient.builder() + //支持多key传入,请求时候随机选择 + .apiKey(Collections.singletonList(v.getValue())) + .apiHost(LinkAiContent.LINK_AI_DOMAIN) + //自定义key的获取策略:默认KeyRandomStrategy + .keyStrategy(new KeyRandomStrategy()) + .authInterceptor(new DynamicKeyOpenAiAuthInterceptor()) + .okHttpClient(okHttpClient) + .build()), HashMap::putAll); + } + + public ChatResponse chat(LinkAiChatRequest chatRequest){ + ChatResponse chatResponse = ChatResponse.builder().status("200").build(); + String traceId = MdcUtils.generateTraceId(); + MdcUtils.setTraceId(traceId); + Message message = Message.builder().role(Message.Role.USER).content(chatRequest.getPrompt()).build(); + List messages = chatRequest.getMessages(); + if (messages == null) + messages = new ArrayList<>(); + messages.add(message); + try { + ChatCompletion chatCompletion = ChatCompletion + .builder() + .messages(messages) + .maxTokens(chatRequest.getMaxTokens() != null?chatRequest.getMaxTokens():8000) + .temperature(chatRequest.getTemperature() != null?chatRequest.getTemperature():0.2) + .topP(chatRequest.getTopP() != null?chatRequest.getTopP():1.0) + .n(chatRequest.getN() != null?chatRequest.getN():1) + .model(chatRequest.getModel() != null?chatRequest.getModel() : ChatCompletion.Model.GPT_3_5_TURBO_16K_0613.getName()) + .build(); + OpenAiClient openAiClient = linkAiClientMap.get(chatRequest.getKnowledgeBase()); + GptContext gptContext = GptContext.builder() + .gptHandlerHistories(new ArrayList<>()) + .messages(messages) + .openAiClient(openAiClient) + .chatCompletion(chatCompletion) + .requestId(chatRequest.getRequestId()) + .timeout(120000l) + .build(); + ContextMap.put(traceId, gptContext); + ChatCompletionResponse chatCompletionResponse = openAiClient.chatCompletion(chatCompletion); + String rs = chatCompletionResponse.getChoices().get(0).getMessage().getContent(); + chatResponse.setResult(rs); + log.info("traceId:{},成功获取结果,调用链路:{}", traceId, gptContext.getGptHandlerHistories()); + } catch (Exception e) { + log.error("traceId:{},调用chat接口异常", traceId, e); + chatResponse.setStatus("500"); + } finally { + ContextMap.remove(traceId); + MdcUtils.removeTraceId(); + } + return chatResponse; + } + + public SseEmitter createSse(String requestId) { + //默认30秒超时,设置为0L则永不超时 + SseEmitter sseEmitter = new SseEmitter(0l); + //完成后回调 + sseEmitter.onCompletion(() -> { + log.info("[{}]结束连接...................", requestId); + }); + //超时回调 + sseEmitter.onTimeout(() -> { + log.info("[{}]连接超时...................", requestId); + }); + //异常回调 + sseEmitter.onError( + throwable -> { + try { + log.info("[{}]连接异常,{}", requestId, throwable.toString()); + sseEmitter.send(SseEmitter.event() + .id(requestId) + .name("发生异常!") + .data(Message.builder().content("发生异常请重试!").build()) + .reconnectTime(3000)); + } catch (IOException e) { + e.printStackTrace(); + } + } + ); + try { + sseEmitter.send(SseEmitter.event()); + } catch (IOException e) { + e.printStackTrace(); + } + log.info("[{}]创建sse连接成功!", requestId); + return sseEmitter; + } + + + public void chatStream(LinkAiChatRequest chatRequest, SseEmitter sseEmitter){ + String traceId = MdcUtils.generateTraceId(); + MdcUtils.setTraceId(traceId); + FunctionEventSourceListener eventSourceListener = new FunctionEventSourceListener(sseEmitter); + Message message = Message.builder().role(Message.Role.USER).content(chatRequest.getPrompt()).build(); + List messages = chatRequest.getMessages(); + if (messages == null) + messages = new ArrayList<>(); + messages.add(message); + try { + ChatCompletion chatCompletion = ChatCompletion + .builder() + .messages(messages) + .maxTokens(chatRequest.getMaxTokens() != null?chatRequest.getMaxTokens():2048) + .temperature(chatRequest.getTemperature() != null?chatRequest.getTemperature():0.2) + .topP(chatRequest.getTopP() != null?chatRequest.getTopP():1.0) + .n(chatRequest.getN() != null?chatRequest.getN():1) + .model(chatRequest.getModel() != null?chatRequest.getModel() : ChatCompletion.Model.GPT_3_5_TURBO_16K_0613.getName()) + .build(); + OpenAiStreamClient openAiClient = linkAiStreamClientMap.get(chatRequest.getKnowledgeBase()); + GptStreamContext gptStreamContext = GptStreamContext.builder() + .gptHandlerHistories(new ArrayList<>()) + .messages(messages) + .openAiStreamClient(openAiClient) + .chatCompletion(chatCompletion) + .requestId(chatRequest.getRequestId()) + .functionEventSourceListener(eventSourceListener) + .timeout(120000l) + .build(); + ContextMap.putStreamContext(traceId, gptStreamContext); + openAiClient.streamChatCompletion(chatCompletion, eventSourceListener); + log.info("traceId:{},成功获取结果,调用链路:{}", traceId, gptStreamContext.getGptHandlerHistories()); + ContextMap.remove(traceId); + } catch (Exception e) { + log.error("traceId:{},异常:{}", traceId, e); + }finally { + ContextMap.remove(traceId); + MdcUtils.removeTraceId(); + sseEmitter.complete(); + } + } + +} diff --git a/src/main/java/com/ai/aigenerate/chat/custom/BilibiliGtpFunctionHandler.java b/src/main/java/com/ai/aigenerate/chat/custom/BilibiliGtpFunctionHandler.java new file mode 100644 index 0000000..0ff35ad --- /dev/null +++ b/src/main/java/com/ai/aigenerate/chat/custom/BilibiliGtpFunctionHandler.java @@ -0,0 +1,54 @@ +package com.ai.aigenerate.chat.custom; + +import cn.hutool.json.JSONObject; +import com.ai.aigenerate.chat.AbstractGptFunctionHandler; +import com.ai.aigenerate.chat.tool.BilibiliService; +import com.ai.aigenerate.model.request.Bilibili.BilibiliRequest; +import com.ai.aigenerate.model.response.bilibili.BilibiliResponse; +import com.alibaba.fastjson2.JSON; +import com.unfbx.chatgpt.entity.chat.Functions; +import com.unfbx.chatgpt.entity.chat.Parameters; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.Arrays; + +@Component +public class BilibiliGtpFunctionHandler extends AbstractGptFunctionHandler { + + @Autowired + private BilibiliService bilibiliService; + + @Override + public String doHandle(String paramJson) { + BilibiliRequest bilibiliRequest = JSON.parseObject(paramJson, BilibiliRequest.class); + BilibiliResponse response = bilibiliService.getBilibiliVideo(bilibiliRequest); + String content = "{ " + + "\"视频的名称\": \""+response.getTitle()+"\"" + + "\"视频的介绍\": \""+response.getDesc()+"\"" + + "\"视频的up主\": \""+response.getUpName()+"\"" + + "\"视频的详细信息\": \""+response.getDetail()+"\"" + + "}"; + return content; + } + + @Override + public Functions getFunction() { + JSONObject videoUrl = new JSONObject(); + videoUrl.putOpt("type", "string"); + videoUrl.putOpt("description", "视频的url"); + //参数 + JSONObject properties = new JSONObject(); + properties.putOpt("videoUrl", videoUrl); + Parameters parameters = Parameters.builder() + .type("object") + .properties(properties) + .required(Arrays.asList("videoUrl")).build(); + Functions functions = Functions.builder() + .name("getBilibiliVideoInfo") + .description("获取bilibili视频信息") + .parameters(parameters) + .build(); + return functions; + } +} diff --git a/src/main/java/com/ai/aigenerate/chat/tool/AliyunDrawService.java b/src/main/java/com/ai/aigenerate/chat/tool/AliyunDrawService.java new file mode 100644 index 0000000..659cc41 --- /dev/null +++ b/src/main/java/com/ai/aigenerate/chat/tool/AliyunDrawService.java @@ -0,0 +1,71 @@ +package com.ai.aigenerate.chat.tool; + +import com.ai.aigenerate.utils.OssUtils; +import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesis; +import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesisParam; +import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesisResult; +import com.alibaba.dashscope.exception.ApiException; +import com.alibaba.dashscope.exception.NoApiKeyException; +import lombok.SneakyThrows; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import org.springframework.stereotype.Component; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Map; + +@Component +public class AliyunDrawService { + + private static final OkHttpClient CLIENT = new OkHttpClient(); + private static final String MODEL = "stable-diffusion-v1.5"; + private static final String PROMPT = "dog flying freely in the blue sky and white clouds"; + private static final String SIZE = "512*512"; + + @SneakyThrows + public String basicCall(String prompt) { + ImageSynthesis is = new ImageSynthesis(); + ImageSynthesisParam param = + ImageSynthesisParam.builder().apiKey("") + .model(MODEL) + .n(1) + .size(SIZE) + .prompt(prompt) + .negativePrompt("garfield") + .build(); + + ImageSynthesisResult result = is.call(param); + System.out.println(result); + // save image to local files. + for(Map item :result.getOutput().getResults()){ + String paths = new URL(item.get("url")).getPath(); + String[] parts = paths.split("/"); + String fileName = parts[parts.length-1]; + Request request = new Request.Builder() + .url(item.get("url")) + .build(); + + try (Response response = CLIENT.newCall(request).execute()) { + if (!response.isSuccessful()) { + throw new IOException("Unexpected code " + response); + } + + Path file = Paths.get(fileName); + Files.write(file, response.body().bytes()); + InputStream inputStream = new BufferedInputStream(file.toUri().toURL().openStream()); + return OssUtils.upload(inputStream); + } catch (Exception e) { + throw new RuntimeException(e); + } + + } + return null; + } +} diff --git a/src/main/java/com/ai/aigenerate/chat/tool/BilibiliService.java b/src/main/java/com/ai/aigenerate/chat/tool/BilibiliService.java new file mode 100644 index 0000000..60aa2f3 --- /dev/null +++ b/src/main/java/com/ai/aigenerate/chat/tool/BilibiliService.java @@ -0,0 +1,17 @@ +package com.ai.aigenerate.chat.tool; + +import com.ai.aigenerate.model.request.Bilibili.BilibiliRequest; +import com.ai.aigenerate.model.response.bilibili.BilibiliResponse; +import com.ai.aigenerate.utils.HttpClientUtils; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import org.springframework.stereotype.Component; + +@Component +public class BilibiliService { + + public BilibiliResponse getBilibiliVideo(BilibiliRequest bilibiliRequest) { + JSONObject jsonObject = HttpClientUtils.httpPost("http://localhost:5000/parseVideo", JSON.toJSONString(bilibiliRequest)); + return JSON.parseObject(jsonObject.toJSONString(), BilibiliResponse.class); + } +} diff --git a/src/main/java/com/ai/aigenerate/chat/tool/MjService.java b/src/main/java/com/ai/aigenerate/chat/tool/MjService.java index 1fbfc4f..87625ba 100644 --- a/src/main/java/com/ai/aigenerate/chat/tool/MjService.java +++ b/src/main/java/com/ai/aigenerate/chat/tool/MjService.java @@ -1,6 +1,6 @@ package com.ai.aigenerate.chat.tool; -import com.ai.aigenerate.config.GptFunctionConfig; +import com.ai.aigenerate.config.GptConfig; import com.ai.aigenerate.constant.MjConstant; import com.ai.aigenerate.model.request.mj.CreateTaskRequest; import com.ai.aigenerate.model.response.mj.MjTaskResponse; @@ -19,14 +19,14 @@ public class MjService { @Autowired - private GptFunctionConfig gptFunctionConfig; + private GptConfig gptConfig; private DelayQueue taskQueue = new DelayQueue<>(); public MjTaskResponse createTextTask(String prompt){ CreateTaskRequest createTaskRequest = new CreateTaskRequest(); createTaskRequest.setPrompt(prompt); - JSONObject jsonObject = HttpClientUtils.httpPost(gptFunctionConfig.getMjServiceUrl()+ MjConstant.IMAGE_URL, JSON.toJSONString(createTaskRequest)); + JSONObject jsonObject = HttpClientUtils.httpPost(gptConfig.getMjServiceUrl()+ MjConstant.IMAGE_URL, JSON.toJSONString(createTaskRequest)); return JSONObject.toJavaObject(jsonObject, MjTaskResponse.class); } @@ -34,7 +34,7 @@ public QueryTaskResponse getTask(String taskId){ if (StringUtils.isBlank(taskId)){ return null; } - JSONObject jsonObject = HttpClientUtils.httpGet(gptFunctionConfig.getMjServiceUrl()+ MjConstant.QUERY_TASK_URL+taskId+ MjConstant.QUERY_TASK_URL_FETCH); + JSONObject jsonObject = HttpClientUtils.httpGet(gptConfig.getMjServiceUrl()+ MjConstant.QUERY_TASK_URL+taskId+ MjConstant.QUERY_TASK_URL_FETCH); return JSONObject.toJavaObject(jsonObject, QueryTaskResponse.class); } @@ -42,7 +42,7 @@ public QueryTaskResponse getTask(String taskId){ public QueryTaskResponse addTask(String prompt){ DelayQueue taskQueue = new DelayQueue<>(); MjTaskResponse mjTaskResponse = createTextTask(prompt); - MjTaskDelayed mjTaskDelayed = new MjTaskDelayed(mjTaskResponse.getResult(), gptFunctionConfig.getMjServiceWaitTime()); + MjTaskDelayed mjTaskDelayed = new MjTaskDelayed(mjTaskResponse.getResult(), gptConfig.getMjServiceWaitTime()); int maxAttempts = 5; // 最大重试次数 int attempts = 0; while (attempts < maxAttempts) { @@ -57,7 +57,7 @@ public QueryTaskResponse addTask(String prompt){ return queryTaskResponse; } - mjTaskDelayed.resetDelay(gptFunctionConfig.getMjServiceWaitTime()); + mjTaskDelayed.resetDelay(gptConfig.getMjServiceWaitTime()); attempts++; } diff --git a/src/main/java/com/ai/aigenerate/chat/tool/StableDiffusionService.java b/src/main/java/com/ai/aigenerate/chat/tool/StableDiffusionService.java new file mode 100644 index 0000000..b2b9224 --- /dev/null +++ b/src/main/java/com/ai/aigenerate/chat/tool/StableDiffusionService.java @@ -0,0 +1,31 @@ +package com.ai.aigenerate.chat.tool; + +import com.ai.aigenerate.model.request.stablediffusion.SdTextToImageRequest; +import com.ai.aigenerate.model.request.stablediffusion.TextToImageDTO; +import com.ai.aigenerate.model.response.stablediffusion.TextToImageRespDTO; +import com.ai.aigenerate.utils.HttpClientUtils; +import com.alibaba.fastjson.JSONObject; +import com.alibaba.fastjson2.JSON; +import org.springframework.stereotype.Component; + +@Component +public class StableDiffusionService { + public String textToImage(SdTextToImageRequest sdTextToImageRequest){ + TextToImageDTO textToImageDTO = new TextToImageDTO(); + textToImageDTO.setKey(""); + textToImageDTO.setModel_id("midjourney"); + textToImageDTO.setPrompt(sdTextToImageRequest.getPrompt()); + textToImageDTO.setNegative_prompt(""); + textToImageDTO.setScheduler("EulerDiscreteScheduler"); + textToImageDTO.setWidth("1024"); + textToImageDTO.setHeight("1024"); + textToImageDTO.setSamples("1"); + textToImageDTO.setNum_inference_steps("30"); + textToImageDTO.setGuidance_scale(7.5); + textToImageDTO.setWebhook(null); + textToImageDTO.setTrack_id(null); + JSONObject jsonObject = HttpClientUtils.httpPost("https://stablediffusionapi.com/api/v4/dreambooth", JSON.toJSONString(textToImageDTO)); + TextToImageRespDTO textToImageRespDTO = jsonObject.toJavaObject(TextToImageRespDTO.class); + return textToImageRespDTO.getOutput().get(0); + } +} diff --git a/src/main/java/com/ai/aigenerate/chat/tool/TranslateService.java b/src/main/java/com/ai/aigenerate/chat/tool/TranslateService.java new file mode 100644 index 0000000..6a9dfc4 --- /dev/null +++ b/src/main/java/com/ai/aigenerate/chat/tool/TranslateService.java @@ -0,0 +1,36 @@ +package com.ai.aigenerate.chat.tool; + +import com.ai.aigenerate.chat.ChatService; +import com.ai.aigenerate.model.request.chat.ChatRequest; +import com.unfbx.chatgpt.entity.chat.ChatCompletion; +import com.unfbx.chatgpt.entity.chat.Message; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +@Component +public class TranslateService { + + @Autowired + private ChatService chatService; + + public String translate(String prompt){ + ChatRequest completionRequest = new ChatRequest(); + Message systemMessage = Message.builder().role(Message.Role.SYSTEM).content("你现在是一个AI翻译官,你会将我发给你的文字转换为英文描述,以下是我的要求\n" + + "1、所有返回均使用英文\n" + + "2、不要输出除了翻译的文字之外的内容\n" + + "3、我提供的任何语言都原封不动的转换为英文").build(); + List messageList = new ArrayList(); + messageList.add(systemMessage); + completionRequest.setMessages(messageList); + completionRequest.setPrompt(prompt); + completionRequest.setRequestId(UUID.randomUUID().toString()); + completionRequest.setIsFunction(false); + completionRequest.setMaxTokens(2000); + completionRequest.setModel(ChatCompletion.Model.GPT_3_5_TURBO.getName()); + String result = chatService.chat(completionRequest).getResult(); + return result; + } +} diff --git a/src/main/java/com/ai/aigenerate/chat/tool/WeiboService.java b/src/main/java/com/ai/aigenerate/chat/tool/WeiboService.java index 3f9aed8..c2d761e 100644 --- a/src/main/java/com/ai/aigenerate/chat/tool/WeiboService.java +++ b/src/main/java/com/ai/aigenerate/chat/tool/WeiboService.java @@ -132,7 +132,7 @@ private String queryWeiboResult(String type) { jsonObject.put("序号", rankEle.text()); String title = textEle.text().replaceAll(" ", "%20"); jsonObject.put("标题", textEle.text()); - jsonObject.put("链接地址", "https://s.weibo.com/weibo?q=%23" + title + "%23"); + jsonObject.put("链接地址", "https://s.weibo.com/weibo?q=" + title); //1. 可以在中括号内加上任何想要删除的字符,实际上是一个正则表达式 String regExp = "[\n`~!@#$%^&*()+=|{}':;',\\[\\]<>/?~!@#¥%……&*()——+|{}【】‘;:”“’。, 、?\uE652]"; //2. 这里是将特殊字符换为空字符串,""代表直接去掉 diff --git a/src/main/java/com/ai/aigenerate/config/ApiKeyMapProperties.java b/src/main/java/com/ai/aigenerate/config/ApiKeyMapProperties.java new file mode 100644 index 0000000..393adf5 --- /dev/null +++ b/src/main/java/com/ai/aigenerate/config/ApiKeyMapProperties.java @@ -0,0 +1,20 @@ +package com.ai.aigenerate.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +import java.util.Map; + +@Configuration +@ConfigurationProperties(prefix = "linkai.api.key.map") +public class ApiKeyMapProperties { + private Map map; + + public Map getMap() { + return map; + } + + public void setMap(Map map) { + this.map = map; + } +} diff --git a/src/main/java/com/ai/aigenerate/config/GptConfig.java b/src/main/java/com/ai/aigenerate/config/GptConfig.java new file mode 100644 index 0000000..658b20e --- /dev/null +++ b/src/main/java/com/ai/aigenerate/config/GptConfig.java @@ -0,0 +1,40 @@ +package com.ai.aigenerate.config; + +import lombok.Getter; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Getter +@Component +public class GptConfig { + + @Value("${mj.service.url:}") + private String mjServiceUrl; + + @Value("${mj.service.waitTime:90000}") + private Integer mjServiceWaitTime; + + @Value("${chatgpt.api.key}") + private List chatgptApiKey; + + @Value("${linkai.api.key.map}") + private String linkAiApiKeyMap; + + @Value("${system.prompt:}") + private String systemPrompt; + + public Map getLinkAiApiKeyMap(){ + Map map = new HashMap<>(); + String[] split = linkAiApiKeyMap.split(","); + for (String s : split) { + String[] split1 = s.split(":"); + map.put(split1[0],split1[1]); + } + return map; + } + +} diff --git a/src/main/java/com/ai/aigenerate/config/GptFunctionConfig.java b/src/main/java/com/ai/aigenerate/config/GptFunctionConfig.java deleted file mode 100644 index 99fb7e3..0000000 --- a/src/main/java/com/ai/aigenerate/config/GptFunctionConfig.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.ai.aigenerate.config; - -import lombok.Getter; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; - -import java.util.List; - -@Getter -@Component -public class GptFunctionConfig { - - @Value("${mj.service.url:}") - private String mjServiceUrl; - - @Value("${mj.service.waitTime:90000}") - private Integer mjServiceWaitTime; - - @Value("${chatgpt.api.key:}") - private List chatgptApiKey; - -} diff --git a/src/main/java/com/ai/aigenerate/config/JuheKey.java b/src/main/java/com/ai/aigenerate/config/JuheKey.java index eb421ef..92ef2cc 100644 --- a/src/main/java/com/ai/aigenerate/config/JuheKey.java +++ b/src/main/java/com/ai/aigenerate/config/JuheKey.java @@ -8,6 +8,6 @@ @Component public class JuheKey { - @Value("${juhe.news.key}") + @Value("${juhe.news.key:}") private String newsKey; } diff --git a/src/main/java/com/ai/aigenerate/constant/LinkAiContent.java b/src/main/java/com/ai/aigenerate/constant/LinkAiContent.java new file mode 100644 index 0000000..a574210 --- /dev/null +++ b/src/main/java/com/ai/aigenerate/constant/LinkAiContent.java @@ -0,0 +1,6 @@ +package com.ai.aigenerate.constant; + +public class LinkAiContent { + + public static final String LINK_AI_DOMAIN = "https://api.link-ai.chat/"; +} diff --git a/src/main/java/com/ai/aigenerate/constant/VoiceContent.java b/src/main/java/com/ai/aigenerate/constant/VoiceContent.java new file mode 100644 index 0000000..7ee54aa --- /dev/null +++ b/src/main/java/com/ai/aigenerate/constant/VoiceContent.java @@ -0,0 +1,11 @@ +package com.ai.aigenerate.constant; + +/** + * Change the path to your own + */ +public class VoiceContent { + + public static final String TTS_PATH = "/Users/liyifan/Downloads/voice/"; + + public static final String ASR_PATH = "/Users/liyifan/Downloads/test11111.mp3"; +} diff --git a/src/main/java/com/ai/aigenerate/facade/ChatFacade.java b/src/main/java/com/ai/aigenerate/facade/ChatFacade.java index f5decee..afb3199 100644 --- a/src/main/java/com/ai/aigenerate/facade/ChatFacade.java +++ b/src/main/java/com/ai/aigenerate/facade/ChatFacade.java @@ -1,7 +1,9 @@ package com.ai.aigenerate.facade; +import com.ai.aigenerate.chat.LinkAiChatService; import com.ai.aigenerate.model.request.chat.ChatRequest; import com.ai.aigenerate.chat.ChatService; +import com.ai.aigenerate.model.request.chat.LinkAiChatRequest; import com.ai.aigenerate.model.response.chat.ChatResponse; import com.ai.aigenerate.model.response.chat.FunctionResponse; import org.springframework.beans.factory.annotation.Autowired; @@ -23,6 +25,9 @@ public class ChatFacade { @Autowired private ChatService chatService; + @Autowired + private LinkAiChatService linkAiChatService; + @Autowired @Qualifier("streamThreadPool") private Executor executor; @@ -39,7 +44,7 @@ public ChatResponse chat(@RequestBody ChatRequest chatRequest){ } @PostMapping("chatStream") - public SseEmitter queryTask(@RequestBody ChatRequest chatRequest){ + public SseEmitter chatStream(@RequestBody ChatRequest chatRequest){ if (chatRequest.getToken() == null || !chatRequest.getToken().equals(token)){ throw new RuntimeException("token error"); } @@ -50,6 +55,34 @@ public SseEmitter queryTask(@RequestBody ChatRequest chatRequest){ return sseEmitter; } + @PostMapping("auto/chat") + public ChatResponse chatDefaultFunction(@RequestBody ChatRequest chatRequest){ + if (chatRequest.getToken() == null || !chatRequest.getToken().equals(token)){ + throw new RuntimeException("token error"); + } + return chatService.chatDefaultFunction(chatRequest); + } + + @PostMapping("/knowledgeBase/chat") + public ChatResponse knowledgeBaseChat(@RequestBody LinkAiChatRequest chatRequest){ + if (chatRequest.getToken() == null || !chatRequest.getToken().equals(token)){ + throw new RuntimeException("token error"); + } + return linkAiChatService.chat(chatRequest); + } + + @PostMapping("/knowledgeBase/chatStream") + public SseEmitter knowledgeBaseChatStream(@RequestBody LinkAiChatRequest chatRequest){ + if (chatRequest.getToken() == null || !chatRequest.getToken().equals(token)){ + throw new RuntimeException("token error"); + } + SseEmitter sseEmitter = linkAiChatService.createSse(chatRequest.getRequestId()); + executor.execute(() -> { + linkAiChatService.chatStream(chatRequest,sseEmitter); + }); + return sseEmitter; + } + @GetMapping("queryFunction") public List queryFunction(){ return chatService.queryFunctionNameList(); diff --git a/src/main/java/com/ai/aigenerate/facade/ImageFacade.java b/src/main/java/com/ai/aigenerate/facade/ImageFacade.java new file mode 100644 index 0000000..3d83c0e --- /dev/null +++ b/src/main/java/com/ai/aigenerate/facade/ImageFacade.java @@ -0,0 +1,59 @@ +package com.ai.aigenerate.facade; + +import com.ai.aigenerate.chat.tool.AliyunDrawService; +import com.ai.aigenerate.chat.tool.MjService; +import com.ai.aigenerate.chat.tool.StableDiffusionService; +import com.ai.aigenerate.chat.tool.TranslateService; +import com.ai.aigenerate.model.request.chat.DrawRequest; +import com.ai.aigenerate.model.request.stablediffusion.SdTextToImageRequest; +import com.ai.aigenerate.model.response.chat.DrawImageResponse; +import com.ai.aigenerate.model.response.mj.QueryTaskResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RequestMapping("ai") +@RestController +public class ImageFacade { + + @Autowired + private AliyunDrawService aliyunDrawService; + + @Autowired + private StableDiffusionService stableDiffusionService; + + @Autowired + private MjService mjService; + + @Autowired + private TranslateService translateService; + + @RequestMapping("drawImage") + public DrawImageResponse drawImage(@RequestBody DrawRequest drawRequest){ + String url = aliyunDrawService.basicCall(drawRequest.getPrompt()); + DrawImageResponse drawImageResponse = new DrawImageResponse(); + drawImageResponse.setImageUrl(url); + return drawImageResponse; + } + + @RequestMapping("sd/textToImage") + public DrawImageResponse textToImage(@RequestBody SdTextToImageRequest sdTextToImageRequest){ + String url = stableDiffusionService.textToImage(sdTextToImageRequest); + DrawImageResponse drawImageResponse = new DrawImageResponse(); + drawImageResponse.setImageUrl(url); + return drawImageResponse; + } + + @RequestMapping("mj/textToImage") + public DrawImageResponse createImage(@RequestBody SdTextToImageRequest sdTextToImageRequest){ + String prompt = translateService.translate(sdTextToImageRequest.getPrompt()); + QueryTaskResponse queryTaskResponse = mjService.addTask(prompt); + DrawImageResponse drawImageResponse = new DrawImageResponse(); + if (queryTaskResponse == null) { + return drawImageResponse; + } + drawImageResponse.setImageUrl(queryTaskResponse.getImageUrl()); + return drawImageResponse; + } +} diff --git a/src/main/java/com/ai/aigenerate/model/request/Bilibili/BilibiliRequest.java b/src/main/java/com/ai/aigenerate/model/request/Bilibili/BilibiliRequest.java new file mode 100644 index 0000000..508646d --- /dev/null +++ b/src/main/java/com/ai/aigenerate/model/request/Bilibili/BilibiliRequest.java @@ -0,0 +1,9 @@ +package com.ai.aigenerate.model.request.Bilibili; + +import lombok.Data; + +@Data +public class BilibiliRequest { + + private String videoUrl; +} diff --git a/src/main/java/com/ai/aigenerate/model/request/chat/ChatVoiceRequest.java b/src/main/java/com/ai/aigenerate/model/request/chat/ChatVoiceRequest.java new file mode 100644 index 0000000..d9366ff --- /dev/null +++ b/src/main/java/com/ai/aigenerate/model/request/chat/ChatVoiceRequest.java @@ -0,0 +1,10 @@ +package com.ai.aigenerate.model.request.chat; + +import lombok.Data; + +@Data +public class ChatVoiceRequest { + + private String question; + private String answer; +} diff --git a/src/main/java/com/ai/aigenerate/model/request/chat/DrawRequest.java b/src/main/java/com/ai/aigenerate/model/request/chat/DrawRequest.java new file mode 100644 index 0000000..104f943 --- /dev/null +++ b/src/main/java/com/ai/aigenerate/model/request/chat/DrawRequest.java @@ -0,0 +1,9 @@ +package com.ai.aigenerate.model.request.chat; + +import lombok.Data; + +@Data +public class DrawRequest { + + private String prompt; +} diff --git a/src/main/java/com/ai/aigenerate/model/request/chat/LinkAiChatRequest.java b/src/main/java/com/ai/aigenerate/model/request/chat/LinkAiChatRequest.java new file mode 100644 index 0000000..b991d18 --- /dev/null +++ b/src/main/java/com/ai/aigenerate/model/request/chat/LinkAiChatRequest.java @@ -0,0 +1,30 @@ +package com.ai.aigenerate.model.request.chat; + +import com.unfbx.chatgpt.entity.chat.Message; +import lombok.Data; + +import java.util.List; + +@Data +public class LinkAiChatRequest { + + private String requestId; + + private String prompt; + + private Double temperature; + + private Integer n; + + private String model; + + private Double topP; + + private Integer maxTokens; + + private List messages; + + private String knowledgeBase; + + private String token; +} diff --git a/src/main/java/com/ai/aigenerate/model/request/stablediffusion/SdTextToImageRequest.java b/src/main/java/com/ai/aigenerate/model/request/stablediffusion/SdTextToImageRequest.java new file mode 100644 index 0000000..4ed65e4 --- /dev/null +++ b/src/main/java/com/ai/aigenerate/model/request/stablediffusion/SdTextToImageRequest.java @@ -0,0 +1,11 @@ +package com.ai.aigenerate.model.request.stablediffusion; + +import lombok.Data; + +@Data +public class SdTextToImageRequest { + + private String prompt; + + private String model; +} diff --git a/src/main/java/com/ai/aigenerate/model/request/stablediffusion/TextToImageDTO.java b/src/main/java/com/ai/aigenerate/model/request/stablediffusion/TextToImageDTO.java new file mode 100644 index 0000000..5aca118 --- /dev/null +++ b/src/main/java/com/ai/aigenerate/model/request/stablediffusion/TextToImageDTO.java @@ -0,0 +1,34 @@ +package com.ai.aigenerate.model.request.stablediffusion; + +import lombok.Data; + +@Data +public class TextToImageDTO { + + private String key; + private String model_id; + private String prompt; + private String negative_prompt; + private String width; + private String height; + private String samples; + private String num_inference_steps; + private String safety_checker; + private String enhance_prompt; + private String seed; + private Double guidance_scale; + private String multi_lingual; + private String panorama; + private String self_attention; + private String upscale; + private String embeddings_model; + private String lora_model; + private String tomesd; + private String clip_skip; + private String use_karras_sigmas; + private String vae; + private String lora_strength; + private String scheduler; + private String webhook; + private String track_id; +} diff --git a/src/main/java/com/ai/aigenerate/model/response/bilibili/BilibiliResponse.java b/src/main/java/com/ai/aigenerate/model/response/bilibili/BilibiliResponse.java new file mode 100644 index 0000000..1ea9f8a --- /dev/null +++ b/src/main/java/com/ai/aigenerate/model/response/bilibili/BilibiliResponse.java @@ -0,0 +1,15 @@ +package com.ai.aigenerate.model.response.bilibili; + +import lombok.Data; + +@Data +public class BilibiliResponse { + + private String upName; + + private String title; + + private String detail; + + private String desc; +} diff --git a/src/main/java/com/ai/aigenerate/model/response/chat/DrawImageResponse.java b/src/main/java/com/ai/aigenerate/model/response/chat/DrawImageResponse.java new file mode 100644 index 0000000..45ca839 --- /dev/null +++ b/src/main/java/com/ai/aigenerate/model/response/chat/DrawImageResponse.java @@ -0,0 +1,9 @@ +package com.ai.aigenerate.model.response.chat; + +import lombok.Data; + +@Data +public class DrawImageResponse { + + private String imageUrl; +} diff --git a/src/main/java/com/ai/aigenerate/model/response/chat/VoiceResponse.java b/src/main/java/com/ai/aigenerate/model/response/chat/VoiceResponse.java new file mode 100644 index 0000000..b6bbd8e --- /dev/null +++ b/src/main/java/com/ai/aigenerate/model/response/chat/VoiceResponse.java @@ -0,0 +1,13 @@ +package com.ai.aigenerate.model.response.chat; + +import lombok.Data; + +@Data +public class VoiceResponse { + + private String audio; + + private String questionAsr; + + private String answerAsr; +} diff --git a/src/main/java/com/ai/aigenerate/model/response/stablediffusion/Meta.java b/src/main/java/com/ai/aigenerate/model/response/stablediffusion/Meta.java new file mode 100644 index 0000000..b02d0ac --- /dev/null +++ b/src/main/java/com/ai/aigenerate/model/response/stablediffusion/Meta.java @@ -0,0 +1,37 @@ +package com.ai.aigenerate.model.response.stablediffusion; + +import lombok.Data; + +@Data +public class Meta { + private String prompt; + private String model_id; + private String negative_prompt; + private String scheduler; + private String safety_checker; + private Integer W; + private Integer H; + private Double guidance_scale; + private Integer seed; + private Integer steps; + private Integer n_samples; + private String full_url; + private String instant_response; + private String tomesd; + private String upscale; + private String multi_lingual; + private String panorama; + private String self_attention; + private String use_karras_sigmas; + private String algorithm_type; + private String safety_checker_type; + private String embeddings; + private String vae; + private String lora; + private Integer lora_strength; + private Integer clip_skip; + private String temp; + private String base64; + private String file_prefix; + +} diff --git a/src/main/java/com/ai/aigenerate/model/response/stablediffusion/TextToImageRespDTO.java b/src/main/java/com/ai/aigenerate/model/response/stablediffusion/TextToImageRespDTO.java new file mode 100644 index 0000000..7adab8d --- /dev/null +++ b/src/main/java/com/ai/aigenerate/model/response/stablediffusion/TextToImageRespDTO.java @@ -0,0 +1,19 @@ +package com.ai.aigenerate.model.response.stablediffusion; + +import lombok.Data; + +import java.util.List; + +@Data +public class TextToImageRespDTO { + + private String status; + + private Long generationTime; + + private Long id; + + private List output; + + private Meta meta; +} diff --git a/src/main/java/com/ai/aigenerate/service/ChatGptService.java b/src/main/java/com/ai/aigenerate/service/ChatGptService.java index 076d043..3ae3b82 100644 --- a/src/main/java/com/ai/aigenerate/service/ChatGptService.java +++ b/src/main/java/com/ai/aigenerate/service/ChatGptService.java @@ -4,7 +4,7 @@ import cn.hutool.json.JSONUtil; import com.ai.aigenerate.chat.FunctionEventSourceListener; import com.ai.aigenerate.chat.tool.MjService; -import com.ai.aigenerate.config.GptFunctionConfig; +import com.ai.aigenerate.config.GptConfig; import com.ai.aigenerate.model.request.mail.EmailRequest; import com.ai.aigenerate.model.request.mj.CreateTaskRequest; import com.ai.aigenerate.model.request.mail.ImageMail; @@ -47,7 +47,7 @@ public class ChatGptService { private MjService mjService; @Autowired - private GptFunctionConfig gptFunctionConfig; + private GptConfig gptConfig; private OpenAiStreamClient openAiStreamClient; @@ -67,7 +67,7 @@ public void init() { .build(); openAiClient = OpenAiClient.builder() //支持多key传入,请求时候随机选择 - .apiKey(gptFunctionConfig.getChatgptApiKey()) + .apiKey(gptConfig.getChatgptApiKey()) //自定义key的获取策略:默认KeyRandomStrategy .keyStrategy(new KeyRandomStrategy()) .authInterceptor(new DynamicKeyOpenAiAuthInterceptor()) @@ -76,7 +76,7 @@ public void init() { .build(); openAiStreamClient = OpenAiStreamClient.builder() //支持多key传入,请求时候随机选择 - .apiKey(gptFunctionConfig.getChatgptApiKey()) + .apiKey(gptConfig.getChatgptApiKey()) //自定义key的获取策略:默认KeyRandomStrategy .keyStrategy(new KeyRandomStrategy()) .authInterceptor(new DynamicKeyOpenAiAuthInterceptor()) diff --git a/src/main/java/com/ai/aigenerate/utils/MdcUtil.java b/src/main/java/com/ai/aigenerate/utils/MdcUtils.java similarity index 96% rename from src/main/java/com/ai/aigenerate/utils/MdcUtil.java rename to src/main/java/com/ai/aigenerate/utils/MdcUtils.java index 9b04aa2..3d8ad33 100644 --- a/src/main/java/com/ai/aigenerate/utils/MdcUtil.java +++ b/src/main/java/com/ai/aigenerate/utils/MdcUtils.java @@ -5,7 +5,7 @@ import java.util.Map; import java.util.UUID; -public class MdcUtil { +public class MdcUtils { public static final String TRACE_ID = "traceId"; public static String generateTraceId() { diff --git a/src/main/java/com/ai/aigenerate/utils/OssUtils.java b/src/main/java/com/ai/aigenerate/utils/OssUtils.java new file mode 100644 index 0000000..7f602af --- /dev/null +++ b/src/main/java/com/ai/aigenerate/utils/OssUtils.java @@ -0,0 +1,68 @@ +package com.ai.aigenerate.utils; + +import com.aliyun.oss.OSS; +import com.aliyun.oss.OSSClientBuilder; +import com.aliyun.oss.OSSException; +import com.aliyun.oss.common.auth.CredentialsProviderFactory; +import com.aliyun.oss.common.auth.DefaultCredentialProvider; +import com.aliyun.oss.internal.OSSHeaders; +import com.aliyun.oss.model.CannedAccessControlList; +import com.aliyun.oss.model.ObjectMetadata; +import com.aliyun.oss.model.PutObjectRequest; +import com.aliyun.oss.model.PutObjectResult; +import com.aliyun.oss.model.StorageClass; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; + +import java.io.InputStream; +import java.util.UUID; + +@Slf4j +public class OssUtils { + + @SneakyThrows + public static String upload(InputStream inputStream) { + // Endpoint以华东1(杭州)为例,其它Region请按实际情况填写。 + String endpoint = "https://oss-cn-beijing.aliyuncs.com"; + // 从环境变量中获取访问凭证。运行本代码示例之前,请确保已设置环境变量OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET。 + DefaultCredentialProvider defaultCredentialProvider = CredentialsProviderFactory.newDefaultCredentialProvider("",""); + // 填写Bucket名称,例如examplebucket。 + String bucketName = "ai-image-1"; + // 填写Object完整路径,完整路径中不能包含Bucket名称,例如exampledir/exampleobject.txt。 + String objectName = UUID.randomUUID().toString() + ".png"; + // 填写本地文件的完整路径,例如D:\\localpath\\examplefile.txt。 + // 如果未指定本地路径,则默认从示例程序所属项目对应本地路径中上传文件。 + + // 创建OSSClient实例。 + OSS ossClient = new OSSClientBuilder().build(endpoint, defaultCredentialProvider); + + try { + // 创建PutObjectRequest对象。 + PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, objectName, inputStream); + // 如果需要上传时设置存储类型和访问权限,请参考以下示例代码。 + ObjectMetadata metadata = new ObjectMetadata(); + metadata.setHeader(OSSHeaders.OSS_STORAGE_CLASS, StorageClass.Standard.toString()); + metadata.setObjectAcl(CannedAccessControlList.PublicRead); + metadata.setHeader("Content-Disposition", "inline"); + metadata.setContentType("image/png"); + putObjectRequest.setMetadata(metadata); + + // 上传文件。 + PutObjectResult result = ossClient.putObject(putObjectRequest); + log.info(result.toString()); + return "http://your domain/" + objectName; + } catch (OSSException oe) { + log.error("Caught an OSSException, which means your request made it to OSS, " + + "but was rejected with an error response for some reason."); + log.error("Error Message:" + oe.getErrorMessage()); + log.error("Error Code:" + oe.getErrorCode()); + log.error("Request ID:" + oe.getRequestId()); + log.error("Host ID:" + oe.getHostId()); + } finally { + if (ossClient != null) { + ossClient.shutdown(); + } + } + return ""; + } +} From 7d680b9e4be6696a01068f0923d8bf651832def9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E4=B8=80=E5=87=A1?= Date: Tue, 21 Nov 2023 16:47:58 +0800 Subject: [PATCH 03/12] =?UTF-8?q?1=E3=80=81rename=20file=202=E3=80=81?= =?UTF-8?q?=E4=B8=80=E4=B8=AA=E5=8F=AF=E4=BB=A5=E8=BF=9B=E8=A1=8C=E5=AE=9E?= =?UTF-8?q?=E6=97=B6=E8=AF=AD=E9=9F=B3=E4=BA=A4=E4=BA=92=E7=9A=84html?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/ai/aigenerate/facade/VoiceFacade.java | 74 ++++++++ src/main/resources/static/voice.html | 174 ++++++++++++++++++ 2 files changed, 248 insertions(+) create mode 100644 src/main/java/com/ai/aigenerate/facade/VoiceFacade.java create mode 100644 src/main/resources/static/voice.html diff --git a/src/main/java/com/ai/aigenerate/facade/VoiceFacade.java b/src/main/java/com/ai/aigenerate/facade/VoiceFacade.java new file mode 100644 index 0000000..5f42868 --- /dev/null +++ b/src/main/java/com/ai/aigenerate/facade/VoiceFacade.java @@ -0,0 +1,74 @@ +package com.ai.aigenerate.facade; + +import com.ai.aigenerate.chat.ChatService; +import com.ai.aigenerate.constant.VoiceContent; +import com.ai.aigenerate.model.request.chat.ChatRequest; +import com.ai.aigenerate.model.request.chat.ChatVoiceRequest; +import com.ai.aigenerate.model.response.chat.ChatResponse; +import com.ai.aigenerate.model.response.chat.VoiceResponse; +import com.alibaba.fastjson2.JSON; +import com.unfbx.chatgpt.entity.chat.Message; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +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.multipart.MultipartFile; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; + +@RequestMapping("/voice") +@RestController +public class VoiceFacade { + + + @Autowired + private ChatService chatService; + + @PostMapping("/upload-audio") + public VoiceResponse handleAudioUpload(@RequestParam("audio") MultipartFile audioFile, @RequestParam("chatHistory") String chatHistoryStr, HttpServletResponse response) throws IOException { + // 在这里处理上传的音频文件 + List chatVoiceRequests = JSON.parseArray(chatHistoryStr, ChatVoiceRequest.class); + // 可以将音频保存到服务器上的某个位置,或者执行其他操作 + File file = new File(VoiceContent.ASR_PATH); + BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(new FileOutputStream(file)); + bufferedOutputStream.write(audioFile.getBytes()); + String questionAsr = chatService.speechToTextTranslations(file); + ChatRequest chatRequest = new ChatRequest(); + chatRequest.setPrompt(questionAsr); + List messages = new ArrayList<>(); + for (ChatVoiceRequest chatVoiceRequest : chatVoiceRequests) { + Message message = new Message(); + message.setRole(Message.Role.USER.getName()); + message.setContent(chatVoiceRequest.getQuestion()); + messages.add(message); + Message message1 = new Message(); + message1.setRole(Message.Role.ASSISTANT.getName()); + message1.setContent(chatVoiceRequest.getAnswer()); + messages.add(message1); + } + chatRequest.setMessages(messages); + ChatResponse chatResponse = chatService.chat(chatRequest); + VoiceResponse voiceResponse = new VoiceResponse(); + voiceResponse.setQuestionAsr(questionAsr); + voiceResponse.setAnswerAsr(chatResponse.getResult()); + File answerTts = chatService.textToSpeed(chatResponse.getResult()); + byte[] audioBytes = Files.readAllBytes(Paths.get(answerTts.getPath())); + // 将音频字节编码为Base64字符串 + String audioBase64 = Base64.getEncoder().encodeToString(audioBytes); + voiceResponse.setAudio(audioBase64); + response.addHeader("Access-Control-Allow-Origin", "*"); + response.addHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE"); + response.addHeader("Access-Control-Allow-Headers", "Content-Type"); + return voiceResponse; + } +} diff --git a/src/main/resources/static/voice.html b/src/main/resources/static/voice.html new file mode 100644 index 0000000..ce8a1fe --- /dev/null +++ b/src/main/resources/static/voice.html @@ -0,0 +1,174 @@ + + + + + +实时语音交互页面 + + + +

语音聊天

+ +
+ + +
+
+ + + + From fffa5c0f74b031c5558619444d2d3a46a088a63f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E4=B8=80=E5=87=A1?= Date: Mon, 25 Dec 2023 10:18:30 +0800 Subject: [PATCH 04/12] Changes --- Dockerfile | 21 +----- .../com/ai/aigenerate/chat/ChatService.java | 10 +-- .../chat/custom/MjGptFunctionHandler.java | 24 +++++- .../chat/tool/DallE3ImageService.java | 75 +++++++++++++++++++ .../chat/tool/MorningPaperService.java | 16 ++++ .../com/ai/aigenerate/config/GptConfig.java | 12 +++ .../com/ai/aigenerate/facade/ImageFacade.java | 18 ++++- .../com/ai/aigenerate/facade/VoiceFacade.java | 19 ++++- .../model/request/mj/CreateTaskRequest.java | 2 + 9 files changed, 168 insertions(+), 29 deletions(-) create mode 100644 src/main/java/com/ai/aigenerate/chat/tool/DallE3ImageService.java create mode 100644 src/main/java/com/ai/aigenerate/chat/tool/MorningPaperService.java diff --git a/Dockerfile b/Dockerfile index 5c73df9..99cee08 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,17 +1,4 @@ -# 使用官方提供的 OpenJDK 17 镜像作为基础镜像 -FROM adoptopenjdk:17-jdk-hotspot - -# 将当前目录下的所有文件复制到镜像的 /app 目录中 -COPY src/main/Dockerfile /app - -# 设置工作目录 -WORKDIR /app - -FROM maven:3.6.3-jdk-8 AS build -COPY src /usr/src/app/src -COPY pom.xml /usr/src/app - -# 构建项目(根据具体情况选择适当的构建工具和命令) -RUN mvn -f /usr/src/app/pom.xml clean package -DskipTests=true - -ENTRYPOINT [ "sh", "-c", "java -jar /app.jar" ] \ No newline at end of file +FROM openjdk:19 +COPY target/chatgpt-plus-1.0.1-SNAPSHOT.jar /app.jar +EXPOSE 15600 +CMD ["java", "-jar", "/app.jar"] \ No newline at end of file diff --git a/src/main/java/com/ai/aigenerate/chat/ChatService.java b/src/main/java/com/ai/aigenerate/chat/ChatService.java index 55a59a5..6cc947b 100644 --- a/src/main/java/com/ai/aigenerate/chat/ChatService.java +++ b/src/main/java/com/ai/aigenerate/chat/ChatService.java @@ -108,7 +108,7 @@ public ChatResponse chat(ChatRequest chatRequest){ ChatCompletion chatCompletion = ChatCompletion .builder() .messages(messages) - .maxTokens(chatRequest.getMaxTokens() != null?chatRequest.getMaxTokens():4097) + .maxTokens(chatRequest.getMaxTokens() != null?chatRequest.getMaxTokens():4096) .temperature(chatRequest.getTemperature() != null?chatRequest.getTemperature():0.2) .topP(chatRequest.getTopP() != null?chatRequest.getTopP():1.0) .n(chatRequest.getN() != null?chatRequest.getN():1) @@ -359,7 +359,7 @@ public List queryFunctionNameList(){ public String speechToTextTranslations(File file) { Translations translations = Translations.builder() .model(Whisper.Model.WHISPER_1.getName()) - .prompt("请你务必返回中文") + .prompt("必须将结果翻译成中文返回") .temperature(0.2) .responseFormat(Whisper.ResponseFormat.JSON.getName()) .build(); @@ -369,14 +369,14 @@ public String speechToTextTranslations(File file) { return whisperResponse.getText(); } - public File textToSpeed(String text) { + public File textToSpeed(String text,String voice) { TextToSpeech textToSpeech = TextToSpeech.builder() .model(TextToSpeech.Model.TTS_1.getName()) .input(text) - .voice(TtsVoice.NOVA.getName()) + .voice(StringUtils.isNotBlank(voice)?voice:TtsVoice.NOVA.getName()) .responseFormat(TtsFormat.MP3.getName()) .build(); - File file = new File(VoiceContent.TTS_PATH +Math.random()+".mp3"); + File file = new File(gptConfig.getTtsPath() +Math.random()+".mp3"); CountDownLatch countDownLatch = new CountDownLatch(1); openAiClient.textToSpeech(textToSpeech, new Callback() { @SneakyThrows diff --git a/src/main/java/com/ai/aigenerate/chat/custom/MjGptFunctionHandler.java b/src/main/java/com/ai/aigenerate/chat/custom/MjGptFunctionHandler.java index 2427223..8601c0f 100644 --- a/src/main/java/com/ai/aigenerate/chat/custom/MjGptFunctionHandler.java +++ b/src/main/java/com/ai/aigenerate/chat/custom/MjGptFunctionHandler.java @@ -1,6 +1,7 @@ package com.ai.aigenerate.chat.custom; import cn.hutool.json.JSONObject; +import com.ai.aigenerate.chat.tool.DallE3ImageService; import com.ai.aigenerate.model.request.mj.CreateTaskRequest; import com.ai.aigenerate.model.response.mj.QueryTaskResponse; import com.ai.aigenerate.chat.tool.MjService; @@ -8,10 +9,12 @@ import com.alibaba.fastjson2.JSON; import com.unfbx.chatgpt.entity.chat.Functions; import com.unfbx.chatgpt.entity.chat.Parameters; +import org.apache.commons.collections4.CollectionUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.util.Arrays; +import java.util.List; @Component public class MjGptFunctionHandler extends AbstractGptFunctionHandler { @@ -19,10 +22,19 @@ public class MjGptFunctionHandler extends AbstractGptFunctionHandler urls = dallE3ImageService.generateImage(createTaskRequest.getPrompt(),1); + url = CollectionUtils.isNotEmpty(urls) ? urls.get(0) : ""; + } String content = "{ " + "\"这个是获取到的图片链接\": \"" + url + "\"" + @@ -43,16 +55,22 @@ public Functions getFunction() { JSONObject imagePrompt = new JSONObject(); imagePrompt.putOpt("type", "string"); imagePrompt.putOpt("description", "图片的描述,例如:一张猫的图片,统一转换为英文"); + + JSONObject imageType = new JSONObject(); + imageType.putOpt("type", "string"); + imageType.putOpt("enum",Arrays.asList("midjoureny","dallE3")); + imageType.putOpt("description", "画图方式, 默认dallE3"); //参数 JSONObject properties = new JSONObject(); properties.putOpt("prompt", imagePrompt); + properties.putOpt("type", imageType); Parameters parameters = Parameters.builder() .type("object") .properties(properties) - .required(Arrays.asList("prompt")).build(); + .required(Arrays.asList("prompt","type")).build(); Functions functions = Functions.builder() .name("createImage") - .description("如果需要生成图片,可以根据描述生成一张图片,返回为图片地址") + .description("如果需要生成图片,可以根据描述生成一张图片,返回为图片地址,不要变更返回的地址,否则会导致图片无法显示") .parameters(parameters) .build(); return functions; diff --git a/src/main/java/com/ai/aigenerate/chat/tool/DallE3ImageService.java b/src/main/java/com/ai/aigenerate/chat/tool/DallE3ImageService.java new file mode 100644 index 0000000..bc43a54 --- /dev/null +++ b/src/main/java/com/ai/aigenerate/chat/tool/DallE3ImageService.java @@ -0,0 +1,75 @@ +package com.ai.aigenerate.chat.tool; + +import com.ai.aigenerate.config.GptConfig; +import com.unfbx.chatgpt.OpenAiClient; +import com.unfbx.chatgpt.entity.images.Image; +import com.unfbx.chatgpt.entity.images.ImageResponse; +import com.unfbx.chatgpt.entity.images.SizeEnum; +import com.unfbx.chatgpt.function.KeyRandomStrategy; +import com.unfbx.chatgpt.interceptor.DynamicKeyOpenAiAuthInterceptor; +import com.unfbx.chatgpt.interceptor.OpenAILogger; +import com.unfbx.chatgpt.interceptor.OpenAiResponseInterceptor; +import jakarta.annotation.PostConstruct; +import okhttp3.OkHttpClient; +import okhttp3.logging.HttpLoggingInterceptor; +import org.apache.commons.collections4.CollectionUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +@Component +public class DallE3ImageService { + + @Autowired + private GptConfig gptConfig; + + private OpenAiClient openAiClient; + + @PostConstruct + public void init(){ + HttpLoggingInterceptor httpLoggingInterceptor = new HttpLoggingInterceptor(new OpenAILogger()); + OkHttpClient okHttpClient = new OkHttpClient + .Builder() + .addInterceptor(httpLoggingInterceptor) + .addInterceptor(new OpenAiResponseInterceptor()) + .connectTimeout(100, TimeUnit.SECONDS) + .writeTimeout(300, TimeUnit.SECONDS) + .readTimeout(300, TimeUnit.SECONDS) + .build(); + openAiClient = OpenAiClient.builder() + //支持多key传入,请求时候随机选择 + .apiKey(gptConfig.getChatgptApiKey()) + //自定义key的获取策略:默认KeyRandomStrategy + .keyStrategy(new KeyRandomStrategy()) + .authInterceptor(new DynamicKeyOpenAiAuthInterceptor()) + .okHttpClient(okHttpClient) + .build(); + } + + public List generateImage(String text,Integer n){ + ImageResponse imageResponse = generateImageByDall_e_3(text,n); + List urls = new ArrayList<>(); + if (CollectionUtils.isNotEmpty(imageResponse.getData())){ + imageResponse.getData().forEach(image -> { + urls.add(image.getUrl()); + }); + } + return urls; + } + + public ImageResponse generateImageByDall_e_3(String prompt,Integer n) { + Image image = Image.builder() + .responseFormat(com.unfbx.chatgpt.entity.images.ResponseFormat.URL.getName()) + .model(Image.Model.DALL_E_3.getName()) + .prompt(prompt) + .n(n) + .quality(Image.Quality.STANDARD.getName()) + .size(SizeEnum.size_1024_1792.getName()) + .style(Image.Style.NATURAL.getName()) + .build(); + return openAiClient.genImages(image); + } +} diff --git a/src/main/java/com/ai/aigenerate/chat/tool/MorningPaperService.java b/src/main/java/com/ai/aigenerate/chat/tool/MorningPaperService.java new file mode 100644 index 0000000..aebe44c --- /dev/null +++ b/src/main/java/com/ai/aigenerate/chat/tool/MorningPaperService.java @@ -0,0 +1,16 @@ +package com.ai.aigenerate.chat.tool; + +import com.ai.aigenerate.utils.HttpClientUtils; +import com.alibaba.fastjson.JSONObject; +import org.springframework.stereotype.Component; + +@Component +public class MorningPaperService { + + public String getMorningPaper() { + JSONObject jsonObject = HttpClientUtils.httpGet("http://dwz.2xb.cn/zaob"); + String url = jsonObject.getString("imageUrl"); + url = url.replace("https", "http"); + return url; + } +} diff --git a/src/main/java/com/ai/aigenerate/config/GptConfig.java b/src/main/java/com/ai/aigenerate/config/GptConfig.java index 658b20e..3ebd061 100644 --- a/src/main/java/com/ai/aigenerate/config/GptConfig.java +++ b/src/main/java/com/ai/aigenerate/config/GptConfig.java @@ -27,6 +27,18 @@ public class GptConfig { @Value("${system.prompt:}") private String systemPrompt; + @Value("${tts.path:/Users/liyifan/Downloads/voice/}") + private String ttsPath; + + @Value("${asr.path:/Users/liyifan/Downloads/asr/}") + private String asrPath; + + @Value("${voice.api.token:6543213}") + private String voiceApiToken; + + @Value("${voice.prompt.system:请使用中文交流,回答要求尽可能简短,不能超过100个字}") + private String voicePromptSystem; + public Map getLinkAiApiKeyMap(){ Map map = new HashMap<>(); String[] split = linkAiApiKeyMap.split(","); diff --git a/src/main/java/com/ai/aigenerate/facade/ImageFacade.java b/src/main/java/com/ai/aigenerate/facade/ImageFacade.java index 3d83c0e..97d7e0d 100644 --- a/src/main/java/com/ai/aigenerate/facade/ImageFacade.java +++ b/src/main/java/com/ai/aigenerate/facade/ImageFacade.java @@ -1,7 +1,9 @@ package com.ai.aigenerate.facade; import com.ai.aigenerate.chat.tool.AliyunDrawService; +import com.ai.aigenerate.chat.tool.DallE3ImageService; import com.ai.aigenerate.chat.tool.MjService; +import com.ai.aigenerate.chat.tool.MorningPaperService; import com.ai.aigenerate.chat.tool.StableDiffusionService; import com.ai.aigenerate.chat.tool.TranslateService; import com.ai.aigenerate.model.request.chat.DrawRequest; @@ -26,12 +28,26 @@ public class ImageFacade { @Autowired private MjService mjService; + @Autowired + private MorningPaperService morningPaperService; + @Autowired private TranslateService translateService; + @Autowired + private DallE3ImageService dallE3ImageService; + + @RequestMapping("zb") + public DrawImageResponse getZb(@RequestBody DrawRequest drawRequest){ + String url = morningPaperService.getMorningPaper(); + DrawImageResponse drawImageResponse = new DrawImageResponse(); + drawImageResponse.setImageUrl(url); + return drawImageResponse; + } + @RequestMapping("drawImage") public DrawImageResponse drawImage(@RequestBody DrawRequest drawRequest){ - String url = aliyunDrawService.basicCall(drawRequest.getPrompt()); + String url = dallE3ImageService.generateImage(drawRequest.getPrompt(),1).get(0); DrawImageResponse drawImageResponse = new DrawImageResponse(); drawImageResponse.setImageUrl(url); return drawImageResponse; diff --git a/src/main/java/com/ai/aigenerate/facade/VoiceFacade.java b/src/main/java/com/ai/aigenerate/facade/VoiceFacade.java index 5f42868..fac6b2a 100644 --- a/src/main/java/com/ai/aigenerate/facade/VoiceFacade.java +++ b/src/main/java/com/ai/aigenerate/facade/VoiceFacade.java @@ -1,6 +1,7 @@ package com.ai.aigenerate.facade; import com.ai.aigenerate.chat.ChatService; +import com.ai.aigenerate.config.GptConfig; import com.ai.aigenerate.constant.VoiceContent; import com.ai.aigenerate.model.request.chat.ChatRequest; import com.ai.aigenerate.model.request.chat.ChatVoiceRequest; @@ -34,18 +35,29 @@ public class VoiceFacade { @Autowired private ChatService chatService; + @Autowired + private GptConfig gptConfig; + @PostMapping("/upload-audio") - public VoiceResponse handleAudioUpload(@RequestParam("audio") MultipartFile audioFile, @RequestParam("chatHistory") String chatHistoryStr, HttpServletResponse response) throws IOException { + public VoiceResponse handleAudioUpload(@RequestParam("audio") MultipartFile audioFile,@RequestParam("token") String token,@RequestParam("voice") String voice, @RequestParam("chatHistory") String chatHistoryStr, HttpServletResponse response) throws IOException { + if (token == null || !token.equals(gptConfig.getVoiceApiToken())){ + throw new RuntimeException("token error"); + } // 在这里处理上传的音频文件 List chatVoiceRequests = JSON.parseArray(chatHistoryStr, ChatVoiceRequest.class); // 可以将音频保存到服务器上的某个位置,或者执行其他操作 - File file = new File(VoiceContent.ASR_PATH); + File file = new File(gptConfig.getAsrPath()+System.currentTimeMillis()+".mp3"); BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(new FileOutputStream(file)); bufferedOutputStream.write(audioFile.getBytes()); String questionAsr = chatService.speechToTextTranslations(file); ChatRequest chatRequest = new ChatRequest(); chatRequest.setPrompt(questionAsr); + chatRequest.setModel("gpt-4-1106-preview"); List messages = new ArrayList<>(); + Message system = new Message(); + system.setRole(Message.Role.SYSTEM.getName()); + system.setContent(gptConfig.getVoicePromptSystem()); + messages.add(system); for (ChatVoiceRequest chatVoiceRequest : chatVoiceRequests) { Message message = new Message(); message.setRole(Message.Role.USER.getName()); @@ -61,11 +73,12 @@ public VoiceResponse handleAudioUpload(@RequestParam("audio") MultipartFile audi VoiceResponse voiceResponse = new VoiceResponse(); voiceResponse.setQuestionAsr(questionAsr); voiceResponse.setAnswerAsr(chatResponse.getResult()); - File answerTts = chatService.textToSpeed(chatResponse.getResult()); + File answerTts = chatService.textToSpeed(chatResponse.getResult(),voice); byte[] audioBytes = Files.readAllBytes(Paths.get(answerTts.getPath())); // 将音频字节编码为Base64字符串 String audioBase64 = Base64.getEncoder().encodeToString(audioBytes); voiceResponse.setAudio(audioBase64); + bufferedOutputStream.close(); response.addHeader("Access-Control-Allow-Origin", "*"); response.addHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE"); response.addHeader("Access-Control-Allow-Headers", "Content-Type"); diff --git a/src/main/java/com/ai/aigenerate/model/request/mj/CreateTaskRequest.java b/src/main/java/com/ai/aigenerate/model/request/mj/CreateTaskRequest.java index 242a39d..037e746 100644 --- a/src/main/java/com/ai/aigenerate/model/request/mj/CreateTaskRequest.java +++ b/src/main/java/com/ai/aigenerate/model/request/mj/CreateTaskRequest.java @@ -12,4 +12,6 @@ public class CreateTaskRequest { private String prompt; private String state; + + private String imageType; } From 11630a21cb3dec1e3ea353204edabc47f807052c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E4=B8=80=E5=87=A1?= Date: Wed, 7 Feb 2024 19:20:08 +0800 Subject: [PATCH 05/12] add plug-in --- .../chat/AbstractGptFunctionHandler.java | 8 +- .../chat/custom/AiNewsFunctionHandler.java | 28 ++++++ .../custom/AnalyzeLinkFunctionHandler.java | 46 ++++++++++ .../custom/BaiduSearchGptFunctionHandler.java | 6 +- .../custom/BilibiliGtpFunctionHandler.java | 4 +- .../chat/custom/KfcFunctionHandler.java | 28 ++++++ .../chat/custom/WeiboGptFunctionHandler.java | 2 +- .../chat/tool/AnalyzeLinkService.java | 31 +++++++ .../chat/tool/CopyWritingService.java | 15 +++ .../chat/tool/CountDownHourService.java | 30 ++++++ .../chat/tool/CrawlerAiNewsService.java | 92 +++++++++++++++++++ .../chat/tool/MorningPaperService.java | 4 +- .../ai/aigenerate/chat/tool/MoyuService.java | 27 ++++++ .../ai/aigenerate/chat/tool/VideoService.java | 23 +++++ .../ai/aigenerate/chat/tool/WeiboService.java | 18 ++-- .../com/ai/aigenerate/config/GptConfig.java | 2 +- .../com/ai/aigenerate/facade/ChatFacade.java | 27 ++++++ .../com/ai/aigenerate/facade/ImageFacade.java | 19 ++++ .../com/ai/aigenerate/facade/VideoFacade.java | 27 ++++++ .../model/request/link/LinkRequest.java | 9 ++ .../model/response/BeCommonResponse.java | 11 +++ 21 files changed, 442 insertions(+), 15 deletions(-) create mode 100644 src/main/java/com/ai/aigenerate/chat/custom/AiNewsFunctionHandler.java create mode 100644 src/main/java/com/ai/aigenerate/chat/custom/AnalyzeLinkFunctionHandler.java create mode 100644 src/main/java/com/ai/aigenerate/chat/custom/KfcFunctionHandler.java create mode 100644 src/main/java/com/ai/aigenerate/chat/tool/AnalyzeLinkService.java create mode 100644 src/main/java/com/ai/aigenerate/chat/tool/CopyWritingService.java create mode 100644 src/main/java/com/ai/aigenerate/chat/tool/CountDownHourService.java create mode 100644 src/main/java/com/ai/aigenerate/chat/tool/CrawlerAiNewsService.java create mode 100644 src/main/java/com/ai/aigenerate/chat/tool/MoyuService.java create mode 100644 src/main/java/com/ai/aigenerate/chat/tool/VideoService.java create mode 100644 src/main/java/com/ai/aigenerate/facade/VideoFacade.java create mode 100644 src/main/java/com/ai/aigenerate/model/request/link/LinkRequest.java create mode 100644 src/main/java/com/ai/aigenerate/model/response/BeCommonResponse.java diff --git a/src/main/java/com/ai/aigenerate/chat/AbstractGptFunctionHandler.java b/src/main/java/com/ai/aigenerate/chat/AbstractGptFunctionHandler.java index e05bc92..a6811ec 100644 --- a/src/main/java/com/ai/aigenerate/chat/AbstractGptFunctionHandler.java +++ b/src/main/java/com/ai/aigenerate/chat/AbstractGptFunctionHandler.java @@ -24,7 +24,13 @@ public ChatChoice preHandle(ChatChoice chatChoice){ log.error("当前方法不匹配:{}", chatChoice.getMessage().getFunctionCall()); return chatChoice; } - String result = doHandle(chatChoice.getMessage().getFunctionCall().getArguments()); + String result; + try { + result = doHandle(chatChoice.getMessage().getFunctionCall().getArguments()); + }catch (Exception e){ + log.error("插件调用失败,chatChoice:{},errorMsg:{}",chatChoice,e); + result = "插件调用失败"; + } FunctionCall functionCall = FunctionCall.builder() .arguments(chatChoice.getMessage().getFunctionCall().getArguments()) .name(functions.getName()) diff --git a/src/main/java/com/ai/aigenerate/chat/custom/AiNewsFunctionHandler.java b/src/main/java/com/ai/aigenerate/chat/custom/AiNewsFunctionHandler.java new file mode 100644 index 0000000..eb097f6 --- /dev/null +++ b/src/main/java/com/ai/aigenerate/chat/custom/AiNewsFunctionHandler.java @@ -0,0 +1,28 @@ +package com.ai.aigenerate.chat.custom; + +import com.ai.aigenerate.chat.AbstractGptFunctionHandler; +import com.ai.aigenerate.chat.tool.CrawlerAiNewsService; +import com.unfbx.chatgpt.entity.chat.Functions; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class AiNewsFunctionHandler extends AbstractGptFunctionHandler { + + @Autowired + private CrawlerAiNewsService crawlerAiNewsService; + + @Override + public String doHandle(String paramJson) { + return crawlerAiNewsService.getAiNews(); + } + + @Override + public Functions getFunction() { + Functions functions = Functions.builder() + .name("getAiNews") + .description("获取跟Ai相关的新闻资讯") + .build(); + return functions; + } +} diff --git a/src/main/java/com/ai/aigenerate/chat/custom/AnalyzeLinkFunctionHandler.java b/src/main/java/com/ai/aigenerate/chat/custom/AnalyzeLinkFunctionHandler.java new file mode 100644 index 0000000..57c97aa --- /dev/null +++ b/src/main/java/com/ai/aigenerate/chat/custom/AnalyzeLinkFunctionHandler.java @@ -0,0 +1,46 @@ +package com.ai.aigenerate.chat.custom; + +import cn.hutool.json.JSONObject; +import com.ai.aigenerate.chat.AbstractGptFunctionHandler; +import com.ai.aigenerate.chat.tool.AnalyzeLinkService; +import com.ai.aigenerate.model.request.link.LinkRequest; +import com.alibaba.fastjson2.JSON; +import com.unfbx.chatgpt.entity.chat.Functions; +import com.unfbx.chatgpt.entity.chat.Parameters; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.Arrays; + +@Component +public class AnalyzeLinkFunctionHandler extends AbstractGptFunctionHandler { + + @Autowired + private AnalyzeLinkService analyzeLinkService; + + @Override + public String doHandle(String paramJson) { + LinkRequest linkRequest = JSON.parseObject(paramJson, LinkRequest.class); + return analyzeLinkService.analyzeLink(linkRequest.getUrl()); + } + + @Override + public Functions getFunction() { + JSONObject url = new JSONObject(); + url.putOpt("type", "string"); + url.putOpt("description", "链接的url,要读取的完整的链接"); + //参数 + JSONObject properties = new JSONObject(); + properties.putOpt("url", url); + Parameters parameters = Parameters.builder() + .type("object") + .properties(properties) + .required(Arrays.asList("url")).build(); + Functions functions = Functions.builder() + .name("analyzeLink") + .description("根据给出的网址链接地址解析网页中的内容,以进行后续的分析") + .parameters(parameters) + .build(); + return functions; + } +} diff --git a/src/main/java/com/ai/aigenerate/chat/custom/BaiduSearchGptFunctionHandler.java b/src/main/java/com/ai/aigenerate/chat/custom/BaiduSearchGptFunctionHandler.java index 97095f5..eba6a5d 100644 --- a/src/main/java/com/ai/aigenerate/chat/custom/BaiduSearchGptFunctionHandler.java +++ b/src/main/java/com/ai/aigenerate/chat/custom/BaiduSearchGptFunctionHandler.java @@ -9,6 +9,7 @@ import com.unfbx.chatgpt.entity.chat.Functions; import com.unfbx.chatgpt.entity.chat.Parameters; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import java.util.Arrays; @@ -18,6 +19,9 @@ public class BaiduSearchGptFunctionHandler extends AbstractGptFunctionHandler { - @Autowired + //@Autowired private BilibiliService bilibiliService; @Override diff --git a/src/main/java/com/ai/aigenerate/chat/custom/KfcFunctionHandler.java b/src/main/java/com/ai/aigenerate/chat/custom/KfcFunctionHandler.java new file mode 100644 index 0000000..7e63a6a --- /dev/null +++ b/src/main/java/com/ai/aigenerate/chat/custom/KfcFunctionHandler.java @@ -0,0 +1,28 @@ +package com.ai.aigenerate.chat.custom; + +import com.ai.aigenerate.chat.AbstractGptFunctionHandler; +import com.ai.aigenerate.chat.tool.CopyWritingService; +import com.unfbx.chatgpt.entity.chat.Functions; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class KfcFunctionHandler extends AbstractGptFunctionHandler { + + @Autowired + private CopyWritingService copyWritingService; + + @Override + public String doHandle(String paramJson) { + return "{\"文案\":\""+copyWritingService.getKfcText()+"\"}"; + } + + @Override + public Functions getFunction() { + Functions functions = Functions.builder() + .name("getCrazyKfc") + .description("获取疯狂星期四的文案,得到的结果不要做修饰直接返回") + .build(); + return functions; + } +} diff --git a/src/main/java/com/ai/aigenerate/chat/custom/WeiboGptFunctionHandler.java b/src/main/java/com/ai/aigenerate/chat/custom/WeiboGptFunctionHandler.java index f5c8d7a..651991e 100644 --- a/src/main/java/com/ai/aigenerate/chat/custom/WeiboGptFunctionHandler.java +++ b/src/main/java/com/ai/aigenerate/chat/custom/WeiboGptFunctionHandler.java @@ -41,7 +41,7 @@ public Functions getFunction() { .required(Arrays.asList("num")).build(); Functions functions = Functions.builder() .name("weiboHotSearch") - .description("根据描述的类型获取微博热榜数据") + .description("获取微博热搜数据,必须提及微博热搜才进行调用") .parameters(parameters) .build(); return functions; diff --git a/src/main/java/com/ai/aigenerate/chat/tool/AnalyzeLinkService.java b/src/main/java/com/ai/aigenerate/chat/tool/AnalyzeLinkService.java new file mode 100644 index 0000000..655db85 --- /dev/null +++ b/src/main/java/com/ai/aigenerate/chat/tool/AnalyzeLinkService.java @@ -0,0 +1,31 @@ +package com.ai.aigenerate.chat.tool; + +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.select.Elements; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +public class AnalyzeLinkService { + + @SneakyThrows + public String analyzeLink(String link){ + StringBuilder str = new StringBuilder(); + // 从URL加载HTML文档 + Document doc = Jsoup.connect(link).get(); + str.append(doc.text()); + + // 选择所有

元素并提取其文本内容 + Elements paragraphs = doc.getElementsByTag("p"); + for (Element p : paragraphs) { + str.append(p.text()); + } + log.info("----------------------字符串长度:{}",str.length()); + return str.toString(); + } + +} diff --git a/src/main/java/com/ai/aigenerate/chat/tool/CopyWritingService.java b/src/main/java/com/ai/aigenerate/chat/tool/CopyWritingService.java new file mode 100644 index 0000000..4550a6b --- /dev/null +++ b/src/main/java/com/ai/aigenerate/chat/tool/CopyWritingService.java @@ -0,0 +1,15 @@ +package com.ai.aigenerate.chat.tool; + +import com.ai.aigenerate.utils.HttpClientUtils; +import com.alibaba.fastjson.JSONObject; +import org.springframework.stereotype.Service; + +@Service +public class CopyWritingService { + + public String getKfcText(){ + JSONObject jsonObject = HttpClientUtils.httpGet("https://api.khkj6.com/kfc/"); + String text = jsonObject.getString("msg"); + return text; + } +} diff --git a/src/main/java/com/ai/aigenerate/chat/tool/CountDownHourService.java b/src/main/java/com/ai/aigenerate/chat/tool/CountDownHourService.java new file mode 100644 index 0000000..d7fb90b --- /dev/null +++ b/src/main/java/com/ai/aigenerate/chat/tool/CountDownHourService.java @@ -0,0 +1,30 @@ +package com.ai.aigenerate.chat.tool; + +import com.ai.aigenerate.utils.HttpClientUtils; +import com.alibaba.fastjson.JSONObject; +import org.springframework.stereotype.Service; + +@Service +public class CountDownHourService { + + public String queryWord(){ + JSONObject jsonObject = HttpClientUtils.httpGet("https://zj.v.api.aa1.cn/api/wenan-mj/?type=json"); + String msg = jsonObject.getString("msg"); + return msg; + } + + public String countDownHour(){ + JSONObject jsonObject = HttpClientUtils.httpGet("http://v.api.aa1.cn/api/rsdjs/"); + String month = jsonObject.getString("month"); + String week = jsonObject.getString("week"); + String day = jsonObject.getString("day"); + String time = jsonObject.getString("time"); + String str = "\n**人生倒计时:**\n" + + "\n" + + "- "+month+"\n" + + "- "+week+"\n" + + "- "+day+"\n" + + "- "+time+""; + return str; + } +} diff --git a/src/main/java/com/ai/aigenerate/chat/tool/CrawlerAiNewsService.java b/src/main/java/com/ai/aigenerate/chat/tool/CrawlerAiNewsService.java new file mode 100644 index 0000000..d301fb5 --- /dev/null +++ b/src/main/java/com/ai/aigenerate/chat/tool/CrawlerAiNewsService.java @@ -0,0 +1,92 @@ +package com.ai.aigenerate.chat.tool; + +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.jsoup.select.Elements; +import org.springframework.stereotype.Service; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import java.io.IOException; +import java.time.LocalDate; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; + +@Slf4j +@Service +public class CrawlerAiNewsService { + + @SneakyThrows + public String getAiNews() { + + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MM月dd日"); + ZoneId shanghaiZoneId = ZoneId.of("Asia/Shanghai"); + + // 获取今天的日期(上海时区) + LocalDate today = LocalDate.now(shanghaiZoneId); + String formattedToday = today.format(formatter); + + // 获取昨天的日期 + LocalDate yesterday = today.minusDays(1); + String formattedYesterday = yesterday.format(formatter); + + // 获取前天的日期 + LocalDate dayBeforeYesterday = today.minusDays(2); + String formattedDayBeforeYesterday = dayBeforeYesterday.format(formatter); + + String news = getJson(formattedToday,formattedYesterday); + if (StringUtils.isEmpty(news)){ + news = getJson(formattedYesterday,formattedDayBeforeYesterday); + } + return news; + } + + public String getJson(String formattedYesterday,String formattedDayBeforeYesterday) { + try { + // 创建HttpGet对象,设置要请求的URL + Document document = Jsoup.connect("https://ai-bot.cn/daily-ai-news/").get(); + + Element startDateElement = document.select("div.news-date:contains(" + formattedYesterday + ")").first(); + + // Find the element for the end date + Element endDateElement = document.select("div.news-date:contains(" + formattedDayBeforeYesterday + ")").first(); + JSONArray jsonArray = new JSONArray(); + + // Ensure both dates exist + if (startDateElement != null && endDateElement != null) { + // Elements that follow the start date and precede the end date + Elements newsItems = new Elements(); + + Element nextElement = startDateElement.nextElementSibling(); + while (nextElement != null && !nextElement.hasSameValue(endDateElement)) { + if (nextElement.hasClass("news-item")) { + newsItems.add(nextElement); + } + nextElement = nextElement.nextElementSibling(); + } + + // Now newsItems contains all the desired elements + for (Element newsItem : newsItems) { + Element link = newsItem.select("a").first(); + Element summary = newsItem.select("p.text-muted.text-sm").first(); + JSONObject jsonObject = new JSONObject(); + jsonObject.put("Title", link.text()); + jsonObject.put("URL", link.attr("href")); + jsonObject.put("Summary", summary.text()); + jsonArray.add(jsonObject); + } + } else { + log.error("One of the date elements could not be found."); + return null; + } + return jsonArray.toJSONString(); + }catch (Exception e){ + log.error("",e); + return null; + } + } + +} diff --git a/src/main/java/com/ai/aigenerate/chat/tool/MorningPaperService.java b/src/main/java/com/ai/aigenerate/chat/tool/MorningPaperService.java index aebe44c..91cf7a2 100644 --- a/src/main/java/com/ai/aigenerate/chat/tool/MorningPaperService.java +++ b/src/main/java/com/ai/aigenerate/chat/tool/MorningPaperService.java @@ -8,8 +8,8 @@ public class MorningPaperService { public String getMorningPaper() { - JSONObject jsonObject = HttpClientUtils.httpGet("http://dwz.2xb.cn/zaob"); - String url = jsonObject.getString("imageUrl"); + JSONObject jsonObject = HttpClientUtils.httpGet("http://api.suxun.site/api/sixs?type=json"); + String url = jsonObject.getString("image"); url = url.replace("https", "http"); return url; } diff --git a/src/main/java/com/ai/aigenerate/chat/tool/MoyuService.java b/src/main/java/com/ai/aigenerate/chat/tool/MoyuService.java new file mode 100644 index 0000000..7ff2dce --- /dev/null +++ b/src/main/java/com/ai/aigenerate/chat/tool/MoyuService.java @@ -0,0 +1,27 @@ +package com.ai.aigenerate.chat.tool; + +import com.ai.aigenerate.utils.HttpClientUtils; +import com.alibaba.fastjson.JSONObject; +import org.springframework.stereotype.Component; + +@Component +public class MoyuService { + + //https://api.j4u.ink/v1/store/other/proxy/remote/moyu.json + + public String getRelaxPaper() { + //https://api.52vmy.cn/api/wl/moyu +// JSONObject jsonObject = HttpClientUtils.httpGet("https://api.j4u.ink/v1/store/other/proxy/remote/moyu.json"); +// JSONObject data = jsonObject.getJSONObject("data"); +// String url = data.getString("moyu_url"); +// url = url.replace("https", "http"); + return "https://api.52vmy.cn/api/wl/moyu"; + } + + public String getImage(){ + JSONObject jsonObject = HttpClientUtils.httpGet("https://v2.api-m.com/api/heisi"); + String url = jsonObject.getString("data"); + url = url.replace("https", "http"); + return url; + } +} diff --git a/src/main/java/com/ai/aigenerate/chat/tool/VideoService.java b/src/main/java/com/ai/aigenerate/chat/tool/VideoService.java new file mode 100644 index 0000000..34f8d94 --- /dev/null +++ b/src/main/java/com/ai/aigenerate/chat/tool/VideoService.java @@ -0,0 +1,23 @@ +package com.ai.aigenerate.chat.tool; + +import com.ai.aigenerate.utils.HttpClientUtils; +import com.alibaba.fastjson.JSONObject; +import org.springframework.stereotype.Service; + +@Service +public class VideoService { + + public String getVideoMyPaper() { + JSONObject jsonObject = HttpClientUtils.httpGet("https://dayu.qqsuu.cn/moyuribaoshipin/apis.php?type=json"); + String url = jsonObject.getString("data"); + url = url.replace("https", "http"); + return url; + } + + public String getDanceVideo(){ + JSONObject jsonObject = HttpClientUtils.httpGet("http://www.wudada.online/Api/ScSp"); + String url = jsonObject.getString("data"); + return url; + } + +} diff --git a/src/main/java/com/ai/aigenerate/chat/tool/WeiboService.java b/src/main/java/com/ai/aigenerate/chat/tool/WeiboService.java index c2d761e..3629db9 100644 --- a/src/main/java/com/ai/aigenerate/chat/tool/WeiboService.java +++ b/src/main/java/com/ai/aigenerate/chat/tool/WeiboService.java @@ -70,13 +70,17 @@ public String load(String type) throws Exception { @SneakyThrows public String getWeiboResult(String type){ - String result = weiboCache.get(type); - if (StringUtils.isNotBlank(result)) { - return result; - }else { - weiboCache.refresh(type); - return weiboCache.get(type); - } + JSONObject jsonObject = HttpClientUtils.httpGet("https://zj.v.api.aa1.cn/api/weibo-rs/"); + JSONArray data = jsonObject.getJSONArray("data"); + return data.toString(); + +// String result = weiboCache.get(type); +// if (StringUtils.isNotBlank(result)) { +// return result; +// }else { +// weiboCache.refresh(type); +// return weiboCache.get(type); +// } } private String queryWeiboResult(String type) { diff --git a/src/main/java/com/ai/aigenerate/config/GptConfig.java b/src/main/java/com/ai/aigenerate/config/GptConfig.java index 3ebd061..96652e6 100644 --- a/src/main/java/com/ai/aigenerate/config/GptConfig.java +++ b/src/main/java/com/ai/aigenerate/config/GptConfig.java @@ -12,7 +12,7 @@ @Component public class GptConfig { - @Value("${mj.service.url:}") + @Value("${mj.service.url:http://localhost:8080}") private String mjServiceUrl; @Value("${mj.service.waitTime:90000}") diff --git a/src/main/java/com/ai/aigenerate/facade/ChatFacade.java b/src/main/java/com/ai/aigenerate/facade/ChatFacade.java index afb3199..89f45be 100644 --- a/src/main/java/com/ai/aigenerate/facade/ChatFacade.java +++ b/src/main/java/com/ai/aigenerate/facade/ChatFacade.java @@ -1,9 +1,12 @@ package com.ai.aigenerate.facade; import com.ai.aigenerate.chat.LinkAiChatService; +import com.ai.aigenerate.chat.tool.CopyWritingService; +import com.ai.aigenerate.chat.tool.CountDownHourService; import com.ai.aigenerate.model.request.chat.ChatRequest; import com.ai.aigenerate.chat.ChatService; import com.ai.aigenerate.model.request.chat.LinkAiChatRequest; +import com.ai.aigenerate.model.response.BeCommonResponse; import com.ai.aigenerate.model.response.chat.ChatResponse; import com.ai.aigenerate.model.response.chat.FunctionResponse; import org.springframework.beans.factory.annotation.Autowired; @@ -28,6 +31,12 @@ public class ChatFacade { @Autowired private LinkAiChatService linkAiChatService; + @Autowired + private CountDownHourService countDownHourService; + + @Autowired + private CopyWritingService copyWritingService; + @Autowired @Qualifier("streamThreadPool") private Executor executor; @@ -87,4 +96,22 @@ public SseEmitter knowledgeBaseChatStream(@RequestBody LinkAiChatRequest chatReq public List queryFunction(){ return chatService.queryFunctionNameList(); } + + @RequestMapping("countDownHour") + public BeCommonResponse countDownHour(){ + String countDownHour = countDownHourService.countDownHour(); + return BeCommonResponse.builder().result(countDownHour).build(); + } + + @RequestMapping("queryWord") + public BeCommonResponse queryWord(){ + String word = countDownHourService.queryWord(); + return BeCommonResponse.builder().result(word).build(); + } + + @RequestMapping("queryCrazyKfc") + public BeCommonResponse queryCrazyKfc(){ + String fkc = copyWritingService.getKfcText(); + return BeCommonResponse.builder().result(fkc).build(); + } } diff --git a/src/main/java/com/ai/aigenerate/facade/ImageFacade.java b/src/main/java/com/ai/aigenerate/facade/ImageFacade.java index 97d7e0d..bed6a8b 100644 --- a/src/main/java/com/ai/aigenerate/facade/ImageFacade.java +++ b/src/main/java/com/ai/aigenerate/facade/ImageFacade.java @@ -4,10 +4,12 @@ import com.ai.aigenerate.chat.tool.DallE3ImageService; import com.ai.aigenerate.chat.tool.MjService; import com.ai.aigenerate.chat.tool.MorningPaperService; +import com.ai.aigenerate.chat.tool.MoyuService; import com.ai.aigenerate.chat.tool.StableDiffusionService; import com.ai.aigenerate.chat.tool.TranslateService; import com.ai.aigenerate.model.request.chat.DrawRequest; import com.ai.aigenerate.model.request.stablediffusion.SdTextToImageRequest; +import com.ai.aigenerate.model.response.BeCommonResponse; import com.ai.aigenerate.model.response.chat.DrawImageResponse; import com.ai.aigenerate.model.response.mj.QueryTaskResponse; import org.springframework.beans.factory.annotation.Autowired; @@ -37,6 +39,9 @@ public class ImageFacade { @Autowired private DallE3ImageService dallE3ImageService; + @Autowired + private MoyuService moyuService; + @RequestMapping("zb") public DrawImageResponse getZb(@RequestBody DrawRequest drawRequest){ String url = morningPaperService.getMorningPaper(); @@ -45,6 +50,14 @@ public DrawImageResponse getZb(@RequestBody DrawRequest drawRequest){ return drawImageResponse; } + @RequestMapping("moyu") + public DrawImageResponse getMoyu(@RequestBody DrawRequest drawRequest){ + String url = moyuService.getRelaxPaper(); + DrawImageResponse drawImageResponse = new DrawImageResponse(); + drawImageResponse.setImageUrl(url); + return drawImageResponse; + } + @RequestMapping("drawImage") public DrawImageResponse drawImage(@RequestBody DrawRequest drawRequest){ String url = dallE3ImageService.generateImage(drawRequest.getPrompt(),1).get(0); @@ -72,4 +85,10 @@ public DrawImageResponse createImage(@RequestBody SdTextToImageRequest sdTextToI drawImageResponse.setImageUrl(queryTaskResponse.getImageUrl()); return drawImageResponse; } + + @RequestMapping("hs") + public BeCommonResponse getHsImage(){ + String url = moyuService.getImage(); + return BeCommonResponse.builder().result(url).build(); + } } diff --git a/src/main/java/com/ai/aigenerate/facade/VideoFacade.java b/src/main/java/com/ai/aigenerate/facade/VideoFacade.java new file mode 100644 index 0000000..cdc811a --- /dev/null +++ b/src/main/java/com/ai/aigenerate/facade/VideoFacade.java @@ -0,0 +1,27 @@ +package com.ai.aigenerate.facade; + +import com.ai.aigenerate.chat.tool.VideoService; +import com.ai.aigenerate.model.response.BeCommonResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("video") +public class VideoFacade { + + @Autowired + private VideoService videoService; + + @RequestMapping("moyu") + public BeCommonResponse queryMoyuVideo(){ + String url = videoService.getVideoMyPaper(); + return BeCommonResponse.builder().result(url).build(); + } + + @RequestMapping("dance") + public BeCommonResponse queryDanceVideo(){ + String url = videoService.getDanceVideo(); + return BeCommonResponse.builder().result(url).build(); + } +} diff --git a/src/main/java/com/ai/aigenerate/model/request/link/LinkRequest.java b/src/main/java/com/ai/aigenerate/model/request/link/LinkRequest.java new file mode 100644 index 0000000..1150eca --- /dev/null +++ b/src/main/java/com/ai/aigenerate/model/request/link/LinkRequest.java @@ -0,0 +1,9 @@ +package com.ai.aigenerate.model.request.link; + +import lombok.Data; + +@Data +public class LinkRequest { + + private String url; +} diff --git a/src/main/java/com/ai/aigenerate/model/response/BeCommonResponse.java b/src/main/java/com/ai/aigenerate/model/response/BeCommonResponse.java new file mode 100644 index 0000000..5cebaf3 --- /dev/null +++ b/src/main/java/com/ai/aigenerate/model/response/BeCommonResponse.java @@ -0,0 +1,11 @@ +package com.ai.aigenerate.model.response; + +import lombok.Builder; +import lombok.Data; + +@Builder +@Data +public class BeCommonResponse { + + private String result; +} From 78a9582f16a368c8a31455d617351e4c5d73c4ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E4=B8=80=E5=87=A1?= Date: Mon, 19 Feb 2024 13:36:56 +0800 Subject: [PATCH 06/12] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=B5=81=E5=BC=8Fautog?= =?UTF-8?q?pt=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/ai/aigenerate/chat/ChatService.java | 83 +++++++++++++++++++ .../com/ai/aigenerate/facade/ChatFacade.java | 22 ++++- .../model/request/chat/ChatRequest.java | 2 + 3 files changed, 106 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/ai/aigenerate/chat/ChatService.java b/src/main/java/com/ai/aigenerate/chat/ChatService.java index 6cc947b..1f400f1 100644 --- a/src/main/java/com/ai/aigenerate/chat/ChatService.java +++ b/src/main/java/com/ai/aigenerate/chat/ChatService.java @@ -44,6 +44,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -197,6 +198,88 @@ public ChatResponse chatDefaultFunction(ChatRequest chatRequest){ return chatResponse; } + public void autoChatStream(ChatRequest chatRequest, SseEmitter sseEmitter){ + String traceId = MdcUtils.generateTraceId(); + MdcUtils.setTraceId(traceId); + FunctionEventSourceListener eventSourceListener = new FunctionEventSourceListener(sseEmitter); + Message message = Message.builder().role(Message.Role.USER).content(chatRequest.getPrompt()).build(); + List messages = chatRequest.getMessages(); + if (messages == null) + messages = new ArrayList<>(); + messages.add(message); + try { + ChatCompletion chatCompletion = ChatCompletion + .builder() + .messages(messages) + .maxTokens(chatRequest.getMaxTokens() != null?chatRequest.getMaxTokens():2048) + .temperature(chatRequest.getTemperature() != null?chatRequest.getTemperature():0.2) + .topP(chatRequest.getTopP() != null?chatRequest.getTopP():1.0) + .n(chatRequest.getN() != null?chatRequest.getN():1) + .model(chatRequest.getModel() != null?chatRequest.getModel() : ChatCompletion.Model.GPT_3_5_TURBO_16K_0613.getName()) + .build(); + if (chatRequest.getIsFunction() != null && chatRequest.getIsFunction()) { + List functionList = autoFindFunction(chatRequest); + if (!CollectionUtils.isEmpty(functionList)){ + chatCompletion.setFunctions(gptFunctionFactory.getFunctionsByFunctionNameList(functionList)); + chatCompletion.setFunctionCall("auto"); + } + } + GptStreamContext gptStreamContext = GptStreamContext.builder() + .gptHandlerHistories(new ArrayList<>()) + .messages(messages) + .openAiStreamClient(openAiStreamClient) + .chatCompletion(chatCompletion) + .requestId(chatRequest.getRequestId()) + .functionEventSourceListener(eventSourceListener) + .timeout(120000l) + .build(); + ContextMap.putStreamContext(traceId, gptStreamContext); + //todo + MdcUtils.setTraceId(traceId); + openAiStreamClient.streamChatCompletion(chatCompletion, eventSourceListener); + ChatChoice chatChoice = eventSourceListener.getChatChoice(); + doStreamFunction(chatChoice); + log.info("traceId:{},成功获取结果,调用链路:{}", traceId, gptStreamContext.getGptHandlerHistories()); + ContextMap.remove(traceId); + } catch (Exception e) { + log.error("traceId:{},异常:{}", traceId, e); + }finally { + ContextMap.remove(traceId); + MdcUtils.removeTraceId(); + sseEmitter.complete(); + } + } + + public void pictureChatStream(ChatRequest chatRequest, SseEmitter sseEmitter){ + String traceId = MdcUtils.generateTraceId(); + MdcUtils.setTraceId(traceId); + FunctionEventSourceListener eventSourceListener = new FunctionEventSourceListener(sseEmitter); + Content textContent = Content.builder().text(chatRequest.getPrompt()).type(Content.Type.TEXT.getName()).build(); + ImageUrl imageUrl = ImageUrl.builder().url(chatRequest.getImageUrl()).build(); + Content imageContent = Content.builder().imageUrl(imageUrl).type(Content.Type.IMAGE_URL.getName()).build(); + List contentList = new ArrayList<>(); + contentList.add(textContent); + contentList.add(imageContent); + MessagePicture message = MessagePicture.builder().role(Message.Role.USER).content(contentList).build(); + ChatCompletionWithPicture chatCompletion = ChatCompletionWithPicture + .builder() + .messages(Collections.singletonList(message)) + .model(ChatCompletion.Model.GPT_4_VISION_PREVIEW.getName()) + .build(); + try { + openAiStreamClient.streamChatCompletion(chatCompletion, eventSourceListener); + ChatChoice chatChoice = eventSourceListener.getChatChoice(); + doStreamFunction(chatChoice); + ContextMap.remove(traceId); + } catch (Exception e) { + log.error("traceId:{},异常:{}", traceId, e); + }finally { + ContextMap.remove(traceId); + MdcUtils.removeTraceId(); + sseEmitter.complete(); + } + } + private List autoFindFunction(ChatRequest chatRequest) { ChatRequest completionRequest = new ChatRequest(); diff --git a/src/main/java/com/ai/aigenerate/facade/ChatFacade.java b/src/main/java/com/ai/aigenerate/facade/ChatFacade.java index 89f45be..036081b 100644 --- a/src/main/java/com/ai/aigenerate/facade/ChatFacade.java +++ b/src/main/java/com/ai/aigenerate/facade/ChatFacade.java @@ -59,7 +59,27 @@ public SseEmitter chatStream(@RequestBody ChatRequest chatRequest){ } SseEmitter sseEmitter = chatService.createSse(chatRequest.getRequestId()); executor.execute(() -> { - chatService.chatStream(chatRequest,sseEmitter); + if ("gpt-4-vision-preview".equals(chatRequest.getModel())) { + chatService.pictureChatStream(chatRequest, sseEmitter); + }else { + chatService.chatStream(chatRequest,sseEmitter); + } + }); + return sseEmitter; + } + + @PostMapping("auto/chatStream") + public SseEmitter autoChatStream(@RequestBody ChatRequest chatRequest){ + if (chatRequest.getToken() == null || !chatRequest.getToken().equals(token)){ + throw new RuntimeException("token error"); + } + SseEmitter sseEmitter = chatService.createSse(chatRequest.getRequestId()); + executor.execute(() -> { + if ("gpt-4-vision-preview".equals(chatRequest.getModel())) { + chatService.pictureChatStream(chatRequest, sseEmitter); + }else { + chatService.autoChatStream(chatRequest, sseEmitter); + } }); return sseEmitter; } diff --git a/src/main/java/com/ai/aigenerate/model/request/chat/ChatRequest.java b/src/main/java/com/ai/aigenerate/model/request/chat/ChatRequest.java index babeec5..8a862ae 100644 --- a/src/main/java/com/ai/aigenerate/model/request/chat/ChatRequest.java +++ b/src/main/java/com/ai/aigenerate/model/request/chat/ChatRequest.java @@ -31,4 +31,6 @@ public class ChatRequest { private String token; + private String imageUrl; + } From ccda1a34e79345ca575ad8c8a67f203167b1f01c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E4=B8=80=E5=87=A1?= Date: Wed, 21 Feb 2024 13:50:48 +0800 Subject: [PATCH 07/12] =?UTF-8?q?1=E3=80=81=E6=96=B0=E5=A2=9EGoogle?= =?UTF-8?q?=E6=90=9C=E7=B4=A2=E6=8F=92=E4=BB=B6=202=E3=80=81=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E6=8F=92=E4=BB=B6=E5=86=B3=E7=AD=96=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/AbstractGptFunctionHandler.java | 20 +++++++- .../com/ai/aigenerate/chat/ChatService.java | 48 ++++++++----------- .../com/ai/aigenerate/chat/GptContext.java | 5 +- .../ai/aigenerate/chat/GptStreamContext.java | 4 +- .../chat/custom/AiNewsFunctionHandler.java | 2 +- .../custom/AnalyzeLinkFunctionHandler.java | 2 +- .../chat/custom/BaiduGptFunctionHandler.java | 8 ++-- .../chat/custom/MoyuPaperFunctionHandler.java | 28 +++++++++++ .../chat/custom/NewsImageFunctionHandler.java | 28 +++++++++++ .../chat/tool/GoogleSearchService.java | 38 +++++++++++++++ .../com/ai/aigenerate/config/GptConfig.java | 3 ++ .../ai/aigenerate/constant/PromptContent.java | 26 ++++++++++ ...uSearchRequest.java => SearchRequest.java} | 2 +- 13 files changed, 176 insertions(+), 38 deletions(-) create mode 100644 src/main/java/com/ai/aigenerate/chat/custom/MoyuPaperFunctionHandler.java create mode 100644 src/main/java/com/ai/aigenerate/chat/custom/NewsImageFunctionHandler.java create mode 100644 src/main/java/com/ai/aigenerate/chat/tool/GoogleSearchService.java create mode 100644 src/main/java/com/ai/aigenerate/constant/PromptContent.java rename src/main/java/com/ai/aigenerate/model/request/baidu/{BaiduSearchRequest.java => SearchRequest.java} (79%) diff --git a/src/main/java/com/ai/aigenerate/chat/AbstractGptFunctionHandler.java b/src/main/java/com/ai/aigenerate/chat/AbstractGptFunctionHandler.java index a6811ec..29770d9 100644 --- a/src/main/java/com/ai/aigenerate/chat/AbstractGptFunctionHandler.java +++ b/src/main/java/com/ai/aigenerate/chat/AbstractGptFunctionHandler.java @@ -1,15 +1,21 @@ package com.ai.aigenerate.chat; +import com.ai.aigenerate.config.GptConfig; import com.ai.aigenerate.utils.MdcUtils; import com.unfbx.chatgpt.OpenAiClient; import com.unfbx.chatgpt.OpenAiStreamClient; import com.unfbx.chatgpt.entity.chat.*; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; + import java.util.List; @Slf4j public abstract class AbstractGptFunctionHandler implements GptFunctionService { + @Autowired + private GptConfig gptConfig; + public ChatChoice preHandle(ChatChoice chatChoice){ String requestId = MdcUtils.getTraceId(); Functions functions = getFunction(); @@ -26,6 +32,9 @@ public ChatChoice preHandle(ChatChoice chatChoice){ } String result; try { + if (System.currentTimeMillis()-gptContext.getStartTime() > gptConfig.getChatFunctionTimeout()){ + throw new IllegalArgumentException("函数调用超时"); + } result = doHandle(chatChoice.getMessage().getFunctionCall().getArguments()); }catch (Exception e){ log.error("插件调用失败,chatChoice:{},errorMsg:{}",chatChoice,e); @@ -60,7 +69,16 @@ public ChatChoice streamHandle(ChatChoice chatChoice){ FunctionEventSourceListener functionEventSourceListener = gptStreamContext.getFunctionEventSourceListener(); String args = chatChoice.getDelta().getFunctionCall().getArguments(); log.info("构造的方法参数:{}", args); - String result = doHandle(args); + String result; + try { + if (System.currentTimeMillis()-gptStreamContext.getStartTime() > gptConfig.getChatFunctionTimeout()){ + throw new IllegalArgumentException("函数调用超时"); + } + result = doHandle(args); + }catch (Exception e){ + log.error("插件调用失败,chatChoice:{},errorMsg:{}",chatChoice,e); + result = "插件调用失败"; + } FunctionCall functionCall = FunctionCall.builder() .arguments(args) .name(functions.getName()) diff --git a/src/main/java/com/ai/aigenerate/chat/ChatService.java b/src/main/java/com/ai/aigenerate/chat/ChatService.java index 1f400f1..451f723 100644 --- a/src/main/java/com/ai/aigenerate/chat/ChatService.java +++ b/src/main/java/com/ai/aigenerate/chat/ChatService.java @@ -3,7 +3,7 @@ import cn.hutool.json.JSONArray; import cn.hutool.json.JSONObject; import com.ai.aigenerate.config.GptConfig; -import com.ai.aigenerate.constant.VoiceContent; +import com.ai.aigenerate.constant.PromptContent; import com.ai.aigenerate.model.request.chat.ChatRequest; import com.ai.aigenerate.model.response.chat.ChatResponse; import com.ai.aigenerate.model.response.chat.FunctionResponse; @@ -113,7 +113,7 @@ public ChatResponse chat(ChatRequest chatRequest){ .temperature(chatRequest.getTemperature() != null?chatRequest.getTemperature():0.2) .topP(chatRequest.getTopP() != null?chatRequest.getTopP():1.0) .n(chatRequest.getN() != null?chatRequest.getN():1) - .model(chatRequest.getModel() != null?chatRequest.getModel() : ChatCompletion.Model.GPT_3_5_TURBO_16K_0613.getName()) + .model(chatRequest.getModel() != null?chatRequest.getModel() : "gpt-3.5-turbo-0125") .build(); if (chatRequest.getIsFunction() != null && chatRequest.getIsFunction()) { chatCompletion.setFunctions(gptFunctionFactory.getFunctionsByFunctionNameList(chatRequest.getFunctionNameList())); @@ -125,7 +125,8 @@ public ChatResponse chat(ChatRequest chatRequest){ .openAiClient(openAiClient) .chatCompletion(chatCompletion) .requestId(chatRequest.getRequestId()) - .timeout(1200000000l) + .timeout(gptConfig.getChatFunctionTimeout()) + .startTime(System.currentTimeMillis()) .build(); ContextMap.put(traceId, gptContext); ChatCompletionResponse chatCompletionResponse = openAiClient.chatCompletion(chatCompletion); @@ -161,11 +162,11 @@ public ChatResponse chatDefaultFunction(ChatRequest chatRequest){ ChatCompletion chatCompletion = ChatCompletion .builder() .messages(messages) - .maxTokens(chatRequest.getMaxTokens() != null?chatRequest.getMaxTokens():8000) + .maxTokens(chatRequest.getMaxTokens() != null?chatRequest.getMaxTokens():4096) .temperature(chatRequest.getTemperature() != null?chatRequest.getTemperature():0.2) .topP(chatRequest.getTopP() != null?chatRequest.getTopP():1.0) .n(chatRequest.getN() != null?chatRequest.getN():1) - .model(chatRequest.getModel() != null?chatRequest.getModel() : ChatCompletion.Model.GPT_3_5_TURBO_16K_0613.getName()) + .model(chatRequest.getModel() != null?chatRequest.getModel() : "gpt-3.5-turbo-0125") .build(); if (chatRequest.getIsFunction() != null && chatRequest.getIsFunction()) { List functionList = autoFindFunction(chatRequest); @@ -180,7 +181,8 @@ public ChatResponse chatDefaultFunction(ChatRequest chatRequest){ .openAiClient(openAiClient) .chatCompletion(chatCompletion) .requestId(chatRequest.getRequestId()) - .timeout(120000l) + .timeout(gptConfig.getChatFunctionTimeout()) + .startTime(System.currentTimeMillis()) .build(); MdcUtils.setTraceId(traceId); ContextMap.put(traceId, gptContext); @@ -211,11 +213,11 @@ public void autoChatStream(ChatRequest chatRequest, SseEmitter sseEmitter){ ChatCompletion chatCompletion = ChatCompletion .builder() .messages(messages) - .maxTokens(chatRequest.getMaxTokens() != null?chatRequest.getMaxTokens():2048) + .maxTokens(chatRequest.getMaxTokens() != null?chatRequest.getMaxTokens():4096) .temperature(chatRequest.getTemperature() != null?chatRequest.getTemperature():0.2) .topP(chatRequest.getTopP() != null?chatRequest.getTopP():1.0) .n(chatRequest.getN() != null?chatRequest.getN():1) - .model(chatRequest.getModel() != null?chatRequest.getModel() : ChatCompletion.Model.GPT_3_5_TURBO_16K_0613.getName()) + .model(chatRequest.getModel() != null?chatRequest.getModel() : "gpt-3.5-turbo-0125") .build(); if (chatRequest.getIsFunction() != null && chatRequest.getIsFunction()) { List functionList = autoFindFunction(chatRequest); @@ -231,7 +233,8 @@ public void autoChatStream(ChatRequest chatRequest, SseEmitter sseEmitter){ .chatCompletion(chatCompletion) .requestId(chatRequest.getRequestId()) .functionEventSourceListener(eventSourceListener) - .timeout(120000l) + .timeout(gptConfig.getChatFunctionTimeout()) + .startTime(System.currentTimeMillis()) .build(); ContextMap.putStreamContext(traceId, gptStreamContext); //todo @@ -268,8 +271,6 @@ public void pictureChatStream(ChatRequest chatRequest, SseEmitter sseEmitter){ .build(); try { openAiStreamClient.streamChatCompletion(chatCompletion, eventSourceListener); - ChatChoice chatChoice = eventSourceListener.getChatChoice(); - doStreamFunction(chatChoice); ContextMap.remove(traceId); } catch (Exception e) { log.error("traceId:{},异常:{}", traceId, e); @@ -292,26 +293,16 @@ private List autoFindFunction(ChatRequest chatRequest) { jsonObject.putOpt("函数描述",function.getDescription()); jsonArray.add(jsonObject); } - Message systemMessage = Message.builder().role(Message.Role.SYSTEM).content("你现在是一个函数判断器,这是我的要求\n" + - "1、请根据函数描述返回需要使用的函数\n" + - "2、必须用json返回结果,例如[\"queryWeather\",\"sendMail\"],不要输出额外的内容,没有命中就返回空数组\n" + - "3、这是所有的函数定义:"+jsonArray).build(); - Message userMessage = Message.builder().role(Message.Role.USER).content("将上海天气发送给4198123131@qq.com").build(); - Message assistantMessage = Message.builder().role(Message.Role.ASSISTANT).content("[\"queryWeather\",\"sendMail\"]").build(); - Message userMessage1 = Message.builder().role(Message.Role.USER).content("你是谁").build(); - Message assistantMessage1 = Message.builder().role(Message.Role.ASSISTANT).content("[]").build(); + Message systemMessage = Message.builder().role(Message.Role.SYSTEM).content(PromptContent.autoStrategyPrompt).build(); roleList.add(systemMessage); - roleList.add(userMessage); - roleList.add(assistantMessage); - roleList.add(userMessage1); - roleList.add(assistantMessage1); completionRequest.setMessages(roleList); completionRequest.setPrompt(chatRequest.getPrompt()); completionRequest.setRequestId(chatRequest.getRequestId()); completionRequest.setIsFunction(false); - completionRequest.setMaxTokens(12000); - completionRequest.setModel(ChatCompletion.Model.GPT_3_5_TURBO_16K.getName()); + completionRequest.setMaxTokens(4096); + completionRequest.setModel("gpt-4-turbo-preview"); String result = chat(completionRequest).getResult(); + log.info("函数决策结果:{}",result); return JSON.parseArray(result,String.class); } @@ -380,11 +371,11 @@ public void chatStream(ChatRequest chatRequest, SseEmitter sseEmitter){ ChatCompletion chatCompletion = ChatCompletion .builder() .messages(messages) - .maxTokens(chatRequest.getMaxTokens() != null?chatRequest.getMaxTokens():2048) + .maxTokens(chatRequest.getMaxTokens() != null?chatRequest.getMaxTokens():4096) .temperature(chatRequest.getTemperature() != null?chatRequest.getTemperature():0.2) .topP(chatRequest.getTopP() != null?chatRequest.getTopP():1.0) .n(chatRequest.getN() != null?chatRequest.getN():1) - .model(chatRequest.getModel() != null?chatRequest.getModel() : ChatCompletion.Model.GPT_3_5_TURBO_16K_0613.getName()) + .model(chatRequest.getModel() != null?chatRequest.getModel() : "gpt-3.5-turbo-0125") .build(); if (chatRequest.getIsFunction() != null && chatRequest.getIsFunction() && !CollectionUtils.isEmpty(chatRequest.getFunctionNameList())) { chatCompletion.setFunctions(gptFunctionFactory.getFunctionsByFunctionNameList(chatRequest.getFunctionNameList())); @@ -397,7 +388,8 @@ public void chatStream(ChatRequest chatRequest, SseEmitter sseEmitter){ .chatCompletion(chatCompletion) .requestId(chatRequest.getRequestId()) .functionEventSourceListener(eventSourceListener) - .timeout(120000l) + .timeout(gptConfig.getChatFunctionTimeout()) + .startTime(System.currentTimeMillis()) .build(); ContextMap.putStreamContext(traceId, gptStreamContext); openAiStreamClient.streamChatCompletion(chatCompletion, eventSourceListener); diff --git a/src/main/java/com/ai/aigenerate/chat/GptContext.java b/src/main/java/com/ai/aigenerate/chat/GptContext.java index 39f6c53..3d79f9c 100644 --- a/src/main/java/com/ai/aigenerate/chat/GptContext.java +++ b/src/main/java/com/ai/aigenerate/chat/GptContext.java @@ -18,9 +18,12 @@ public class GptContext { private String requestId; - //todo 超时时间,毫秒 + //超时时间,毫秒 private Long timeout; + //开始时间 + private Long startTime; + private OpenAiClient openAiClient; private ChatCompletion chatCompletion; diff --git a/src/main/java/com/ai/aigenerate/chat/GptStreamContext.java b/src/main/java/com/ai/aigenerate/chat/GptStreamContext.java index 9ac4abe..6490ebc 100644 --- a/src/main/java/com/ai/aigenerate/chat/GptStreamContext.java +++ b/src/main/java/com/ai/aigenerate/chat/GptStreamContext.java @@ -19,9 +19,11 @@ public class GptStreamContext{ private String requestId; - //todo 超时时间,毫秒 + //超时时间,毫秒 private Long timeout; + private Long startTime; + private OpenAiStreamClient openAiStreamClient; private ChatCompletion chatCompletion; diff --git a/src/main/java/com/ai/aigenerate/chat/custom/AiNewsFunctionHandler.java b/src/main/java/com/ai/aigenerate/chat/custom/AiNewsFunctionHandler.java index eb097f6..2319784 100644 --- a/src/main/java/com/ai/aigenerate/chat/custom/AiNewsFunctionHandler.java +++ b/src/main/java/com/ai/aigenerate/chat/custom/AiNewsFunctionHandler.java @@ -21,7 +21,7 @@ public String doHandle(String paramJson) { public Functions getFunction() { Functions functions = Functions.builder() .name("getAiNews") - .description("获取跟Ai相关的新闻资讯") + .description("获取当天人工智能技术的资讯信息") .build(); return functions; } diff --git a/src/main/java/com/ai/aigenerate/chat/custom/AnalyzeLinkFunctionHandler.java b/src/main/java/com/ai/aigenerate/chat/custom/AnalyzeLinkFunctionHandler.java index 57c97aa..b1d60a7 100644 --- a/src/main/java/com/ai/aigenerate/chat/custom/AnalyzeLinkFunctionHandler.java +++ b/src/main/java/com/ai/aigenerate/chat/custom/AnalyzeLinkFunctionHandler.java @@ -38,7 +38,7 @@ public Functions getFunction() { .required(Arrays.asList("url")).build(); Functions functions = Functions.builder() .name("analyzeLink") - .description("根据给出的网址链接地址解析网页中的内容,以进行后续的分析") + .description("根据指定链接读取信息。当涉及实时资讯信息通过谷歌搜索后根据当前信息无法直接获取结果,选择最匹配的一个链接进行解析,以进行后续的分析") .parameters(parameters) .build(); return functions; diff --git a/src/main/java/com/ai/aigenerate/chat/custom/BaiduGptFunctionHandler.java b/src/main/java/com/ai/aigenerate/chat/custom/BaiduGptFunctionHandler.java index d331f97..331f994 100644 --- a/src/main/java/com/ai/aigenerate/chat/custom/BaiduGptFunctionHandler.java +++ b/src/main/java/com/ai/aigenerate/chat/custom/BaiduGptFunctionHandler.java @@ -1,7 +1,7 @@ package com.ai.aigenerate.chat.custom; import com.ai.aigenerate.chat.AbstractGptFunctionHandler; -import com.ai.aigenerate.model.request.baidu.BaiduSearchRequest; +import com.ai.aigenerate.model.request.baidu.SearchRequest; import com.ai.aigenerate.utils.HttpClientUtils; import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson2.JSON; @@ -12,13 +12,13 @@ import java.util.Arrays; @Component -public class BaiduGptFunctionHandler extends AbstractGptFunctionHandler { +public class BaiduGptFunctionHandler extends AbstractGptFunctionHandler { @Override public String doHandle(String paramJson) { - BaiduSearchRequest baiduSearchRequest = JSON.parseObject(paramJson, BaiduSearchRequest.class); - String key = baiduSearchRequest.getKeyword().replace(" ",""); + SearchRequest searchRequest = JSON.parseObject(paramJson, SearchRequest.class); + String key = searchRequest.getKeyword().replace(" ",""); JSONObject jsonObject = HttpClientUtils.httpGet("https://baike.baidu.com/api/openapi/BaikeLemmaCardApi?scope=103&format=json&appid=379020&bk_key="+key+"&bk_length=600"); return jsonObject.toJSONString(); } diff --git a/src/main/java/com/ai/aigenerate/chat/custom/MoyuPaperFunctionHandler.java b/src/main/java/com/ai/aigenerate/chat/custom/MoyuPaperFunctionHandler.java new file mode 100644 index 0000000..c32e88f --- /dev/null +++ b/src/main/java/com/ai/aigenerate/chat/custom/MoyuPaperFunctionHandler.java @@ -0,0 +1,28 @@ +package com.ai.aigenerate.chat.custom; + +import com.ai.aigenerate.chat.AbstractGptFunctionHandler; +import com.ai.aigenerate.chat.tool.MoyuService; +import com.unfbx.chatgpt.entity.chat.Functions; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class MoyuPaperFunctionHandler extends AbstractGptFunctionHandler { + + @Autowired + private MoyuService moyuService; + + @Override + public String doHandle(String paramJson) { + return "{\"图片的png链接\":\"" + moyuService.getRelaxPaper() + "\"}"; + } + + @Override + public Functions getFunction() { + Functions functions = Functions.builder() + .name("getMoyuPaper") + .description("获取摸鱼日报的图片") + .build(); + return functions; + } +} \ No newline at end of file diff --git a/src/main/java/com/ai/aigenerate/chat/custom/NewsImageFunctionHandler.java b/src/main/java/com/ai/aigenerate/chat/custom/NewsImageFunctionHandler.java new file mode 100644 index 0000000..dd2ad3a --- /dev/null +++ b/src/main/java/com/ai/aigenerate/chat/custom/NewsImageFunctionHandler.java @@ -0,0 +1,28 @@ +package com.ai.aigenerate.chat.custom; + +import com.ai.aigenerate.chat.AbstractGptFunctionHandler; +import com.ai.aigenerate.chat.tool.MorningPaperService; +import com.unfbx.chatgpt.entity.chat.Functions; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class NewsImageFunctionHandler extends AbstractGptFunctionHandler { + + @Autowired + private MorningPaperService morningPaperService; + + @Override + public String doHandle(String paramJson) { + return "{\"图片的链接\":\"" + morningPaperService.getMorningPaper() + "\"}"; + } + + @Override + public Functions getFunction() { + Functions functions = Functions.builder() + .name("getNewsPicture") + .description("获取新闻早报的图片") + .build(); + return functions; + } +} \ No newline at end of file diff --git a/src/main/java/com/ai/aigenerate/chat/tool/GoogleSearchService.java b/src/main/java/com/ai/aigenerate/chat/tool/GoogleSearchService.java new file mode 100644 index 0000000..5f8a7fe --- /dev/null +++ b/src/main/java/com/ai/aigenerate/chat/tool/GoogleSearchService.java @@ -0,0 +1,38 @@ +package com.ai.aigenerate.chat.tool; + +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import lombok.SneakyThrows; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.select.Elements; +import org.springframework.stereotype.Service; +import java.net.URLEncoder; + +@Service +public class GoogleSearchService { + + @SneakyThrows + public String googleSearch(String keyword) { + int numResults = 10; // 返回结果数量 + String languageCode = "cn"; // 语言设置(中文) + String url = String.format("https://www.google.com/search?q=%s&num=%d&hl=%s", URLEncoder.encode(keyword), numResults, languageCode); + Document document = Jsoup.connect(url).timeout(40000).get(); + Elements results = document.select("div.g"); // Google搜索结果的CSS选择器 + JSONArray jsonArray = new JSONArray(); + for (Element result : results) { + // Extract the title and link of the result + String title = result.select("h3").text(); + String link = result.select("h3").parents().attr("href"); + String snippet = result.select(".VwiC3b").text(); + JSONObject jsonObject = new JSONObject(); + jsonObject.put("title", title); + jsonObject.put("url", link); + jsonObject.put("content", snippet); + // 将JSON对象添加到数组中 + jsonArray.add(jsonObject); + } + return jsonArray.toJSONString(); + } +} diff --git a/src/main/java/com/ai/aigenerate/config/GptConfig.java b/src/main/java/com/ai/aigenerate/config/GptConfig.java index 96652e6..2a81ffa 100644 --- a/src/main/java/com/ai/aigenerate/config/GptConfig.java +++ b/src/main/java/com/ai/aigenerate/config/GptConfig.java @@ -39,6 +39,9 @@ public class GptConfig { @Value("${voice.prompt.system:请使用中文交流,回答要求尽可能简短,不能超过100个字}") private String voicePromptSystem; + @Value("${chat.function.timeout:180000}") + private Long chatFunctionTimeout; + public Map getLinkAiApiKeyMap(){ Map map = new HashMap<>(); String[] split = linkAiApiKeyMap.split(","); diff --git a/src/main/java/com/ai/aigenerate/constant/PromptContent.java b/src/main/java/com/ai/aigenerate/constant/PromptContent.java new file mode 100644 index 0000000..03daa79 --- /dev/null +++ b/src/main/java/com/ai/aigenerate/constant/PromptContent.java @@ -0,0 +1,26 @@ +package com.ai.aigenerate.constant; + +public class PromptContent { + + public static final String autoStrategyPrompt = "你现在是一个函数决策工具,这是我的要求\n" + + "1、请根据函数描述返回需要使用的函数\n" + + "2、必须用json返回结果,例如[\"queryWeather\",\"sendMail\"],不要输出额外的内容,没有命中就返回空数组\n" + + "3、这是所有的函数定义:\n" + + "```\n" + + "1、函数名:getAiNews;触发条件:当涉及当天人工智能技术的资讯信息时使用\n" + + "2、函数名:analyzeLink;触发条件:当涉及到解析链接内容时触发,或者需要获取实时资讯信息时配合googleSearch触发\n" + + "3、函数名:baiduBaikeSearch;触发条件:当涉及到进行百度百科搜索时触发\n" + + "4、函数名:baiduSearch;触发条件:通过百度进行搜索,只有在提及使用百度进行搜索时才会触发,否则默认使用googleSearch函数\n" + + "5、函数名:getCurrentTime;触发条件:当需要获取当前最新时间时触发该函数\n" + + "6、函数名:googleSearch;触发条件:当需要使用搜索实时信息或资讯有关的问题会通过意图识别决策到该插件去谷歌上搜索,一般还会配合analyzeLink读取链接里面的内容\n" + + "7、函数名:getCrazyKfc;触发条件:当被要求获取疯狂星期四的文案时触发\n" + + "8、函数名:sendMail;触发条件:当需要给指定邮箱发送邮件时触发\n" + + "9、函数名:createImage;触发条件:当被要求创作绘画一张图片时触发\n" + + "10、函数名:getMoyuPaper;触发条件:当获取摸鱼日报的图片时触发\n" + + "11、函数名:getNews;触发条件:当需要获取新闻信息时触发,但不包括人工智能技术的资讯\n" + + "12、函数名:getNewsPicture;触发条件:当需要获取新闻早报图片时触发\n" + + "13、函数名:queryWeather;触发条件:当需要获取指定地区最新天气时触发\n" + + "14、函数名:weiboHotSearch;触发条件:获取微博热搜数据,必须提及微博热搜才进行调用\n" + + "```" + ; +} diff --git a/src/main/java/com/ai/aigenerate/model/request/baidu/BaiduSearchRequest.java b/src/main/java/com/ai/aigenerate/model/request/baidu/SearchRequest.java similarity index 79% rename from src/main/java/com/ai/aigenerate/model/request/baidu/BaiduSearchRequest.java rename to src/main/java/com/ai/aigenerate/model/request/baidu/SearchRequest.java index 4c2de69..fd9cb48 100644 --- a/src/main/java/com/ai/aigenerate/model/request/baidu/BaiduSearchRequest.java +++ b/src/main/java/com/ai/aigenerate/model/request/baidu/SearchRequest.java @@ -3,7 +3,7 @@ import lombok.Data; @Data -public class BaiduSearchRequest { +public class SearchRequest { private String keyword; From 68c8f56fd55aa72c9b1b530c634b45b42f69d84f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E4=B8=80=E5=87=A1?= Date: Wed, 21 Feb 2024 13:51:54 +0800 Subject: [PATCH 08/12] add search plugin --- .../custom/BaiduSearchGptFunctionHandler.java | 13 +++-- .../GoogleSearchGptFunctionHandler.java | 55 +++++++++++++++++++ 2 files changed, 62 insertions(+), 6 deletions(-) create mode 100644 src/main/java/com/ai/aigenerate/chat/custom/GoogleSearchGptFunctionHandler.java diff --git a/src/main/java/com/ai/aigenerate/chat/custom/BaiduSearchGptFunctionHandler.java b/src/main/java/com/ai/aigenerate/chat/custom/BaiduSearchGptFunctionHandler.java index eba6a5d..4451cf1 100644 --- a/src/main/java/com/ai/aigenerate/chat/custom/BaiduSearchGptFunctionHandler.java +++ b/src/main/java/com/ai/aigenerate/chat/custom/BaiduSearchGptFunctionHandler.java @@ -4,28 +4,29 @@ import cn.hutool.json.JSONObject; import com.ai.aigenerate.chat.AbstractGptFunctionHandler; import com.ai.aigenerate.chat.tool.BaiduSearchService; -import com.ai.aigenerate.model.request.baidu.BaiduSearchRequest; +import com.ai.aigenerate.model.request.baidu.SearchRequest; import com.alibaba.fastjson2.JSON; import com.unfbx.chatgpt.entity.chat.Functions; import com.unfbx.chatgpt.entity.chat.Parameters; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; + import java.util.Arrays; @Component -public class BaiduSearchGptFunctionHandler extends AbstractGptFunctionHandler { +public class BaiduSearchGptFunctionHandler extends AbstractGptFunctionHandler { @Autowired private BaiduSearchService baiduSearchService; - @Value("${function.baidu.search.desc:根据关键词在网络上进行搜索查询,比如获取近况或者关键词的最新消息等,关键字不允许出现空格,搜索结果以json格式返回}") + @Value("${function.baidu.search.desc:通过百度进行搜索,搜索结果以json格式返回}") private String baiduSearchFunctionDesc; @Override public String doHandle(String paramJson) { - BaiduSearchRequest baiduSearchRequest = JSON.parseObject(paramJson, BaiduSearchRequest.class); - return baiduSearchService.getBaiduSearchResult(baiduSearchRequest.getKeyword()); + SearchRequest searchRequest = JSON.parseObject(paramJson, SearchRequest.class); + return baiduSearchService.getBaiduSearchResult(searchRequest.getKeyword()); } @Override @@ -35,7 +36,7 @@ public Functions getFunction() { keyword.putOpt("description", "查询的关键字,参数中不允许出现空格"); //参数 - JSONObject properties = new cn.hutool.json.JSONObject(); + JSONObject properties = new JSONObject(); properties.putOpt("keyword", keyword); Parameters parameters = Parameters.builder() .type("object") diff --git a/src/main/java/com/ai/aigenerate/chat/custom/GoogleSearchGptFunctionHandler.java b/src/main/java/com/ai/aigenerate/chat/custom/GoogleSearchGptFunctionHandler.java new file mode 100644 index 0000000..7ea5db5 --- /dev/null +++ b/src/main/java/com/ai/aigenerate/chat/custom/GoogleSearchGptFunctionHandler.java @@ -0,0 +1,55 @@ +package com.ai.aigenerate.chat.custom; + + +import cn.hutool.json.JSONObject; +import com.ai.aigenerate.chat.AbstractGptFunctionHandler; +import com.ai.aigenerate.chat.tool.GoogleSearchService; +import com.ai.aigenerate.model.request.baidu.SearchRequest; +import com.alibaba.fastjson2.JSON; +import com.unfbx.chatgpt.entity.chat.Functions; +import com.unfbx.chatgpt.entity.chat.Parameters; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import java.util.Arrays; + +@Component +public class GoogleSearchGptFunctionHandler extends AbstractGptFunctionHandler { + + @Autowired + private GoogleSearchService googleSearchService; + + @Value("${function.google.search.desc:" + + "当需要使用谷歌搜索实时信息或资讯有关的问题会通过意图识别决策到该插件,使用 \"搜索\" 或 \"查询\" 等关键词可以提高命中率\n" + + "You need to fully understand user needs and provide as complete and accurate search keywords as possible. \n" + + "Separate multiple keywords with spaces. \n" + + "Please avoid providing any extra text, so that I can directly pass the keywords to the search engine,搜索结果以json格式返回}") + private String googleSearchFunctionDesc; + + @Override + public String doHandle(String paramJson) { + SearchRequest searchRequest = JSON.parseObject(paramJson, SearchRequest.class); + return googleSearchService.googleSearch(searchRequest.getKeyword()); + } + + @Override + public Functions getFunction() { + JSONObject keyword = new JSONObject(); + keyword.putOpt("type", "string"); + keyword.putOpt("description", "查询的关键字,参数中不允许出现空格"); + + //参数 + JSONObject properties = new cn.hutool.json.JSONObject(); + properties.putOpt("keyword", keyword); + Parameters parameters = Parameters.builder() + .type("object") + .properties(properties) + .required(Arrays.asList("keyword")).build(); + Functions functions = Functions.builder() + .name("googleSearch") + .description(googleSearchFunctionDesc) + .parameters(parameters) + .build(); + return functions; + } +} From 962531cd408109c6759ccd4a7c9ee31c674519bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E4=B8=80=E5=87=A1?= Date: Wed, 21 Feb 2024 15:11:27 +0800 Subject: [PATCH 09/12] update wiki --- README.md | 67 ++++++++++++++++++++++++++----- README_EN.md | 110 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 167 insertions(+), 10 deletions(-) create mode 100644 README_EN.md diff --git a/README.md b/README.md index a25ecda..e0e1680 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # chatgpt-plus +To English Doc -> [English Doc](README_EN.md) + # 📖 项目简介 **ChatGPT自定义插件的客户端** @@ -15,15 +17,25 @@ #### 流式输出: image +### 自动决策 +根据请求内容,内置决策模块自动识别需要使用的插件,无需指定对应的插件名,支持多个插件同时使用,例如:将北京的天气发送给4214142@gmail.com + ### 目前已内置插件: -- [x] 当前时间查询 -- [x] mid-journey图片生成 -- [x] 城市天气查询 -- [x] 新闻查询 -- [x] 邮件发送 -- . -- . -- . +- [✅] 当前时间查询 +- [✅] mid-journey图片生成 +- [✅] 城市天气查询 +- [✅] 新闻查询 +- [✅] 邮件发送 +- [✅] 微博热搜 +- [✅] 百度搜索 +- [✅] 百度百科 +- [✅] 谷歌搜索 +- [✅] 网页链接读取 +- [✅] AI每日技术资讯 +- [✅] DallE3图片生成 +- [✅] 每日早报 +- [✅] 摸鱼日报 +- [todo] B站视频总结 - 持续更新中 # 🚀 快速开始 @@ -52,12 +64,47 @@ baidu.weather.secretKey = ## 新闻查询使用了聚合数据的接口,需要申请聚合数据的账号,然后创建应用,获取key,https://www.juhe.cn/docs/api/id/235 juhe.news.key = + +## 项目中使用了动态IP获取实时数据,如需使用百度微博等功能需要配置 ,我使用的产品https://www.kuaidaili.com/doc/product/dps/#fetchtypeip +proxy.ip.signature = +proxy.ip.secretId = + +## 接口的权限验证,配置后请求中必须带有token,否则会认证失败 +chatgpt.api.token = 123456 + +server.port = 15600 ``` ### 二、docker启动 -还未支持docker启动,很快会支持 + +docker pull uswccr.ccs.tencentyun.com/liyf/images:chatgpt-plus-v1.0 +或者 +docker pull a419820659/liyf007:chatgpt-plus-v1.0 + +``` +version: '3' +services: + myapp: + image: chatgpt-plus-v1.0 + ports: + - 15600:15600 + environment: + - mj.service.url=http://xxxxx:8080 + - chatgpt.api.key=sk-32131321ky8ph1231B2xxxxxvUqBX9 + - mail.host=smtp.qq.com + - mail.port=465 + - mail.username=xsds@qq.com + - mail.password=2312313 + - mail.subject=AI Chatbot + - baidu.weather.accessKey=sds + - baidu.weather.secretKey=sds + - juhe.news.key=ds + - proxy.ip.signature=dsds + - proxy.ip.secretId=dsds + - chatgpt.api.token=123123 +``` # 🙏 鸣谢 项目中依赖了大佬的代码,在此表示感谢🌹: - OpenAi:https://openai.com/ - chatgpt-java: https://github.com/Grt1228/chatgpt-java -- midjourney-proxy: https://github.com/novicezk/midjourney-proxy 项目内非代码直接依赖,如需图片生成自己单独启动该项目 +- midjourney-proxy: https://github.com/novicezk/midjourney-proxy 项目内非代码直接依赖,如需图片生成自己单独启动该项目 \ No newline at end of file diff --git a/README_EN.md b/README_EN.md new file mode 100644 index 0000000..9dbd21c --- /dev/null +++ b/README_EN.md @@ -0,0 +1,110 @@ +# chatgpt-plus + +To English Doc -> [中文文档](README.md) + +# 📖 Project Introduction + +**A client for custom ChatGPT plugins** + +#### This project is the full-blooded plus version of Open AI's ChatGPT. It provides some additional capabilities on top of the official ChatGPT, such as querying for daily news, weather, gas prices, stock market, etc. There's nothing you can't imagine that it can't do. The project is developed based on OPEN AI's functional invocation and currently has some pre-installed plugins (which will continue to be updated). It also supports developers adding their own custom plugins. The project supports both streaming and non-streaming calling methods. + +# 🚩 Features +#### Non-streaming output: +image +image + + +#### Streaming output: +image + +### Automatic Decision-making +According to the request content, the built-in decision module automatically identifies the plugins that need to be used, without the need to specify the name of the corresponding plugin, and supports the use of multiple plugins simultaneously, for example: sending Beijing's weather to 4214142@gmail.com + +### Currently Built-in Plugins: +- [✅] Current time query +- [✅] Mid-journey image generation +- [✅] City weather query +- [✅] News query +- [✅] Email sending +- [✅] Weibo hot search +- [✅] Baidu search +- [✅] Baidu encyclopedia +- [✅] Google search +- [✅] Web link reading +- [✅] AI daily tech news +- [✅] Dall-E 3 image generation +- [✅] Daily morning news +- [✅] Slack off daily +- [todo] Bilibili video summary +- Continuously updating + +# 🚀 Quick Start +### I. Local Launch + +Project environment requirements: jdk17 +##### Environment Variables + +``` +## The project referenced the mid-journey proxy project, which must be launched separately. Enter the address after startup +mj.service.url = http://ip:port + +## chatgpt api key +chatgpt.api.key = sk-xxxxx + +## Configuration information for sending emails +mail.host = smtp.xx.com +mail.port = 465 +mail.username = xxxx@xx.com +mail.password = xxxxxxx +mail.subject = AI Chatbot + +## The weather query used the interface of Baidu Intelligent Cloud. You need to apply for a Baidu Intelligent Cloud account, then create an application to get accessKey and secretKey. https://apis.baidu.com/store/detail/d031401a-4081-4572-8dd7-aca64223197e +baidu.weather.accessKey = +baidu.weather.secretKey = + +## The news query used the interface of Juhe Data. You need to apply for an account with Juhe Data, then create an application to get a key, https://www.juhe.cn/docs/api/id/235 +juhe.news.key = + +## The project used dynamic IP to get real-time data. To use functions like Baidu and Weibo, configure the following. I used the product https://www.kuaidaili.com/doc/product/dps/#fetchtypeip +proxy.ip.signature = +proxy.ip.secretId = + +## Interface authentication, you must carry a token in the request after configuration, otherwise authentication will fail +chatgpt.api.token = 123456 + +server.port = 15600 +``` +### II. Docker Launch + +docker pull uswccr.ccs.tencentyun.com/liyf/images:chatgpt-plus-v1.0 +or +docker pull a419820659/liyf007:chatgpt-plus-v1.0 + +``` +version: '3' +services: + myapp: + image: chatgpt-plus-v1.0 + ports: + - 15600:15600 + environment: + - mj.service.url=http://xxxxx:8080 + - chatgpt.api.key=sk-32131321ky8ph1231B2xxxxxvUqBX9 + - mail.host=smtp.qq.com + - mail.port=465 + - mail.username=xsds@qq.com + - mail.password=2312313 + - mail.subject=AI Chatbot + - baidu.weather.accessKey=sds + - baidu.weather.secretKey=sds + - juhe.news.key=ds + - proxy.ip.signature=dsds + - proxy.ip.secretId=dsds + - chatgpt.api.token=123123 +``` + +# 🙏 Acknowledgments +The project depends on the code of great developers, I would like to express my thanks 🌹: +- OpenAi: https://openai.com/ +- chatgpt-java: https://github.com/Grt1228/chatgpt-java +- midjourney-proxy: https://github.com/novicezk/midjourney-proxy The project does not have a direct code dependency, if you need image generation, you need to start the project separately \ No newline at end of file From 3e1b047742e1cc49f97151de7732aba80e0ac9a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E4=B8=80=E5=87=A1?= Date: Wed, 21 Feb 2024 15:40:43 +0800 Subject: [PATCH 10/12] update wiki --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index e0e1680..bece3de 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,7 @@ To English Doc -> [English Doc](README_EN.md) - [✅] 每日早报 - [✅] 摸鱼日报 - [todo] B站视频总结 +- [todo] 知识库引入,支持不同问题回复指定答案 - 持续更新中 # 🚀 快速开始 From 839ff3b591d1d313272bb81218bb86febcaa6a80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E4=B8=80=E5=87=A1?= Date: Tue, 23 Apr 2024 17:23:20 +0800 Subject: [PATCH 11/12] =?UTF-8?q?1=E3=80=81=E6=8E=A5=E5=85=A5=E7=9F=A5?= =?UTF-8?q?=E8=AF=86=E5=BA=93=EF=BC=8Clinkai=202=E3=80=81suno?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose.yml | 23 ++ .../com/ai/aigenerate/chat/ChatService.java | 39 ++- .../ai/aigenerate/chat/LinkAiChatService.java | 5 +- .../custom/BlackSilkImageFunctionHandler.java | 28 ++ .../com/ai/aigenerate/config/GptConfig.java | 37 ++- .../ai/aigenerate/constant/PromptContent.java | 4 +- .../com/ai/aigenerate/suno/SongGenerator.java | 274 ++++++++++++++++++ .../ai/aigenerate/suno/SunoSongGenerator.java | 41 +++ 8 files changed, 443 insertions(+), 8 deletions(-) create mode 100644 docker-compose.yml create mode 100644 src/main/java/com/ai/aigenerate/chat/custom/BlackSilkImageFunctionHandler.java create mode 100644 src/main/java/com/ai/aigenerate/suno/SongGenerator.java create mode 100644 src/main/java/com/ai/aigenerate/suno/SunoSongGenerator.java diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..4c87450 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,23 @@ +version: '3' +services: + myapp: + image: chatgpt-plus-v1.0 + ports: + - 15600:15600 + environment: + - mj.service.url=http://xxxxx:8080 + - chatgpt.api.key=sk-xxxxx + - mail.host=smtp.qq.com + - mail.port=465 + - mail.username=xsds@qq.com + - mail.password=xxxxxx + - mail.subject=AI Chatbot + - baidu.weather.accessKey=sds + - baidu.weather.secretKey=sds + - juhe.news.key=ds + - proxy.ip.signature=dsds + - proxy.ip.secretId=dsds + - chatgpt.api.token=123456 + + + diff --git a/src/main/java/com/ai/aigenerate/chat/ChatService.java b/src/main/java/com/ai/aigenerate/chat/ChatService.java index 451f723..32491e2 100644 --- a/src/main/java/com/ai/aigenerate/chat/ChatService.java +++ b/src/main/java/com/ai/aigenerate/chat/ChatService.java @@ -5,8 +5,10 @@ import com.ai.aigenerate.config.GptConfig; import com.ai.aigenerate.constant.PromptContent; import com.ai.aigenerate.model.request.chat.ChatRequest; +import com.ai.aigenerate.model.request.chat.LinkAiChatRequest; import com.ai.aigenerate.model.response.chat.ChatResponse; import com.ai.aigenerate.model.response.chat.FunctionResponse; +import com.ai.aigenerate.model.response.chat.KnowledgeResult; import com.ai.aigenerate.utils.MdcUtils; import com.alibaba.fastjson.JSON; import com.unfbx.chatgpt.OpenAiClient; @@ -29,6 +31,7 @@ import okhttp3.ResponseBody; import okhttp3.logging.HttpLoggingInterceptor; import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; @@ -36,7 +39,6 @@ import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; - import java.io.BufferedOutputStream; import java.io.File; import java.io.FileOutputStream; @@ -60,6 +62,9 @@ public class ChatService { @Autowired private GptConfig gptConfig; + @Autowired + private LinkAiChatService linkAiChatService; + private OpenAiClient openAiClient; private OpenAiStreamClient openAiStreamClient; @@ -145,6 +150,23 @@ public ChatResponse chat(ChatRequest chatRequest){ public ChatResponse chatDefaultFunction(ChatRequest chatRequest){ ChatResponse chatResponse = ChatResponse.builder().status("200").build(); + //知识库查询 + if (gptConfig.getKnowledgeSwitch()) { + try { + LinkAiChatRequest linkAiChatRequest = new LinkAiChatRequest(); + BeanUtils.copyProperties(chatRequest, linkAiChatRequest); + linkAiChatRequest.setKnowledgeBase(gptConfig.getKnowledgeBase()); + ChatResponse chat = linkAiChatService.chat(linkAiChatRequest); + log.info("linkAI返回结果:{}", chat.getResult()); + KnowledgeResult knowledgeResult = JSON.parseObject(chat.getResult(), KnowledgeResult.class); + if (knowledgeResult.getIsHitTarget()) { + chatResponse.setResult(knowledgeResult.getResponse()); + return chatResponse; + } + }catch (Exception e){ + log.error("调用Linkai失败",e); + } + } List messages = new ArrayList<>(); Message systemMessage = Message.builder().role(Message.Role.SYSTEM).content(gptConfig.getSystemPrompt()).build(); messages.add(systemMessage); @@ -271,6 +293,8 @@ public void pictureChatStream(ChatRequest chatRequest, SseEmitter sseEmitter){ .build(); try { openAiStreamClient.streamChatCompletion(chatCompletion, eventSourceListener); + ChatChoice chatChoice = eventSourceListener.getChatChoice(); + doStreamFunction(chatChoice); ContextMap.remove(traceId); } catch (Exception e) { log.error("traceId:{},异常:{}", traceId, e); @@ -293,14 +317,23 @@ private List autoFindFunction(ChatRequest chatRequest) { jsonObject.putOpt("函数描述",function.getDescription()); jsonArray.add(jsonObject); } - Message systemMessage = Message.builder().role(Message.Role.SYSTEM).content(PromptContent.autoStrategyPrompt).build(); + Message systemMessage = Message.builder().role(Message.Role.SYSTEM).content(gptConfig.getStrategyPrompt()).build(); roleList.add(systemMessage); + //将最近四条上下文放入决策中 + Message userMessage = Message.builder().role(Message.Role.USER).content("将上海天气发送给4198123131@qq.com").build(); + Message assistantMessage = Message.builder().role(Message.Role.ASSISTANT).content("[\"queryWeather\",\"sendMail\",\"googleSearch\"]").build(); + Message userMessage1 = Message.builder().role(Message.Role.USER).content("你是谁").build(); + Message assistantMessage1 = Message.builder().role(Message.Role.ASSISTANT).content("[\"googleSearch\"]").build(); + roleList.add(userMessage); + roleList.add(assistantMessage); + roleList.add(userMessage1); + roleList.add(assistantMessage1); completionRequest.setMessages(roleList); completionRequest.setPrompt(chatRequest.getPrompt()); completionRequest.setRequestId(chatRequest.getRequestId()); completionRequest.setIsFunction(false); completionRequest.setMaxTokens(4096); - completionRequest.setModel("gpt-4-turbo-preview"); + completionRequest.setModel(gptConfig.getFindFunctionModel()); String result = chat(completionRequest).getResult(); log.info("函数决策结果:{}",result); return JSON.parseArray(result,String.class); diff --git a/src/main/java/com/ai/aigenerate/chat/LinkAiChatService.java b/src/main/java/com/ai/aigenerate/chat/LinkAiChatService.java index 400d074..fa4e1a4 100644 --- a/src/main/java/com/ai/aigenerate/chat/LinkAiChatService.java +++ b/src/main/java/com/ai/aigenerate/chat/LinkAiChatService.java @@ -7,6 +7,7 @@ import com.ai.aigenerate.utils.MdcUtils; import com.unfbx.chatgpt.OpenAiClient; import com.unfbx.chatgpt.OpenAiStreamClient; +import com.unfbx.chatgpt.entity.chat.BaseChatCompletion; import com.unfbx.chatgpt.entity.chat.ChatCompletion; import com.unfbx.chatgpt.entity.chat.ChatCompletionResponse; import com.unfbx.chatgpt.entity.chat.Message; @@ -96,7 +97,7 @@ public ChatResponse chat(LinkAiChatRequest chatRequest){ .temperature(chatRequest.getTemperature() != null?chatRequest.getTemperature():0.2) .topP(chatRequest.getTopP() != null?chatRequest.getTopP():1.0) .n(chatRequest.getN() != null?chatRequest.getN():1) - .model(chatRequest.getModel() != null?chatRequest.getModel() : ChatCompletion.Model.GPT_3_5_TURBO_16K_0613.getName()) + .model(chatRequest.getModel() != null?chatRequest.getModel() : ChatCompletion.Model.GPT_3_5_TURBO.getName()) .build(); OpenAiClient openAiClient = linkAiClientMap.get(chatRequest.getKnowledgeBase()); GptContext gptContext = GptContext.builder() @@ -105,7 +106,6 @@ public ChatResponse chat(LinkAiChatRequest chatRequest){ .openAiClient(openAiClient) .chatCompletion(chatCompletion) .requestId(chatRequest.getRequestId()) - .timeout(120000l) .build(); ContextMap.put(traceId, gptContext); ChatCompletionResponse chatCompletionResponse = openAiClient.chatCompletion(chatCompletion); @@ -185,7 +185,6 @@ public void chatStream(LinkAiChatRequest chatRequest, SseEmitter sseEmitter){ .chatCompletion(chatCompletion) .requestId(chatRequest.getRequestId()) .functionEventSourceListener(eventSourceListener) - .timeout(120000l) .build(); ContextMap.putStreamContext(traceId, gptStreamContext); openAiClient.streamChatCompletion(chatCompletion, eventSourceListener); diff --git a/src/main/java/com/ai/aigenerate/chat/custom/BlackSilkImageFunctionHandler.java b/src/main/java/com/ai/aigenerate/chat/custom/BlackSilkImageFunctionHandler.java new file mode 100644 index 0000000..7921f5a --- /dev/null +++ b/src/main/java/com/ai/aigenerate/chat/custom/BlackSilkImageFunctionHandler.java @@ -0,0 +1,28 @@ +package com.ai.aigenerate.chat.custom; + +import com.ai.aigenerate.chat.AbstractGptFunctionHandler; +import com.ai.aigenerate.chat.tool.MoyuService; +import com.unfbx.chatgpt.entity.chat.Functions; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class BlackSilkImageFunctionHandler extends AbstractGptFunctionHandler { + + @Autowired + private MoyuService moyuService; + + @Override + public String doHandle(String paramJson) { + return "{\"图片的png链接\":\"" + moyuService.getImage() + "\"}"; + } + + @Override + public Functions getFunction() { + Functions functions = Functions.builder() + .name("getHsImage") + .description("随机获取一张黑丝的图片") + .build(); + return functions; + } +} \ No newline at end of file diff --git a/src/main/java/com/ai/aigenerate/config/GptConfig.java b/src/main/java/com/ai/aigenerate/config/GptConfig.java index 2a81ffa..dcb484e 100644 --- a/src/main/java/com/ai/aigenerate/config/GptConfig.java +++ b/src/main/java/com/ai/aigenerate/config/GptConfig.java @@ -21,7 +21,7 @@ public class GptConfig { @Value("${chatgpt.api.key}") private List chatgptApiKey; - @Value("${linkai.api.key.map}") + @Value("${linkai.api.key.map:}") private String linkAiApiKeyMap; @Value("${system.prompt:}") @@ -39,9 +39,44 @@ public class GptConfig { @Value("${voice.prompt.system:请使用中文交流,回答要求尽可能简短,不能超过100个字}") private String voicePromptSystem; + @Value("${autugpt.function:gpt-3.5-turbo-0125}") + private String findFunctionModel; + + + @Value("${autoGpt.strategy.prompt:你现在是一个函数决策工具,这是我的要求\\n\n" + + "1、请根据函数描述返回需要使用的函数\\n\" \n" + + "2、必须用json返回结果,例如[\\\"queryWeather\\\",\\\"sendMail\\\"],不要输出额外的内容,没有命中就返回空数组\\n\n" + + "3、这是所有的函数定义:\\n\n" + + "```\\n\n" + + "1、函数名:getAiNews;触发条件:当涉及当天人工智能技术的资讯信息时使用\\n\n" + + "2、函数名:analyzeLink;触发条件:当涉及到解析链接内容时触发,或者需要获取实时资讯信息时配合googleSearch触发\\n\n" + + "3、函数名:baiduBaikeSearch;触发条件:当涉及到进行百度百科搜索时触发\\n\n" + + "4、函数名:baiduSearch;触发条件:通过百度进行搜索,只有在提及使用百度进行搜索时才会触发,否则默认使用googleSearch函数\\n\n" + + "5、函数名:getCurrentTime;触发条件:当需要获取当前最新时间时触发该函数\\n\n" + + "6、函数名:googleSearch;触发条件:当需要获取实时信息或资讯有关的问题会使用此夹不是函数去谷歌上搜索,一般还会配合analyzeLink读取链接里面的内容\\n\n" + + "7、函数名:getCrazyKfc;触发条件:当被要求获取疯狂星期四的文案时触发\\n\n" + + "8、函数名:sendMail;触发条件:当需要给指定邮箱发送邮件时触发\\n\n" + + "9、函数名:createImage;触发条件:当被要求创作绘画一张图片时触发\\n\n" + + "10、函数名:getMoyuPaper;触发条件:当获取摸鱼日报的图片时触发\\n\n" + + "11、函数名:getNews;触发条件:当需要获取新闻信息时触发,但不包括人工智能技术的资讯\\n\n" + + "12、函数名:getNewsPicture;触发条件:当需要获取新闻早报图片时触发\\n\n" + + "13、函数名:queryWeather;触发条件:当需要获取指定地区最新天气时触发\\n\n" + + "14、函数名:weiboHotSearch;触发条件:获取微博热搜数据,必须提及微博热搜才进行调用\\n\n" + + "15、函数名:getHsImage;触发条件:随机获取一张黑丝图片时调用\\n\n" + + "```}" + + "4、返回json中默认必须携带googleSearch") + + private String strategyPrompt; + @Value("${chat.function.timeout:180000}") private Long chatFunctionTimeout; + @Value("${linkai.knowledge.database:test}") + private String knowledgeBase; + + @Value("${linkai.knowledge.database.switch:false}") + private Boolean knowledgeSwitch; + public Map getLinkAiApiKeyMap(){ Map map = new HashMap<>(); String[] split = linkAiApiKeyMap.split(","); diff --git a/src/main/java/com/ai/aigenerate/constant/PromptContent.java b/src/main/java/com/ai/aigenerate/constant/PromptContent.java index 03daa79..e966551 100644 --- a/src/main/java/com/ai/aigenerate/constant/PromptContent.java +++ b/src/main/java/com/ai/aigenerate/constant/PromptContent.java @@ -12,7 +12,7 @@ public class PromptContent { "3、函数名:baiduBaikeSearch;触发条件:当涉及到进行百度百科搜索时触发\n" + "4、函数名:baiduSearch;触发条件:通过百度进行搜索,只有在提及使用百度进行搜索时才会触发,否则默认使用googleSearch函数\n" + "5、函数名:getCurrentTime;触发条件:当需要获取当前最新时间时触发该函数\n" + - "6、函数名:googleSearch;触发条件:当需要使用搜索实时信息或资讯有关的问题会通过意图识别决策到该插件去谷歌上搜索,一般还会配合analyzeLink读取链接里面的内容\n" + + "6、函数名:googleSearch;触发条件:当需要获取实时信息或资讯有关的问题会通过意图识别决策到该插件去谷歌上搜索,一般还会配合analyzeLink读取链接里面的内容\n" + "7、函数名:getCrazyKfc;触发条件:当被要求获取疯狂星期四的文案时触发\n" + "8、函数名:sendMail;触发条件:当需要给指定邮箱发送邮件时触发\n" + "9、函数名:createImage;触发条件:当被要求创作绘画一张图片时触发\n" + @@ -21,6 +21,8 @@ public class PromptContent { "12、函数名:getNewsPicture;触发条件:当需要获取新闻早报图片时触发\n" + "13、函数名:queryWeather;触发条件:当需要获取指定地区最新天气时触发\n" + "14、函数名:weiboHotSearch;触发条件:获取微博热搜数据,必须提及微博热搜才进行调用\n" + + "15、函数名:getHsImage;触发条件:随机获取一张黑丝图片时调用\n" + "```" ; + } diff --git a/src/main/java/com/ai/aigenerate/suno/SongGenerator.java b/src/main/java/com/ai/aigenerate/suno/SongGenerator.java new file mode 100644 index 0000000..f7372a5 --- /dev/null +++ b/src/main/java/com/ai/aigenerate/suno/SongGenerator.java @@ -0,0 +1,274 @@ +package com.ai.aigenerate.suno; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import okhttp3.*; +import org.apache.commons.lang3.StringUtils; +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.Random; +import java.util.concurrent.TimeUnit; +import java.util.regex.Pattern; + +public class SongGenerator { + private static final String GET_SESSION_URL = "https://clerk.suno.ai/v1/client?_clerk_js_version=4.70.5"; + private static final String EXCHANGE_TOKEN_URL = "https://clerk.suno.ai/v1/client/sessions/%s/tokens/api?_clerk_js_version=4.70.0"; + private static final String BASE_URL = "https://studio-api.suno.ai"; + private static final String BROWSER_VERSION = "edge101"; + private static final String[] MUSIC_GENRE_LIST = { + "African", "Asian", "South and southeast Asian", "Avant-garde", "Blues", + "Caribbean and Caribbean-influenced", "Comedy", "Country", "Easy listening", + "Electronic", "Folk", "Hip hop", "Jazz", "Latin", "Pop", "R&B and soul", "Rock" + }; + + private final OkHttpClient client; + private final Gson gson; + private final String cookie; + private String authToken; + private String sid; + + public SongGenerator(String cookie) throws IOException { + this.cookie = cookie; + this.client = new OkHttpClient.Builder() + .connectTimeout(10, TimeUnit.SECONDS) + .writeTimeout(10, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .build(); + this.gson = new Gson(); + init(); + } + + private void init() throws IOException { + this.sid = getSessionId(); + this.authToken = getAuthToken(); + } + + private String getSessionId() throws IOException { + Request request = new Request.Builder() + .url(GET_SESSION_URL) + .header("User-Agent", getRandomUserAgent()) + .header("Cookie", this.cookie) + .build(); + + try (Response response = client.newCall(request).execute()) { + if (!response.isSuccessful()) throw new IOException("Unexpected code " + response); + + JsonObject jsonObject = gson.fromJson(response.body().string(), JsonObject.class); + JsonObject r = jsonObject.getAsJsonObject("response"); + if (r == null) throw new IOException("Failed to get session id"); + + return r.get("last_active_session_id").getAsString(); + } + } + + private String getAuthToken() throws IOException { + RequestBody body = RequestBody.create(null, new byte[0]); + String url = String.format(EXCHANGE_TOKEN_URL, this.sid); + Request request = new Request.Builder() + .url(url) + .post(body) + .header("User-Agent", getRandomUserAgent()) + .header("Cookie", this.cookie) + .build(); + + try (Response response = client.newCall(request).execute()) { + if (!response.isSuccessful()) throw new IOException("Unexpected code " + response); + + JsonObject jsonObject = gson.fromJson(response.body().string(), JsonObject.class); + return jsonObject.get("jwt").getAsString(); + } + } + + public int getLimitLeft() throws IOException { + Request request = new Request.Builder() + .url(BASE_URL + "/api/billing/info/") + .header("User-Agent", getRandomUserAgent()) + .header("Authorization", "Bearer " + this.authToken) + .build(); + + try (Response response = client.newCall(request).execute()) { + if (!response.isSuccessful()) throw new IOException("Unexpected code " + response); + + JsonObject jsonObject = gson.fromJson(response.body().string(), JsonObject.class); + return jsonObject.get("total_credits_left").getAsInt() / 10; + } + } + + private void parseLyrics(JsonObject data,Map songInfoDict) { + String songName = data.has("title") ? data.get("title").getAsString() : ""; + JsonObject mt = data.getAsJsonObject("metadata"); + if (mt == null) return; + + String lyrics = mt.get("prompt").getAsString().replaceAll("\\[.*?\\]", ""); + songInfoDict.put("song_name", songName); + songInfoDict.put("lyric", lyrics); + } + + private boolean fetchSongsMetadata(String id1, String id2) throws IOException, InterruptedException { + String url = BASE_URL + "/api/feed/?ids=" + id1 + "%2C" + id2; + Request request = new Request.Builder() + .url(url) + .header("User-Agent", getRandomUserAgent()) + .header("Authorization", "Bearer " + this.authToken) + .build(); + + try (Response response = client.newCall(request).execute()) { + if (!response.isSuccessful()) throw new IOException("Unexpected code " + response); + + String responseBody = response.body().string(); + JsonArray jsonArray = gson.fromJson(responseBody, JsonArray.class); + for (JsonElement element : jsonArray) { + JsonObject jsonObject = element.getAsJsonObject(); + // 处理每个 JsonObject + Map songInfoDict = new HashMap<>(); + if (jsonObject.has("detail") && jsonObject.get("detail").getAsString().equals("Unauthorized")) { + parseLyrics(jsonObject,songInfoDict); + songInfoDict.put("song_url", "https://audiopipe.suno.ai/?item_id=" + id1); + System.out.println("Token expired, will sleep 30 seconds and try to download"); + Thread.sleep(30000); + return true; + } + + if (jsonObject.has("audio_url") && StringUtils.isNotBlank(jsonObject.get("audio_url").getAsString())) { + parseLyrics(jsonObject,songInfoDict); + songInfoDict.put("song_url", jsonObject.get("audio_url").getAsString()); + return true; + } + } + } + + System.out.println("Will sleep 30s and get the music url"); + //Thread.sleep(15000); +// System.out.println("https://audiopipe.suno.ai/?item_id=" + id1); +// System.out.println("https://audiopipe.suno.ai/?item_id=" + id2); + return false; + } + + public void getSongs(String prompt, String tags, String title, boolean isCustom) throws IOException, InterruptedException { + String url = BASE_URL + "/api/generate/v2/"; + JsonObject payload = new JsonObject(); + payload.addProperty("mv", "chirp-v3-0"); + payload.addProperty("make_instrumental", false); + + if (isCustom) { + payload.addProperty("prompt", prompt); + payload.addProperty("gpt_description_prompt", ""); + payload.addProperty("title", title); + if (tags == null || tags.isEmpty()) { + payload.addProperty("tags", getRandomMusicGenre()); + } else { + payload.addProperty("tags", tags); + } + } else { + payload.addProperty("gpt_description_prompt", prompt); + payload.addProperty("prompt", ""); + } + + RequestBody body = RequestBody.create(MediaType.parse("application/json"), gson.toJson(payload)); + Request request = new Request.Builder() + .url(url) + .post(body) + .header("User-Agent", getRandomUserAgent()) + .header("Authorization", "Bearer " + this.authToken) + .build(); + + String responseBody; + try (Response response = client.newCall(request).execute()) { + if (!response.isSuccessful()) throw new IOException("Unexpected code " + response); + responseBody = response.body().string(); + } + + JsonObject responseObject = gson.fromJson(responseBody, JsonObject.class); + String id1 = responseObject.getAsJsonArray("clips").get(0).getAsJsonObject().get("id").getAsString(); + String id2 = responseObject.getAsJsonArray("clips").get(1).getAsJsonObject().get("id").getAsString(); + + long startWait = System.currentTimeMillis(); + System.out.print("Waiting for results..."); + int sleepTime = 10; + + while (true) { + if (System.currentTimeMillis() - startWait > 600000) { + System.out.println("https://audiopipe.suno.ai/?item_id=" + id1); + System.out.println("https://audiopipe.suno.ai/?item_id=" + id2); + return; + } + + if (fetchSongsMetadata(id1, id2)) break; + + if (sleepTime > 2) { + Thread.sleep(sleepTime * 1000); + sleepTime--; + } else { + Thread.sleep(2000); + } + System.out.print("."); + } + } + +// public void saveSongs(String prompt, String outputDir, String tags, String title, boolean isCustom) throws IOException, InterruptedException { +// getSongs(prompt, tags, title, isCustom); +// +// String songName = this.songInfoDict.getOrDefault("song_name", "Untitled"); +// String lyric = this.songInfoDict.get("lyric"); +// String songUrl = this.songInfoDict.get("song_url"); +// +// File dir = new File(outputDir); +// if (!dir.exists()) dir.mkdirs(); +// +// int index = 0; +// File outputFile; +// do { +// outputFile = new File(dir, "suno_" + index + ".mp3"); +// index++; +// } while (outputFile.exists()); +// +// Request request = new Request.Builder() +// .url(songUrl) +// .build(); +// +// try (Response response = client.newCall(request).execute()) { +// if (!response.isSuccessful()) throw new IOException("Unexpected code " + response); +// +// try (FileOutputStream fos = new FileOutputStream(outputFile)) { +// fos.write(response.body().bytes()); +// } +// } +// +// File lyricsFile = new File(dir, songName.replace(" ", "_") + ".lrc"); +// try (FileOutputStream fos = new FileOutputStream(lyricsFile)) { +// fos.write((songName + "\n\n" + lyric).getBytes()); +// } +// } + + @NotNull + private String getRandomUserAgent() { + return "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36 Edg/105.0.1343.27"; + } + + private String getRandomMusicGenre() { + int idx = new Random().nextInt(MUSIC_GENRE_LIST.length); + return MUSIC_GENRE_LIST[idx]; + } + + public static void main(String[] args) throws IOException { + String cookie = "__client"; + + SongGenerator songGenerator = new SongGenerator(cookie); + try { + int limitLeft = songGenerator.getLimitLeft(); + System.out.printf("%d times left\n", limitLeft); + + String prompt = "春风十里不如你"; + songGenerator.getSongs(prompt, null, "春风十里", false); + } catch (IOException | InterruptedException e) { + e.printStackTrace(); + } + } +} diff --git a/src/main/java/com/ai/aigenerate/suno/SunoSongGenerator.java b/src/main/java/com/ai/aigenerate/suno/SunoSongGenerator.java new file mode 100644 index 0000000..0163d44 --- /dev/null +++ b/src/main/java/com/ai/aigenerate/suno/SunoSongGenerator.java @@ -0,0 +1,41 @@ +package com.ai.aigenerate.suno; + +import java.util.Map; + +public interface SunoSongGenerator { + + /** + * 初始化方法,使用给定的身份验证Cookie创建一个SongGenerator实例 + * @param authCookie 包含身份验证信息的Cookie字符串 + */ + void init(String authCookie); + + /** + * 获取当前账号剩余的歌曲生成次数 + * @return 剩余的歌曲生成次数 + */ + int getLimitLeft(); + + /** + * 根据提示生成歌曲信息 + * @param prompt 用于生成歌曲的文本提示 + * @return 包含生成的歌曲信息的Map,包括歌名、歌词和歌曲URL + */ + Map generateSong(String prompt); + + /** + * 使用自定义参数生成歌曲信息 + * @param prompt 用于生成歌曲的文本提示 + * @param title 指定的歌曲标题,可以为空 + * @param tags 指定的歌曲标签,可以为空 + * @return 包含生成的歌曲信息的Map,包括歌名、歌词和歌曲URL + */ + Map generateCustomSong(String prompt, String title, String tags); + + /** + * 将生成的歌曲保存到指定目录 + * @param songInfo 包含要保存的歌曲信息的Map + * @param outputDir 保存歌曲文件的目录路径 + */ + void saveSong(Map songInfo, String outputDir); +} \ No newline at end of file From ccc1059719e4f3e2c1fada4cf5de04e45c65d775 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E4=B8=80=E5=87=A1?= Date: Thu, 6 Jun 2024 00:23:33 +0800 Subject: [PATCH 12/12] test --- .../java/com/ai/aigenerate/express/AST.java | 143 +++++++++++++++ .../ai/aigenerate/express/Application.java | 14 ++ .../ai/aigenerate/express/DatabaseSchema.java | 25 +++ .../express/EnhancedSemanticAnalyzer.java | 35 ++++ .../java/com/ai/aigenerate/express/Lexer.java | 172 +++++++++++++++++ .../com/ai/aigenerate/express/Parser.java | 173 ++++++++++++++++++ .../ai/aigenerate/express/SQLProcessor.java | 28 +++ .../aigenerate/express/SemanticAnalyzer.java | 41 +++++ .../java/com/ai/aigenerate/express/Token.java | 11 ++ .../com/ai/aigenerate/express/TokenType.java | 5 + 10 files changed, 647 insertions(+) create mode 100644 src/main/java/com/ai/aigenerate/express/AST.java create mode 100644 src/main/java/com/ai/aigenerate/express/Application.java create mode 100644 src/main/java/com/ai/aigenerate/express/DatabaseSchema.java create mode 100644 src/main/java/com/ai/aigenerate/express/EnhancedSemanticAnalyzer.java create mode 100644 src/main/java/com/ai/aigenerate/express/Lexer.java create mode 100644 src/main/java/com/ai/aigenerate/express/Parser.java create mode 100644 src/main/java/com/ai/aigenerate/express/SQLProcessor.java create mode 100644 src/main/java/com/ai/aigenerate/express/SemanticAnalyzer.java create mode 100644 src/main/java/com/ai/aigenerate/express/Token.java create mode 100644 src/main/java/com/ai/aigenerate/express/TokenType.java diff --git a/src/main/java/com/ai/aigenerate/express/AST.java b/src/main/java/com/ai/aigenerate/express/AST.java new file mode 100644 index 0000000..47af7aa --- /dev/null +++ b/src/main/java/com/ai/aigenerate/express/AST.java @@ -0,0 +1,143 @@ +package com.ai.aigenerate.express; + +import java.util.List; + +public abstract class AST { + + public String toSQL() { + return ""; + } +} + +class BinaryOperation extends AST { + AST left; + Token op; + AST right; + + BinaryOperation(AST left, Token op, AST right) { + this.left = left; + this.op = op; + this.right = right; + } + + public String toSQL() { + return left.toSQL() + " " + op.value + " " + right.toSQL(); + } +} + +class Num extends AST { + Token token; + + Num(Token token) { + this.token = token; + } + + @Override + public String toSQL() { + return token.value; + } +} + +class Var extends AST { + Token token; + + Var(Token token) { + this.token = token; + } + + @Override + public String toSQL() { + return token.value; + } +} + +class Select extends AST { + List selectList; // 使用AST的列表来支持多列选择 + AST table; + AST whereClause; + + Select(List selectList, AST table, AST whereClause) { + this.selectList = selectList; + this.table = table; + this.whereClause = whereClause; + } + + @Override + public String toSQL() { + StringBuilder selectSQL = new StringBuilder("SELECT "); + for (int i = 0; i < selectList.size(); i++) { + selectSQL.append(selectList.get(i).toSQL()); + if (i < selectList.size() - 1) { + selectSQL.append(", "); + } + } + selectSQL.append(" FROM ").append(table.toSQL()); + if (whereClause != null) { + selectSQL.append(" WHERE ").append(whereClause.toSQL()); + } + return selectSQL.toString(); + } +} + +class Condition extends AST { + String columnName; + String value; + + Condition(String columnName, String value) { + this.columnName = columnName; + this.value = value; + } +} + +class TableWithPrefix extends AST { + String prefix; + Var table; + + TableWithPrefix(String prefix, Var table) { + this.prefix = prefix; + this.table = table; + } + + @Override + public String toSQL() { + return prefix + table.toSQL(); + } +} + +class InCondition extends AST { + Var variable; + List valueList; + + InCondition(Var variable, List valueList) { + this.variable = variable; + this.valueList = valueList; + } + + @Override + public String toSQL() { + StringBuilder sql = new StringBuilder(variable.toSQL() + " IN ("); + for (int i = 0; i < valueList.size(); i++) { + sql.append(valueList.get(i).toSQL()); + if (i < valueList.size() - 1) { + sql.append(", "); + } + } + sql.append(")"); + return sql.toString(); + } +} + +class Column extends AST { + String name; + + Column(String name) { + this.name = name; + } + + @Override + public String toSQL() { + return name; + } +} + + diff --git a/src/main/java/com/ai/aigenerate/express/Application.java b/src/main/java/com/ai/aigenerate/express/Application.java new file mode 100644 index 0000000..f16173b --- /dev/null +++ b/src/main/java/com/ai/aigenerate/express/Application.java @@ -0,0 +1,14 @@ +package com.ai.aigenerate.express; + +import java.util.Set; + +public class Application { + + public static void main(String[] args) { + String sql = "SELECT a,b FROM test WHERE id = 1 and b > 2"; + DatabaseSchema schema = new DatabaseSchema(); + schema.addTable("test", Set.of("id", "name", "deleted","b","c")); + String result = new SQLProcessor(schema).process(sql); + System.out.println("Parsed SQL AST: " + result); + } +} diff --git a/src/main/java/com/ai/aigenerate/express/DatabaseSchema.java b/src/main/java/com/ai/aigenerate/express/DatabaseSchema.java new file mode 100644 index 0000000..ee74768 --- /dev/null +++ b/src/main/java/com/ai/aigenerate/express/DatabaseSchema.java @@ -0,0 +1,25 @@ +package com.ai.aigenerate.express; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +public class DatabaseSchema { + private Set tableNames = new HashSet<>(); + private Map> tableColumns = new HashMap<>(); + + public void addTable(String tableName, Set columns) { + tableNames.add(tableName); + tableColumns.put(tableName, columns); + } + + public boolean tableExists(String tableName) { + return tableNames.contains(tableName); + } + + public boolean columnExists(String tableName, String columnName) { + Set columns = tableColumns.get(tableName); + return columns != null && columns.contains(columnName); + } +} diff --git a/src/main/java/com/ai/aigenerate/express/EnhancedSemanticAnalyzer.java b/src/main/java/com/ai/aigenerate/express/EnhancedSemanticAnalyzer.java new file mode 100644 index 0000000..0757903 --- /dev/null +++ b/src/main/java/com/ai/aigenerate/express/EnhancedSemanticAnalyzer.java @@ -0,0 +1,35 @@ +package com.ai.aigenerate.express; + +public class EnhancedSemanticAnalyzer extends SemanticAnalyzer{ + public EnhancedSemanticAnalyzer(DatabaseSchema schema) { + super(schema); + } + + public Select enhanceSelect(Select select) { + // 增强功能 1:添加默认的删除条件 + select.whereClause = addDefaultWhereCondition(select.whereClause); + + // 增强功能 2:添加默认的数据库前缀 + select.table = addDefaultDatabasePrefix(select.table); + + return select; + } + + private AST addDefaultWhereCondition(AST whereClause) { + // 创建代表 "deleted = 0" 的新条件 + BinaryOperation deletedCondition = new BinaryOperation( + new Var(new Token(TokenType.IDENTIFIER,"deleted")), + new Token(TokenType.EQUALS, "="), + new Num(new Token(TokenType.NUMBER, "0"))); + // 新增 AND 条件 + return new BinaryOperation(whereClause, new Token(TokenType.AND, "and"), deletedCondition); + } + + private AST addDefaultDatabasePrefix(AST table) { + if (table instanceof Var && !((Var) table).token.value.contains(".")) { + // 添加默认前缀 + return new TableWithPrefix("a.", (Var) table); + } + return table; // 如果已经有前缀,直接返回 + } +} diff --git a/src/main/java/com/ai/aigenerate/express/Lexer.java b/src/main/java/com/ai/aigenerate/express/Lexer.java new file mode 100644 index 0000000..e8b973a --- /dev/null +++ b/src/main/java/com/ai/aigenerate/express/Lexer.java @@ -0,0 +1,172 @@ +package com.ai.aigenerate.express; + +public class Lexer { + private String input; + private int pos; + private char currentChar; + + Lexer(String input) { + this.input = input; + this.pos = 0; + this.currentChar = input.charAt(pos); + } + + private void advance() { + pos++; + currentChar = pos < input.length() ? input.charAt(pos) : (char) -1; + } + + private void skipWhitespace() { + while (currentChar != (char) -1 && Character.isWhitespace(currentChar)) { + advance(); + } + } + + private String identifier() { + StringBuilder result = new StringBuilder(); + while (currentChar != (char) -1 && Character.isLetterOrDigit(currentChar)) { + result.append(currentChar); + advance(); + } + return result.toString(); + } + + private String number() { + StringBuilder result = new StringBuilder(); + while (currentChar != (char) -1 && Character.isDigit(currentChar)) { + result.append(currentChar); + advance(); + } + return result.toString(); + } + + public Token getNextToken() { + while (currentChar != (char) -1) { + if (Character.isWhitespace(currentChar)) { + skipWhitespace(); + continue; + } + + if (Character.isLetter(currentChar)) { + String id = identifier(); + switch (id.toUpperCase()) { + case "SELECT": + return new Token(TokenType.SELECT, id); + case "FROM": + return new Token(TokenType.FROM, id); + case "WHERE": + return new Token(TokenType.WHERE, id); + case "AND": + return new Token(TokenType.AND, id); + default: + return new Token(TokenType.IDENTIFIER, id); + } + } + + if (currentChar == '*') { + advance(); + return new Token(TokenType.STAR, "*"); + } + + if (currentChar == '=') { + advance(); + return new Token(TokenType.EQUALS, "="); + } + + if (Character.isDigit(currentChar)) { + return new Token(TokenType.NUMBER, number()); + } + + if (currentChar == '(') { + advance(); + return new Token(TokenType.LPAREN, "("); + } + + if (currentChar == ')') { + advance(); + return new Token(TokenType.RPAREN, ")"); + } + + if (currentChar == ',') { + advance(); + return new Token(TokenType.COMMA, ","); + } + + if (currentChar == '>') { + advance(); + if (currentChar == '=') { + advance(); + return new Token(TokenType.GREATER_EQUALS, ">="); + } + return new Token(TokenType.GREATER, ">"); + } + + if (currentChar == '<') { + advance(); + if (currentChar == '=') { + advance(); + return new Token(TokenType.LESS_EQUALS, "<="); + } + return new Token(TokenType.LESS, "<"); + } + + if (currentChar == '!') { + advance(); + if (currentChar == '=') { + advance(); + return new Token(TokenType.NOT_EQUALS, "!="); + } + } + + if (currentChar == '&') { + advance(); + if (currentChar == '&') { + advance(); + return new Token(TokenType.AND, "&&"); + } + } + + if (currentChar == '|') { + advance(); + if (currentChar == '|') { + advance(); + return new Token(TokenType.OR, "||"); + } + } + + if (currentChar == 'i') { + advance(); + if (currentChar == 'n') { + advance(); + return new Token(TokenType.IN, "in"); + } + } + + if (currentChar == 'n') { + advance(); + if (currentChar == 'o') { + advance(); + if (currentChar == 't') { + advance(); + if (currentChar == ' ') { + advance(); + if (currentChar == 'i') { + advance(); + if (currentChar == 'n') { + advance(); + return new Token(TokenType.NOT_IN, "not in"); + } + } + } + } + } + } + + // Handle unexpected character + throw new RuntimeException("Unexpected character: " + currentChar); + } + + return new Token(TokenType.EOF, ""); + } +} + diff --git a/src/main/java/com/ai/aigenerate/express/Parser.java b/src/main/java/com/ai/aigenerate/express/Parser.java new file mode 100644 index 0000000..b0768b1 --- /dev/null +++ b/src/main/java/com/ai/aigenerate/express/Parser.java @@ -0,0 +1,173 @@ +package com.ai.aigenerate.express; + +import java.util.ArrayList; +import java.util.List; + +public class Parser { + private Lexer lexer; + private Token currentToken; + + Parser(Lexer lexer) { + this.lexer = lexer; + this.currentToken = lexer.getNextToken(); + } + + private void eat(TokenType type) { + if (currentToken.type == type) { + currentToken = lexer.getNextToken(); + } else { + throw new RuntimeException("Token mismatch!"); + } + } + + private AST whereClause() { + eat(TokenType.WHERE); + // 调用expression()来处理可能存在的逻辑操作符,如 AND/OR + AST result = expression(); + return result; + } + + private AST table() { + eat(TokenType.FROM); + Var table = (Var) term(); + return table; + } + + private List selectList() { + eat(TokenType.SELECT); + // 解析可能由逗号分隔的列名列表 + List columns = new ArrayList<>(); + columns.add(factor()); // 假设factor()能够处理列名 + + while (currentToken.type == TokenType.COMMA) { + eat(TokenType.COMMA); + columns.add(factor()); // 再次假设factor()能够处理列名 + } + + return columns; + } + + public Select parse() { + // 现在selectList()返回的是列名列表 + List selectList = selectList(); + AST table = table(); + AST whereClause = null; + if (currentToken.type == TokenType.WHERE) { + whereClause = whereClause(); + } + return new Select(selectList, table, whereClause); + } + + private AST comparison() { + // 解析比较表达式(例如 a > 3) + AST left = term(); + Token op = currentToken; // 假设这里获取到的是比较操作符 + eat(currentToken.type); // 消耗操作符 + AST right = term(); + return new BinaryOperation(left, op, right); + } + + private AST expression() { + // 解析表达式中的第一个项(可能是比较或另一个表达式) + AST result = term(); + + // 递归处理表达式中的逻辑连接符,如 AND/OR + while (currentToken.type == TokenType.AND || currentToken.type == TokenType.OR) { + Token token = currentToken; + if (token.type == TokenType.AND) { + eat(TokenType.AND); + } else if (token.type == TokenType.OR) { + eat(TokenType.OR); + } + + // 递归地构建二元操作节点 + result = new BinaryOperation(result, token, term()); + } + + return result; + } + + private AST term() { + // 解析比较操作符的左侧项 + AST node = factor(); + + // 处理比较操作符(例如:=, !=, >, <, >=, <=) + while (currentToken.type == TokenType.EQUALS || currentToken.type == TokenType.NOT_EQUALS || + currentToken.type == TokenType.GREATER || currentToken.type == TokenType.LESS || + currentToken.type == TokenType.GREATER_EQUALS || currentToken.type == TokenType.LESS_EQUALS) { + Token token = currentToken; + // 对于每种可能的比较操作符,调用相应的eat方法 + switch (token.type) { + case EQUALS: + eat(TokenType.EQUALS); + break; + case NOT_EQUALS: + eat(TokenType.NOT_EQUALS); + break; + case GREATER: + eat(TokenType.GREATER); + break; + case LESS: + eat(TokenType.LESS); + break; + case GREATER_EQUALS: + eat(TokenType.GREATER_EQUALS); + break; + case LESS_EQUALS: + eat(TokenType.LESS_EQUALS); + break; + } + // 构建二元操作节点,节点类型为比较操作符,左侧为先前解析的项,右侧为新解析的项 + node = new BinaryOperation(node, token, factor()); + } + + return node; + } + + private AST factor() { + // 这里可以进一步处理括号内的表达式或其他因子,如NOT操作、IN操作等 + // 例如,处理一个数字或者变量 + Token token = currentToken; + if (token.type == TokenType.NUMBER) { + eat(TokenType.NUMBER); + return new Num(token); + } else if (token.type == TokenType.IDENTIFIER) { + eat(TokenType.IDENTIFIER); + // 如果后面跟的是IN关键字,就处理IN表达式 + if (currentToken.type == TokenType.IN) { + return inCondition(); + } else { + return new Var(token); + } + } else if (token.type == TokenType.STAR){ + eat(TokenType.STAR); + return new Var(token); + } + + // 如果遇到左括号,则解析括号内的表达式 + if (token.type == TokenType.LPAREN) { + eat(TokenType.LPAREN); + AST node = expression(); + eat(TokenType.RPAREN); + return node; + } + + throw new RuntimeException("Unexpected token: " + token.value); + } + + + private AST inCondition() { + // 解析IN条件(例如 a IN (1, 2)) + Var variable = (Var) term(); + eat(TokenType.IN); // 消耗IN关键字 + eat(TokenType.LPAREN); // 消耗左括号 + List valueList = new ArrayList<>(); + valueList.add(term()); + while (currentToken.type == TokenType.COMMA) { + eat(TokenType.COMMA); // 消耗逗号 + valueList.add(term()); + } + eat(TokenType.RPAREN); // 消耗右括号 + return new InCondition(variable, valueList); + } +} diff --git a/src/main/java/com/ai/aigenerate/express/SQLProcessor.java b/src/main/java/com/ai/aigenerate/express/SQLProcessor.java new file mode 100644 index 0000000..3673e33 --- /dev/null +++ b/src/main/java/com/ai/aigenerate/express/SQLProcessor.java @@ -0,0 +1,28 @@ +package com.ai.aigenerate.express; + +public class SQLProcessor { + + SQLProcessor(DatabaseSchema schema) { + this.schema = schema; + } + + private DatabaseSchema schema; + + public String process(String sql) { + // 1. 词法分析和语法分析 + Lexer lexer = new Lexer(sql); + Parser parser = new Parser(lexer); + Select select = parser.parse(); // 假设解析结果是Select类型的AST节点 + + // 2. 语义分析 + EnhancedSemanticAnalyzer semanticAnalyzer = new EnhancedSemanticAnalyzer(schema); + semanticAnalyzer.analyze(select); + + // 3. 增强功能 + // 添加默认的删除条件和默认的数据库前缀 + select = semanticAnalyzer.enhanceSelect(select); + + // 4. 输出增强后的SQL + return select.toSQL(); + } +} diff --git a/src/main/java/com/ai/aigenerate/express/SemanticAnalyzer.java b/src/main/java/com/ai/aigenerate/express/SemanticAnalyzer.java new file mode 100644 index 0000000..bdf8a01 --- /dev/null +++ b/src/main/java/com/ai/aigenerate/express/SemanticAnalyzer.java @@ -0,0 +1,41 @@ +package com.ai.aigenerate.express; + +public class SemanticAnalyzer { + + private DatabaseSchema schema; + + private Select currentSelect; // Instance variable to hold the current select context + + public SemanticAnalyzer(DatabaseSchema schema) { + this.schema = schema; + } + + public void analyze(AST node) throws RuntimeException { + if (node instanceof Select) { + Select select = (Select) node; + currentSelect = select; // Set the current select context + analyze(select.table); + analyze(select.whereClause); + } else if (node instanceof Var) { + Var var = (Var) node; + if (!schema.tableExists(var.token.value)) { + throw new RuntimeException("Table not found: " + var.token.value); + } + } else if (node instanceof BinaryOperation) { + BinaryOperation binOp = (BinaryOperation) node; + if (binOp.left instanceof Var && binOp.right instanceof Num) { + Var var = (Var) binOp.left; + if (!schema.columnExists(((Var) currentSelect.table).token.value, var.token.value)) { + throw new RuntimeException("Column not found: " + var.token.value); + } + } else { + if (binOp.left instanceof BinaryOperation) + analyze(binOp.left); + if (binOp.right instanceof BinaryOperation) + analyze(binOp.right); + } + } else { + throw new RuntimeException("Invalid AST Node."); + } + } +} diff --git a/src/main/java/com/ai/aigenerate/express/Token.java b/src/main/java/com/ai/aigenerate/express/Token.java new file mode 100644 index 0000000..76370a1 --- /dev/null +++ b/src/main/java/com/ai/aigenerate/express/Token.java @@ -0,0 +1,11 @@ +package com.ai.aigenerate.express; + +public class Token { + TokenType type; + String value; + + Token(TokenType type, String value) { + this.type = type; + this.value = value; + } +} diff --git a/src/main/java/com/ai/aigenerate/express/TokenType.java b/src/main/java/com/ai/aigenerate/express/TokenType.java new file mode 100644 index 0000000..7d63709 --- /dev/null +++ b/src/main/java/com/ai/aigenerate/express/TokenType.java @@ -0,0 +1,5 @@ +package com.ai.aigenerate.express; + +public enum TokenType { + SELECT, STAR, FROM, WHERE, IDENTIFIER, NUMBER, EQUALS, EOF, AND, IN, LPAREN, RPAREN, COMMA, NOT_IN, NOT_EQUALS ,OR ,GREATER ,LESS ,GREATER_EQUALS ,LESS_EQUALS; +}