From 31c2bb06fc5ac0066f0c9d6d15cd0bd821ad186e Mon Sep 17 00:00:00 2001 From: zhouchengrong Date: Tue, 25 Jul 2023 17:57:37 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9Echat=20Robot=20=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3=E6=9C=8D=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../security/filter/AuthenticationFilter.java | 9 +- .../ai/da/controller/PythonController.java | 48 ++++++--- .../com/ai/da/model/dto/ChatFlushDTO.java | 27 +++++ .../java/com/ai/da/model/dto/ChatSendDTO.java | 34 +++++++ .../com/ai/da/service/ChatRobotService.java | 18 ++++ .../da/service/impl/ChatRobotServiceImpl.java | 96 ++++++++++++++++++ .../resources/application-test.properties | 4 +- target/classes/application-test.properties | 4 +- .../filter/AuthenticationFilter.class | Bin 6834 -> 6897 bytes .../ai/da/controller/PythonController.class | Bin 5277 -> 6482 bytes 10 files changed, 213 insertions(+), 27 deletions(-) create mode 100644 src/main/java/com/ai/da/model/dto/ChatFlushDTO.java create mode 100644 src/main/java/com/ai/da/model/dto/ChatSendDTO.java create mode 100644 src/main/java/com/ai/da/service/ChatRobotService.java create mode 100644 src/main/java/com/ai/da/service/impl/ChatRobotServiceImpl.java diff --git a/src/main/java/com/ai/da/common/security/filter/AuthenticationFilter.java b/src/main/java/com/ai/da/common/security/filter/AuthenticationFilter.java index f026d9fc..eefad390 100644 --- a/src/main/java/com/ai/da/common/security/filter/AuthenticationFilter.java +++ b/src/main/java/com/ai/da/common/security/filter/AuthenticationFilter.java @@ -8,17 +8,12 @@ import com.ai.da.common.utils.LocalCacheUtils; import com.ai.da.common.utils.MultiReadHttpServletRequest; import com.ai.da.common.utils.MultiReadHttpServletResponse; import com.ai.da.model.vo.AuthPrincipalVo; -import lombok.AllArgsConstructor; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Required; import org.springframework.context.annotation.Configuration; import org.springframework.lang.NonNull; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.util.StopWatch; import org.springframework.util.StringUtils; -import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.filter.OncePerRequestFilter; import javax.annotation.Resource; @@ -28,7 +23,6 @@ import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; -import java.sql.Statement; import java.util.Arrays; import java.util.List; @@ -50,7 +44,8 @@ public class AuthenticationFilter extends OncePerRequestFilter { Arrays.asList("/favicon.ico","/doc.html","api/account/login","api/account/preLogin","api/account/sendEmail", "/webjars/","/swagger-resources","/v2/api-docs","api/account/resetPwd", "/api/python/saveGeneratePicture", "/api/python/getLibraryByUserId", - "/api/third/party/addUser","/api/third/party/editUser","/api/element/initDefaultSysFile"); + "/api/third/party/addUser","/api/third/party/editUser","/api/element/initDefaultSysFile", + "/api/python/chatStream","/api/python/flush"); @Override protected void doFilterInternal(HttpServletRequest httpServletRequest, @NonNull HttpServletResponse httpServletResponse, @NonNull FilterChain filterChain) throws ServletException, IOException { diff --git a/src/main/java/com/ai/da/controller/PythonController.java b/src/main/java/com/ai/da/controller/PythonController.java index f9dbd0f4..7a9a0ea3 100644 --- a/src/main/java/com/ai/da/controller/PythonController.java +++ b/src/main/java/com/ai/da/controller/PythonController.java @@ -3,23 +3,23 @@ package com.ai.da.controller; import com.ai.da.common.enums.CollectionLevel1TypeEnum; import com.ai.da.common.response.Response; import com.ai.da.common.utils.CopyUtil; -import com.ai.da.common.utils.MD5Utils; -import com.ai.da.model.dto.CollectionDeleteFileDTO; -import com.ai.da.model.dto.CollectionElementUploadDTO; -import com.ai.da.model.dto.CollectionGeneratePrintDTO; -import com.ai.da.model.dto.CollectionSavePrintDTO; -import com.ai.da.model.vo.*; +import com.ai.da.model.dto.ChatFlushDTO; +import com.ai.da.model.dto.ChatSendDTO; +import com.ai.da.model.vo.PythonLibraryVo; +import com.ai.da.model.vo.SysFileVO; import com.ai.da.python.PythonService; -import com.ai.da.service.*; +import com.ai.da.service.ChatRobotService; +import com.ai.da.service.LibraryService; +import com.ai.da.service.SysFileService; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiParam; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import javax.annotation.Resource; -import javax.validation.Valid; import java.util.Collections; import java.util.List; import java.util.Map; @@ -39,13 +39,16 @@ public class PythonController { @Resource private LibraryService libraryService; + @Resource + private ChatRobotService chatRobotService; + @ApiOperation(value = "python服务保存图片到java服务") @PostMapping("/saveGeneratePicture") public Response upload(@RequestParam("file") MultipartFile file, - @ApiParam("操作类型 generatePrint ->生成印花 " + - "designCollection ->设计collection generateAdvancedDesign ->生成高级design") - @RequestParam(value = "operateType") String operateType) { - return Response.success(pythonService.upload(file,operateType)); + @ApiParam("操作类型 generatePrint ->生成印花 " + + "designCollection ->设计collection generateAdvancedDesign ->生成高级design") + @RequestParam(value = "operateType") String operateType) { + return Response.success(pythonService.upload(file, operateType)); } @ApiOperation(value = "通过文件类型获取系统文件") @@ -56,12 +59,25 @@ public class PythonController { @ApiOperation(value = "通过用户id获取library") @GetMapping("/getLibraryByUserId") - public Response>> getLibraryByUserId(@RequestParam(value = "userId") Long userId) { + public Response>> getLibraryByUserId(@RequestParam(value = "userId") Long userId) { List response = CopyUtil.copyList(libraryService.selectByAccountIdAnd1TypeList(userId, - Collections.singletonList(CollectionLevel1TypeEnum.SKETCH_BOARD.getRealName())),PythonLibraryVo.class); + Collections.singletonList(CollectionLevel1TypeEnum.SKETCH_BOARD.getRealName())), PythonLibraryVo.class); //key转小写 统一 - return Response.success(response.stream().collect(Collectors.groupingBy(v ->v.getLevel2Type().toLowerCase(), - Collectors.mapping(PythonLibraryVo::getUrl,Collectors.toList())))); + return Response.success(response.stream().collect(Collectors.groupingBy(v -> v.getLevel2Type().toLowerCase(), + Collectors.mapping(PythonLibraryVo::getUrl, Collectors.toList())))); + } + + @ApiOperation(value = "发送用户输入消息") + @PostMapping("/chatStream") + public SseEmitter MessageToPythonChatStream(@RequestBody ChatSendDTO chatSendDTO) { + log.info(chatSendDTO.toString()); + return chatRobotService.sendMessageToChatRobot(chatSendDTO); + } + + @ApiOperation(value = "刷新会话缓存") + @PostMapping("/flush") + public Response ChatBufferFlush(@RequestBody ChatFlushDTO chatFlushDTO) { + return Response.success(chatRobotService.chatBufferFlush(chatFlushDTO)); } } diff --git a/src/main/java/com/ai/da/model/dto/ChatFlushDTO.java b/src/main/java/com/ai/da/model/dto/ChatFlushDTO.java new file mode 100644 index 00000000..8f331480 --- /dev/null +++ b/src/main/java/com/ai/da/model/dto/ChatFlushDTO.java @@ -0,0 +1,27 @@ +package com.ai.da.model.dto; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +/** + * @author aida + * @version 1.0 + * @project aida_back + * @description 刷新缓存DTO + * @date 2023/7/25 16:51:16 + */ + +@Data +public class ChatFlushDTO { + @NotNull(message = "userId cannot be empty!") + @ApiModelProperty("用户id") + private String user_id; + + @NotBlank(message = "sessionId cannot be empty!") + @ApiModelProperty("会话ID") + private String session_id; + +} diff --git a/src/main/java/com/ai/da/model/dto/ChatSendDTO.java b/src/main/java/com/ai/da/model/dto/ChatSendDTO.java new file mode 100644 index 00000000..b50f5a0c --- /dev/null +++ b/src/main/java/com/ai/da/model/dto/ChatSendDTO.java @@ -0,0 +1,34 @@ +package com.ai.da.model.dto; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +/** + * @author aida + * @version 1.0 + * @project aida_back + * @description 机器人对话DTO + * @date 2023/7/25 16:35:55 + */ + +@Data +@ApiModel("chatRobot 对话") +public class ChatSendDTO { + + @NotNull(message = "userId cannot be empty!") + @ApiModelProperty("用户id") + private String user_id; + + @NotBlank(message = "sessionId cannot be empty!") + @ApiModelProperty("会话ID") + private String session_id; + + @NotBlank(message = "Please input the message !") + @ApiModelProperty("消息") + private String message; + +} diff --git a/src/main/java/com/ai/da/service/ChatRobotService.java b/src/main/java/com/ai/da/service/ChatRobotService.java new file mode 100644 index 00000000..93dacb9a --- /dev/null +++ b/src/main/java/com/ai/da/service/ChatRobotService.java @@ -0,0 +1,18 @@ +package com.ai.da.service; + +import com.ai.da.model.dto.ChatFlushDTO; +import com.ai.da.model.dto.ChatSendDTO; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +/** + * @author aida + * @version 1.0 + * @project aida_back + * @description 对话机器人服务接口 + * @date 2023/7/25 16:42:18 + */ +public interface ChatRobotService { + SseEmitter sendMessageToChatRobot(ChatSendDTO chatSendDTO); + + String chatBufferFlush(ChatFlushDTO chatFlushDTO); +} diff --git a/src/main/java/com/ai/da/service/impl/ChatRobotServiceImpl.java b/src/main/java/com/ai/da/service/impl/ChatRobotServiceImpl.java new file mode 100644 index 00000000..bef77381 --- /dev/null +++ b/src/main/java/com/ai/da/service/impl/ChatRobotServiceImpl.java @@ -0,0 +1,96 @@ + + + +package com.ai.da.service.impl; + +import com.ai.da.model.dto.ChatFlushDTO; +import com.ai.da.model.dto.ChatSendDTO; +import com.ai.da.service.ChatRobotService; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** + * @author aida + * @version 1.0 + * @project ChatRobot + * @description 请求python Chat Stream 接口服务实现类 + * @date 2023/7/10 10:41:45 + */ +@Slf4j +@Service +public class ChatRobotServiceImpl implements ChatRobotService { + + // @Value("") + String chatStreamUrl = "http://127.0.0.1:6789/api/chat_stream"; + String chatBufferFlushUrl = "http://127.0.0.1:6789/api/chat_flush"; + + RestTemplate restTemplate = new RestTemplate(); + + + Gson gson = new GsonBuilder().create(); + private final ExecutorService executorService = Executors.newSingleThreadExecutor(); + Integer timeout = 9999999; + + @Override + public SseEmitter sendMessageToChatRobot(ChatSendDTO chatSendDTO) { + SseEmitter emitter = new SseEmitter(); + String requestBody = gson.toJson(chatSendDTO); + executorService.execute(() -> { + try { + // 这里根据你的业务逻辑,从服务获取数据 + // 示例:从服务获取数据并逐条发送给客户端 + URL urlObj = new URL(chatStreamUrl); + HttpURLConnection connection = (HttpURLConnection) urlObj.openConnection(); + connection.setConnectTimeout(timeout); + connection.setReadTimeout(timeout); + connection.setRequestMethod("POST"); + connection.setRequestProperty("Content-Type", "application/json"); + connection.setDoOutput(true); + + OutputStream outputStream = connection.getOutputStream(); + outputStream.write(requestBody.getBytes()); + outputStream.flush(); + outputStream.close(); + + int responseCode = connection.getResponseCode(); + + if (responseCode == HttpURLConnection.HTTP_OK) { + BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream())); + String line; + while ((line = reader.readLine()) != null) { + log.info(line); + emitter.send(line); + } + reader.close(); + } else { + System.out.println("Request failed with status code: " + responseCode); + } + + connection.disconnect(); + emitter.complete(); + } catch (Exception e) { + emitter.completeWithError(e); + } + }); + return emitter; + } + + @Override + public String chatBufferFlush(ChatFlushDTO chatFlushDTO) { + log.info(chatBufferFlushUrl); + return String.valueOf(restTemplate.postForEntity(chatBufferFlushUrl, chatFlushDTO, String.class)); + } + +} diff --git a/src/main/resources/application-test.properties b/src/main/resources/application-test.properties index 3292f56f..a49d4252 100644 --- a/src/main/resources/application-test.properties +++ b/src/main/resources/application-test.properties @@ -3,8 +3,8 @@ server.port=5567 #datasource spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.url=jdbc:mysql://18.167.251.121:33006/aida?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true -spring.datasource.username=nacos -spring.datasource.password=nacos +spring.datasource.username=root +spring.datasource.password=root #security spring.security.jwtSecret=JWTSECRET diff --git a/target/classes/application-test.properties b/target/classes/application-test.properties index 3292f56f..a49d4252 100644 --- a/target/classes/application-test.properties +++ b/target/classes/application-test.properties @@ -3,8 +3,8 @@ server.port=5567 #datasource spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.url=jdbc:mysql://18.167.251.121:33006/aida?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true -spring.datasource.username=nacos -spring.datasource.password=nacos +spring.datasource.username=root +spring.datasource.password=root #security spring.security.jwtSecret=JWTSECRET diff --git a/target/classes/com/ai/da/common/security/filter/AuthenticationFilter.class b/target/classes/com/ai/da/common/security/filter/AuthenticationFilter.class index cbaf17acf0e9c4c38093305b178c449391848f30..c7f70b32332fb976b5fe6b34a37a46a2d84e7565 100644 GIT binary patch delta 1870 zcmZ8hiCo+H-z9j~9{&wbhT8vCWw67NRk{qVTGZU^|T2X>xa2cn!M^ z((7jWh8J)8u?KHicpL91yz9k#7P9fa34UPVLwsb+$6kD5VJ|*4W}k)6@VO!Rg@rG% z-;1xz|BlsT(0p4{?xbw za}`c#{Do8YF;DM)r!hg}Zxi_kX9R(8B+^h<6*gOj>RX!{HO}Ik#$AXKT;sgJ9;Nzw z$`KV5*rQe7XlsP%)f(j@T3nbQNU0Cc3WplPjWwYTeoErBxDD7ij8_t`#UqM%B~49j zt!*vg$e8N3+NNo(i5~F=#K)Hv5WnV@os=YKNtA$=BuQ41q9rJ)T-uJ3XJE?OTIw2W zw4_P8md-R7i|dti(UL)F(p9=?jKVc`r8gs?yOti(Q%R!%g1(?6tGlOAOOad=Yqg5yNbKU+yS_Vsr#{F1g&rB(jQhQU% zQTK2`wk^SQduy;TI+9KAMusT_M=m26g6lZ;@IA)yv26eIWtan_?*=Dc=H#-Y=&7J5 z#JBJrpSu&X%}JR^Yc>ROF^N{UCm?Y$rf}-UR8-Pd*v+Yjq9*w!q|*+77XuN*AauuI zWMT-iP>dXu5PcXXq7>6G0*(AQ%%+07sNp_xT}F1xsbd9otfGe1^sS+99an82=a*3K zkf?-@IUd|dG%pbr;bz=|_$YzqBaB-a%0d)X1W_cj1?^>T*Uks6`w% z+K4*TQ_OAj=EHa1s8DE7n4!?9(4-JiXl|bZX~|h56hlL&#w%j3?qw z@^v|UHC$(qM}{Q|^BnYeVWQ~_-ga(LVfW~~(K(^9NTGiUKde*Hg*D7%6?0j(0+yyf zi&pHMO*QLYi!uUCLIZWo;Au7S-^|Tha1YwBghE%6zG;iW6kg2iyBTpaJH&&i=hqrM z#|y+)VL{ZXegYO_p~KujF2cQ}XKLOB@n^uB899q=QCNHiJvd1^jbx&kUK4hSDJ-Qe z00Ne|AEtJ64dC0HrYARV2mG5ka3O3lDZ&(TA8`OrHgjAu>#cQvZvbn;W)?71_*o-|{a^e^Yh< delta 1806 zcmZ9Mc~n(Z6vn^(c<-IdeMfjQh@uus7-B{UWZ;mHLz57MqW6oE~ zMoQ7F)YO+lS~gEMo8^E0(Mrt*%|MrS2z*LmU&TK4=enja8lt%lCc9UL*XZZJz(1S zvYPse>Z&@0pA~*_j(VHqlr!Jg=sm6QtJB55r+qO>xLo55e$$wS=?bSbe#akz&d$oH z&N&)?qD12_6aE`#1;J2FO=ZRSkl8nLQeAbG#yOnVn2A{=(D(<<&JfkpUyNCTY^O+d z57*TYp-iI~7c?%SL=ZnIG&Pi28LBGJY=Km`qySF!k4T_H+e^8VbOQMz7S^}cAw2~Mlv08%CnoBRMlo*+k`r3-BaxHNZuO)#7 zcVe)TL@jMtnj}eEje!^>NIbS`#j%y!k8hZBym90A`!EOs`>=Sn~4X#7cUfgsH(N=S0POUMZi;05*~*_6hS$IwD>JI8*0 z?{Ix=#Q%KB<_PE;>c)%QTwWA|Ficp9VxCImV$g4;{jr_AoznEmp z$+n6$tRa5`k8vG+jr6T2??$fNN>T$c7UqTeF@__H3JY*Ib$ww9&BML8kD*kA5yChq zrf;L5fi#Z=Inh3HGjhovw{VX{J{!UGCb3e}gV zK%2PKK?*heO*xBbk3wy4uJd!xI@IGqj$1`h4io|Vc#l%Jn7Od?Y55zmlg_g=*9gehTKY z$fYcEE#b|H^kv}%%%07*dzqbyIYjd@($E6lfVpAH2!$8%oD6w3<>uoN;+dYegRdDG ztlw}H$c^Aci+yJxfq<-s791i_oIwB<=Gq^$WH7nidaxe;^<#yEM&MPv4&e*}r< zjfkrg=jNqzZ5FR(54JLko<|w?U|GwvT+Z}9^w|*UB{xfN*1VZ>^QvBC7hb~4*zzyJ C!b#Nt diff --git a/target/classes/com/ai/da/controller/PythonController.class b/target/classes/com/ai/da/controller/PythonController.class index da03c859da21ebe2fc4587ea236b5dab1d85b721..534ea363d7e4d9d12d665267684547d6a3e8fce0 100644 GIT binary patch delta 2265 zcma)6X>e0j6#ib)_N6bkN$iZ27HtcHc7eW9D2qjbvItmgr7VJ=hUO&=CV7~=R=_2I zqIH1+4-wI-s3hTx`P!C-9_*rzC8~(;}W(v+A#6S52*3o2JfJwQ40?@2^n=^slHz?s<}148j`Cc%7ma_)0A+T06S+HyDgyaIUf+v zyQ6W<@uka-uG`Yt(0Z(0?^v<9v!$VP*>(bx4K!PfZz+)}9#yV-7Slsco=JI~*QUm#W-f%A)wz`DaK>yvAO>TdVpRein>>HWD7{|XlU(RzvD>T z#$(%?j<)F?+cpSDk-eO0^H#A=m{A0op+5$Q*uqeXXGJ{6@I1CMxb!;FYVjIH? z*v|bs7z$7*Vkg5cG&AhR9uX}Jt=KDKAH#mUNV({TQu;+jv@yJdb`dW#ynSNL5BBnP{aoeAL1j1ez-`) zA%?^J;g11P3b^hQUPKEm;5G^bn3XVjtqo2HU<{UIM^q=tQRB;XyoK(0`I zJ|!$5$sh8n0?yB?Sm3UA%K>-DC!67A`7^0D-SDT43LmIyW1@ORNcdeN!zE@+9LbHV z<`lUNJGUnEKg3@8AZOHdgrNh+8N9sMzffh`oTb7E{iO3`(r*F==$l*_`oFG{Xb$}g z0m;bX#BrLBy3@mFqcaJQ)?z~9yFhz#K`VrvWJE0( zOdEd*#H55)W=3EL{YirCQVb)0{PvZw(ho>0*bPhJz!q5dB54OX541}&= zI7S%2X&8x1=|&mdOSQ}+Kga`&GH@<463?LTms9lU2*zM6H?D;cK{>`zP_Bg>mPK^6T()kKZA_B$wqV(K~?)*k3Lw2(XbIg2kn_eE}!VhwDTgG`ImL&UxrFl5l$wx`D#of+SL@rOKp0llZRI%S)jmp zF=q5+HIuW7#;P&Ps0QN=ja8$P%xf?kNk+FS3DQDX8M}~C*n-~skZA#RE~{`K`dHB2 zH^~T_Ol+nY+m7$rxH z#swCfK|xgsEF4`d9B^YkXAvb(DwCTwvw1Z=*?1DzD5tP-NqiuQ1($OPp>f&U3R-Ys z3$nW_Vvd8^Npl=*sD++HMs+UuNuOU{L(|Ga6x4-Daj zMNhs=Bfuhy#VoN{%2zu5I+bN^ma{@5-)Q99G`?e{B}=u%DptFxVU5XJi*-cYtY?Ey zD7X4B*lMwz9eRoCWv83(*=6Bjx5Xazni|?*CJz(-f{k7Mz|BS+vSiTi8W);w1 z(VLLTkBUM3WO7h(h{KA$^fO2*udX&ZqNwMn#%)C&`6kB{$2p-mNrTBL#c9r%oK>9T zy#3N$;JKi<$R(4@iYxqVhm^nU7cW;cQn==2mf}}_%Sh(B$qmI#ZYgflsA!_mq*-x? znBsR$-J)ov!647><;qJpxvOa7o}w4Q*b8+qDW#o14D#*Xo-BKfH!~@~pLU&hknvZn z#p^TdMqku>U-5t^ikZ6aQyT2D%%R3}dwu4sZhvdy`pi80aQA+d0l6ILMvf+z$fa!& ze{&^Hk-d+Di7W46;z$=v*Nd~VhV5XMzo0O|fv^pbSjh<_^C1IeH^lfN$-;J3RxsL8 zTkO<^NRpcLM6HgB`N;V{oe~C%&LMK1<`NhIV5od9hB?BsB^oZpBO;7sln$yeB9!v6 z6op&@d=ft`mclX}`Xh{f3zi7*Ddi#{Y50Bs>ARk;zOE-4#-$9gQPX z1<{Tq8&dezjcHv~P1mZT30X57JCq?D2|FfwE&X7GDl8n@2Yq3wg{@?_@R%NJ;)CMQOZWq qI1VnPO3pr)JnrZhJeJ&&{2|YHBpz&(+%NJr$$y*KLTw`2M(zJ{r=BAK