From 5d1c8468b44b8fd7f880e5dd562e750673cf2257 Mon Sep 17 00:00:00 2001 From: Luis Otavio Date: Tue, 3 Dec 2024 19:50:52 -0300 Subject: [PATCH 01/75] Initial api --- .vscode/settings.json | 2 +- apps/api/.env.example | 6 +- apps/messaging/package.json | 27 ++++++ apps/messaging/src/index.ts | 174 +++++++++++++++++++++++++++++++++++ apps/messaging/src/routes.ts | 84 +++++++++++++++++ apps/messaging/src/types.ts | 18 ++++ apps/messaging/tsconfig.json | 19 ++++ bun.lockb | Bin 715736 -> 720640 bytes package.json | 1 + 9 files changed, 327 insertions(+), 4 deletions(-) create mode 100644 apps/messaging/package.json create mode 100644 apps/messaging/src/index.ts create mode 100644 apps/messaging/src/routes.ts create mode 100644 apps/messaging/src/types.ts create mode 100644 apps/messaging/tsconfig.json diff --git a/.vscode/settings.json b/.vscode/settings.json index 91574a6..bd1f531 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,7 +4,7 @@ "editor.defaultFormatter": "biomejs.biome" }, "[typescript]": { - "editor.defaultFormatter": "vscode.typescript-language-features" + "editor.defaultFormatter": "esbenp.prettier-vscode" }, "[typescriptreact]": { "editor.defaultFormatter": "biomejs.biome" diff --git a/apps/api/.env.example b/apps/api/.env.example index 956b407..03fea4e 100644 --- a/apps/api/.env.example +++ b/apps/api/.env.example @@ -1,4 +1,4 @@ # Optional, only needed for developing Dashboard in the future -PROJECT_ID= -GOOGLE_CLIENT_ID= -GOOGLE_SECRET= \ No newline at end of file +PROJECT_ID=xxxx +GOOGLE_CLIENT_ID=xxxx +GOOGLE_SECRET=xxxx \ No newline at end of file diff --git a/apps/messaging/package.json b/apps/messaging/package.json new file mode 100644 index 0000000..59da657 --- /dev/null +++ b/apps/messaging/package.json @@ -0,0 +1,27 @@ +{ + "name": "@eda/messaging", + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "bun --watch src/index.ts", + "build": "bun build ./src/index.ts --outdir ./dist --target node", + "start": "bun run dist/index.js", + "test": "bun test", + "clean": "rm -rf .turbo node_modules dist", + "lint": "biome lint", + "format": "biome format --write .", + "typecheck": "tsc-files --noEmit" + }, + "dependencies": { + "@eda/logger": "workspace:*", + "@eda/types": "workspace:*", + "@supabase/supabase-js": "^2.45.4", + "@elysiajs/swagger": "^0.8.0", + "elysia": "^0.8.17", + "zod": "^3.22.4" + }, + "devDependencies": { + "@types/bun": "latest", + "typescript": "^5.0.0" + } +} diff --git a/apps/messaging/src/index.ts b/apps/messaging/src/index.ts new file mode 100644 index 0000000..e1ba793 --- /dev/null +++ b/apps/messaging/src/index.ts @@ -0,0 +1,174 @@ +import { swagger } from "@elysiajs/swagger"; +import { Elysia } from "elysia"; +import { + handleGetMessages, + handleHealthCheck, + handleSendMessage, +} from "./routes"; +import { messageSchema, queryParamsSchema } from "./types"; + +const app = new Elysia() + .use( + swagger({ + documentation: { + info: { + title: "Earth Defenders Assistant Messaging API", + version: "1.0.0", + description: + "API for handling messaging across different platforms (WhatsApp, Telegram, Simulator)", + }, + tags: [ + { name: "messages", description: "Message operations" }, + { name: "health", description: "Health check endpoints" }, + ], + }, + }), + ) + .post("/api/messages/send", handleSendMessage, { + detail: { + tags: ["messages"], + description: "Send a new message", + request: { + body: { + type: "object", + properties: { + userId: { type: "string", example: "user123" }, + text: { type: "string", example: "Hello world" }, + platform: { + type: "string", + enum: ["whatsapp", "telegram", "simulator"], + example: "whatsapp", + }, + meta: { + type: "object", + example: { priority: "high" }, + optional: true, + }, + }, + required: ["userId", "text", "platform"], + }, + }, + responses: { + "200": { + description: "Message sent successfully", + content: { + "application/json": { + schema: { + type: "object", + properties: { + id: { type: "string" }, + userId: { type: "string" }, + text: { type: "string" }, + platform: { type: "string" }, + timestamp: { type: "number" }, + }, + }, + }, + }, + }, + "400": { + description: "Invalid request payload", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { type: "string" }, + }, + }, + }, + }, + }, + }, + }, + }) + .get("/api/messages/receive", handleGetMessages, { + detail: { + tags: ["messages"], + description: "Get received messages", + query: { + userId: { type: "string", optional: true }, + limit: { type: "number", optional: true }, + offset: { type: "number", optional: true }, + platform: { + type: "string", + enum: ["whatsapp", "telegram", "simulator"], + optional: true, + }, + }, + responses: { + "200": { + description: "Messages retrieved successfully", + content: { + "application/json": { + schema: { + type: "object", + properties: { + messages: { + type: "array", + items: { + type: "object", + properties: { + id: { type: "string" }, + userId: { type: "string" }, + text: { type: "string" }, + platform: { type: "string" }, + timestamp: { type: "number" }, + }, + }, + }, + }, + }, + }, + }, + }, + "400": { + description: "Invalid request parameters", + content: { + "application/json": { + schema: { + type: "object", + properties: { + error: { type: "string" }, + }, + }, + }, + }, + }, + }, + }, + }) + .get("/api/messages/health", handleHealthCheck, { + detail: { + tags: ["health"], + description: "Check API health status", + responses: { + "200": { + description: "Health check successful", + content: { + "application/json": { + schema: { + type: "object", + properties: { + status: { type: "string", example: "healthy" }, + }, + }, + }, + }, + }, + }, + }, + }) + .listen(3000); + +const swaggerUrl = `http://${app.server?.hostname || "localhost"}:${ + app.server?.port +}/swagger`; + +console.log( + "🦊 Messaging API is running at", + app.server?.hostname, + "on port", + app.server?.port, + `\nπŸ“š Swagger documentation available at ${swaggerUrl}`, +); diff --git a/apps/messaging/src/routes.ts b/apps/messaging/src/routes.ts new file mode 100644 index 0000000..dd73059 --- /dev/null +++ b/apps/messaging/src/routes.ts @@ -0,0 +1,84 @@ +import { logger } from "@eda/logger"; +import { createClient } from "@supabase/supabase-js"; +import { messageSchema, queryParamsSchema } from "./types"; + +const supabase = createClient( + "http://localhost:54321", // Local Supabase URL from config.toml + process.env.SUPABASE_SERVICE_ROLE_KEY!, // Get this from Supabase Studio +); + +export async function handleSendMessage(req: Request) { + try { + const payload = messageSchema.parse(await req.json()); + + const { data, error } = await supabase + .from("tosend_messages") + .insert([ + { + user_id: payload.userId, + text: payload.text, + timestamp: Date.now(), + meta: { + platform: payload.platform, + ...payload.meta, + }, + }, + ]) + .select() + .single(); + + if (error) throw error; + + logger.info("Message queued for sending", { userId: payload.userId }); + return Response.json(data); + } catch (error) { + logger.error("Error queueing message", { error }); + return Response.json({ error: "Failed to queue message" }, { status: 400 }); + } +} + +export async function handleGetMessages(req: Request) { + try { + const url = new URL(req.url); + const params = queryParamsSchema.parse( + Object.fromEntries(url.searchParams), + ); + + let query = supabase + .from("received_messages") + .select("*") + .order("timestamp", { ascending: false }); + + if (params.userId) { + query = query.eq("user_id", params.userId); + } + if (params.limit) { + query = query.limit(params.limit); + } + if (params.offset) { + query = query.range( + params.offset, + params.offset + (params.limit || 10) - 1, + ); + } + if (params.platform) { + query = query.eq("meta->platform", params.platform); + } + + const { data, error } = await query; + + if (error) throw error; + + return Response.json({ messages: data }); + } catch (error) { + logger.error("Error fetching messages", { error }); + return Response.json( + { error: "Failed to fetch messages" }, + { status: 400 }, + ); + } +} + +export function handleHealthCheck() { + return Response.json({ status: "healthy" }); +} diff --git a/apps/messaging/src/types.ts b/apps/messaging/src/types.ts new file mode 100644 index 0000000..e1e0a59 --- /dev/null +++ b/apps/messaging/src/types.ts @@ -0,0 +1,18 @@ +import { z } from "zod"; + +export const messageSchema = z.object({ + userId: z.string(), + text: z.string(), + platform: z.enum(["whatsapp", "telegram", "simulator"]), + meta: z.record(z.any()).optional(), +}); + +export const queryParamsSchema = z.object({ + userId: z.string().optional(), + limit: z.number().int().min(0).optional(), + offset: z.number().int().min(0).optional(), + platform: z.enum(["whatsapp", "telegram", "simulator"]).optional(), +}); + +export type Message = z.infer; +export type QueryParams = z.infer; diff --git a/apps/messaging/tsconfig.json b/apps/messaging/tsconfig.json new file mode 100644 index 0000000..8360a1a --- /dev/null +++ b/apps/messaging/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "@eda/typescript/nextjs.json", + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + }, + "strict": true + }, + "include": [ + "next-env.d.ts", + "next.config.js", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + "tailwind.config.ts" + ], + "exclude": ["node_modules"] +} diff --git a/bun.lockb b/bun.lockb index 0b149a7c913638f4e75a048f11693e5a402d1c8c..ef2a5de5937a1dac36939b54dd6013f0b8a16f2c 100755 GIT binary patch delta 141347 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

Wsk)O091)xNiA(oi7B&|W+>j=v=;bEbKC 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 Lokq^17?HW6WF delta 139322 zcmc${33yaR+Wy_ANkbn+SzHi7K~!*yfC8O`qzR&*5?|Q%M>&xV>`>E}zr=EJM zPW2%+{&wg)Zy$Qa;RnBx?DX^e=O3QErh5K^!}h=Z#UoaY?$>tcW8+>La#6;4;}0r1 z_N$#D`aF5%89f?Kx~r`r_E*NbL3dC1MBaz+UjbLZ60_+5y19k*Y z20MU5!Ck@osPBWN#gl5LR8&m6G8T#K4nG&%6P)b$DdG(X@|>WLE3gNsjyxydoM9E% z0PcbOdOA>keZ@qhb!U2$B9|eT0&~!QA8-QL4LlCi!1{xGfn6O&!Es#>d`$z=>@9~4 zG_(tR3cuQeV?d3tw!FTiva-19X?jt;2SLg2<(E3X$>CMt&hYbs*S#(WSHaZE?G#a~ zAt-(ss8Vws&I&q3`y6~Trq*BrP`Q_YvSv9Zq@l)jAbL0cH7JapeHD5@&TJo1Q0*3`Db+H}?f791`)#iS7k(|`u4r@wbb>^hf!pR>Yml?YTt7APH)-c=h!4;rp z@)K&yctukROQ)gp8*s@VA831A3M&5&hl?<|>X%e!&YV(M6S*3>JZ?@R66p?>f#TyF zF3hr%bsmU3IHzsbhWXS{AH}nZiwfy;agNn|0jRdm1*LIiVa=4xX~lIFj!y(N4`Uoo z8*Hud45)gk@-wHD6iuPs)O?&wd5xi>ytt;MthgakR#H|{lsT!q3Pt2e6N{(U7M4aL z4>EY>tnqZ@iXRK$%5O?WA~;9mMzAM%FUYEF%yQ+vA7JCkD&#Ukb#d8@;;KkwKl0_z zSx2$Nz+b@qA`Owo4J3LX*f1352M;{jj(8_m;W|22#aD+}k1DMwmnTPlIL2D{V~1}z zdxbTkBzV$-34|)egoJZT;%YZ6YKy!0ySmtfLa>sPs9gm;=4D@KgrI?i=Ya0 zJlS@54yb~4{j?eR#QL?R8J`>D`OIWV2af70u+#C>Oke6 z18VAy2X_Z=IK%EW2VgdBC3}NX=*rP{pgRT;eBe!&F{F56Vc*)4NYAl$4*xvHTBP($ zt5{KWb#Zl3Br*oBIsS}t^5(k2vQk;-IOMWWE-1w+E2?X#6S;Jp9eEq%QY6#mSDX!( z4^);+s#61JBG3qWQ9x7C#UV?4a%shc$mi$S26k}`m5jFyZ$l9^ycI5mCR7y`72_P! zSksa(1Eo-#2{!**S6&0yM-|?4Bf-p*i;rNmuH&}BZ98?zevvV2ycZE$H1e$STihZWq$=d}~A5l>C51M9&P*zf2QdwACo!ZE@m00P+qyP!g^ILsBK|*rIn8ly!JgBI%7atxGg9XRTUQ& z)i8{*D$8F6)!JrUO3q(YT0M*G$lGwWejTX%##)PSQdvHJ_zWB89)a%%Z$HzUnxu=}~TT!zCn^>#q1@|)o* zf75Jh-zKm#{0p#?38WNIfcpRilyN_rslv_YS%q?ObmIWkr6u@HZSOBV26)5}cdbu6Olb6}v zegS3Yk3r=hx6s;kt;=5mN}JPy?{@9cFzyQ5+7V!Tq^E+?eZ?Zn^WkcxJE&H70kzb( zkuPI?1Zp;_D@#jij)+8FhpY7$KxrGrc$(G8RTa}St0zu7X|c`Ex7g6QnS>lY_iEel z6`(5g2c-xHg5u1H6C&n7I<2JUaLRW?{?;|Np`LJ+uMKU7yTN6il<`x+{jtEh#|yZc z>dz>wEoGfVBG=z&8>(@BSY1*6Uj15s$YR?e9zru4gV-8wT3S-_GxH*ngVixRE5%t ziG|fuB9S9$P=-o{yoF4FI@tL>JO1e)d zQ1TJvcTfZx>>8R_SX0cAvbsN93LON>0&%b>`0EX};%0{%!QGMfd&r(Odx08RQQ5?4 zcu!=^!`A1Yr`#UM`#fUH-vjTi_Tb$Wfyy4W2a%5ubcF{L& zpfvyPI@d6$2A>0^@LiztFLhW2c7>k}9S(B1KPZJdK7;+0Xnxw_mk!?nJ0pL= z;UfV1w&AalOXD{|Im>ELCchcf9Bz5ju5%txYif!oMtI7b5#e*=TXyv< zbmjWLZQDN()bf1a;W`jS+ccKF6MVVHz71WEu&b?Pa(P7+-cej#Syc?S+i2y5Q%&=c zyWp~H07{=q`cXKntt!c!T8%?HJ^|Ef90jW174KSomw`Tf1aifxp%xn&*S=>5R$4rx zTmyL?t^yA^diN(d0#I;D4YoIS^d5EovjQmjf#G&Hq>9xgGb&=GAd^e_0GY|*mqpLo)GcoZK+hFzQHt?+l zWq~h0weraol@m398{u+=4?#0xhm$I*2<1gnI(=r#|4e&+0|kHi!glmB1+}bmz|P>9 zFRiIRq@Y&kqhHx6nCLp5{k2_{yL@9iI+2FeVKZ{M%GY2g@MBQ&>7WAaNb0NoGbrDn zxynV*1w8j#+tH_>3M|~>d=AuCoQcQy?hrFtIa`CJ>$}4P4 z0M-6!p!z!slwucriv5+*X{#OSsf<(^bDW8u#u94q0H7#1$?pZ#-qoNixbrWTUj)|_ReXm1rBRkEa1huL-W_ZYMnN_7BOR&2DNR;>4_pn- za0*NWWw8T6DbUej1XO!3Ffgt1rJxkO1D}$QRX4Z`kJMB7rDUk1I#3Nx1Et{EP9FQ+ zTB-z;bAI-b&CexY{H%y)>P>X3Vt?f7ustYCZ>7BW)@yA$4fk>cREO7r>hPL2o+)?= zC<~mD;hBIq2-FA~k!$2r9gYK4e>(LK0*8QdvbpU%^8~b4+%ti%EvWWW4}^7vrJUnx z8X^-Y*n^BEzGouq&TyIh5KwcsHz*TMwGZ==?QLzoo;(X#? z5t%`TMtmeF#dJGYOoLP4QYZ<^1MURPaN7a8I532-G+ zOzq9!)=m+%A+vaTY4aYo!e&sW-^VrJgBp257b`C+EK+^ekd>>0%8H6op{^vWYbItE zm(PeihDBtln(Cs8@<~P04(;lhWxRE~wb-PIB~@ysS644MxBGz&yLGdc*}k{6Y>$0B zSYa(@9e zGsTn>o(0NqLqMGtvgt_m%PT5zgh-@t!i1{g8Jx8bR5^UG4+%|m>Utz~n=!DroziYD zpIa2wkM!?jJ3iNqY>LA(K>6Y^4hK3sz~OEVqYl5v@*Pj0!beJgZ-8=v@0bc{oLb&@ z9%A{ShguV-mibM{HPW39vmJYf1ylF!-E~%`)$bO>YUZ@dY*?Eb+_P_=hX4PLohC>& z{J%bSng<%p|N9^tOFnh@H&EgHVTU(>in8ZBECuDrqrg2i-$O~rQx66;@1t@o_JGUr zHo#@KJ>fEM>M3_ZZS{nTx=7^XTw6YM{g%2;tEeh8{;_GWb;Na`+J6aDyZ7ZG&mb`| zIHhN@;crPh>kok{b`z*#pANBZw+__8nO9(&t8(i!4Y-%t(@wXV?TJ7W@iAN# zpC4r>b~-5gy^36lZac&B6>v@XT@IVLZPQkB+!$+t8Iwv%wdI&V$*%e-`5M@LW38oc z7B(QbgoH-e2x@}1pJ_Y#1e78hsVGnQyOU2Wo>5xCoLo)E+B#~vbuJ;MGhzCMG1B>L zs~|TD<~zd)g(aov5qWu>Eq~%U)?!1!292z$xJI`Rk?!NI1v-L~KLV=bE``>F&2Y`( zXK;;lJ_=|Gf(dqH4WI^6gItyjifn%U%#s>ilJ}ly6?hHQlsqxfYcM0+-DT`D$(pnc zC`e>LLuQ8P>P=bYJhX5 zHrR${PV>xna%Y0_+Ha;>FMqAnYJ3Rt{>b+OwRRfHttAczYZf{WeJ30PCL!~YFuEVE6EuZhHC=K7NwjJ(5#~RsDXfDeuX4(^6S5O^qpJCg78$1x+@Hh#r=S85-q&HJh4cu673tj}O<7wwwc_~~L zIvrF8gFwZB4YTc3rJljFouZrOSd(X-XBFfduW2PXV^=IsKdgT;G?x(sa*EI3a^<@W`=t4=&)k)nH9TlzA=lhG*UgKd7Ufe8mpNQ|sa5M% zQ1+RoXZwmt-6F$zVMT&3G(9~z&^f#=jubk?xva|5f33gOlue92xu1>y!tJRMjz5_~` zi6xUJ(Rn1bDc`=>&i22arBY`qw`n)*xLebZ`}#fd+BJ66qmy>pzYJ{%8F{T;FzY~V zv$L-A%q`bJprZ34%4v4rxZWx{fit_djt7v-omL{(ihso9b3D0?gfgxMoS;Cac(tn=O`tvcQ?3-0Q?!Z2ci{Jw&FqpJ!eDTbzO~-%7jMJ{p{0 z!EJT~)jUJ#s-e1YmMj|S`)Auh>g$6l%F7SB+-^s@6x<&kfabM{NY3_TIp&0hB9qxf# z7W$ZxXzn+FvPfm&W4!Uj;SlHz}tWl6tSKrkLA>D&9NUX!Bi6X=uC~feNOc z`j3QbK57d2N}KccY^O*CsCgX;D*W2lHzJXLy=xypzNV)aC`IC+g3(_$*=cwH)IiRB zU(?EbUxz>)euzLF%z!I=o&m~ko8Ghy_59Fwd?l!Z)fVLP{0$%3CG;#Phr8Ed8K{HR z#h{k(8p_Lo*L-68sV%Q8t!?1e>-bM?#c34KT1;JirJiq7_adp+NCtgoE%yEAb|mkD z8o-N=FLV5QPzqh(YbbGXX!OTMxVoC|7AU--2RA{C$- zuBM#g`6#$fLifOB+SChisTbW+JoUm`>P0x-&T23Xq+VRR$te)oYCHJH5B5--dW|bp zF4e%JyZ5P&sM=@pq%+`P|m&hmj-Lf%E_7f0=+tan_V03 zK`YW9F3ao>s^T6FDbF`tS&?CMa0v1qpibYves2#SzyE42xgIW4F9oIO`QV{oAt;~7 zZy+IO+shUB{ySSR`wc5!3s?RqGqZaZYCj9n2X=S$`W0+Oy)xVw2Y`3`7lt+X?okJS*BA!m9H+TtFNxg*iDqI%$E2xgwbhi1G zd>^Ef|FGSx$t`o|@`hFi^UU3C#oF2u?sFrN)RDTZwz`I|&WbB|V_28igPkIM_Ov6u z7?g9q4l0tK>-gneZ2oLeBQFKb#sz8ss%O3eOTX)TguS9py=8eC1vQdAC@9lrfNJQ6 zJuH3J6Pf``U7469<|(zir=OPR zp#Q)wy*`JjvwwgpoqGRz9bBV-xkoT{VDE;Mkz#ZvEC0&E&~%SJ(DD(Ws(HNvElZ8(;i)WxKZHL2H09b-~mxIY~hG?xtyvMZ9_Z9!c{GImES z5;-}TQ;{3{k<@9yoauuz+NMO;kwQe~H)&a3yOiuoQglo1d!%e5nQ@~OvU5p|RIc}G zkX@DZdIfb=$=E4awIIl=%Jpstw!yDw7RCh4RSbgE=_Yk7GjxhcT}SF9llo^mm0+Gu zFtYiij#tWC8*Hmd#;QD?jl4BY z3$kY={W&bBq3R%9y_c62ET5H({@y8Qnw3vC*>&XO#yR9if`W>K_js_pE*br{bI??m z@AVF{>yzHtpsqgYFC}0eYxJBuGZFs@mfyNde*h6z&9n&$W+uEjLEX8@_`Apkt5jjw z)+iR+i@n>7!#gX;o}G*>h7ZuVyqALIvy=YL95M!&R_Mh`2HA6xel;=;FA{k4d=D&3 zZC+UxGi^q{*)wRGlkXqMo|8?+PQl#2Wo1By1aoHQ`b$Wud|PARkAiLICH>RbEu>e5 zTJ&#)X=E8j^JuWVA?Y8>Sz{P7FDRIk@GpaDw6V}T(#vY+^1@EbthxPNiS3$0T9}pa z&J4CSCcQ;L_S~fZ8aZmGP2iDJ8Z1Y4AIFB1ZRyzwe>+SiGJ@5!6W)-ZZeG&AgfrbS zl7$5s(X2R3+Ij&t@!NAS6^jO|=OyBWF!gMvKI}Kt^MmE*C;g8RAEpL0XuksoC>b>t z6x1jD5ipsPp*1G_c`!{I?)O_(ux)-ax_#du=Yss$AkL+%$C|pr?%s*Uu(l1SZ z4?9jxhI23Uvf@3hi$!HY|038CrpdWw34euS(O~XViRjlogPaBVa_R-iconA$4JGU} zGoxSd;6;8M!3YFx>_~e>BK=@E=B$K27?um`Y}Eb>Y&a}tO8x>!1quHu*wLxlUYB6o#Yt~+kbOxqcJBe0BbYNI*Z+`|M(BkF=X+ThoVD`J z`0Ik~OOxKJppM^o2fvRBwq2U^8+xZrheejdq%DS=m+-%UNu!uy2Xa1B9%}>rP7ks# zPkKR6cX`rZOOB=nE%EnG!M4kjvC|JSYoa38zgVesy_Lc8g-QQgWOiKG+Upi%FG~97 z9Bh3Y`_D?mLzsF?`=|GQux(LNtNDtgfA%4KyGd>**(JURrXK7RC>}kBM6-w07bW~3 zVF_5EfZ6#u3^io0orBfP&&9A)VeLY19v%V9gYBYOk9Ey7A^53WznGMC zGb=B4H7rXpU%@v>`aKV~evh4Ku?i;N#_zG{Lon%WH1R$TvKJ?r$GXKye?(vF8PsY_ zM4S2sO^fsW0raAdeB&UMj$xoViTJIsq2#3vS#gNEZ5yt>lweyICcSrp<^0|$*cK-J6OOcQ%VMZcXluJR>A#Ci z24{8RM!PZ_GMpKVzlJl&$uMhzZY*2dlMHgYW9ZB}z7eJ|wb5jFn}Y1?linUd-StWT zI_B?m+v3bbbaNtTx<20<6lC9!^lxQcBgisq)BhT#ILMkpxdFj8N>mV0od;f=@NN(4 zZcKV_1j}zs`un1Z)Q-!*@w;F$VA`bdACXuiYjMl75-eYt&JJ`YENxc*0hebr`4*;l zYr|J23rVb9P(Zw_hbcCilJQqzwwF{a?tt~wE)kblR0xxn7->er4`5oAR_C=anX^S_ zpKcTy!@~PB`i1@KJU@;ktz~>BO!i9G@Sj5>`_TTZMD*wUp#HY}*cnN4_P8zAKa}y( z3-iZficNu?9n85cH}(Rlu_o2C0Hc`Hcv75V$bEzq$0Jg|k>YehOXC=&I>HEx6W*1< zw%e1jilZWtsX_DYxv{Re>scmMOKP-9y-kXP7j^p+`DA#^*^r1%KRRf-JwN)A0ly=* zlu6oY`|;cS$-1v`0slyt^32i1uh@azn`VR||8tn8!IX?0j2Go=Q}-t;m6H2+z%+GU zIM-)egUOh-PU1LP2%6cj>qtpkY&J6yTMnCI#RO%^GlO*$c14cQ)C}5od!@P}qn7nIY)pYe4uZ77nZB2C93A*J_7G>dGi)p{0n;A?QiE4WSkCXgf^GaB6J$S{j7|1GIV+`$)_*ZfE1uxW-g!%q{Wv=?GP!}>sXLCdp`X*@yo7gJ zu{?_)gO#5T&LA;3XvVMiJlh^8I_NMIuY<`O(o^6+iliSBv!BF1g`FI%oR=FP zc#fTT>sS3cME$svn4^oo3?>tsgJkSG*x+F0<%8qnZ3kEsH=Y5LMWf-|0nAD|uV85+ z)(p$hj_M~0tu5?cS__kk?ToWO4I?@z(qo7ALETfy*fA5-S#x8qKaZ5G6uo*i%{xLk zZAirSETYjMZ&_}1cu|luGvB)?*tRn1Z$zv`=LO!434fP~DL?U8W=Fwv^1>9CC;Y{* zA*Q4j$Hy=e5VYg?2Ny>oNmEkGQ*Y$iIKFyr!dn__dphZVhHRKE!B&}z+uOyhjq?In zy1c(K%{b+F-Gl7aN&ni(Jg`w4Pke+E8qAq`b|The3a6@I&VpROoK!xg2upJl{u3~j z;>1HZ*__T(kB61md9Y6rdT~tJV1pYH{!1`U3VIk>obZpFN@!PJIQJS%3Om-csaDp) zG-_5=Rl@H-&7Ng;3JNYs`1*Ix8dJMq?uA)#h^ECi$E{6{F};^0yn#X8^GSbZsVxyV zn_c&^6vIdTQ84QOVpln49*SarhYbnxh`rs*t$|sXOwV|jOhJ!NWyKxAQ8{kB3zITt z?B1TiwilEBJr%?yWah9E{k0;fe<|PB>&r5WS#tipFgxKos%@_fa$e4lWloPo4iB2= z=lW+VMd4VuniF^&W@o$LhJ;RtuO$7PD(h*TH6O7$*pOi5>|B2xDXWjdbNgxn8|_4b zxeF5hM3`NSI*TrbjR^8MT_0VWdZM5$@8Tf)Z%O~J$e0k#K9Tvv8QQS(ULBl4V!T=T zhtIS}U{k=q0(LUR&0!?=0gOkc*9OOD*{Ea-&4y_lmOTR_jhldS*ygY4In ze)~Dr4|&R7mGF)amLt0enLO3D@fvJsnsq*}wO-3$Cz=+uYKj_CYe?HmhT z#R@|kQ!Nu#{7Yc+9Q47rHp8Sjn>H)ypt-4WC{|4fmak9xPav~vR3(0bnbl?HZ1_C0 zPjaL1V6gnnq~HI1>vF7mw!E1zjwTo9dXENmZ}G5uez4-L{Mg+2I(re>b#i_?>FpQP zy-i5;crVIrJ+<-J;m}=MXh&$L^v2}f8`QnS)7b^qDjcVXVc8d^7M;i3)jDQEDIL{q zNcsmauq8OQGoP2h??cHEb*MWR4pN3sc~`ApvvDPnI*dWAt9zYhqO^SdJ0 zwkhd9cDWrPoV0GKG)kAWPgzKyDhS-n^4C7F)CR8L4$t@ zGDQL&3bFRoE37pLO|NFfAw$SgbK3W#A5~i87%j^-DU3A{MtT@CrsqPNG zBbCK?{lYX;oVgA*DBby5mq+K+%em8bV4uCugSC`=1m?OU?*0g~aawBjy~{G}&wxr` zww`*w1tz6A8$H9R&oNw@vr*!1>t@_5&QJIaFio_H3;vfdji`gBC)($pAm?XJ6!)Z- z9Mz+LN7$I}w_jp?khoi$h>lwlzxleLDc7$cb((3R zfRjfP>?l|Vow}oAmj?B}=KEJIO$B+G=3CfMiWsv+`#%umY|oD_ejupdp6_pdAZ2bs zSgiMh3^B-?lN*~!Do3Yq|3gw!sBNA~qsJ`^Ry5~FFJBfkHS>_U%=QyEhCTlwdv3Ap zQGEi6wg5Jg=Il9P z1MK89JNU6w$kK_b8g{H&k3C817<0n$9(N_exev0`VWZNuSElmJ$)L*juY3j7}Fi@F_bO9ExMnxiF(t=v~G@VO9-wywA!Vi~vg;A+}O1nDgM^ z_^Q;1rCkwBlSnAIG~wR`Q|K{qH~QVGu&JGwA06{_m=pK%V`Ep-S(q31a$|Rs9AT4N zTarVbO-nCpNxnmJxGmf3ImT;~Q(KY`wIrKbl0*NRE_+E!a$QSu@8{D6CXhVFY1We5 z+>#vdLb^ab$zitas+MH*#kBO8mgFTZ$@fSO3|2mz8|(QBe^kb#&LuU*q}G!gWgnHxQ9L)f$zhMKq` z%;}2D-?Bl+tmgA_{Wp}Nh949>o``kasOX72I&ou|vp1t(jpQuy?N?j-{KJl(Gr_Wi zUk1~nw>$d7FtubX9Q#~(=J7N0-PBUlRyZA|v2fYKs{bo&Fw9}1b;5rib}V_AZFa)%v&kCRYIO$8)HAoXfnzq2Y_{JfeE)rGE%N{n zO}=lOC{}|sN1Df%lFFiV>Xa9ceWn`Woc`Y6b|2Wf>Bj|s3X*(E8_$T}>6p1L*W*U~ z6C}2^)TMv-4{aM}?&9NMs-OPy*1y5!6O6Id>#!qC``%qy@sCnZA{+>#;YVT9eqMgO z`C}Uz(+}^~6s%k`I8MR_{khZl?ihAfx?trpKjUlp5N*V+BsJ39n(z6Io567P)m#OL1uNm%1<*y?(+zdi{NB7OHyw0GpQ830$i;GEd)iFCa_5rD*gS zxp|pS>QtjXPlx{7uu)>+T(-rGA6QDkN~Uc(DGf3ju0Db75@t#TIzAt`HMJA!=CBs# z%+Z*LJq^n@CyQoM8UagzWtshBss*A_Y!QqRGtPgI(wwk-cvMRKWEXsTD*ZV~MyK+l zZ~he4_a$ib`Pr89Lhn$tgSAY^_P#*yM0kLkXN6zwp!??|bv4GoE3t(%%z zi=Pi0NnXoH-*gficKd9%Iv5tKfSql0dzIAals0V+Y>tFa4)F5)#%Ajhd?!+tX!8i- z+<}O{Lu}WZc8w!{OKY$+(Wc?INcgJ?J%?C@h_Wvw{I0*JCYP-xHXC-L8O$rBhN)EO z4f1%}j(Dka+Wr|7GO-OAv==sKd%4m6v9Lae zY7H?ja)K>*ej@r|ENns=+o!D;sWJuqdq_%C(^Blu?YziDlMx#f_abN8kDw-} zgc*t0emkd%`-LQ>+|Ge_1y77H`4OJOw)H+t;m&?rduV&l1a)hWlJ?;W-eB_YM5=}< zQ=f>v0~=-aID8i`QXbAZ*BcxsrCGO6WSe#gn}%TZLv{^w3cT~8i*^lH6wuK6UG1Fe$PlaW|cxQmEyb-2?TsN?$ z{stRhs%ag5(8;s0#;nRiI@=MkVc@e)@4KjH;w)EW%^QGF1 zR_qq8IGO=0Ln=$yCyb9_gHvVv9=qGtuq#@Ofw{?GonH!TDY+gd?c0O}yn__qBh|0! zjd09NqCXFot`}Pc>t`ecs>KBjTt@e%~C-(+4mWu9BjepkM7c1 z--R$2U2aH3*LMkXhBMwad!@_prFR}IE6lr`=~jxCc<$!`{QkYdoZ~6|G14Sbbmy67 zSXW!d9JivgyM|3FvmB{xPRvBtA7I+fnbjo;fB(I0o{4bLaeIeNBdBpX(!(i(6wRL8 zJFGv!J1>4vH_t@d^zkElaknt%M9L@j@yt~en?AlV4|W{2IW#RzMAz&SHc=)T-8alR ziL%4@wWFpid(Uk!MSg3?Utsd8c45KiIBO3rxxABj>Z&x!=F#5Z7NP$O!UChE;`*eX zp4m0Hf?Sx0t$>;Lul&e<)~n1HKe6***}A7vfn90W_Bjbt~a%CFH9}aGkyICv-_K@KeV^av#;*VhUHR{ zZ_=>Q<1jO3-KfO2z<5oQV@+}P95Y|h&-;W;XE2KgA7o8} zH*yA!uwtZJLM5vEQpduIo>`yqBnHt!sm>Y3#l zU3N&gVk`=M~k&2_`tOEgqGx0>7@S&NyYYELvIAD^$;27hi=y8?T1NIxL^S0Hh^a(S^Hc39B7l84Hnlr--EjUkmKnSUiI z8(y$ubnVcvsRC8vM_E-+^x~`x$cgFiApM)6ZsR77egac{A~>wqFxzw6aBedz5~evK z1kFzPx5E@qI70Dk^&ZTgX;z<~@OL{p#dI`32WH#kD?%+5TkKqfMUGlDv=PiB{Uw zl05bl7ORymZb^R7l00lA6K)Flb4hAuFfe=a(=hiiG3M0t!^GnxN19W(@1Mpq0>yET z8HsoiOjDj-aIx!riij>+judn&z&avUZXcfC1mK`?Fr4%XI!j9yy z@u_EDjjIM``+>!thVc^oQ@Qbeh1Orv_k{i>NX{}EspJnZ^Ae*!aDqJ$n>mT!4I4?R zw0FdtkZ414@4!9As{=*$xNVPke}U;h?T4%9aD<+SuI9baQc@Zl-v}SXMH)>0+oI1= z#dZ$NbH9HBOb_3?$a?XfldKP=weZW4Xg2Nls7qjH!=mBbQS><(osB=-sFaa=PvCnn z%`#zfW}?m5Dg1G@nR)RgQ>?jyd+-x#~r`l(OosGL)4YOUdTw~Wy;}P2I7V%OZ{7q^KsVU}~sV(jB|D!ZDMs>Y%>REIfN600{=hxMS}xyBk#KARPXXp-!* zdIP4XGHa0|YSRmlk=z9{LBgCxe}u`(#;)<~8TRyK9jr*YM-EQaFf!Ud_gINs~B3QcS`o`w% zGu!HGSgaa0Da?C>WlKs_L!|ySntu-xMPi~V&ifBoj%{?0d8x^k#}_+hj|h*#)FFpY&Ln%EZ%1T( z`Yf0hcbjl-FU}qJK(%n}GK$W(MLB^n_+c=`I_nCT!sI6DeJ;KMi55i5wC#3*eQqMQ zR`As^Ogp2AyRkah&>-*9T>nW@EyvnlVH&fI&dCc~#nAYju&f<=_!@~ybABW?WiM!@ zL1^9yMxrM-yGU<`+1;ezmxSN$qLe;bzh1qimJw^AwG|ai3Wj*0iE5%{h@>jvkks;+G@wS(>YSp~CHX4aEX7dfQGL#+W ztz;39l52DB;{ss6%Qa+u$2X3Y=8;)vx4a9c;g~s$y)PCtznJTHS!f$2SS?A!&Vccn z7uW3pDS4chr=$^Pm1MB7$6)4*huBuh%~ueITwzO? zXQrHXPKzq#LH=Onj$fR=3|_8nxZYv#+wP za1o0QE{ACw!Z(=2_h7o3;lYb{ySfLh9YFuS!3)BsC49YhH90%3hqX>(!iVoK#%8o* z_syeW@>Kisd@W25adESOJp{86gA;oE7npImmRA$6Zmr*ij@hvL7EDfNED+!O8rx^f z?mY^L^63}5Uk@8(0^g>TJS;eze{B(lE0(gi{2qo)U~JB{UgY$!c`1?YLy~{8$s4cJ zPGzLqNe&HHK7gBDZ#}fF{nulHrb@16t;~Wch_KFJPdH{n%`Y%wuaq5elZnU=aJ#X?xx(L0hRk3BMQr}fW|3UT zmP?AAfSo+!77kM;HI9_IPmJA8ihqWLHr}mnT;^uW&xLBn%o>W%h1qj`>YrA9hGYWy ztdbd7@!MM8+ujHpX-1;GZW~O?!njN<`_H(AURR1uCN(fzc>rIM-1z6P=`jMsn}}N0 zQ=i*ypEh>C4l6hPX&om2Vz)oDJ;ZN<>5!8)vj2sXSkFB14tq$k>~5Hgp+Ulr-Dyvl zc72V9Y4`BL0)3GOlj?Rax&JOUUzjNw-Rvq-KObff9C}Lo3Z_=l?Z!{K&x@4qAbApr zTq0ejP5%Anoa!&U-&%~>`!XvIkRXc@_HgR#tjmV^Fdmr?r%h5S#XxRI z#P@o{Iz?JyAB+E3IP9q~=S51*e#%B~!Wwrd_rVm| zO)QV~T!}#yQ=?a`40B#0dbU}`-AI`Cl9wBwK+-r_I+)*yL<(ituVyyslz1@G-nOnPd>?GLfrbzU`XU*_q&4^AiBLAZ2?4yL;8$X1(uPq3}J^yO+aGvKA zu@V^nK9he?bq}c{8AUYo;v716dY%gsOZhT7b|&<=p!sQD_9msT^f}$J({_7-FGpbM zdwjnN$?>Wf6kL;)A!2pv^`eaf9C)zVc`$1;E<~fxycn)n%N%rk$%_<+dDy_OC8;^! z70!A5GfCL+aOGNzv-`_r*yL!E+$tgUA1A2=VowLzf<~}r%ISYv%QJ>1$JVv^4r1jSMAj= z|AgWI>^BoC!#Mi)9qEC8XZl)l>vyDUp{KUw_FB6Gt%Yi4T+1&&^>pLt9+@kry=UR~T$eh2<8txIj_suY8BMW!&n_)fZ zuX@Y6I?IcT(TumXOwF4!Ui7yOVf}k3cIL)#1?WG#(Z&!uf1Im- zn9h8xGG2eq{6|>7i853EVVwn|tWWq4!n7r@8XB|W5ZMF0_yVivyVmM#lN|CUz_>DC zA^w$=qGg+K^>vH|CZ){*+s}H>Dq!mPH89;6kar_b*)Z#u9?RU{WP=aN{gQ~jy(z5! zkh$sdzLvtA4_PtAB&8X*ki30;1MC=^v6-X-rHS3KLq5>)Q8i+hkmTRfs`5u9-OzvG zJMa&6mNR9RlbmYH?Eg_}x6p&#R+whU=;IIjIJH|S@3I}(dYG27DH-ei36VF<`-Jbq zcn|JV4Ezof-71;a9sS)tP3>1YL`{H=B9BmvCqDr@Bh1^#d92{GaK&d-p7)tu*S>iZ z=OLITgxSVqU&HJ}BL74zI+)k&KF4J3{%Jzk=NMol$rHkrO5V~U{jx=x_`-x>6}XTj zkx$9jNgi#Jdw%IfDvU<43rKQAe}@8pBbjTH?Y?5k+2jzCR_d=NsbO*B$`;?{YdqGL z`3p%-(Q4!ulDtWyWd1j4=~W~-oJjgENxoSm>G#=eFJv%rQ^H>W)8WIif28t4?_T0q z?zib#Y)Zr~fDH-fe8X&f8#aA|dAe<3Cd1}$=%9$C)jir}Yq(-F=DujF{W5~ZK#=`v zYnbybbqan|oxE?Ub1_N&iDPZ9|1v3^MI)j2BK_w5WIZe+EI5hpfY0T%v@NW}?LUPp zgd=_qn}qND9OisSM+f}Ex7O5VopI(YglRA1{+o4r3(PvJ{<-9PFwHWHkR#1W+fu_5 zyAGz%#PZ@MXAR7}dKTZM$wmbe`oagtaUgRNdjz{0rnO|w?*1BBIgF8PK=tfj!*Cxy*);vX4_y2e+3j*V}Jn)n?S zyu=>=JCv(9)hYIrRGruwlBGfOBL2yB6k%Gm=)Jr~_6vjfLSmzUe-eKQPkT{!bF=UY z%-$h+>$Bqgp)4&mnW%5rugN>sFGOU|Kl+z0cVvVsnlQlL{PjDXR?M@nKO3gG;}A>8 zTmf?dl&?Y|*62FK4`^o_Gdr7K3X?n7e`kCTOc4!t;615NVLHWPzWIr0-+0)xolz|2 zkL&Ty*mkV+7D;JO1{ajyo;8-ZUgS)|n6z%h)NZ*T;F<96?HhM}rNC=pH+V zD}KX4$L$<8f&QKRt-ZeLHC)}xOT^!Vs*!ZF@%`IdwbGJU84?~WH|6>_kQzp5`@P5e zuzt3qOB2x!yIj2@n(rUSpV*UW*fa0S%7C!y8BA;{f0~bX?KG~JNb1yVF5nM3v_lka zuye%x4OV{JkB9saJ98mti{Q^PqWEii(Tk1(elDxJok-P723av`2%^Vj}{!fdPwVQStp zFC)a?g=&N?z3t5(3seE_wSUfvL+m+Qd($T{wM&fVCEf$}ibnKeRr(0!7bDR#Eb)(R z@Lrf2veD^NnCi0A^X>70UBjH6&~$uPTgLj-^^VyK()BQ_w??t&-nKq%Gwe~08I$?f zz%&SYqU3tVI682o>CE3O%nzGA9z~V1+R?(oG{flrKVZ__+(<+Z?H=ar63vel@#hSA zah*R1cT@Lp1rq;zBpQVs;Su}T$!4P`rcZ&%J*{-HA@ zs#L=cPgh(5GZSRq!Ttm$f5LCL)YzLpWvH#eu=og=^3snz(Rn?>6&+Ete@`0)FbMC3 zHo|xdn}5po4k@cXzS`!1{h~qctUQ0>es(EG!nu7>burXDMBCSjV6qIm5$Dxjy}}ip z>0oTHs5u_;92I26A$Hn5j-cy%h4s5pq{IH!+&ry5%n1^vQQ6z71!CcxPkGE;O;Uzw zalmazboAuC1zs%6JixKA;2e}Igla8#_BQP*s0PGh;5(9aFt=RK=cRc5kfZ&orihf} z_Tu6;nCft$$KRUzP^DmLFOKcgTeGgG%rlfqmGrNK*~o>*#9o1!`_T9>tdH zZzW~!4pUd8vB3x99_BsldZkPY1=nW9A@VPb^n@XH6_^d{5UUA(&vQa4tVN4IyFBCT z@r{nP+z)p@G&M7;`4<#%$LwmK57RVp`Y2%59)+r5`z-Yp%z5zziReCuh4uU3FOv_m zUWk@_B^1Kkn!u-?KP=4IH(KAYXT-cT4E!f?#{|-Is?8DDa{Pc14{{w6=MgN0> zkv5cM|1uw8l%K=+iSxr;n$Lfawtv%rG-n#kN9gnOCw?U7x!in&?f4mOsX>i=jN|_` zW~gGg&!}jR1|`qtN9~=%k7k17p!u{$A2AE=UcSOmjI=N8gr#UWEewpJ!<(KpS z-Y_B(#?sRbi$sK~P|c49UeAwaVh%r2c&_8~K{a#%Kl-#r)n8zwJE9DGrITN&cboKS z5&Yj=9bCmvjGxeE8E*zRZsrdd1-y zP#>Y{y~>aB|IUv-Lh-fyDF1cG*Ezmk6Rn2d;YXj=sEQl-5pLv1AE6q0SA@?Wp*-Vb zlWS4+KXF_rOK;{!?QA)KUn=k&Kl%uj@q-8-p%;$ZKdS!~6MvCdGCuzfcO^fA!fK|i zD*X#e9(VaWfhw}IeucY?jdm#@5or&V|8$Vpp^_aP|0C>5{=wvXVcA#~u&QN3W!?U+ znozwT3CeA=oV+zEKgY@c2o;Eqa`~-MqG3)BhQE%D`r-Dm(Rf!4mC0iltH*7H8c3WE7uzF@jm#t?9Dx%Y3>tOOsFKca{9aYk?$S< z0o3t==W63nJF7Auq0Gz!r}_L5DvJk4V+Wqc%m2$kgUZ$3ii@ocwOa27Y8)+lpvn&DVyIA=2R5h|JKQF zXjkbeuE43TfKUyM0j0oMptK(6@`aL*cl_U>$`!eCLM4kGFOD7x+d=R@b1%Gne6$O( z@Ft+4BL(YdM$OL#rQRIJ8$tey%;T2=?4?e=5Y*%bj$Z@vU*uZ-a;W^9;2QqzF8>aP zcRBg}{0{_7SbP$J6jnJ$>*H>1;<~KkdIIbzYMCwzk_P$b+9w|F(`-l64Ywi z4C>Q@Y)Go`9T{riCr}Ok>`nUn)LS+2KA#(JseH{o|D9P_>3D8$900TchM7knaqh;mVCxxiBx2 zUPwg7xM~K&IYrUF{bj5Ph&2l(E?X#Fr-2fc^GmH)h=k237p~8Y?iC(3F&b}BMl~6# zP^$tCC7%IT!*jr4;A&7s{>m?@xmM(VKxM6S`9dY%bi5Us6VVPC>i8{JP$#Y$-j4Uq4*Duw?@ggwz2(_fEhsxs=$v z)L~DD`$@=0D0weX6%TN7p?GhH2Z1Vgu*(-JnaM8=v>&Ma{u1g*M+uEM;WGI5?xw*U zTOcBovmFiU=r9tL*N+9&@mZjpvVwX13Q2>fcgkydeXU-gpTRU zL3Qu~C?{JFO0kXLZs0edqU!H1e-|cDXP5m!&E-M-QvHFTIvxVbX@`SSXq3YVpaw7% zjLH8hNT^^Ps0J=hnz6Ev0_{DLd zI&5-Ws0zP2E)?JHxX=sd9O<4b#5L9NZcPTn6p9DWWc z%S;4SuNc%vC_bqz-vg?^G?&pDRiM<#g(^24lxQZult0U19oQx;8;IRy=ebZ7ztCkb zaCni!i$UpsspFS{`UsW0%E<#zLG5~nw}A=)_k-$Z8K`z12K520C$|!$GK{{DxC)Pf z(*JRXPdHrea0RFiR)P8m<>;#&7pmjuKn?UIm%pYh{j1>L5vYT;E@K_22H$dggX8Zx z{Lta24!;2P5vrXp9T$p!1xf+^|B%$*7EtYPZE%909X2^^b|`zQLK}x|L51O6K{ePB zl!?28`Uq9Nhr?c=I_mBC!H#D-Z0Juy8Xf`CXCw>MhVv&-rW)b$PjGxBs0K%YDu0&4 z37|eg)td~e-6^0Ho$B&iBl!)HX|6z-t5EKEB`8I7m7@mdgZi{ab#Q@`3#Hh_pgO!1 zRMu7el0~it)z0-2I&35VyU7WJs(6dzLh;)i|07iX?c__5zqs;uy7EGGe2>dt;^ab= zzgH;#zmJ4W`H;gWKz)QNxZH8!-tae^e7%zkrN~>3w?-+j!Ij_W@`Wn@E@&QQ-gg;7 zndlQx9e?WZGnX%viN6F@?pr7SPf+Ezy7H~iuK!j7itSiX9sWW^DYOlg=D)gzgpzLu zHPYWfm2aaZ@-L|Tw#Ze!9k>VBJx>1;?1MlJ_iz=3lJ|7-UJeg%`9kF%=&-lLKA=W= znB)5Yn&~5y{0JvM5>$T!8(e{btxzM#c3ddU2ZQP`&&h?74{={>ho?C_-Qg&QXE+?~aE!yTpbn~2Kz+al zqxw`Q5NhGqftu$=P(zys>cDxK;|oE3TBFKc;p9RM;7Z5;2$g?TT<2bs;G`P9Iyl-R ze0OlP+?4!_#;$> z-(CKHf@ZyQ2vxld9Y!5Wk#o(hH4VpZ0ek#9@GeD zJ3a^0r#0&Nagi%`B`6CmcKjMp9fqJjLgioY~#Ud>K?juY$^d z&EXrMEV=keF2^IVpR70PE3PIn2`Uus)501A+iMI1g^_yM3 zQ0sY_NH4Lza3QlyFq<~ z>iAwKf6(Eh4J6dSa!?>0F>e%g8H;Z z$v;A__8R`_3baNk^tF=@ifnaqq2xb0{K?7x8${uT$TrtN6R3`Va}BjdjnFI! zhw9jKyR=aGZ9vI09B=D*JBQlUmG6W4Se%?n{4q+QU0emBI_luKPz`o;yfvy^XP3Vl z*e1+7m(3PwWG|?)_jc7IrJb_jl!6qg?Y)mw&j!{yTFh(1%1C z)!-418w|fWnoA9>jsL^m+ral&{{P?m*o_TKOR^;)VWTBUCgd(`G`GnXi-l~tOOh@0 zaT7L@(0XrJEMco8Nt+}j+1ykrnM$H$DwX=Gsnqp+&GGsE_U+gA_y1kj|9}1e*W-Hl zJUqRR<9QzEaUSP!-hR%TecC&%sP^9K@_!dqO-EO*Gg0|2uDoJ6`McckA#QjQQS*`P z^7jxm-O<%qDRjQ~up2OisE?v5p6cQ>=l`py@{dqXGccW~j>&NG2{(S_sB(*|S1czb z$W@5{9M$rt7_Lnsm#8JN(v?@#@Mm3KQ4Lw`@`{?!TB62((RoGfG&Xu&flW@lMpOk` zi0YX=Zumau_Y>8{CtUt3qCSe6kbd=8bN&NSU3|{vJ?z9)Zxt8)i45Wg!AYoK9il#p zs-QkmExeYfy+<=5|NL%t`SwKhL`S0b-+hRhKwqLhipuvRs)q&=RX)KDSEPQgUy>`3 z?BYllQ;8a3jLY9o)JL)6;s8+-nC`rydSIr@SB{#Ixz7JLqruUCF-Lnf5hb#Rq;`x%6;zQG3UP^>QgzYp(pW* z-?-t5%70&j<*y3QxB-f)_(zvl)XMpr^S?Wo?%4Ya+yrjM^VGmU0$)`WPqsgZ7RoN z1m`C?pXTC}3*O5y^FoUry&(KDh#GO8i;odi;X*fjiHqij=MXhVPZKrAE8Or^M6G1c zJO2VvA4Sc;OBH#RzYtY;8jmUH=KCkh*3!}_(B0KyC@3kTTLsX=HO;mK^s?r zqV^A6oxjcbzl)l1H&?E^E7!dy(^tiDPAF>5dlTjR5!Lbm&L9MtIZ3oo)n0b?Gjb|GTIP_PXJUn(#iCuN+m+JI?zes^Vg2 z#Va-}%NkE2D=OvU*qYL2c0b>;v4*J>E)pX0IoRe`qj|Lw2Ws1+CA zuHmJc;p&NtZ`bU)&%V)f{)&|(J9%*w`(f8qv9=_i*MIl=nLMmx%hSsU2*a4n!kI? zMrSA&->$iE#&z-Snu#{+BysWWnhPfi z7vHYA_;$_3w`(rGUGsNu+vqaP-@HU~;SSZ`45&E5%Illh#kXrNzFl+i?V5{k*Zkev zHrhY<^Hhc|`Cfdx=HlBm7vHX_xZkhiC-?nzWANhJH7WFj9^AV4cFo1NYyR@O4J(G8 z#N(r=gUQ9WYby2{fA?06x=CKg+KX@3=&c%k6t&`9e7ol2+clNnve9#W7vHYYTQ=G{ zF1}sUo@ZvY=e+oK4IQBq(~ED{&92T{|UFED;}vsAoBGh_r5q^Ahzf zt|ub0J7Qf=L_<3#Q7Vzt3(?35dLc4;AVP0PG`7Us5izldtrATwBpy*FF*Y6%Wt%0k z;}G?FBbr-kZ$xZQ#D0lri|B(WmzdTE(ZWh3@_Qko`yyIedS67s?T8~1tt_e^A~+tg zpdX^O9hNAP=+Gb0#xnaOQhFm!OSHAN0}$bT5V->o?d+69u|)hGhz^!>2O_O6;=Dvh ziyMfD?1xx45YgGrNt8+?-HGUG1$QDc`XfRIA-Y-OAVkan#8!zO7LtG{lNg(Th_lTS z*>@o7B_euRY9b(-Mi+_HIP@U5MPf5rgfNM6pEtP(+gD3`L|3Mx2)z zYH`C5kwXych9Q#e9Kv&t^-CrUw*rL`=DCM3(h?Q!wG9fREMzz##gY|LZL`9C7B+$~ z+ENw9*barU7BP}A&L${~w-SZ>t>L|d36`$#fE`enXi=jG584ccNp@J_A#0gJm~5E} zX?9E@-P)!S9=1gaQ|y$&RO@^nVVdPAJYr`QycRc_Fx~PLX4pA}nbvO%VU`srJZhe? zgxQv;Fvm71%(aklgbYho$h6H0^DJyUVZNm*JZ3u-7FfjngvV`y!a^%ic)}V^AS|+U zg~fJ2VTnaOKzPz-C}i1Tg{Q3LM1ol+!BWPvC{9mgQRGcfcGhY*_9M7hMYDTwt}B9T8C5j_>L!P2K9 z64DSyBsN;qG(>PZV!<@TCOa%qB+=m!M4@Fqf=GE7aav-twe=#xryz2@h%I(XqF5q+ zI%2EkOh=?mMVyz|W^pqRk<$?CW*~OhIf+t~>t?fKS_#8y;Jj5q%e<;+K<%|)D-IBIc^AtEyn>mEZKvvU%q5=jdXUs%BcL`Ehe^l`)qOMDy=GY_#< z;-rNvM3hO4U5Gekn&w8fb7GJjUFo`#8Bf>~!Y$7AP==9Ee#Wh4BpARCeK zBqB5i5on1yh?p$IR*9+>vJ_D!F?J~;#5PN0KZU5b3{l-ummy*ev0oz8B9m#A-X&mbb7Myz`V(a_FGlu9J6LNv01Rfvog zh|p&djVZ< zL<=jC$X|ts&PTMg^n66Zvxp-Stt_el5xg3)pa9X@4oehCba)QY#xkEnq^v=lmS}5j zpGSnRMdUt@XlJJ+iY4M-KyKI^AuONb7qQrt%C^66uOB6|T*n}8l znVS$P8xW@@60Pm4i13#YxvwGy+bM}+iTFZ9lI0X4(l#Q_OANKRB1GgXh;>DXWIHEO zDv`7qG299^BQiE2LSI9Sw8YmCF|Q)FN{q6QEr>FSv0D(Swpk*(5K-@S#Ar)>9T8iE z*e@~GBDNyRC8ljfjJFbr{LP5yHxLsn{S8FIYltHf6D?{RB6tg8!8XJsJ1kMO#n(X12M%;Nfb-OzloS;Id3A;-awp}@LJqXMC3Nax}As_ zc21&HBIzx}EGu{mk+B^Sx(hMe5_chDb|AJ&%(alW5oHo%-$rEGW{K=K5%qQ>=3DA+ zMC?w)eu)JZQH&^;m{yEfXeAQ)Zy};f5Q{9m1d*@{aYSN?MeRWZzl~V12a#omC5j|E z>_wPm?nR{RMx2(&v9|jV;l+sDeTZdtN}^aIem^4Da`q$AN)YEIR#@CSh{!#Nb?+eZ z?3_fYMAEy6RaWpWB4aNi^gYCCOMDLzvk$RVVy%T7K$J<0J%A{%%@W!B5%u0jJa4J* zBVyk{?3Y+)5g#DRC8m9VSZ^f~`R^j4KSXS>^bZjU?;(yzY_zC@h~NW=1qTtE?65?U zM2C+Mg_ijdBISL=X^G9&_7Ecc14Qm2#1=axQ7jQ(ir8v7rJT^cVP_P!S=?d5cFR-P zVdoUyw0<8Gc3OeLTju$Mu*(t^-nI=2yDj8XLa`+)l-Op4Jr;I^u-8%*_Sp`F{TA^V z;T@Zx@UE38yk`xM5)N3p!uxhW;RB2MobaK|P&jCZ6+W_-#|VckQ=!z3DIB)8#|a{yv(2e*7JKzhvd-rS^(;r>YCEG)-{O8HG_X8{hIUTj8teBPp^+6RTx*`+ z35_jLp^0r!Xlfzn36Ykp5M`ScnpxN%gyxp2aGmW?h_;A73D?^Mg%(z#a6?wZfI*&? zS?Tfos%e)ND4x9wSpi-MioS8RYW&Stcr;7 zM{JeoVIjeYGKsOlh&bCUksW}j7lP@0f86xHq z#8!z>7E%jQCNZ`aBGooaWYj)JL3_$g#H9BElOWa<4@! zvr`hq67h`@xt7xyk=77#USfsCH9&Ei@jBBK%OS|WDXIf+t8vP#`TEM zR)}4e*a{KT0ROYCaH;pK>RZih%~gIL{;5=ZTfL}UzNV0*+d%WID)l?d#B_`>>i zKxDKb=LsSC<1I|*8t8aWKPKZ5@0vp zhDqs!$+``5F2IhxTI)z!rDI6w92IIUitMx?|G1U{-g>{25?B%0za>4D7+g zJ(kyliI++Q#v=T!Uo0ZyHpC{0K=Z^QV!9zl#v!WO28l9>+C32=mfRDO-5s$@qPm6k zLd5n!OzMRQwH*@W5{++1)U*k=Bl2Sr2PML+VLT!s4lz3(QOgcU1ouR=?v1EzGkPP6 zBu+@wv6g)hDZLO`eGn0LOd|YtM7O?(y0)kCQ*AJBFd5nBC`7-c1bk1 zusad4{SlMyL`2&TiE@d?gAgrj!XQNc0K`Fwmew!W-wypP(%;gAWJ&sZN>;h zk;Dm!LDq64A|)A-H4>3%#}J;otnIyo!L~?Yh@DbMvd*IjcUz9aP&-4g$PtV>FojW* zEiVO8DiN597;gPi5g8*9n=RQQty@-+bAx7B-i86`WqYE*c*G=|F&-Qic<9@^@iP`3vgov4d7&!?s*EUF$Nz{G_k!i^fA+jGp?2?#o zVUrQD6A_apBNo^WiE@d?X^4e3Aq|oLAmX6JB5Rn=@>*=^3QO#O!jl&DFd@rkC_H6{ z70g;rA!J*oLXI6%SZZyj5|-H_f)%Gx?b)eRn`@n?;h(N(5N8-?lv`F@Oed_dM1{4sfna4*skZhEsx7eO8Hns@0X6Kc83DI*RuMK65&H-w zCe5V8I@=*pF41@vV!ch6g~<0J4oYmWhL0i=rXyxQir8oeB!XukTF*vovKg}xMG_|@ z3a#ZFM9NG=)*QrUJ0=l63(;*ZVv8-Bizt>jE3wr&XCTrZMXb(1Y_l^Gk+Ts4GZ8y1 zFB4HJ5jYRA)B4RrWXwTqlGtUQ`G}agh>`OVyKRF+nMCc!5G9uU7$Q3Zu}fmFg)KnD zW+EmnK{x-CY0Vv80diY3lU9I?(z5NQh$tCt{-+8G2_CE}hW z9J4%y<91Hr3+tCf_|gg#PMGH@!dI54aMCsqtZXqA)ix?RWyyxfUV_*q@r{LLQ}A0$ zRXA-s6uz^F9K!cDLE#50Q8;4_mlA%obcLVn0KtNdidrw@7=Jd8JR)N)Vv|H;^SpqF$w!QQ0nyYpNR&y`UWbUXmzf z<`(uMBKA4Nq!$skWw3 zHe&;#NaBP<8*BM8BIQLy*2{>tc1$9CJ)+x2L_1rw5m790R-%J-eg%>C5@Pi$h>mtf zB60&_;3h<8%iDw~l?Z$l(bf9BipY2wu}Pwvc?uCR8xbQ55j|{!M43eGB1D`e7a_7= zLF|&~Wnr5Uv6~Q+HY4I~heWwVA=0)WR&PTL zwKEctuOkL-M$d}uu@$jNVx)QAM8v#-82Kh*lx>hGlc>EDk!s01 z5!u@iyCg58z+5w5+ zoru=E5tD4jZbXs935m(pvKW!_79y({k#5H%!gnFMl^~|rq7p>0#94`H)_D&i?QO*B zJqWLzk%-)l7`PWP!}9hbN+kmKA!b>>eTa-=#3qT^=Gl*kDM5_fkCnN^G!(pCS@U5wkx< zY_tOs!G{s8k03VLj3bC5i4ziq*77q%%EyST&k&pKm_+y|ob`-Z7jT=^{H@yMw)|*7 z1C9Y(j|R;0ykMO_52)+ODopx3ppGYSF|SUB6t=7#*s98s8^#9MOJ4+h8CcHmkkl&7 z`!%42r$X*K5fB{Mn;(A-mE7hDnVgb3JcWnXrxoU%WU8Lyg{R8`7J5Q^^6)=@u6z3K zD1IVHWtUv2pux8R!L^&-Q1O?=r|a#2Nn=t+5d21P@jl>W$d@fD-r<~Xg~tX5*1s@L z-|v`ca$9~KqA>aMfa;!*9vv#)dY#^{aKiTi9X+1Dg&Ted_|p@*uUEw%1fPDK*hmxg zUZ|kcKd9jH{`@t5qk=-sNl5bW@ehn+x+4q2&IU~Jcn%jX_&K0vfTyT1_t${Ep1?Er ztN4zi0i#qraO?y8srjJ%=6@}5BKqgc%fmRg_EBxF)@bc;v9s7IDz~DMxy;1Rl0Tul+ za^gLcQpb$&>%GmdaD7tXL67|q78o4(&Tjg((yUFdg*^S9TOb!Yx!Qdbqtlbej`4e` zwD9`cfnNnj^zHH=9sa<`iQ~sSG}5o4KUC8ekKM32Frdo5z{2n92Uhd=e^t$6+l~gz zu&E8H{K>oMY?X7X!0Xop2K&ExsmB)n9MCdwPMC)kq@rJZJrWv%V`Z8iQ^(^Msvcjc zSqyz};_&g~?j1h5b%f`_EOfoFbfz|8D&Jg9*$eY!!I6Ot0^2wC(Ep2m4ro!mDM|J4 ziN>D76~6`~R*V)ga%^(y7#3jfW*+@0&2+tUvoFx&S6j6Vbql^ZZT*iPY!Jn1_oR-q&Nl|8s$k)DHw4~($rWjjGQ$k2 zynw%*Wslz)cs%r_(u!X&nXVV1F0*6j0@iC?3Aiwwf1Q1{vz`8`Ht?0D^wvLnaD<%Q z-<^hjmq!n1gFIQWL53zzeDHzPaijcJza(F=CD`cTroI z#v30!X5vy@*o9&fws`cDMAK&z`2nwr74H5AMn3pp>c|ILzs};R==jhwl5FeKt8Df7 zU9FaUaG@pt;;r+y0bkg^cJ0~=-TxnJY(;(5CXc-5{^a4K{pK9Bclrf>SLGjv3*YD; z*v!BBwI@A#jeYv{Cq0ENY6l(*R4;_&1XQV5?iKrjy`C!V3kzRN3jEB&N!x;iqM?CxtB*^+f2@}3xTq>N^z(onTE$yl*m5iX z&pJE(-wdi;?Eh?Xzh+8vdqh|IhZ>|LoN7 zxG-CqU)TSrMs~;xu^Xv46wcZ%ivEmP6{qg68kNhwEFr$+0SF?Tsv-<6BN#WL3w%fEq> z9Af=+zN}9}=jxJZ4_N%bMxxyT^*H_;m) zuXi%ajoc8|!ntP7U4v`oTyy6d;cj;BI_Iv%wQ(*Qr(S3bG0wGc<(g>zHl(X+AfM50+!)eR+%(2G$2Tdj z-*rxob@CPpR&{QibM0`Sy9tkXPMg<5^tm?4`<>(KnBQ=mHpvOj>6eH)f<6y8r+k+R zL;p{7vJ>LA3hC$fAWk#T8M-_7kSnJVpVL(NOm?m->Ge2m<7sYO)fL0S<|N(kVOOpj z>07h{@R=f~<=-9Jxq?$&!5+9yZjPrp7mIt*xkqqnU>tB7Uh%u0(_OiqxMeg(`-~aR z^&%ZaIqfrMI(Iv1?;JNLvz&}aR>x`Od(^q!q-(lzvvFF}`@k5Sb}kt>t#EzedROi- zH*P;%OXn6i*B^J|1?Tm9+{ppRKv!@fP7}EUesO!Q#je~yTq6zSqrU*F4!IMiyK+xD zHwZVwxh&@raF^n=Q+uky(f@pH_6u_b%@w>0cbRk9&JD(0?p%&@LvXd7Tk2dA?h5CY zId?ZM+_~k>4aHsMbu!n;(R~nz23QTxMr^0OE}Hlc+k(M>+`ZJcR%UL+U4=t=-dR- zY0kak#(e;nURB#4pI2SMiKHKK1q+>f5T`%Z)RWAYu;Gbt8Z1MxKki%8h)`jhun2MOsI*L#|vV>917@ zpHk=Mk#5h_bl^Jd#+^_42DO0CC%*H)$H=rq>S*?<8+ifg+OFUc=N`u$r!hJpedgRk z(qDl-N1c0u^u`LqfeXh!ewxkC?-kGi=_^-mvASO?ojxa#n%)x7ilfgdSMW*FT1WKx z+Lg;99a~}i{JwSNo+97Vxzo-W`99DYzQbuTWy5PVSx35%ID=Hr<$!vjEAeMH@>0^i zzs>cFbIWiyutjtumOHncbP+wH2Ay**m-K7S{p#G)INuLI{^mRXTY-F>*68!QE4Y$$ z9b9kXdFS#-Pr&sd{^8s+q#tnO{^{H*+(cJSr$4m8?^*EuT&D-8URbU5ZvX{#f8lMp7o+rJMU2vk7 ze>EpxAiWH!gHLtm){%Y^r-M%o=UycJ1WpH^Q0LZ@UVziV=Mv{$BCTKH)xoExa~nwO zr+bGIFU5J)H7|pH*LN5(%oWtyI|i3byv#YRy+d*O)N*bU=`Of?h?hI}D(RhW-(MTY zD&V5vpigU!DZoQ5^r{H59z0zYvbHroY$O;adIDWCQke8w$AM*&DG`#cZ+lH z;D+P$X@}DszYF&|*8!(R@*bo**BPfqAAr#poY(I*C*Mc*Le3@jz-jmgaJw5h*0~RH z@vdAPP7^)|yz%vZIW4x@>?1OW@fD6fFD3IWyFVR3db@&$Nyk$2JYpZ` zJ|_JgEQG!|P52Yg%BlT*e^>5P(pov?1~_+wv{p{JJKVURk>09hE_tUbc$Bo(PJISB z_c`h9pihEx$4I{k8aL5Zc%1YzpwD1e?hDe|DJ_N}ICcM*pk1?$Gv1-D;0ZDhgAO#q zocoIORM02cxs#-|Gt$9Fe|%p(Tn34tgU@iBCUOe4v3Jyg=3ZCsYtq|s`i#2Zynf%1 zi9tR~OhKw!zl9FY-RDL=jcY^A&k;wva#|dHFS&479qY<{kJInXYbP|$xgSXDd_g;* z@y?wgy;r{*&tAyyekXqhpke{~-Oaa}%9Ai__oV)3Sfixt~d| zb33U?&i#UW(Up71>ts3dR##AGujX~2R5c`TZi4VJSzmXomz@5Y? z&izjM4voNPs&nT_4|L_GIp_TYImk(!`D(&{LV_Dv4Xj8qpeGZ4i(xZdIS)=-iFRN! zovVVI47+i&obxCDuyc<(7eIch&i_trj>JN`9Pkk2`k> z`SH#zbgm}(B+w4*3Fj^)-ygKITI5`q=IjdAOzoK#J9!ySdF?coI9Cg|i2QMQ5~oe< za@-?Ceaw~9Df$k!9BpdZ&gm4r2TogBj&pTzT6A7*R!g1KdAoLx+FX`7r}Os5+?u}J zInLYtHnFm5P0qz>{m^Oq#%d(^tZ?OY+I~Cbv~sU>t{yIcm0BxzURBOR$op~6)$euC ziua5wSfBhV(4tx8Tm#bTEj9F6=Ngg@qC!=?+PVARpb($nBlgjrpe9znCn&*tD|68i z^ry|%!HcjSf^bzK7($>LTnb^JtKhZZa;ObgKphB&2>6sO;t1%<_)*Z+@Bjz|T?JnU zFTsYYexv=?kXQ@(;AH_$2VL#f)$LjED9i?3+18cp49J9eFdrU+1@Jg5gePDTEQTfU zBxJ!;{`T44p!;}OW8l7^7Oi`s27|6!4+CARt_5}AW;&@Y+yd>OJ#>KUENp*J{lKOq z>sjmlK}|hZSknHWI^NFA^hb2sAt;5zpo`kNhW!d`f>%M;uZv(ayarp~b=V4TfUae4 zhaI46*gHYjt#zIHZP*RPPy%~kFYLR7YiavQ)TQNDK|Q$I-yV7==<1fb9^D7}LO30i|Lk?WGUE`hg&TcBMC9oXKY-VLf7(v|#e z(9PoC4Qk;%#tQredwgJ+prsop#=88Uf2gO z!$#NyuY&epI(X{9sNxSG6l=O%zCiyFn*Jf%H{X2nO|AHQ2^!<965yZ^15j z8+0+f#9DqF)FSL7l82xa4#UUhIS^FKdz9qoa14&a7w{!q#g3~UTn+U>w_&b!jlH{B^`=2xC311wYaJv!KiLzr%UZ{<#Y96oX$N1nH+}b$vb<<}uN9(6#xg zFby67FHDCSFakzGA`F2f=mH-y{z3Q%O5q562D+rLOZdO?1z{E7KZB$2IUIxIa4U3#&d>$A!fntUy0IVlm|e~%@F^UDH5^OV)}#*$Na#+$ z^Y9{UfScH!Tf@!J2HJwI_}@Zf+d+G{6*@vE=nP$89lQwZ;bqteE2wWJKI?RAs@F>g%UD%%s8ITF{U_LwsH6Ro&0bSU?6vE&#h=OL&9Ik_C&_(?g za0BRKzAoMeYQksejvwJC_y_z9x?q14K8IuQ1$+r7paqNK251R4!c7nfQP2#U!*y^C z=pz5MP#>l!}8h-`yhe*F7{p9 zU=-WK6C6Ai!D3hfPeK+LWJ3-tg=MfDa^Y!M0V`n<^*qbIc{R)>p8=UQSpM}`v95;S z*x24+;5OI}JK#;2Nx@n0D9naA@G^cQyaJox4aVOF+hGUng12EetN~rHUjbubJWPNG z;K5}6n*{0bB8Bd8rHyC$cF;BlDZ?H4(MjVH7t?FP!IGSPv74j z0)6e$*Q|8VS1Wy$;;WO_Pv4dF)up*AGZp>kM9J=hwisz|)g{O(7D#reD5+Z{ak22j9a9 z_z(`lXeK@u*0CQhB40zhJbeSxH?B|yTmm)0!vWAAYQm+U=T5F<>5gQ?YUK4q$0fjX zhkmy~H|P#QxSuJjXIvsF9|gLFa0+f@g00{tXbm?*8+{qp)fQb{X%8KsBXok!&;`1} zZO{$6Lo;X&0UX~Vh*!cbOsE6g3LT*n=$=F?s0+I7Pz_$7A?x5pSPxeAQBaI`4nw|x zFX1aV3G3iRSPvWE5~kJw^c%{5!g)9jU%;1e0=|NiPzI;qYxoAfh12jId=L61>i6LT zNc3kVI7Z*yPi6v4ga@GqoS?tIf(@W+${XQ1m&yl4H zE7?ZkEAT25g6>0HU!9ep1&KHs@ha$Mg1-ANCN6;|Aqy6O?kQXa_25d-eS%cD4~D}q z7!3aK9eek$K$q%ch;5-A`@D+RglTZe4g06sXgY7U3 z?t$Sj0!Bd!jDc}59wxv8FcBVvNuURlJWvJBu=73(-k;RE;(4#Hco8+2v- zSy&Cvu!DGwebO*UhT-rIJ@_6RfDhn9_y`U`DIA86;S=~2jzJmqd=tX*|CYpNRSNkG zD1aAW9pu3(co+<_AqSSiGFZnrFTxtANkcA!T2LD@89oo@!$LSinIXha81_4yhx)o? zt}7ckFdb%sE;Rkgz)%Y7a{WI@f5ym1VFGMp@mxmKwV@isP`CsFAqf2894p*?4F8q* z8|0Awn5awm``|HH2(z?boCh~jX&0id=XZnd5DhKh251S_z^9Zu0=inRtK+&FJ{ojk zyEj}9&0!oX-g+kb68yxTDG=@@JqltO-UIaHomY?AUB@=w489?K8pcpqPr>NI$9B+z zOL|bL6|{z%p$)W$4$u*LKrF;TPv`}=LvQE<1K~~>1PPD`cj=bhU=l+h3GRlu(33^* zDe(&!M#FkRXXpwy!;kP2d<{FHDm$m&nd6_~9Q*@*f-~@g^33)3@CIy$9qA%kA^YO5AFb6Up9CG8oJmPAHzP_tdqZ75>LY_SPg4nEfm00kPSJo6lTCINC#auo(kWyWgdg$Pzs0P5a@F8 zE3ln)_$|nRrLYW^LoQ6L#zycUiAnGfOolW_SDKO2ARVT{Xt)c8fG#nA#RhT`eqp6A zhZ5KWdtnz8F{}agTnkO18C(a~LlVR8hM`{m8wN*^pTSXRz+xQ^dcv(H)S|Kws0N>~ zA?SMaouEt0@#JHn2XulC&<1XVhgi{prs{Zg>Qy z!%Db~LT|%v&=q-I%iImc@D>b%5%36%h0Zi|3hTjCm|jyth6Yrk2RNIajIFk~WjMTn(4PIa+)dT+0ZJArivj3aACw(txW$ zH+CjL2yQs>6uqtocl1oohj0*z;W>C7UV;sl^kq9y|l9;8|D=YhW$p!x$J3DUb@fbgv8Gy5i17Z!VS50NvfX9Db*B1L@dy z%#)sc(ao$Ip#}`VzeMaoJkH3I=v?s%o%=jI1ZmI*^lXT4+z#}K9`_i+xLp}fk9oWX zo1m`_$GSwH%?S0OHt4a92)G~gxW+_Cg8>i+tw2vsG=Zj2&Vp)6Lpl=MLp4|l%Rmo5 zJOO&RK@T&efu2p-{g zv*0N(xC>^2?xqigdtd~Nf)uzDdO~=s)>O%5Ds53{5u-4nQC`_6%^)u zlZ@_ou7m=}g@<7Z42DD)00ZGp*h06z4qM?3*aoUxKeMOhyoCv9`L4n*1T9*vE9pd? z?(2@N?$F)}-E=LC|0+-CcbFboX>4 zsDWziA37X9%5w6j^-nQ(x<&X=ympE31#)0kws*)ra1gfY%v;y6@Dy4B+Y0eOJulX>@`Btt2q~C^} zunDwC7eVDk_cZyrFdgm&4fFbz>rNJf?*Da%9&VBN=GdRER)t!z-l03Syyp|=!Zy0| zHFYWImYr_V1;Sx^>~6~Wx;l}x){WQUc7|zYm|@Nd5x#Eob)zrJ>sDUnywai$jnN{j zJco10ze(dYQB6QS_c)Dwm4+3Po%~==!5t{1{W$nS{VV0imMt;h<@-V!+wMZ zp%I*cD;T61(D)knJp2y)8p_q>L05WxekFA-M3sht zuky=DM}W?rbg@rYR(y@nmao{@It5t`)U)g_VAa0$HHLNPFkC=v0ZWCn?Sb zty2q$U5F~%iTH@MJR4NYrv4UGBgE%B+oIosM(NLt{ub0VOjfneg^IB({`a8f{56ie z^Fej&z2Aa@8^=*Z6VSIi9mg`kSJ@+^GvGq`tV^q2Ui)v0+xg#vT2O2B`Jnn?z7ncP z)9qqO=Yv|*R-ONB25Hq@QLi`jUnXtQe{ic>JGnV_?6;sgSO3dUwC1Nj@j@@yXTJqq z`Ij2j{Snm2)5#9}5j2HAFc$wOx&HhaSh~f{_z!;u4GYszjL5Punv~NZ47&g_=L8! zED}$`5?BNa;c-|16sXu0s!^KQV)6z#Fo%3LaVcm9DmPq>RYNsSE^#@mfTv+4JOj@{ zEuG4*C7}t`qE#;t)zasn0Cdu|f%uZ6khmF&;8jqsy#n&eE2_t~!#3CfZ^BMc9aWgd zYU)(4>uG|9dLE+z=w{Q^a22SaE;omh)*R@8i7TKsXjgDKu@+ng#pG4lStx-p+@(-T z`UB8qYF)0@W$TZ~zX!iT4f55&`xBXWpcPu)y z!rZ-!J>dEYqlhxIK&-E<5zu}|Q0I0~PE=I&F_Ff~Z1yrRZY z+Sgz;PCaoL{%(eR!%r|=<5es&FA23)EmQY@1wVl%@FSdoAK-iV4%E%xf*N`fG@)FtCi*-pl;K0*YeW)gqy!J1R0RaPsMT;;qkt@<_G zH=brfX*G~u<)W?=sz{Z1SaE#IPz_U~e3fZ2sDaA+8lZ|ao>o*nAK{yjyjIA{d95?* zQ7uvp->c=X5q*WUcK^GyHZ2WPLp4{Ls4CHxp)Enfl>TcO&ArO0M>PZ5nzVIjMnkmy z*PsFVDyt_1eIxt2%r~*hd8M`UhyZPB+AsLVQ^T$#@0+R0;>~&wR*!D9 zgH~`oXrz{~mc^lG23mlg8PG^KfhMeps8Tsi>=u_-V>N*bm0u{EMm;)*)i8}ShV(dy zQC`bm18%M~K!sF^Z*IDh*6HAF#8~JKZ;|gojC1+1#6GYN{{r+Py@G}vC*7O$eWc@w zw}Vc;wWSPGUfrxY7(`|ajE4Il74C&37zzF0PDq5IFc=bGAoPd6V0n$JhI>`=4j2Hc zRby%BSel}|YPbtjPF_<`nIYgi1D!{!?k2A$YPia3yb&-Q?g7=23@K1)F1&uD5W1Q1 z0C7C%28Sx1K)m0jClM!t8l(fiqRQ(ULjma7p2?uI+I-?#SOcr!Sy%Astkym{KYIh)eS~%BoeZGzO;s^oW@@U2D(V}6!A%azpE=A=v+!B2eRQ4I`kgMCB5A8 zs#k5{EhG6Ad)?$ zBfJDfcDPOsFX%Hhn|e) z5PeVdJwW;lPW!pW>Lt>zfOZnv*1saHRp*44#0uyDS}&Twm!yw_dgB;z6MPQC82Kns zUn`CfKZZl_5sYST4iaAjJy)l@3?D#S($NgtPkJBhg(l?p5bq===^@2)Oz0g74nY)= z9}A;M7ZO{+tFRuf#l1*;0iK8Fpe5V@*F$r-0(^_o-_F#mTG!hMbqzFx2B60uuZDV{ z#~|y%l@I~pP{*OPoQA24%4!^q+YEHQK=Yz!D?1Tyg6l|Ye&yvzd;MA=^R;X+>ATzjb)URQ0#)oQ zH-@z4ZZzBn_keFMhmyVK;HZ-gZBl`GO&_&VJEY@#}W)&X@Bd2Km5$i6~)BW!?|;ANLDA-)N(Y58v0Xk+>A-a{-`_G+)KT>lc)PNN(YK~N?D)Mb!p$s2N zUK6cFdJ=IMTt@mq;-$oza0!IDwCd5EUjgbl)i+hke>#a2xEE4Em1|9oAcn(LP!}dL z=T{Q-Z9$E^nzUj=;x(W-Q-d0iu3u@`V4Q}v29;OdH(sD#XK4gJsq@bh6wvZs4s#)j z;Z0#bX`T70+Z&Tt*T+Fi&?0PtQ%`BL6v}Htw-bAT=J-ZpPte(952Cj_|J??ipaaA} zGthp2JZR$Tu1I2gq9(4!X+k&Qnu8jxtK_Xn-vBM(I+#Kk^{5)7iBALdbTq^6B{Ixg zQE@vm*CSQXx2!dXu29A4mxie_Upe30w4~fr{9h{m=Qt|&cf&P9?X>)Tt^0QsX{BpJ z;hUi~WRmxFtyU^cz_(&n_L>pj{@*v;x8kZNHPcPB{I!h!T9KR@)|TOlzKXS3X@c$X zT9#U|DmTt;R(`iIY&gU2hIXzFEjBGm)psjSb$UDUpKlp!8FnVG^*{wGkI;p@M$oeE zN~~x-QH{}}Qg^DnD(^;GWi?EV)XFBWnNqL#W`befe>(@UjHo%#gyLLfs-!>Ze$W^C zKyT1C?%U=ElGarQZACw`snXtx?@06Ty2O-0oC%MDH<0D8Z)A^?4_Er2m z72QWZ6;i<02z~wZl^=&63z~=;pkcmp$}6oI@C{c!9UcVl1N^5#6NwMOB=EIX1=CJom1W-rH0)_yH`2Q5c@5-}Uk;04Av~_---<4NjEpX_=rWr2{R_x{ z$cXBh${#sjIbE1}f^xdl>bs<~jQr>L|G1>XxgRbEvccdf$bu(fNdW&Xwhr~H*72%t zO{4LW<^EfnlWe0Vh)%S z2F&RoW{)CfOALrPBj%j-7>_yU1ZKr>%;EpmbPo#%UeEpA@AG-uVYc3`uCA`G?yl~c z;a>MQu4j>sAgS&inGWOcLr6R(9l&)z(mEur{61XwB5}xU53U=Kb|Xb2twmacq+z$> z@6Fs23|f!G(Qht128km@n{eeV+>P?e&3puD3+|WWx(inpYA3GSkai$#N75qc8<6)P z?pZJvf-7?h=_Jx|q@zfGv3(swI)QW=iHUsAz%xjk=^_&UehKLU(s?A-$T=jYGhV|n zooRfZq@Y9?{+)4GkgBqpIdN^dHhkW04gS7{#8Hz+xZXkHb5^|LeiMJ+K;q_N$95an zTQn=TA@7Jd{27b%5a|KZeWZIxcahj^*i6_oIK4p9?w{jtHg6NyvpKy+dWG~7NyG5H z_P1to%t!kT*YYZ3?FTOK0{|b9-XXn3;`{eV{~&2}?QeV_$S<$KS$&*D-u7J%#SDYW6CBZ?icLU@SCOoM~*n zY4F>(M86quZ;iwcS0w(O6~%d&o(_rg8gb=E%J|W-Oh_4#j{#0=JAQoxU>^Qtu%eX1b-=18lyg600lkFN8Xa6TIzjpki}>uccIH!nis)$M*H zZGy2J^kqm3k^Cd^-vXrhNb`{9AQ94Rq$SA2r{MWHmSITzh)Nx#sz@b~N+6k$e31D0 z7~U>(8S#VV;YViNk@%5%mVqCmSp``=a4nAHi!=@OsDf){B%U@@#I-b1DWoz;>>VrM z%8%HTMe;)`hs5+sNc~XPE2t;);>T_H(VLpwqAU+Tk5dh)IwuIguCy8wgE{fDI{cgt z|1C0#dsbR&{Kn7iaJ{3YBJsSA`ut8~Tw5WvL~4lC0I5C_;~CqQ3t}*vQd3+x1MfLC z!j(tICb$M5WI_ ze}C{Af;0$e0MbCD!AKgG^*tQ-tm`_~IIGG^Km`V(m^(io(+U_TDm2}t9S9)nL9t~?)@gsVU3 zjNwDU5XD+I7e7YOxkA{-T#P>#Aw?lAL0XEW1!K13?`=rI;KzHhtMT_Lq^9_r2SMJW zNHis>^ZbssKTah*)X?fP*?~OD#eM;4pLOp2=g4Gar?I?rI$ma#sAp&l}^UaOnE; z`@b!jd&|Yunnw{^{8AQIoEr`uUCvx?WNexjuGa373vW511w(tby4-3kWz-9LlG=nQ4n_r9=G8_3F4;zN?liWt1{A){-2*+Q9$8k}4z zp=nvjYgx*kIe6FV@3eX^cW-x32wI;i0)WqgwPajx;8;NCnoS%!DHo<&aG^8>0N?2V zWCLK*(6nc-mjcDYf5(w-<$?-j-zqbe7Ub2IJs^8 zh_rf7_mb{D0O-lK7-X=gBDlB|gbBfu|rm4rEJ{>EpJ!ULK#NunD;aZpl9bh4^Ol9vIT%o)36L%sJOeTHolnnStJ%E z_LFyyhk4T|3@@dM{5&0nxEmZ@6ja#(h%9eub}qj&Q5J*~ZEy!ePdbTE-UFD@`Md8@l+`0)UO`5BI8(g=?RhnMVRVbj>AYbidv^o_(k@ z85B=xE&E_Aqn_pb#K**xj&8)@}B^R`emBs5G@KD4wT2Sf#PO^l}jy0(fwL@zBRQCwhlzeK4=BiO6oCKWObPe97`B} zt?=<;y>|sj9CTiY764~@3;?(0+CA1=nmZp9j67gWx_6HBwY0%eaPr2d<8qY%-!=5A z4H^wPoKB5OK-3tTQp(^&Q6&t{TFvaJE!R3Hy+RI`VlKkTWH02VpMKZ$LKOifjp+%k zW1K&gD~U@W-NT)dq*$)UvXU^&8t5!+8U^)Bs}0@a_eRw;YK)lyy#)Zcf~C-gLK=MJ z@s9&WcWX4Cw{9Zcf`EFo$PB1kZ4vMPv8488P;^%+U*6zNF=e2LXgXO2LJh+A-JyZ8 zd4wH?I~DyLbJKc`qL-aYH-i=eK>lS7&dSR?)U~W(Gd>cVgEQUFE39sHhFGtw3o8#* z;KS9vpc7n`7kMeD9DI^>KB3<%HID^my19R)qW>bZs@%Ka3y%gr2-B0w_(2bw=w5oG zlkz@4_3=aQfdxd5uQDyi9=Eb{X3IW;)+t0w{7|hLl%YJtNsEre{(kEFWl=kGq#4EH znAKjd*1HbAyE+-w7e-nKhu5=IKw%BG6|P`Nqt_RuVij>)fvQ&o%ht3WcfQ?<2;01P zF;|NgtI}n%W6z-4@Muujea?Q_xzFMIX~)~?hu{}3em;FeR+p~1#jUN=5q$2j7)Q4(Uf;7=I0v8usci|CxZ&`YAx&Qrt0B5u}W`FcI;4b5fi!r=qjImj# zZexPbLh$@h0wyu~%HS762b$w@fqW|)yp@N=se5HZXXQn4I$zlknowKnSp{9{KJsyw zD&2W?zZnJFbVq5@gTM5k#Z?Sm_&VsNDiF&Fy^Ou>fvjczY3o1zq-xY^-`h#SRZ-kY zDpwV?kax9d5`J`Pi!sz0xmJAHpKa2rK2gZ!Bi!B)+Q)$ZSefyZsT#PiqxE%hxkwdp zr@Zl@yLDg?>3ziz_qs;INxh$VRaEpI?!G(%=yKB(&Q=m1y~ibhig9kC7|=3gR^h_(xM$M*R8hV*_3-e(mc?N9r6o1EA;yIt9}mg+dwHP zdSP?QQ47*^qyzQgReDncz8gk8aB&$Ae(bE4&5nF->Fxp!laSky0(QRe!mJegS*etz#xh+H^bh{3KD*%x( zGi~c_Uf13N_zVi%PUy@_S9>;YKjEE)lH5FO^ri|zoYW~&p40)t41O{_3va4%hRmf3X=8ZW8NrQy501uiEPJb9x>Z2nT^>?}CZ zp)}ELDwX5`aPgbhzPHr(>skXeq73{jBalV0mhnOq;Sx zUL)Ja=*b@|Q*>ht+~aA{5~CfxYHqNS28Hj9Ga@m?g8`5O)Otzz@iuPagnugY zoQ7wr@KXD!Tto1_006tO702tASfHD|NoED$0R;n~ysbuI4be!OXjwz}LRB(#S|u1H zQ-$Vqf6>B!Qd@4?q$*6BPd}TABx`vFc^@bTUAu})cMp1KwEBLIro3V^E9TMdqOdy%6J15~>1 zD!wWW&iAQK6X0Id5yQ~h-kzI-UUrBCP7Lt6i!>4dj5721B6Fz{IO&g4>m5cXmnl+; zZ$bK)K6$EKM`k%YzDm;=U{has==Q<+%2mjarKBo_I=gC14Vq#!>PHKkg8N9ajeybH-qRM8;Tw^I;PVZw^H?XfH@ln2DV;Q zCl+JZZU(QUR~PViJpUa<88EvrZEGxyCd-3a1@d?_y`^U4g`G|m zi!uEd28EADw}1+!(Hg#7qJ86V*+h46$Fs-NqgCtW$s|5+U2#0>QmrM%;RqVm62QfD z3qN6jy`JyZ(U%Fh9HKfCak)a@@T2mwIpv%LCf4NH3cqHh+I(@P$&(FE*%mh!)8ASj zj|W-3TGH4CZ85Et;fnqi^>#5jnc`ZAF?+D7+u3^)+VFya?Feq`RSSx2ZE#dRwV=H& zMn_Cc&$R~MO!SN|4z!-FEDz;s0}!ymq^d5p7QSB825=u#5^ z?$0LgtR?fG&9?vmOiH-8=ClX^eGmn=HDsrAZ45ckN?0R#D{(C-Ut5E}An^p5#?zwY-Eh-EfGEA_3W}gqw>1dUF-*noY?iIOo4`x%A>Y4H4qBxMymob zReBdFR1gsO{Kx01?cM_~!#wyNdppRqfr{W_O4DB8+=|!u`mv=QtEz;UFure3f$h{5 z7;zYjI`m%$n$ZrTLwCp8VWN7JzO*yc@qG^tT>D8KXKvVi{rDZR7U8MfgpQ)Wk2e&K ztg~d}3bi(vJ0svvf3$~daG)RUAwyIrq2MZ6VpjB@e0HKbEmNBdP2khP;AM8}EF43} zu&qtH-Hd#x)<#`rr&v#QM*1$0=666-Yiq_vbfyF9-JPCvfOG0cIXa>%kD(qN@ndaz z!IvhqkS`r*M@K`EbZff`@l~R?%cpKaQ~RCsTkmQcco#BoYy%x|B2BYF_yc2u(p?zl zSesTsL1XmuMUC0RWuo4l(7-tW;P}kyjF-RvsCRZMj0tMI5t-ZdMrpZ)yL1nx*lvc zbD5&wfL|O?>Q%~J&uHhWdbt<)jUyu+n;$wDnI+0f(O<G zRf5E*`|$Owm**W@1l%gSzY0*L(78~X%ruQz2GSb744@6K4Gw91bm4Rd_b#>ih?)h~ z?2sYPp8@RVxw#;U3UDB|t_HhwS^8qL2i$v-?JR?xqe?%H-+1Nqc;&<_moHmGt{v2` zD`GQu=|DgDg*W{urYkzeA^H)6g01KezPQl2NL)PW-eO#8kY_hs5Vq)r%Mhx#2$xwj zf`8mh3pe6&i4M#Kw=eyvPhW5|kUr8-q@2S5?DU}U2)H;FVwhu@&)vp1?rfviD4{?w zrQw6+J!ZCF-vkxIOb z{3iN=0+QLgXxjf;=%iXzTQNZ&qC#4O{YDEfpdG<*`w`T3mLYpCtka(i7emUbdO6O` zpMHK9YRsW$bd}G;sVsV=BCoPiT3Nqu94W}3(8$Spm-HIaGqj@O>`?|&gJrk~ucP^w zq>e?pAmK{%GpIMrP#;O>*8F6Rzu{PZgb0s$c%H|RkE4wFRU7bjH^VYWKgi(jsydP) zBg7a|Vvk|lE>pdus@~KkVHs*S0F$#X{l&29Y@gqDWZp)n?DW-WG6?CaAt!HZETZ)T z4Bjc=)~h(c+YK`0PJu4?NV@(ejTmTfDi#|m++kzS85=HNKNg`%?Zq22|4|U>AJySk ztzt?-B`q|mLr?0v{*V3s%_!Qd-nT7RaF>pOX_-1@DTJ4brzbv zB>KP`6qyli)qBpFEFly8$wq$(nR(dhJ68CRvh!OH$S`PJI_xk(+rCh^n9ftip$2D{ z~S|8$7CtD7jns%Xj-~86cfZ5c|>1b z+Oh2)i~V~y0)Xd6m;)d#o^=?e(Hp4kFgUnFj*vv0P6!hQC4KrDY~} zpGx=ssnP%}c>e6^p*uwDkkzHrcme2JYsbr?SBtFy0QS!QU?ANa28mab{cwY~@1Y4o zr~P)7ob+s`@wP?Sv!JL&bM*0KE&45FyAi95dy)_0Kjvvu(Uge#6BM4$bb?Y z21GQW0RYO!FbWERVP&2ye0+)B-JDlCXYQ(cZ!-tFchN!s#U_Q5Ufj|15VYf^W}<19 zoSZXcVfXXhP^XfJxI%3;DfbBEky#Tiy+=S*JLyI!Xn)ZjKwQpE5j8lN=J`^4=Pz5q zL$1c|(`x{fPg5vkCXk>DtUr0LnFviOMrWFFJsWiyv@29sT_Ld$0Fv z4jfk*@`Oj%h$b_jH2|3a@N>JL?fJ^jIRTLI0l`5C$UEX$MuM%_JJoNc$|KQ@Qa9bw zG;Ji-4C-#Dlgp<3Vq!Y2c#Fvewv;QS=+n;>%F0Hs_Fy{hO2tNjyE~oPf;&HI$9MIp z-WW*uQbMOhRPXx6Wp!~&RmBzz9UFzEzb7sF(}=*27gn0zIQLX?7!CbirrM)%H-I|f z&iu3GO2gF+giW()G++7bXq82$2H||d9MP}k8vPfQkn&3DWnF~ydRalC2p$-Vj=);l zK$*s)YVS}47n**y?}XvSI@W=M@KJY(b!sHQ+hmLop|$(*1#Y>O=zRkK1QmG#>k>gz z$7AI6iJ;ZvvBFg21m324$Y2AG^{Wrb6nVx!AP;j?_xIXHP|*nnM^j%QoPd~8qJ5c~ z=Zkp)fdvnDy-5)i2!weB5F9Ok6LJ0Esns1`s|a;KI|K?(eImAp)<5aJ@12EmErOzv z*Y`CL*ryD&zIJ>5q>lCjEr^VB1=qCUySANjxX}m{EKRsv5!5gh0)WT7a@ik*IocPp zQ2}bPRp*k&pAf7q5NymVi;kMu=$%Ue3!*F0r-K!8arL6@}|?*`W03wYOO^K$J|%~+6=CC=FP*WWh_~{gTltK zod&#Ja(iJSRff!?`6$*jL+1T*;X>7N!(w(?@~)akXMvCrBGVB7S#ibIug=||?1b_F zVidy6PA6hVolVzeVUxQLnt7x1`0PNG#1haK!^S*{LUc*_GLLpnGI%M*`ScR^E`{cc z9=x!?)ma7nx}8-ES0@u94yAa`r?O$lP_A$Cx7JQ(0hfI?w<2rM|mJ0 zcC%O|E$7o-0L@*2V4Ypvx9;M%>(4d=0kXSiDP0ICY~5wHjxF{ezK<;^W?s*E>cZz! z#>obMm$^Xjz|wZ{x|fa4cVDX_u;B96B@T<#K_KnMWJ8f=Hvnh5`FY);QOYe4!W_C6 zHoNB(Z)+Y2)Frj95hz?#U-vg&9ey;4;r#~a41-C>1=J=SDhpmfgToE}%9sW8SGb{# zX)1E?;Qe!HQx{N$DF#g^ z_2RsKK(N8zup4zLZb@9cg?psTn=#P-db&$b5X5ssi}AniOK3OpDv{ zWV@XFW`O6<5<3II?b*@0SpRN4JL!g)ZFWr`8x^4F^GZt5u)uHZ^x(jP*<*QkFgkf&kjL~KU;R#N7f(9{VaxI2uA3cOq{k0C_WgAaGj zyDKT^l);%=%{18L)s_rDH;P74=kOf4TQw8LwPh9E278x7tHjtG)~M;yIqM^^QLN4= zA%MUuD5dBuj4#Npw-13Bo#dj!ajjQ3|5-10dC zQ7A=o(J1HElE-YQ{?=NmHCrnxyH!b?(igwbiP?s%`3|ka$=?(iX_PIiW)t|7iza6> zxLGxbMg!5b0S5c@THjLKG%2z&MaX*9vkW99D`flIzX6FU~;5Q0&*RdiNKWOZa=ZF|G7Zd ze}acr)1kR!JSvcS1E4hDL=z*>$@^>)!{X2N?6Zl^Mi?B^Yw9Yvncg5r`ZAkEhmi|* zWyUB-q$-<1kD5>Wb+P@Pe$rzoCxBR!| z%3wmG@{-1Ll0`uN+xV6i`jAWM6s7$Q-dcks(?QZmiVVso4VolTlZ;Nu*zCWrqE;JG zZh|SupiPp3l`p?CjNh-a*4(nHN%dHJ8@aE7Nr-^lzpqb*q)OLG> zyZU9Q^*0*x_d1fa#*};sq6)m9BgdHJolIEVzwi9NCzc$auI;3Zn}2iE`(LZ%-?mrL8Uf7K;_!$|*IpY%(^{FM#068!uB$;Tz^naK{FqTK&~+oH?5=3;qf^J)F?OmQ{yij6*A+9etH{_BP` zB@d~I0JsX}dcd}60?5Db%x35Ip7~Q#= z=j`hTEQoQE;+D0Lw|>%tMHb53{WKqWP08n+L}iRLvGvCmz0*|WBu5-}<*PM>=r291ZvkBNZpmLJdGq<21% z-$E&Kj9zm2EhOTy*RCIv-!#=*5CcKsL%-Jj&vy(u{T5B8ah-XL%I=3|HUW_fh?4Qc z>SeYWf6jt9e~kJ7VSWchULZcUi3stTf8vJ);c#4VkE@v9_)q03hbZxrZ_=tZVvzl1MMbVzaMbw$T9$UTL>lg0fpnYPRFLDyEQh3 zH~;vs5GW(gk^f-`5e|e65S>Dv4C>qb9-n37O)LZwtqJGjoXn9D;1#s#?TIs8y8ytu zf1Y^O6qIbp`Y3wHw5IoiKd8)onE8QobQ;Xfmw@1i?MA0uwGU*=3(ur&UpxoJ36y&i zZuH+!_kN5yx)1M&xlq)V6Q5)iF1u} zxn6JL?K0jV0*~l(H0FrGO__UvVvZnEt2UF9(&!>RM@A+75?TL+Ct2oSre1&H;iJT@ znEwj-9fjzgS48FNtvfg9#T6%CFlWty`FT*-bFRtVZ1Lx@Kk~_9VGVb#P#E$mJ+IR8 zqlk<-k>_}vMSpSBkV`RKr_9GdgJ^}0L9|-esqry`t0|QhLOC<8(^Sr};ySH9h8W(4 zn{*rZ=AAc%ef20eb3*Win@3T;Je8R`W#cU>dK^4<-=e_d;CbX04dweYwMnf z z2{=cJ(x{Z9bn=9{WjJCy9zFbi!jRRk^?f|l52eQ8%BF1ZxS(iu61i5tGh1Il>{>Ky_a4||)YZBQ~zJW_<+9i9TeqpoUG1tJLE)2vla7UCxbpYyFM`5P6-7U!+xO8E{sO`Ui1^Tx_1y}5 znyVsI2XOBp_4ymR`0$WyZUU)`6*WJxa(tGnR@JfVuJsw4SlSDuDL)WAhFu?hz%l#j z*W94otI(jOV=3$+c=^YY$3t8LVrlp-Tsy{6h11A65INb|yy*Dw$@>Yz{MDT5k=3!W zH2*4)^J6LSfx$i(yvr(pc|gpPHu8R3FH@*22F`C^EUkQuW;`BCeq7wWSh6{T>(f{Y zy^T}nX)hQYtbqF`mfGNVvl1tY?KyXM?rH7z9t0=$ekj8h6joe@Ywu;@aw-moS0YPE|#g;NYF8;Ec+j=lqFz6%8RfIbz5uh~DMOlFCIWqt(385G9?k8QCx zpxU;yCHY{0Y-@Wf$d#zMofX?Sa=ieC$xKkomdLI?J`sL9e%pkp5yn`4$bma47&d=O zyO~F_B4u96b5$vI41g2lR42(LVV3z_$PEdY3d2p744oqt&c8w z%WIug%Q5T~ow_0v#QsNV^l&yDfhk3ortS@E&P8gz6g0mN*25jl>&Mm{b*(1uwA3;7 zm9W`kRra0PHpQ>5)EsKBc^u6WE63AW6l`uBFUA*BwUGlKUsZBRUeIKFP#jQ-`<&c` zpH|G{E-7fqA@P*+8XQumIBJNiDfJc&kEf_>sHN-|bpD#b#~cU7EPm99E;cWArfmzx zJfMJayrg_@J*DZh^VP0M3dEQ0T636ee_C^C*F?>h=+EDyHhM@EZlu}_+*@qlQT;op z2|7(UBafpE*!L{XJZHy%$-62@?RenjE7G-zmZf($&@F`cKiiM^J)sLKnw4n33mypr z3v8jnV@wpp*d=WzftoZ^swU;3)5a-en2OO-a{%OVJ3-Whp@}sudXASiW@_bXmYLxr z-F+hZcrurwXj;>TE|dtjpt^fmHR%-;O>0^u5@|wpB%0%a-_jgT&|Qez;PdQQFckKn z_9#s(EeA+q+|j&}T6_v9;ZxPYIF(+3!ZW>~uzS#1BJQxZ{rAuvlBrYC##F6JT9Ju< zPptj~yqv^HADUQM9UoHB2QWT=7$1+p`RnA4I$Zu{Hr4o4pDx7u)f^y|ksZBHHcl?| z{nV!rT4nx&-}2I0DZx6W!o;wmwYr$HIO)})Uve zO57_FWk`@e?mh*@LHAZaM6jiM*Xkb!-7O=WuR66Aa~Vfdiqm2oIoCAIPG5C5Ia2oe z+>qUV=xF3Kyyv6DBaFhot#qx@QrSGOOl7FVeC;>6dM7lcoYy9sWN_d64^vb{v@=$y zI=wols_3qC4eIj2q#CX|?~$`U&9`XX3q4NSifI_eHlC8|&elKtt@;CPN{Z5j%_R3v zsXD-f3Z;=+c931|-!u%*9ez`RKlO!#a?Kbv)Cu|KPdTqo4UObeONeCfiw1Xgj5elu%VZ?k{A>}y^Fd(zKhHH?9DwvFRIDK3KpG>0q z&oRn($LCR4cNwQwZEQQ{YIV!Gp-@RF_m{CyBjeF67Ru~26p6eF__@A-;hXVk6<$;2 zKeD`0L6`1nHCapHn-uh1A`?p1aHddo2QGz}p&` z;{37ai&eGr;5kE~)>1lW1H~HpdCi9|-SdzA?Pv>fq=7QOLTIB9ReXi>Cpg<5IRP20jl$6XGG+U|;7Jxo ziv*5Fnu-jj)MZ?qj&8qV#Y?pc#bi&bsw_S?nmmJns>S1I9<-z0w@x$r4O%F5g~)1K z`{J2QHv@f*x3JeAr{b?Mr%<0Va8fLa_R?BNuRc(jJuSus_1Qb`WaTQ7V{)7Wro*!R zej!@2=I5P;vu~gur_7>p_JqBwX7_5yUlzmcotbjJMWk*d5F8pg=~U9?<5l;3d3a^x z&A}P*%z<(;GX()?YLG>*&SX2ZN;|pZ_J@3)4(oS5+deWgMFF8~%|bih8ocTye|uY= zRNvladCuV&xU&_-wZ8V?bn#6YAO<`*FR;`%aiw$woFg9S0lI9|`yEuDmi~Na=pv^w zWcbIh!=$s-t0Vl!$=L_n9UoK%B72E<(WJAboByCvS%KiSe6EtC>t5WTzZzzjP+ID{GzV;HIM?N@EG>B2p}qx*b{S&Xr4hCDQnqHL zhaX@+sn;6voJOtn;Z*x0juHwbcxmclahYsRUBRidK59p%Pw1G~e~1Z!FB|pA;PZ=? zk}V`fa5TNN7k(>ETc@`dv#c$rwz?X?{boYhjR8ZRGRHWxbl6_L80_@FTF6kNcS-ED9| z>optiM|$yDVzqne?t{X{F`@mzb3671=q;2tE>!dzN>NOr_jG$)JJP+*DNiK01gQ;BHUb#LsSPi~Z^6{a%mko-W~-9d9JTlP=9899YdZT^^5Kk`^bF;V#u@6*uOg3gAJP z^+q3!q3J)u&8Z_&r--8LX^CRYp=jk^SB%akm;6GF!Q%=>9M#?3BNWW%`U~bKn>0o* z#XCP$Nn`YP@x`a-c-}M2ZR_rK4}AIyv*3Q+oz|qm(?t{MNE)NJd>??~S&;0~8mlC4 z+5g3T8!O{(95-^Y#*0!GxCx&U(qVitx15*6e>AX}dL4P^1Vww>Az=HubZ!W;SwG;>v%&#I%nGtLpcI zs=6K6wgbwPg?+WA-3Cm)Ql46IUNH}HOl9VWz0%*D`7tld{Q4S=p)PuSF^3o9n~u$U zR(wX=oa(#EcoK;t==`oSC4EsUl+Nfco&dBnsK?_M7Nx1_@F>+LT9wW?PrkN{+N3x7 zxHRc1V#5vpyd0ggS*`S_ANoH3^Q3in&x*_7Vxk-LwdxV~xWd$rY$NK^eLD4#GH=sV z94W{OiEiKCw_uIeo>II&?pX?5OSc$D88R3h%_$vN5Qx8I-UG4IR%Uak{=t&>DvmO6 z-joh3h`isBmwoEuRUzG98uOpGRwha@Ip8>&cIq`u7!CfzpBV=-@@+2~P;89Cn2OMR{1nwyvSP*$} zV4-0qqrZ~fOi@58xy%$a3U(phifV->D+~&LG5`F~udiA7ib}Y$&PIA(>Tg$MMl-xc z7M9G@+a_izn;GQ?0l}f%E2FY+NOz%VS_@*dnfd@>o(BYnGp~L5@K^d555`&$n?T_W zyY(YRJkFZ#)f@}u6!MNX+UG(+u>kRYOO0{5T18hhgZeZPewgV^W}}yjQwcE>*t=}g zks9kZ*HB;c=)>m(#?$N3@Msa#FN@LJ)TFdPS&VMV z!_xFJi&1;p$duxnbku0_gECab#^{z;dzDHG?{(pk2&~ojx;W_-TElNw5pQtOCwbQj zWwkZdbvag6^gP|Dy(^DZ>x(ssx*3bEF5W0*`q|s5kCOm!zAVMqqA6~J1$(bGlNOB| zmFYuAu;6J05W@dCDr?KpcUz+u9wPS4YV2aFRbFJ+67kth_kDN4JYvv;OK4V}HfM!j z*+Tkk#xCXs0P^UV@$&5c7d&p=0cUk*6)(02g?(_>Ps2mgmfwQazZP)cSe_!Yq51d9 zLL47|cdtEV09Q=D&gB#+Hpshs@xCInXT+8Th1c&Gkyeza=g4cyS3%7FOdrOcZ)Ml&8x;DpDT2lDFa{ zqn!aQ56x8|KYMVAML9e)o_b&^(KtkFAjM8@C*Q~NrPQmyJ1f%aTQ?dEJp4c+dLeNere>J zvCNS*guu0D`PH=D-#tua1#?Hcfx>oDV_C20tGTTC3<}zUdB}HVr7)WF2+Kp-2IOT+ znp2>qn|=78ZOF?uhL+r2gEE?+hNBX3FLuG`mj_FRtL3WQQHBD{=Ye2F4W870Pqycu zCj%i5=cK-_x?O|9!QCbG7t4vq>lLv8b2L3gA*?9V$IuL$_pKeN3aYlo#~NhAVKcds zGFjIY2L9B#=$`JkAFfaZR*~6jQvG)590h=|1+Vn=PKKU{x@87}y(?;60+g(vIIpib zKTD=QpT$eCp<|sEoU2)rJT@a3=Zy(N%bIi-C7XL87yIkJ8_&c|EL9G)nPywO0gH?|HjkZ*L~mpt~X6AIl?NvZQV-GdqJK5{+euG-z2 zG(4xVvndrffX4HwMc)ctLaUW6_W6kG)uILCmg2p+K0|0Kp^Zu;=aLGmc)B2ysACK2NHcH{?RZ088F0 zwdgeGeF+4QPbQa7lVZKI;2rLoWY+jH01rK{n%$a}xz-j33&piInOq@RSs-|wacN`M zse9)=@x$N*n}q|_HLFeafiMpMf)Di7=(V)U_a13)Sr9WMW$S^(8_ii`msuzqYLhEs zai+6CxFT=vv{|C=G~RIEf{3q8&%s@h*IytdFLUapr7oeY!!K$n^`P`^NV{{x56P&Z zDQ_d8(+~brUU@#u%Lgpf;BzT_1VV&lWhBy621wS=?o#@r=Q~~dWI@WHt;RE1FgH{w z6+q#wp@Yf+)G9BuRVP4n{`fado6K1}Ef$=3#Ru2YJb-Rmj8#e#> zm>Kset>^X+pxZzyvVWS!AP4u)YUi#v`fT|U0iNuX;Lv6TP|9AE!1xkC(EuyKP3UX^ivS7t zN1BkS7=9O7EI93VE#If6bhg6Jd`!VjgmMbp`Gh z>%E%yIkY3kqQGf% z?HbpPLW?G;w`w~h+S6(LY?{$txXsvakD9*CcaUAaRIv>EN7@ar>1;u;LI)w5mme#x8WX1b%nzLYY}?-!9b0H_^3PR|@pRy^x2(JuF73 z$tG&-uW0vwbf>9Jkws+nO88yV^^5LQ!3?ct>OtQdLJ7Hhh*?y%HKT&Y5B}?j#cbSq zkcSsIwd_H?%HTSv2SxedstGBD`$gB(kXki0j@s`+7Dwf55Y_cg#Q9B68s!ZcHCey+ zq_2LUJM<#=lDKO8+eU`sjlZT&P2x_ys3`KAQg6Buz364~y7@r@tprVC ztwECCYdDRrNvPe+nwa+W5pJw*t%`1;bx+Q<7|+c<6mGWE(exHbUeFeJI?lAs)jER( znLe0qGEyr;mTfK&tk>m6j*|iNmP*L9j6SWc(5~X8z`n@mnwCLA3AJaFH37)0VO}Q zv)#_L&cS(QRSV_oK=Lb#yiS9JjWh}^*tz_RSY9(p=T#aM-i=>;koU}DBMJqJf@1`EZgzcr9^-tC^5w)>_quE}{IUG~s6jc!_O^(+TB(0rFSHkAP zwc*sKQX+4lbp<^8v6p|>ELEEw%3<{OA5@?+%1AnpD{@pd$KvB)ysWxhuD+>O`v!b| zg1Odt&|n8b=zCSv z;(9ZxUmY!b4W)B5=IOF1^V%DJ4!k?2o>$eyg-{G5bt5PeNX2Oc`B%d=8Ha@XDPtQY5Zu~6)J*F;cvQl9wE4?V7weOqklV0X;SG zKVvHH;IK=c8$N4e0Bi(G*_;k_;Xr)L(U4$6Z7EmEhwrVj-gw zNJJRMkzY-4nmmr0)kMzQ<0uUGi0$GFQ`u=(O=AhgV?4d9Y4kS@oFHaLGn%Z>*I1^9 zwTCb_5tM0F3y6ky#o7HP4Nlf=XS4Yz%99gC@#IMndR5EVTZx)PfwduC^du41XrJkL ztKF5WZnub6D~x8=HhQ_A4-+kTqoRFP11Sk@N3IG}6!svAwFceN37Rkx1S zLn0pjsF_$vX2-jj)ptHQ(Qtpv0qcZ|x-5P+v`GI}Z^z4`5k>G`B(^qI{Q06&fp&$e z%Q_&w@g6ux=fT z8LtSS`FwkI8l9~Je)p%*Q@(#TjdIpS04ar6W@F9EjGU-JU3?Mg-82ef;Fx;ih-JWq z=lO>AwrOrD@hB|>5MMheQx6yQ4c7d@c&@0;-N&-(U@vhp*Axn< zXY@9w5KoqEdLCh7uJF=;rK=-Ow&(yxeEtLWaJww+tq0Fpq=+agc=;{ez=BS^dY3~| z3PEVOmR&NFGS>g47KE78tUh)vQz!v}c-9p{)Zz8j5H)}IMmam1Zq_%RPFO%nAE9(6 zy4wJaAUvUZu(mG=qZ?d#_$H-2y@J%T#v_Q_G38dK5Jh4YYFY@S4K(n4;+lM68P0Ol zP0k$TJRr4IJ*;UxQ4CMt4}u4eF4^|z+{bo4fXzp>k0RoAnaHC#O4DW^Pl04Nd;iDG zwq<=o`H>C;n0#?)6Qhb(BUn(wIn=q4(Jf!sIRbeeS3dMm-h8`xzGAQ1SaKAKAQP{n z%SF)XMrf%P5h8k$sd~31Y4X;dZb1$PB_DWHI$UgAvpieJSSW%=!kXM%rlkm>H(!(Y zX{NCQc{JoUMP2>o3OS+=z4>cv{-!t{p+-Lt-jbTsR?DUt9YmaDv{RQ;CknQ829i4w zc#H|4=PYreSVz*~$TSP3usycR@w!PiXII`Jkk+OxHW$sM2?5a4=DD;x0J`6U92`-Z zzrgn5yq9M=W+dBBwvHl~r=|-)^0d{X-?_t$z6UT;@=m@`O*{jnrA;s{NHOI_YMwGS z1xafLZ5+mwR1I?^8_Z2F7l>)XpO0OgtF(G|0maMnS#n*h#dA^!GiZX!HQFy0%@rak zuo+Y=S9PYAk)kmwdacR3Ww-r&OQnM%DH=%gKp;2}mSaG>$p?CW;6npaEp-=)CNEaC zN=e6SO%kn>WZr~nxMt?Zqo`JM1X8rQVDeQ`@_F3L1?1cU#}URa62`v!@4r{99)04v zMTSi@vIV+6URgIEK<#^SvK<_jP|lX9ti0{Lg!Z<=?Z_n*#6N3s2{{uo&sZXY1{vq9 zYH2!hXA4S^LuoW9d7*di=l!nV&LKBW<8sLP$uhc$c&I7$jTE^r zkR%mQxHEn(bEzd2Xz|7o8c6u+m_9^yjZcAMnfWqFi_rW`-UT0m&lM%@^ za0U4XLhx=YD6}W8gK@>Zc?@!}j$JS7cg%Tvg=ZPEwA7cbBF4-|qdjzYmeJ!t)GK@i znXt?Ai$IO=I+nQ9q?Htj97@DW+TIQv@Feqd=8$ofUj5jbRbI+i>sd~os!NI7SCK11 zFy@*-@Lt8jT^C*M_NmSLd$KJ%fWoVt>5mpnT{kBeFHU9N#0yLgHfTo+V>+Xiwb+lg zQo9c}9H?*oY0cYgC2o6I3P|3=WwjGR$sJeI)Q%SRY&YyJO{tq-a*kS1TC+5<(sj}& zWlXBv(wLMEYv@#Gl&blqe_xM8rK%?`RrdcqyX0y~qV4~?v~uh*{a7p9%RK*MYbLL2 zH`vlnj_YWC7ih$79j)cqt+uF5+Fw&$`=$s>@B(Uhm`eK^?eT^N%;Wy`Q22{pqBTT{ z68yY`#vHLvE6kt8L0P4LG=+ChWPZuG$knOoQnVN;TE;e+`ZOT-EK4`hEJ~YH{Jw^h zO)^aJD1%*5x~PG@e#{>gk22&V|8faUMT0OquDV|MGAl#y)LrFPx3iqo(`;sF5TXL| z##AdtxXqC9AI~g%>&zQWvg^yL(pPX*G_R!j6xR)6!`)%i$dzTvf50hk%`a;HuvQGs z?TMjXE2tZC@Se2Q^mWDCx;gNiGFd@By-?864OG7u6rQZvXg-L)==8FG&{O6ii&O?} zBv;<@(Q3jxwDH1}`lz5~mxHo4QfZNh|4&;~4lY_yMAJgm^;9SrE?rZm7nZ_2V3oeK zsqn;RVLf?s7uv**T-7Z!H5e|v1rWT%xL-BCM*5;bJuHY`pzs7ZOVg8&rw+e7$wC>k zg>G}+)aP;kadxPT+D6s-8arb}zOFAmE0Fpbg)iI0I%<~n*~o$;`tYlY<&hEbd8C}a zx(wUJP?~SbjVX0r-^OGogx}(ev(Wb2$)lgq(YG)VT$heL@(+v~Xt-uUlm~?)@vFNH z>XG;T;oBC2T*us<6pJ9V)&ZXz&!{j8L`Ig zznxB@SkrhQ*p_D&J>}7^dY43qi0!lcLxbx9;9~)Qwprx)VrPa#fTN%k2Bk=a(t{_@ zjSjXjkKaypk=LZ`5Wb|+g7SW08v^q|F!|b$96RXlV51wY?Qb-i3IfI}!^mj?>pJwj z*31r{xW}Ri1_anyW^@?{UKW>HnZ=hmt2%6k{=urf;cKgc2n7b2}0OYkAcP#roMZ`q#?e9 z-xke}J$V(8jA1S`UfO*^9h)0YkAB>{ zFCHe*rZSoo7PxijqNywMV2)Yr7N)#=R*9;A+vFJ!xTR6KwUm$p)Mha1H2Hwo_IbVPaRZx1i+Rz3 z6+a(k(!}#jXbG^kH+ZvdQ7D^6`lSa&C1v3Wt(u}yVJ4RXS1pg?e~4nx-sZN4ghe#X zdcjvI)ej>Is;a&xsUIk8gYDO+yWuu4Xe=l)5~G!W?hrW+h2u)QkNk%wZ~9RFMXJkm z^RVEhbLm@bQ~R;C)Kb)XzdlT-fi#~(X*MWrR>nBL$ZO8%(rRO1^^!W}$q_OQgI0eW z5n3JF=wnrVO}|RWi=M#s*UC2@6?5Qg#nWHEUN@SL>hr5%ex;#b_ps+ESDLz`0*B=~ z9CG|@(=b9$OF!g~tZEYd1N-NN_KnIe-!l6Z$62wG(#gYMRqkny({6CpxUtM%vie_Jy5mJ1%BqkVwuyU?`}#sDSsBzc5F zbJM}z2DtVPLrXo_HnK$?K6|8|bQB+6aHQd(M*fT_zPixXbOC5S_15#0(08r{%3YtV zn;+TfJD#GVBT=7_Q&erF(cfx35aONb`KI!p$n^80DH;VQiq9gmODvG=W2Q~%YRq?M z9e)!|ehgtIr5R4$%XUG*(baNPV+>xsUv3ify7%nx7Y)F=|HRu2GLiIf|#Q zV0OB`)OsuoX*`XXiAZ~?3nDVq_4?54f3?b|9#ZWg;f{GeH;*bfnck? zJooDH5BFZ(wIEJjpwlzK>*@s>4y5wz0?i(W1I_x2^nIMsEnU`&Vmn%W^O-lkq?+hO zUrLWlGW}dtx>~C%=ZMf|N3V0r&%$o7;w`by5*p7 z-~Bpa#q_JEgY7|)-<4Q@nfxZAuwMy=?KN-Hr7L3P6yCaLx3py+_%REF;4l+I@%x#@ zT_AYs-s)DjpmE;bJOhw1b@7Q&NK`zp1X z1hwC}N>g!_p-_EqYETQ$_ST~hli*gpuT#-55W7>`FieVTUKdR>+a|Q4ck5_`dqVg` zyqAY=B`AEtdd!b4kzX9``TT;M31Mj%9fs}0WKUx|Y45>vN#Xr+JR^j~i?*VwlZ`n{ zKid%?YEN!@n?fd|c?RC5y5X?;h}&Y)|ITgKj?U?}BU%TfdQg1*ZR#@>h{Hgz4JrVHQ-mKK6nY(pgELmmoieRB= zQcb%@^I59Y$iD&hUShAnA2-{ui-r)$omU9gy6pbXo6~Dq+VF+mHq8TGN;~1sdLvP z)Y?Kys+@dC8D$h%izcgA^nn)B5QLDu(^NZFD7J-u^X{65rk6pxvwg!A;K1-ra4=DG@k;Hog~HYGOMrA>_x?9VBARI3xje4QvJx8w|)@Knt`p^evd>Cz4Lc) z%lHQ?kEMlt-s%KpEGX=?UjO)~Sy-t-OD&YSkLddhnAHX#c(oj0KWXE*u^uHYh@&#^ z;Ld+lG3`E*$wIk__tIe?ad{&V`EUN%-Z{sQ2Nr}F__R?=(MBG5H4S?v!dA{>>{=EW zx(-|9+9)Q+xr7`OpOJYMb}|8fWn_}O(-Hx5&WqD?dKOPAqWs~O= zISW0j_j7XP5p&#gv0`kP=9~T2t+$7yUY5-9lhnz^-~`!{%laSgF&kL41Pg4nVb)ME zVS;dxCgI{z(pS9bR@qbhpY~P8Uc!eK5xe0y_*Ki0SZ}959;b&)J z@C8hLxHX0jG&k5$?>R;*m+{5LBb6;w?X4+SCb zPbM!$TVe9*oLKixxaF_PgtuYK0x>6oN#Ma?olfg#a>lFmM;Kx51+c#HR0bE{Ky({E(A|Yl^FAQdCt_Dj{?@wbG7McjC+4MvtYi{~qW2}#d`gtjN?G}VW=ADV z6_uGE=^<{-_MgOL`|yN|r(3)`iw=c2tZJ2cC8g$BQ@U2eE{CYWLY3nAiToGAHT!*{ z&Wpe@B@zCm#LAB^RDW?Q`q8GhNt-okjZ@TpVgq33o7gITD?9tGr0uEvsv)VsGNUs) z`DMsg)qYKosB3Z6MUyAn<-GL51xr~<~d%iMkIrqOE8Rae7D#d$|Et-{g06@c+l`1sTJ zcix08LL`LuJK!I5sXd$Nt&;~O({Dmvo=juS%5y?fm9&bw$ZSwPZJP5lGw(nI3EqYs zbmO$!F0L|;(VOo8!pppsWBc0|-7vTzq>@K(9!pA%y{*#pIUXpUxfD^#v=qJuTM}P@ z;Qjx}ZK|#bx3xtHi2Npx}Fd3_c1Iw=SA^7Bqc7;Q*Vs#%_n;B3rjNGiq;~pX$tP` zaNl{z%R-ArHGK_)oY*e0qPvXP1_V!6tPVZyvZ-2BYYUGvR%Evh^JX}g;p>2W0wgcp z-*m4#-F5tf{k(C3IT8MSu%Z|s%vRQlI+I=ev6n~MW6nJ-p_}AwXM?=#=J6n?HGOC9 zU6GfYE_Th?BhlM0@)R8UVI2lrlUFpK8~}nFXlUO}$17c1_7~TMV+YrcpjbgnEi`?I7r>Zyq!Wc4i+>eT;pp)}0B}P=l7s2Uv>sxl@OBw~To6(G zlj*2FBW{5QuThOOGt_01Mtecf_;fU8JzQpY+O{4`bpH(W9Jk7h4CESvE69yw zjGggH_SB^OJCm@Ifzi1KwAku#+9Ll=P`Dj4kJ#oo#%)NDg%U3*BRVh3dua0eG!{xK zEsM?<>YG_nSEFfMGfwdwyn2bEPm5oeQV&<&1d6KiB+IMA>s~TD>b}8PTD(ERN#BO{ zZ7@3hq7tcBFLhCC+la1SD!OuI8zpRtMV?f+HGqi!jZRW8h;r^RI@7_8*lBKWOSdY?|Aw33p#%j&r+#EY_g{)Ak2p(;^+a2>7B0- zmU|FJ?dumLrRUPX#dQx|<_bv41ABVOdEWxT-teT)4fmGktp{4Fm^Qnhw0QZvUZdkS znL&~7Y_iWzWw&9XRS<}*$Xn_&upHt-}wHgkdY%{(CZj_jf^<8-R_XV>8Gy(aRK;(0%C-c6JEl>C!BG- zTP+XegoD4*+JU~KTvHIrv4Mb{$4z_c^3R?D$S((y;SLnK%V?hy@r=m;aV_drANS57 zXJ#Jcc*MYXAie;vNiDyQVU14;dBf0J@4(_Th2!e7BI1Q3nqbKlAq7-~7>++FN~0&s z_kU%VT5-vW(e(F|Sf^a5{%*M5+$I`>tGR_qwBxl7nb(d!-5z6n2=@Z`|2R;%tA5Iq zXN!G>sCY$xUH+1+EcO#VDY%BaIfQGdV@198ph-nn&Ys3sCq8!Xn|ePhm{XMf^zriz z;kspri^AyD1xq%1wFlPXO2tMY^y^M%w%|^7%X(CA41OF)!F!D(l|?z}-CpBn-{ZMN z4R_ScTEY2qJELkrYUuyGq@=4oe8;nR2f7>*BGW|<0?*Q!3v?lWf7h+gT0+2O~izCayUK zOgm3BrrR9hXkc2SxqaUeU=%XF)|`ImD8~z^f(Gsz+c|(C$H1^-C&%>cUtCht1CMir zGwz?h@;HaQ;*==JZky@Qj3Oo0^7+h5wJp8~3@pUnkHg}3H?`LA}d-g*^~4@$4^9hLJg zd~ZIr4af&I%)V7_$?@@2ZaE26AN=HE(ZombayNi{kb^g^jcQ#UJYm&4ARpwx=P#EV ziR}o_`Um8L9K6@&*v5-FwpU;5gaq>aXF%65FlU!ocvH(# z^GZ^4GfPTRi;TBN=W$BDo8Di+DK~v~3a83+{ctX!?QJn!2W$i#iZk<)a}qO)^h+uW zQj_v4w!6*b>P+BLcn&t2q2f7k{W4d+6l|7TeuLQ#rCcMI15o)3FSlp?;(Et8-C!rH z;dI@dtOlH~AUYdffpn@(m-x%2G2P=YR|MAysEo(!>8t*7r7;#wm;A@2ulfP#1yHRa e0Gv<<)t`Dm8kAkqfi!67+YLwyO;3ElG8q7b!G-w% diff --git a/package.json b/package.json index f71fc2b..1c49fef 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "dev:all": "turbo dev --parallel", "dev:api": "turbo dev --filter=@eda/api", "dev:ai": "turbo dev --filter=@eda/ai-api", + "dev:message": "turbo dev --filter=@eda/messaging", "dev:simulator": "turbo dev --filter=@eda/simulator", "dev:dashboard": "turbo dev --filter=@eda/dashboard", "dev:landingpage": "turbo dev --filter=@eda/landingpage", From 2d7bc830eb95a9239a0fe32977e81e90d8631101 Mon Sep 17 00:00:00 2001 From: Luis Otavio Date: Sat, 7 Dec 2024 12:53:48 -0300 Subject: [PATCH 02/75] changed lc to llamaindex --- .../eda_ai_api/api/routes/classifier.py | 117 ++- apps/ai_api/pyproject.toml | 5 + apps/ai_api/uv.lock | 796 +++++++++++++++++- 3 files changed, 868 insertions(+), 50 deletions(-) diff --git a/apps/ai_api/eda_ai_api/api/routes/classifier.py b/apps/ai_api/eda_ai_api/api/routes/classifier.py index f281fac..d9165b0 100644 --- a/apps/ai_api/eda_ai_api/api/routes/classifier.py +++ b/apps/ai_api/eda_ai_api/api/routes/classifier.py @@ -3,13 +3,12 @@ from typing import Any, Dict, List, Optional from fastapi import APIRouter, File, Form, UploadFile -from langchain.chains import LLMChain -from langchain.prompts import PromptTemplate -from langchain_groq import ChatGroq +from llama_index.llms.groq import Groq from onboarding.crew import OnboardingCrew from opportunity_finder.crew import OpportunityFinderCrew from proposal_writer.crew import ProposalWriterCrew +from llama_index.core import PromptTemplate from eda_ai_api.models.classifier import ClassifierResponse from eda_ai_api.utils.audio_converter import convert_ogg from eda_ai_api.utils.transcriber import transcribe_audio @@ -25,54 +24,70 @@ "audio/ogg": "ogg", } -# Setup LLM and prompt -llm = ChatGroq( - model_name="llama3-groq-70b-8192-tool-use-preview", +# Setup LLM +llm = Groq( + model="llama3-groq-70b-8192-tool-use-preview", api_key=os.environ.get("GROQ_API_KEY"), temperature=0.5, ) -ROUTER_TEMPLATE = """ -Given a user message, determine the appropriate service to handle the request. -Choose between: -- discovery: For finding grant opportunities -- proposal: For writing grant proposals -- onboarding: For getting help using the system -- heartbeat: For checking system health +# Define prompts +ROUTER_TEMPLATE = PromptTemplate( + """Given a user message, determine the appropriate service to handle the request. + Choose between: + - discovery: For finding grant opportunities + - proposal: For writing grant proposals + - onboarding: For getting help using the system + - heartbeat: For checking system health -User message: {message} + User message: {message} -Return only one word (discovery/proposal/onboarding/heartbeat):""" - -TOPIC_EXTRACTOR_TEMPLATE = """ -Extract up to 5 most relevant topics for grant opportunity research from the user message. -Return only a comma-separated list of topics (maximum 5), no other text. + Return only one word (discovery/proposal/onboarding/heartbeat):""" +) -User message: {message} +TOPIC_TEMPLATE = PromptTemplate( + """Extract up to 5 most relevant topics for grant opportunity research from the user message. + Return only a comma-separated list of topics (maximum 5), no other text. -Topics:""" + User message: {message} -PROPOSAL_EXTRACTOR_TEMPLATE = """ -Extract the community project name and grant program name from the user message. -Return in format: project_name|grant_name -If either cannot be determined, use "unknown" as placeholder. + Topics:""" +) -User message: {message} +PROPOSAL_TEMPLATE = PromptTemplate( + """Extract the community project name and grant program name from the user message. + Return in format: project_name|grant_name + If either cannot be determined, use "unknown" as placeholder. -Output:""" + User message: {message} -# Create prompt templates and chains -router_prompt = PromptTemplate(input_variables=["message"], template=ROUTER_TEMPLATE) -topic_prompt = PromptTemplate( - input_variables=["message"], template=TOPIC_EXTRACTOR_TEMPLATE + Output:""" ) -proposal_prompt = PromptTemplate( - input_variables=["message"], template=PROPOSAL_EXTRACTOR_TEMPLATE + +INSUFFICIENT_PROPOSAL_TEMPLATE = PromptTemplate( + """The user wants to write a proposal but hasn't provided enough information. + Generate a friendly response in the same language as the user's message asking for: + 1. The project name and brief description + 2. The specific grant program they're applying to (if any) + 3. The main objectives of their project + 4. The target community or region + + User message: {message} + + Response:""" ) -router_chain = LLMChain(llm=llm, prompt=router_prompt) -topic_chain = LLMChain(llm=llm, prompt=topic_prompt) -proposal_chain = LLMChain(llm=llm, prompt=proposal_prompt) +INSUFFICIENT_DISCOVERY_TEMPLATE = PromptTemplate( + """The user wants to find grant opportunities but hasn't provided enough information. + Generate a friendly response in the same language as the user's message asking for: + 1. The main topics or areas of their project + 2. The target region or community + 3. Any specific funding requirements or preferences + + User message: {message} + + Response:""" +) def detect_content_type(file: UploadFile) -> Optional[str]: @@ -123,14 +138,15 @@ async def process_audio(audio: UploadFile) -> str: def extract_topics(message: str) -> List[str]: """Extract topics from message""" - topics_raw = topic_chain.run(message=message) - topics = [t.strip() for t in topics_raw.split(",") if t.strip()][:5] + response = llm.complete(TOPIC_TEMPLATE.format(message=message)) + topics = [t.strip() for t in response.text.split(",") if t.strip()][:5] return topics if topics else ["AI", "Technology"] def extract_proposal_details(message: str) -> tuple[str, str]: """Extract project and grant details""" - extracted = proposal_chain.run(message=message).split("|") + response = llm.complete(PROPOSAL_TEMPLATE.format(message=message)) + extracted = response.text.split("|") community_project = extracted[0].strip() if len(extracted) > 0 else "unknown" grant_call = extracted[1].strip() if len(extracted) > 1 else "unknown" return community_project, grant_call @@ -147,13 +163,30 @@ def process_decision(decision: str, message: str) -> Dict[str, Any]: print("\n==================================================") print(f" EXTRACTED TOPICS: {topics}") print("==================================================\n") + + # If no specific topics found, ask for more information + if topics == ["AI", "Technology"]: + response = llm.complete( + INSUFFICIENT_DISCOVERY_TEMPLATE.format(message=message) + ) + return {"response": response.text} + return ( OpportunityFinderCrew().crew().kickoff(inputs={"topics": ", ".join(topics)}) ) + elif decision == "proposal": community_project, grant_call = extract_proposal_details(message) print(f" PROJECT NAME: {community_project}") print(f" GRANT PROGRAM: {grant_call}") + + # If either project or grant is unknown, ask for more information + if community_project == "unknown" or grant_call == "unknown": + response = llm.complete( + INSUFFICIENT_PROPOSAL_TEMPLATE.format(message=message) + ) + return {"response": response.text} + return ( ProposalWriterCrew( community_project=community_project, grant_call=grant_call @@ -161,10 +194,13 @@ def process_decision(decision: str, message: str) -> Dict[str, Any]: .crew() .kickoff() ) + elif decision == "heartbeat": return {"is_alive": True} + elif decision == "onboarding": return OnboardingCrew().crew().kickoff() + else: return {"error": f"Unknown decision type: {decision}"} @@ -214,7 +250,8 @@ async def classifier_route( ) # Process the combined input - decision = router_chain.run(message=combined_message).strip().lower() + response = llm.complete(ROUTER_TEMPLATE.format(message=combined_message)) + decision = response.text.strip().lower() result = process_decision(decision, combined_message) return ClassifierResponse(result=str(result)) diff --git a/apps/ai_api/pyproject.toml b/apps/ai_api/pyproject.toml index 8ff01b5..bf46d65 100644 --- a/apps/ai_api/pyproject.toml +++ b/apps/ai_api/pyproject.toml @@ -18,6 +18,11 @@ dependencies = [ "onboarding", "ffmpeg-python>=0.2.0", "python-multipart>=0.0.17", + "llama-index>=0.12.3", + "llama-index-llms-groq>=0.3.0", + "llama-index-embeddings-huggingface>=0.4.0", + "llama-index-vector-stores-postgres>=0.3.2", + "llama-index-readers-file>=0.4.1", ] [project.optional-dependencies] diff --git a/apps/ai_api/uv.lock b/apps/ai_api/uv.lock index 5567a37..0859a78 100644 --- a/apps/ai_api/uv.lock +++ b/apps/ai_api/uv.lock @@ -171,6 +171,42 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/45/86/4736ac618d82a20d87d2f92ae19441ebc7ac9e7a581d7e58bbe79233b24a/asttokens-2.4.1-py2.py3-none-any.whl", hash = "sha256:051ed49c3dcae8913ea7cd08e46a606dba30b79993209636c4875bc1d637bc24", size = 27764 }, ] +[[package]] +name = "async-timeout" +version = "5.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233 }, +] + +[[package]] +name = "asyncpg" +version = "0.29.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "async-timeout", marker = "python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c1/11/7a6000244eaeb6b8ed2238bf33477c486515d6133f2c295913aca3ba4a00/asyncpg-0.29.0.tar.gz", hash = "sha256:d1c49e1f44fffafd9a55e1a9b101590859d881d639ea2922516f5d9c512d354e", size = 820455 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/28/3e3c4e243778f0361214b9d6e8bc6aa8e8bf55f35a2d2cb8949a6863caab/asyncpg-0.29.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4900ee08e85af01adb207519bb4e14b1cae8fd21e0ccf80fac6aa60b6da37b4", size = 653061 }, + { url = "https://files.pythonhosted.org/packages/4a/13/f96284d7014dd06db2e78bea15706443d7895548bf74cf34f0c3ee1863fd/asyncpg-0.29.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a65c1dcd820d5aea7c7d82a3fdcb70e096f8f70d1a8bf93eb458e49bfad036ac", size = 638740 }, + { url = "https://files.pythonhosted.org/packages/27/25/d140bd503932f99528edc0a1461648973ad3c1c67f5929d11f3e8b5f81f4/asyncpg-0.29.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b52e46f165585fd6af4863f268566668407c76b2c72d366bb8b522fa66f1870", size = 2788952 }, + { url = "https://files.pythonhosted.org/packages/c4/41/a0bdc18f13bdd5f27e7fc1b5de7e1caae19951967c109bca1a2e99cf3331/asyncpg-0.29.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc600ee8ef3dd38b8d67421359779f8ccec30b463e7aec7ed481c8346decf99f", size = 2809108 }, + { url = "https://files.pythonhosted.org/packages/f2/1f/1737248d7b1b75d19e7f07a98321bc58cb6fc979754c78544cfebff3359b/asyncpg-0.29.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:039a261af4f38f949095e1e780bae84a25ffe3e370175193174eb08d3cecab23", size = 3355924 }, + { url = "https://files.pythonhosted.org/packages/88/b0/6bebd69ed484055d47b78ea34fd9887c35694b63c9a648a7f02759d3bf73/asyncpg-0.29.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6feaf2d8f9138d190e5ec4390c1715c3e87b37715cd69b2c3dfca616134efd2b", size = 3391360 }, + { url = "https://files.pythonhosted.org/packages/5b/89/3ed6e9d235f8aa13aa8ee8dc3a70f754962dbd441bec2dcfdae9f9e0e2e3/asyncpg-0.29.0-cp311-cp311-win32.whl", hash = "sha256:1e186427c88225ef730555f5fdda6c1812daa884064bfe6bc462fd3a71c4b675", size = 496216 }, + { url = "https://files.pythonhosted.org/packages/f2/39/f7e755b5d5aa59d8385c08be58726aceffc1da9360041031554d664c783f/asyncpg-0.29.0-cp311-cp311-win_amd64.whl", hash = "sha256:cfe73ffae35f518cfd6e4e5f5abb2618ceb5ef02a2365ce64f132601000587d3", size = 543321 }, + { url = "https://files.pythonhosted.org/packages/f2/b7/38b7c195f66a5598413c538da499b3f8119ba5764ded6fff620f7eb84c65/asyncpg-0.29.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6011b0dc29886ab424dc042bf9eeb507670a3b40aece3439944006aafe023178", size = 636282 }, + { url = "https://files.pythonhosted.org/packages/eb/0b/d128b57f7e994a6d71253d0a6a8c949fc50c969785010d46b87d8491be24/asyncpg-0.29.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b544ffc66b039d5ec5a7454667f855f7fec08e0dfaf5a5490dfafbb7abbd2cfb", size = 618024 }, + { url = "https://files.pythonhosted.org/packages/49/ac/0396e559e1e7ab23787f790ae96b22affe2d66acebb084d6fc42293d12b8/asyncpg-0.29.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d84156d5fb530b06c493f9e7635aa18f518fa1d1395ef240d211cb563c4e2364", size = 3196465 }, + { url = "https://files.pythonhosted.org/packages/99/38/0bfb00e9b828513bd759174860fd2b1c5e36d0b33985c90ff4ed6f96814c/asyncpg-0.29.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54858bc25b49d1114178d65a88e48ad50cb2b6f3e475caa0f0c092d5f527c106", size = 3275564 }, + { url = "https://files.pythonhosted.org/packages/16/1b/bb42784e9895832bf460ee6643f818bd53e4d6a6308cca5984c581a51845/asyncpg-0.29.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bde17a1861cf10d5afce80a36fca736a86769ab3579532c03e45f83ba8a09c59", size = 3164724 }, + { url = "https://files.pythonhosted.org/packages/d5/d1/7ed5169e30e80573c942f5a6f29b2f87d5b8379bdd9bd916f0ed136c874e/asyncpg-0.29.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:37a2ec1b9ff88d8773d3eb6d3784dc7e3fee7756a5317b67f923172a4748a175", size = 3252834 }, + { url = "https://files.pythonhosted.org/packages/91/2e/20e024608c57c2099531ba492c761b12fdd80891a67e58c92de44d05d57e/asyncpg-0.29.0-cp312-cp312-win32.whl", hash = "sha256:bb1292d9fad43112a85e98ecdc2e051602bce97c199920586be83254d9dafc02", size = 487254 }, + { url = "https://files.pythonhosted.org/packages/71/86/7a18e1a457afb73991e5e5586e2341af09a31c91d8f65cc003f0b4553252/asyncpg-0.29.0-cp312-cp312-win_amd64.whl", hash = "sha256:2245be8ec5047a605e0b454c894e54bf2ec787ac04b1cb7e0d3c67aa1e32f0fe", size = 530253 }, +] + [[package]] name = "attrs" version = "24.2.0" @@ -722,6 +758,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/02/c3/253a89ee03fc9b9682f1541728eb66db7db22148cd94f89ab22528cd1e1b/deprecation-2.1.0-py2.py3-none-any.whl", hash = "sha256:a10811591210e1fb0e768a8c25517cabeabcba6f0bf96564f8ff45189f90b14a", size = 11178 }, ] +[[package]] +name = "dirtyjson" +version = "1.0.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/db/04/d24f6e645ad82ba0ef092fa17d9ef7a21953781663648a01c9371d9e8e98/dirtyjson-1.0.8.tar.gz", hash = "sha256:90ca4a18f3ff30ce849d100dcf4a003953c79d3a2348ef056f1d9c22231a25fd", size = 30782 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/69/1bcf70f81de1b4a9f21b3a62ec0c83bdff991c88d6cc2267d02408457e88/dirtyjson-1.0.8-py3-none-any.whl", hash = "sha256:125e27248435a58acace26d5c2c4c11a1c0de0a9c5124c5a94ba78e517d74f53", size = 25197 }, +] + [[package]] name = "distro" version = "1.9.0" @@ -778,6 +823,11 @@ dependencies = [ { name = "ffmpeg-python" }, { name = "joblib" }, { name = "langchain-groq" }, + { name = "llama-index" }, + { name = "llama-index-embeddings-huggingface" }, + { name = "llama-index-llms-groq" }, + { name = "llama-index-readers-file" }, + { name = "llama-index-vector-stores-postgres" }, { name = "loguru" }, { name = "numpy" }, { name = "onboarding" }, @@ -812,6 +862,11 @@ requires-dist = [ { name = "isort", marker = "extra == 'dev'", specifier = ">=5.13.2" }, { name = "joblib", specifier = ">=1.3.2" }, { name = "langchain-groq", specifier = ">=0.2.1" }, + { name = "llama-index", specifier = ">=0.12.3" }, + { name = "llama-index-embeddings-huggingface", specifier = ">=0.4.0" }, + { name = "llama-index-llms-groq", specifier = ">=0.3.0" }, + { name = "llama-index-readers-file", specifier = ">=0.4.1" }, + { name = "llama-index-vector-stores-postgres", specifier = ">=0.3.2" }, { name = "loguru", specifier = ">=0.7.2" }, { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.8.0" }, { name = "numpy", specifier = ">=1.26.2" }, @@ -922,6 +977,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b9/f8/feced7779d755758a52d1f6635d990b8d98dc0a29fa568bbe0625f18fdf3/filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0", size = 16163 }, ] +[[package]] +name = "filetype" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bb/29/745f7d30d47fe0f251d3ad3dc2978a23141917661998763bebb6da007eb1/filetype-1.2.0.tar.gz", hash = "sha256:66b56cd6474bf41d8c54660347d37afcc3f7d1970648de365c102ef77548aadb", size = 998020 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/79/1b8fa1bb3568781e84c9200f951c735f3f157429f44be0495da55894d620/filetype-1.2.0-py2.py3-none-any.whl", hash = "sha256:7ce71b6880181241cf7ac8697a2f1eb6a8bd9b429f7ad6d27b8db9ba5f1c2d25", size = 19970 }, +] + [[package]] name = "flake8" version = "7.1.1" @@ -1472,6 +1536,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/60/bf/cea0b9720c32fa01b0c4ec4b16b9f4ae34ca106b202ebbae9f03ab98cd8f/huggingface_hub-0.26.2-py3-none-any.whl", hash = "sha256:98c2a5a8e786c7b2cb6fdeb2740893cba4d53e312572ed3d8afafda65b128c46", size = 447536 }, ] +[package.optional-dependencies] +inference = [ + { name = "aiohttp" }, +] + [[package]] name = "humanfriendly" version = "10.0" @@ -1534,12 +1603,11 @@ wheels = [ [[package]] name = "instructor" -version = "1.6.3" +version = "1.5.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp" }, { name = "docstring-parser" }, - { name = "jinja2" }, { name = "jiter" }, { name = "openai" }, { name = "pydantic" }, @@ -1548,9 +1616,9 @@ dependencies = [ { name = "tenacity" }, { name = "typer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b8/e6/21969fe0de9d278979872240b6af17510af8bd5020f6845891719c1d3eef/instructor-1.6.3.tar.gz", hash = "sha256:399cd90e30b5bc7cbd47acd7399c9c4e84926a96c20c8b5d00c5a04b41ed41ab", size = 56708 } +sdist = { url = "https://files.pythonhosted.org/packages/45/31/f7499f60a513411a1e8c47c530987d1d3f2dbf9da0d621e52a100a80eebb/instructor-1.5.2.tar.gz", hash = "sha256:fdd5ccbca21b4c558a24e9ba12c84afd907e65153a39d035f47c25800011a977", size = 49521 } wheels = [ - { url = "https://files.pythonhosted.org/packages/10/98/c96bf0b1656173d06cd6c3a5adaf3ac429f86d5d696ae8e90e6eb15e89be/instructor-1.6.3-py3-none-any.whl", hash = "sha256:a8f973fea621c0188009b65a3429a526c24aeb249fc24100b605ea496e92d622", size = 69447 }, + { url = "https://files.pythonhosted.org/packages/0e/94/5f96c1d87ba732ba6b2a9751314ddb69bfcd8d05ed0f7e93ee17fd4df068/instructor-1.5.2-py3-none-any.whl", hash = "sha256:da25abbf1ab792fb094992f1d9ce593e26fe458cb1f9a8e7ebf9d68f3f2c757a", size = 61494 }, ] [[package]] @@ -1963,6 +2031,313 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1b/c9/1988134406bab7ecee2edd2595c62e3a67fd4d2718418eac1cf7784a63d9/litellm-1.51.1-py3-none-any.whl", hash = "sha256:1a389ca5b8ddd7a98d97ad229118d8323caeaaf9c1c5b79b1072edc2a18e773d", size = 6329653 }, ] +[[package]] +name = "llama-cloud" +version = "0.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cb/fe/1684f3bf5348419c0db7eaacc1955388dc22def16ec7564e252be095398f/llama_cloud-0.1.6.tar.gz", hash = "sha256:21200f6fdd46e08455d34b136f645ce6b8c3800e0ae13d8077913171a921da5a", size = 75539 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/58/b2049d1216197f97e7c4b9001bccc1b4f82fbaa4540b5e664f08875e9418/llama_cloud-0.1.6-py3-none-any.whl", hash = "sha256:43595081e03ff552fd18d9553fcaada897ff267456c0f89f4cb098b927dc4dc7", size = 195760 }, +] + +[[package]] +name = "llama-index" +version = "0.12.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "llama-index-agent-openai" }, + { name = "llama-index-cli" }, + { name = "llama-index-core" }, + { name = "llama-index-embeddings-openai" }, + { name = "llama-index-indices-managed-llama-cloud" }, + { name = "llama-index-legacy" }, + { name = "llama-index-llms-openai" }, + { name = "llama-index-multi-modal-llms-openai" }, + { name = "llama-index-program-openai" }, + { name = "llama-index-question-gen-openai" }, + { name = "llama-index-readers-file" }, + { name = "llama-index-readers-llama-parse" }, + { name = "nltk" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1d/0c/dd88674421f31069a1761ca8cf21e6462a380b4fa59edcf71d0fd40dbdbc/llama_index-0.12.3.tar.gz", hash = "sha256:d3ea4d599225c934ff9e56712203f5236e7e143eaf2144d92238f37f1de24ccd", size = 7849 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/c3/43913245b45fe60177cb66734b1609402d4bdf4d085f6e003593bf8bcd94/llama_index-0.12.3-py3-none-any.whl", hash = "sha256:0fe8836c84becf05bb95c19aaf15c643bd57a5cb324088a84f2f299a779e97bb", size = 6801 }, +] + +[[package]] +name = "llama-index-agent-openai" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "llama-index-core" }, + { name = "llama-index-llms-openai" }, + { name = "openai" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/32/93/4c1cd6c722404eef50a51c066f8aa6d36e4f7d6b8c85b07cdb5582f644ad/llama_index_agent_openai-0.4.0.tar.gz", hash = "sha256:31d2675dbd84489756dd062a7ffed330b2abdca3b7715d511674f5b5075e4dd6", size = 10539 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/18/4c59c7be22c5baed00d006705a82a5cbad0c3f1463a8fb6e16df29c7ea4a/llama_index_agent_openai-0.4.0-py3-none-any.whl", hash = "sha256:71b2f46bb24813129ab6bc2d5bcebb9aebf323403ebf1e6cc9840687a34a6169", size = 13032 }, +] + +[[package]] +name = "llama-index-cli" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "llama-index-core" }, + { name = "llama-index-embeddings-openai" }, + { name = "llama-index-llms-openai" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0a/52/81e1448d4dcff5beb1453f397f34f9ac769b7fcdb6b7c8fbd4c20b73e836/llama_index_cli-0.4.0.tar.gz", hash = "sha256:d6ab201359962a8a34368aeda3a49bbbe67e9e009c59bd925c4fb2be4ace3906", size = 24710 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/29/2b659e5930ea44253bf99e2afc395daaa2a3edaa579d99e63ea53df03313/llama_index_cli-0.4.0-py3-none-any.whl", hash = "sha256:60d12f89e6b85e80a0cc3a8b531f05a911b5eebaebc37314411476d1ba685904", size = 27785 }, +] + +[[package]] +name = "llama-index-core" +version = "0.12.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "dataclasses-json" }, + { name = "deprecated" }, + { name = "dirtyjson" }, + { name = "filetype" }, + { name = "fsspec" }, + { name = "httpx" }, + { name = "nest-asyncio" }, + { name = "networkx" }, + { name = "nltk" }, + { name = "numpy" }, + { name = "pillow" }, + { name = "pydantic" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "sqlalchemy", extra = ["asyncio"] }, + { name = "tenacity" }, + { name = "tiktoken" }, + { name = "tqdm" }, + { name = "typing-extensions" }, + { name = "typing-inspect" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/32/ce/1c26d6355623f383e8feff72c8568fa482d5d85bd5c7da512911803156b2/llama_index_core-0.12.3.tar.gz", hash = "sha256:61fa0a1155a022b5b63c081d6709f5b57bae231b1c847c78e2052c93a231b90a", size = 1334016 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/9f/70e446852ecfc87d80ed7851ca854b60a1ca46aeb3f6eedad09303b8b515/llama_index_core-0.12.3-py3-none-any.whl", hash = "sha256:f0034965e74f508594cb96b8ebb17ce123d73d5f8858adcf38f5cdd936b94262", size = 1585253 }, +] + +[[package]] +name = "llama-index-embeddings-huggingface" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub", extra = ["inference"] }, + { name = "llama-index-core" }, + { name = "sentence-transformers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e5/e8/7b3ac3322774500599f97c37fdf23feb4812ff74ebf0d56e9803e170dd6c/llama_index_embeddings_huggingface-0.4.0.tar.gz", hash = "sha256:ce8f8b30b29cff85401aba2118285fb63fb8147a56b656ee20f7e8510ca085a2", size = 7634 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/54/4f85c1c3f351849479d51c19170ef58dfe54eab350b2e8333e9df9ac04fa/llama_index_embeddings_huggingface-0.4.0-py3-none-any.whl", hash = "sha256:a5890bab349b118398054138b298a9e429776b85bcf8017fdf01cd5d60fbba12", size = 8583 }, +] + +[[package]] +name = "llama-index-embeddings-openai" +version = "0.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "llama-index-core" }, + { name = "openai" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/02/a2604ef3a167131fdd701888f45f16c8efa6d523d02efe8c4e640238f4ea/llama_index_embeddings_openai-0.3.1.tar.gz", hash = "sha256:1368aad3ce24cbaed23d5ad251343cef1eb7b4a06d6563d6606d59cb347fef20", size = 5492 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/45/ca55b91c4ac1b6251d4099fa44121a6c012129822906cadcc27b8cfb33a4/llama_index_embeddings_openai-0.3.1-py3-none-any.whl", hash = "sha256:f15a3d13da9b6b21b8bd51d337197879a453d1605e625a1c6d45e741756c0290", size = 6177 }, +] + +[[package]] +name = "llama-index-indices-managed-llama-cloud" +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "llama-cloud" }, + { name = "llama-index-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/58/29afa6086e2080ae27da79949e319f77f08cb7d1b2bd26e56a676dab1338/llama_index_indices_managed_llama_cloud-0.6.3.tar.gz", hash = "sha256:f09e4182cbc2a2bd75ae85cebb1681075247f0d91b931b094cac4315386ce87a", size = 10483 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/c6/ebb53a15e63c8da3633a595f53fc965509e8c6707da6a8b1bfa9b7923236/llama_index_indices_managed_llama_cloud-0.6.3-py3-none-any.whl", hash = "sha256:7f125602f624a2d321b6a4130cd98df35eb8c15818a159390755b2c13068f4ce", size = 11077 }, +] + +[[package]] +name = "llama-index-legacy" +version = "0.9.48.post4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "dataclasses-json" }, + { name = "deprecated" }, + { name = "dirtyjson" }, + { name = "fsspec" }, + { name = "httpx" }, + { name = "nest-asyncio" }, + { name = "networkx" }, + { name = "nltk" }, + { name = "numpy" }, + { name = "openai" }, + { name = "pandas" }, + { name = "requests" }, + { name = "sqlalchemy", extra = ["asyncio"] }, + { name = "tenacity" }, + { name = "tiktoken" }, + { name = "typing-extensions" }, + { name = "typing-inspect" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d6/18/9a276184d281876707ee257716eef5a80dab897624058eb1b539f7a678d0/llama_index_legacy-0.9.48.post4.tar.gz", hash = "sha256:f8a9764e7e134a52bfef5e53d2d62561bfc01fc09874c51cc001df6f5302ae30", size = 781650 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/47/ffb4a02d6d499a1207c89f0214e8a72f0437fb7da46ec4927d239ec10520/llama_index_legacy-0.9.48.post4-py3-none-any.whl", hash = "sha256:4b817d7c343fb5f7f00c4410eff519f320013b8d5f24c4fedcf270c471f92038", size = 1200763 }, +] + +[[package]] +name = "llama-index-llms-groq" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "llama-index-core" }, + { name = "llama-index-llms-openai-like" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/1e/3ef527582f9658afb055cfbab10d3119462f8bde9cf8fc2293fe185f63ca/llama_index_llms_groq-0.3.0.tar.gz", hash = "sha256:f3ce9511783fa9fc75e00e048d8590901b4801ab18d277d3b96eab84cec1ca64", size = 2740 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a4/8b6647c405fba7525c3f1aeb0b430a5f67d963032ac341ef14ca23f63686/llama_index_llms_groq-0.3.0-py3-none-any.whl", hash = "sha256:47799133e35b8671ca0a70e8c6af39713f28c39b99dd5687e1c3bb847d263357", size = 2878 }, +] + +[[package]] +name = "llama-index-llms-openai" +version = "0.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "llama-index-core" }, + { name = "openai" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5c/72/a48143cc6fffc90cad00f78f787d29803d0c4c6703d84b8a7cffc3a35487/llama_index_llms_openai-0.3.2.tar.gz", hash = "sha256:8a443a564e7d12779a9f030cb82fe3243803e217d72410764ac116dd43554fe5", size = 13454 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/3f/417e694183337a2fe4f39068ab461909848e72bef73321627e8e1d1f806c/llama_index_llms_openai-0.3.2-py3-none-any.whl", hash = "sha256:439b8ac8183168156a9724d03e1b3aeeb95d8d3c605b866a6b803b84fae131f6", size = 13630 }, +] + +[[package]] +name = "llama-index-llms-openai-like" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "llama-index-core" }, + { name = "llama-index-llms-openai" }, + { name = "transformers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/90/f9/b43bd77b0c5208ffca16ff47a2d2c01834dcf815f9028779dc468a1092bb/llama_index_llms_openai_like-0.3.0.tar.gz", hash = "sha256:021e83d64240b46587f22e57c60c531ded34e18d769c879b4eea481c88c97b3d", size = 2890 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/4e/af56b41f8f64fc1c7e11e00a57c0e86796457b3a636d9729c4781b1da9e0/llama_index_llms_openai_like-0.3.0-py3-none-any.whl", hash = "sha256:df15a2ee48fdd592dd1d9a7f92c285eaf70fdbb3f4a8b4ef2f4e371d2293ecfd", size = 3131 }, +] + +[[package]] +name = "llama-index-multi-modal-llms-openai" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "llama-index-core" }, + { name = "llama-index-llms-openai" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c6/ff/d3112dd9f17b75bb9957abdcde80d5b039d1d800402a8f0d41e50b205889/llama_index_multi_modal_llms_openai-0.3.0.tar.gz", hash = "sha256:71e983c7771c39088e4058cd78029219315a0fb631b9e12b903e53243b9a3fd6", size = 5190 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/c0/3e9649e8ab66a1cd95947a6871222cbea63a52d50b99bae67b72fc5a8c2d/llama_index_multi_modal_llms_openai-0.3.0-py3-none-any.whl", hash = "sha256:9b7e3e39b19b2668b9c75014bcb90795bb546f0f9e1af8b7f1087f8687805763", size = 5883 }, +] + +[[package]] +name = "llama-index-program-openai" +version = "0.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "llama-index-agent-openai" }, + { name = "llama-index-core" }, + { name = "llama-index-llms-openai" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/b8/24f1103106bfeed04f0e33b587863345c2d7fad001828bb02844a5427fbc/llama_index_program_openai-0.3.1.tar.gz", hash = "sha256:6039a6cdbff62c6388c07e82a157fe2edd3bbef0c5adf292ad8546bf4ec75b82", size = 4818 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/59/3f31171c30a08c8ba21155d5241ba174630e57cf43b03d97fd77bf565b51/llama_index_program_openai-0.3.1-py3-none-any.whl", hash = "sha256:93646937395dc5318fd095153d2f91bd632b25215d013d14a87c088887d205f9", size = 5318 }, +] + +[[package]] +name = "llama-index-question-gen-openai" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "llama-index-core" }, + { name = "llama-index-llms-openai" }, + { name = "llama-index-program-openai" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4e/47/c57392e2fb00c0f596f912e7977e3c639ac3314f2aed5d4ac733baa367f1/llama_index_question_gen_openai-0.3.0.tar.gz", hash = "sha256:efd3b468232808e9d3474670aaeab00e41b90f75f52d0c9bfbf11207e0963d62", size = 2608 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/2c/765b0dfc2c988bbea267e236c836d7a96c60a20df76d842e43e17401f800/llama_index_question_gen_openai-0.3.0-py3-none-any.whl", hash = "sha256:9b60ec114273a63b50349948666e5744a8f58acb645824e07c979041e8fec598", size = 2899 }, +] + +[[package]] +name = "llama-index-readers-file" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beautifulsoup4" }, + { name = "llama-index-core" }, + { name = "pandas" }, + { name = "pypdf" }, + { name = "striprtf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a5/70/1f41a6c482f2838b4ad61ebd192a3e70f99b2001ef4d6254af0013147b94/llama_index_readers_file-0.4.1.tar.gz", hash = "sha256:1150300bcebab7cddd9e29b7271e097b278fbe3518de26e435595855b12c3b9a", size = 22002 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/32/9fcc849ab77dafe6e0f825d19ef116252d9685a51b5bdee0fb931a0ce0de/llama_index_readers_file-0.4.1-py3-none-any.whl", hash = "sha256:51df6c4c6f6f244a704907aac4edc5c7a1c61a67672b1ca7fb182e6409226708", size = 38902 }, +] + +[[package]] +name = "llama-index-readers-llama-parse" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "llama-index-core" }, + { name = "llama-parse" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/35/30/4611821286f82ba7b5842295607baa876262db86f88b87d83595eed172bf/llama_index_readers_llama_parse-0.4.0.tar.gz", hash = "sha256:e99ec56f4f8546d7fda1a7c1ae26162fb9acb7ebcac343b5abdb4234b4644e0f", size = 2472 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/4f/e30d4257fe9e4224f5612b77fe99aaceddae411b2e74ca30534491de3e6f/llama_index_readers_llama_parse-0.4.0-py3-none-any.whl", hash = "sha256:574e48386f28d2c86c3f961ca4a4906910312f3400dd0c53014465bfbc6b32bf", size = 2472 }, +] + +[[package]] +name = "llama-index-vector-stores-postgres" +version = "0.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asyncpg" }, + { name = "llama-index-core" }, + { name = "pgvector" }, + { name = "psycopg2-binary" }, + { name = "sqlalchemy", extra = ["asyncio"] }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/a1/8c1972c5ffba190f3ecd9df3221d0afff3372d7dae41e96855b1d75c9cbb/llama_index_vector_stores_postgres-0.3.2.tar.gz", hash = "sha256:372d61290530af3bdbfc62365916bcd22d04b04f76a2a484763ea114195ee87f", size = 8699 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/9b/840dbca972fe67f509d762e6d661ff915f766888e63ef3ec92bbf9e5f0cd/llama_index_vector_stores_postgres-0.3.2-py3-none-any.whl", hash = "sha256:e9f512c432b9447824d8c5b4fe1922b507cfc60639eb1896b6ece0353244e2a8", size = 8977 }, +] + +[[package]] +name = "llama-parse" +version = "0.5.17" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "llama-index-core" }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/88/15/786ea8194a383a799f8da856195b2dcd2645cecdfe7589e054c0b0042a10/llama_parse-0.5.17.tar.gz", hash = "sha256:2ba2700ca3b15e84ef072fedcbbfeae2fc13ce7b63fee20fcd5249e6fcdbd381", size = 15497 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/48/8282de410dbaa267c544449346d884e38c55eb11e6c9a9d467d5f7b5e628/llama_parse-0.5.17-py3-none-any.whl", hash = "sha256:0981c7e4ac21bc3b314830c6e9eaed4b20db874ec8f8868540707cf2cca07051", size = 14825 }, +] + [[package]] name = "loguru" version = "0.7.2" @@ -2272,6 +2647,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 }, ] +[[package]] +name = "nest-asyncio" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/83/f8/51569ac65d696c8ecbee95938f89d4abf00f47d58d48f6fbabfe8f0baefe/nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe", size = 7418 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195 }, +] + [[package]] name = "networkx" version = "3.4.2" @@ -2281,6 +2665,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b9/54/dd730b32ea14ea797530a4479b2ed46a6fb250f682a9cfb997e968bf0261/networkx-3.4.2-py3-none-any.whl", hash = "sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f", size = 1723263 }, ] +[[package]] +name = "nltk" +version = "3.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "joblib" }, + { name = "regex" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3c/87/db8be88ad32c2d042420b6fd9ffd4a149f9a0d7f0e86b3f543be2eeeedd2/nltk-3.9.1.tar.gz", hash = "sha256:87d127bd3de4bd89a4f81265e5fa59cb1b199b27440175370f7417d2bc7ae868", size = 2904691 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/66/7d9e26593edda06e8cb531874633f7c2372279c3b0f46235539fe546df8b/nltk-3.9.1-py3-none-any.whl", hash = "sha256:4fa26829c5b00715afe3061398a8989dc643b92ce7dd93fb4585a70930d168a1", size = 1505442 }, +] + [[package]] name = "nodeenv" version = "1.9.1" @@ -2314,6 +2713,126 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/16/2e/86f24451c2d530c88daf997cb8d6ac622c1d40d19f5a031ed68a4b73a374/numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818", size = 15517754 }, ] +[[package]] +name = "nvidia-cublas-cu12" +version = "12.4.5.8" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/7f/7fbae15a3982dc9595e49ce0f19332423b260045d0a6afe93cdbe2f1f624/nvidia_cublas_cu12-12.4.5.8-py3-none-manylinux2014_aarch64.whl", hash = "sha256:0f8aa1706812e00b9f19dfe0cdb3999b092ccb8ca168c0db5b8ea712456fd9b3", size = 363333771 }, + { url = "https://files.pythonhosted.org/packages/ae/71/1c91302526c45ab494c23f61c7a84aa568b8c1f9d196efa5993957faf906/nvidia_cublas_cu12-12.4.5.8-py3-none-manylinux2014_x86_64.whl", hash = "sha256:2fc8da60df463fdefa81e323eef2e36489e1c94335b5358bcb38360adf75ac9b", size = 363438805 }, +] + +[[package]] +name = "nvidia-cuda-cupti-cu12" +version = "12.4.127" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/b5/9fb3d00386d3361b03874246190dfec7b206fd74e6e287b26a8fcb359d95/nvidia_cuda_cupti_cu12-12.4.127-py3-none-manylinux2014_aarch64.whl", hash = "sha256:79279b35cf6f91da114182a5ce1864997fd52294a87a16179ce275773799458a", size = 12354556 }, + { url = "https://files.pythonhosted.org/packages/67/42/f4f60238e8194a3106d06a058d494b18e006c10bb2b915655bd9f6ea4cb1/nvidia_cuda_cupti_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl", hash = "sha256:9dec60f5ac126f7bb551c055072b69d85392b13311fcc1bcda2202d172df30fb", size = 13813957 }, +] + +[[package]] +name = "nvidia-cuda-nvrtc-cu12" +version = "12.4.127" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/aa/083b01c427e963ad0b314040565ea396f914349914c298556484f799e61b/nvidia_cuda_nvrtc_cu12-12.4.127-py3-none-manylinux2014_aarch64.whl", hash = "sha256:0eedf14185e04b76aa05b1fea04133e59f465b6f960c0cbf4e37c3cb6b0ea198", size = 24133372 }, + { url = "https://files.pythonhosted.org/packages/2c/14/91ae57cd4db3f9ef7aa99f4019cfa8d54cb4caa7e00975df6467e9725a9f/nvidia_cuda_nvrtc_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl", hash = "sha256:a178759ebb095827bd30ef56598ec182b85547f1508941a3d560eb7ea1fbf338", size = 24640306 }, +] + +[[package]] +name = "nvidia-cuda-runtime-cu12" +version = "12.4.127" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/aa/b656d755f474e2084971e9a297def515938d56b466ab39624012070cb773/nvidia_cuda_runtime_cu12-12.4.127-py3-none-manylinux2014_aarch64.whl", hash = "sha256:961fe0e2e716a2a1d967aab7caee97512f71767f852f67432d572e36cb3a11f3", size = 894177 }, + { url = "https://files.pythonhosted.org/packages/ea/27/1795d86fe88ef397885f2e580ac37628ed058a92ed2c39dc8eac3adf0619/nvidia_cuda_runtime_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl", hash = "sha256:64403288fa2136ee8e467cdc9c9427e0434110899d07c779f25b5c068934faa5", size = 883737 }, +] + +[[package]] +name = "nvidia-cudnn-cu12" +version = "9.1.0.70" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas-cu12" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/fd/713452cd72343f682b1c7b9321e23829f00b842ceaedcda96e742ea0b0b3/nvidia_cudnn_cu12-9.1.0.70-py3-none-manylinux2014_x86_64.whl", hash = "sha256:165764f44ef8c61fcdfdfdbe769d687e06374059fbb388b6c89ecb0e28793a6f", size = 664752741 }, +] + +[[package]] +name = "nvidia-cufft-cu12" +version = "11.2.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink-cu12" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/8a/0e728f749baca3fbeffad762738276e5df60851958be7783af121a7221e7/nvidia_cufft_cu12-11.2.1.3-py3-none-manylinux2014_aarch64.whl", hash = "sha256:5dad8008fc7f92f5ddfa2101430917ce2ffacd86824914c82e28990ad7f00399", size = 211422548 }, + { url = "https://files.pythonhosted.org/packages/27/94/3266821f65b92b3138631e9c8e7fe1fb513804ac934485a8d05776e1dd43/nvidia_cufft_cu12-11.2.1.3-py3-none-manylinux2014_x86_64.whl", hash = "sha256:f083fc24912aa410be21fa16d157fed2055dab1cc4b6934a0e03cba69eb242b9", size = 211459117 }, +] + +[[package]] +name = "nvidia-curand-cu12" +version = "10.3.5.147" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/9c/a79180e4d70995fdf030c6946991d0171555c6edf95c265c6b2bf7011112/nvidia_curand_cu12-10.3.5.147-py3-none-manylinux2014_aarch64.whl", hash = "sha256:1f173f09e3e3c76ab084aba0de819c49e56614feae5c12f69883f4ae9bb5fad9", size = 56314811 }, + { url = "https://files.pythonhosted.org/packages/8a/6d/44ad094874c6f1b9c654f8ed939590bdc408349f137f9b98a3a23ccec411/nvidia_curand_cu12-10.3.5.147-py3-none-manylinux2014_x86_64.whl", hash = "sha256:a88f583d4e0bb643c49743469964103aa59f7f708d862c3ddb0fc07f851e3b8b", size = 56305206 }, +] + +[[package]] +name = "nvidia-cusolver-cu12" +version = "11.6.1.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas-cu12" }, + { name = "nvidia-cusparse-cu12" }, + { name = "nvidia-nvjitlink-cu12" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/6b/a5c33cf16af09166845345275c34ad2190944bcc6026797a39f8e0a282e0/nvidia_cusolver_cu12-11.6.1.9-py3-none-manylinux2014_aarch64.whl", hash = "sha256:d338f155f174f90724bbde3758b7ac375a70ce8e706d70b018dd3375545fc84e", size = 127634111 }, + { url = "https://files.pythonhosted.org/packages/3a/e1/5b9089a4b2a4790dfdea8b3a006052cfecff58139d5a4e34cb1a51df8d6f/nvidia_cusolver_cu12-11.6.1.9-py3-none-manylinux2014_x86_64.whl", hash = "sha256:19e33fa442bcfd085b3086c4ebf7e8debc07cfe01e11513cc6d332fd918ac260", size = 127936057 }, +] + +[[package]] +name = "nvidia-cusparse-cu12" +version = "12.3.1.170" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink-cu12" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/a9/c0d2f83a53d40a4a41be14cea6a0bf9e668ffcf8b004bd65633f433050c0/nvidia_cusparse_cu12-12.3.1.170-py3-none-manylinux2014_aarch64.whl", hash = "sha256:9d32f62896231ebe0480efd8a7f702e143c98cfaa0e8a76df3386c1ba2b54df3", size = 207381987 }, + { url = "https://files.pythonhosted.org/packages/db/f7/97a9ea26ed4bbbfc2d470994b8b4f338ef663be97b8f677519ac195e113d/nvidia_cusparse_cu12-12.3.1.170-py3-none-manylinux2014_x86_64.whl", hash = "sha256:ea4f11a2904e2a8dc4b1833cc1b5181cde564edd0d5cd33e3c168eff2d1863f1", size = 207454763 }, +] + +[[package]] +name = "nvidia-nccl-cu12" +version = "2.21.5" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/99/12cd266d6233f47d00daf3a72739872bdc10267d0383508b0b9c84a18bb6/nvidia_nccl_cu12-2.21.5-py3-none-manylinux2014_x86_64.whl", hash = "sha256:8579076d30a8c24988834445f8d633c697d42397e92ffc3f63fa26766d25e0a0", size = 188654414 }, +] + +[[package]] +name = "nvidia-nvjitlink-cu12" +version = "12.4.127" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/45/239d52c05074898a80a900f49b1615d81c07fceadd5ad6c4f86a987c0bc4/nvidia_nvjitlink_cu12-12.4.127-py3-none-manylinux2014_aarch64.whl", hash = "sha256:4abe7fef64914ccfa909bc2ba39739670ecc9e820c83ccc7a6ed414122599b83", size = 20552510 }, + { url = "https://files.pythonhosted.org/packages/ff/ff/847841bacfbefc97a00036e0fce5a0f086b640756dc38caea5e1bb002655/nvidia_nvjitlink_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl", hash = "sha256:06b3b9b25bf3f8af351d664978ca26a16d2c5127dbd53c0497e28d1fb9611d57", size = 21066810 }, +] + +[[package]] +name = "nvidia-nvtx-cu12" +version = "12.4.127" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/39/471f581edbb7804b39e8063d92fc8305bdc7a80ae5c07dbe6ea5c50d14a5/nvidia_nvtx_cu12-12.4.127-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7959ad635db13edf4fc65c06a6e9f9e55fc2f92596db928d169c0bb031e88ef3", size = 100417 }, + { url = "https://files.pythonhosted.org/packages/87/20/199b8713428322a2f22b722c62b8cc278cc53dffa9705d744484b5035ee9/nvidia_nvtx_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl", hash = "sha256:781e950d9b9f60d8241ccea575b32f5105a5baf4c2351cab5256a24869f12a1a", size = 99144 }, +] + [[package]] name = "oauthlib" version = "3.2.2" @@ -2714,6 +3233,66 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772 }, ] +[[package]] +name = "pgvector" +version = "0.2.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/bb/4686b1090a7c68fa367e981130a074dc6c1236571d914ffa6e05c882b59d/pgvector-0.2.5-py2.py3-none-any.whl", hash = "sha256:5e5e93ec4d3c45ab1fa388729d56c602f6966296e19deee8878928c6d567e41b", size = 9638 }, +] + +[[package]] +name = "pillow" +version = "11.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/26/0d95c04c868f6bdb0c447e3ee2de5564411845e36a858cfd63766bc7b563/pillow-11.0.0.tar.gz", hash = "sha256:72bacbaf24ac003fea9bff9837d1eedb6088758d41e100c1552930151f677739", size = 46737780 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/eb/f7e21b113dd48a9c97d364e0915b3988c6a0b6207652f5a92372871b7aa4/pillow-11.0.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1c1d72714f429a521d8d2d018badc42414c3077eb187a59579f28e4270b4b0fc", size = 3154705 }, + { url = "https://files.pythonhosted.org/packages/25/b3/2b54a1d541accebe6bd8b1358b34ceb2c509f51cb7dcda8687362490da5b/pillow-11.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:499c3a1b0d6fc8213519e193796eb1a86a1be4b1877d678b30f83fd979811d1a", size = 2979222 }, + { url = "https://files.pythonhosted.org/packages/20/12/1a41eddad8265c5c19dda8fb6c269ce15ee25e0b9f8f26286e6202df6693/pillow-11.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8b2351c85d855293a299038e1f89db92a2f35e8d2f783489c6f0b2b5f3fe8a3", size = 4190220 }, + { url = "https://files.pythonhosted.org/packages/a9/9b/8a8c4d07d77447b7457164b861d18f5a31ae6418ef5c07f6f878fa09039a/pillow-11.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f4dba50cfa56f910241eb7f883c20f1e7b1d8f7d91c750cd0b318bad443f4d5", size = 4291399 }, + { url = "https://files.pythonhosted.org/packages/fc/e4/130c5fab4a54d3991129800dd2801feeb4b118d7630148cd67f0e6269d4c/pillow-11.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:5ddbfd761ee00c12ee1be86c9c0683ecf5bb14c9772ddbd782085779a63dd55b", size = 4202709 }, + { url = "https://files.pythonhosted.org/packages/39/63/b3fc299528d7df1f678b0666002b37affe6b8751225c3d9c12cf530e73ed/pillow-11.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:45c566eb10b8967d71bf1ab8e4a525e5a93519e29ea071459ce517f6b903d7fa", size = 4372556 }, + { url = "https://files.pythonhosted.org/packages/c6/a6/694122c55b855b586c26c694937d36bb8d3b09c735ff41b2f315c6e66a10/pillow-11.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b4fd7bd29610a83a8c9b564d457cf5bd92b4e11e79a4ee4716a63c959699b306", size = 4287187 }, + { url = "https://files.pythonhosted.org/packages/ba/a9/f9d763e2671a8acd53d29b1e284ca298bc10a595527f6be30233cdb9659d/pillow-11.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cb929ca942d0ec4fac404cbf520ee6cac37bf35be479b970c4ffadf2b6a1cad9", size = 4418468 }, + { url = "https://files.pythonhosted.org/packages/6e/0e/b5cbad2621377f11313a94aeb44ca55a9639adabcaaa073597a1925f8c26/pillow-11.0.0-cp311-cp311-win32.whl", hash = "sha256:006bcdd307cc47ba43e924099a038cbf9591062e6c50e570819743f5607404f5", size = 2249249 }, + { url = "https://files.pythonhosted.org/packages/dc/83/1470c220a4ff06cd75fc609068f6605e567ea51df70557555c2ab6516b2c/pillow-11.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:52a2d8323a465f84faaba5236567d212c3668f2ab53e1c74c15583cf507a0291", size = 2566769 }, + { url = "https://files.pythonhosted.org/packages/52/98/def78c3a23acee2bcdb2e52005fb2810ed54305602ec1bfcfab2bda6f49f/pillow-11.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:16095692a253047fe3ec028e951fa4221a1f3ed3d80c397e83541a3037ff67c9", size = 2254611 }, + { url = "https://files.pythonhosted.org/packages/1c/a3/26e606ff0b2daaf120543e537311fa3ae2eb6bf061490e4fea51771540be/pillow-11.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d2c0a187a92a1cb5ef2c8ed5412dd8d4334272617f532d4ad4de31e0495bd923", size = 3147642 }, + { url = "https://files.pythonhosted.org/packages/4f/d5/1caabedd8863526a6cfa44ee7a833bd97f945dc1d56824d6d76e11731939/pillow-11.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:084a07ef0821cfe4858fe86652fffac8e187b6ae677e9906e192aafcc1b69903", size = 2978999 }, + { url = "https://files.pythonhosted.org/packages/d9/ff/5a45000826a1aa1ac6874b3ec5a856474821a1b59d838c4f6ce2ee518fe9/pillow-11.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8069c5179902dcdce0be9bfc8235347fdbac249d23bd90514b7a47a72d9fecf4", size = 4196794 }, + { url = "https://files.pythonhosted.org/packages/9d/21/84c9f287d17180f26263b5f5c8fb201de0f88b1afddf8a2597a5c9fe787f/pillow-11.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f02541ef64077f22bf4924f225c0fd1248c168f86e4b7abdedd87d6ebaceab0f", size = 4300762 }, + { url = "https://files.pythonhosted.org/packages/84/39/63fb87cd07cc541438b448b1fed467c4d687ad18aa786a7f8e67b255d1aa/pillow-11.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:fcb4621042ac4b7865c179bb972ed0da0218a076dc1820ffc48b1d74c1e37fe9", size = 4210468 }, + { url = "https://files.pythonhosted.org/packages/7f/42/6e0f2c2d5c60f499aa29be14f860dd4539de322cd8fb84ee01553493fb4d/pillow-11.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:00177a63030d612148e659b55ba99527803288cea7c75fb05766ab7981a8c1b7", size = 4381824 }, + { url = "https://files.pythonhosted.org/packages/31/69/1ef0fb9d2f8d2d114db982b78ca4eeb9db9a29f7477821e160b8c1253f67/pillow-11.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8853a3bf12afddfdf15f57c4b02d7ded92c7a75a5d7331d19f4f9572a89c17e6", size = 4296436 }, + { url = "https://files.pythonhosted.org/packages/44/ea/dad2818c675c44f6012289a7c4f46068c548768bc6c7f4e8c4ae5bbbc811/pillow-11.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3107c66e43bda25359d5ef446f59c497de2b5ed4c7fdba0894f8d6cf3822dafc", size = 4429714 }, + { url = "https://files.pythonhosted.org/packages/af/3a/da80224a6eb15bba7a0dcb2346e2b686bb9bf98378c0b4353cd88e62b171/pillow-11.0.0-cp312-cp312-win32.whl", hash = "sha256:86510e3f5eca0ab87429dd77fafc04693195eec7fd6a137c389c3eeb4cfb77c6", size = 2249631 }, + { url = "https://files.pythonhosted.org/packages/57/97/73f756c338c1d86bb802ee88c3cab015ad7ce4b838f8a24f16b676b1ac7c/pillow-11.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:8ec4a89295cd6cd4d1058a5e6aec6bf51e0eaaf9714774e1bfac7cfc9051db47", size = 2567533 }, + { url = "https://files.pythonhosted.org/packages/0b/30/2b61876e2722374558b871dfbfcbe4e406626d63f4f6ed92e9c8e24cac37/pillow-11.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:27a7860107500d813fcd203b4ea19b04babe79448268403172782754870dac25", size = 2254890 }, + { url = "https://files.pythonhosted.org/packages/63/24/e2e15e392d00fcf4215907465d8ec2a2f23bcec1481a8ebe4ae760459995/pillow-11.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bcd1fb5bb7b07f64c15618c89efcc2cfa3e95f0e3bcdbaf4642509de1942a699", size = 3147300 }, + { url = "https://files.pythonhosted.org/packages/43/72/92ad4afaa2afc233dc44184adff289c2e77e8cd916b3ddb72ac69495bda3/pillow-11.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0e038b0745997c7dcaae350d35859c9715c71e92ffb7e0f4a8e8a16732150f38", size = 2978742 }, + { url = "https://files.pythonhosted.org/packages/9e/da/c8d69c5bc85d72a8523fe862f05ababdc52c0a755cfe3d362656bb86552b/pillow-11.0.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ae08bd8ffc41aebf578c2af2f9d8749d91f448b3bfd41d7d9ff573d74f2a6b2", size = 4194349 }, + { url = "https://files.pythonhosted.org/packages/cd/e8/686d0caeed6b998351d57796496a70185376ed9c8ec7d99e1d19ad591fc6/pillow-11.0.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d69bfd8ec3219ae71bcde1f942b728903cad25fafe3100ba2258b973bd2bc1b2", size = 4298714 }, + { url = "https://files.pythonhosted.org/packages/ec/da/430015cec620d622f06854be67fd2f6721f52fc17fca8ac34b32e2d60739/pillow-11.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:61b887f9ddba63ddf62fd02a3ba7add935d053b6dd7d58998c630e6dbade8527", size = 4208514 }, + { url = "https://files.pythonhosted.org/packages/44/ae/7e4f6662a9b1cb5f92b9cc9cab8321c381ffbee309210940e57432a4063a/pillow-11.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:c6a660307ca9d4867caa8d9ca2c2658ab685de83792d1876274991adec7b93fa", size = 4380055 }, + { url = "https://files.pythonhosted.org/packages/74/d5/1a807779ac8a0eeed57f2b92a3c32ea1b696e6140c15bd42eaf908a261cd/pillow-11.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:73e3a0200cdda995c7e43dd47436c1548f87a30bb27fb871f352a22ab8dcf45f", size = 4296751 }, + { url = "https://files.pythonhosted.org/packages/38/8c/5fa3385163ee7080bc13026d59656267daaaaf3c728c233d530e2c2757c8/pillow-11.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fba162b8872d30fea8c52b258a542c5dfd7b235fb5cb352240c8d63b414013eb", size = 4430378 }, + { url = "https://files.pythonhosted.org/packages/ca/1d/ad9c14811133977ff87035bf426875b93097fb50af747793f013979facdb/pillow-11.0.0-cp313-cp313-win32.whl", hash = "sha256:f1b82c27e89fffc6da125d5eb0ca6e68017faf5efc078128cfaa42cf5cb38798", size = 2249588 }, + { url = "https://files.pythonhosted.org/packages/fb/01/3755ba287dac715e6afdb333cb1f6d69740a7475220b4637b5ce3d78cec2/pillow-11.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:8ba470552b48e5835f1d23ecb936bb7f71d206f9dfeee64245f30c3270b994de", size = 2567509 }, + { url = "https://files.pythonhosted.org/packages/c0/98/2c7d727079b6be1aba82d195767d35fcc2d32204c7a5820f822df5330152/pillow-11.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:846e193e103b41e984ac921b335df59195356ce3f71dcfd155aa79c603873b84", size = 2254791 }, + { url = "https://files.pythonhosted.org/packages/eb/38/998b04cc6f474e78b563716b20eecf42a2fa16a84589d23c8898e64b0ffd/pillow-11.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4ad70c4214f67d7466bea6a08061eba35c01b1b89eaa098040a35272a8efb22b", size = 3150854 }, + { url = "https://files.pythonhosted.org/packages/13/8e/be23a96292113c6cb26b2aa3c8b3681ec62b44ed5c2bd0b258bd59503d3c/pillow-11.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:6ec0d5af64f2e3d64a165f490d96368bb5dea8b8f9ad04487f9ab60dc4bb6003", size = 2982369 }, + { url = "https://files.pythonhosted.org/packages/97/8a/3db4eaabb7a2ae8203cd3a332a005e4aba00067fc514aaaf3e9721be31f1/pillow-11.0.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c809a70e43c7977c4a42aefd62f0131823ebf7dd73556fa5d5950f5b354087e2", size = 4333703 }, + { url = "https://files.pythonhosted.org/packages/28/ac/629ffc84ff67b9228fe87a97272ab125bbd4dc462745f35f192d37b822f1/pillow-11.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:4b60c9520f7207aaf2e1d94de026682fc227806c6e1f55bba7606d1c94dd623a", size = 4412550 }, + { url = "https://files.pythonhosted.org/packages/d6/07/a505921d36bb2df6868806eaf56ef58699c16c388e378b0dcdb6e5b2fb36/pillow-11.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1e2688958a840c822279fda0086fec1fdab2f95bf2b717b66871c4ad9859d7e8", size = 4461038 }, + { url = "https://files.pythonhosted.org/packages/d6/b9/fb620dd47fc7cc9678af8f8bd8c772034ca4977237049287e99dda360b66/pillow-11.0.0-cp313-cp313t-win32.whl", hash = "sha256:607bbe123c74e272e381a8d1957083a9463401f7bd01287f50521ecb05a313f8", size = 2253197 }, + { url = "https://files.pythonhosted.org/packages/df/86/25dde85c06c89d7fc5db17940f07aae0a56ac69aa9ccb5eb0f09798862a8/pillow-11.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5c39ed17edea3bc69c743a8dd3e9853b7509625c2462532e62baa0732163a904", size = 2572169 }, + { url = "https://files.pythonhosted.org/packages/51/85/9c33f2517add612e17f3381aee7c4072779130c634921a756c97bc29fb49/pillow-11.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:75acbbeb05b86bc53cbe7b7e6fe00fbcf82ad7c684b3ad82e3d711da9ba287d3", size = 2256828 }, +] + [[package]] name = "platformdirs" version = "4.3.6" @@ -2884,6 +3463,48 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/05/33/2d74d588408caedd065c2497bdb5ef83ce6082db01289a1e1147f6639802/psutil-5.9.8-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:d16bbddf0693323b8c6123dd804100241da461e41d6e332fb0ba6058f630f8c8", size = 249898 }, ] +[[package]] +name = "psycopg2-binary" +version = "2.9.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cb/0e/bdc8274dc0585090b4e3432267d7be4dfbfd8971c0fa59167c711105a6bf/psycopg2-binary-2.9.10.tar.gz", hash = "sha256:4b3df0e6990aa98acda57d983942eff13d824135fe2250e6522edaa782a06de2", size = 385764 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/8f/9feb01291d0d7a0a4c6a6bab24094135c2b59c6a81943752f632c75896d6/psycopg2_binary-2.9.10-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:04392983d0bb89a8717772a193cfaac58871321e3ec69514e1c4e0d4957b5aff", size = 3043397 }, + { url = "https://files.pythonhosted.org/packages/15/30/346e4683532011561cd9c8dfeac6a8153dd96452fee0b12666058ab7893c/psycopg2_binary-2.9.10-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:1a6784f0ce3fec4edc64e985865c17778514325074adf5ad8f80636cd029ef7c", size = 3274806 }, + { url = "https://files.pythonhosted.org/packages/66/6e/4efebe76f76aee7ec99166b6c023ff8abdc4e183f7b70913d7c047701b79/psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5f86c56eeb91dc3135b3fd8a95dc7ae14c538a2f3ad77a19645cf55bab1799c", size = 2851370 }, + { url = "https://files.pythonhosted.org/packages/7f/fd/ff83313f86b50f7ca089b161b8e0a22bb3c319974096093cd50680433fdb/psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b3d2491d4d78b6b14f76881905c7a8a8abcf974aad4a8a0b065273a0ed7a2cb", size = 3080780 }, + { url = "https://files.pythonhosted.org/packages/e6/c4/bfadd202dcda8333a7ccafdc51c541dbdfce7c2c7cda89fa2374455d795f/psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2286791ececda3a723d1910441c793be44625d86d1a4e79942751197f4d30341", size = 3264583 }, + { url = "https://files.pythonhosted.org/packages/5d/f1/09f45ac25e704ac954862581f9f9ae21303cc5ded3d0b775532b407f0e90/psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:512d29bb12608891e349af6a0cccedce51677725a921c07dba6342beaf576f9a", size = 3019831 }, + { url = "https://files.pythonhosted.org/packages/9e/2e/9beaea078095cc558f215e38f647c7114987d9febfc25cb2beed7c3582a5/psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5a507320c58903967ef7384355a4da7ff3f28132d679aeb23572753cbf2ec10b", size = 2871822 }, + { url = "https://files.pythonhosted.org/packages/01/9e/ef93c5d93f3dc9fc92786ffab39e323b9aed066ba59fdc34cf85e2722271/psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6d4fa1079cab9018f4d0bd2db307beaa612b0d13ba73b5c6304b9fe2fb441ff7", size = 2820975 }, + { url = "https://files.pythonhosted.org/packages/a5/f0/049e9631e3268fe4c5a387f6fc27e267ebe199acf1bc1bc9cbde4bd6916c/psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:851485a42dbb0bdc1edcdabdb8557c09c9655dfa2ca0460ff210522e073e319e", size = 2919320 }, + { url = "https://files.pythonhosted.org/packages/dc/9a/bcb8773b88e45fb5a5ea8339e2104d82c863a3b8558fbb2aadfe66df86b3/psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:35958ec9e46432d9076286dda67942ed6d968b9c3a6a2fd62b48939d1d78bf68", size = 2957617 }, + { url = "https://files.pythonhosted.org/packages/e2/6b/144336a9bf08a67d217b3af3246abb1d027095dab726f0687f01f43e8c03/psycopg2_binary-2.9.10-cp311-cp311-win32.whl", hash = "sha256:ecced182e935529727401b24d76634a357c71c9275b356efafd8a2a91ec07392", size = 1024618 }, + { url = "https://files.pythonhosted.org/packages/61/69/3b3d7bd583c6d3cbe5100802efa5beacaacc86e37b653fc708bf3d6853b8/psycopg2_binary-2.9.10-cp311-cp311-win_amd64.whl", hash = "sha256:ee0e8c683a7ff25d23b55b11161c2663d4b099770f6085ff0a20d4505778d6b4", size = 1163816 }, + { url = "https://files.pythonhosted.org/packages/49/7d/465cc9795cf76f6d329efdafca74693714556ea3891813701ac1fee87545/psycopg2_binary-2.9.10-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:880845dfe1f85d9d5f7c412efea7a08946a46894537e4e5d091732eb1d34d9a0", size = 3044771 }, + { url = "https://files.pythonhosted.org/packages/8b/31/6d225b7b641a1a2148e3ed65e1aa74fc86ba3fee850545e27be9e1de893d/psycopg2_binary-2.9.10-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9440fa522a79356aaa482aa4ba500b65f28e5d0e63b801abf6aa152a29bd842a", size = 3275336 }, + { url = "https://files.pythonhosted.org/packages/30/b7/a68c2b4bff1cbb1728e3ec864b2d92327c77ad52edcd27922535a8366f68/psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3923c1d9870c49a2d44f795df0c889a22380d36ef92440ff618ec315757e539", size = 2851637 }, + { url = "https://files.pythonhosted.org/packages/0b/b1/cfedc0e0e6f9ad61f8657fd173b2f831ce261c02a08c0b09c652b127d813/psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b2c956c028ea5de47ff3a8d6b3cc3330ab45cf0b7c3da35a2d6ff8420896526", size = 3082097 }, + { url = "https://files.pythonhosted.org/packages/18/ed/0a8e4153c9b769f59c02fb5e7914f20f0b2483a19dae7bf2db54b743d0d0/psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f758ed67cab30b9a8d2833609513ce4d3bd027641673d4ebc9c067e4d208eec1", size = 3264776 }, + { url = "https://files.pythonhosted.org/packages/10/db/d09da68c6a0cdab41566b74e0a6068a425f077169bed0946559b7348ebe9/psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cd9b4f2cfab88ed4a9106192de509464b75a906462fb846b936eabe45c2063e", size = 3020968 }, + { url = "https://files.pythonhosted.org/packages/94/28/4d6f8c255f0dfffb410db2b3f9ac5218d959a66c715c34cac31081e19b95/psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dc08420625b5a20b53551c50deae6e231e6371194fa0651dbe0fb206452ae1f", size = 2872334 }, + { url = "https://files.pythonhosted.org/packages/05/f7/20d7bf796593c4fea95e12119d6cc384ff1f6141a24fbb7df5a668d29d29/psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d7cd730dfa7c36dbe8724426bf5612798734bff2d3c3857f36f2733f5bfc7c00", size = 2822722 }, + { url = "https://files.pythonhosted.org/packages/4d/e4/0c407ae919ef626dbdb32835a03b6737013c3cc7240169843965cada2bdf/psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:155e69561d54d02b3c3209545fb08938e27889ff5a10c19de8d23eb5a41be8a5", size = 2920132 }, + { url = "https://files.pythonhosted.org/packages/2d/70/aa69c9f69cf09a01da224909ff6ce8b68faeef476f00f7ec377e8f03be70/psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3cc28a6fd5a4a26224007712e79b81dbaee2ffb90ff406256158ec4d7b52b47", size = 2959312 }, + { url = "https://files.pythonhosted.org/packages/d3/bd/213e59854fafe87ba47814bf413ace0dcee33a89c8c8c814faca6bc7cf3c/psycopg2_binary-2.9.10-cp312-cp312-win32.whl", hash = "sha256:ec8a77f521a17506a24a5f626cb2aee7850f9b69a0afe704586f63a464f3cd64", size = 1025191 }, + { url = "https://files.pythonhosted.org/packages/92/29/06261ea000e2dc1e22907dbbc483a1093665509ea586b29b8986a0e56733/psycopg2_binary-2.9.10-cp312-cp312-win_amd64.whl", hash = "sha256:18c5ee682b9c6dd3696dad6e54cc7ff3a1a9020df6a5c0f861ef8bfd338c3ca0", size = 1164031 }, + { url = "https://files.pythonhosted.org/packages/3e/30/d41d3ba765609c0763505d565c4d12d8f3c79793f0d0f044ff5a28bf395b/psycopg2_binary-2.9.10-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:26540d4a9a4e2b096f1ff9cce51253d0504dca5a85872c7f7be23be5a53eb18d", size = 3044699 }, + { url = "https://files.pythonhosted.org/packages/35/44/257ddadec7ef04536ba71af6bc6a75ec05c5343004a7ec93006bee66c0bc/psycopg2_binary-2.9.10-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e217ce4d37667df0bc1c397fdcd8de5e81018ef305aed9415c3b093faaeb10fb", size = 3275245 }, + { url = "https://files.pythonhosted.org/packages/1b/11/48ea1cd11de67f9efd7262085588790a95d9dfcd9b8a687d46caf7305c1a/psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:245159e7ab20a71d989da00f280ca57da7641fa2cdcf71749c193cea540a74f7", size = 2851631 }, + { url = "https://files.pythonhosted.org/packages/62/e0/62ce5ee650e6c86719d621a761fe4bc846ab9eff8c1f12b1ed5741bf1c9b/psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c4ded1a24b20021ebe677b7b08ad10bf09aac197d6943bfe6fec70ac4e4690d", size = 3082140 }, + { url = "https://files.pythonhosted.org/packages/27/ce/63f946c098611f7be234c0dd7cb1ad68b0b5744d34f68062bb3c5aa510c8/psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3abb691ff9e57d4a93355f60d4f4c1dd2d68326c968e7db17ea96df3c023ef73", size = 3264762 }, + { url = "https://files.pythonhosted.org/packages/43/25/c603cd81402e69edf7daa59b1602bd41eb9859e2824b8c0855d748366ac9/psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8608c078134f0b3cbd9f89b34bd60a943b23fd33cc5f065e8d5f840061bd0673", size = 3020967 }, + { url = "https://files.pythonhosted.org/packages/5f/d6/8708d8c6fca531057fa170cdde8df870e8b6a9b136e82b361c65e42b841e/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:230eeae2d71594103cd5b93fd29d1ace6420d0b86f4778739cb1a5a32f607d1f", size = 2872326 }, + { url = "https://files.pythonhosted.org/packages/ce/ac/5b1ea50fc08a9df82de7e1771537557f07c2632231bbab652c7e22597908/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bb89f0a835bcfc1d42ccd5f41f04870c1b936d8507c6df12b7737febc40f0909", size = 2822712 }, + { url = "https://files.pythonhosted.org/packages/c4/fc/504d4503b2abc4570fac3ca56eb8fed5e437bf9c9ef13f36b6621db8ef00/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f0c2d907a1e102526dd2986df638343388b94c33860ff3bbe1384130828714b1", size = 2920155 }, + { url = "https://files.pythonhosted.org/packages/b2/d1/323581e9273ad2c0dbd1902f3fb50c441da86e894b6e25a73c3fda32c57e/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f8157bed2f51db683f31306aa497311b560f2265998122abe1dce6428bd86567", size = 2959356 }, +] + [[package]] name = "ptyprocess" version = "0.7.0" @@ -3589,6 +4210,70 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ad/1b/81855a88c6db2b114d5b2e9f96339190d5ee4d1b981d217fa32127bb00e0/schema-0.7.7-py2.py3-none-any.whl", hash = "sha256:5d976a5b50f36e74e2157b47097b60002bd4d42e65425fcc9c9befadb4255dde", size = 18632 }, ] +[[package]] +name = "scikit-learn" +version = "1.5.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "joblib" }, + { name = "numpy" }, + { name = "scipy" }, + { name = "threadpoolctl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/37/59/44985a2bdc95c74e34fef3d10cb5d93ce13b0e2a7baefffe1b53853b502d/scikit_learn-1.5.2.tar.gz", hash = "sha256:b4237ed7b3fdd0a4882792e68ef2545d5baa50aca3bb45aa7df468138ad8f94d", size = 7001680 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/91/609961972f694cb9520c4c3d201e377a26583e1eb83bc5a334c893729214/scikit_learn-1.5.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:03b6158efa3faaf1feea3faa884c840ebd61b6484167c711548fce208ea09445", size = 12088580 }, + { url = "https://files.pythonhosted.org/packages/cd/7a/19fe32c810c5ceddafcfda16276d98df299c8649e24e84d4f00df4a91e01/scikit_learn-1.5.2-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:1ff45e26928d3b4eb767a8f14a9a6efbf1cbff7c05d1fb0f95f211a89fd4f5de", size = 10975994 }, + { url = "https://files.pythonhosted.org/packages/4c/75/62e49f8a62bf3c60b0e64d0fce540578ee4f0e752765beb2e1dc7c6d6098/scikit_learn-1.5.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f763897fe92d0e903aa4847b0aec0e68cadfff77e8a0687cabd946c89d17e675", size = 12465782 }, + { url = "https://files.pythonhosted.org/packages/49/21/3723de321531c9745e40f1badafd821e029d346155b6c79704e0b7197552/scikit_learn-1.5.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8b0ccd4a902836493e026c03256e8b206656f91fbcc4fde28c57a5b752561f1", size = 13322034 }, + { url = "https://files.pythonhosted.org/packages/17/1c/ccdd103cfcc9435a18819856fbbe0c20b8fa60bfc3343580de4be13f0668/scikit_learn-1.5.2-cp311-cp311-win_amd64.whl", hash = "sha256:6c16d84a0d45e4894832b3c4d0bf73050939e21b99b01b6fd59cbb0cf39163b6", size = 11015224 }, + { url = "https://files.pythonhosted.org/packages/a4/db/b485c1ac54ff3bd9e7e6b39d3cc6609c4c76a65f52ab0a7b22b6c3ab0e9d/scikit_learn-1.5.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f932a02c3f4956dfb981391ab24bda1dbd90fe3d628e4b42caef3e041c67707a", size = 12110344 }, + { url = "https://files.pythonhosted.org/packages/54/1a/7deb52fa23aebb855431ad659b3c6a2e1709ece582cb3a63d66905e735fe/scikit_learn-1.5.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:3b923d119d65b7bd555c73be5423bf06c0105678ce7e1f558cb4b40b0a5502b1", size = 11033502 }, + { url = "https://files.pythonhosted.org/packages/a1/32/4a7a205b14c11225609b75b28402c196e4396ac754dab6a81971b811781c/scikit_learn-1.5.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f60021ec1574e56632be2a36b946f8143bf4e5e6af4a06d85281adc22938e0dd", size = 12085794 }, + { url = "https://files.pythonhosted.org/packages/c6/29/044048c5e911373827c0e1d3051321b9183b2a4f8d4e2f11c08fcff83f13/scikit_learn-1.5.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:394397841449853c2290a32050382edaec3da89e35b3e03d6cc966aebc6a8ae6", size = 12945797 }, + { url = "https://files.pythonhosted.org/packages/aa/ce/c0b912f2f31aeb1b756a6ba56bcd84dd1f8a148470526a48515a3f4d48cd/scikit_learn-1.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:57cc1786cfd6bd118220a92ede80270132aa353647684efa385a74244a41e3b1", size = 10985467 }, + { url = "https://files.pythonhosted.org/packages/a4/50/8891028437858cc510e13578fe7046574a60c2aaaa92b02d64aac5b1b412/scikit_learn-1.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9a702e2de732bbb20d3bad29ebd77fc05a6b427dc49964300340e4c9328b3f5", size = 12025584 }, + { url = "https://files.pythonhosted.org/packages/d2/79/17feef8a1c14149436083bec0e61d7befb4812e272d5b20f9d79ea3e9ab1/scikit_learn-1.5.2-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:b0768ad641981f5d3a198430a1d31c3e044ed2e8a6f22166b4d546a5116d7908", size = 10959795 }, + { url = "https://files.pythonhosted.org/packages/b1/c8/f08313f9e2e656bd0905930ae8bf99a573ea21c34666a813b749c338202f/scikit_learn-1.5.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:178ddd0a5cb0044464fc1bfc4cca5b1833bfc7bb022d70b05db8530da4bb3dd3", size = 12077302 }, + { url = "https://files.pythonhosted.org/packages/a7/48/fbfb4dc72bed0fe31fe045fb30e924909ad03f717c36694351612973b1a9/scikit_learn-1.5.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7284ade780084d94505632241bf78c44ab3b6f1e8ccab3d2af58e0e950f9c12", size = 13002811 }, + { url = "https://files.pythonhosted.org/packages/a5/e7/0c869f9e60d225a77af90d2aefa7a4a4c0e745b149325d1450f0f0ce5399/scikit_learn-1.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:b7b0f9a0b1040830d38c39b91b3a44e1b643f4b36e36567b80b7c6bd2202a27f", size = 10951354 }, +] + +[[package]] +name = "scipy" +version = "1.14.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/62/11/4d44a1f274e002784e4dbdb81e0ea96d2de2d1045b2132d5af62cc31fd28/scipy-1.14.1.tar.gz", hash = "sha256:5a275584e726026a5699459aa72f828a610821006228e841b94275c4a7c08417", size = 58620554 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/ab/070ccfabe870d9f105b04aee1e2860520460ef7ca0213172abfe871463b9/scipy-1.14.1-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:2da0469a4ef0ecd3693761acbdc20f2fdeafb69e6819cc081308cc978153c675", size = 39076999 }, + { url = "https://files.pythonhosted.org/packages/a7/c5/02ac82f9bb8f70818099df7e86c3ad28dae64e1347b421d8e3adf26acab6/scipy-1.14.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:c0ee987efa6737242745f347835da2cc5bb9f1b42996a4d97d5c7ff7928cb6f2", size = 29894570 }, + { url = "https://files.pythonhosted.org/packages/ed/05/7f03e680cc5249c4f96c9e4e845acde08eb1aee5bc216eff8a089baa4ddb/scipy-1.14.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3a1b111fac6baec1c1d92f27e76511c9e7218f1695d61b59e05e0fe04dc59617", size = 23103567 }, + { url = "https://files.pythonhosted.org/packages/5e/fc/9f1413bef53171f379d786aabc104d4abeea48ee84c553a3e3d8c9f96a9c/scipy-1.14.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8475230e55549ab3f207bff11ebfc91c805dc3463ef62eda3ccf593254524ce8", size = 25499102 }, + { url = "https://files.pythonhosted.org/packages/c2/4b/b44bee3c2ddc316b0159b3d87a3d467ef8d7edfd525e6f7364a62cd87d90/scipy-1.14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:278266012eb69f4a720827bdd2dc54b2271c97d84255b2faaa8f161a158c3b37", size = 35586346 }, + { url = "https://files.pythonhosted.org/packages/93/6b/701776d4bd6bdd9b629c387b5140f006185bd8ddea16788a44434376b98f/scipy-1.14.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fef8c87f8abfb884dac04e97824b61299880c43f4ce675dd2cbeadd3c9b466d2", size = 41165244 }, + { url = "https://files.pythonhosted.org/packages/06/57/e6aa6f55729a8f245d8a6984f2855696c5992113a5dc789065020f8be753/scipy-1.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b05d43735bb2f07d689f56f7b474788a13ed8adc484a85aa65c0fd931cf9ccd2", size = 42817917 }, + { url = "https://files.pythonhosted.org/packages/ea/c2/5ecadc5fcccefaece775feadcd795060adf5c3b29a883bff0e678cfe89af/scipy-1.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:716e389b694c4bb564b4fc0c51bc84d381735e0d39d3f26ec1af2556ec6aad94", size = 44781033 }, + { url = "https://files.pythonhosted.org/packages/c0/04/2bdacc8ac6387b15db6faa40295f8bd25eccf33f1f13e68a72dc3c60a99e/scipy-1.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:631f07b3734d34aced009aaf6fedfd0eb3498a97e581c3b1e5f14a04164a456d", size = 39128781 }, + { url = "https://files.pythonhosted.org/packages/c8/53/35b4d41f5fd42f5781dbd0dd6c05d35ba8aa75c84ecddc7d44756cd8da2e/scipy-1.14.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:af29a935803cc707ab2ed7791c44288a682f9c8107bc00f0eccc4f92c08d6e07", size = 29939542 }, + { url = "https://files.pythonhosted.org/packages/66/67/6ef192e0e4d77b20cc33a01e743b00bc9e68fb83b88e06e636d2619a8767/scipy-1.14.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:2843f2d527d9eebec9a43e6b406fb7266f3af25a751aa91d62ff416f54170bc5", size = 23148375 }, + { url = "https://files.pythonhosted.org/packages/f6/32/3a6dedd51d68eb7b8e7dc7947d5d841bcb699f1bf4463639554986f4d782/scipy-1.14.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:eb58ca0abd96911932f688528977858681a59d61a7ce908ffd355957f7025cfc", size = 25578573 }, + { url = "https://files.pythonhosted.org/packages/f0/5a/efa92a58dc3a2898705f1dc9dbaf390ca7d4fba26d6ab8cfffb0c72f656f/scipy-1.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30ac8812c1d2aab7131a79ba62933a2a76f582d5dbbc695192453dae67ad6310", size = 35319299 }, + { url = "https://files.pythonhosted.org/packages/8e/ee/8a26858ca517e9c64f84b4c7734b89bda8e63bec85c3d2f432d225bb1886/scipy-1.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f9ea80f2e65bdaa0b7627fb00cbeb2daf163caa015e59b7516395fe3bd1e066", size = 40849331 }, + { url = "https://files.pythonhosted.org/packages/a5/cd/06f72bc9187840f1c99e1a8750aad4216fc7dfdd7df46e6280add14b4822/scipy-1.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:edaf02b82cd7639db00dbff629995ef185c8df4c3ffa71a5562a595765a06ce1", size = 42544049 }, + { url = "https://files.pythonhosted.org/packages/aa/7d/43ab67228ef98c6b5dd42ab386eae2d7877036970a0d7e3dd3eb47a0d530/scipy-1.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:2ff38e22128e6c03ff73b6bb0f85f897d2362f8c052e3b8ad00532198fbdae3f", size = 44521212 }, + { url = "https://files.pythonhosted.org/packages/50/ef/ac98346db016ff18a6ad7626a35808f37074d25796fd0234c2bb0ed1e054/scipy-1.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1729560c906963fc8389f6aac023739ff3983e727b1a4d87696b7bf108316a79", size = 39091068 }, + { url = "https://files.pythonhosted.org/packages/b9/cc/70948fe9f393b911b4251e96b55bbdeaa8cca41f37c26fd1df0232933b9e/scipy-1.14.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:4079b90df244709e675cdc8b93bfd8a395d59af40b72e339c2287c91860deb8e", size = 29875417 }, + { url = "https://files.pythonhosted.org/packages/3b/2e/35f549b7d231c1c9f9639f9ef49b815d816bf54dd050da5da1c11517a218/scipy-1.14.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e0cf28db0f24a38b2a0ca33a85a54852586e43cf6fd876365c86e0657cfe7d73", size = 23084508 }, + { url = "https://files.pythonhosted.org/packages/3f/d6/b028e3f3e59fae61fb8c0f450db732c43dd1d836223a589a8be9f6377203/scipy-1.14.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:0c2f95de3b04e26f5f3ad5bb05e74ba7f68b837133a4492414b3afd79dfe540e", size = 25503364 }, + { url = "https://files.pythonhosted.org/packages/a7/2f/6c142b352ac15967744d62b165537a965e95d557085db4beab2a11f7943b/scipy-1.14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b99722ea48b7ea25e8e015e8341ae74624f72e5f21fc2abd45f3a93266de4c5d", size = 35292639 }, + { url = "https://files.pythonhosted.org/packages/56/46/2449e6e51e0d7c3575f289f6acb7f828938eaab8874dbccfeb0cd2b71a27/scipy-1.14.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5149e3fd2d686e42144a093b206aef01932a0059c2a33ddfa67f5f035bdfe13e", size = 40798288 }, + { url = "https://files.pythonhosted.org/packages/32/cd/9d86f7ed7f4497c9fd3e39f8918dd93d9f647ba80d7e34e4946c0c2d1a7c/scipy-1.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e4f5a7c49323533f9103d4dacf4e4f07078f360743dec7f7596949149efeec06", size = 42524647 }, + { url = "https://files.pythonhosted.org/packages/f5/1b/6ee032251bf4cdb0cc50059374e86a9f076308c1512b61c4e003e241efb7/scipy-1.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:baff393942b550823bfce952bb62270ee17504d02a1801d7fd0719534dfb9c84", size = 44469524 }, +] + [[package]] name = "selenium" version = "4.25.0" @@ -3606,6 +4291,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/aa/85/fa44f23dd5d5066a72f7c4304cce4b5ff9a6e7fd92431a48b2c63fbf63ec/selenium-4.25.0-py3-none-any.whl", hash = "sha256:3798d2d12b4a570bc5790163ba57fef10b2afee958bf1d80f2a3cf07c4141f33", size = 9693127 }, ] +[[package]] +name = "sentence-transformers" +version = "3.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, + { name = "pillow" }, + { name = "scikit-learn" }, + { name = "scipy" }, + { name = "torch" }, + { name = "tqdm" }, + { name = "transformers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/79/0a/c677efe908b20e7e8d4ed6cce3a3447eebc7dc5e348e458f5f9a44a72b00/sentence_transformers-3.3.1.tar.gz", hash = "sha256:9635dbfb11c6b01d036b9cfcee29f7716ab64cf2407ad9f403a2e607da2ac48b", size = 217914 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/c8/990e22a465e4771338da434d799578865d6d7ef1fdb50bd844b7ecdcfa19/sentence_transformers-3.3.1-py3-none-any.whl", hash = "sha256:abffcc79dab37b7d18d21a26d5914223dd42239cfe18cb5e111c66c54b658ae7", size = 268797 }, +] + [[package]] name = "sentry-sdk" version = "2.19.0" @@ -3739,6 +4442,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b8/49/21633706dd6feb14cd3f7935fc00b60870ea057686035e1a99ae6d9d9d53/SQLAlchemy-2.0.36-py3-none-any.whl", hash = "sha256:fddbe92b4760c6f5d48162aef14824add991aeda8ddadb3c31d56eb15ca69f8e", size = 1883787 }, ] +[package.optional-dependencies] +asyncio = [ + { name = "greenlet" }, +] + [[package]] name = "stack-data" version = "0.6.3" @@ -3777,16 +4485,25 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/50/70762bdb23f6c2b746b90661f461d33c4913a22a46bb5265b10947e85ffb/stevedore-5.3.0-py3-none-any.whl", hash = "sha256:1efd34ca08f474dad08d9b19e934a22c68bb6fe416926479ba29e5013bcc8f78", size = 49661 }, ] +[[package]] +name = "striprtf" +version = "0.0.26" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/25/20/3d419008265346452d09e5dadfd5d045b64b40d8fc31af40588e6c76997a/striprtf-0.0.26.tar.gz", hash = "sha256:fdb2bba7ac440072d1c41eab50d8d74ae88f60a8b6575c6e2c7805dc462093aa", size = 6258 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/cf/0fea4f4ba3fc2772ac2419278aa9f6964124d4302117d61bc055758e000c/striprtf-0.0.26-py3-none-any.whl", hash = "sha256:8c8f9d32083cdc2e8bfb149455aa1cc5a4e0a035893bedc75db8b73becb3a1bb", size = 6914 }, +] + [[package]] name = "sympy" -version = "1.13.3" +version = "1.13.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mpmath" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/11/8a/5a7fd6284fa8caac23a26c9ddf9c30485a48169344b4bd3b0f02fef1890f/sympy-1.13.3.tar.gz", hash = "sha256:b27fd2c6530e0ab39e275fc9b683895367e51d5da91baa8d3d64db2565fec4d9", size = 7533196 } +sdist = { url = "https://files.pythonhosted.org/packages/ca/99/5a5b6f19ff9f083671ddf7b9632028436167cd3d33e11015754e41b249a4/sympy-1.13.1.tar.gz", hash = "sha256:9cebf7e04ff162015ce31c9c6c9144daa34a93bd082f54fd8f12deca4f47515f", size = 7533040 } wheels = [ - { url = "https://files.pythonhosted.org/packages/99/ff/c87e0622b1dadea79d2fb0b25ade9ed98954c9033722eb707053d310d4f3/sympy-1.13.3-py3-none-any.whl", hash = "sha256:54612cf55a62755ee71824ce692986f23c88ffa77207b30c1368eda4a7060f73", size = 6189483 }, + { url = "https://files.pythonhosted.org/packages/b2/fe/81695a1aa331a842b582453b605175f419fe8540355886031328089d840a/sympy-1.13.1-py3-none-any.whl", hash = "sha256:db36cdc64bf61b9b24578b6f7bab1ecdd2452cf008f34faa33776680c26d66f8", size = 6189177 }, ] [[package]] @@ -3800,11 +4517,11 @@ wheels = [ [[package]] name = "tenacity" -version = "9.0.0" +version = "8.5.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cd/94/91fccdb4b8110642462e653d5dcb27e7b674742ad68efd146367da7bdb10/tenacity-9.0.0.tar.gz", hash = "sha256:807f37ca97d62aa361264d497b0e31e92b8027044942bfa756160d908320d73b", size = 47421 } +sdist = { url = "https://files.pythonhosted.org/packages/a3/4d/6a19536c50b849338fcbe9290d562b52cbdcf30d8963d3588a68a4107df1/tenacity-8.5.0.tar.gz", hash = "sha256:8bc6c0c8a09b31e6cad13c47afbed1a567518250a9a171418582ed8d9c20ca78", size = 47309 } wheels = [ - { url = "https://files.pythonhosted.org/packages/b6/cb/b86984bed139586d01532a587464b5805f12e397594f19f931c4c2fbfa61/tenacity-9.0.0-py3-none-any.whl", hash = "sha256:93de0c98785b27fcf659856aa9f54bfbd399e29969b0621bc7f762bd441b4539", size = 28169 }, + { url = "https://files.pythonhosted.org/packages/d2/3f/8ba87d9e287b9d385a02a7114ddcef61b26f86411e121c9003eb509a1773/tenacity-8.5.0-py3-none-any.whl", hash = "sha256:b594c2a5945830c267ce6b79a166228323ed52718f30302c1359836112346687", size = 28165 }, ] [[package]] @@ -3816,6 +4533,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d9/5f/8c716e47b3a50cbd7c146f45881e11d9414def768b7cd9c5e6650ec2a80a/termcolor-2.4.0-py3-none-any.whl", hash = "sha256:9297c0df9c99445c2412e832e882a7884038a25617c60cea2ad69488d4040d63", size = 7719 }, ] +[[package]] +name = "threadpoolctl" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/55/b5148dcbf72f5cde221f8bfe3b6a540da7aa1842f6b491ad979a6c8b84af/threadpoolctl-3.5.0.tar.gz", hash = "sha256:082433502dd922bf738de0d8bcc4fdcbf0979ff44c42bd40f5af8a282f6fa107", size = 41936 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/2c/ffbf7a134b9ab11a67b0cf0726453cedd9c5043a4fe7a35d1cefa9a1bcfb/threadpoolctl-3.5.0-py3-none-any.whl", hash = "sha256:56c1e26c150397e58c4926da8eeee87533b1e32bef131bd4bf6a2f45f3185467", size = 18414 }, +] + [[package]] name = "tiktoken" version = "0.7.0" @@ -3895,6 +4621,44 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c4/ac/ce90573ba446a9bbe65838ded066a805234d159b4446ae9f8ec5bbd36cbd/tomli_w-1.1.0-py3-none-any.whl", hash = "sha256:1403179c78193e3184bfaade390ddbd071cba48a32a2e62ba11aae47490c63f7", size = 6440 }, ] +[[package]] +name = "torch" +version = "2.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "fsspec" }, + { name = "jinja2" }, + { name = "networkx" }, + { name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" }, + { name = "nvidia-cuda-cupti-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" }, + { name = "nvidia-cuda-nvrtc-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" }, + { name = "nvidia-cuda-runtime-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" }, + { name = "nvidia-cudnn-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" }, + { name = "nvidia-cufft-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" }, + { name = "nvidia-curand-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" }, + { name = "nvidia-cusolver-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" }, + { name = "nvidia-cusparse-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" }, + { name = "nvidia-nccl-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" }, + { name = "nvidia-nvjitlink-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" }, + { name = "nvidia-nvtx-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" }, + { name = "setuptools", marker = "python_full_version >= '3.12'" }, + { name = "sympy" }, + { name = "triton", marker = "python_full_version < '3.13' and platform_machine == 'x86_64' and platform_system == 'Linux'" }, + { name = "typing-extensions" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/35/e8b2daf02ce933e4518e6f5682c72fd0ed66c15910ea1fb4168f442b71c4/torch-2.5.1-cp311-cp311-manylinux1_x86_64.whl", hash = "sha256:de5b7d6740c4b636ef4db92be922f0edc425b65ed78c5076c43c42d362a45457", size = 906474467 }, + { url = "https://files.pythonhosted.org/packages/40/04/bd91593a4ca178ece93ca55f27e2783aa524aaccbfda66831d59a054c31e/torch-2.5.1-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:340ce0432cad0d37f5a31be666896e16788f1adf8ad7be481196b503dad675b9", size = 91919450 }, + { url = "https://files.pythonhosted.org/packages/0d/4a/e51420d46cfc90562e85af2fee912237c662ab31140ab179e49bd69401d6/torch-2.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:603c52d2fe06433c18b747d25f5c333f9c1d58615620578c326d66f258686f9a", size = 203098237 }, + { url = "https://files.pythonhosted.org/packages/d0/db/5d9cbfbc7968d79c5c09a0bc0bc3735da079f2fd07cc10498a62b320a480/torch-2.5.1-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:31f8c39660962f9ae4eeec995e3049b5492eb7360dd4f07377658ef4d728fa4c", size = 63884466 }, + { url = "https://files.pythonhosted.org/packages/8b/5c/36c114d120bfe10f9323ed35061bc5878cc74f3f594003854b0ea298942f/torch-2.5.1-cp312-cp312-manylinux1_x86_64.whl", hash = "sha256:ed231a4b3a5952177fafb661213d690a72caaad97d5824dd4fc17ab9e15cec03", size = 906389343 }, + { url = "https://files.pythonhosted.org/packages/6d/69/d8ada8b6e0a4257556d5b4ddeb4345ea8eeaaef3c98b60d1cca197c7ad8e/torch-2.5.1-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:3f4b7f10a247e0dcd7ea97dc2d3bfbfc90302ed36d7f3952b0008d0df264e697", size = 91811673 }, + { url = "https://files.pythonhosted.org/packages/5f/ba/607d013b55b9fd805db2a5c2662ec7551f1910b4eef39653eeaba182c5b2/torch-2.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:73e58e78f7d220917c5dbfad1a40e09df9929d3b95d25e57d9f8558f84c9a11c", size = 203046841 }, + { url = "https://files.pythonhosted.org/packages/57/6c/bf52ff061da33deb9f94f4121fde7ff3058812cb7d2036c97bc167793bd1/torch-2.5.1-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:8c712df61101964eb11910a846514011f0b6f5920c55dbf567bff8a34163d5b1", size = 63858109 }, + { url = "https://files.pythonhosted.org/packages/69/72/20cb30f3b39a9face296491a86adb6ff8f1a47a897e4d14667e6cf89d5c3/torch-2.5.1-cp313-cp313-manylinux1_x86_64.whl", hash = "sha256:9b61edf3b4f6e3b0e0adda8b3960266b9009d02b37555971f4d1c8f7a05afed7", size = 906393265 }, +] + [[package]] name = "tqdm" version = "4.66.6" @@ -3979,6 +4743,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/48/be/a9ae5f50cad5b6f85bd2574c2c923730098530096e170c1ce7452394d7aa/trio_websocket-0.11.1-py3-none-any.whl", hash = "sha256:520d046b0d030cf970b8b2b2e00c4c2245b3807853ecd44214acd33d74581638", size = 17408 }, ] +[[package]] +name = "triton" +version = "3.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock", marker = "python_full_version < '3.13'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/17/d9a5cf4fcf46291856d1e90762e36cbabd2a56c7265da0d1d9508c8e3943/triton-3.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f34f6e7885d1bf0eaaf7ba875a5f0ce6f3c13ba98f9503651c1e6dc6757ed5c", size = 209506424 }, + { url = "https://files.pythonhosted.org/packages/78/eb/65f5ba83c2a123f6498a3097746607e5b2f16add29e36765305e4ac7fdd8/triton-3.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8182f42fd8080a7d39d666814fa36c5e30cc00ea7eeeb1a2983dbb4c99a0fdc", size = 209551444 }, +] + [[package]] name = "typer" version = "0.12.5" From 98a479bfd0e5f8cf1b6e54c1efcf4a40c372e90c Mon Sep 17 00:00:00 2001 From: Luis Otavio Date: Sun, 8 Dec 2024 00:24:07 -0300 Subject: [PATCH 03/75] Added Zep, refactored classifier --- .../eda_ai_api/api/routes/classifier.py | 218 +++++------------- apps/ai_api/eda_ai_api/utils/audio_utils.py | 53 +++++ apps/ai_api/eda_ai_api/utils/logger.py | 6 + apps/ai_api/eda_ai_api/utils/memory.py | 72 ++++++ apps/ai_api/eda_ai_api/utils/prompts.py | 59 +++++ apps/ai_api/pyproject.toml | 3 +- apps/ai_api/uv.lock | 35 ++- 7 files changed, 271 insertions(+), 175 deletions(-) create mode 100644 apps/ai_api/eda_ai_api/utils/audio_utils.py create mode 100644 apps/ai_api/eda_ai_api/utils/logger.py create mode 100644 apps/ai_api/eda_ai_api/utils/memory.py create mode 100644 apps/ai_api/eda_ai_api/utils/prompts.py diff --git a/apps/ai_api/eda_ai_api/api/routes/classifier.py b/apps/ai_api/eda_ai_api/api/routes/classifier.py index d9165b0..5d43bf0 100644 --- a/apps/ai_api/eda_ai_api/api/routes/classifier.py +++ b/apps/ai_api/eda_ai_api/api/routes/classifier.py @@ -1,29 +1,27 @@ import os -import tempfile -from typing import Any, Dict, List, Optional +from typing import Any, Dict, Optional from fastapi import APIRouter, File, Form, UploadFile +from llama_index.core import PromptTemplate from llama_index.llms.groq import Groq +from loguru import logger + +from eda_ai_api.models.classifier import ClassifierResponse +from eda_ai_api.utils.audio_utils import process_audio_file +from eda_ai_api.utils.memory import ZepConversationManager +from eda_ai_api.utils.prompts import ( + ROUTER_TEMPLATE, + TOPIC_TEMPLATE, + PROPOSAL_TEMPLATE, + INSUFFICIENT_TEMPLATES, +) + from onboarding.crew import OnboardingCrew from opportunity_finder.crew import OpportunityFinderCrew from proposal_writer.crew import ProposalWriterCrew -from llama_index.core import PromptTemplate -from eda_ai_api.models.classifier import ClassifierResponse -from eda_ai_api.utils.audio_converter import convert_ogg -from eda_ai_api.utils.transcriber import transcribe_audio - router = APIRouter() -ALLOWED_FORMATS = { - "audio/mpeg": "mp3", - "audio/mp4": "mp4", - "audio/mpga": "mpga", - "audio/wav": "wav", - "audio/webm": "webm", - "audio/ogg": "ogg", -} - # Setup LLM llm = Groq( model="llama3-groq-70b-8192-tool-use-preview", @@ -31,112 +29,8 @@ temperature=0.5, ) -# Define prompts -ROUTER_TEMPLATE = PromptTemplate( - """Given a user message, determine the appropriate service to handle the request. - Choose between: - - discovery: For finding grant opportunities - - proposal: For writing grant proposals - - onboarding: For getting help using the system - - heartbeat: For checking system health - - User message: {message} - - Return only one word (discovery/proposal/onboarding/heartbeat):""" -) -TOPIC_TEMPLATE = PromptTemplate( - """Extract up to 5 most relevant topics for grant opportunity research from the user message. - Return only a comma-separated list of topics (maximum 5), no other text. - - User message: {message} - - Topics:""" -) - -PROPOSAL_TEMPLATE = PromptTemplate( - """Extract the community project name and grant program name from the user message. - Return in format: project_name|grant_name - If either cannot be determined, use "unknown" as placeholder. - - User message: {message} - - Output:""" -) - -INSUFFICIENT_PROPOSAL_TEMPLATE = PromptTemplate( - """The user wants to write a proposal but hasn't provided enough information. - Generate a friendly response in the same language as the user's message asking for: - 1. The project name and brief description - 2. The specific grant program they're applying to (if any) - 3. The main objectives of their project - 4. The target community or region - - User message: {message} - - Response:""" -) - -INSUFFICIENT_DISCOVERY_TEMPLATE = PromptTemplate( - """The user wants to find grant opportunities but hasn't provided enough information. - Generate a friendly response in the same language as the user's message asking for: - 1. The main topics or areas of their project - 2. The target region or community - 3. Any specific funding requirements or preferences - - User message: {message} - - Response:""" -) - - -def detect_content_type(file: UploadFile) -> Optional[str]: - """Helper to detect content type from file""" - if hasattr(file, "content_type") and file.content_type: - return file.content_type - - if hasattr(file, "mime_type") and file.mime_type: - return file.mime_type - - ext = os.path.splitext(file.filename)[1].lower() - return { - ".mp3": "audio/mpeg", - ".mp4": "audio/mp4", - ".mpeg": "audio/mpeg", - ".mpga": "audio/mpga", - ".m4a": "audio/mp4", - ".wav": "audio/wav", - ".webm": "audio/webm", - ".ogg": "audio/ogg", - }.get(ext) - - -async def process_audio(audio: UploadFile) -> str: - """Process audio file and return transcription""" - content_type = detect_content_type(audio) - content = await audio.read() - audio_path = "" - - try: - if not content_type: - content_type = "audio/mpeg" - - if content_type == "audio/ogg": - audio_path = convert_ogg(content, output_format="mp3") - else: - with tempfile.NamedTemporaryFile( - suffix=f".{ALLOWED_FORMATS.get(content_type, 'mp3')}", delete=False - ) as temp_file: - temp_file.write(content) - audio_path = temp_file.name - - return transcribe_audio(audio_path) - finally: - if os.path.exists(audio_path): - os.unlink(audio_path) - - -def extract_topics(message: str) -> List[str]: +def extract_topics(message: str) -> list[str]: """Extract topics from message""" response = llm.complete(TOPIC_TEMPLATE.format(message=message)) topics = [t.strip() for t in response.text.split(",") if t.strip()][:5] @@ -154,20 +48,15 @@ def extract_proposal_details(message: str) -> tuple[str, str]: def process_decision(decision: str, message: str) -> Dict[str, Any]: """Process routing decision and return result""" - print("\n==================================================") - print(f" DECISION: {decision}") - print("==================================================\n") + logger.info(f"Processing decision: {decision} for message: {message}") if decision == "discovery": topics = extract_topics(message) - print("\n==================================================") - print(f" EXTRACTED TOPICS: {topics}") - print("==================================================\n") + logger.info(f"Extracted topics: {topics}") - # If no specific topics found, ask for more information if topics == ["AI", "Technology"]: response = llm.complete( - INSUFFICIENT_DISCOVERY_TEMPLATE.format(message=message) + INSUFFICIENT_TEMPLATES["discovery"].format(message=message) ) return {"response": response.text} @@ -177,13 +66,11 @@ def process_decision(decision: str, message: str) -> Dict[str, Any]: elif decision == "proposal": community_project, grant_call = extract_proposal_details(message) - print(f" PROJECT NAME: {community_project}") - print(f" GRANT PROGRAM: {grant_call}") + logger.info(f"Project: {community_project}, Grant: {grant_call}") - # If either project or grant is unknown, ask for more information if community_project == "unknown" or grant_call == "unknown": response = llm.complete( - INSUFFICIENT_PROPOSAL_TEMPLATE.format(message=message) + INSUFFICIENT_TEMPLATES["proposal"].format(message=message) ) return {"response": response.text} @@ -209,52 +96,53 @@ def process_decision(decision: str, message: str) -> Dict[str, Any]: async def classifier_route( message: Optional[str] = Form(default=None), audio: Optional[UploadFile] = File(default=None), + session_id: Optional[str] = Form(default=None), + user_id: Optional[str] = Form(default=None), ) -> ClassifierResponse: - """Main route handler for classifier API""" + """Main route handler with conversation memory""" try: + logger.info(f"New request - Session: {session_id}, User: {user_id}") + + zep = ZepConversationManager() + session_id = await zep.get_or_create_session( + user_id=user_id, session_id=session_id + ) + + # Process inputs combined_parts = [] - has_valid_audio = False - # Process audio if provided if audio is not None: - # Check if audio is not empty - audio_content = await audio.read() - has_valid_audio = len(audio_content) > 0 - - if has_valid_audio: - await audio.seek(0) - transcription = await process_audio(audio) - print("==================================================") - print(f" TRANSCRIPTION: {transcription}") - print("==================================================") - combined_parts.append( - f'Transcription of attached audio: "{transcription}"' - ) - - # Add message if provided + transcription = await process_audio_file(audio) + logger.info(f"Audio transcription: {transcription}") + combined_parts.append(f'Transcription: "{transcription}"') + if message: + logger.info(f"Text message: {message}") combined_parts.append(f'Message: "{message}"') - # Combine parts with newlines combined_message = "\n".join(combined_parts) + if not combined_message: + return ClassifierResponse(result="Error: No valid input provided") - if combined_message: - print("==================================================") - print(f" COMBINED MESSAGE:\n{combined_message}") - print("==================================================") + # Get conversation history and make decision + history = await zep.get_conversation_history(session_id) + context = "\n".join([f"{msg['role']}: {msg['content']}" for msg in history]) - # Ensure we have some input to process - if not combined_message: - return ClassifierResponse( - result="Error: Neither valid message nor valid audio provided" - ) + router_prompt = PromptTemplate( + """Previous conversation:\n{context}\n\nGiven the current user message, determine the appropriate service:\n{message}\n\nReturn only one word (discovery/proposal/onboarding/heartbeat):""" + ) - # Process the combined input - response = llm.complete(ROUTER_TEMPLATE.format(message=combined_message)) + response = llm.complete( + router_prompt.format(context=context, message=combined_message) + ) decision = response.text.strip().lower() + + # Process decision and store conversation result = process_decision(decision, combined_message) + await zep.add_conversation(session_id, combined_message, str(result)) - return ClassifierResponse(result=str(result)) + return ClassifierResponse(result=str(result), session_id=session_id) except Exception as e: - return ClassifierResponse(result=f"Error processing request: {str(e)}") + logger.error(f"Error in classifier route: {str(e)}") + return ClassifierResponse(result=f"Error: {str(e)}") diff --git a/apps/ai_api/eda_ai_api/utils/audio_utils.py b/apps/ai_api/eda_ai_api/utils/audio_utils.py new file mode 100644 index 0000000..7007e32 --- /dev/null +++ b/apps/ai_api/eda_ai_api/utils/audio_utils.py @@ -0,0 +1,53 @@ +import os +import tempfile +from typing import Optional, Dict +from fastapi import UploadFile +from .audio_converter import convert_ogg +from .transcriber import transcribe_audio + +ALLOWED_FORMATS = { + "audio/mpeg": "mp3", + "audio/mp4": "mp4", + "audio/mpga": "mpga", + "audio/wav": "wav", + "audio/webm": "webm", + "audio/ogg": "ogg", +} + + +def detect_content_type(file: UploadFile) -> Optional[str]: + if hasattr(file, "content_type") and file.content_type: + return file.content_type + if hasattr(file, "mime_type") and file.mime_type: + return file.mime_type + ext = os.path.splitext(file.filename)[1].lower() + return { + ".mp3": "audio/mpeg", + ".mp4": "audio/mp4", + ".mpeg": "audio/mpeg", + ".mpga": "audio/mpga", + ".m4a": "audio/mp4", + ".wav": "audio/wav", + ".webm": "audio/webm", + ".ogg": "audio/ogg", + }.get(ext) + + +async def process_audio_file(audio: UploadFile) -> str: + content_type = detect_content_type(audio) + content = await audio.read() + + try: + if content_type == "audio/ogg": + audio_path = convert_ogg(content, output_format="mp3") + else: + with tempfile.NamedTemporaryFile( + suffix=f".{ALLOWED_FORMATS.get(content_type, 'mp3')}", delete=False + ) as temp_file: + temp_file.write(content) + audio_path = temp_file.name + + return transcribe_audio(audio_path) + finally: + if os.path.exists(audio_path): + os.unlink(audio_path) diff --git a/apps/ai_api/eda_ai_api/utils/logger.py b/apps/ai_api/eda_ai_api/utils/logger.py new file mode 100644 index 0000000..579d267 --- /dev/null +++ b/apps/ai_api/eda_ai_api/utils/logger.py @@ -0,0 +1,6 @@ +# /utils/logger.py +from loguru import logger + + +def setup_logging(): + logger.add("api.log", rotation="500 MB") diff --git a/apps/ai_api/eda_ai_api/utils/memory.py b/apps/ai_api/eda_ai_api/utils/memory.py new file mode 100644 index 0000000..015d082 --- /dev/null +++ b/apps/ai_api/eda_ai_api/utils/memory.py @@ -0,0 +1,72 @@ +import uuid +from typing import Optional, Dict, List +from zep_python.client import AsyncZep +from zep_python.types import Message +from loguru import logger + + +class ZepConversationManager: + def __init__( + self, api_key: str = "abc123", base_url: str = "http://localhost:3002" + ): + self.client = AsyncZep(api_key=api_key, base_url=base_url, timeout=30) + + async def get_or_create_session( + self, user_id: Optional[str] = None, session_id: Optional[str] = None + ) -> str: + """Get existing session or create new one""" + try: + if session_id: + try: + await self.client.memory.get(session_id=session_id) + return session_id + except Exception: + pass + + user_id = user_id or uuid.uuid4().hex + await self.client.user.add( + user_id=user_id, metadata={"source": "classifier_api"} + ) + + new_session_id = uuid.uuid4().hex + await self.client.memory.add_session( + session_id=new_session_id, user_id=user_id + ) + + await self.client.memory.add(session_id=new_session_id, messages=[]) + + return new_session_id + + except Exception as e: + raise RuntimeError(f"Session error: {str(e)}") from e + + async def add_conversation( + self, session_id: str, user_message: str, assistant_response: str + ) -> None: + """Store conversation messages""" + await self.client.memory.add( + session_id=session_id, + messages=[ + Message(role_type="user", content=user_message), + Message(role_type="assistant", content=str(assistant_response)), + ], + ) + + async def get_conversation_history( + self, session_id: str, limit: int = 5 + ) -> List[Dict[str, str]]: + """Get conversation history with detailed logging""" + memory = await self.client.memory.get(session_id=session_id) + + logger.info("\n===== CONVERSATION HISTORY =====") + for idx, msg in enumerate(memory.messages[-limit:], 1): + logger.info(f"\nMessage {idx}:") + logger.info(f"Role: {msg.role_type}") + logger.info(f"Content: {msg.content}") + logger.info("-" * 40) + logger.info("==============================\n") + + return [ + {"role": msg.role_type, "content": msg.content} + for msg in memory.messages[-limit:] + ] diff --git a/apps/ai_api/eda_ai_api/utils/prompts.py b/apps/ai_api/eda_ai_api/utils/prompts.py new file mode 100644 index 0000000..d9827ca --- /dev/null +++ b/apps/ai_api/eda_ai_api/utils/prompts.py @@ -0,0 +1,59 @@ +from llama_index.core import PromptTemplate + +ROUTER_TEMPLATE = PromptTemplate( + """Given a user message, determine the appropriate service to handle the request. + Choose between: + - discovery: For finding grant opportunities + - proposal: For writing grant proposals + - onboarding: For getting help using the system + - heartbeat: For checking system health + + User message: {message} + + Return only one word (discovery/proposal/onboarding/heartbeat):""" +) + +TOPIC_TEMPLATE = PromptTemplate( + """Extract up to 5 most relevant topics for grant opportunity research from the user message. + Return only a comma-separated list of topics (maximum 5), no other text. + + User message: {message} + + Topics:""" +) + +PROPOSAL_TEMPLATE = PromptTemplate( + """Extract the community project name and grant program name from the user message. + Return in format: project_name|grant_name + If either cannot be determined, use "unknown" as placeholder. + + User message: {message} + + Output:""" +) + +INSUFFICIENT_TEMPLATES = { + "proposal": PromptTemplate( + """The user wants to write a proposal but hasn't provided enough information. + Generate a friendly response in the same language as the user's message asking for: + 1. The project name and brief description + 2. The specific grant program they're applying to (if any) + 3. The main objectives of their project + 4. The target community or region + + User message: {message} + + Response:""" + ), + "discovery": PromptTemplate( + """The user wants to find grant opportunities but hasn't provided enough information. + Generate a friendly response in the same language as the user's message asking for: + 1. The main topics or areas of their project + 2. The target region or community + 3. Any specific funding requirements or preferences + + User message: {message} + + Response:""" + ), +} diff --git a/apps/ai_api/pyproject.toml b/apps/ai_api/pyproject.toml index bf46d65..ae15718 100644 --- a/apps/ai_api/pyproject.toml +++ b/apps/ai_api/pyproject.toml @@ -23,6 +23,8 @@ dependencies = [ "llama-index-embeddings-huggingface>=0.4.0", "llama-index-vector-stores-postgres>=0.3.2", "llama-index-readers-file>=0.4.1", + "httpx>=0.24.0,<0.25.0", + "zep-python>=2.0.2", ] [project.optional-dependencies] @@ -33,7 +35,6 @@ dev = [ "flake8>=6.1.0", "bandit>=1.7.6", "pytest>=7.4.3", - "httpx>=0.26.0", "pytest-cov>=4.1.0" ] diff --git a/apps/ai_api/uv.lock b/apps/ai_api/uv.lock index 0859a78..99189f0 100644 --- a/apps/ai_api/uv.lock +++ b/apps/ai_api/uv.lock @@ -821,6 +821,7 @@ source = { editable = "." } dependencies = [ { name = "fastapi" }, { name = "ffmpeg-python" }, + { name = "httpx" }, { name = "joblib" }, { name = "langchain-groq" }, { name = "llama-index" }, @@ -837,6 +838,7 @@ dependencies = [ { name = "python-multipart" }, { name = "requests" }, { name = "uvicorn" }, + { name = "zep-python" }, ] [package.optional-dependencies] @@ -844,7 +846,6 @@ dev = [ { name = "bandit" }, { name = "black" }, { name = "flake8" }, - { name = "httpx" }, { name = "isort" }, { name = "mypy" }, { name = "pytest" }, @@ -858,7 +859,7 @@ requires-dist = [ { name = "fastapi", specifier = ">=0.109.1" }, { name = "ffmpeg-python", specifier = ">=0.2.0" }, { name = "flake8", marker = "extra == 'dev'", specifier = ">=6.1.0" }, - { name = "httpx", marker = "extra == 'dev'", specifier = ">=0.26.0" }, + { name = "httpx", specifier = ">=0.24.0,<0.25.0" }, { name = "isort", marker = "extra == 'dev'", specifier = ">=5.13.2" }, { name = "joblib", specifier = ">=1.3.2" }, { name = "langchain-groq", specifier = ">=0.2.1" }, @@ -879,6 +880,7 @@ requires-dist = [ { name = "python-multipart", specifier = ">=0.0.17" }, { name = "requests", specifier = ">=2.31.0" }, { name = "uvicorn", specifier = ">=0.25.0" }, + { name = "zep-python", specifier = ">=2.0.2" }, ] [[package]] @@ -1448,15 +1450,17 @@ wheels = [ [[package]] name = "httpcore" -version = "1.0.6" +version = "0.17.3" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "anyio" }, { name = "certifi" }, { name = "h11" }, + { name = "sniffio" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b6/44/ed0fa6a17845fb033bd885c03e842f08c1b9406c86a2e60ac1ae1b9206a6/httpcore-1.0.6.tar.gz", hash = "sha256:73f6dbd6eb8c21bbf7ef8efad555481853f5f6acdeaff1edb0694289269ee17f", size = 85180 } +sdist = { url = "https://files.pythonhosted.org/packages/63/ad/c98ecdbfe04417e71e143bf2f2fb29128e4787d78d1cedba21bd250c7e7a/httpcore-0.17.3.tar.gz", hash = "sha256:a6f30213335e34c1ade7be6ec7c47f19f50c56db36abef1a9dfa3815b1cb3888", size = 62676 } wheels = [ - { url = "https://files.pythonhosted.org/packages/06/89/b161908e2f51be56568184aeb4a880fd287178d176fd1c860d2217f41106/httpcore-1.0.6-py3-none-any.whl", hash = "sha256:27b59625743b85577a8c0e10e55b50b5368a4f2cfe8cc7bcfa9cf00829c2682f", size = 78011 }, + { url = "https://files.pythonhosted.org/packages/94/2c/2bde7ff8dd2064395555220cbf7cba79991172bf5315a07eb3ac7688d9f1/httpcore-0.17.3-py3-none-any.whl", hash = "sha256:c2789b767ddddfa2a5782e3199b2b7f6894540b17b16ec26b2c4d8e103510b87", size = 74513 }, ] [[package]] @@ -1490,18 +1494,17 @@ wheels = [ [[package]] name = "httpx" -version = "0.27.2" +version = "0.24.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "anyio" }, { name = "certifi" }, { name = "httpcore" }, { name = "idna" }, { name = "sniffio" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/78/82/08f8c936781f67d9e6b9eeb8a0c8b4e406136ea4c3d1f89a5db71d42e0e6/httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2", size = 144189 } +sdist = { url = "https://files.pythonhosted.org/packages/f8/2a/114d454cb77657dbf6a293e69390b96318930ace9cd96b51b99682493276/httpx-0.24.1.tar.gz", hash = "sha256:5853a43053df830c20f8110c5e69fe44d035d850b2dfe795e196f00fdb774bdd", size = 81858 } wheels = [ - { url = "https://files.pythonhosted.org/packages/56/95/9377bcb415797e44274b51d46e3249eba641711cf3348050f76ee7b15ffc/httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0", size = 76395 }, + { url = "https://files.pythonhosted.org/packages/ec/91/e41f64f03d2a13aee7e8c819d82ee3aa7cdc484d18c0ae859742597d5aa0/httpx-0.24.1-py3-none-any.whl", hash = "sha256:06781eb9ac53cde990577af654bd990a4949de37a28bdb4a230d434f3a30b9bd", size = 75377 }, ] [package.optional-dependencies] @@ -5161,6 +5164,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/93/86/f1305e1ab1d6dc27d245ffc83d18d88f2bebf6c6488725ee82dffb3eda7a/yarl-1.17.0-py3-none-any.whl", hash = "sha256:62dd42bb0e49423f4dd58836a04fcf09c80237836796025211bbe913f1524993", size = 44053 }, ] +[[package]] +name = "zep-python" +version = "2.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "pydantic" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b5/64/1b0c4afa6d0e8f35e1072eacdfe8ec216871be5f2dfd569942f69232482f/zep_python-2.0.2.tar.gz", hash = "sha256:919fd635ad5801f30d9ef6da0bb99e854ed4c3cd1357dc8ada4a39b8171a41bc", size = 22273 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/d4/09a6bc0c32f91b7b5b93322513d51299ca9b35bdd6b0560b39104c296f39/zep_python-2.0.2-py3-none-any.whl", hash = "sha256:d7b2cadf1eb15007825362cae5ddff9c9d0ada5b4f75cf91e5b87986459f7817", size = 40011 }, +] + [[package]] name = "zipp" version = "3.20.2" From 626084a60cb555337abc7def7a157c22c4cfb0f6 Mon Sep 17 00:00:00 2001 From: Luis Otavio Date: Mon, 9 Dec 2024 20:36:37 -0300 Subject: [PATCH 04/75] Enhance classifier route to generate unique user IDs using UUID --- apps/ai_api/eda_ai_api/api/routes/classifier.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/ai_api/eda_ai_api/api/routes/classifier.py b/apps/ai_api/eda_ai_api/api/routes/classifier.py index 5d43bf0..af02632 100644 --- a/apps/ai_api/eda_ai_api/api/routes/classifier.py +++ b/apps/ai_api/eda_ai_api/api/routes/classifier.py @@ -1,5 +1,6 @@ import os from typing import Any, Dict, Optional +import uuid from fastapi import APIRouter, File, Form, UploadFile from llama_index.core import PromptTemplate @@ -97,15 +98,16 @@ async def classifier_route( message: Optional[str] = Form(default=None), audio: Optional[UploadFile] = File(default=None), session_id: Optional[str] = Form(default=None), - user_id: Optional[str] = Form(default=None), ) -> ClassifierResponse: """Main route handler with conversation memory""" try: - logger.info(f"New request - Session: {session_id}, User: {user_id}") + logger.info( + f"New request - Session: {session_id}, User: {session_id + uuid.uuid4().hex}" + ) zep = ZepConversationManager() session_id = await zep.get_or_create_session( - user_id=user_id, session_id=session_id + user_id=session_id + uuid.uuid4().hex, session_id=session_id ) # Process inputs From 4ee675893f587c0b9e84a4e9cd8a0a92e44331cb Mon Sep 17 00:00:00 2001 From: Luis Otavio Date: Mon, 9 Dec 2024 22:51:25 -0300 Subject: [PATCH 05/75] Refactor classifier functions to include conversation context and improve topic extraction handling --- .../eda_ai_api/api/routes/classifier.py | 54 +++++++-------- apps/ai_api/eda_ai_api/utils/prompts.py | 69 +++++++++++-------- 2 files changed, 68 insertions(+), 55 deletions(-) diff --git a/apps/ai_api/eda_ai_api/api/routes/classifier.py b/apps/ai_api/eda_ai_api/api/routes/classifier.py index af02632..4cdc13b 100644 --- a/apps/ai_api/eda_ai_api/api/routes/classifier.py +++ b/apps/ai_api/eda_ai_api/api/routes/classifier.py @@ -31,58 +31,58 @@ ) -def extract_topics(message: str) -> list[str]: - """Extract topics from message""" - response = llm.complete(TOPIC_TEMPLATE.format(message=message)) +async def extract_topics(message: str, history: list) -> list[str]: + """Extract topics with conversation context""" + context = "\n".join([f"{msg['role']}: {msg['content']}" for msg in history]) + response = llm.complete(TOPIC_TEMPLATE.format(context=context, message=message)) + if response.text.strip() == "INSUFFICIENT_CONTEXT": + return ["INSUFFICIENT_CONTEXT"] topics = [t.strip() for t in response.text.split(",") if t.strip()][:5] - return topics if topics else ["AI", "Technology"] + return topics if topics else ["INSUFFICIENT_CONTEXT"] -def extract_proposal_details(message: str) -> tuple[str, str]: - """Extract project and grant details""" - response = llm.complete(PROPOSAL_TEMPLATE.format(message=message)) +async def extract_proposal_details(message: str, history: list) -> tuple[str, str]: + """Extract project and grant details with conversation context""" + context = "\n".join([f"{msg['role']}: {msg['content']}" for msg in history]) + response = llm.complete(PROPOSAL_TEMPLATE.format(context=context, message=message)) extracted = response.text.split("|") community_project = extracted[0].strip() if len(extracted) > 0 else "unknown" grant_call = extracted[1].strip() if len(extracted) > 1 else "unknown" return community_project, grant_call -def process_decision(decision: str, message: str) -> Dict[str, Any]: - """Process routing decision and return result""" +async def process_decision( + decision: str, message: str, history: list +) -> Dict[str, Any]: + """Process routing decision with conversation context""" logger.info(f"Processing decision: {decision} for message: {message}") if decision == "discovery": - topics = extract_topics(message) + topics = await extract_topics(message, history) logger.info(f"Extracted topics: {topics}") - if topics == ["AI", "Technology"]: + if topics == ["INSUFFICIENT_CONTEXT"]: + context = "\n".join([f"{msg['role']}: {msg['content']}" for msg in history]) response = llm.complete( - INSUFFICIENT_TEMPLATES["discovery"].format(message=message) + INSUFFICIENT_TEMPLATES["discovery"].format( + context=context, message=message + ) ) return {"response": response.text} - return ( - OpportunityFinderCrew().crew().kickoff(inputs={"topics": ", ".join(topics)}) - ) - elif decision == "proposal": - community_project, grant_call = extract_proposal_details(message) + community_project, grant_call = await extract_proposal_details(message, history) logger.info(f"Project: {community_project}, Grant: {grant_call}") if community_project == "unknown" or grant_call == "unknown": + context = "\n".join([f"{msg['role']}: {msg['content']}" for msg in history]) response = llm.complete( - INSUFFICIENT_TEMPLATES["proposal"].format(message=message) + INSUFFICIENT_TEMPLATES["proposal"].format( + context=context, message=message + ) ) return {"response": response.text} - return ( - ProposalWriterCrew( - community_project=community_project, grant_call=grant_call - ) - .crew() - .kickoff() - ) - elif decision == "heartbeat": return {"is_alive": True} @@ -140,7 +140,7 @@ async def classifier_route( decision = response.text.strip().lower() # Process decision and store conversation - result = process_decision(decision, combined_message) + result = await process_decision(decision, combined_message, history) await zep.add_conversation(session_id, combined_message, str(result)) return ClassifierResponse(result=str(result), session_id=session_id) diff --git a/apps/ai_api/eda_ai_api/utils/prompts.py b/apps/ai_api/eda_ai_api/utils/prompts.py index d9827ca..0e7534b 100644 --- a/apps/ai_api/eda_ai_api/utils/prompts.py +++ b/apps/ai_api/eda_ai_api/utils/prompts.py @@ -14,46 +14,59 @@ ) TOPIC_TEMPLATE = PromptTemplate( - """Extract up to 5 most relevant topics for grant opportunity research from the user message. - Return only a comma-separated list of topics (maximum 5), no other text. + """Previous conversation: +{context} - User message: {message} +Extract up to 5 most relevant topics for grant opportunity research from the user message. +If there isn't enough context to determine specific topics, return "INSUFFICIENT_CONTEXT". +Otherwise, return only a comma-separated list of topics (maximum 5), no other text. + +User message: {message} - Topics:""" +Output:""" ) PROPOSAL_TEMPLATE = PromptTemplate( - """Extract the community project name and grant program name from the user message. - Return in format: project_name|grant_name - If either cannot be determined, use "unknown" as placeholder. + """Previous conversation: +{context} - User message: {message} +Extract the community project name and grant program name from the user message. +Return in format: project_name|grant_name +If either cannot be determined, use "unknown" as placeholder. - Output:""" +User message: {message} + +Output:""" ) INSUFFICIENT_TEMPLATES = { "proposal": PromptTemplate( - """The user wants to write a proposal but hasn't provided enough information. - Generate a friendly response in the same language as the user's message asking for: - 1. The project name and brief description - 2. The specific grant program they're applying to (if any) - 3. The main objectives of their project - 4. The target community or region - - User message: {message} - - Response:""" + """Previous conversation: +{context} + +The user wants to write a proposal but hasn't provided enough information. +Generate a friendly response in the same language as the user's message asking for: +1. The project name and brief description +2. The specific grant program they're applying to (if any) +3. The main objectives of their project +4. The target community or region + +User message: {message} + +Response:""" ), "discovery": PromptTemplate( - """The user wants to find grant opportunities but hasn't provided enough information. - Generate a friendly response in the same language as the user's message asking for: - 1. The main topics or areas of their project - 2. The target region or community - 3. Any specific funding requirements or preferences - - User message: {message} - - Response:""" + """Previous conversation: +{context} + +The user wants to find grant opportunities but hasn't provided enough information. +Generate a friendly response in the same language as the user's message asking for: +1. The main topics or areas of their project +2. The target region or community +3. Any specific funding requirements or preferences + +User message: {message} + +Response:""" ), } From a0336d9b317c52bf1280cb2c5b139d00870931d0 Mon Sep 17 00:00:00 2001 From: Luis Otavio Date: Sat, 14 Dec 2024 18:26:10 -0300 Subject: [PATCH 06/75] Working whatsapp --- apps/whatsapp/.gitignore | 4 +- apps/whatsapp/Dockerfile | 47 ---- apps/whatsapp/index.ts | 77 +----- apps/whatsapp/package.json | 10 +- apps/whatsapp/src/client.ts | 154 ++++++++++++ apps/whatsapp/src/commands/commands_index.ts | 68 ++++++ apps/whatsapp/src/commands/help.ts | 45 ++++ apps/whatsapp/src/commands/invalid_command.ts | 21 ++ apps/whatsapp/src/constants.ts | 16 ++ apps/whatsapp/src/database.ts | 53 ----- apps/whatsapp/src/message/message.ts | 46 ++++ apps/whatsapp/src/messageHandler.ts | 112 --------- apps/whatsapp/src/tests/database.test.ts | 58 ----- .../whatsapp/src/tests/messageHandler.test.ts | 122 ---------- apps/whatsapp/src/tests/utils.test.ts | 72 ------ apps/whatsapp/src/tests/utils.ts | 28 --- apps/whatsapp/src/types.ts | 67 ------ apps/whatsapp/src/utils/whatsapp.ts | 223 ++++++++++++++++++ bun.lockb | Bin 720640 -> 720408 bytes 19 files changed, 584 insertions(+), 639 deletions(-) delete mode 100644 apps/whatsapp/Dockerfile create mode 100644 apps/whatsapp/src/client.ts create mode 100644 apps/whatsapp/src/commands/commands_index.ts create mode 100644 apps/whatsapp/src/commands/help.ts create mode 100644 apps/whatsapp/src/commands/invalid_command.ts create mode 100644 apps/whatsapp/src/constants.ts delete mode 100644 apps/whatsapp/src/database.ts create mode 100644 apps/whatsapp/src/message/message.ts delete mode 100644 apps/whatsapp/src/messageHandler.ts delete mode 100644 apps/whatsapp/src/tests/database.test.ts delete mode 100644 apps/whatsapp/src/tests/messageHandler.test.ts delete mode 100644 apps/whatsapp/src/tests/utils.test.ts delete mode 100644 apps/whatsapp/src/tests/utils.ts delete mode 100644 apps/whatsapp/src/types.ts create mode 100644 apps/whatsapp/src/utils/whatsapp.ts diff --git a/apps/whatsapp/.gitignore b/apps/whatsapp/.gitignore index c903711..28e085c 100644 --- a/apps/whatsapp/.gitignore +++ b/apps/whatsapp/.gitignore @@ -173,4 +173,6 @@ dist # Finder (MacOS) folder config .DS_Store -message_db.json \ No newline at end of file +message_db.json + +wa_auth \ No newline at end of file diff --git a/apps/whatsapp/Dockerfile b/apps/whatsapp/Dockerfile deleted file mode 100644 index ef94fa9..0000000 --- a/apps/whatsapp/Dockerfile +++ /dev/null @@ -1,47 +0,0 @@ -FROM oven/bun:1 AS base - -# The web Dockerfile is copy-pasted into our main docs at /docs/handbook/deploying-with-docker. -# Make sure you update this Dockerfile, the Dockerfile in the web workspace and copy that over to Dockerfile in the docs. - -FROM base AS builder -# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. -RUN apk update -RUN apk add --no-cache libc6-compat -# Set working directory -WORKDIR /app -RUN bun install -g turbo -COPY . . -RUN turbo prune api --docker - -# Add lockfile and package.json's of isolated subworkspace -FROM base AS installer -RUN apk update -RUN apk add --no-cache libc6-compat -WORKDIR /app - -# First install dependencies (as they change less often) -COPY --from=builder /app/out/json/ . -RUN bun install - -# Build the project and its dependencies -COPY --from=builder /app/out/full/ . - -# Uncomment and use build args to enable remote caching -# ARG TURBO_TEAM -# ENV TURBO_TEAM=$TURBO_TEAM - -# ARG TURBO_TOKEN -# ENV TURBO_TOKEN=$TURBO_TOKEN - -RUN bun run turbo build --filter=api... - -FROM base AS runner -WORKDIR /app - -# Don't run production as root -RUN addgroup --system --gid 1001 expressjs -RUN adduser --system --uid 1001 expressjs -USER expressjs -COPY --from=installer /app . - -CMD bun run apps/api/dist/index.js \ No newline at end of file diff --git a/apps/whatsapp/index.ts b/apps/whatsapp/index.ts index 07169ca..f271dcf 100644 --- a/apps/whatsapp/index.ts +++ b/apps/whatsapp/index.ts @@ -1,76 +1,3 @@ -import os from "node:os"; -import path from "node:path"; -import { logger } from "@eda/logger"; -import dotenv from "dotenv"; -import mongoose from "mongoose"; -import qrcode from "qrcode-terminal"; -import { Client, LocalAuth, RemoteAuth } from "whatsapp-web.js"; -import { MongoStore } from "wwebjs-mongo"; -import { handleMessage } from "./src/messageHandler"; -const DEFAULT_APP_FOLDER = path.join( - os.homedir(), - ".earth-defenders-assistant", -); +import { sock } from "./src/client"; -dotenv.config(); -const args = - process.env.NODE_ENV === "production" - ? ["--no-sandbox", "--disable-setuid-sandbox"] - : undefined; - -async function initializeClient() { - let client: Client; - - if (process.env.MONGODB_URI) { - try { - logger.info("Connecting to MongoDB..."); - await mongoose.connect(process.env.MONGODB_URI); - logger.info("MongoDB connected successfully"); - - const store = new MongoStore({ mongoose: mongoose }); - logger.info("Creating client with RemoteAuth..."); - client = new Client({ - authStrategy: new RemoteAuth({ - store, - backupSyncIntervalMs: 300000, - }), - webVersionCache: { - type: "remote", - remotePath: - "https://raw.githubusercontent.com/wppconnect-team/wa-version/main/html/2.2412.54.html", - }, - }); - } catch (error) { - logger.error(`Failed to connect to MongoDB: ${error}`); - process.exit(1); - } - } else { - logger.warn("MONGODB_URI not found, using LocalAuth..."); - client = new Client({ - webVersion: "2.2306.7", // this keeps the Whatsapp Verison with the latest version worked πŸ‘ - puppeteer: { - args, - }, - authStrategy: new LocalAuth({ - dataPath: DEFAULT_APP_FOLDER, - }), - }); - } - - client.on("qr", (qr: string) => { - logger.info("Scan the QR code below to log in:"); - qrcode.generate(qr, { small: true }); - }); - - client.on("ready", () => { - logger.info("Client is ready!"); - }); - - client.on("message_create", (message) => { - logger.info(`New message: ${message.body}`); - handleMessage(message); - }); - client.initialize(); -} - -initializeClient().catch((error) => logger.error(`Unhandled error: ${error}`)); +sock; diff --git a/apps/whatsapp/package.json b/apps/whatsapp/package.json index 5701ae6..a869bce 100644 --- a/apps/whatsapp/package.json +++ b/apps/whatsapp/package.json @@ -18,11 +18,13 @@ }, "dependencies": { "@trigger.dev/sdk": "3.2.1", + "@types/common-tags": "^1.8.4", + "@whiskeysockets/baileys": "^6.7.9", + "common-tags": "^1.8.2", "dotenv": "^16.4.5", + "dotenv-expand": "^12.0.1", "mkdirp": "^3.0.1", - "mongoose": "^8.7.1", - "qrcode-terminal": "^0.12.0", - "whatsapp-web.js": "^1.26.0", - "wwebjs-mongo": "^1.1.0" + "pino": "^7.0.0", + "qrcode-terminal": "^0.12.0" } } diff --git a/apps/whatsapp/src/client.ts b/apps/whatsapp/src/client.ts new file mode 100644 index 0000000..ea42284 --- /dev/null +++ b/apps/whatsapp/src/client.ts @@ -0,0 +1,154 @@ +import type { Boom } from "@hapi/boom"; +import { + DisconnectReason, + type WASocket, + fetchLatestBaileysVersion, + makeWASocket, + type proto, + useMultiFileAuthState, +} from "@whiskeysockets/baileys"; +import P from "pino"; +import { handleCommand } from "./commands/commands_index"; +import { CMD_PREFIX } from "./constants"; +import { handleMessage } from "./message/message"; +import { + isGroupMessage, + react, + shouldIgnore, + shouldIgnoreUnread, + shouldReply, +} from "./utils/whatsapp"; + +const messageQueue: { [key: string]: proto.IWebMessageInfo[] } = {}; +let isProcessingMessage = false; + +async function processMessageQueue(chatId: string) { + isProcessingMessage = true; + + while (messageQueue[chatId] && messageQueue[chatId].length > 0) { + const message = messageQueue[chatId].shift(); + if (message) { + await handleMessageWithQueue(message); + } + } + + isProcessingMessage = false; +} + +async function handleMessageWithQueue(message: proto.IWebMessageInfo) { + const messageBody = message.message?.conversation; + const isCommand = messageBody?.startsWith(CMD_PREFIX); + + console.log("messageBody:", messageBody); + + if (isCommand) { + await handleCommand(message); + } else { + await handleMessage(message); + } +} + +const { state, saveCreds } = await useMultiFileAuthState("wa_auth"); +const { version, isLatest } = await fetchLatestBaileysVersion(); +console.log(`using WA v${version.join(".")}, isLatest: ${isLatest}`); + +// Create a custom logger with a higher log level +const logger = P({ level: "info" }); // Set to 'warn' to ignore info and debug logs + +export let sock: WASocket = makeWASocket({ + version, + logger, + printQRInTerminal: true, + auth: state, + generateHighQualityLinkPreview: true, +}); + +const unreadCounts: { [key: string]: number } = {}; + +async function startSock() { + sock.ev.process(async (events) => { + if (events["connection.update"]) { + const update = events["connection.update"]; + const { connection, lastDisconnect } = update; + if (connection === "close") { + if ( + (lastDisconnect?.error as Boom)?.output?.statusCode !== + DisconnectReason.loggedOut + ) { + console.log("Connection closed. Attempting to reconnect..."); + await reconnect(); + } else { + console.log("Connection closed. You are logged out."); + } + } + + console.log("connection update", update); + } + + if (events["chats.update"]) { + for (const chat of events["chats.update"]) { + if (chat.id && chat.unreadCount !== undefined) { + if (chat.unreadCount !== null) { + unreadCounts[chat.id] = chat.unreadCount; + } + } + } + } + + if (events["messages.upsert"]) { + const upsert = events["messages.upsert"]; + //console.log("recv messages ", JSON.stringify(upsert, undefined, 2)); + + if (upsert.type === "notify") { + for (const message of upsert.messages) { + const chatId = message.key.remoteJid!; + const unreadCount = unreadCounts[chatId] || 0; + + if (await shouldIgnore(message)) return; + if (!(await shouldReply(message))) return; + if (await shouldIgnoreUnread(message, unreadCount)) return; + + await react(message, "queued"); + + // Add the message to the queue + if (!messageQueue[chatId]) { + messageQueue[chatId] = []; + } + messageQueue[chatId].push(message); + + // Process the queue if not already processing + if (!isProcessingMessage) { + await processMessageQueue(chatId); + } + } + } + } + + // Don't forget to save credentials whenever they are updated + if (events["creds.update"]) { + await saveCreds(); + } + }); +} + +async function reconnect() { + let connected = false; + while (!connected) { + try { + sock = makeWASocket({ + version, + logger, + printQRInTerminal: true, + auth: state, + generateHighQualityLinkPreview: true, + }); + await startSock(); + connected = true; + } catch (error) { + console.log("Reconnection failed, retrying in 5 seconds...", error); + await new Promise((resolve) => setTimeout(resolve, 5000)); + } + } +} + +await startSock(); diff --git a/apps/whatsapp/src/commands/commands_index.ts b/apps/whatsapp/src/commands/commands_index.ts new file mode 100644 index 0000000..0cd9a50 --- /dev/null +++ b/apps/whatsapp/src/commands/commands_index.ts @@ -0,0 +1,68 @@ +import type { WASocket, proto } from "@whiskeysockets/baileys"; +import { stripIndents } from "common-tags"; +import { sock } from "../client"; +import { BOT_PREFIX, CMD_PREFIX } from "../constants"; +import { + isGroupMessage, + react, + unauthorizedCommandFor, +} from "../utils/whatsapp"; +import { handleHelp } from "./help"; + +const adminCommands = ["jailbreak", "reset", "change"]; +export const helpStatement = stripIndents`Run *_${CMD_PREFIX}help_* to see the available commands.`; + +export async function handleCommand(message: proto.IWebMessageInfo) { + const messageBody = message.message?.conversation; + + if (!messageBody) { + await react(message, "error"); + return; + } + const [command, ..._args] = messageBody.split(CMD_PREFIX)[1].split(" "); + + const args = _args.join(" ").toLowerCase(); + let reply: string; + + await react(message, "working"); + + const chatId = message.key.remoteJid; + if (!chatId) { + await react(message, "error"); + return; + } + let isAdmin = true; // default to true for private chats + if (chatId.endsWith("@g.us")) { + const groupMetadata = await sock.groupMetadata(chatId); + const senderId = message.key.participant || message.key.remoteJid; + isAdmin = groupMetadata.participants.some( + (participant) => participant.id === senderId && participant.admin, + ); + } + + if (adminCommands.includes(command) && !isAdmin) { + reply = unauthorizedCommandFor(command); + await sock.sendMessage(chatId, { text: reply }, { quoted: message }); + await react(message, "done"); + return; + } + + switch (command) { + case "ping": + reply = `${BOT_PREFIX}*_pong!_*`; + break; + + case "help": + reply = await handleHelp(message, args); + break; + default: + reply = stripIndents` + ${BOT_PREFIX}Unknown command _"${CMD_PREFIX + command}"_ + + ${helpStatement}`; + break; + } + + await sock.sendMessage(chatId, { text: reply }, { quoted: message }); + await react(message, "done"); +} diff --git a/apps/whatsapp/src/commands/help.ts b/apps/whatsapp/src/commands/help.ts new file mode 100644 index 0000000..5d4fb41 --- /dev/null +++ b/apps/whatsapp/src/commands/help.ts @@ -0,0 +1,45 @@ +import type { proto } from "@whiskeysockets/baileys"; +import { oneLine, stripIndents } from "common-tags"; +import { BOT_NAME, BOT_PREFIX, CMD_PREFIX } from "../constants"; +import { invalidArgumentMessage } from "./invalid_command"; + +export async function handleHelp(message: proto.IWebMessageInfo, args: string) { + let reply: string; + + switch (args) { + case "help": + reply = helpHelpMessage; + break; + case "ping": + reply = pingHelpMessage; + break; + case "": + reply = helpMessage; + break; + default: + reply = invalidArgumentMessage(args); + break; + } + + return reply; +} + +const helpMessage = stripIndents`${BOT_PREFIX}Available commands: + +πŸ†˜ *${CMD_PREFIX}help __* +Displays the available commands, their functionalities and how to use them. +- Run *${CMD_PREFIX}help __* for more information about a specific command. + +πŸ“ *${CMD_PREFIX}ping* +Checks if the bot is alive by responding with '*_pong!_*'. +`; + +const helpHelpMessage = stripIndents`I see what you did there. + +That's pretty meta, but I'm not gonna help you with that. + +Smart ass. +`; + +const pingHelpMessage = stripIndents`πŸ“ *${CMD_PREFIX}ping* +Checks if the bot is alive by responding with '*_pong!_*'.`; diff --git a/apps/whatsapp/src/commands/invalid_command.ts b/apps/whatsapp/src/commands/invalid_command.ts new file mode 100644 index 0000000..e395dd4 --- /dev/null +++ b/apps/whatsapp/src/commands/invalid_command.ts @@ -0,0 +1,21 @@ +import { stripIndents } from "common-tags"; +import { BOT_PREFIX, CMD_PREFIX } from "../constants"; + +export const helpStatement = stripIndents`Run *_${CMD_PREFIX}help_* to see the available commands.`; + +export function invalidArgumentMessage(args: string, usage?: string) { + return stripIndents`${BOT_PREFIX}Invalid argument _"${args}"_ + + ${usage ? `Usage: *_${CMD_PREFIX}${usage}_*\n` : ""} + ${helpStatement} + `; +} + +export function unauthorizedCommandFor(command: string) { + return stripIndents` +${BOT_PREFIX}Unauthorized: You are not an admin in this group. + +Only admins can run the *${command}* command + +${helpStatement}`; +} diff --git a/apps/whatsapp/src/constants.ts b/apps/whatsapp/src/constants.ts new file mode 100644 index 0000000..34595ce --- /dev/null +++ b/apps/whatsapp/src/constants.ts @@ -0,0 +1,16 @@ +import dotenv from "dotenv"; +import dotenvExpand from "dotenv-expand"; +dotenvExpand.expand(dotenv.config()); + +export const CMD_PREFIX = process.env.CMD_PREFIX?.trim() as string; +export const BOT_PREFIX = `${process.env.BOT_PREFIX?.trim()} ` as string; +export const BOT_NAME = process.env.BOT_NAME?.trim() as string; +export const ENABLE_REACTIONS = process.env.ENABLE_REACTIONS as string; +export const ALLOWED_USERS = process.env.ALLOWED_USERS + ? process.env.ALLOWED_USERS.split(",") + : []; +export const BLOCKED_USERS = process.env.BLOCKED_USERS + ? process.env.BLOCKED_USERS.split(",") + : []; +export const IGNORE_MESSAGES_WARNING = process.env + .IGNORE_MESSAGES_WARNING as string; diff --git a/apps/whatsapp/src/database.ts b/apps/whatsapp/src/database.ts deleted file mode 100644 index 4c899f7..0000000 --- a/apps/whatsapp/src/database.ts +++ /dev/null @@ -1,53 +0,0 @@ -import fs from "node:fs"; -import os from "node:os"; -import path from "node:path"; -import { tasks } from "@trigger.dev/sdk/v3"; -import type { FormattedMessage } from "./types"; - -const DEFAULT_APP_FOLDER = path.join( - os.homedir(), - ".earth-defenders-assistant", -); -const getDbPath = (appFolder: string) => - path.join(appFolder, "message_db.json"); - -const ensureDirectoryExists = (directory: string) => { - if (!fs.existsSync(directory)) { - fs.mkdirSync(directory, { recursive: true }); - } -}; - -export const getMessages = ( - appFolder: string = DEFAULT_APP_FOLDER, -): FormattedMessage[] => { - const dbPath = getDbPath(appFolder); - if (fs.existsSync(dbPath)) { - const data = fs.readFileSync(dbPath, "utf8"); - return JSON.parse(data); - } - return []; -}; - -export const saveMessage = async ( - message: FormattedMessage, - appFolder: string = DEFAULT_APP_FOLDER, -) => { - const handle = await tasks.trigger("save-received-message", message); - - //return a success response with the handle - return Response.json(handle); -}; - -export const updateMessageProcessedState = ( - messageId: string, - processed: boolean, - appFolder: string = DEFAULT_APP_FOLDER, -) => { - ensureDirectoryExists(appFolder); - const dbPath = getDbPath(appFolder); - const messages = getMessages(appFolder); - const updatedMessages = messages.map((msg) => - msg.id === messageId ? { ...msg, processed } : msg, - ); - fs.writeFileSync(dbPath, JSON.stringify(updatedMessages, null, 2), "utf8"); -}; diff --git a/apps/whatsapp/src/message/message.ts b/apps/whatsapp/src/message/message.ts new file mode 100644 index 0000000..097a15a --- /dev/null +++ b/apps/whatsapp/src/message/message.ts @@ -0,0 +1,46 @@ +import type { WAMessage } from "@whiskeysockets/baileys"; +import { sock } from "../client"; +import { BOT_PREFIX } from "../constants"; +import { react } from "../utils/whatsapp"; + +export async function handleMessage(message: WAMessage) { + await react(message, "working"); + + const chatId = message.key.remoteJid; + if (!chatId) { + return console.error("Invalid chat ID"); + } + + const isGroup = chatId.endsWith("@g.us"); + const streamingReply = await sock.sendMessage( + chatId, + { text: "..." }, + { quoted: message }, + ); + + if (!streamingReply) return console.error("No streaming reply"); + + let response: string; + + response = "Hello World!"; + + try { + if (!response) return "No response found"; + + await sock.sendMessage( + chatId, + { text: response, edit: streamingReply.key }, + { quoted: message }, + ); + + await react(message, "done"); + } catch (error) { + console.error(error); + + const errorReply = await sock.sendMessage(chatId, { + text: BOT_PREFIX + (error as Error).message, + }); + + await react(message, "error"); + } +} diff --git a/apps/whatsapp/src/messageHandler.ts b/apps/whatsapp/src/messageHandler.ts deleted file mode 100644 index 787c6f4..0000000 --- a/apps/whatsapp/src/messageHandler.ts +++ /dev/null @@ -1,112 +0,0 @@ -import fs from "node:fs"; -import os from "node:os"; -import path from "node:path"; -import { logger } from "@eda/logger"; -import type { Message } from "whatsapp-web.js"; -import { saveMessage } from "./database"; -import type { FormattedMessage } from "./types"; - -const DEFAULT_APP_FOLDER = path.join( - os.homedir(), - ".earth-defenders-assistant", -); - -export const handleMessage = async ( - message: Message, - appFolder: string = DEFAULT_APP_FOLDER, -) => { - const mediaDir = path.join(appFolder, "media"); - try { - if (!fs.existsSync(mediaDir)) { - fs.mkdirSync(mediaDir); - } - - let mediaPath: string | undefined; - - const chat = await message.getChat(); - const groups = ["Casa de Palha"]; // Add your group IDs or names here - - const isGroup = chat.isGroup; - const groupId = message.fromMe ? message.to : message.from; - const groupName = chat.name; - - const isDebugMode = process.env.DEBUG === "true"; - const isFromSelf = message.fromMe; - const isRelevantGroup = - isGroup && (groups.includes(groupId) || groups.includes(groupName)); - if ((isDebugMode && isFromSelf) || isRelevantGroup) { - const formattedMessage: FormattedMessage = { - id: message.id.id, - userId: message.from, - text: message.body, - timestamp: message.timestamp, - chatId: { - server: "g.us", - user: message.from, - _serialized: `${message.from}@g.us`, - }, - processed: false, - mediaPath, - meta: { - fromMe: message.fromMe, - self: message.id.self, - _serialized: message.id._serialized, - type: message.type, - location: - message.type === "location" - ? { - lat: message.location?.latitude - ? Number(message.location.latitude) - : 0, - lng: message.location?.longitude - ? Number(message.location.longitude) - : 0, - } - : null, - isGroup, - groupMetadata: { - id: { - server: "g.us", - user: groupId, - _serialized: `${groupId}@g.us`, - }, - name: groupName, - participants: chat.participants?.map((participant) => ({ - id: participant.id, - isAdmin: participant.isAdmin, - isSuperAdmin: participant.isSuperAdmin, - })), - }, - from: message.from, - to: message.to, - author: message.author || "", - ack: message.ack, - hasReaction: message.hasReaction || false, - mentionedIds: message.mentionedIds?.map((id) => id._serialized) || [], - groupMentions: - message.groupMentions?.map((id) => id._serialized) || [], - links: message.links || [], - }, - }; - if (message.hasQuotedMsg) { - const quotedMessage = await message.getQuotedMessage(); - console.log(quotedMessage); - formattedMessage.quotedMessage = quotedMessage; - } - if (message.hasMedia) { - const media = await message.downloadMedia(); - mediaPath = path.join( - mediaDir, - `${message.id.id}.${media.mimetype.split("/")[1]}`, - ); - fs.writeFileSync(mediaPath, media.data, "base64"); - logger.info(`Media saved to ${mediaPath}`); - formattedMessage.mediaPath = mediaPath; - } - saveMessage(formattedMessage, appFolder); - logger.info(`Message saved: ${message.id.id}`); - } - } catch (error) { - logger.error(`Error processing message: ${error}`); - } -}; diff --git a/apps/whatsapp/src/tests/database.test.ts b/apps/whatsapp/src/tests/database.test.ts deleted file mode 100644 index 1450b44..0000000 --- a/apps/whatsapp/src/tests/database.test.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { afterEach, beforeEach, describe, expect, it } from "bun:test"; -import { mock } from "bun:test"; -import crypto from "node:crypto"; -import os from "node:os"; -import path from "node:path"; -import { - getMessages, - saveMessage, - updateMessageProcessedState, -} from "../database"; -import { cleanupTestDB, createMockMessage } from "./utils"; - -const DEFAULT_APP_FOLDER = path.join( - os.homedir(), - ".earth-defenders-assistant", -); - -describe("Database functions", () => { - let TEST_APP_FOLDER: string; - - beforeEach(() => { - TEST_APP_FOLDER = path.join( - os.tmpdir(), - `test-earth-defenders-assistant-${crypto.randomBytes(8).toString("hex")}`, - ); - cleanupTestDB(TEST_APP_FOLDER); - }); - - afterEach(() => { - cleanupTestDB(TEST_APP_FOLDER); - }); - - it("should save and retrieve messages with custom appFolder", () => { - const mockMessage = createMockMessage(); - - saveMessage(mockMessage, TEST_APP_FOLDER); - const messages = getMessages(TEST_APP_FOLDER); - - expect(messages).toHaveLength(1); - expect(messages[0]).toEqual(mockMessage); - }); - - it("should update message processed state with custom appFolder", () => { - const mockMessage = createMockMessage(); - saveMessage(mockMessage, TEST_APP_FOLDER); - - updateMessageProcessedState(mockMessage.id, true, TEST_APP_FOLDER); - const messages = getMessages(TEST_APP_FOLDER); - - expect(messages[0].processed).toBe(true); - }); - - it("should return an empty array when no messages exist with custom appFolder", () => { - const messages = getMessages(TEST_APP_FOLDER); - - expect(messages).toEqual([]); - }); -}); diff --git a/apps/whatsapp/src/tests/messageHandler.test.ts b/apps/whatsapp/src/tests/messageHandler.test.ts deleted file mode 100644 index 5dc8709..0000000 --- a/apps/whatsapp/src/tests/messageHandler.test.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { afterEach, beforeEach, describe, expect, it, mock } from "bun:test"; -import crypto from "node:crypto"; -import fs from "node:fs"; -import os from "node:os"; -import path from "node:path"; -import { getMessages } from "../database"; -import { handleMessage } from "../messageHandler"; -import type { FormattedMessage, MockMessage } from "../types"; -import { cleanupTestDB } from "./utils"; - -describe("Message Handler", () => { - let TEST_APP_FOLDER: string; - let mockMediaDir: string; - - beforeEach(() => { - TEST_APP_FOLDER = path.join( - os.tmpdir(), - `test-earth-defenders-assistant-${crypto.randomBytes(8).toString("hex")}`, - ); - mockMediaDir = path.join(TEST_APP_FOLDER, "media"); - - fs.mkdirSync(TEST_APP_FOLDER, { recursive: true }); - fs.mkdirSync(mockMediaDir); - }); - - afterEach(() => { - cleanupTestDB(TEST_APP_FOLDER); - }); - - const createMockMessage = (overrides = {}): MockMessage => ({ - id: { id: "test-id", _serialized: "test-id", self: false }, - from: "test-user", - body: "Test message", - timestamp: Date.now(), - fromMe: false, - hasMedia: false, - type: "chat", - getChat: mock(() => - Promise.resolve({ - isGroup: true, - name: "Casa de Palha", - participants: [], - }), - ), - author: "", - ack: 0, - hasReaction: false, - mentionedIds: [], - groupMentions: [], - links: [], - to: "test-group", - ...overrides, - }); - - const messageGenerator = (): MockMessage[] => [ - createMockMessage(), // Text message - createMockMessage({ - id: { - id: "media-message-id", - self: false, - _serialized: "media-message-id", - }, - body: "This is a media message", - type: "image", - hasMedia: true, - downloadMedia: mock(() => - Promise.resolve({ - data: Buffer.from("fake-image-data").toString("base64"), - mimetype: "image/jpeg", - }), - ), - }), // Media message - ]; - - it.each(messageGenerator())( - "should handle different types of messages correctly", - async (mockMessage: MockMessage) => { - await handleMessage(mockMessage, TEST_APP_FOLDER); - - const savedMessages = getMessages(TEST_APP_FOLDER); - expect(savedMessages).toHaveLength(1); - - const savedMessage = savedMessages[0] as FormattedMessage; - expect(savedMessage).toMatchObject({ - id: mockMessage.id.id, - userId: mockMessage.from, - text: mockMessage.body, - timestamp: expect.any(Number), - chatId: expect.any(Object), - processed: false, - meta: expect.objectContaining({ - fromMe: mockMessage.fromMe, - self: mockMessage.id.self, - _serialized: mockMessage.id._serialized, - type: mockMessage.type, - isGroup: true, - from: mockMessage.from, - to: mockMessage.to, - author: mockMessage.author, - ack: mockMessage.ack, - hasReaction: mockMessage.hasReaction, - mentionedIds: expect.any(Array), - groupMentions: expect.any(Array), - links: expect.any(Array), - }), - }); - if (mockMessage.hasMedia) { - expect(savedMessage.mediaPath).toBeTruthy(); - if (savedMessage.mediaPath) { - expect(savedMessage.mediaPath).toMatch( - new RegExp(`${mockMessage.id.id}\\.jpeg$`), - ); - expect(fs.existsSync(savedMessage.mediaPath)).toBe(true); - } else { - throw new Error("Expected mediaPath to be defined"); - } - } else { - expect(savedMessage.mediaPath).toBeUndefined(); - } - }, - ); -}); diff --git a/apps/whatsapp/src/tests/utils.test.ts b/apps/whatsapp/src/tests/utils.test.ts deleted file mode 100644 index 6bc6d98..0000000 --- a/apps/whatsapp/src/tests/utils.test.ts +++ /dev/null @@ -1,72 +0,0 @@ -import fs from 'node:fs'; -import { TEST_DB_PATH, cleanupTestDB, createMockMessage } from './utils'; -import type { FormattedMessage } from '../types'; - -describe('Whatsapp Store Utils', () => { - afterEach(() => { - cleanupTestDB(); - }); - - describe('TEST_DB_PATH', () => { - it('should be a valid path', () => { - expect(TEST_DB_PATH).toBe('/tmp/test_message_db.json'); - }); - }); - - describe('cleanupTestDB', () => { - it('should remove the test database file if it exists', () => { - // Create a dummy file - fs.writeFileSync(TEST_DB_PATH, 'test'); - expect(fs.existsSync(TEST_DB_PATH)).toBe(true); - - cleanupTestDB(); - - expect(fs.existsSync(TEST_DB_PATH)).toBe(false); - }); - - it('should not throw an error if the file does not exist', () => { - expect(() => cleanupTestDB()).not.toThrow(); - }); - }); - - describe('createMockMessage', () => { - it('should create a mock message with default values', () => { - const mockMessage = createMockMessage(); - - expect(mockMessage).toEqual(expect.objectContaining({ - id: 'test-id', - userId: 'test-user', - chatId: 'test-chat', - text: 'Test message', - processed: false, - meta: { - fromMe: false, - }, - })); - - expect(mockMessage.timestamp).toBeDefined(); - expect(typeof mockMessage.timestamp).toBe('number'); - }); - - it('should override default values with provided overrides', () => { - const overrides: Partial = { - id: 'custom-id', - text: 'Custom message', - processed: true, - }; - - const mockMessage = createMockMessage(overrides); - - expect(mockMessage).toEqual(expect.objectContaining({ - id: 'custom-id', - userId: 'test-user', - chatId: 'test-chat', - text: 'Custom message', - processed: true, - meta: { - fromMe: false, - }, - })); - }); - }); -}); diff --git a/apps/whatsapp/src/tests/utils.ts b/apps/whatsapp/src/tests/utils.ts deleted file mode 100644 index 4934dcb..0000000 --- a/apps/whatsapp/src/tests/utils.ts +++ /dev/null @@ -1,28 +0,0 @@ -import fs from "node:fs"; -import path from "node:path"; -import type { FormattedMessage } from "../types"; - -export const TEST_DB_PATH = path.join("/tmp", "test_message_db.json"); - -export function cleanupTestDB() { - if (fs.existsSync(TEST_DB_PATH)) { - fs.unlinkSync(TEST_DB_PATH); - } -} - -export function createMockMessage( - overrides: Partial = {}, -): FormattedMessage { - return { - id: "test-id", - userId: "test-user", - chatId: "test-chat", - text: "Test message", - timestamp: Date.now(), - processed: false, - meta: { - fromMe: false, - }, - ...overrides, - }; -} diff --git a/apps/whatsapp/src/types.ts b/apps/whatsapp/src/types.ts deleted file mode 100644 index 9a1cb3d..0000000 --- a/apps/whatsapp/src/types.ts +++ /dev/null @@ -1,67 +0,0 @@ -import type { ChatId } from "whatsapp-web.js"; - -export interface FormattedMessage { - id: string; - userId: string; - chatId: ChatId; - text: string; - timestamp: number; - processed: boolean; - mediaPath?: string; - meta: { - quotedMessage?: string; - fromMe: boolean; - self: boolean; - _serialized: string; - type: string; - location: { - lat: number; - lng: number; - } | null; - isGroup: boolean; - groupMetadata: { - id: { - server: string; - user: string; - _serialized: string; - }; - name: string; - participants: { - id: string; - isAdmin: boolean; - isSuperAdmin: boolean; - }[]; - }; - from: string; - to: string; - author: string; - ack: number; - hasReaction: boolean; - mentionedIds: string[]; - groupMentions: string[]; - links: { link: string; isSuspicious: boolean }[]; - }; -} - -export interface MockMessage { - id: { id: string; _serialized: string; self: boolean }; - from: string; - body: string; - timestamp: number; - fromMe: boolean; - hasMedia: boolean; - type: string; - getChat: () => Promise<{ - isGroup: boolean; - name: string; - participants: { id: string; isAdmin: boolean; isSuperAdmin: boolean }[]; - }>; - author: string; - ack: number; - hasReaction: boolean; - mentionedIds: { _serialized: string }[]; - groupMentions: { _serialized: string }[]; - links: { link: string; isSuspicious: boolean }[]; - to: string; - downloadMedia?: () => Promise<{ data: string | Buffer; mimetype: string }>; -} diff --git a/apps/whatsapp/src/utils/whatsapp.ts b/apps/whatsapp/src/utils/whatsapp.ts new file mode 100644 index 0000000..6dfa4cc --- /dev/null +++ b/apps/whatsapp/src/utils/whatsapp.ts @@ -0,0 +1,223 @@ +import type { proto } from "@whiskeysockets/baileys"; +import { stripIndents } from "common-tags"; +import { sock } from "../client"; +import { helpStatement } from "../commands/commands_index"; +import { + ALLOWED_USERS, + BLOCKED_USERS, + BOT_PREFIX, + CMD_PREFIX, + ENABLE_REACTIONS, + IGNORE_MESSAGES_WARNING, +} from "../constants"; + +export function isGroupMessage(message: proto.IWebMessageInfo) { + return message.key.remoteJid?.endsWith("@g.us") ?? false; +} + +export function getPhoneNumber(message: proto.IWebMessageInfo) { + return message.key.remoteJid?.split("@")[0] ?? ""; +} + +async function getGroupName(groupJid: string): Promise { + try { + const groupMetadata = await sock.groupMetadata(groupJid); + return groupMetadata.subject || groupJid; + } catch (error) { + console.error(`Error fetching group name for ${groupJid}:`, error); + return groupJid; + } +} + +export const REACTIONS = { + queued: process.env.QUEUED_REACTION || "πŸ”", + working: process.env.WORKING_REACTION || "βš™οΈ", + done: process.env.DONE_REACTION || "βœ…", + error: process.env.ERROR_REACTION || "⚠️", +}; + +export type Reaction = keyof typeof REACTIONS; + +export async function react( + message: proto.IWebMessageInfo, + reaction: keyof typeof REACTIONS, +) { + switch (ENABLE_REACTIONS) { + case "false": + break; + case "true": + await sock.sendMessage(message.key.remoteJid!, { + react: { + text: REACTIONS[reaction], + key: message.key, + }, + }); + break; + case "dms_only": + if (isGroupMessage(message)) return; + await sock.sendMessage(message.key.remoteJid!, { + react: { + text: REACTIONS[reaction], + key: message.key, + }, + }); + break; + case "groups_only": + if (!isGroupMessage(message)) return; + await sock.sendMessage(message.key.remoteJid!, { + react: { + text: REACTIONS[reaction], + key: message.key, + }, + }); + break; + default: + break; + } +} + +export function invalidArgumentMessage(args: string, usage?: string) { + return stripIndents`${BOT_PREFIX}Invalid argument _"${args}"_ + + ${usage ? `Usage: *_${CMD_PREFIX}${usage}_*\n` : ""} + ${helpStatement} + `; +} + +export function unauthorizedCommandFor(command: string) { + return stripIndents` +${BOT_PREFIX}Unauthorized: You are not an admin in this group. + +Only admins can run the *${command}* command + +${helpStatement}`; +} + +export async function shouldIgnore(message: proto.IWebMessageInfo) { + if (ALLOWED_USERS.length === 0 && BLOCKED_USERS.length === 0) { + return false; + } + + const senderNumber = getPhoneNumber(message); + + if (isGroupMessage(message)) { + // Check if this message came from a blocked user + if (BLOCKED_USERS.includes(senderNumber)) { + console.warn( + `Ignoring message from blocked user "${message.pushName}" <${senderNumber}>`, + ); + return true; + } + + // Fetch group metadata + const groupJid = message.key.remoteJid!; + try { + const groupMetadata = await sock.groupMetadata(groupJid); + + // Check if any allowed users are in the group + const allowedInGroup = groupMetadata.participants.some((participant) => { + const participantNumber = participant.id.split("@")[0]; + return ALLOWED_USERS.includes(participantNumber); + }); + + if (!allowedInGroup) { + console.warn( + `Ignoring message from group "${groupMetadata.subject}" because no allowed users are in it`, + ); + return true; + } + } catch (error) { + console.error("Error fetching group metadata:", error); + return true; // Ignore the message if we can't fetch group data + } + } else { + // It's a private message, so just check if the user is blocked or isn't in the allowed list + if ( + BLOCKED_USERS.includes(senderNumber) || + !ALLOWED_USERS.includes(senderNumber) + ) { + console.warn( + `Ignoring message from blocked/not allowed user "${message.pushName}" <${senderNumber}>`, + ); + return true; + } + } + + return false; +} + +export async function shouldReply(message: proto.IWebMessageInfo) { + // Extract the message body from either conversation or extendedTextMessage + const messageBody = + message.message?.conversation || message.message?.extendedTextMessage?.text; + + // Check if the message is a command + const isCommand = (messageBody ?? "").startsWith(CMD_PREFIX); + + if (isGroupMessage(message) && !isCommand) { + // Get mentions and check if the bot is mentioned + const mentions = + message.message?.extendedTextMessage?.contextInfo?.mentionedJid || []; + + // Extract the core user ID without suffix for comparison + const userId = sock.user?.id?.split("@")[0].split(":")[0] || ""; + const isMentioned = mentions.some( + (mention) => + mention.split("@")[0] === userId || mention === sock.user?.id, + ); + + // Check if the message is quoting the bot's message + const quotedParticipant = + message.message?.extendedTextMessage?.contextInfo?.participant; + const isQuotingBot = + quotedParticipant?.split("@")[0].split(":")[0] === userId; + + if (!isMentioned && !isQuotingBot) { + // Ignore if not mentioned or not quoting the bot + console.warn( + "Group message received, but the bot was not mentioned and its last completion was not quoted. Ignoring.", + ); + return false; + } + } + + return true; +} + +export async function shouldIgnoreUnread( + message: proto.IWebMessageInfo, + unreadCount: number, +) { + if (unreadCount > 1) { + const chatJid = message.key.remoteJid!; + + // Mark messages as read + await sock.readMessages([message.key]); + + const isGroup = chatJid.endsWith("@g.us"); + let warningMessage = ""; + + if (isGroup) { + const groupName = await getGroupName(chatJid); + console.warn( + `Too many unread messages (${unreadCount}) for group chat "${groupName}". Ignoring...`, + ); + warningMessage = `Too many unread messages (${unreadCount}) since I've last seen this chat. I'm ignoring them. If you need me to respond, please @mention me or quote my last completion in this chat.`; + } else { + console.warn( + `Too many unread messages (${unreadCount}) for chat with user "${getPhoneNumber( + message, + )}". Ignoring...`, + ); + warningMessage = `Too many unread messages (${unreadCount}) since I've last seen this chat. I'm ignoring them. If you need me to respond, please message me again.`; + } + + if (IGNORE_MESSAGES_WARNING === "true") { + await sock.sendMessage(chatJid, { text: BOT_PREFIX + warningMessage }); + } + + return true; + } + + return false; +} diff --git a/bun.lockb b/bun.lockb index ef2a5de5937a1dac36939b54dd6013f0b8a16f2c..223eb489f3b7d6483f376a6443b950770bf79887 100755 GIT binary patch delta 164267 zcmc${cYGDa_ddRN$%VTpy-4p;lu)GHkdO;OdJ~W)N=dnpKpH8ONJxUHG--=2Qp8RX zl_qEaAt<8Q6-5LQ8v-Jtf+glC>J?aY}oXJ&SnnE&_f zEC0HEX`5CHTO7WBXTvk|jt#qSMXj(7``_-;@0X9NoLX7FL8U)7`+9`WD=BaA=gnoK z8k!xqY@JcdlefNtVN^1V{Pd*MH0FI>(J*cS9|)lZd;xefa19Xu-diqSm$V^X8%DTfa-a+M>$ln31VHkP&Pa;tr zf+fHzz6oS;5q4hOq>MOykZ_QzNjZ1y$9q-i-5|c!C=tesiUz| z^A~}fl3@)Dqb@KrD(zqm(7u+=#eI;CB$YX8pij?N0odTF-#AI*#4qM zus;*71@44^Cj1Z+#F4!TWd1H7<;hJ%(fc&s1*E(;kc!35NJ?YhN1BR{ybGj!ax4Ll9ba%Ede zxvwFoMK*&|;nhHvFNU1$q^2bV!>7kiTY-91V1aITc5~RD1;#@FE9LhFV*2x+f&$cV zF0eju_w9xO|t zse_bT2c!j(QYR)SO-js~5}Y}W`5QWlypwKkVr*vCRLF;A1hX@fCeAe4brQ`zoy8)9 zfLw;ZLQVy4)fL12Qm!$OmOHNT17HPq9ATfU(I9U^7a4HUq|~$wczZB2JtGLbIa0ey zd|E~-`j4Hg)HBXNUI*2W06Fe*=#JfIXC#GBhEJtur_PMWEY{TWt1u|-_A`)be4{Zl zTG=Tte=-vEz0Z+BgS@LTDKlKz?74s#bibCj>Lwk|267zfu~~`Xu-^2l9DrxiAkvm;o0d%%bqfj`Wn;2lhfj3v(QL#Qc6;mQNNeu2Q#N;2lFy! z8u5v-$y1=fF=WtGiLsgCaGyyZ=z>r6mXTfrQo*O;<8;B#`WOZlX#QhBu8G+|#Uobm%eDp-A>{2D~)SP2Tku1>;cXQJrNVk zh&&QV-*&fz>}1&*77r22j2J4u-Vb;)%DGORnXKZh5uAolfS;_)*opLwb;G3I;$e9b ze;!0bw17JmS>RQ`7mg4wp95t6OpWm)r2#nU^u*XKBMEXUICPYRkr*I(dyR3UWgx?V zl)HOes-0A}Qr>9PW^ZuZ_*kenX^aeK43I897|3C{uHHj)Kkz2V!-1TiW_5X zzL8)Ui0Jt{fgJG@n5F8#ML;SxGd5!q9MSj=k(8D?0%X*A8%WFS(O4M~fE{>&v`jEF zJ~lZvAsC;Wl%0h7x8g4~kL@GWnxBb!mEi;V8=z4G;ONQXc^SdXwB%_)qx}>aNeduN zTpOs@Xxel%F$LzN!uwE7O!v3u2DiEKTHYF(`He+IDa9S`mD{~T>qXNGI zIrkaC*aV&=V^e3Q#Ad}OW@g7{#AXJK0ryLX=}8%gwwXps4%IM>+h$3}S&6|^>Q+_r z`al>nKQa#$*|57sf1fL+J_4i#-T_kcl%&+8^w`YI{ou4zDimjaERYSk&h_sd$92w8 z1u_uVIo1GHi?=EDnW_V~%O5 zM}S-vuvB~&a&|r_yy`>JPd4)Dtnta2m;}uKI0)ErBOn!dXP#gKAf0pie2E7o56k)< zu|VR)MsOufHk@P2df0u>tZQ5NMm&% zN90!UgVzKtS`vQ)Kh*8ElaCNlL zLV|NS9>@mTtPl&_sjIwxa>Ug$#Kcn`lfX0q$a2GhoQf!oI~-AH zJ&-1OI5>4`{FLaBmC>;?+pMq4`GBm~<7t`2OS;~Nz$%c>1hW13G_3d_{NM*vrq$d# z?!T{=?o~1$KEd$+B#_2%@51iYw1G!DoRpC^H9Rw6O84ib!?qe50y*0)){DFfct!f* zTM$rzFbJrbYi&2my0fzWIhpo(wS16WBMLsPCd5}?mm)38i#<{!2aBIP7nCLy?qbe5S7~cY%18%feX1Xendaeh; zV0roVP=GGrR&ejmI#?Q4UY3UM*e8ZK15U$*?Uxz40y#VW1afwC5J-zB;m8r3k!2(l ziO*UGM7|UGm5~2DP~ArouzTUqmbn@NDzY5N5oZ7!0fRs`I9g**V0G|O2jx`V56BT= zrB8tu!Ko#5wMDrapo0%d`D)-j$YUDjKflK zgXV4C6-z7uXZ@L4J`TuOd8fwPfwjP^0~u2M6+3Q}E#rGWXf1 z#mVQMmDQ7X3W-XnaP3FQn3R@|JAq-m2+jt719CNt)i^ON1A!nuvB?=J@6&SYyjb!} zaQemoU{&B`$T?-dp=0{WTR)41vZdd={6{Xx+&1_{IvS6L*>v^4D*^|C)0Yb`S)z!{I!cNIsuj0@ zYW+_{0ZzfMs6f-50J6jPft2q7vcczpG-17B;g5rJit>R}sGrX70;J`_fj0xI0ohJj zAj`-7CGtY_o5uzp)(T_;tAck1Qh~-As{q;HQH+evwGT)|cfzUY-R^@9_Y$`Ta(1); z$o6IdsbHFx*YtRlPvm$!au@pZdp(CJz~z`?dQ`;({m7>yfv z`P~9!2kwJieVQ{eQBEz_n>=cH)&r->qkx>c+kv#;WVr+y6?FcjJUph1#lx&|5YXfU zf$ZQPpGTQ&QZSW|fQ`vGCv?KHd=heo=KG*vbKsD9GBrJc9O-S9r9*Wcoifc>4o=gb z09wF&Alr|tA{NTSQBh?agMgZ5XC#MDPD@Iin#m|&81Bi8u{$#h&r_?3qFsQrkOSnD z9jY!JWF}3*Bi1zIVaPe+onlcql!P!Ap zQtHf1yeJu~^Knc{qGD`t8lJAG$A!VE$rWo!`G0N^)3?#}>i{|OrL{#KAB$T!eyS*W zPw5~%EiIWH@rh7oRzf&mlpKOZXsWEt__WlC@l(3i^{931KCPRGOEDYjP}k0>*F3LT zeKAZ0Abqr5Lyz*=+CcL3jHJvIJh3eUPT#wLLo;XmsYYV>l-P_Zcz$hs2|0cGEsfhW zt^?B0u}!4D`yhA(I1HSJRhXO*#Ixk|*bF3tGd;~j-(-Yq>N&Tm7zlTK+-HNC327EnAlzAFG0?c)@>slS8ZcYZqU5W{I;Us7Kqu5dwptuzrCqJ^E{kh z)WrV(ba=_jZ}#c`x5JA%7Q_B^yGShYXgm|)QQ`bUjk|!1y6ZH~1JaW-fa)p2I3(z* zy?~tfjIM%_;51k}Aa}lw(ZXAR)6VYYDK0xRE^Ve^1OigeeIVpM@JY+Sl@32$x`{`I z1KCegV0nh?a@{4vKgJ%`s7+p6vMO%GWlx;QJC(PG^hbAPznQpUr6YY9a{9>@Alt*; zJbWVJ-RF0TzV&*FJUu&w#f_ejb4B1u+4NMjyBhU4uJOIZu?GUNH1qQRLV^V|B*Yl* zJ0U*FO2Mr^r?(hFy>^R@OB#lTsYrVu*LD*i9rrnOKm+apvOQP9>iuLuaj_Y}aJ*4e z2+co${3b3zy!i~qrpz$jPyz&t{xXtLKx#Y~NK3dQjq4~DR#!xfv}Ayk?>A67UJaxo zcqx{VhJ4q8lZVKL+8@{m`L_cb;w+V40STT{f4NKW6Ce-5JAn)$t2E{VxsN3R>0?8I z^sOi$qvNd_D+3uNuMHOb5lEN&RO3F4F9GuyG|rC{6OMtPG59N^M3J>Xb`%3S&n$7c z%!YUFXtdP4GLZS%V??o~gJf#Pf>Yk@UYYBk!09u6fK)W|KH(k5=(9{?WNT!w?G*kIyflhiu76LRivZ^w(p_5eAuj9?b; z&BkL1Vu1xf%G(5`<7Gfva5|7vIANj;^zXcTBz#twBqO_u436Xi9t@)T)*mBu+h*2~+UEFI3$4I~1oKzw{! zHeXI>U{1N5PC+5!r$C00LqLx3&s1sX*EEm1jC}^A(@p}?%ll6ig`Wj)555}6)pRvO zSt2ig0}^zKuSJmmI*?Ad3`p;u0i@}QvZdiCfy|$)c?yu_Mr!N=r0*o79u>T6hIIHa zI_ALkLUCFqdL}K5_5TF7CP^7TNj5;)|v z+;(WW&?(75zP2#Z!|~QUJ7{FvVu#YV>h>osnwza3wToLc$a7!CxUXKeJ|@<8U%j*j zre%uaxQj_sPvY!ReDho*W6#fO{fvU&;Db%kW3` zkm+u+MqBU^DOuKt|<5sK;r&X`LtrZ>YoRy!|yR9 znDIW4(?1Q!t$aBe9WoLga+2`L+e*mkoTFcn0X+p|`~LvxGxK%5mv@S#r@eq^ z$cD!ugTd?H9S(w=b6OY3xvK)CMbcv@1-b6`ye17j4`c_+fh?D+<&%MIKQ2B!yrbVR z`t}eD6z-P(M&#|41|xwSS*O>dEt;KWW6oU>VVTiT7kU$Al~w4!OL5{BR)_a$Pvs% zK{nt&EFColvV+xVm{ZdIU2(d_;2gkoAU8^P#UYpNkAoVq#1yZm4&jlZc9^A+m0x56#g_L(Axcda${eq_|@@bIi!MOC; z_$kKcU&@TW59DH+0%SX{0x>ap`Q=WCK2v}U8=H|qA1(Wp;3dduh`)|YzWcRSRuGRj zGO#h7lzcbLdz^PDHYegP-g>aNw03F&ks6czlsD8 zS+^jAi^2F&R@K?l;)9=PTmj@UbPJFH@V8^4z;9=yqwLgle0PXpjd;eX~ZD1Ah`Ixo(HSyjo&EEp2iSGvI zIr`bZM8U^^^zui5^!k}4V&U{j;e1V;+4OH&MKki`E@sq(fR^w9S+N)uh^TG0IXd>`-U~eFOr74h3Sn`MDAJv#Q;izQnfPe)S z>x_6H8|VS#$m(hNAD5+p_koPy>ouQK&QuB)X#N1&rDYfSM8OAuw1j#f9h;uSXFqV7 zyv#0^sYc!k$YnP2CR457K|of>OiIUxnTf{4)M?@H@62P6GoS{5^pzduO|=}?0J(aS zD@c3uz!~SVG{yqC1AJCdgEn<;yAQkCYOY+mhF%(=DUIb^y!frgu z=dr?LwWXqa3Qx(-%)+a^U|M=Gl@Hk)YX!#Fm5~<#>6}(Q>1ey=2f=BX9Y9*(Sss|M`Xr9v8y*4aDK`JyzchnWghHlqb7sv*0(s-?o@Siju*BI7VI?R&icE)mW zwtEkdzSE$ISR_8NU2H}I=KDQVq=r?siQNxa#y6FMDG8m_+#hRdf7!7?-eveI4RG(R zriw@XfE;+G=2HA#jp?yd(pawx@~OA`Z$$WU5aMO%0p)GTXZH<&oT+L+?qTJD)Z3$R zdRj&T{=JAjy;JQx*NEq!3(KAYa%}v%n0(1?($?o%{yvaxy{XZ4OZUPV(OTBc|J!HO z8Nn2M;%L~FI#+2@uZ@f=O|4MeEs`dw#p8bD)U>VLqw`?%`!?H`ckb+IncJq<6Rifk zx8m^{cieJ!pFWRmeWvl1w6>x8Aor@BYs& zyd5Z;wQOs0+<~uJ?>v35;EQr${c>Mj-1XhM7Y6qG=+*}B-tyV#U%Rh8wCCfaNi&{_ zUA=Kx+cA$ceb#G#BSSgzV3wIm(9Jm=8%d*zjrRLc{zXXi0r?IHSBpM?w<7pTTU;DxVhYhr^j`C z>(%K6y&i34ugLm#$UDB<_CGfKvjO>+jFUfXU;Kd4@tZn_-+Fe(o1eXMbV=X+-%g8O z+hy>P6XlKXBc~6@`^eSO)$TKA(95;1^l0sBJ-AA%mexDto84A@M2q;JhYa2K*N%7Z zxDqkomlm&AS+k_)n6FCwk$>+v=b74i!G$I#s~%ZD|L0)y*p_`RzL}f8=IDE$MrNY8}Zs?O8f9T!y$hV8` ze<^Br*T^pq%pZB;!?VXKWDH;a+s47KOdPfCeANTx-#nhU=+KL2PkdAN*x>!kx8Brv z^ZS=7r^VEJdFtayG0xD4x4!~`?Z%_MBlMLmn0NE`Tei59hLVcU zPoF#Sbep@fJC$Gi&6jh_eb>Rh*tKP&4XsCgd3V)wM@Jm{=Ju)$2jtXm@=eqIR+HDp zc-9>6{pY%L`_+Lf^Lo7zAOC)f-!tqJiG>wLA3r>}?SZFjAAX~H+|*{#-EXQC@$u+Q zAN9Mm?uUuTx@{W~9`VD*`d_{M*|uJPm?s{ZwQ_!k)Mdx}dA5B#;o%Qz^o@D;(&RG@ z&2PsIy>KD@o_!a*Yfk>$vPQojMpjtf?7PKZOvtOB`p=OMzTca^a`EM_+jo2R)>#i! z@zk;NySK}`^nBGd=JJV4-SK3;dGPqVkN%SQ(uv*Y8)vqz^YdD(daK|gAEY+;ecxwa zH0$#Admo&uGh|$wWxL*ZBdJOIdF2M*KRvzGg&t}BcK-6tts~YvGVNfWUR@TBqOdd{#bN7h>2YeFM8@VEiVjdpHevN!ZW33)O*stfJmN`XBMh#nI{QXPuVf)pQjoMv!yLVFECllWu@aRJ) z-uHZVaKVHY!~gueW5wRvioA1Hy%PI+`JrQ*+}^gN+rgt#yO#-ndixh6XH=?nc=)*7 zjn6!~=Glbtxla#FyE|C&LS+3ed6%~jndmEHpNXh!k4{>;{bJI*o6HX?*u_f%Rt;>5 z?Xky{vkMOTtv|p9I|Qh z@*Is-1jeq)*+sMb);^qv+ktuQf*im3OD#KRVZiKA+b&!fu(EKEVo8rvP{$JyMrN0M z+|}ob?U?C)Z%f>Bd)T?}bqzxzpeASoQ> z6(U8GsZ!ozu&#DA`n_ksqV4G1DD&3(cHxYG6~rB#9f#R5 z`F`seu+GvX*fE_aO5M`HFrtuG*^XK5w^G2yft9girunUR!C0@X@~?`xkM{rzb3CRe z!qd=>oZk(9=cvDnuXMf@{-=J-M)I_#vPO~OQ#|lnouaS9>e}t@w{8ZbO*v6MPPUq< zDh|Yv5aV>l(<}!6YjO+vlJzli{Hp(={_qprvp8MKF|7zN>SoxU8U6}6V9KiEPY|mnUg|H@4#fw6)Vgbs$;jt*~>y(u7bX6%kcTSXbGp>4U9G-!B z8w^H+mA7LS_$$l@-$L5c&i$yXubI1!>2Nc_v;(~4_q`9+6IDZN$gFa! z9eFxn4aS|71NGPiEB)4kU=d&jy|lt!uztF~O0IibWt)r7n6{_DZ|wo2WlPX zL&9ie*fFd8)&a0yig|wVd&}X)goIa(p+G($z!=rfIPhlL^WaIxL6eYu zv+Y3`8QR*3tmX+=E!*H_C!}JwRbV4R>=!Vqf%UZ2Z{63{Fb0ERl`Qa^o7y@#)lt7D z+;J$R7zwO135433rO#x z6oJ%lwSa>S0TWlB4%Qc}teV|-z}O<<<~ zYpX%}OyqM4OI@&gM>}#>z?y*!SzU|@+jTJ&gw0<&+QoYV)}5X36_;wsv&L^t1CtPg z1!!#s<2=C>kNB+$$mp(0ay0jW$s$0UwVnp+rb^PKzR@L7FU@at$086jW16hRU~C0O zM=bqOGqFhvcvuhB13f+iOqpIq-9j)l3+pikUj)N{)ND_Lr;Afu%M&n@x;T-wJpu2t zU0^Z0WOkJG1yd+y*)hBP-Wq5vl5#U6(k|Q@FrSNbBI{tDOay}aRBHvFM)*KDO>kBC z$V*@xCg$QnzxNUtTK=(X7$z&i&P|E(PC%-+y?0iW^*mDa0gMa_=^R*hXK!Qd(paY) zaXGcBbHR{>D8%u<2F5YMA;EqJ<2)KpQ7eQ-B*_rA5yaDwRO5FF8cH5pL8O4Ce$pke zQDO&-z(i1G?EHKDzJ*}4sZ(XEZ1L9nkkED*66!XDYH&Wpm4Xdu*JWyFeE^KTAZB64 z_h}{`>y2^8!MTdj41qh-Z*2mjcVc#+&u?IKLNRC)D5Kj%-Z;&$OJYk~1x9Nivj5_@ zj(~B*sF&t9%ipP%x3}A!oJ82$Hxnt2qjY3jAz=;lJHzk$QM1x0)S;(XRC$|ks%B2? zixQ=LFGC_NO5C!15U{~AbUI&4tOZU;=oDm4goOE6;0V}jz-UTLCQR}p7^-t?hw*O5 zjIe2O^ms6~B-8#97{*GqH%EklAR4COq|isK2)}vMZ(Ri&rp)D1o+G1vvZt)xp|mf z2sk;+E_MP|Lvc$+* z!GL7Y4dY4&qwc6S!*6aIX-BRKm}f`YIjaIz$|%`KVFB#52S(Y&$SXhEj$Dmn^=LV# zseohc2jk-RIx*qc3h%LtQRL2hLM}4jZ%)6*&RG*M*WY6ot_fJbA&uq+aIiSWzErfqRRm=S!Xb(#TZY|l#aTBaqOX)@=b$;z7Prs z)@DdTZTr3l<9Rex!)iBHh6VeH4KSyO4MPz zVT@6!N092SQtu-r6&g?A#f(*_AT^9BbMFMZ_?durb*y1bwo9Ig^7e|uZlzLBAvId1 zt|B$q-uqNnPdrPTPbl!7jyH@%RbX&}n>vfsM9R#&f_CA?fVnMb7X$8?Xh&`eSo0?u z>O4@zj!E%bm%x;>IWb@1#xzMqkbPZ!NFb``*RTt6BS`4dSg1LEUyDRpmZe6@g@lvf zbBYGR!@+1$I2j!9cQ7^gPQiXSOA^C_nV4ipz7()lCW#}dAn82?Hj-*vDU;<8r|x^^ zx0CJ2mjk{bQ!pwtTY3cAImIq~Ibi-a#V&q1VEL2Xnexnv@PYI|VY!RJ-`6&b%p^!KrrPj)1ui62Pgc zcH}Fwy@eTSreDS#G1D%7C14e2iotPA!15WBCF3kL|6)i6qidKE$A}YP^m-0iwoIp2&B?=HJy8TPXpY}}H`^{okxJ7-Cd>DGhl1hijAs{*AjOD@X9b9U7byn| z&4Sf>x&$;8{jB@I5H6|9g@|$>>?fqE?;<4hl#s+~Gea7L6>&A64u-4w^eFFZl&fpX zWu!PD*|6Hq40%@pg5ylPcwfMJ5kht(Ma&#Gej;951HmGY2Y18{J~zbpyym+cJF+NX zjlW+m1em)WerpR@XH>utX8FCR!CKq74@X&@W{K`_lTRXiAS{HFHWtwY2~&D0*`y`_L*iccO^X7-wEN4^vAPMd2OZS0b{QP#6eAsgWz4*dyOPcS&xYQI_YK|Atrz3+Y)m|c|k-WRX z2D*(~>%egQq>Zup|FOi5{5W8ZSSlWfv(oE+bH!4-5W>H|Adzl+FIL3RyN);;x zV_3qX#KPOW++98Fp@wZoej2d)*rjIhTMNOYX`V$t1*025B{-aag(#2f5LVGt&D6Rv zUsz!m9tl{UN8KJ+q#KwlEokPo!LYYXi!!f1Y8QSMuyP)g$$_iC84(78=YaF0%)cMA zb3PB4y&Svn^MLoT!)=?jTCB7qj|R-iEA5=4f18&8Uj(eXAD7``o5GcNJ(w&puKMpZ zQ#X8b;3_-kSio{tNfFr@uYl28P-MT~*LSrvRvK8BL&6zSkqUL&C5Ts%2GMi{pv3is!^BV7TS(iL#zUifax5W`W;#8jRtsv|ha@+%Q^z z;SC4tgnS(OkNJJ`!8)qa?hSM=B)uUK-OH?#nF&2jF=N)*ktYM@L+k7uK))yLLcoeA z?c$RG-w#iUl}h{W@RV3Wo-OBrwL{x@*2Q}=7<~?If>3z%DLd!ufH~x8yYTCP_mQWe zg&my}Wq$g!9eFBXRewfSI%XZ4$6e3Zg{Nj)4?xKFaM;IrWjh#ch&h5sUk2;WzSO?i z=~>Y%%qcpAp274OVaZ)!LzE@UJtr$&qQF=%t>5&vcKY{ytv4XyoJdd_`n=?0F=HcH z4b~HR7|q9i-`8MV5VBidJZSmWOJQu|*CN6|@az)LJwHY&fO0CFT9r2F?Z=6E94D>~ zcI26WxnqN!1GoTqIAGnj(J)4-3ZA8YvtXkgiQ~tvjdsqC&i2JKea$kP?8vhLYu+aD z6))Yd|$K1 z7CYyBz`AdXVGKYqY$@1w%WRc{BEkh$eFRCzVD6rYIjfk7mjrOwQ{e#*_FT zV4Xw#wth_-LBFWyfUyI4_I4bsv}Bb+w>!`32p|-9vKgwnSbgx=kBp1|HPXk2#2c@sBPZ9 z*N)5&SV?=u>k)pl{bnJAR|1yxx(KmC&=yD+R8t$fl@G?1jc#7X6KXK7dU?L~8<;p) z%o4cUK6hm!l$$T_vm=WGW{v%JPI16HVn6)GjxLU}9z%*s!4x=LG%k{bP>F{=v!KY% zxfZZqgK!9n$h_4&a9wCIM<1{w{|cD957;?>;T_`}u8|R$yo127A>~GSpFk>7JxKAr ziTfSe!rgX{-%NkgE-b;ae$y^430PN=$9{0wgFQDLltYQIo8J=43mXE)VPjVC2=iqy zY>W9(ruUGY^H0D`K4cgE6R@@)(uWnN;2Hwm+nU)iZz;lUSq>}4JLuRc@!%OKQnL4R zzg-2^mkoQrMXERVP;bk_D4`B8i;)`XwqbowxnkVt1Mj-AjH_c77zgHYpm2l_j3r@l zI9Sd1+#R0vCf~qb2jkhp;}on#z4zU-4(p8uyD!w=jvMl-e;_k2J&XaKOQ(qyYIL(-Je=Xva?79Rci1z_~(u)^^S5q|B&0-x z14v1^#^1TJ9HgS`=vh(bE8jWARq>{x`uFbn4BMc&;d`gJu_xfGcv}1=w5OT*r=6T8 zcqQ`aS*H-_{pBpzSv1yJr*kTJ%!slwk!q*BuK*tV3|J4va6b0_0EWPVQVoBSkdECE zUVaZ)1WMu1zu#{z`^hP6hSoo18GG-`QQmsz;bCfJ&qfMQRi;JxK1XUG>V&kjn*8jp z+k)rtR0(W2Yx_Fs*K)%$6rhB>({5len?HDZv8B~yK`MyfAU)^|wtS7YH? z48M3?d213_K;;#^6A=c2Ti&cF@2^PpvZHrIc{~3KV{+);r;!@2a(%z4a(EK_GE!Xh z9;fIxEXLpE$S3<Xqe$JY zw8RP8Jafs8oEGo~F2l#|y_l3aNO6F2hUJ zWGntDnoFeb4>r=teHSVq1&iPwc^)Yp2=SJ??-fi0`Up*pwHT7o((6lp^Yj%br!8C| z;;Nc3=r|jx(n)w5Y`7|om-be8v8z9i7t_J$dN^$0HREewy`O zoJdq#TFzq(LKi6bfZzPBoKp;`w~fzZWT=8xA(GTowdCDyd5i>=;q6o2V~msJb4d1f zN+LZ`)=x-AqB!0%+=6v@lSiK|J@ANo!6t;7Itn&M>bJkygLksj%R3dxo~kMDD@gW` zR*ecC^*9>m>S_2)3QUSy8AwvQn`~T(tW98aIXDht;zcl-Mm|*PTG6B8i&!M5qEpxn z!`}tz9jXQvrS~k@NYSHPC6AG+lGckzav8{jre7*K#ob}|sLD=cjAyp_WM!u?1}*(i zSq2{VXp9*VR>i}wHKs?IGpjf`Jv_6mbC9qRIk>j2Dr&2Xwv_-Dj=ZwA2XD5Qfw3g+ zS2(MG1=dm3;~M<6nn&V`Q`8RD#jm%~ArlvcnP7A^8T?0JoCjQpu#(Hyka>`})E$iL z6eS<=oB1`I!d@7_0Z3^ItU=7xC9tk;8LM4QX-(CzCWEo0JQZ96R$B50FlvuHXy4)% zw_j>MRx>q=mJJrF=lu|@oqC*EyOvvr({>jaCs-aTJ`BbYV;s2=VIcC}^rqS&Ltx%V zf^q1g>^d;|1crs@kU!LRBKu>o)$4@HEb)5>gCV{n5Iw{cO5?(l;x}Kf<3tWX=}VCE z{07BwW*t#i%BVY|`AA)-m}L$_%3kGW7FG|#0mGDT@>?Cjn5V*PR=0DY+sl99u;OoFSE^6^_|GUDBriCM_s(J$HTNXSU=Rp1S3eDXy_E9j9IOb z6L}ZP-rGntBJ>m zQED=(PY2_EfrkW({oZ%MWD2S@6`zt<+DpK?*trLzthbp`?Rsh;9^TYUCLLPi#?}Xn zR+1+U)4@2ocoKdoA`AqNxbT?7d#h`1UM-@*=r!;M7-1$DwHBY>8)EbvPjh!dFqPgY zuu)2*pONBZV)xjBYefrD0T0kHwe!GawXuagV4ODGlhIe%mNG)MGh26qG0$>h`e7x4 zMS+!fVjdEv#>`8l*V792;|azZh?Got0oG&=7)OHk@ZfVN7}t%OF!M?)r+5^mG4eJs z3Ec4*zV*6+?Yx0qa+#Vx>yFz!>iL554l50emX&5+1Y^g-&fdUoZ7su+ymT(Z)P-;FjTj#OuhuJyTLl6B%bTQfHs(ngI`O07%CZa)^C+-CliA4Bc%4w z3|~^fg>u2T#-+_yG?P&LbBG~qnzy!hBFAGp9NQjytCJgtV;E9{oaph`sjikLd*2a~ z+A5iwfTBnZkfOJDz*?1LR%vo?X|im`P=UKjlPgP;-;^eA>l7-RiDVyXx3DycgNG~a zTbf*gWG^Xuq%>LEACitMO+KTNdof-gQVjJK9nU~4-(FqhK%(i|8|h_G9$s|rfXEx@ zQ=p6-I`{I8v|p4*o$_?L^j-li}>spjEc$Qw;J80w6J@?WX17mmjgx> z@w^`IE`f`>5}5xPK1OD zTZNA@D|=#I9*OpzfD|7Y;jmk|m(-V~mkh=Ts2*GUUIOE3ru3VS^1ZJ!ly@*#YrAA` zl=nfT2CJ9PpCT1OnRQbiH{kJ&LBBpuaT=EK97y|eoA(|@YA`=o?Vvt=uGLp|OKc-B z*f=oGfv`1T++x&QSL+lQ4S*rd@>})$NgTtW4i~&pV4N+O;{m_*NQm*ea12b^EJD3o z`g_zD$fgrh8~&RDDw%wh@dB8*9CxIjz-VJ-6|2Pn=@9dbTSqJyK1st%?zKqiodxHt zcfmOSuq$qw)#-73O*y8(vPEIxs5%&*v3W3(5As%Cr>YIM_ zJ6L>(jU4JRVnb{f*!U2uIqZ6t4K_BE_X*g&Ar^kOW=_!vT!X+yJA1PcTu&i6T#_A! zBQi+xAtZ-L^7GPU^AVg(RV}MDc>u|QBCR+wlpIr+Z(s(^J3Ntyvh#!;cmsL&nf zQzZ2rr~PQE!8?le0Fq-+9GABWW<)tKPJ5}}SG&ifzEqaYCjsm(`v|^B`e9?unST zSHZZDWcimJD@=U50E}(Rne`M{oMJru^cW{jB-fBEF#5e5lQw`21CwtSI*u2YGM%ED zIQ(cP2edt4jN=#!T>It;;>>twhaMJz4M!ey!ESyEY&0007I_h|vO>x@G1Jf{m@bJC zy#YpDcJD7g}hB~8b(4(L-Y0#d;&b*t(47c`!bJl9Cs|xQG?=b_v42_?FSP2`Rdc{*ork z@=X+nkb)^->O&gqWu*9&Moo>c$s{p&$Vq%zkkGKIA!kcH)AB*W3jw}SL2Pc2DE09* zB@X)A!0>%EJ{CER6o;pr!xx`)gF5(je=j7Q1_Uwe7Bwcz(}4;q!mk2TF*)?l2hKnu zTN(QFHk*Rgq9XB|NZ~6vd=OMKS*NT-q@-mJ9>^R?_SpANkM=c75q(0R7WvX4360F& zGS3QwpraGL1M`LygO{A#B2wiPAUkI!*qzA3lK>2VCm1g+Hz{wbl;(EL8Lpm=tGx^n zuVXRB5RMzLzN zVU`D92htGpks9el??q9hO2bE3wyQw&G`H+}q(rH5(@{&+9~(+-M=Fjov&IZ3av1{l z=owB9&{{M@Oea2g0*sRr=6JSa4b2qy5Q}UE6Ln)?t;RXm$A~mAT3OlEw;gOGSZIQ* zFYoslS;`=Ek-@W|wPIYiuYxg#n2*Mw%UnD~B5{fq3QG)CyM(+MF7qYNmM%*>>5!)z zQWN+n7=1z}{&z4@kO9u0FHe;u&jF*lig|wqn@CexsRi&&$jdlIOFa?gz{JP&x2xWI zbGVoClZqfx42rnPVt9pM^m%!I=X<~@c5sq->H(B?N*rtkUm?lxj)jIdXszeM-BpHn zHj=}o%r{6fipYl0^FcSJaD#tbGdLSQ)cYEYLo3}(nm#0P81WR3zD9sWN~24VaRo6<{2Z`19vreBxKeDQJSjN7H$-Ze$hB0%K8$t*?VI!igt% z=j-L~{?kI=-H`BlCqXE^D*g=^B<)mMe{I^U?Bz6QSN6*sDN6ZR{ceK8?j@q_^kpk(FbeS z+6BgK(&NO8#^wNqvlKoy?fi&S_$20d*&`@z@5K{{Lrl3E`bxlfSWvOp+42wuy;!uA z5oCg~w3<+BBN)$Oaz;K5cH{ImTq3rBU*3r|0wxC$SjJqt#EE0ez zefh-kq-N^Na?AgiC?gy8R4{JQcp!{19|YrS!ea;SdyYIK!bNYE-!~jAG~? z_!71e{~D(l=zV34#~9(1yo7Uj?X`TCppr+BoFK`LPr!{-(s~d{9(&}NSbd$0UOnaZ z4F-##KCY))4?)6cs=lo79RcGe6k6ThmQSL-^3*t_q$Ce=TfpQ3O%J=MnZ%WLPpLRd zZRaA1=!>N9b0lf7P}^3=r)A74GI;lpu_v$b4E>m?u1Mi4f9w{IBE{*z&Q%R|15@W3 ztIM-`eDahn6O_f&vhlqJMpKk_+2A?Z#IP`MbjSxQeTsYpjAv+Z+xE|g77e~in+n#3 zpO1LgA=Sy*+YE1Pjy&%a??g1HuwK@%nmcPe7&~Raj__@elZ|}1G98Rtp2sOjMA!i1 z!LL-i#v4TE??WhPG{_XBM<}-k$V9ZnD)^`$&^HHi&*Bw&2bb;8A@YpY2aR!X` zMNnGe_ceG$oG(<`7Y~UFOlpCppS8ju7^pnB#!mNuhhv#935<>^YOC7MD-QJqJ zpr}*w8Uip<`jn1GvwOhA!SRy8GG7f1XSLrq1dOVN)Uj4U!ZoMv4xV$))dP5(bjNEF z1Y~RZ7)-k>Vqon;X+rYi!9uIa+6Y!!@>eiUm6YtX+YR|VK;(h(;XACHiGLih+lf2~ zzsuU|`W;o@0!G_o(cp$Z>UEEiVwcQ}GT(gNiF^yCo9+`g!wK!E-@I?1QwXW~**>S3 zIBUNXc?hL;?00X9e0cIb7{jfK=ibppFerno`F4>L`8L96)B!vOaiZTwG+KqE^0ZJu zKM4sH!r`3niTl1Geue2s!D|RG*`^B|zxOa$Tf1a6{-ykzG6AOJsf-5?U@F9g!s{kT z7;x1_?EMz(erNAn2#jM7dW?G{`3;iz3Le{e%v;*tPQhaca$qA=5%bJjYWUtxhxpi= z4Osh;m@~_IO90ihyFoUK_NZ zU9vCAI)M~FEWjg1Y_@kF#)}%T(E9{y6C?vv``9BIk2yI&?+;(X+nthnW>?>Fy@{)Lvq3QW zg}R%W8;?6V$1z=XPsrNBu?CyaG%&_lc_^|2jC%l98KO_suR`pk-yHUpQ+NXPS3=6Y zzOMST^C%em@Trd?JSUx;ub|nSlTIPf`tqd25ooXr-$#BeePN~Hi`3y?J2@v&W(lNp z7+B?q-#QA$eF0Yt+#oBQa>v8BX??&LJMiEL_tE)acvgUa4Ez&Pa+jml4Zjhs)j8W5 z55`EQ>R7A6xSo->22l=7oRgRBcHfF~Lc1$|&)?3>Z?MV&-*G+UeuHJR2uW&&ODVpn zJOI{PGHQG;UBDe)iwFampyb}INRE~w)(@ePU-5erz=lhiQ%FvhGW}1xI|eUwb$^su zQrcKkz+}f@-u4^VH83t`DLLqj1YgC>7tT02KfuQSK+1gtH+jrMXoC=w5Ob=ZfFuDGf|sjDLRs#*@FW{cfJ)c>yu5 z(WTHFqv&KXt~OQF+;qt){u#zIE@Q5plAqCYUnE6+^RFvT;RQ@a%2jz)fz^Ug{Lxh> z@)y);Ud%etzo1SklK7Wr*-_R@Nb%5WIG#i3yU8{2!7wN0E|1@waLviNh^6|?HK&l+ z{x7GPxc4t7@>g_ZmB?#$%mPlQy}`KQ;juUzXf7Co20R`gnjZk;)~ucbSRMa%hln6& z%?4xqRL^?7Tfo$pa=zb?V%R7>VRimTB-m>4uq_9Siw!-%MYn*Zf?*`bu)j7iOn#|# z884!digThbd7{c(G)!mrB~P^Xc8_T!I=L4ivpuF0bs68lU4<+?)PR{}I)#^EwnL_g z#meedi!!DWWC`xn-bG*&oxPV~&)<+l8Jc!jm|NVt4atd4$wkb**KNA!M_73tsEYCY z^A`|d_Ebb$8Vgx}%HbCI6@`z;>?yiTYXgOAlrzRkpKwS6TSy+=LC z@>FsP|AvqDsO%I2t%dlNv2EF_Q-tSzzCutoB0Fw+INm^25w&zb$E`33aQ!@qf3t{G zPZY)p3x4`0SUWI`bfMq8ROQh^GhnsBFNURg)g9NH2!^$f!Fcb&uaw2RGmh&;BzcKb z&*r_qf#HM*#qX#trc=*@TCM=gM=7tNe#;R4mY>Ox-v)W3t7R82$*ISQYum4gP0AmK=G z(Rn<=hhI(G14M$(hhS=q-bweCPB>E1F4M~)$-$3eO_rp8pu??Ka^3Zc7q%2d`@w|e|2bsK@ zYv`(sjn*0r#?hc%thB|N;c)}|0ZnifsP zCE!~7{nivP4hV*XnlFI0303?SOwErSgJ-4nnz>!@1Ud|iTLc=t1J81Fp1vdfX?dDC zSl77p-YSs*Ho<3qZ-C*Oar`=$y7(QyVn^&7WhON@?I?U9vmR2eXTvFI3r&xL%K3@g zNy8Rm7;H*7*9KcSg;mkPT1a_{l*fG^>oV$bpLt75C#M?93~MPrI@ew?J{d2FZPar1S=F*(j`o}C9QYBBz(bRyk=`mjCz^d z3#lF`6}qbW9)?7%sn8qJgIc1VGW*Je>oM~=fV?|^L}CPY^r>LHSRumxgRe8dv_0^x z<~0Jz$(C97AF-Pd|gQUCaRVY1&@9Fn0V-*~E3$z($CG!nq5kD3{uaCmh$Oy^V&^?N2bn1^Z8#=Q_nw9e?##b-= z{{--erIgM9@`uR5%_hMQk!`6;|4XDX3h;+N*P%3Zoj`RkN6Hz*a`-b}^XrifF4Xet zk=9*|KU8!H{;=FK{NWFg^5rD>S#CIE8k^r)vFoq;4B@ zIU@NBv?rC?tTTw@FKYgu(1*NNbh(`xcj@v(He9IXyMY|wUi{(D{}c6mGJ_qz&Qkcf z9@)Tt{2?B|AN~+|BErAzRQ28lHpia}_`{#;k@YSrsUQ{nUGv}BKRdpJKP>nM{_y92 zf~@x^{;kM%lx2pqx2btc9&^g;?Ca5jyqcC1 zS*?cVL?&x$PGs^H&52Cb()@blIO^*BdiY;Of#QE*4gr0vk*@HckXem&{(nO%+EkY# zGTBUXB9pgjZ0^BRIsP<Mp1|#B9mS57wZLRSPHuFFO5ug$6v%4Eho~0`T)7+?gFxbp*o+) znQYaC7?eu%6;Msp&U%y|4CT~tG{I)lh$oW^*K2^8YzdZZ|*^C#;3>yZj3Le70C z6UYu{Xv_uTKV!CXY$nM5g%!aUY5DcYh8IIlTnc27WxCw;NKaXziT97Itb*q(ZL(InupaemydOpO$OPyFtL!jCTU+=X#{*9WDQ# zAd4Q><^LO&Kv8B5gxfI=lWge{{f_v;@OO< zXQDCIFAIdoz`aS8uK+B^z=GSWG8ryzs$<;3)DMvrag9;iFHYa;=Rcu}3OXMLU-ff6 zG9L#`H2@sl)DO|=I^Oit1&D0e)|^Pb0!Yzg z_={7wmgHYZg`b3+<)6~{v@Z8=%%gyFu|XFgGPz0PW-TW&`I6>D*4qJOhr596@Kv2p zqek9a`i8@e4L@gWd0R!TIg?`&rVpx1XAt;R;2&cL<0XAwKdiw z!4HuY8fbZA&710cB1hB=$PQZpnbjJ9RRsos^pRdbI%_{>{tNRc8;HNyz)&C?9Ht8r zS#hM6j{>s6aX>0ILGwvK{`@Co{VB+2kyKsoI+XQyoj}FUX(-5wIY4$a3rNLsf%p$M zM&SiO=08B;{{=a)dAi&JU9J@M`Y#p8+${Y+?A?1@P5J*X@b<1Kgk0MQF;NJiQA`L? zlIx(DhL|Yi7N(HIL?MK{3n4@ygj5K*7IKe5u7g5Mu7$>Vy|U)}+rM+>%=!M#`902G z*2BYVf1aPu`mE3TthM)E?OLTYWO4&5ksQD(#hEOxR?1916#U0y8-ry11|>Hl*-tEz z%fFKI&xV_k+~aLZ115dDlJSabC+id7yvjJBv`bdnFmmf}p7&nbBx$#&UFJ(Kkp6~ByRzc&@nRs0qb+YQor`1OI#Gb9)7WJk{x z|GQ+>OZ?Xp*$&gkvJH|m(v8#kze%?1QC^SZ&j*BF$_oFK98hni9g}>8p@jFH&(P%c5j7lI_+i^-Mn8pojhE9>yvSHY*L7+|zi)6Oin1r&7=4Np=v) z3yzaWK8wyH*)AK&v;I1=I`VfUKgan4$@M-V*{+OJSPc)VaK4q~Lu35MYw{LIZlEKQ zCzm6V19L&vME;EAYyA|ZelC(%JgbrTrxT6;xc)XIH=Kmz$$A*c0R^2^9$Z6m4{jsz zPv<`VW5Z`icJKkoDJ)^7lI%baqp6Fm&r$u?GwAOvz4Q^K|;*os% z>{0R{k|)~dG!16fM@^GKF2D4AUW^G~>_z$GL%a1F_Y$vwWIl$qRc zE|PniuhidH@)45#{jQXsBH7<_rTkJUzgF^nU**9^CI3WnVX~tyN}0(rr-cJhBRMdA zBsXBlvc&kR+XV4h+*E01j^xByBe^iSS_dUNBDs2JrQAg+cUQ6(lAG;~PIW(aTRsCmv=Y;z!jz{`74qOlPk_avg1G`_ow{p(7#gbZPj(<7h8Nd&W){B z)`>=P(N6ZUR&geGW&@IaZbGtZJO1NN??7_fdx%QX_bFv2*E^t;kwN8fP=UWou8@j) z4kS(K;IPu3$qgJ;>W?YTWc%YvW*|9PXO+xGa>?dr1Z;Rw0cJgv3y}0e#hDz)bET}E z9KcJZ{VS!O$@axa*1uJp$r&m|vj2}fEPU`uX~5(Ze?hXL9#6J^GuhryY5(tL5F2uX zMo12%Dv~3wu5`qt8zZ?VwUBIYqSR|A>+37+8zA`&7i*>54#|EyAh|HnuRQIfHY)!b z-A<`sa)X_e?5yMuNbaG7QtpoA!ld_B+zH7I^-=1zlLPOkIFkb&Xn>QD8yuumFj+A~ zDKpvOFeEqh6O!#clzQ#t20WGe(Mr8`vY%fR|Bu9b4m|qgY)c(RO$uE3yWB#yamZcJK0a1;!Ks~Ps1z1i)r}4ibL71)G>MSaa1Y) ze@MPz|MzZ?r{Oha2ma6bb^HIg0%tEr=}8g+k1;-zwZ@ia(kbUBwcKbZ(P-o?58@C zXKyVe+i8$onCz#%QbzVEuOM(k4V4NeJ8Gnqne4EsQvSPCiEmeRbD7f>rybi`D1BHe z>oB>ER*G98+0I(2|EJ`79h7!V?!@;>+9EktJEa~O#2+@&@dxngewkA7|B~Fma%Dp+ zl#EbvrIM?VTvi$4pmK%P3NTrYRC0}yqGS}3?`Rv8@X?GCG z8A?;iho!Wex^nrMbwsH;hU5^ABe^iS$umf9DpRS?Qp)F*@&zQ%uvTNOn{i$*=wDAh|Hr66lYoDtf{nP5;`wiBiqvYUWB= zJK246#kG^0vrt?+>6UO#Xlo>g*$&C4c9%-ZgmzUbdLX$%Pb3#6%Z^B6Xd{y3E~677bI6y<2+&NBRTR4Ja7J|q*sKq zLnEbLJ2@a@#hGkh1IdAC6lcPNbm}P$>MIrhmE=GhD(g2wa>GrPezcQ2Xs$Su18P=Y z#_{I^Hn31CER}LgrQAwMD7R`P+R26;*#K#am zGeRkQA~_SkAo)r@9m)P@Ah86MKOoEmaEfLjxkBLA2Ia{O%vb6cD!Ew6Wk|MIMjp6eYz^25hWWql@h=z>!IyVT|L z?-Cktif<_m|1P-)w^7fL=PT{BlLL6DxOQ>?PZa;VG-QVbXi!mN`rs;pJM;!cuJ-}S zem^6*{W2sMCSA?&*I!XnT9oJRL zOrDF46*p6y$sPC($#b!V;!LjJ63HXf70H1)DD_M}>70ykIM{)UQo-ccbfc8=e`j() zp6Hi38p-W?1t~BV$*CEylqVp$FuB4+B(I|amGWGrp2>Rl!#xZ}vY!wnzq($JPx;X!I&MFO=e1W@!WP>Y6?qLp+Xa6lE z7bg8KlI`v*`B3pENG?qF_Y%ngy;ADmDCM_EZeNEF%AaZcNQTS*LvlcBtj`VVD*Z5d z=qe~>ChIFAExBh^6fi<^YHK37LLDVFNVSyxrhGo|9g3U)3nUjNo3~^$q!p6St`10U z#|HTw(jCcmKPfp9$#!FrY&QlwS2nplhpu@(;vwdW+@G4Be{XO zNG?q7-vXunH^u**WIv10o&#B;U>fz?Ph6eI@{t$2*$o0Qz5_;w@*v`Z=P zMRL7;N*+M6zZ9kZu#(4=`ivkvUJb4Tu|~5R#nzxa$MDv{%Rt*gSD0Ve@eEitF-&OEPn$)J^MF9 za-zPgrkoOZP`;sNN(W3fY_2qHp*WK}+8W7;Y>(tyaTg@pcSUm1PVQhY#hDzalTvo7 z#%HhstmuOZcI2Wo_`Bqh8jN~=CFgn~AqnUWDmZf_-$OAsFrRwW zkIpC^{X5C_S!mC^psc5z99TA-c~QwrN;{^CC(~s-Fp}^e@dC}_V-)%N_?-2~7fKc( zxiC2quaxrNCD(ta)HB)7d!?+M?59L=WXhu-)yCg$>QP>mzP`U>hW@t9sS9Q$7OnjH zf}#cGb3gxUyKgu3a2^ts$cOrk;Pgxre9CKi$lO>hk(;H}!nGsps2GJv!fR z>iKq4&$pX;biUox^X;adf4Z56R}HM^)y21)dcNJ%^X;Y{-rU0vjKAH~^H2BW@O9%K z>-p*1KQ-VB&bOO-zTMRG?WUgpd@~Q9SiHH1&yR06^?bXj#~7c-^Ltv~ZtD4VQ_p|z zJvV%%(Yjga>$l6afc4r9{>$ZWH}zo3zTMP=N%$T=ufR|EzunaH?WUe@H}!nGsps2G zJv#q%lMk==zTMQrn|t`C(Um_}s3gBC>ZX+cE?Mt@`foS&U?TqMW*$_R*K@+Y-PH5# zrk-y%_3$R2Z#VVe<-h^AQSe8#zunaH?WUe@H}!nGsYkixMf+wSUZ8%vsi%VSHx((t-V zl-ZFVw@-U;D%Rlj+EE?+&n=1G@!OgJp?sRiZznl+Sn-4S_zr|Z; z$A~8rj6c3e{p37$|BjROe7~d>pZ9P*K5G5L>tFwD#h=yqi~G(08X8is{gkm48W=jR zt8q5Se4Itkr&D&#$ZapHXP+AFx@vFaj2k@)nm0Rse39mljfd51GZWQ|iaIrKS!aIu z`S(SJ8$X`vvPrC>)FypjZHegJ?c$Aa&(*tXPHJI-%Z}CkVfgvH&_VMZ|48oKeRgQehqI@;p-%eHflYAtL) zl<5B8)hWHQ(1cnBWsy@K{=BF*{{H8`CeucHc!M0wEJDBE+I8bHhs!OS^t;!>;E3*r zj$6GtjF>sY=_i+gy^b_~H?=~tru~~UE2k`}wKRI>)qd^uSIX=v*ssUT-cv#NFhTo1O3|nt5-ak z=J(Hg6SE+1!iXC6TRhslr9k&5!@-a5+7HQ_GcI^rKf4m62^KG_ zKbkcmVU+8bH%-ibcbWKZwYT_g!gJ}9@5xR--FIAaa@5DPgtk{I)$3GaSA_M}HEqVM zomt}cajI?2Y84Hx|FN}g4YSzzS|1l?8MN1lG+Lk=a-_a#v%wKtwdS|0Sf0QW!y*>b z>v&YpMdcu2d4OC(Q68Q;Jw%fX2;B?_Oa?fL-7SFa7C_7`KyN7|6cAi*1DqxLHXv#% zpp4)m&UXM#ae%}-09Pp`ln^}fF-!fmXDQ{!_9F~`f0%jReXEYk@kR3<&h1(8NyGJy z!^d?j2w4^8d3kPj+uv7Q(6<_qvwlGGvkvJ)Cb_N491weQW$(o=UstFkG28GYx%1(e zfrsnHyJqKVm~87jx^>;|oBFiB+-b$9dq337ZRX#5z}VqU&-kr-wY0&3pLS>bQt{%h zjqm23c#&~r*<;K28iV;4P5yet8K^zFGifO&4@6$^>C(BSr`M@<-piX!J9f@A|GV{3 z8xEhziQ96o-IwDFy7bg+o;Rt_4UhVVEgXKoS}Qp`|LN04 z7VE|>oFksCN2p!#SGNB(yusSTi?Mwcc-*l@i#{`^?0GQnvRm^fQx5i7m+dv(!&1F? z@Ua%BHAC<8-ttBBqmJqCld?a#4Bm69MVPBq?d?4Z2Bm&~b&9Q&VR(I?J$1PKTjzVp zx@C=DSg11J4v6Twcf8-_FV_0CuZ@Z5UUl?7r=nKT?%&;Xeb_y5>g_3pk3)|KpVY*) z>UVs;YEP@$TEiP60XuQ9Q{(Z%I=*G|UF}OuZXPJ?U8&BD0N*!8Kh$pfuybWi++O$6 zvv71eamu< zUciaQjk|4p&}((trfH!Q{HFCaeh|O?-A~sS-5s3s!tLuH&HDN-_17P}<0$n4*Ns?s zJM-cktDJn7y}8Py#Labgdo=6+_<4;6CI#D0 zpM8*P^Zs1xxgUQtF?zY#ByIduUrl=4Z~7jMtUvZHZgW&?_C|Yr@D%KF4c38>^17j{YGlfvX0@R16_Q6t}t>*($a12KAmSJk2LBxdf0^0 zYqeGz-kbBw@r4PEUwOquH|u=kVQ}5olg>5S^?IGjq^mm@TIo17{=8Ey4R_)0>bl)S z=d~Mo-skwe_LZYO7oW~}?={fMF!HSFwH5olYxo!){y6w&|u$)W=)0cd0c&C;x0wt4Ywp zMS&3<|a$)ZV$u!CBVtPaE&MS?Ng5t%&25 zhRs=eZ=#9o1G7L)&jC)0>wdB5wD{&m-8QNkhv!wgDS?SNMZaIV_Tq%;na`ir@3Fyg znVn(&q&*pj$2r^>>M}od`_c0)D|D=R`FQoJRccj#v)vxSh9DHqlpZ6vu zy6Sm%Dh==5;98|YqYsYko%-{Gabp{d=pn6kWA2VO7^mld@|4HGpCeA_by-yJ^>4jh zJNCPn*~C}fFsktyvo{@obf4LB#LMJ@nGbAsNB@*-tv_m)XRCPqw9H{`qwapt8r?YU z(Oozg_U@S3`qr}^lx8*B;^_1s+H;ZLF7>$F$XZRS6gZ^6pX0kim0+oF0LsiXm6@@jXO1y zZZAHv|Mddh>79K0B*&i^NHC~y}vtixjVQ)j78fFCEI{eXj$K>Vb-PJ0~ z`kx2(SzfDN>pRZJ#C|WfWO@M4DPJAA)fQVS*^4dNTVvakb!4$MBt8jJM4751o!UX% z_CaFWVKjc)qX~XtTkrJ43%RDT4R)++exXt0W9N(;}?_-KPVffugD0j};+lQ0pti|k0+kG6nrq%4yIM`!-jh-tP zc6)VXog3O zv`07n`1Emcm&0%QeQssz_-90Dy#eVxmm9t}?{<36+Q)IjyVZO-<#M&jKWT=<3_5P3 zD)`>8Ps`rb@6=DpH&1xorReYlKS|t=;ZyVnr|m$`hmxP!CS@P5@EUQ%@K z^z#*aG+1Q0#<)q<6&cqz>2H~~=-iWPmkzD=aj9=o_cxPW_ie{$Y}!vWGXH(=2)(yH zSM3&$9UPVr^nOG1)jB|E$Mc_reLe~dtrd~@GM8YuGFT?Cm_3H z!SiL8*7^*avZTt%8QXuzX){Tj_FVt&#iN&f9t_CSw0JnCaoEmL_q*NtExdtk&d#NN z8#nbVotb%EjFK_D$yZ$$-L9rC{cJy|$%Wj&YKe6s-db#^_VV46Bd?~GynOj4eZcu? zcifu9tQ}Xad-u3o4tbrwGrbVo@5hEM8+HzFbwX?QW@``cN#)|W=Cj|u8lL}ib)%3L z8+QF%E%?lu>T4s0k9elryh@kHvulhBZtSe-Su<@>%)URHOqel!bY#$vmG0ka95rb8 zq<$kLox>YZ`(Xq31@B)KoH%lKZPbF*)n|Au-rPDMegC%Qe#gygSBTp=q0idwW7{3; z_0#i&s#R_tOvqRq^?6WW;}v0t`*i%gPHT99+QaMh%J)t0oIgx8i(3tl3!c{Po*#bd zcc^7oOGA%)lZzjB{qxQd&E)>Mn)Hf;TDBbacJ2I_x}y)RJASO0VdJYie{Q}%OneVw zcqR% z%eB*@!aQtGb$?dcaCxo50{%70zphBW{t(^QG3vRGOgQ(bR!F@)FE7=2wlQdnbj3z-EqnHTEE)#7(Gpz6L z&+|R5rZ@RKdHaa> zegpk&Z!T+cA?jIWt>G=u9^N09@C#Xg?c!eN#N5u77j&;)J7lz?+p9k+)OXoxYIdOh z-Rl#t)atNcvc|$@p2fSN7w4Saw>`M)wxp8}kKH@#x1nn4cM_P23npFr=zi<=9Xvd~ z`t7)px+x|T-c{^8X+gtP{pV&kS@v-4vCCVWT8%bKbiO=1x8cSl|G{xXd)RzR?jBgU zD(2nnSw~~Eh8H5mrFwnKFN(xAO>N}*Z1(=cOQ&?d((zv4>Ps%(r9bp=_|2v7)}GGI zzYlqu(|@_f?}YEdF*EN>3|uqQ?68AxM6D$^mX13gV3@b_?oiD#{#nkyPMk$La$+*x z0}9Zum`}mGLKr@um;#A91j(T+(UB%oAx?)OAyXmYI&zg#Ldo8MFRPY|W+N`@;*S6V zHv%Fgi{O?Hu$hK+R_Vz6X;{bLDC99EQb*ePK~gD^eh|@-hZOH)5XUrh6D?^m=*HwY zz?2(WCq5f^^+>Sd=P5CG7n_cC{4*di)6vUDywiPD4;70OkfM)h6N@&VAi0#7PmnD- z@{AIC65{$95~m|;KSOL!LEf>S?K;wZ26`@_#La*t=*VkI)M?0&nUI}2vUw)N=?p~w zS4g6c4EhyPLP@6V(Gj&jB>pVK%O8@YBl{?BnULB6ko}NCY~3IW;!8=!wL$>eq*6?0 zK~i*N{49v~IY>4{l_oyNI6LP7=CgJCs&MRP>kgBw^SF`@&&6J)>&Us;SjYbYnpn+2 zFUNFb)*OgMHhXCT$IXY-*;H+B(_Zny$Ff9glRu5g_p1;+e-k~%Yd^IeHlmU;S{cg}Y|KiXZD~gz{pGWi44V)_MeY)w~{)H8%40$;+x685%T2Jt& z+M}CdJ^k~z&pDOc?$v4c-KLzI3#ZKVj(PfgclF#cjXYb_j-9@t%|6{0$24u)*J~`E z9U}9Z9$s=uZLa%xcI^0e8#nFVE%_W?*xJn{oo&ts>w3C)+1@|uwiU9^R5)m)1Xe zz3=h2AyK0oThF?+?sIRJ*;3`=B+cXCqa9{^{PFS?(~1>O zOsHvkzW>|Q5tU~o4U_QO{7_8~XWw(l+r_i*4h~b`4IHK-?JX^l0p32Q7GH@#z-HRfT0#{5Lu{c3(r2c9@i{&8pj z8}|m?m7+WNw6Vd&q)or)4sN{k+4&!5#~RN6)xXA~y*kU@Sl@isxv;{r0QacsZ^}*u z#C>U5cFKSF?gM*|6&#f+UI>AA9)P#b7T9Ji2HH(EXKCCOx#GaHM;*LIda$AKacM09@yT{RCe_pcIwgkF(2Y1ov&@nZ)aFvQ(@5c zKNgMbbfQ&V``{77vxnHMki`?nRh)eB^2xn*md8J-v8CS}>k;M?#%NOB-8n0Ix)j9d|uzHs>P#vRvj^@`Sk$} z8Xrl&bjj$*=!gAHhw1AaTWnvPXLmWSN7M~ka|C* z>zmJ>Z?);=$O$G1k+qy2&fGgGXvqMb>{n~gBrJ)sG%_AsdsNa~&D1026({~sZN%vH zBUUbUcv9!a(XJ0J#(q5SFTM{kycLOEcilL+{Ak_O89!}mHg)=dzIC7M__O%)9rNS1 z{u?S6EnIxDS>4Pmk1ju}J}YuH9C^341ruE_t-*a_F%tF>NMZ{^czne!?hveX)$3XlI<^v zsMro5JW=JlMX)7KS!*wE<6N0;JJuB)3}?xFpS0$r6{&A?MG^mjZ7!%yHzsIQV+ zCm^|7a5T~b*GK!H(`kaB3P!i8T zs;T57CB6XSaTa2%k{xFuZiNt|Oh`?Y49|oZJcFcDYOADD79^G8n+2(>l0y{l=Md9# z5RFR4pM#jZfMiqZtEAp}NG2ulJj7Hb=P3R~5UUH2Mk<+g0b=nIl20*HNsDYq?n_*P zEX`K;RX4?KP(oj!jr~QmF~@9NgxLN8DWWu2NvBJY0?Lpp5KEP8z5aq@D4H~2V$d= z6O>enIo>SP9kHi3Al~mGRyQGb*wdR3lMj%5N@wh8E+mukm=|02DruJo@h^cSn_9rBc$X~YTtwOzzFX_LO()IQyekE`w-htkQw(Oy)i;c0mb|Q#91ZN z9zdc#LvkoCI1eAHhpPID|L_I9hKo{p9%R|KZDaxrRCq6}an3SR{-YS4c0l-}n3IHZ*fKee}gt!+1G6}Vx z0X)R>8Ngo`aGEeujGqH6^Z+xS13V>zkV`Os0T?4wUjRb&0XYOOnQDiNTH6W)e2k_V zCs&HlqJUuY5-ofr_$45!qAor_HpO-D1hIYvaN@2+zCsIMc|a&3IQ{{cED?VI;tc`s z2vfzO7~ob35LXQFlVXBFWx$ZvfEg0|8jwoRe*^ec2EGA!R{51ucs)^!h6vM?>g>nhX zGD8JeE*XUQS^#r3AVQ|90dBPcIfPYWrVB8r1IWk6ma0ez)&ryx9`kZb#9ANVT^A6k z4~Uir1QQd0V+FuEiKqa`B)lWUh(kqyzXlLj5wKB;2^RGLLks}15^Dg+CFmOhw#Yz3 zKxlnHG9gY>l>oL40A7^<+a-xmK&V|AkRYCw0a2!a(}bO3Tm|6N5a-JDDmYgXC4*4X z5G~A&&|;5FH3Gyp0^|^q#H=d7tuY{^Dqz1{AsCneY^nj0CAbu1N42i7?$R+640-ThA zwE&^b0Lg^YqN)wBZ4U6_u|F$GgaShCI)E(ktOJO$0GuYA7vs7BCriMLx`1rSAe0cy zO#qiz}pHC zSs#!m4+ti$0gep-w{dRh?@2MiqU~SL@&{ibw=GuDZ-iAI z$-qWv5o!%cCOj5ZV}NZtfLCL{Q%NEe5NewN3dPe55Y--Vn($nVn*f|T0A}FJV^xu8 zngU7)flUFgB#RJlgK@O_4g)KefbRfq-=jtTcWCiOEX@H19RcCyfOnEdNF~@e1ALIM zW&m$nKoOx-?3x2i>;N&%0iUFhkVz=pgV&NWTn+5SYl(j+Na9{R(Z7g;1)5lN2EsOUpUR&Vn*IddFONg#oHd{hMe}L$>fat4bPz#8yJ%<13ukW_0E2>4^5?o-9hF&ev z&`>S=C{bM?wOc_dt7W_u#HlMJn?Jf@q?UU8VU`j~PHRXtH9xI#`TA*9d^d1Des)4_ ztd*{*|I;2e!XAE)h8Dfh(Cd42W`;eaIYxXb-TK2z!8AU%)#;dvWLjFmMILbphB&F(H*Oq${AK#C8RE_XFs6 z1K7#HZUB@1fMh~vQ8@rI30@8Wdr2br4*=Be4(KYL-2oN@0jCKLV%!6eJ5aZ(%glxhX z(ewv|dH@3Z1H2@QVEZ$`Y5-uI1PlNa5b_B=VmS~HH4+d$5HLaV2u`B__JaVv5;jP8 zm}-(dU`!Uf!H6jm!I&zAjA`QVBf?Lj8PlbhF+-e(AZAJ|<5wwV_{+edhyY1o%o5cw z#B6bA%#kEUpcuL#=87jHNYWVd#Mm7XEIy3+lEGLYn&F7wWGW*>vKR}+Yy@JF1TaG7 z3L{J`e?lylV8#;3V=NVG4@9_xF_y^##&WUy8L>hl7!gv)SSb!85vwGcv0925k>WfG zu|{GUBBhKd8R&_KmITIHQH@5d6L-dXNn*r^;TXgQ@nme2G{z<|{sj>$K8($h!Pp`i zFT_@v%7~LJ#x^kgQTZYci#y|pBr(#( za1!FEcruPj8bT}rF#yxa7(j;jOa|l<((wr=t{4n|1BA{6`2GesEolVXAg&SuI4eFO zfC54`Axkt10a5b+feQiWC5zw`46s@R$d-UbfD%GJ;gVQ}0^;Wb!b1U9B#+>>0AL>m zxF%s?0E6EEMT8u&TMS4g#4HBfltO}c2*7m-AWx!~08ADF$_Tf`c_|>1khm0(FQo+k zMF5X*z&%L_2UvsxjFtf&i2E`?E+L)pNDP+)Lc;*Q%K?uijbOVNV7dbERD4zd3JBSR zLeWG3qLu&xBLL4Oi{P{rV6_rZBmpY{C4_u{`W4>&R^h!NJ{%Ih3R0|=TNJlt5c}1T zH)>hD8e*^j6<~0DV{lo9HS^JYLMA#pRnR7wf{ zF#wM(fJTzA1z@oOV6+urChl7SxrB5=Q!$JKgl+`*#sSPFjbOV8V7d*^Tzs|x3JBQ* zOVMlxM8yIEw*y*A7QtyVz$zYKB?0k(5<)(qjaViC;l1FgcgRT6KjI9io zuw;P2UbHAmMvEb0cMyA{SY-jiB_In>LdYj97t3>i_)~!JbASlRBe%{pYAd`@I5fCG#1ph36$0fi#_5#54Dqy?#Tm=*mvIz;Ixdw>J1_WLM z?365m(?x*QbwHv7TnCg8@(FvyG6xWU2@swGNRm8)+hu_L4Zwa0y8$q`0w^LRi``8? zDk0`3AVmrZ-d6#xxqvi@&c(&WAt`1Y7Uw*~5s77_ODW^147`OnCJBt=qPmU95O>B2 zNn)H7!#jvm;>kEIX^b;soR2swK8#GsU}TBrF5;X_Wt^8R#sx9Ehsc%y#zncpxFnYM z5tk*HaYgbNSH=1P;+ljZ#NZBh^Z|AwN9-QLZ%72=rW7)A#o-YmPof#Oq?mDAoPXCH zroN*qJJ#U?)qIIXQFT{J8TVx1W5j()U_21j6U0MtXFQT5#_wYI6!BO*8BZjQ@l=cp z5C!7HD3lDwGtm?xp378(IQ@>X<`iPAMPl|0P(lcK26!b`2=R~EVh6s+E!LGLJMl%X z+Y?CsPJ9~h=Icwe!Bar^bG*yH)0JD#@h+cAv3~*ipeyZCv66QIBs&4!lo9T1a3 zh)oLQldddgn@mbkDx?hZg4YkvDD3Bpu5>Em^}}<#?5r=s%dkoci!i?2=V;>k5(Czi z=$C-d7l1N?zBs=E*cJg2UjZsgDWQPi@dv0y#yE)11gJqF~I2+Ae~?&hOYr7 z1mD+yYLZ5X{{vw924E~cZvbw^fNVld(Yys1yaoim1=N--LMp-P9iXlRyaRZ@0pt@j zV)-6m@)i*O9#CKM2$=-?4**jM`+&pNP#!QEiCqbzu|zP;q>#}>97+*QC7SUaJ`_Qi zi}OcBGl^w1mr{m>4E%(!lmtc#QGG_V6n91|Nn%)uVHu*ecrw~Z8l$Zk|B0{`A4WUL zV6+#_7eoh{%CM0v#`lqCdc#y5BLnoXn}6bC;C$R}VJDUHFI9lfl1IoS*sB5d z5~kK07U{2tqt0?yvC~BZ3l$(n7vLa;gj|BF9-xOr>j6U5fHH!kIO_v!bpeU`fZkF{ zC?I%L060rR1wfP@z^Ed?McgX_ob&X5K|T4DTRbwf@?Lv7>TY12sHwf5xm5?I>5FnAh9}NoRks@2p+}& zA4xCVR~DuNc+@ln{Jt0wzluA>J5ZS_?2$d};yQY5=kcexj)j zFsKO#tPPkUS%g%ARUN>u5>N-=T?>#;2oTG<0F&B)@VbE6l1IoS*qZv#gV5H&;Q0ZYW)6yQ`JkWL5}!-jwof^S2>a!Dh^HvpJ60z`;UBY>MJAe*pCG>riU4FQ3T z0g;kLNF`XA0Yn1uoDuIvfP6x0j!feLMFk!DIi9|ngaaI07Zn2V)q@u zq6r}8J3y=y5^@Qy=722{Z4L-+3MeDQiE}f6?RS90W`ONdN+=+BGzTO|LUTZrIl#yQ zuv6SE08Y&S>4Zcvv;>q8d@TWcB#jW?9AMf4kR(1W0B#n5Y{Gugv;-Jf0s>nCk|m3f zO0a4LNRfb60Phxnd_tO7S^-R20>Z5Thb51YNw9AXNSCnI0RL8iBEm7TYXh*b0>rcd zWJn<)m*CnKa8jb%0zz8@$_S^$*&1Nm29RhCI4h-u0)j_7K$ax514Ok27_|qS7x(r6 zCu=}DAzKVP07?kH9RQakjS$}sU}^)nB0e?%xAuT+!Zp!+4>0He2>c$9BUyx0f>lSr zO$q1-@U{Wu6Y|8;7GUx{Alw#kTk;5*1baI`zJ%EU{5t}Q2=~OU6Tre25Yq|pKne-D z1lP`hM-tr`5NZb~BRm%89{{$U0Es^Uo=Pd9fZ$;dD3kwm zC=$c2fD(dlSHLStBgESSOuGS!#itvC&a%yq=@oGPdYh5EP6m<9MQ2# z3LVjLF2S`IKv$xB0YZBM$_V=6+#6u)2uSP=s3@g`0)mGVz)%vL08zaFM$UlB;_eJ^ z>J3OI7>QvYKncOO51^W)5#pTyrY-M^9|75f_M#aEFc<;|90stFEJ7;5$_>y_0^9)J zLjm~&JF#>Jm<$7iy8}8)9wC!pKOA5$VZ#CbZh#^}SFsxbuy6;&i~u-DAt9IG`V*js zME?W`9S$fXIEu3ez;*;6(F4$1N(lu7kDmd~lJGMi>L-BFNPvsDj|4b*0MZGrVmJy= zLhv00=r3u6_@4o$o`8Yk;|XvZ3CJc47R_i}b^Rz)8ABwCF;vXPAcjc*LcBfEIDZTp zyNl&7@Zl277$JF#pTyb=;UQs+pXC8#q}YwcsusUs)tIqZ)l&)yxdhj7fH4w14iM@E zC?j}@vp2wYEFjSvFiuJd1q2TtfR7~j0HVeLjK%{di2HbelQ$rp;46j`03`(934qCx zMu_(Tm`(&t6`zR!xAA~%f}d!70R|HQfxdtll0`@*SWN=_Dgl!K-V*`&gaEOe3^4Hp zgii*{mOMfx!F~!LP{O7F{3ij52ti^u6<{$L5Hl4JEQN$zg6lNE0*Rgm2%Q2bBZP>v zAHa4hAkh!7NJ1dr)}FiDsWh?)j4ngLiM?lS;Ret>jBxERg^ln{Jp0+ve}A%40Z zKK}YuuP-k;eg(MAK#S~O(PEWo`~e0t0fGL2NXa6k608CMA^`z7#8GmE5iORp5Njov zu}<&1FDB1Xa(8{`3Fqu9+sY?25@tQ0afi$fq{ix^`V_|8T#OL5{n7qLxZ8QY~4 zAq8_V0FNLHAVCs>08xS1i7NB36FbFy9>8fXTBOfIi$pOD29ywdg8_RajSwFMFr5!b z5})}1w|RhU!hX>#02l-V0v7<1C5w5l)M97{GQBATbPZ zR!Rv41dqjlEJ;`lhzbQ5EdiVt_ay+QFhDvXTMU;1N(jD70hc6=5Wg5;8VFj)o&Ujeu+d4x=YeFPw1 z!Xg0v%K=4%dt$c|V6g%avl8$?3JJLc*HwT=61@r#8UZLHJQnBGI4hn=EaRz^G74m1 zB%)9f7|%qt2Ju|n5$YHEvi6XAsJcjB1|=b1>dOu!E+~@N>JKp#M6q}>UP~GSpHf6& zs|JFt&WOTR-$@1`m0%tX_#jiG0p3x7973s>tp%7w147mUKFJkACc$PMFC_G3{yJVr ztc5(Le9@P7>v?rI=7a z7_t#yD6ty>Q5yjIn*fz%;3k06MnE#bNK~*_aEu2um56vi=yt$6g1I;(0Bqv{aR~tY`V&F{VaN`E zrNr(4L?r<9cLG|pI}uqLn8JALiYjQ5gf(g0Kj%XAnpL5w-gf!2t$$q&Jvpp zh&lk!KL~JIj$$(^ntEf@{B?PY&Kz~Uh#2*AWZo#K*11reNE&O0F1yVZ|O$Li+ zDw-Ii0!|Z#h;bSql`ta>FibKC-e~~yLjZS~dI(^02#`YjbR6K30q~WC3_uCN=mcQ0xSs&TX8_U(Q^oKk!0iOU_awki(g+48 z0j8$_GsNccINtz7 zs4Vh`$L)CnSks9>6UZ;F|~7FKGmWJb>vf zK(hGU0;CeM2`Qqv4e-7N2)qqQlPrSCZGhDsz+nlv1IQ%g6Vk;pAK-rn5S|Y>CV2#l ze1QF3K!$|f1>_Qn2q*c<)?cc9s#Ggw{XMQ!xh^{Q{juQ|oG z#VQ#vR^LGXOD!Fpnv!u(Z(O-GexYZezo4#;j!}wrZT%5yaeJ?4kuspI{(TkyH~G5e zyqZ|EXDhaf{ixUDEAD-yXCPIc>AA6Ct8z0M?`IX@={r$pm#vOYRcT6T|ZaO-ZFu-ca>fHFT@*R*W4Y93sXKpLWL;tp_PWkB2kiUX&#zdcA z5IW}v$j|rn4sjPMg$zK)sGl%6rFD^B3zgc&J*9PR{Tt=o8I|I9LDb=9#^R57r&t&1 z;hutSZ?V%i$Lr`+=5{}*<8r7Os{>Rtm6-HuT zxk6WRoz82%@|cM}t;ge+BXAE7NaSw)x%vfB>p9%-U)pnI)`N|wLZv(}fIDxI2NXX9VVb7z2zK<(R9BtlP8TG zGocrL%}h0|Gi**XY~?fV6DvRMg1#P&>iAqzzrMa}a~0-3;=Nv*%C>lj-*PiN8GI?M*JVs)YuB!E9l7Wl~1zK zxQUZTV>O-fC(VBxjmqb`t8}WP&(S`U#G#%31MWpitG4=mtJZlPirq$)_AwnBD$Ba) zKQtb1$gw3{L*LfhH zem%uK{iaXznKVu({{S7cT3IFY`k-OsG?h+`@@Ju$&tcSI)6NR?b=a>mPiFtByp#DpB{kthg2jRzwE4w^bmA^;~d9F%n zQ(ND(LizbJ7r)S&2YkZdf1QBkPtvPZ{$l+;ZPhxJBx0m~{mPR9rucHjZLQVvKZtt^0I80xUAw1& zX`F!|0}L8m1{h#)cXzkJ-CY}Zhp=(?AcMQRJHg#85Fi0U!hPQE+J^~&ob#Rk`|s`K znOe1KmCLSOdw0{|R`@-d1zA2Xir%HsGrjwNBj~HfdH=z9v1h7%C6m9W^nXV~D-QmL z^4e|ipB84ar+$t8A;qGfTVVKS@s^Lp6gl~?cz+4{Pc#3Y7WRMFp084A{HPxL|5cB# zWg&}g|7h{wRd{0Cr2jvp`rno1|5dDsZ3Dh&gNb1W+5c{_EIM+>7ukKS!~gqS2lzzs z=@IIyMauE!fR%lnZ~jDoe)4{#a}uNr9!tLGGt$o@v>i%xzV#~%HVKA$MYPTm7X7uDz#?V!;`L&v4Wt&6|o7v;_;hF7We z-SOJ!Vxp6%a{p#@u}y-k_>Ol*7su#She#?ppSVV+ANG!Ibn%Q%KWQB=y!ja$f1~6I z?#O5i6AKGd^+{T@rNdjV`CrB}LcC=yDldGIX7dF1OJo zN7o}7{jX0Rqx>GZkOl;wyy(<&DWI~^6)<)w(N!_Jf<~7LT~(tiWOS*~)ik=oMwbR% z9iuDaOa5xsv{2V5i(;r&NC)+duDFSu9$h=5D`9jQ&~-4nl17&iozv(_8C@W{_DliI zn9}G}xlF2m2Q!_@8NZ4~LiZDGuBkiB=nAX;r%`I|4mZjo$h*;L?v60JqR0(FpOHpa47rI%<8X{Jy5jg- zfadsUbehH`;Jj8FK9h`HN#x7u;;H>7qf}u_K|R_LOyr8lqY2caG27@WA%~ip z%rUyk=(3~JIx^Sj*n>NA8M}GtXiP^{n1W7g%R+P-Z`CyZY8u0(CUSKG>KNTJqpN|g zuF)+wx|--R8QqWQlu0c}VsxvFU2Syz6v$__(bYj->RI4ue_b=x3Uy(bS@qT!T|IO; z(Iv%QYjpLIa~ZpJM%MscZlhaobRp>S8r=q?YltqN(dl2r_EG;gg8W9g$rv_9SJ3D- z8=VGQQKQ>pbWPC}GrFxt*9=`rquXY5&Cyjby6r}%f$FYglsiyrfV70lMz_o8TA{0I zbbCzX*65Oh;F&j9W}a6=t>&hF{A5@u7c4WN2khl0sV7x`keIG(f(cWjMhZubIL^ShCIgT zPMgTx(Tz2_v&OCmx=BWN&e-)t7f2?$eq1niy^wnvyNgEG8(kE)xk&tElzmV}LzfZv zl1Z>H@*UP{eXbZ?Kji!90&%aIxEf2>(Pffrbp4Shnz%Pi+yUq&8=d=>F&v1pFG^i3 zZX4YoF`R;YkJYRM?k99=1q)P|lDOd+P^$b?XlzDx1f!dVt{OUB zKYWdDI&xa#Dj%28xo6->ZibhBcx{>w1BQO-x6WDH}V)1X-ZebI5TaKtip3z0jc)3xF|qg#Yr z51pU#7CCJgy=@ZvQ|Emd?!UW~UC!R4}hJ2XSOzXYB(Je>bhpqu` ze53mjc{{og+yq9q0(mpKhPVlhZYA;tbd7KWjBXYAJ3>)5#!Y0DtC8oT(|VuS=t7Za zp=*kp#OT%_Pe#`aH>uIBMb^Jpq)#%VTZh~VU31*zMzs8-YLMzGxg?1U!7YeRl{*M! zlrWz{CcQ(*Wu;U97d3{5kzFW<;ubTyBgheruDH=1MW=tiPj)4Y?ijKY-EiEJMt2-J zywR01x)bQ`s#JVRqjMAPI0+AovMfsV(mln zp9TFQpguK>?i}(7V^KWZ7 z7Pr39T}Ccu(raLJSI{Y*r~N~W@+#iPEDTz98yej;p7x^W2 znv$)J?jEwP2bz*?jP5@2Z0z)DYjh8g=NMf(qkD+XeTw<1dD`A6A0eMLbGC!gJw|uV z*mX3zC+Hd*yG};;6kQsoqNZeLbZVJrA|mHQxLu9ibL3S7?p6DDGs+hzS1Sge?nd`B za;VqP(ZlFoqFZZp+ES@&U%@&Pw--8zzd!>r-iO=A*u6&9_|Vkt>(SBvZ}8~1*fmf4 z8N;{8AB@gzbibneXmtIJ?l*L=(H+7aV00RkpU|Da9cXm#kyR?`1{vK43@ynsYV$cWm-nCM)EYH(+|RDBoz&^=|<;=E)F_%!wjQ~iB2t} zg3dI$Sjg!~PwCDwx}*nviAA2;D)Z%0-`?)V)S(HSXKAL--8Zq<3j@2Ls9m^0XXR5<$!Gn+hGU13}^K_>zmkD z2djKpZ^{+l+$|$=NzkEcWQYP$AsR%77~lsnAr^cGI&6&taUmY~Lwra82_XOyL1IV( zNg)}euwweT5?0E9sAJeb$OM^TDaE-0H{d4R0v)&R03ES*f*dq!F31geARpw1f>z8E zzRlw|qgno>SwDf^i0}a&uwJ&#obXNWet@Vm(?{?abXNKlbUdo#&=>GCyo6Wq3%rIm z@D{pJg6_}*dO{nr(~)UA&@pKT=m|xZNht*7xP-xERYymB!ST9ccCaBgC zxf67TF3=UaL3ii@J)sx$hCa|2a#SFmJNa-Fcf&M z?-=Pr$L%6;C+I?`3!g4@A&h`V&={IPQ#eeA9)aU<0#3py&?ZU?uNGSEf3&!2f1~}3 z7EtY1v^~+nsde%OXl=X&x5?kLDBi_$4|Mt0<^CZ&g2$jW?kPNj=b*;~dMuC?bn35D z{v41Ka)A!y^Fltz4?0lS0eXK%&Om4cO+cr+t<&(Q4YY@j&>6ZwSJ0_$clbR5g|PCS z@lE2sfOrv(!U;GD>mU;ilo_%>R>%ptKnK1$;B5`*U>S?SBA5+xU^Ezn z3+LcG)S*G^LOrMt4Il&>LL+DlO`sVxhnCQaspw?+2?yaJ0-T03a1L}}d;u=SaJU>- z5o$%TR-N>X>z+X5U5vZkz^fLHeXt*N_tO?IgkZ=4IYEcMxgihag(^@LszG(A0Xpxk z1+_uvygJv_Ic|JN00|)g5`)fiT@Vo>L6jJ@byS3CFp{Pp1*2gMjDtZi7>2-57zTYo z2f%LV1v>cEIqz{e38&x;1Y6lJ_@;IjLDVU$PF0IT3D9Y3DJTtPpe&Sw@}N`IilBp3 z9h~Z*v?jPfhoL(B)M2O2Hv2;^IwLpafxh^!uuz?cz4$jorGYoHDsO_#um!flcGv+s zVHfO%J+K${!G1UZi%IAR3+Pc;fqx~eg2JrQMIbIceV4F%a33DPLs){%Qdp)9(QEqRun(4^(~0hn z;CAz899y;VFcBt!j&{pHdC-yWTXes{J9rNtAUvy+o>BXP3nD@!hzwCc&!F|pIXdVu zvmeBSSnwUhhB#0tE`Q>IKg5RwP=HMHLtfDNac!DW|F~2G(2iWYZEc!I!x*d5PreD8 zX2L8pWC7i3=*}V`asa$Xj*A-){Glg)-L~|GC29~@1}h;H*1$TL2h(6W%z&9z%uBur z-2)H@!XOw7LtrQjgW)g&M#3l<4Pzi1WQP=x5>i2GNC*Lt2x35ZhycFef`||aqJYk% z??q(9UPHJ6H(@M{hY6sA=SiT$<`t}4I!n$6IzP@1LGS}jI0Y=w8M4ldbsRhgbhNtw z^p7|$h9$5Jmct5I37>R?=O7^;a6&i;4g6I6hIIF%`$gS^>WHQ!=m@4X zlmVTx=r&w;*p;9%RE27wQxu(==#-=u)P_1x0184^E>(fJnP3WwhI<-+ro#-F2|B3^yl0q^_4&OrxNC~MRHKc*GkPgyA2GIKlfpD9&^3zfUpb!)eNB>7e zhy;-#D!3`(1ZWQ(pdD0!s!$E8Lk*}2wV*cCfx1u+>O%tvf#OgEio!gK3+3Q9u2JzAh}Us%z%959I;yV%RpAJQ*7ixK?PHO*M+G2kA_pCOPD;(|`Mb(kH()u$mehCQ$VMuE#@y7r^c*xaBXTeucEC>91-s!aan8Xp$mS-Y9FP-o!zv6` zLny3=xabDp>XnCNkQ^G)*DIO7t3U@5I)6xkJ_vM}9UsCGFFZ^klY-diz)b*IAPBNT zI&i1wPZ~%JHOMCkZc_M#!KV{%9cJH#d9VbuxKfXDC@bi{j7w?8_r4TT@cd=3L|E<~h8NzuJQr_;1_$Z6p_hz&a8@rBeo}%ihX>lk4C80EwfwE8uDnnJM3-x@zXTd;d03py28bcFk1+Aftwc`eN9y1Xe zP%b?x)RVkzRB8##f;lh|%2D?6Pz(}757wQw&>UJpQ)mK>pdqxP4sD9@q<0VVWk&beI7%VHV7mOypTG8|J}yaKix55pxuX3IW(A zf)|YKpW!7uf$L0`V(3djS*QS&pb88mY!D0v_YnR#AsmE+~ zfVo6U#vn@$IzZjVc-Rgbp&12QL_aTvB`^^tz;_TE`qB4p=nn&6And1W4#FN-1#4g- zI3b++|1QmVA9NUc6c!>6f!@#(Vt^mSgjnz^tuPR~M$j0VLMs?a;e4Snh2BafO416Y zAU<*e_(%mDgiWE67EFcBde*IXRP;{DZqUC9R|5u7^0kz3I2q=LLXZZ2C-aYxkOcLL z$v)T*2cRkO^qOc!3ZWM<3*aqF8r`wiOLlsHt`q3}IK2-S72HSYhyFwy2uF#agYxVo zRsafu-b;JO2}3bh$=u$DKR0eZxI-rQK(CPLjj;=G5q80D*aIy{uN4%6qEHNqLkTEJ z{*E#Tm+8Bjl)4`1Ew1Ebasb_E3Y864FH=o{<+REP$O*~7P3=lyUmD6mKF9+(p)PSl zKu-d6ps&|Ydf-OK)!QWZsr}=K6zK`Vb=U>F;Q$fH&w zD{;awpT?R5g@B@mZB4hD)6<&wXem3vM$}gCJZvJN!(^&=9rS8LYN(8VJnY536chlx zx=;kB!wi@O^Pw+vhI*j42P#4(ND2cfT=VG6q$YGp5aw&%=;Ol|C z9?-XfG>{fjKq{yMiXRp9%w5mepMjpM>pA)XDzXyvth^#r20acBrCaB@`Lh6)gPwcn zImh<|>Je0O`r8lm{QDJM^BVMsTP?lVK7JgUW`p81Et~Md- zG3go5W6(=FBvA=fS~dGLnmrA!KTZCU=G61f=b&etdVIMS>f_hrNV}EJ(`vtgo-%%f z*C5?hxD-bIgzO-G5hx19ptznIMu!-nCxg#1dI5T>r$qE1?Xjg-q*B%x+oNP1-S3g3^zd!+B$-snWcjl)Nl)icI&K1 zR)1!MhJ-2eFKcQ0ZC8cXv$e2($s#w@@?rKXp=Xp<8A(>{)={#n6y_RS<=W5%H$8=t zjU71y{=@fJ>-j8A;j(@@>YFm3E-VXJ^RwWN$JJL4^f;+F=q6VQ_(Eb(liwz@TcD}Y z$}0B6cZOTnqb8tBz7zD+Nl%~hLLSf)sqY~<Pq;&1FzD8`H8g{!&;)c~t%GZAMJs>`SqKV( zE~?hNlrDdF8Km-14$4AlCaUW65J5iY=3I1eY_7#xNDum^U-4%iOcU=wVG#V`kcfKf0R zbcvk^qhTbBfZ?EU6;Oq8E3+{$4ko~Om;$q4Dp)WLGQvzq4~jDbri1i>FdOE>T$l%o zU;!)ywM5t!ScP8|CLOou1aAqR<**EXgq5%w*1=j(i6w6a)p#RpfGw~Kb{f=@``|Df zf&*|6j=*s^1Lxo@oQ6~C|C0y`kV3Bd<_cVft8fjjgEF}Tx8OG1g?pfyJ%WeuJ3N44 zApLWA1}fMScnr3!D)or`J%i^duA2V?XpsC0@8D;60cuY5?{A>6m+%X`g4gf{-hwiE z4|XO>OJhnIYn=QE8WVqj3hY*iRoeTY63b9Vb}f{U22a?Usl=Hf9|VHVNBuzWQz(rn zxRF7p01+TO6hI#rbUNUI+sL=n|GJ%u4$&a0ksIOaASDFsj2a;4hl-FLw9Cl>*&q|B zAUes-0LdUJBmtfJYNHw(Q~(`Q#4>VR^}p`v;(|(^09QxD{>Cpik&y#%6M}T++jpPr zvyU|BG(}V6rh*jkJtT*ekRCJ+vf^qpqCy8j7B_!XL!m&qs)_AaO~d-b%C?QVL9HR% zJYcs-edLOuI0ZnhmlN!GvUkf^*Ir#_?Ev{D7cwROS_TT&BBVkqAv>;YZNIIP|I2&| zp(C!_V{8W&Gzpo&zZ9$pVTGZBTrw^MMUl%xSB$R;SPz;V?QyRAvlrw(WXd25s zrv6vI)WcI3*nv1Ifoh@BYfRL{)kdTmRD~)~9cq9Ur8>B^L3Z-nEuu<=?Jm0&!?t!~ z^?x&H3C*De6d@7q@w&ir2!%Gl|Bj>l*#xU$B`kxb&>Cd31Qx?0&|080RQO>q0JNU; z$5p;sL)_gE`obPOeQ} zS&NnmHWdF5^}klML3ot$Ko|^K)r53&VGfLkaWE1_z;GA?V__7GHvY-DKY*6oNw^bX z0@%}2;jIX>aHr@k%^3(94AVdZW-4wum<;!FB35GSa?Y4hKZ^yA+#r>MyUi61yKkS1&>i^vcc5|qtUyEq#RAVJ#XJoqv z&?!uXJOl>~U#mpew6>`KReSY|y5S;R0QH^v%kC>Ro4V*c90O&nkDci`bf@72oPy(E zho3Zh+b^4c$?E^Ja3+jWeQqcAFGF-;TS;l;GRqU?7Ww-?0K+~}cNPh#a!!;-a zp82mx?}7gvzE1BQ&^tUz{5QA*uiz=%hr4hOv^t6>$PeLxaUbJ8f|u|!ynyHM40dCm z8uwRttNx#i@EURx@C&Z|Z(zKUBTyinR;YmCaDT_u35IT+KOuhr={~}r@Q3jiz@#Oz zkBK82;liJTu$&NG-x7+3A}U0I%qVqCk_9(1u6=Bx2$_KRjsWyJxU^d;9Ll`tb-S(; zQk|5>1KsI<2Rc8F32m{_(jFT*F4RSStp4}M(*jpDP(LaGMN(uXqR2{6i6;WpsyQ?T z{{L4&QW2(B(n~_x>FDL46tYMD9yd88gQTE*YzR<4Wf~iaZM4NEiXbU?>cM!7vC0!T{(G{Xny*53bG!dqGdot7NOucg9_b zTL-rzw19Td7TQ2#Xbr7knZ`{^1P##UxS_bsa2r8Gs1J1^2LwY}Sc*++NQ11_Q3VuV zakHVz3PF$w0zn5GI^fVbV>+;H6Jy^^>qb|EECgf*O|k4@j8u!-P>+mi;Z}kQP#$uE zome^KQcxO7Krtu^MW6r_f`X7A@a`uDGQ3EL`xc1`Z7=vYC=`0 z40a*3-smb?1zmNh2GWJKkzTD*1L}c0Y+wkA24E*>%eG&Un}GUII{BL#S$_4kZEL%- zu~$kBR((34YwxvnJLEY^<+VnY&`}5MPQLP9{ScaaW^2e@;{F0ThY(6LQ*+yY@M)tKR?DfaaTob4`>O{!X{bl*xunS7aW z*aX9-w;tPzU|)%MA*(f%hFY7}aC@53&QMKq9*)7cGEq&{Dt3XDp_)}`d>JQf>&X7g z*6ge6#@Cw49tZ!F;J0dLXQCRaHNRCMPZy}^zHG9v{T_C}s7pM9#*`}Tmm{xagftCbQnefQ0peQk_{?F!{##)A7_ee$j8WVeo5#jeq}Y&AA)8@uKH zsSA&2{kQwe9{K+iA*}7UW{5o%zcncT(mL8V=*ktgd5+?@r^wd|GlnksvfI=;iu1LW z(G_(SOo1-g3Mz+zYg3t<7whj}m;=D=*21v6m=OowSO z6)e~d%CIr)Lf#2GU^{Gstxnc}EeM-o6KsSHupZXIT37?2uo|?P5%JjEB!U1)2noO+ zbi7xc%wi*RFzNUXHx|SMUF`gDV?cD+?@5m_@CxAoL_?|f?kxV} z@EhoTIv4zmd=z|#T21|~ z?vq}vqquf=*oBu)1y)5R%l0?*zm)${gpoBUHQ?-ss)b|~P|d2^*zuIcA7FQ(3Z_EY z;j&i&<+lrQjZQB^1xL(dhn`teK@S)mW9X^I&Gs z{w7e<wvixotNyrOIOlJXkaJ5Y420I~r*C#vJ z-~S0h)^~t%fW8})4iqONT%|iQ;HHN_P#k@CNEs^+r70bGxgEhqp@Jv_eYr@9%UH*7 z-M}v7T;%zXjWD}lvbFWnDGj?ax)~0nH45Sug~IR~@r&RV^Z051mI!6wHUYPw1adfB z-RqY|PCx>sa7%)2IyLa>f|^;aRRy^PG>2x;6dFM-Xb9z?GE{>)Py?z$B`ELWPg(Fj z$dzeDr~pb<(G;p9O8J#Ub&#F(%0f0Z!M>{NqFWokil}heDqaXQfcl^`>Om6-Qw<*E zqKHXm?QmN|D^S91aoZTVBW`<8fpk_bSN0uX5p;phpe^b`+yyWn=D}Q;1G8Zk%!C;* z9o(9X{Sf*>ALt26s0Va|uAoFk_b_rVBlixYv+c&A8wmYPnD$!(kjKIp7!AW<5Da$n zXDAGTQ7{rlz;KYEt&?lZ3X`1@uwW95CzFYCVFFBnA7C=*NrfI1PD7pwc0uL3!+O3Z z-zUOdI1M*RWHatY*Z}KcIdnk340j1EhTbGB+oj0=W^%j#adxjX354RVh7}N>#D6sU zM9Axq*Fq68wCy4=AnmvT*vM`bHaf0QVb>V@P2sFxqPh~)KZ5@-9D;*z0QSQ^*b94L zH|&C)umiTkHrNVV;5OWXn{XYj!4=_S9gXTr<+dk_Ts5?Yg-$B2Vsite^Bxj;&xr1z zn~d%rZrFzs7P=|$61^JvIqoCSGYECSLwEpc{KxPVp6IIg4B-X*46h(P5mLi1$klLb zMc@zU>)3D5k%#B2+8RFkiuWJr^p)>larL$Dx5lrpg1aCb{Ep5SHv)V>_Q6%W-!#z_ zF&&A0#Fe28-hpb80sVViyHFYNOZN$#nkYwBD%hit^j=zU0+J~gS#Z+k9!Z-{!;p4s{boGEx&abPH&O7g4QlAK~Ir2 zwx8fG0(~V#!>%3i9wR>l748ASfokKngXCOKL)3{oQcj1;G)9|ES6aP;9ouM)E4%}R@Jv^@gZ9qthay{-^SOcM; zZ@dOWHpl|{D!#s=uhK`dF2`_Xb$^dHIp`btNg)a7oBD|ilBH9aY-FoADIhawoU6=P zk@Mnah9G3sOM2-fXGdo@W<})rz_)n$O%Q~x&;?YvMxcr{0KUTOCw>^Bccyqpt<)E5XH}D5ysZL0pg1T@ ze%a~!#`^BDY)XS2HzO@m6Mr>OjqSozMOM01pt74kl|YHJ_U%zpoDG1PROcVN9X`8z^-Lem9>Rk&ou>uuzb>VHjjJF!6o3ve^AZ(ft`s8AzRyT zyV9u;N=vSt#|Y%%pnQhOXeihj4nZCac4lESl8v3XUC3|wRj9D`VdYVTsTGBL8h<8& zrr}tyn|NFpSp#SSjEB*nLaM3NuNnhNNaJWKSTF^CfXOfk)QV~W73zzEd;9+@0Ul$u zjxtlT+clM~5|iIfOx>n#QLD8xvTd)iGY4kFESLe)K^4re{#SxB@+!RknP2F>47dHt zP_@@gQ95%$1(0qYs8w=9Sy%uX(2~`nnkmIp!-dcRH0p~&*eRpMMop|pnu^NAo`QDR z5)-}}cNr{&#bE2@+WzH6UWL07+&}VXg~5){7=PG=bQ7=+T^pvT_Bgt5Zo{vcql@WQ zF~d$3QRJ9mhQZCqWS}!UZ@F zKNNPQsLJR{;i{l%ku|`Q;|72)BmhlERU#5{e2597@%!Lva3=&OvfOaE5y0)o zpBNC{D0Hl=j5^~+N2kPI<$8B_pWGhDd#U{x9#$Pv-0 z5`qG_J(~iRL^V>PO30pGvCucguT1@sd*e2Pc*wnQnps499Kn_sfrAMch(zF!vE2B!d6+t!5 zfm;EzZ7hph21-FOC=B@^6S%eRYx||l)m#~I3*#zt4Mt^@3teVVsnL9|};3`b%7Q)SnD?8KGZchzuBN-@A5o|-dHo-(xKYp389Y=Ow3%4`2 z3-{M_nE!U>8gzNdBoE|k>uW{g(LD&c- z@GFA)x+Jb$dKE^4QOzlPC0-g?whB`rHQ1!rU{tHvmH3AWxEJ)t|urU!0s=moNKtF$ug3w=Na zqCco`3L6Mv6AjxUYT_X#T((0&X$}X4sp&_93O@=)LKu6u$0I`-C;>adSY%}?o%*&T zf0iLHg(CP1(D(U4VGGa|Le`;cE0~Xe9!!DBFbP^RrY7Lp2Q=EmOjQ4$CZd`q>_LE1@!RR^ToNrTHVS9;UkQknt_J ziQ+nhN4XmJDrg#>#k~MWL2H6e)dte=`fl#n* zx9R%71%-^(gHGd>`3BH&(q`##HyO7Gaw=@L;=hf&8&`$eg}WVg!VZ{=jndjh*dhEX zm94>g_zBb^7eV2&x9t?JF!?`cQHk#B&GPJYI{2YRLO65K|<1?n!Xba!#@zzo7B;n#uibNqVL z@C>&N{=@JT`3XFRNAM8b5BPH*w3mNjTn(^akTr zwk#W4{)+4NWS|T`Vd!84f5g?umj6%q1MF~Hwq31&^@v*+YC$FNAr0BvWI=YJQ`-7v z8~w_Se(grTdXvW2I+@&++?@njznG(6&Ithh4wQZmO1}%GACrm<`jM&dpfcHI42NHp z(yUWMsSL_j;T2835wMZ&Yi0HIhPgckcBS-7Rw}F-K@m0Rqu^IUwobolrC+zwuUv)I ztI-rk;W0sR_3Kyqkt_Y!RUFW-U)jHe<&DcEP^2#tONe3E#Fdy@GOR9aL8VtZ|6~)^ z{$G{SBv3>(ksaA?4cl+mTnWfFY-YAjwqY~)(r?Gnj~s;!b1Ol+c3~4(%YGs=#=7}! z8;%__vJPhL8)3~_{iLFPT2Vi-c!zXCaeu{LE4mg_O{`?N_C94b;j3UNEP=(oj$)2Q z2n%5z%!N7NOQg}bHDNF`hZ;~03W9#gK)=_h-)_`zIO?HaLiZsRA7LthJ2|MjLp3!-m`Uq1rV z6q-O?r~|d3mYY8^){_qzYe?A<^w6vx;kpQi;5LBT^k5_0#t6RIiIq-x4X)M6B2!(b$gfZ;G3w7HvwI}^00o`E|ZromLOU<&95RVKn% z7z66&akvv;Jbb4&V17W*mSi$+S`w3yo`R`_3((C6J$9>vYY)sHk(a|VSOF{HF11p> z??K)TizuWXRj)%{3ssPHP1Yl9U9Z*bEzB zBS>evb~;)@l%X=|0Cw7YvAvYSMKQQU?T@(W$F#1WBb03xjdrL1(jDuYBH(ePkmNq~ zvjpZ0%*KEvBpxAquV>i!;J|=OgailX2+T!05)y@w!})!?Ojy&Vk%<#zP0i#AaO=0d z_`iSLh)r4zUG+YOLW2S`2WD}!ZQZ10C$@XLes8w5+s-*Z5|Y&u;ONk@U3-;lTAZMG zRZ5hYLP!omGP8J)R18w<8(3_1+A_x{6GBp?=lL1V$b@99Q6pNNA9C+<`qU2!3<^xb z_b>Fzn=0U`aZBEuKfUuALCT2a>O{A$_2>1&=2%3<;=5kW4x|lA-4_dzm4$x)OvTTi z?#_45mVQX=4arXN^_w~>%1=Kn+*e~o^!S7jLvh9t5|fa<;hrswli>G_o;WI_=LbJx z6O#2s)n>j$TTde-8*!2lw4O+D2>S5s&vw85xaSB#LEa)9iEb67ka6|T_o)ZgEJ#*m zQ?~fmoIc$$2WGck_ID)?DoaTs5f*p-qDhaplwIUWI7?s7>U*jD0Pu82YUu{hP_*z*WA3#Hl>j7e6qrseb_uWFa2OXjwJk=joos$t%gcoP@->?dNUpWLGoQPhCIRL#I!_>~uwDD}OFmfHgUrD?z}w z8{Bk>b#`CG1n$Bt7;2QgpQ?3S^mbGv@+Wr<#~OsFyW5xA`16sJ)n*csi!x{Bl1E4) zLRJi!^*v2NJqb}gi|0LmXLRpXc?ePCyGgEL zkCyE_GZu4({xSa0k!inqA_c26HxWrK`*Z4@F=vh|TAPsU%!pu)oC(o@+x2n%v7mQ3 z%Xs2w%s5UFqM?y4cD2-vpI5Bp3CTj8GKZ&Y)q_vNr3|V#YVu@f#9MJ$6jPvT)4oZ= z&U~|G_nmI}X5}h>o=9|Hj=-QS(TK#T=wJ16iv)!5L55b>cvicjF8_c(NL{1BfAvOy~OG;y8l{jJ57 zT|v&&@vRQ|zg-uGh{-KZ<2LQvHKkqRE$Y&?Y2MR+IDLWwdsqc}yOOy12e`YmZ=Ka~ zr{a-_>2^=4N_N44>KVt%0PiS@>RLL#ezt>~RgmDo?6kl}LNq2D)mkzj^uf5CpNqK1 zgha1&&w4v^b$>##(d!iDI3b!A2}52_7+fcAMNgc}feEv7=^#jLQT5@RY7N&e)tZ`F zMX+|~a>a9dVt>noMfGdt_e|@rmc_FaRZh$yHW_vwQ0#}alO}f|Lo>AgzI<69gIU&m zl6dFmg);r$IBwimcQI|$O5ZA(H8n3+tRhLh@yf?cnJj3j^oTvEqg3zIAfV7FVCbJO=@cQ3s$++{_hHB_#CpHL8FifnQD(owIN?FLRbm1 z{4t~QALgK$VeaC|y+iQj{=OY%{&f7Dw_03_9Tf=Cg1KbG^QNg9 zrYS*4c4j97tu7%NO&v3ge3AS{Ebj_O7!7MeuK$rE_n+&Z_$W2+oZrTht~d#khnrJr zZnpd0knC<2j$;IAEgd(lM2lY%F3&`eF0&+eo)9gVW4A_oIDNs~m?k+w8g}m4u5s&@ zT~4lS8nhte&XI)B=ZX`;8my%>lg}ip{;`H{2+_qMOOT@#AzJP#bWb-Y`GUQY zM6)jN&X}hmt>5e>M9oA=MiQcu+)OZiY~<=W;}D`f4h=Y&5Y@|P((n{z3SA3Nh%VG_ zBDHJZq-nER@BWA{hb>#Sbb!<8%y;`#eQwJUt6UA1rTU85PSJ(^Bj#H))Bcpf)Dt`aFa6CziqQvC!y?J?hGxYR3;~ z1FXvfxw-{bC`YTaIfg<6Sr&z{lh zk411XV-eJ5-Rq;LS~SOkEuiK@HA1xGc(|(Pgc^4`YI~TKWrKWMWVB8yZXYbzxb$Be zKS`NA-+k}GLW7c-If)Q;-0k64yR0dHG$J9{0=12DtjK5u9H3-}u;9Aazw`ELKdy4d zc;&J1#J!)$gP1|M*xa~8^Eg&uGv4H!oYX;Dn# zyi>o@ix(ZRR0}Z+KV-6=k(P5_W-BloCH{@FsV!?%I$y5dFGp^hG|@(&Z0g%1WBNpo z=e)q~iFHW@cI*nWd=I*sS45ZrF+s;Hln zTuz7@|G=>)8~QaU+fgA_#W)cYSrf9k5;>PCWf2#w*67+}85Lq|hp#bpwCq&iTA;-8$do z;T_LiJ4D`|yC}9#Uk$N3gt!tr7v;CchfvqErCr$q(iZRz{ep3BPR+e5qPC@KEsEc- zfR((AD=6Uig5FGO`^>yr{I@w86j}x-R^LKah9eYbSRpIq2*n#;$m%8k^g`Au`Mu%( z0pkmKQ^+|bwnd>GN9~C1}Q7iLNVx%c*l|D)rCMxC~3z?SW-~C`nBbACA zRjRL6%oB35m~~ufTrOty9~RNy@*5p7LDqT2y}i?@V~TtQqr}jbR@I~MFA32&JyUb} z=~8dzuq5`=SjgtE1{856boVR45k0{Zu9osydnRO<(}x8!r%Et|5M7m>*M|0)IwEVk z&mkKK(fZpi@#|d|E~Q|%X2-ceh;GxCCAxn2!rEfLeh&HjDqBBiVD@0kUBs0re)Bf% z8#U_5k<_ISccONmwCBm9LT0vn5>UgAA&mZU8K&%w7{|>@RHLMKbf1huafnSKC;-Gq~bL)Qv%AhZc%S5&biNd!gvu#&gH&cs?sq?6ghCoTRwA zIXHdLmAF;grk$EMW$AviIMz((#Af3?Rbgk&scg$m8PzPE_aC@=?1!IQcw;FfYiaM| zm8HPKlzj%pOyCI#(&Wxdh*p-9_jYucI`%CKf@krjFJEQ1LK-q=yo-382rM*QS0*=f z1;yV*B#n=!ZTHWp`LNP4B58xj__!M4N*ElWj5oJxAO8bLVT@b+}@t~ z>h;;PG5TLzE-1qH6}-c4Ot%9iisc$P!kLmCbdSIuTzJz|^q$z(35{5@Y0JTFoNRe@ z3(3Qrik4q(#($-X){Wx0T`F3^MOcrARJ2+ZaW!*}t!SMs;_4qzs*-nU$=z&u=SivS zw_-?ow}Ospm8|(iT|pHuvtnyQP;6i0#wSw0?&KZQ$|Qsts7=M5Sb5$yC_7OTNf$_F z_t?tby?Cz$SMOIDvY|5}x=~;f&Z%tWFUGa!+ZIWxTBD1xxyf3U!*^0&^84=ZMlI_w zmo&^|DOc6nEsK9&`#M#v=&JpIs#a)uVk{+1ZK=LL6eYo`JpDKXWc%Q`hdNl*x>XK~ z8(3(=_Nrv%QSBcG75Z%PwyHH=X+^DO?JR?vwwe`4nx=(=oW-kIu}aXwHLF?0OSsAg zw5;actSnABXmDu83cZ{@*|>+%eNWG7)+#Im24bQ1^Lv^m!NxH&IONG-jUTIYV3tg#ahfa=JVg1TRpDmgb!g)gf$rmoQt=ecfw-I*SFUOIe&j@UF zZ~yHOyF}l%Ff;FOqx5s?CJm<yH5Fto0_=iz8k*0_QXzuKhHoji-VhB z!5w}7##i%~Ytr;KJ!#M4zxT#yY}J!rwzXa22#u_=6_~{-8dIo?q!byFTXD_h03-`{>`dfPH27c8qb2?a^*o>jV-c?%lj&4srB!mSwkQE32UT$?jRkYREM1B9hkRVz7(AS|nIip7^b; z4mC+Db!+buJ}9*5sdTw2Z1JQ;8}ep$YpeWy%(u(DbsOvDIXo}rY24O|N+Ip`= zG4B45DtYD_*FAAP$1&clg1*`B_Fm-6Iri^o%r{0taPkh`v&C((VPzIs1veqwm6r^&SfanD2z>vptu52bH-D053YBp%9~9OU(f4#bV!$qKB)wKr)e zt4|&LX**drhS3PC>d*+;I$0lwqb%LYDnAmpekbcs8FlSsB_2iNjOpYZUHcu6tNb#i zdHUp@gXFASAE$M)%GSklNhfRU7?*$Sv{>pIS^C|PHW7YaP}R>TVrOe&UDrit*3wpo zgbb1XVTjzXrnq? zN$caE(%Jf$o6;>LMQ!CWw?8#)LcG2@>);SkNv`c|wZk%aHx_Dd z4yOtEj*trP_ifl7zR`3-v>js$a<{X!y*`u8`w%$D*`%|Tq5%zvzLQ*2x?m?~i@MgQ zzp$XwnbvFTDt9)84B)ebXlO4QG57m)<>%)&X;ae&-K^*#Y*$xvEpW zDUGR3bMBv2o1!s7mvk9(nnQc`HJ1{nBO=s@jR^leqs!VUIO%wF7`dBaIChB9&m7A`QwY7;W zUF6Pvyd%LI9u%;K@eW_nsk z=K_xNeXR~n>7RR8#KmHH)Qf+9Dsn-)GHtnOsc)R85|X2~wX>-!U5X3+yqE89J)_C& zXy~?X`7xf_xvhfD$n7i3lzEOEv%hzoy!+6*#on5eIdT2qHj7~fYbcmEpu9HEP^AjY^#-@HK|?gdfwedNh=rQ zke;Y9z+2{82ZJj=DgFK#A&hX9Y}r6-g>uxOmZ*lQ-v3t4j-4QfJy> z*2@)SkY|_`+5)$9rzvZ2$`7;ph2l0DX3brM+iRFra0%{&VOGT-ahDCV{JP-oMc*0s z#xSdYOWZ%OUyGY~xHWzrZqeaZhfcT^@wauQ%O5hFx4|h`(oWt2_4d#6q2;iu&HQ}q zkRw>=CZ+P-#jPhdT3y}SCRu4NZ)Jj{q)kDEjGe_QBpooW^y0QI8aMHs7wtKbFq4^8Y+lyX0T3-dB}&9sm2h z){VA4w)@=9c3=JnZSd6seN}0f^1sh3_E@W5d)AQTW4*IJYUHN*16m~E8Hp~$>cbp_ zXf2CBq-K#s5$d03WSP~O(H+VU!v&PXLPxdRe_Qmv=apJKqqO5T9Xq81O<)((?gqP) z|NCNp5%aI()zrzJMSl(aYCAp{XKn4sRfEk$$Pa8l*$4!U_g<8iRY-JZ=ESqxsCE_} z{Lmsr##?zcIc?Xj-T!>3{9`S?oOSkEVy_j8CRmwSYXbI6@NTXKKk!TXgDa-1(%wiOF5!&F2@8eyv>?P;g1gpbhYV~k}RbETk7q$5hmY|Xoy^HMr8JQ2f8yGRA zXP{)&yH*V*TCuyjf=YJ5!jFNHV~1e$~Z6qMdm z7th7t+YTA6Ox;}({)58U1MACa^F`PEw<}7XDOS54O#P};yyeY0v2m|Gv*z-wrvw^Z z;6rfLDpqzl&<~MQ!)AtFKhYvB%bQ+ zn{89AD?QnCo}OwI?nR{QQ>|{jIEBkJ%^M!~!8QNEG4kysd2?2pbDDLd7X>Jcg*KcE zn>6SDHazl-Pf!6}Jx? z;4Cw}4b*vC&QVXdM!bQgDRF*6Jh`OYmAB}bOic;VHik%6GZ3n3s(bPedBvgbF8^oIIqsN3Krm2Ei@0eYRTqV$Mc{xf!4EroNCtX z=CL+sSUxEz@KTcm47F0!Npd^!KCo*7qqw~_uPV~USgHiE&0JERx@6t3jX_mH{+cF z?;TIImOedHbl&mqPM=Cky^D^0TkjncLH|&0yL@(e?ed%QI_-Ly7I6A^v{vZg;ERI< zYvy2Ai-2F2d8a_`o6Q>3taR!|a@|63)#u&)saDb=P*7hMZU3udAF$L~lO56Ho-YMUC2C3(2R)4wm5({VhnF7Y+ zG|`=PlUtpJGICkxGq~vy?gF9@BiZ^Zysa2{O7`J>GBzofMoW(8ZrFPm;&0U+#(UeH zFw+shotLSur_0g)DrTI0>Q-RL3Tr$GIcKl1mJcHX@0m$Zz!qYtycJ9Ij5hmJ*=n># zHobq&t?mjd0|zMq*RY6>#kd@e@{~H8F$)$t6reTUuCPL|2#&eZd*=Vkv@3g$FK_(H zW8v9BW+X%l!nDo(D;~?b>&@qoA}g&`idzQ@y?f?+`NqsqP2zO>Y|+i68h-4yO~>P1 ztwM;d!{lp@cmgJ3pox_7^qQwyVRUeF2jXepAD80q9!|;rpUUJ zmpiBRgm|iLPPBq|8jJcpil6%B-jh6^+1zK=7ed?N2{!vQ=OCQ(hsTfw><61-Ltj&_Nm;A z)mGloq_KFl)nPPge0l$9Fp68&zq6?F_-i}0b|Wuw9Tl`pEC zZMDKlh#SJz9=F^6M^ z$*oi1FF}nzRS(q?YH|0Y%q)AKCpq?V{_gbmK|8LUOC@ zcwQcwg=JJqF=D`~9wn#ez3=pHjWTmSLr8Q&7XG~He(#ALdB(}I%TmJ^HP%}v$5W#N z>#axQ$?Y7Ln$~~M?c91RaDr=oK=uvZv{F5+8JI2aK(?*+v3`*a*0TxJsgkkCxNAeA z=IQ%qH5M#u4K`RwCo&e=VWH8oD75*xq0R?|O-ZSBUla1E&x4D}trf96aXrh*=nYmo z#hrmg9O7P$J@CSV1rJ_+PW^WiH)=?nE0Hcd=0rFhJ12b(ZM3!%*GUO4PIT3e++>S) zd)aP_m46ax{=LNAu~3gT`9HON2UwKH^Z(rm^1i1cA_~H(2#SqEK*5gKV?&G@y8=>` zCdC$`*o`rE8Dk5!*s(@5DwY^~Z?VPRV-$PG|7YLbmxB{b^3Ct>lPAafPTASn+1c6I z^42saMa6_^hWOfSn?5$Jvrd~LAm47KNi(4JR=;q}>(Q0YFKs>K5bVg==!~jp#%$wu z{VFBWKklQ-4zXSMeZ^BKs}fov@X96)(v780k8fl(!2lTjhBcamo+ zq_fyXty6Q#DrH#*Cu#XBI+SWgeEsjOTjYxYLtALjVrVZmc4J~2 z4-8$(KAQLgIy>#-W9g?=rG0b)m^pL(fc^9o6#9_;yyK`zk`L+RKL;E|N=hdmpsDG2 zR;SP|2WV0np4Du}zn?<$WQr-{TTn?e574e(P?Gti{eLf6WIfda)hek4{QIg1ioAI- zRg)_F|6N{{les3$6Ig1JJa=A-CCAn5peliyB~z2-|GvViM5+X;b*OD>F2R4d7PUmR zk<^mSwVQB=JXWC2B2oJ9tCnwDdK{*rY}rATqM(*g^~s-_0R+ctp zYN-FaZvRd~RWi*nD!M6GYnq$O|33*-9sScKkxx6nxmH!##n`L%wg2AiYG3-_m0{jn z)ykW%J)2iaHQoQak_&41|6Y>niOfw(ZHNC}g1Hvex)h|f|9dZ6P)TaX{V!fs*>s?q znyR$_y$UL)|1;Au)JotJPMbGthZVci%9Sr#vdJnd%j9qW=MoL(IYnLqri+??>d(|# z|L?}Od2Pm(ZB{z%Cs}PfZL3p!cGvS1z2A<>QY4;>VOnLM>OSO;H6I?BreqnX$a4oK zZ3ls23yi3(4VKp`9COD4*&u9*#2o#SfZR<@db+UD%WWpeU#Dm|qx~o_eA^z`ym0!a zp(czHr@0)SP3H8;C|q1;f>bgfKur9Nxb5`tp!(mpOSUryie+uej=dxqQJmwqRz8_Jeg zW`a~XL*;gY)=yxpY7x~dp;6^|CX7x35*@VYD8=D~g&QLUHb#*V%xSd!K`E=hS z^7Y8Y|KTPM7(0E3bS1Fuc~hLmy2T zetONXzQQj{FP3tc?_OGW6xj=9wHE0e%iBD|K(3A4FUB@3qvRrRj2+V4rhvm}vs$+) z@-T8jZEw>&_MF&|Hsje42TIm4J6#%DrMjU`xK7&#Z*1@;*SYFpx2fC_sAeKStdDM- zwq93k>M@q`V7X-oFi)_lZXZ9RXU8FIkE2*X;Tg7SmMa(r?VK%@c4ZfZu+$jdu=RmO zyKf9}J3ZM4G|CdQm*)6wI&%d3!tMZrrMyD#@VxXFdmEMxNC@bna97SxCj-Kk@V`4S zyvXlIue{?BcEDxl9lCOZH3=}BfRUAQIIx=A8@6Ki6Q z_@U+Fi=Ve_iFIQI6OvZAOPRnl_yU7CMHJ(WJ2~&t@_T5-i+9P7755aPAII;zyOeST>unaNt?b!B_`>(7H{Sb} zyvK`;nX|pZwBXD=;KY_(QHBd3OmRgnJfGEdbCCf;AfyG2_h>caS@$08JC0u73Ybh9 zhp`8K$n3h}rjP~->;ni(-rAj+)HL&_!|HW4;1PL`JWin61A(Cj#sz!wuuTbPTOpXj zAYsZq%DQG%+M=dr@;w>`T-^`%DD4i;@xy3;x<~8StL>=9eXuzSHcWe8FK*vDIQH7p2Y`H4lf7q&BBkEfV3|OXopYJzyaJyUK%G5a+iWOQwT<=rcli=aN|Vz*BfHn#`~UjmY z?d_;@xkt{}AFngFL6t6-WyufuaF~@bVd@Xk?K(`8qk=qd9IR(|*WMeomnDSBA^Mf{F>L_z2HRSjzOzkkHLRH?~>D`a0ddc~x` z=Z~o8Sv1cFV3dG@irlFGpv{bF#e{;;>8u~qRABfPV23cIa*w%%rv0GzEx&crW5FG^ z%ga~J;PVzKWeJYC{#0eo^@@wHI*hhCy-!$sPIY{LU4`T#zpG zPAYtXAz!bitkkK^BQ#$Znm+*pVtvf}>Ku!k1&Q7BM06)uPZNjL6eDquukGJyF%3mja@th1cpBGDV4hbpEU3u zeTiQ~!7cjZQ%bvlT8gegr!H94@_hxyO#HM%5jKDQV$lbT*_Z;x+GiY+-D^shKToyz zT|gkd@Yt%)WcH^vm-?Hl*>e5)&!C}q$p2D-&A_^g%S#HpikhI;OlFuD(FV-@EV7=x zHTnB({z5yjDAl|1mZj&H&@H(5pIQTdZ|Z}pU8(OmkDReZwS@w}d0!W3Q=S@itOCJQ zjF_qmAkQl~vgQm=Ox3*Wc&p>4Fj|#NXLw83?(rn2TW&#y1w3 zYEA_1{m}BJ8kwSw+-l5SRDnLwR3ols)iwN!c45)OKh)o+ok5i9+rdJs1m&CFl)E@J zWdYkif;*umle28KGYfG)7xT4dWcPcUFd`r^~j%fnYaGzAAqqP5L6J(hs zEdZ?!{G9)S)wjeQ-)vr0>9?OcR(9%Vp~_kpcT&?sftgdXN)-$KEZQyJw8f?34gX>R zD7(A_%Hwi6=+Y#L1%F)!3)ggIR%>&FO!@;Tkc&aA}^Hp%460RLepGVr7aXKW>tU{7-VXZg`HDS znDQKJnfersR$KEboPxyqz{@0@1i2NadjiJwj@O$VM> z8O#LGsjNRiR}2g0@;MpJOhbq4>4~RMR5cska62YFZ(;X%$Uc+Nrr1!qXRwXwz+jn^ z!)59?y*}?z(S=O{Ya-jA`C~Sp2RoPSXGtE7 zM@h1I3BGkp(z;i`Yz9oWMJ;u6o5fP?y{{CRYoc1-fh8&BH8j!-7)&E~vfA7TsM;A@ z&=D{wy!s2shZ>{Wj2P|K!2}snl2)S>!+2mY*Yf3}Lm4yrx58bDDlL_yYfO%nCF$); z#6UYsk_Y7RJpc+8J6?vE0~dJA|Zl<;iElwTZQ_Rd3L7w>V`_QD+Xwdv-5AQj_IbBp{%zME-8>tgfEUL zp?kTg+mAXec3&||)zoz2rW0zny2$3X&9cFRZAHQ$+Q5nY-oemto;Z31`~rS=y~95G znoiX7JrtoUOHTwiyICM12Askmt{N# zR4%+~!;J;rY}Y2lg_%ABgqg*JkUb~2W_H$^Ace}2`v;U#5?_*L9yaP;n*|4fb36C8)zX7>Sm(tQjXFYZKS~XGBWe_xA*@* z2T~;)0!T@eyP|eWx2C~$u-j3AAmo`;j@~oLeiRtfADn1)y{7vqww$6gf@VWG^2`SJ zeZa5-kIWJy|GcooVVFs>ivse^{MZ9moXWot5QHI*%F%E}TM?Qm1=@~bmzMvw_{dU* zp$vMOLgo2L>@;d&g~6INm{u#2fwoL}Is#gqXL-7lja&SNm!+VOc*d{$N3@)H)~oHZ z(3wVk9J3-+nmCH|ltw{dh zLFcqmEtR3+Z_#}V#wZxViz-Enby8WKTJA-izFA{8tx(GLVDc$0)>Yce*B3$<9J* zX5P5}7tg&drS0r`0YhPo!RhYYp$zXk-dL?%vEl62DCPJFZX%{YVB{03B34qi)MkK| zjZ(Uy#WqiLu}NmM+}IH}EC4OL)L=_t)T)7z9u}4cn?-kag&7+WV>q+FKO*{L4c_ub zbn|MvGqysbqOf8zZMTBF`G-o3md7;?1q63kS^9^;9pi=IEOELa9#lm}3lI5`CwKq!+x27R;amAper0R&IuDx4Zp^}Aqm{BWN4+MC zdh4ZHhO)yr*_Yp*OE1^8wH_6QM_~V;{&YPm{A~^1I}$AW-?`^M^))jc-BmR&(>Xe}_L5`NXUXlEWVVe~qkS`^IlTU_PL z$AXl3;ys#2E#2g`vT*@%aroN zmUNzB!uWt?SH?ZR#Z}I|${9W(EoR^B{(|&@`%ScdSavN2?)fdQa#}2|epyUvtE*gx z(txS+tV7YGB!@!v>S)R9d&$lcjn*76yvyfEYlk}AmGs6h)6Gb)vq^Tu&D|Tx~ zjJRhjKb~!Z+ym`ssZ@Csq^+w}Zp`~)oTjz=vaV2|DuPp8dTK3s8+?Gl0)@<_8-M#^ z&89E#xdog##d;xb5+wiP@C?|=orGUQLIdtgTde%J!mz$cadZSWa@NH8sD`wsxKvHo zs3AQnF6A8jFywo^i;_%jHQ1D;@2r#&lbkJG(Q+5fc1_xrjZKZJPUKcmg%_~k*>smP!vTUNI5xXUKXM}w< zHD5I0%zpmiZ8c5y_R21N=!6UTiZ|_vqo0u(P_yytn$Uuhu#WA5xx>9|k2X`1S#8A~D2D)H&ANTj&g!W%ZZ`l#Tui#O z2|Wa@lS@+`I~rb%J=IfwKU8ps%Dt$(omA7Q84#EnGW8vLxm2(50SLHMjkU>_6k{hv z=;k(|b9T^zkjCLU3S{z{zVpHlUQm1^D?=f$qbZH7yvm#dUsmBZv&LOMsy+LgQm}Fv zZ;298X&2t=h-z7C!u%^W$R;(@(u+D)UQFFp>RK7Cl;qx&;#9N|0sXsA*}*OJ~=L29}n*eeGKD zab@~s$HvXpFIX$o10(nq5LV5wqy-}`E&o{C1Uc4s0Wi#vkaqJ!+EGg5EQh zt=jVDva;)A_x1fIOki|1R_NTe8J-T3Um^7xDPMmk(+8@5aU@jgauLr~y5XZHR=M^s&|CUV=6;h3w9{jZ(d2T{FuqNV} zv+d{_O7^`6DmG{&Y&d>rV%>(IQY;t#@4bM0tTiCui&YP7gw%l81dvBL&{hfHLqg%< zR^*eO@Q4K>N`RwjEFkUf$v0;Nzh?Ci3wzxIXjnNZ+^{r%X9LZr2T(S+`|Jb{CfTD2 z6D~_F2jFHk)RKv=IV~U~kLxOT9IgAt1i4FX%1dPo?|{JOTzg)-zg~ObuSF&VoA#7a z9->!j&*xrWKMct#I(kJejQRrd_PfTe!%zQ^Y@+SZo{llvNMNur(%|&wyW2I3pEF^k z0OAP9JsF%~G@F6JX3}RjL{80|d*2TOBqOF)-LB zr7vQaG|TR9@rMbcSRjXN-o0ppukG!nCP?)_az^IQ&>9%dpsiq0Jnd?y^*2ly{Q~JB zxa-94XD|gX--1gkSVB>U&(u=rLHm%lRgh|`8A8MSE?lLr+D>`wb<34)@=%iBbv0_< zPozB!KLeAgXM0`k{!?KW-k30XBD9QFWIa_v{yIgsu^Ub91r6-zMm=4jtNr-sDeL-K zPrH0Id(kv@l9NqI;gwEzqp1wz8ZcP5eY2*4U-s=NY(7vsl<2nFNA!m)T%zck+H5LO z5s$^YlY2$bSLn_&4^xi4ol-f=qrK<^F!5U5DTraT1O{8gNZM9nXXnFzu-uKf0;5-V z@~p%!A4%>`=|I;0N$V@+Doi{!>Olr0s>KO%u*7SXD6uD<@XwFOv}$14saL8ShJZf3 zsFoYXsH9#rJUj>4>DOLbWnWvZQBy0_Kfc=yLW-0o7eOsUkddLXhbz|M&kfe?ufTkhjRu%P9n}>@_ld4Jqx=A5)xhm?qy)Uh+4)Ozi z$%Fk0T=l)+r#>1chH|4hm-=|pq~A(BG3g>Tl&)1nVR;#vo;|{7SalOzRhnAvpfJj2 zCd7ViDZg-TrX5%d?7mn4cEfG&2#dBB3c= zu}JD?gg`lpV?61E5gJ!j9T`cznQn(glCuYXCr8p1PyCvxJ}r{squ6SV2WY*cXghEX zt)uwZus>zh+1cxt)__#v!Ve*aFiG7oBTa4!>IO9=N8gZsd|@f3|IjA`H+DG;DmHE) zQuqcC7Ace|KC5@peiL)?8{dy6)qqsI=~UI}M)jv_wefmRf3jvmuf@fqKDmB(MN^O$ zp1C+Q+0$fds%X5vZFOqzc}43R!HhYjcg|~}=7>1*_k}{o$5Hl|(7@a{9`&?XH7a`i z;C(Fa6#ZsN9C>=9=(BMYTOZc`gnq0AzN&;mGDE=~PsLR0s4`UFb5U@0-vA1znXAsx z2{fuEL{vo`jJrhr0H1~nN9*8M<+nV6Y?$sc6DT$Wzbb#BOI6^j3FHoPr-EDU4Na?U zRx>}8uku&rRolY+S;bS~s(k9RsE46$GIvn{ts7TM2{=5*WHw(V(_~*$4Gl5CWHVs5 z2jdJG&V`GZFvld*WrnGiAnC@*JO zRi|F-&vs)I8c~k*P;y3>$?0STw~pvssz;}^`lfPp>ZmDyO<7>)>-9AcoX$4sn>F<7 zxv{3E>;On5H1ZZZ!y0?X<;_iyfnSqf1JKR@1~ZV3DV4(;{dJqoZiLU;1PEKWU-a8+ z*EtDIn9mXrwPP_uW=-~YP5w{ta&;_2!D%!=Tl^F_l z3ThFeNUdxR>jMlhk{!+95N@a$Bx2Wj4xCav8Wo{AvG7oSyACI&SoEZ;qENkcLr zlm>)t!M9Es-h1uU-rG$OVQs?Dywx(zUOU|e0~YehvtHQN|Dg?*f>gs3LgpoK$cIl_ zV+`MUu~mO5teomKL3Gk_B*iz$eXXEFxQ9Ky6ntRlH@%We_CWzVSPS(xPQOpKa9Rf3t=7{82 zr05_51aI>|fFpV9>W`-E=0NMzivnAsMLUk>*^viJ(|lK7^0Q|PY|4&SP0(ms$1sPE zrUk&%%@|E>Tj1A>L(a1~{L~mqVff-bZD;}6E03jPcxmt%%QH@spGJ7?t~#bDs;uNg zv2ArM8CY@ZuX%BZje7S6ydd$!axa_UKV*FP3Tri8@_6$8LYklppFrEcKqJLXpsX(- z$e?#vO)8ye2X>CB^ng9Prc${8c!_*&j>eRd z@yX|-F|Z4kDsfMxXwVw+xgT1{fa~+qfWfZwZ_pmU$nyFJ;ujb4KVp>YQ@L;IwD!l7 z?l#x%nKZ0os+9Qssyz~a`DBnq6B-+FO)TlWWaSTsyY~ezwkZV3vAT^Y6Q%n$F!Is{ z{Pc%rVC6Dw+AIc`e6r4BW_f8g6@|q4eE>k{hfC^h$cH=9?@WWpM^6sKM6TektVna| za-eifo%sn*pj$APu62OAxU*CC*RGt)ox|X!!#3(d=rQUb>Yp)}JP=6h_RpmSogne= zz_fvLdpL1b#Ysg@u+*1`3dF3AybRx4BXm?4nSn9Y`?=J*p9rQ?W>% zN*rA#;>e-OeH?+wtQu!v)Q~; zw7L@6(UPy=`Gso`KSJ$Pvs7Yn~%$)j(ROnzME(weE&IhZ@eA&vMLSF`e5Ya_la( z_1(FYoB#HsN0+S}edwY|2J=l@fZ$v3>>HR7KhZqUvSn@M4o%Sv?Uzxx9#DXIT(*od zd*X5HGKyxe)g*(6b9@gk<0*imvsd&m{C4$c#-IEDJAk-Cb9a4Qy0k2xT^0~wL3vq= z#7GWjHIm|3Dl{*RPr>FHvw~+bT9zE+Y9Ib*ps84%fDsiVzh1~InP(a8SMmY!*27;M zcR7DJ31ar481%*pE9o-wk%oeoF~p*P+FuRs;KeLw{`&3ul|q1;OH=#!IEV_Vhyr$O&(3?smz7KRe5xWKw5Z`K}d`ZU*U#%UD|>jS@6V->9f zKi_u1Wa3;}eLcOni#`J4h=w{}n!EHpBLc3+UERh!p=hYyt0{Zk@2!bzMMJX}(U58&h`)(H!JD<~To&D^07K4b= z3Z9Je6ffHt?WwU}N%w+KuYIe?fEAX{q+QH1Ouw$C1t8F!Tuobo!2sW0eaILzTccGz z^JR%$O|Abd$AWMnQ1}{hMn1)NBrw=I!H?U{I9-cxv))wwxqz@4%=EkSr>>b@o=rCe zZSE-}%O0p(4%daFjnzbsI{9);=Oaw7H>;kNtbN7PRDgMh7qwQCzPfqoG&R)39*Ici zCqH9*<_tv|ILFS^iKQ81Qsfr4q{AhcN5WC6>ZbmEJ#rPR?8H>q|L^R~)sjcs|5;iw zKQz={&t1#hwg*;yzb1IFshI-T)4T{MBxF6UW+_;8-kG((zrh}-BkZ<|svto5XP#w|g4K%r*$?{acrK0h$!rM@6BOf1n-2Q6ngU%IZ znRHv81>A+wUiW|+dzR%WgZjI^+a5<|TCtvkJ&c&yuHH^ZKx^0u3}(YK-H&(%w~WYzadLZV5}4lw0$YUtw)aA>zkVr_ z3*jRmRRO8)UvKdDbJ7Qxm{-|B0iZS1+reE(llhJOCan*u2*JcB#yanyYlEd~v^q(W z41Ivcl4%R3bzajq=0aCHtzSb-k6=4MJHZ%PXYxsq`^8mFPY*b(Q)&p5IvU4J@*jk) z7VI_(pG+#3jNYMAt=P$JrrYvhX%79q44T7c987vOgSJ!l5ah(iBulk?-F9*Hy|XI$ zn}3l_g%##LtZ`}r!j4}pullZB>+`3;fbFua1G5a+MUPlfyhwH^hzsJN>${5@49pRt z6!jk{)i$i$&BKJO+71&dleV!5_{A3P$tj~j;J9kYWki%7fJ<|K>K42T){A7 zSRRen;8(7WOp&MKxMlnGm>`2jbj~-l zdl3AS(3(!wjrCyor@qhtGu-X7YBxV><1!vprWRyPqucx&^&X50{Pr7Pq@rC!H^1v$nZ6^TKf>! z>e!C2n`>M7H32Qmp4DG1-~TWVaWB-+U%VKQ&Nk|^t6x5+q0e`xhbULrwSx}s$$1#$ z_|$-5ahQ&(ovyTnSB^x+J$!C|sj1TkYrh)Ug;hs5b6BUsz5Y?!4z4P9W^!4qlq*NM zyV_YiDP-UMjQi}&6M7u#BCJg3wu}6}fldQpm#hWvX>5Nf@~HA%)LNue{)MEvmAh#d zxaG*9mXfP0Fu9xxmg@uc7>>SUGI5Yj9gY~`yW@0aI0XISI6uW$+3Q@ZO2fPE!iN;t z=zuDj&+h^8M&HIi9J$N=%?9c{LOP+_a-2L27QA!*1(GqaZ{9yRCu2cUvokXJBjgXdoA`y&A}Td$oh1fW6hgz~1U5qmhpNM|-Q? z#z^71vS(@X80@k0ILj@2Y;v>Ubw`(NL#ttDEK3271cZ&ED+11bZ0o-*&~)^AMTMjQgRQHj&FB=Fc5jpeKVk*rVBaL^Jr>Q)zQ{TgdIGH zl?u$gz+^efK5=(EH>|u8#Cm)!Whvpr@8mHKJ?J(tScr0N&iR9{u0OtJ!gxx-mmy&+Hq*+e(gB%uukK=t7;NDT%@u^?G#htoXlGFG z!@+CDX{>}ldeL;>$F@u|_|UNKP?4Q~1|PROmn`$LLd;AF76 z%*WcEe?&!(t67tc{vu7T9#qzUCmDSD1C4DZK9knyLF4Q1dnzI+!&$z2%oWIU>hT>` zqYD2)Q@=xNsYy@u^YXBS*X(`wE95>2zc^hLGzq$mzrx!;)h4BJ&0gt9@C;|W+`TmRt{wo8}g><-*D!H0jDqT z2RVxh(Je$1dl^2pJ-`*s55Gafzefr4Z&1Kw=xx^x9`--4wry*;yao9=U@H5v&)uN- zslfOX7;N&Z+4ap_$1=5TOcDG={I zFqjvqz2t6{W$D=~Oc*s_pDa0-xM_NkZVvVc>Q!2-B1MDtGjedjKZBqOs)|u?VFg$A zA2*V3!HrZfcU3~QErg_SWksfOSEiodKHSS7F!(;uYAQp$&c!?_xRO+o1ykPmNE_Cg9+1s+IN4?;Y4CJ(8TI?Md!|bU zb{g{9bhO)#h?7|_vAjFWH+FV9Thtf5B;QXL<9LcjBTi7}_ia3})F?izMMGyGadF0# zQfI)0&H0me)vHGb^vJro{D1`{t*L}Gs{vuo?a9X%T_@EYw8RA2{U>G5z`$}I7;N6$ zxzu+X#*Ou?W5ReuUNfP`LJz4e#=FvG9&%QdE`Qt-UiRZnj!4(OuqsWx8(2v?e6}yL z0nYr20a6{@&>qSF#eWPzSv+OsIL5Ar+t4*wHCKl^F&GMbU0fFV&cdqYI1o5Pz=W{i zzDdTY#3`kE1#kU@Uy(S}+TJtJ4#5W}|BVCw$WQWuXtHHgCQ%q`;yC zftkcQvr$?V%uLSDKKfP(ld!8Iyg zn&q8n5<&i=o%ubTx*auMgfts_mSeCvEh3Cv!0zE5xps$ zVSZXClqR&Dx*SF`@&&zTh|6B^t#I2e{3Tr)x&WgS;twShJNkk==Rk2U1;({u_c}^H z{N6{wP?D~Omz>tNdZb&~vd(1`h%yE@c}c?=ZC_wiN0r`W2E^Z*JI|G2@SZwZ&>p^R zTW?FNYX%e7O)u#PXg?Eh{{Av`E|`B1EZ&cJ@A&hhLxx?| zOoFv|MFEU95*SRdrzJO>bSV1thJv9~dj=rNr9Sr~K8(3h7MnYu5XHu}zM^@cHCzJ* z+Y7KFyZ)WYi!^IZ+$~xB5=|B^EX?h+LU9V4dz4O z{0n6>)8*)tY&(VP{vwX)^@snU)3$lThku9OG%mN$tcq&*{ymSPw-uXP zwabAUO_>)~20Kmad%E@`T4@O|lmolVzW>mx%Tf%qEaqnurCjPTXPa+_R{9!b9F_)| zzJE`tX}JR4As^@#o_*hb;8Tpf6V4p#{`>@bDRRk*sY}@$(&~gk?)mk(;Yz+!ft1ds zwhO`2HJid0g6C%h`(J8P=$1xD9TrLUpJAL6xaVUlX38n3)pAL#HspU~_Nit3y+x4Q zQcQ_HZ9nuOrT1d^>$w!V7DlQXlJ1|2s#fmr>T9y+JiI@x)Zc*fDQA=w9bp~%Zz$D% z^fzcY19<|2lwq`_Q)Z-{#|2yW+q`kK;?ICLqsNkh0TVlvQC-0cW7i$=+sQ>t;n*mJHjxk)Ri;NtdPcN zefI)|&6AgpO)BNSe(;x&O6)~CDj;8E_AC^CFo>T4RdT!MWSYDR&hi>C*gXCF-pyCn zt9*MCXxSbeuzvsui=P*_dpz=w+ABr@g3n&E4JoZU&}GmXCL=yzv;WLmaRW8Yo6ZIX zyd9%`A(PE&a4)3SDG^Op_NK1)Kb~QybXf8b%O17@vf{Vy>tlCD$_j)HESd^>>dk1q zfl&h7qc3g?+mJnm$JW z0j-GrC+O)K!%EALEz)|Kcx=*>-5N|T;J${f0p>AavW&>negV^+$KTw= z7I%Eml(_p#PwRl;`v4d$$Y1n2*3;sEV~i;SX5M%y!ZsU=3YHYNypya=wiI~wR$uj z>1M2qfJoH`zct6Oz?v6wAnWmP|NRFCfdy+QNV3eD3~M1qK3DRvBaE0rt+S>;hOq}c z*fieJxZsG@8mj{PEs3j~F}Zw_d83bG1fcRP|0t9qnE(*%!+BgQZMH z`dHb2rgjC_yWl!(DkZ!?def#hx=B+^f)>E717!bSlvQv+RBnglNP9M5o_n}7UD*H| z__RBpE6rPu9~gI_Y8$0dY=d?BM)V7#JvX~LqpsDOJiiHc^H;zd<`{kP%WV3f@5hB6 zpZ=@_VK{bZFC}!U+}xziLPzMuWTUyEhu%8 zfh)1}%x*;!$0r>DB}*rwG4t6Rk536R2sVxv1??xFgkh!PgA~Zi3htlTIIhYCc_xFG zZkB5PHQXP zn3cu6O|5(!jDst8Fy4BK-GNr-{-89{*&-9?TX0wH16t;79^8AlezImMa?|KB%6tqr zY-TZTC4M50QD~s@KhX)e3Z2@y{ePmsF?fG#4kcttDY^@;^gL7AR9jn#S9EJDTYty5 z!IENIwb@P!y@1GVhHZVmv^HDC5a+2%R?^Ycoszw8IiN6cMpaF`aIEvD>xwv->w5cC z1B4}h8unj0dStD*qkym=0OJzwl5yV!8>7?nQIzTx-yt z-=up2A*D#yy%G!cG=uj^)c>vCkvi;`D(f^F8n|C-G9o^tUm}B^p!09u1o;mLiHfbk z-o;OU`)fqN&8Ya0Bn7@$>b7?B7e}UXcv51Y1#d4Ww0hhxDL5*6P*iMa%O0oP9#vmo z*$LQeT%_!pO`_Mev8|fGYts7l!D!3Ji#wUY>D7Lz#E9)fInt%hUoYvh%DK#bJTp5R zY2VrUqg|~lr}4~e=eN3jIyM?u@Av6$e6e$O>hwwNlcxQ=0M9G~9ancj^iS@Ivo_(G z1=9^XR0{9@?d1)-@yuoGdiLWvqn7HBn zuYnzY3dJ+4&CR4yQ8uBiF2zowL%gcd#-4aJ`p{fF zm&EhSA)BJxwsIYlF7P|vI+-x-MxSy!@XQ3>u%`RiWgVxkJdI~o!MpdDd6(YOrQc0F zGl6$B*th;#E4@<1kH!db7F4mY79FgchC^9Os zf7p=3xW4_vk`il#pmp$+3nMfxDJ*uNF>G*raBS#+guZd1VaBAegqWz<;AraqOd3k9 zy2(XHU}2T@pQMPPiE*(}eT^Y;aWUb+iAlzUuuvq?gOlQ7@HR0zEG!;b^=P~vkQ|i| zmK2GnP-9{eeq!Pi;`)XqCK?9?C&Wg@MnJ@{!BI)Z=&<1a#_$ACM#aVf73g6JQNhts zL&Fjh6NAIU@QzyakgaJ~H@P%@(^Ix5*JXMq>f2NHwpCVGm|UUB@zLNHK7=Y>ap6KFCS7nyG%JEh$p3FljEoAWX{|9{PL0)D*|EnYFv|r^ zqVk*NO0@Hl-bJp7+>oa zjV?-7bhd}=NKN$mid40`T+RZ{$A`SE1y)S%#gP;6{O4VY(Gs4+CeKQcHzss=Nxh^VB<%o zYmDq48Xg@L5+4~C8y1@!6B3qS?3XCY@51{|N6~kHmKYU*ZeffCvz(`xwoelajAY!G>Uqc2AnpGtaXFE*S)8_Y_=5|WdmqLY)t zjdhFzVf*Y8@^ttO?4r+geI+_xTt2TC-5VuPhdN{WTrpIw1+N5<^S&^m|FZ-P zqz#7$F)~LJ6BZg3to)Ub6xTlt4u^RkG!Gm~lJelMyl4Hm2K$Rn`pad@B)8UUUYcbGuK<;H^3)98Udxo7xk4Y!h1mUnm`qQ zp*sj)5g!)TA09I}l%{^6FJCt2k$EQI=Xs{UjO}28oWNT}|}3 zTRTqA4j5rSjxSBSFI7;?R39CO?ryAsAIh#ISC-UA8qio@xxA?WbU~oyc{X}dhe~?a zs*o*jUuFEv-@zw&%~}s*%GJq%m+J}6?a33VVvpP&ffbqi*@*qdaP6>%=!9h6rNaio zw8LVek}yKnjE)NpPK@;Ei|T}hV)R0PkBLe|=Z_1HMn7PEw5&_scS>uTy`qsNFp{7d zt!QkR+?b{ehO?d<3a6h{ORn!1JUA+jX+!x%#e@e9kBg3u8^lImjIw=`62s#XVvNlH zK#IXbFr3E47%{HX$*LH|7Cw@iQu*GpoUyaFJXu@WoS5mxj2}}Wt-C0@jL7XlnH;$s zsu8;v_|WG+C^#|3m>ZUmSR*_-xNp9IxQGbjz_^5vs6(z|-+Jo2#| zWB`eAo$YH?n%h5#qlG0T#3j&37rC5Mt{1E^6i6bfkIhRo^19^Q(_BU%nZJTqSfo-M zOuky-gQ_x68RLT!5Cn-z)5l)2bJ^S)VBB*l1f64Kmfp41XV`*54BBi@u8*Reu*AsV zp*5H{8x)t&AHh~s3_>X5kg!;)5GxzXn4bHBJl7Ki{~}i^=Wk9ABoi}woFZ2$o4YoO z2pEM$<&56+!2NKvWuUIaZ6<`E`7tiy88te6y%9xA>2&M-x zXJdg=Zu`nX;O0grE#yib`KruhpP(^47(HjLlPgxtmxS3mklBwBX#(4e{{_=K(?AgsAu=flx!>#YZPcMAeSX2Z38w zZnRjVXj(LXTtbpDk(`$yMDbjJfu+$}S!!Sg1`ew8Sq_X1+t&#nVzEGOABEXO6nbgj zNZ1Su(`xifjEhf-ii^d(ApzYlD&80zn;4Z8HW+~e8}<4o#9^Y47#}<+HX=GMgqaLH zZ&XZj0{)AH+ZDwl+=veojyNVPA%cc2kQ>_gGfTp#P$N9iV^DB(|BS78TZpC<*IUxA zds2xE`HEz#rQvC^ZHBZ+o};Blon#y8`V`T1nLiMf*ZxD=PEJ=5sV`bA52gDaauMoi zlrQAroQPQuxqHa(7$d6;mnCv99qo80)uVMk$;U}9FL%k9vrHbXrB1KFsq6}QB6IWp zT(fId$iLEyFJ(WHjR@u2R+ZB-9$L!bS{m{|@}~Heas!I%q%Tap9OPjct5?bi7IsWW zh$)6(cu7o-XN{F;qzVywXWI2vl4;&gdV6Z{P70>%EBaEjs*Jpqg38N!urwJ4){KJN!cp|#=?jBIL|4_IjyHPZGsC|vRgVR}O&k}y|cVm2)R1E!71 z85=rHRN`GO;h_plA1L#*?81T#OJg2xx#b0;+>Z(m(k94=Kyvpar7*Qnj#T2BRG~aC z$jHKVHWcK3WF4DM+F%xzV`?X(j)?H4hgh;^1Xu(+h z)2O7Sn5T&kPKsnPIg3gv*s!_*O???gB_z30vBYR}EWB2Jap5My64ov?@~9UEY>fn^O7BL2{{> zGarS-Y+7o>CLTUP;8WGexUl{-;FV;sRWR8jFxii>t}>aanD0CnIO>#F87iio!Ng+9 zalK@plePg$t|>NvQYAY2K`KTLZ={9Gp1qN@^!7Xw2eaP+aLEFH1ufK<&Up1sDlF+E z!r;S|bVi0`A>Y!{iPD%IuFjI|GWuD`6D{bSBzt7^Eh<~Y0t;kw&y0xj@pxCxJXkC^gQo36yv0=a%BrD`ls}moWGLw86&#LSG6o-;>6M`ndEm-l32>+7E8p`pq?SJA!A@K*`Osi zcc`RYkX)Qiwe{o|C|hg2QyXG#4WtHi=owg`6Is#GFu6E+x*!|#)nmPZK7`4RP}Zi{q8)J-xXjMbND*$&*ezkp^cRDW*5Bz{kWef~B{Y-RNmw z=97X@j}PtTl63qb#>}ff>ZJ^AhWT(Sdj@wShY$J* Sdd&y^a>9p;1a+>wUei_qE*jIj#7k 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 z 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®*K> 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 z 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 C1{i7p From 3017f48dbfc2cc40081292f08e5c46a98e435b09 Mon Sep 17 00:00:00 2001 From: Luis Otavio Date: Sat, 14 Dec 2024 18:27:16 -0300 Subject: [PATCH 07/75] Refactor imports in classifier, audio_utils, and memory modules for improved organization --- .../ai_api/eda_ai_api/api/routes/classifier.py | 18 +++++++----------- apps/ai_api/eda_ai_api/utils/audio_utils.py | 4 +++- apps/ai_api/eda_ai_api/utils/memory.py | 5 +++-- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/apps/ai_api/eda_ai_api/api/routes/classifier.py b/apps/ai_api/eda_ai_api/api/routes/classifier.py index 4cdc13b..4123ea5 100644 --- a/apps/ai_api/eda_ai_api/api/routes/classifier.py +++ b/apps/ai_api/eda_ai_api/api/routes/classifier.py @@ -1,25 +1,21 @@ import os -from typing import Any, Dict, Optional import uuid +from typing import Any, Dict, Optional from fastapi import APIRouter, File, Form, UploadFile from llama_index.core import PromptTemplate from llama_index.llms.groq import Groq from loguru import logger +from onboarding.crew import OnboardingCrew +from opportunity_finder.crew import OpportunityFinderCrew +from proposal_writer.crew import ProposalWriterCrew from eda_ai_api.models.classifier import ClassifierResponse from eda_ai_api.utils.audio_utils import process_audio_file from eda_ai_api.utils.memory import ZepConversationManager -from eda_ai_api.utils.prompts import ( - ROUTER_TEMPLATE, - TOPIC_TEMPLATE, - PROPOSAL_TEMPLATE, - INSUFFICIENT_TEMPLATES, -) - -from onboarding.crew import OnboardingCrew -from opportunity_finder.crew import OpportunityFinderCrew -from proposal_writer.crew import ProposalWriterCrew +from eda_ai_api.utils.prompts import (INSUFFICIENT_TEMPLATES, + PROPOSAL_TEMPLATE, ROUTER_TEMPLATE, + TOPIC_TEMPLATE) router = APIRouter() diff --git a/apps/ai_api/eda_ai_api/utils/audio_utils.py b/apps/ai_api/eda_ai_api/utils/audio_utils.py index 7007e32..b94bcbd 100644 --- a/apps/ai_api/eda_ai_api/utils/audio_utils.py +++ b/apps/ai_api/eda_ai_api/utils/audio_utils.py @@ -1,7 +1,9 @@ import os import tempfile -from typing import Optional, Dict +from typing import Dict, Optional + from fastapi import UploadFile + from .audio_converter import convert_ogg from .transcriber import transcribe_audio diff --git a/apps/ai_api/eda_ai_api/utils/memory.py b/apps/ai_api/eda_ai_api/utils/memory.py index 015d082..21e862e 100644 --- a/apps/ai_api/eda_ai_api/utils/memory.py +++ b/apps/ai_api/eda_ai_api/utils/memory.py @@ -1,8 +1,9 @@ import uuid -from typing import Optional, Dict, List +from typing import Dict, List, Optional + +from loguru import logger from zep_python.client import AsyncZep from zep_python.types import Message -from loguru import logger class ZepConversationManager: From b9d04eca2832fefed1527b22b2160a02e1d4354d Mon Sep 17 00:00:00 2001 From: Luis Otavio Date: Sat, 14 Dec 2024 18:32:02 -0300 Subject: [PATCH 08/75] Integrated AI api --- apps/whatsapp/src/message/message.ts | 47 ++++++++++++++++++++++------ 1 file changed, 38 insertions(+), 9 deletions(-) diff --git a/apps/whatsapp/src/message/message.ts b/apps/whatsapp/src/message/message.ts index 097a15a..6b637a4 100644 --- a/apps/whatsapp/src/message/message.ts +++ b/apps/whatsapp/src/message/message.ts @@ -1,7 +1,7 @@ import type { WAMessage } from "@whiskeysockets/baileys"; import { sock } from "../client"; import { BOT_PREFIX } from "../constants"; -import { react } from "../utils/whatsapp"; +import { getPhoneNumber, react } from "../utils/whatsapp"; export async function handleMessage(message: WAMessage) { await react(message, "working"); @@ -11,7 +11,6 @@ export async function handleMessage(message: WAMessage) { return console.error("Invalid chat ID"); } - const isGroup = chatId.endsWith("@g.us"); const streamingReply = await sock.sendMessage( chatId, { text: "..." }, @@ -20,16 +19,44 @@ export async function handleMessage(message: WAMessage) { if (!streamingReply) return console.error("No streaming reply"); - let response: string; + try { + // Extract message content and phone number + const messageContent = + message.message?.conversation || + message.message?.extendedTextMessage?.text || + ""; + const phoneNumber = getPhoneNumber(message); - response = "Hello World!"; + // Create FormData + const formData = new FormData(); + formData.append("message", messageContent); + formData.append("session_id", phoneNumber); - try { - if (!response) return "No response found"; + // Make API request + const response = await fetch( + "http://127.0.0.1:8083/api/classifier/classify", + { + method: "POST", + headers: { + accept: "application/json", + }, + body: formData, + }, + ); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + + if (!data.result) return "No response found"; + + console.log(data.result); await sock.sendMessage( chatId, - { text: response, edit: streamingReply.key }, + { text: data.result, edit: streamingReply.key }, { quoted: message }, ); @@ -37,8 +64,10 @@ export async function handleMessage(message: WAMessage) { } catch (error) { console.error(error); - const errorReply = await sock.sendMessage(chatId, { - text: BOT_PREFIX + (error as Error).message, + const errorMessage = + error instanceof Error ? error.message : "An error occurred"; + await sock.sendMessage(chatId, { + text: BOT_PREFIX + errorMessage, }); await react(message, "error"); From 2c16eb5c3e07bb663a31a50eaf0a78550d23a01d Mon Sep 17 00:00:00 2001 From: Luis Otavio Date: Sat, 14 Dec 2024 18:47:39 -0300 Subject: [PATCH 09/75] Audio and image handling --- apps/whatsapp/src/client.ts | 2 +- apps/whatsapp/src/commands/commands_index.ts | 6 +-- apps/whatsapp/src/message/message.ts | 39 ++++++++++++++++++- .../src/{utils/whatsapp.ts => utils.ts} | 6 +-- 4 files changed, 42 insertions(+), 11 deletions(-) rename apps/whatsapp/src/{utils/whatsapp.ts => utils.ts} (98%) diff --git a/apps/whatsapp/src/client.ts b/apps/whatsapp/src/client.ts index ea42284..2d1fc79 100644 --- a/apps/whatsapp/src/client.ts +++ b/apps/whatsapp/src/client.ts @@ -17,7 +17,7 @@ import { shouldIgnore, shouldIgnoreUnread, shouldReply, -} from "./utils/whatsapp"; +} from "./utils"; const messageQueue: { [key: string]: proto.IWebMessageInfo[] } = {}; let isProcessingMessage = false; diff --git a/apps/whatsapp/src/commands/commands_index.ts b/apps/whatsapp/src/commands/commands_index.ts index 0cd9a50..3bd8b12 100644 --- a/apps/whatsapp/src/commands/commands_index.ts +++ b/apps/whatsapp/src/commands/commands_index.ts @@ -2,11 +2,7 @@ import type { WASocket, proto } from "@whiskeysockets/baileys"; import { stripIndents } from "common-tags"; import { sock } from "../client"; import { BOT_PREFIX, CMD_PREFIX } from "../constants"; -import { - isGroupMessage, - react, - unauthorizedCommandFor, -} from "../utils/whatsapp"; +import { isGroupMessage, react, unauthorizedCommandFor } from "../utils"; import { handleHelp } from "./help"; const adminCommands = ["jailbreak", "reset", "change"]; diff --git a/apps/whatsapp/src/message/message.ts b/apps/whatsapp/src/message/message.ts index 6b637a4..47065b5 100644 --- a/apps/whatsapp/src/message/message.ts +++ b/apps/whatsapp/src/message/message.ts @@ -1,7 +1,10 @@ -import type { WAMessage } from "@whiskeysockets/baileys"; +import { type WAMessage, downloadMediaMessage } from "@whiskeysockets/baileys"; import { sock } from "../client"; import { BOT_PREFIX } from "../constants"; -import { getPhoneNumber, react } from "../utils/whatsapp"; +import { getPhoneNumber, react } from "../utils"; + +const IMAGE_ERROR_MSG = + "Desculpe, ainda nΓ£o tenho suporte para processar imagens. Por favor, envie apenas mensagens de texto ou Γ‘udio."; export async function handleMessage(message: WAMessage) { await react(message, "working"); @@ -27,11 +30,43 @@ export async function handleMessage(message: WAMessage) { ""; const phoneNumber = getPhoneNumber(message); + let audioBlob: Blob | undefined; + + if (message.message?.imageMessage || message.message?.audioMessage) { + const media = await downloadMediaMessage(message, "buffer", {}); + const mimetype = + message.message?.imageMessage?.mimetype || + message.message?.audioMessage?.mimetype; + + const isImage = mimetype?.includes("image"); + const isAudio = mimetype?.includes("audio"); + + // Early return if image is detected + if (isImage) { + await sock.sendMessage( + chatId, + { text: IMAGE_ERROR_MSG, edit: streamingReply.key }, + { quoted: message }, + ); + await react(message, "done"); + return; + } + + if (isAudio && mimetype) { + audioBlob = new Blob([media], { type: mimetype }); + } + } + // Create FormData const formData = new FormData(); formData.append("message", messageContent); formData.append("session_id", phoneNumber); + // Add audio if present + if (audioBlob) { + formData.append("audio", audioBlob, "audio.ogg"); + } + // Make API request const response = await fetch( "http://127.0.0.1:8083/api/classifier/classify", diff --git a/apps/whatsapp/src/utils/whatsapp.ts b/apps/whatsapp/src/utils.ts similarity index 98% rename from apps/whatsapp/src/utils/whatsapp.ts rename to apps/whatsapp/src/utils.ts index 6dfa4cc..2d2b2f4 100644 --- a/apps/whatsapp/src/utils/whatsapp.ts +++ b/apps/whatsapp/src/utils.ts @@ -1,7 +1,7 @@ import type { proto } from "@whiskeysockets/baileys"; import { stripIndents } from "common-tags"; -import { sock } from "../client"; -import { helpStatement } from "../commands/commands_index"; +import { sock } from "./client"; +import { helpStatement } from "./commands/commands_index"; import { ALLOWED_USERS, BLOCKED_USERS, @@ -9,7 +9,7 @@ import { CMD_PREFIX, ENABLE_REACTIONS, IGNORE_MESSAGES_WARNING, -} from "../constants"; +} from "./constants"; export function isGroupMessage(message: proto.IWebMessageInfo) { return message.key.remoteJid?.endsWith("@g.us") ?? false; From 3bc40e59e76bdd3563e9eb75224e2103190046bb Mon Sep 17 00:00:00 2001 From: Luis Otavio Date: Sat, 14 Dec 2024 20:45:34 -0300 Subject: [PATCH 10/75] Process response from crewai --- apps/ai_api/.gitignore | 4 ++ .../eda_ai_api/api/routes/classifier.py | 53 +++++++++++++++---- apps/ai_api/eda_ai_api/utils/prompts.py | 27 ++++++++++ apps/whatsapp/src/message/message.ts | 6 +++ 4 files changed, 80 insertions(+), 10 deletions(-) diff --git a/apps/ai_api/.gitignore b/apps/ai_api/.gitignore index dce0f05..19787df 100644 --- a/apps/ai_api/.gitignore +++ b/apps/ai_api/.gitignore @@ -111,3 +111,7 @@ ENV/ # mypy .mypy_cache/ .idea/ + +# CSV Reports +*.csv +report_*.csv \ No newline at end of file diff --git a/apps/ai_api/eda_ai_api/api/routes/classifier.py b/apps/ai_api/eda_ai_api/api/routes/classifier.py index 4123ea5..4df8dcc 100644 --- a/apps/ai_api/eda_ai_api/api/routes/classifier.py +++ b/apps/ai_api/eda_ai_api/api/routes/classifier.py @@ -13,9 +13,13 @@ from eda_ai_api.models.classifier import ClassifierResponse from eda_ai_api.utils.audio_utils import process_audio_file from eda_ai_api.utils.memory import ZepConversationManager -from eda_ai_api.utils.prompts import (INSUFFICIENT_TEMPLATES, - PROPOSAL_TEMPLATE, ROUTER_TEMPLATE, - TOPIC_TEMPLATE) +from eda_ai_api.utils.prompts import ( + INSUFFICIENT_TEMPLATES, + PROPOSAL_TEMPLATE, + ROUTER_TEMPLATE, + TOPIC_TEMPLATE, + RESPONSE_PROCESSOR_TEMPLATE, +) router = APIRouter() @@ -47,6 +51,13 @@ async def extract_proposal_details(message: str, history: list) -> tuple[str, st return community_project, grant_call +async def process_llm_response(message: str, response: str) -> str: + processed = llm.complete( + RESPONSE_PROCESSOR_TEMPLATE.format(original_message=message, response=response) + ) + return processed.text + + async def process_decision( decision: str, message: str, history: list ) -> Dict[str, Any]: @@ -64,7 +75,15 @@ async def process_decision( context=context, message=message ) ) - return {"response": response.text} + processed_response = await process_llm_response(message, response.text) + return {"response": processed_response} + + # Add return for successful discovery + crew_result = ( + OpportunityFinderCrew().crew().kickoff(inputs={"topics": ", ".join(topics)}) + ) + processed_response = await process_llm_response(message, str(crew_result)) + return {"response": processed_response} elif decision == "proposal": community_project, grant_call = await extract_proposal_details(message, history) @@ -77,13 +96,20 @@ async def process_decision( context=context, message=message ) ) - return {"response": response.text} + processed_response = await process_llm_response(message, response.text) + return {"response": processed_response} elif decision == "heartbeat": - return {"is_alive": True} + processed_response = await process_llm_response( + message, str({"is_alive": True}) + ) + return {"response": processed_response} elif decision == "onboarding": - return OnboardingCrew().crew().kickoff() + result = OnboardingCrew().crew().kickoff() + processed_response = await process_llm_response(message, str(result)) + logger.info(f"Processed onboarding response: {processed_response}") + return {"response": processed_response} else: return {"error": f"Unknown decision type: {decision}"} @@ -137,10 +163,17 @@ async def classifier_route( # Process decision and store conversation result = await process_decision(decision, combined_message, history) - await zep.add_conversation(session_id, combined_message, str(result)) - return ClassifierResponse(result=str(result), session_id=session_id) + # Process final result if it's not already processed + if isinstance(result.get("response"), str): + final_result = await process_llm_response(combined_message, str(result)) + else: + final_result = str(result) + + await zep.add_conversation(session_id, combined_message, final_result) + return ClassifierResponse(result=final_result, session_id=session_id) except Exception as e: logger.error(f"Error in classifier route: {str(e)}") - return ClassifierResponse(result=f"Error: {str(e)}") + error_msg = await process_llm_response(combined_message, f"Error: {str(e)}") + return ClassifierResponse(result=error_msg) diff --git a/apps/ai_api/eda_ai_api/utils/prompts.py b/apps/ai_api/eda_ai_api/utils/prompts.py index 0e7534b..6f5948e 100644 --- a/apps/ai_api/eda_ai_api/utils/prompts.py +++ b/apps/ai_api/eda_ai_api/utils/prompts.py @@ -70,3 +70,30 @@ Response:""" ), } + +RESPONSE_PROCESSOR_TEMPLATE = PromptTemplate( + """IMPORTANT: You must respond in exactly the same language as the user's original message: +{original_message} + +Process this response to: +1. MATCH THE EXACT LANGUAGE of the input message (this is crucial!) +2. Be clear and conversational in that language +3. Not exceed 2000 characters (summarize if longer) +4. Use WhatsApp formatting: + - *bold* for important terms + - _italic_ for emphasis + - ```code``` for technical terms + - ~strikethrough~ for corrections + - Lists with emoji bullets + - For URLs: Write "Link: " followed by the plain URL (no markdown) + Example: "Link: https://example.com" +5. If response looks like JSON, convert to natural language in the user's language: + - For heartbeat: "*Yes, I'm here! 🟒*\n_Ready to help you!_" (translate to match user's language) + - For errors: "⚠️ *Error*: _[error message]_" (translate to match user's language) + - For other JSON: Convert to organized message with formatting (in user's language) + +Original response: +{response} + +Respond in the SAME LANGUAGE as the original message with WhatsApp formatting:""" +) diff --git a/apps/whatsapp/src/message/message.ts b/apps/whatsapp/src/message/message.ts index 47065b5..71200e1 100644 --- a/apps/whatsapp/src/message/message.ts +++ b/apps/whatsapp/src/message/message.ts @@ -68,6 +68,9 @@ export async function handleMessage(message: WAMessage) { } // Make API request + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 600000); // 10 minute timeout (10 * 60 * 1000 ms) + const response = await fetch( "http://127.0.0.1:8083/api/classifier/classify", { @@ -76,9 +79,12 @@ export async function handleMessage(message: WAMessage) { accept: "application/json", }, body: formData, + signal: controller.signal, }, ); + clearTimeout(timeoutId); // Clear timeout if request completes + if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } From b8df9b11d3f972461eb78363a80c263fda823cf2 Mon Sep 17 00:00:00 2001 From: Luis Otavio Date: Sat, 14 Dec 2024 20:56:41 -0300 Subject: [PATCH 11/75] Translate command responses and error messages to Portuguese for better localization --- apps/whatsapp/src/commands/commands_index.ts | 2 +- apps/whatsapp/src/commands/help.ts | 18 +++++----- apps/whatsapp/src/commands/invalid_command.ts | 10 +++--- apps/whatsapp/src/message/message.ts | 34 ++++++++++++++----- 4 files changed, 41 insertions(+), 23 deletions(-) diff --git a/apps/whatsapp/src/commands/commands_index.ts b/apps/whatsapp/src/commands/commands_index.ts index 3bd8b12..eee8606 100644 --- a/apps/whatsapp/src/commands/commands_index.ts +++ b/apps/whatsapp/src/commands/commands_index.ts @@ -53,7 +53,7 @@ export async function handleCommand(message: proto.IWebMessageInfo) { break; default: reply = stripIndents` - ${BOT_PREFIX}Unknown command _"${CMD_PREFIX + command}"_ + ${BOT_PREFIX}Comando desconhecido: _"${CMD_PREFIX + command}"_ ${helpStatement}`; break; diff --git a/apps/whatsapp/src/commands/help.ts b/apps/whatsapp/src/commands/help.ts index 5d4fb41..ed1a7f0 100644 --- a/apps/whatsapp/src/commands/help.ts +++ b/apps/whatsapp/src/commands/help.ts @@ -24,22 +24,22 @@ export async function handleHelp(message: proto.IWebMessageInfo, args: string) { return reply; } -const helpMessage = stripIndents`${BOT_PREFIX}Available commands: +const helpMessage = stripIndents`${BOT_PREFIX}Comandos disponΓ­veis: -πŸ†˜ *${CMD_PREFIX}help __* -Displays the available commands, their functionalities and how to use them. -- Run *${CMD_PREFIX}help __* for more information about a specific command. +πŸ†˜ *${CMD_PREFIX}help __* +Mostra os comandos disponΓ­veis e como usΓ‘-los. +- Digite *${CMD_PREFIX}help __* para mais informaçáes sobre um comando especΓ­fico. πŸ“ *${CMD_PREFIX}ping* -Checks if the bot is alive by responding with '*_pong!_*'. +Verifica se o bot estΓ‘ funcionando respondendo com '*_pong!_*'. `; -const helpHelpMessage = stripIndents`I see what you did there. +const helpHelpMessage = stripIndents`Olha sΓ³, que esperto! -That's pretty meta, but I'm not gonna help you with that. +NΓ£o vou te ajudar com isso nΓ£o, vocΓͺ jΓ‘ sabe o que estΓ‘ fazendo. -Smart ass. +Espertinho(a) 😏 `; const pingHelpMessage = stripIndents`πŸ“ *${CMD_PREFIX}ping* -Checks if the bot is alive by responding with '*_pong!_*'.`; +Verifica se o bot estΓ‘ funcionando respondendo com '*_pong!_*'.`; diff --git a/apps/whatsapp/src/commands/invalid_command.ts b/apps/whatsapp/src/commands/invalid_command.ts index e395dd4..ccc1f09 100644 --- a/apps/whatsapp/src/commands/invalid_command.ts +++ b/apps/whatsapp/src/commands/invalid_command.ts @@ -1,21 +1,21 @@ import { stripIndents } from "common-tags"; import { BOT_PREFIX, CMD_PREFIX } from "../constants"; -export const helpStatement = stripIndents`Run *_${CMD_PREFIX}help_* to see the available commands.`; +export const helpStatement = stripIndents`Digite *_${CMD_PREFIX}help_* para ver os comandos disponΓ­veis.`; export function invalidArgumentMessage(args: string, usage?: string) { - return stripIndents`${BOT_PREFIX}Invalid argument _"${args}"_ + return stripIndents`${BOT_PREFIX}Argumento invΓ‘lido: _"${args}"_ - ${usage ? `Usage: *_${CMD_PREFIX}${usage}_*\n` : ""} + ${usage ? `Uso correto: *_${CMD_PREFIX}${usage}_*\n` : ""} ${helpStatement} `; } export function unauthorizedCommandFor(command: string) { return stripIndents` -${BOT_PREFIX}Unauthorized: You are not an admin in this group. +${BOT_PREFIX}Acesso negado: VocΓͺ nΓ£o Γ© admin neste grupo. -Only admins can run the *${command}* command +Apenas admins podem usar o comando *${command}* ${helpStatement}`; } diff --git a/apps/whatsapp/src/message/message.ts b/apps/whatsapp/src/message/message.ts index 71200e1..bde4d35 100644 --- a/apps/whatsapp/src/message/message.ts +++ b/apps/whatsapp/src/message/message.ts @@ -4,7 +4,17 @@ import { BOT_PREFIX } from "../constants"; import { getPhoneNumber, react } from "../utils"; const IMAGE_ERROR_MSG = - "Desculpe, ainda nΓ£o tenho suporte para processar imagens. Por favor, envie apenas mensagens de texto ou Γ‘udio."; + "NΓ£o consigo processar imagens no momento. Por favor, me envie apenas mensagens de texto ou Γ‘udio."; + +const WAITING_MSG = + "Estou analisando sua mensagem... Como preciso pensar com cuidado, pode demorar alguns minutos."; + +const ERROR_MESSAGES = { + NO_RESPONSE: "Desculpe, nΓ£o consegui gerar uma resposta. Tente novamente.", + HTTP_ERROR: "Ops, tive um problema tΓ©cnico. Pode tentar novamente?", + TIMEOUT: "Desculpe, demorei muito para responder. Pode tentar novamente?", + UNKNOWN: "Ocorreu um erro inesperado. Pode tentar novamente?", +}; export async function handleMessage(message: WAMessage) { await react(message, "working"); @@ -16,7 +26,7 @@ export async function handleMessage(message: WAMessage) { const streamingReply = await sock.sendMessage( chatId, - { text: "..." }, + { text: WAITING_MSG }, { quoted: message }, ); @@ -91,7 +101,7 @@ export async function handleMessage(message: WAMessage) { const data = await response.json(); - if (!data.result) return "No response found"; + if (!data.result) return ERROR_MESSAGES.NO_RESPONSE; console.log(data.result); @@ -104,12 +114,20 @@ export async function handleMessage(message: WAMessage) { await react(message, "done"); } catch (error) { console.error(error); - const errorMessage = - error instanceof Error ? error.message : "An error occurred"; - await sock.sendMessage(chatId, { - text: BOT_PREFIX + errorMessage, - }); + error instanceof Error + ? error.message.includes("timeout") + ? ERROR_MESSAGES.TIMEOUT + : error.message.includes("HTTP") + ? ERROR_MESSAGES.HTTP_ERROR + : ERROR_MESSAGES.UNKNOWN + : ERROR_MESSAGES.UNKNOWN; + + await sock.sendMessage( + chatId, + { text: errorMessage, edit: streamingReply.key }, + { quoted: message }, + ); await react(message, "error"); } From af2debb55786f30091b473214d809ff2836a9903 Mon Sep 17 00:00:00 2001 From: Luis Otavio Date: Sat, 14 Dec 2024 21:28:40 -0300 Subject: [PATCH 12/75] Linting --- .../eda_ai_api/api/routes/classifier.py | 27 +++++++++++---- apps/ai_api/eda_ai_api/utils/audio_utils.py | 9 +++-- apps/ai_api/eda_ai_api/utils/logger.py | 2 +- apps/ai_api/eda_ai_api/utils/memory.py | 7 ++-- apps/ai_api/eda_ai_api/utils/prompts.py | 33 ++++++++++--------- 5 files changed, 51 insertions(+), 27 deletions(-) diff --git a/apps/ai_api/eda_ai_api/api/routes/classifier.py b/apps/ai_api/eda_ai_api/api/routes/classifier.py index 4df8dcc..1051860 100644 --- a/apps/ai_api/eda_ai_api/api/routes/classifier.py +++ b/apps/ai_api/eda_ai_api/api/routes/classifier.py @@ -16,9 +16,8 @@ from eda_ai_api.utils.prompts import ( INSUFFICIENT_TEMPLATES, PROPOSAL_TEMPLATE, - ROUTER_TEMPLATE, - TOPIC_TEMPLATE, RESPONSE_PROCESSOR_TEMPLATE, + TOPIC_TEMPLATE, ) router = APIRouter() @@ -99,6 +98,15 @@ async def process_decision( processed_response = await process_llm_response(message, response.text) return {"response": processed_response} + # Add crew result and return + crew_result = ( + ProposalWriterCrew() + .crew() + .kickoff(inputs={"project": community_project, "grant": grant_call}) + ) + processed_response = await process_llm_response(message, str(crew_result)) + return {"response": processed_response} + elif decision == "heartbeat": processed_response = await process_llm_response( message, str({"is_alive": True}) @@ -123,13 +131,15 @@ async def classifier_route( ) -> ClassifierResponse: """Main route handler with conversation memory""" try: - logger.info( - f"New request - Session: {session_id}, User: {session_id + uuid.uuid4().hex}" - ) + # Generate a default session_id if none provided + current_session_id = session_id or str(uuid.uuid4()) + user_id = f"{current_session_id}_{uuid.uuid4().hex}" + + logger.info(f"New request - Session: {current_session_id}, User: {user_id}") zep = ZepConversationManager() session_id = await zep.get_or_create_session( - user_id=session_id + uuid.uuid4().hex, session_id=session_id + user_id=user_id, session_id=current_session_id ) # Process inputs @@ -153,7 +163,10 @@ async def classifier_route( context = "\n".join([f"{msg['role']}: {msg['content']}" for msg in history]) router_prompt = PromptTemplate( - """Previous conversation:\n{context}\n\nGiven the current user message, determine the appropriate service:\n{message}\n\nReturn only one word (discovery/proposal/onboarding/heartbeat):""" + """Previous conversation:\n{context}\n\n""" + """Given the current user message, determine the appropriate service:""" + """\n{message}\n\n""" + """Return only one word (discovery/proposal/onboarding/heartbeat):""" ) response = llm.complete( diff --git a/apps/ai_api/eda_ai_api/utils/audio_utils.py b/apps/ai_api/eda_ai_api/utils/audio_utils.py index b94bcbd..35eac25 100644 --- a/apps/ai_api/eda_ai_api/utils/audio_utils.py +++ b/apps/ai_api/eda_ai_api/utils/audio_utils.py @@ -1,6 +1,6 @@ import os import tempfile -from typing import Dict, Optional +from typing import Optional from fastapi import UploadFile @@ -40,11 +40,16 @@ async def process_audio_file(audio: UploadFile) -> str: content = await audio.read() try: + # Add null check and default to mp3 + file_format = "mp3" + if content_type is not None: + file_format = ALLOWED_FORMATS.get(content_type, "mp3") + if content_type == "audio/ogg": audio_path = convert_ogg(content, output_format="mp3") else: with tempfile.NamedTemporaryFile( - suffix=f".{ALLOWED_FORMATS.get(content_type, 'mp3')}", delete=False + suffix=f".{file_format}", delete=False ) as temp_file: temp_file.write(content) audio_path = temp_file.name diff --git a/apps/ai_api/eda_ai_api/utils/logger.py b/apps/ai_api/eda_ai_api/utils/logger.py index 579d267..280ef71 100644 --- a/apps/ai_api/eda_ai_api/utils/logger.py +++ b/apps/ai_api/eda_ai_api/utils/logger.py @@ -2,5 +2,5 @@ from loguru import logger -def setup_logging(): +def setup_logger() -> None: logger.add("api.log", rotation="500 MB") diff --git a/apps/ai_api/eda_ai_api/utils/memory.py b/apps/ai_api/eda_ai_api/utils/memory.py index 21e862e..c821a82 100644 --- a/apps/ai_api/eda_ai_api/utils/memory.py +++ b/apps/ai_api/eda_ai_api/utils/memory.py @@ -20,9 +20,11 @@ async def get_or_create_session( if session_id: try: await self.client.memory.get(session_id=session_id) + logger.info(f"Found existing session: {session_id}") return session_id - except Exception: - pass + except Exception as e: + logger.warning(f"Failed to get session {session_id}: {str(e)}") + # Continue to create new session user_id = user_id or uuid.uuid4().hex await self.client.user.add( @@ -35,6 +37,7 @@ async def get_or_create_session( ) await self.client.memory.add(session_id=new_session_id, messages=[]) + logger.info(f"Created new session: {new_session_id}") return new_session_id diff --git a/apps/ai_api/eda_ai_api/utils/prompts.py b/apps/ai_api/eda_ai_api/utils/prompts.py index 6f5948e..732b3a0 100644 --- a/apps/ai_api/eda_ai_api/utils/prompts.py +++ b/apps/ai_api/eda_ai_api/utils/prompts.py @@ -45,11 +45,11 @@ {context} The user wants to write a proposal but hasn't provided enough information. -Generate a friendly response in the same language as the user's message asking for: -1. The project name and brief description -2. The specific grant program they're applying to (if any) -3. The main objectives of their project -4. The target community or region +Generate a friendly response asking for: +1. Project name and brief description +2. Specific grant program (if any) +3. Main project objectives +4. Target community/region User message: {message} @@ -59,11 +59,11 @@ """Previous conversation: {context} -The user wants to find grant opportunities but hasn't provided enough information. -Generate a friendly response in the same language as the user's message asking for: -1. The main topics or areas of their project -2. The target region or community -3. Any specific funding requirements or preferences +The user wants to find grant opportunities but needs more information. +Generate a friendly response asking for: +1. Main project topics/areas +2. Target region/community +3. Funding requirements/preferences User message: {message} @@ -72,7 +72,8 @@ } RESPONSE_PROCESSOR_TEMPLATE = PromptTemplate( - """IMPORTANT: You must respond in exactly the same language as the user's original message: + """IMPORTANT: You must respond in exactly the same language as the user's + original message: {original_message} Process this response to: @@ -85,11 +86,13 @@ - ```code``` for technical terms - ~strikethrough~ for corrections - Lists with emoji bullets - - For URLs: Write "Link: " followed by the plain URL (no markdown) - Example: "Link: https://example.com" + - For URLs: + Write "Link: " followed by URL + Example: + Link: http://example.com 5. If response looks like JSON, convert to natural language in the user's language: - - For heartbeat: "*Yes, I'm here! 🟒*\n_Ready to help you!_" (translate to match user's language) - - For errors: "⚠️ *Error*: _[error message]_" (translate to match user's language) + - For heartbeat: "*Yes, I'm here! 🟒*\n_Ready to help you!_" + - For errors: "⚠️ *Error*: _[error message]_" - For other JSON: Convert to organized message with formatting (in user's language) Original response: From 1a0a71162d5e2b5010c0f7849752a9ac58130aa3 Mon Sep 17 00:00:00 2001 From: Luis Otavio Date: Sat, 14 Dec 2024 21:40:49 -0300 Subject: [PATCH 13/75] Ignored some sherif linting rules --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 1c49fef..c3f968b 100644 --- a/package.json +++ b/package.json @@ -27,8 +27,8 @@ "start:dashboard": "turbo start --filter=@eda/dashboard", "test": "turbo test --parallel", "format": "biome format --write .", - "lint:repo": "bunx sherif@latest", - "lint:repo:fix": "bunx sherif@latest --fix", + "lint:repo": "bunx sherif@latest -r unordered-dependencies -r multiple-dependency-versions", + "lint:repo:fix": "bunx sherif@latest -r unordered-dependencies -r multiple-dependency-versions --fix", "typecheck": "turbo typecheck" }, "devDependencies": { From b8b5393a502af66278a29ef1a6904089c03aebfa Mon Sep 17 00:00:00 2001 From: Luis Otavio Date: Mon, 16 Dec 2024 21:37:55 -0300 Subject: [PATCH 14/75] Enhance logging and configuration for WhatsApp integration - Added logging for processed responses and character count in classifier. - Implemented truncation for results exceeding 2499 characters. - Expanded .env.example with additional configuration options for WhatsApp bot. - Updated README.md for clearer setup instructions and usage guidelines. --- .../eda_ai_api/api/routes/classifier.py | 11 +++++ apps/whatsapp/.env.example | 35 +++++++++++++- apps/whatsapp/README.md | 48 +++++++++++++++++-- 3 files changed, 88 insertions(+), 6 deletions(-) diff --git a/apps/ai_api/eda_ai_api/api/routes/classifier.py b/apps/ai_api/eda_ai_api/api/routes/classifier.py index 1051860..f46ed29 100644 --- a/apps/ai_api/eda_ai_api/api/routes/classifier.py +++ b/apps/ai_api/eda_ai_api/api/routes/classifier.py @@ -54,6 +54,7 @@ async def process_llm_response(message: str, response: str) -> str: processed = llm.complete( RESPONSE_PROCESSOR_TEMPLATE.format(original_message=message, response=response) ) + logger.info(f"Processed response: {processed.text}") return processed.text @@ -183,6 +184,16 @@ async def classifier_route( else: final_result = str(result) + # Truncate if result exceeds character limit + if len(final_result) > 2499: + logger.warning( + f"Result exceeded 2499 characters (was {len(final_result)}). Truncating..." + ) + final_result = final_result[:2499] + + # Log both result and character count + logger.info(f"Final result ({len(final_result)} chars): {final_result}") + await zep.add_conversation(session_id, combined_message, final_result) return ClassifierResponse(result=final_result, session_id=session_id) diff --git a/apps/whatsapp/.env.example b/apps/whatsapp/.env.example index 08c8216..4359b14 100644 --- a/apps/whatsapp/.env.example +++ b/apps/whatsapp/.env.example @@ -1,4 +1,37 @@ TRIGGER_SECRET_KEY= TRIGGER_API_URL= -MONGODB_URI= PORT=4000 +PUPPETEER_EXECUTABLE_PATH="/snap/bin/chromium" + +# Whatsapp + +# This is how the bot will prefix its messages when answering to commands +# or when replying to itself (e.g. when you run the bot in your own personal whatsapp account) +# Note: must be different from CMD_PREFIX and cannot be empty +BOT_PREFIX="*[BOT]:*" + +# This is how the user should prefix their messages when issuing commands to the bot +CMD_PREFIX="!" + +# The assistant's name. Call it whatever you want. +BOT_NAME="Sydney" + +# Accepted values are "true", "dms_only", "groups_only" or "false" +ENABLE_REACTIONS="true" + +# The bot will only reply to these users. Leave this commented to allow everyone to use the bot. +# See the readme.md file to learn how this works. +#ALLOWED_USERS="" # Example: "5511999999999,14155551111" where 55 is the country code, 11 is the area code, and the rest is the phone number. + +# The bot will ignore these users. Leave this commented to allow everyone to use the bot. +# See the readme.md file to learn how this works. +#BLOCKED_USERS="" # Example: "5511999999999,14155551111" where 55 is the country code, 11 is the area code, and the rest is the phone number. + +# The "Too many unread messages..." warning when the bot starts. +IGNORE_MESSAGES_WARNING="false" # Accepted values are "true" or "false" + +# Change to your liking +QUEUED_REACTION="πŸ”" +WORKING_REACTION="βš™οΈ" +DONE_REACTION="βœ…" +ERROR_REACTION="⚠️" \ No newline at end of file diff --git a/apps/whatsapp/README.md b/apps/whatsapp/README.md index 4bd576d..d8e0712 100644 --- a/apps/whatsapp/README.md +++ b/apps/whatsapp/README.md @@ -1,15 +1,53 @@ -# ai-whatsapp +# Whatsapp + +A WhatsApp integration module for the Earth Defenders Assistant platform using Baileys library. + +## Requirements + +- [Bun](https://bun.sh) v1.1.24 or higher +- Node.js environment +- WhatsApp account with an active phone number + +## Environment Setup + +1. Create a `.env` file in the root directory by copying `.env.example`: +```bash +cp .env.example .env +``` + +2. Configure the environment variables in your `.env` file. + +## Installation To install dependencies: +1. Go into the repo's main folder `./earth-defenders-assistant` and use: ```bash bun install ``` -To run: - +2. Then use: ```bash -bun run index.ts +bun dev:whatsapp ``` -This project was created using `bun init` in bun v1.1.24. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime. +## First-time Setup + +1. When you first run the application, a QR code will appear in your terminal +2. Scan this QR code with your WhatsApp mobile app +3. Wait for the authentication process to complete +4. The bot will be ready to receive and process messages once connected + +## Usage + +- Use the configured `CMD_PREFIX` (default: "!") to send commands to the bot +- The bot will respond with messages prefixed with the configured `BOT_PREFIX` +- Reactions can be enabled/disabled using the `ENABLE_REACTIONS` setting + +## Project Structure + +This project is part of the Earth Defenders Assistant platform and uses: +- `@whiskeysockets/baileys` for WhatsApp integration +- Trigger.dev for workflow automation + +For more information about the Earth Defenders Assistant platform and its components, please refer to the main project documentation. \ No newline at end of file From b0ae989cbb47c2383612910f6a49a64c525c076a Mon Sep 17 00:00:00 2001 From: Luis Otavio Date: Fri, 27 Dec 2024 02:21:32 -0300 Subject: [PATCH 15/75] fix errors --- apps/messaging/.env.example | 1 + apps/messaging/src/index.ts | 77 +++++++++++++++++++++++++------------ 2 files changed, 53 insertions(+), 25 deletions(-) create mode 100644 apps/messaging/.env.example diff --git a/apps/messaging/.env.example b/apps/messaging/.env.example new file mode 100644 index 0000000..6451bc9 --- /dev/null +++ b/apps/messaging/.env.example @@ -0,0 +1 @@ +SUPABASE_SERVICE_ROLE_KEY=xxxxx \ No newline at end of file diff --git a/apps/messaging/src/index.ts b/apps/messaging/src/index.ts index e1ba793..d8b817e 100644 --- a/apps/messaging/src/index.ts +++ b/apps/messaging/src/index.ts @@ -7,6 +7,7 @@ import { } from "./routes"; import { messageSchema, queryParamsSchema } from "./types"; +const PORT = process.env.PORT || 3000; const app = new Elysia() .use( swagger({ @@ -28,24 +29,28 @@ const app = new Elysia() detail: { tags: ["messages"], description: "Send a new message", - request: { - body: { - type: "object", - properties: { - userId: { type: "string", example: "user123" }, - text: { type: "string", example: "Hello world" }, - platform: { - type: "string", - enum: ["whatsapp", "telegram", "simulator"], - example: "whatsapp", - }, - meta: { + requestBody: { + content: { + "application/json": { + schema: { type: "object", - example: { priority: "high" }, - optional: true, + properties: { + userId: { type: "string", example: "user123" }, + text: { type: "string", example: "Hello world" }, + platform: { + type: "string", + enum: ["whatsapp", "telegram", "simulator"], + example: "whatsapp", + }, + meta: { + type: "object", + additionalProperties: true, + example: { priority: "high" }, + }, + }, + required: ["userId", "text", "platform"], }, }, - required: ["userId", "text", "platform"], }, }, responses: { @@ -86,16 +91,35 @@ const app = new Elysia() detail: { tags: ["messages"], description: "Get received messages", - query: { - userId: { type: "string", optional: true }, - limit: { type: "number", optional: true }, - offset: { type: "number", optional: true }, - platform: { - type: "string", - enum: ["whatsapp", "telegram", "simulator"], - optional: true, + parameters: [ + { + in: "query", + name: "userId", + schema: { type: "string" }, + required: false, }, - }, + { + in: "query", + name: "limit", + schema: { type: "number" }, + required: false, + }, + { + in: "query", + name: "offset", + schema: { type: "number" }, + required: false, + }, + { + in: "query", + name: "platform", + schema: { + type: "string", + enum: ["whatsapp", "telegram", "simulator"], + }, + required: false, + }, + ], responses: { "200": { description: "Messages retrieved successfully", @@ -159,7 +183,10 @@ const app = new Elysia() }, }, }) - .listen(3000); + .listen({ + port: PORT, + hostname: "0.0.0.0", // This exposes the server externally + }); const swaggerUrl = `http://${app.server?.hostname || "localhost"}:${ app.server?.port From 4447de4b761fd07ac83e242d953197757fae4cf8 Mon Sep 17 00:00:00 2001 From: Luis Otavio Date: Fri, 27 Dec 2024 03:37:08 -0300 Subject: [PATCH 16/75] Changed to messaging api between whatsapp and ai_api --- .../eda_ai_api/api/routes/classifier.py | 2 +- apps/messaging/src/index.ts | 35 ++++------- apps/messaging/src/routes.ts | 60 ++++++++++++------- apps/messaging/src/types.ts | 14 +++-- apps/whatsapp/src/message/message.ts | 51 ++++++++-------- .../src/opportunity_finder/crew.py | 2 +- .../src/proposal_writer/crew.py | 2 +- plugins/onboarding/src/onboarding/crew.py | 2 +- 8 files changed, 89 insertions(+), 79 deletions(-) diff --git a/apps/ai_api/eda_ai_api/api/routes/classifier.py b/apps/ai_api/eda_ai_api/api/routes/classifier.py index f46ed29..f5ea23b 100644 --- a/apps/ai_api/eda_ai_api/api/routes/classifier.py +++ b/apps/ai_api/eda_ai_api/api/routes/classifier.py @@ -24,7 +24,7 @@ # Setup LLM llm = Groq( - model="llama3-groq-70b-8192-tool-use-preview", + model="llama-3.3-70b-versatile", api_key=os.environ.get("GROQ_API_KEY"), temperature=0.5, ) diff --git a/apps/messaging/src/index.ts b/apps/messaging/src/index.ts index d8b817e..e59feb2 100644 --- a/apps/messaging/src/index.ts +++ b/apps/messaging/src/index.ts @@ -5,7 +5,7 @@ import { handleHealthCheck, handleSendMessage, } from "./routes"; -import { messageSchema, queryParamsSchema } from "./types"; +import { messageSchema } from "./types"; const PORT = process.env.PORT || 3000; const app = new Elysia() @@ -28,44 +28,32 @@ const app = new Elysia() .post("/api/messages/send", handleSendMessage, { detail: { tags: ["messages"], - description: "Send a new message", + description: "Process a message through AI API", requestBody: { content: { "application/json": { schema: { type: "object", properties: { - userId: { type: "string", example: "user123" }, - text: { type: "string", example: "Hello world" }, - platform: { - type: "string", - enum: ["whatsapp", "telegram", "simulator"], - example: "whatsapp", - }, - meta: { - type: "object", - additionalProperties: true, - example: { priority: "high" }, - }, + message: { type: "string", example: "Hello world" }, + sessionId: { type: "string", example: "5515991306053" }, + audio: { type: "string", format: "binary", nullable: true }, }, - required: ["userId", "text", "platform"], + required: ["message", "sessionId"], }, }, }, }, responses: { "200": { - description: "Message sent successfully", + description: "Message processed successfully", content: { "application/json": { schema: { type: "object", properties: { - id: { type: "string" }, - userId: { type: "string" }, - text: { type: "string" }, - platform: { type: "string" }, - timestamp: { type: "number" }, + result: { type: "string" }, + session_id: { type: "string" }, }, }, }, @@ -183,10 +171,7 @@ const app = new Elysia() }, }, }) - .listen({ - port: PORT, - hostname: "0.0.0.0", // This exposes the server externally - }); + .listen(PORT); const swaggerUrl = `http://${app.server?.hostname || "localhost"}:${ app.server?.port diff --git a/apps/messaging/src/routes.ts b/apps/messaging/src/routes.ts index dd73059..e86099c 100644 --- a/apps/messaging/src/routes.ts +++ b/apps/messaging/src/routes.ts @@ -7,33 +7,51 @@ const supabase = createClient( process.env.SUPABASE_SERVICE_ROLE_KEY!, // Get this from Supabase Studio ); +const AI_API_URL = process.env.AI_API_URL || "http://127.0.0.1:8083"; + export async function handleSendMessage(req: Request) { try { - const payload = messageSchema.parse(await req.json()); - - const { data, error } = await supabase - .from("tosend_messages") - .insert([ - { - user_id: payload.userId, - text: payload.text, - timestamp: Date.now(), - meta: { - platform: payload.platform, - ...payload.meta, - }, - }, - ]) - .select() - .single(); + // Access body directly from Elysia's parsed request + const payload = messageSchema.parse(req.body); - if (error) throw error; + logger.info("Received message request", { + sessionId: payload.sessionId, + hasAudio: !!payload.audio, + }); + + // Create FormData for AI API + const formData = new FormData(); + formData.append("message", payload.message); + formData.append("session_id", payload.sessionId); + + if (payload.audio) { + formData.append("audio", payload.audio); + } + + // Forward request to AI API + const response = await fetch(`${AI_API_URL}/api/classifier/classify`, { + method: "POST", + headers: { + accept: "application/json", + }, + body: formData, + }); + + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.message || "Failed to process message"); + } - logger.info("Message queued for sending", { userId: payload.userId }); + logger.info("Message processed by AI API", { + sessionId: payload.sessionId, + }); return Response.json(data); } catch (error) { - logger.error("Error queueing message", { error }); - return Response.json({ error: "Failed to queue message" }, { status: 400 }); + const message = + error instanceof Error ? error.message : "Failed to process message"; + logger.error("Error processing message", { error: message }); + return Response.json({ error: message }, { status: 400 }); } } diff --git a/apps/messaging/src/types.ts b/apps/messaging/src/types.ts index e1e0a59..2e36154 100644 --- a/apps/messaging/src/types.ts +++ b/apps/messaging/src/types.ts @@ -1,10 +1,14 @@ import { z } from "zod"; export const messageSchema = z.object({ - userId: z.string(), - text: z.string(), - platform: z.enum(["whatsapp", "telegram", "simulator"]), - meta: z.record(z.any()).optional(), + message: z.string(), + sessionId: z.string(), + audio: z.instanceof(Blob).optional(), +}); + +export const messageResponseSchema = z.object({ + result: z.string(), + session_id: z.string().optional(), }); export const queryParamsSchema = z.object({ @@ -15,4 +19,4 @@ export const queryParamsSchema = z.object({ }); export type Message = z.infer; -export type QueryParams = z.infer; +export type MessageResponse = z.infer; diff --git a/apps/whatsapp/src/message/message.ts b/apps/whatsapp/src/message/message.ts index bde4d35..0834997 100644 --- a/apps/whatsapp/src/message/message.ts +++ b/apps/whatsapp/src/message/message.ts @@ -16,6 +16,12 @@ const ERROR_MESSAGES = { UNKNOWN: "Ocorreu um erro inesperado. Pode tentar novamente?", }; +interface MessagePayload { + message: string; + sessionId: string; + audio?: Blob; +} + export async function handleMessage(message: WAMessage) { await react(message, "working"); @@ -33,7 +39,6 @@ export async function handleMessage(message: WAMessage) { if (!streamingReply) return console.error("No streaming reply"); try { - // Extract message content and phone number const messageContent = message.message?.conversation || message.message?.extendedTextMessage?.text || @@ -67,43 +72,41 @@ export async function handleMessage(message: WAMessage) { } } - // Create FormData - const formData = new FormData(); - formData.append("message", messageContent); - formData.append("session_id", phoneNumber); + // Prepare request payload + const payload: MessagePayload = { + message: messageContent, + sessionId: phoneNumber, + }; - // Add audio if present if (audioBlob) { - formData.append("audio", audioBlob, "audio.ogg"); + payload.audio = audioBlob; } - // Make API request + // Make API request to messaging service const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), 600000); // 10 minute timeout (10 * 60 * 1000 ms) - - const response = await fetch( - "http://127.0.0.1:8083/api/classifier/classify", - { - method: "POST", - headers: { - accept: "application/json", - }, - body: formData, - signal: controller.signal, + const timeoutId = setTimeout(() => controller.abort(), 600000); + + const response = await fetch("http://localhost:3001/api/messages/send", { + method: "POST", + headers: { + "Content-Type": "application/json", }, - ); + body: JSON.stringify(payload), + signal: controller.signal, + }); - clearTimeout(timeoutId); // Clear timeout if request completes + clearTimeout(timeoutId); if (!response.ok) { + console.dir(response, { depth: null }); throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); - if (!data.result) return ERROR_MESSAGES.NO_RESPONSE; - - console.log(data.result); + if (!data.result) { + throw new Error("No result from API"); + } await sock.sendMessage( chatId, diff --git a/plugins/grant_plugin/opportunity_finder/src/opportunity_finder/crew.py b/plugins/grant_plugin/opportunity_finder/src/opportunity_finder/crew.py index 6b21206..8e1d7bc 100644 --- a/plugins/grant_plugin/opportunity_finder/src/opportunity_finder/crew.py +++ b/plugins/grant_plugin/opportunity_finder/src/opportunity_finder/crew.py @@ -7,7 +7,7 @@ search_tool = SerperDevTool() scrape_tool = ScrapeWebsiteTool() llm = LLM( - model="groq/llama3-groq-70b-8192-tool-use-preview", # Replace with your chosen Cerebras model name, e.g., "cerebras/llama3.1-8b" + model="groq/llama-3.3-70b-versatile", # Replace with your chosen Cerebras model name, e.g., "cerebras/llama3.1-8b" api_key=os.environ.get("GROQ_API_KEY"), # Your Cerebras API key temperature=0.5, ) diff --git a/plugins/grant_plugin/proposal_writer/src/proposal_writer/crew.py b/plugins/grant_plugin/proposal_writer/src/proposal_writer/crew.py index 21c835a..1d36402 100644 --- a/plugins/grant_plugin/proposal_writer/src/proposal_writer/crew.py +++ b/plugins/grant_plugin/proposal_writer/src/proposal_writer/crew.py @@ -6,7 +6,7 @@ # Initialize LLM llm = LLM( - model="groq/llama3-groq-70b-8192-tool-use-preview", # Replace with your chosen Cerebras model name, e.g., "cerebras/llama3.1-8b" + model="groq/llama-3.3-70b-versatile", # Replace with your chosen Cerebras model name, e.g., "cerebras/llama3.1-8b" api_key=os.environ.get("GROQ_API_KEY"), # Your Cerebras API key temperature=0.5, ) diff --git a/plugins/onboarding/src/onboarding/crew.py b/plugins/onboarding/src/onboarding/crew.py index 3594af8..c894407 100644 --- a/plugins/onboarding/src/onboarding/crew.py +++ b/plugins/onboarding/src/onboarding/crew.py @@ -4,7 +4,7 @@ # Configure LLM llm = LLM( - model="groq/llama3-groq-70b-8192-tool-use-preview", + model="groq/llama-3.3-70b-versatile", api_key=os.environ.get("GROQ_API_KEY"), temperature=0.5, ) From 1568cbaddacdb26900bd4410a7f13fc9cf3676b2 Mon Sep 17 00:00:00 2001 From: Luis Otavio Date: Sat, 28 Dec 2024 21:18:22 -0300 Subject: [PATCH 17/75] Fix: audio messages not working --- apps/messaging/src/routes.ts | 10 +++++++++- apps/messaging/src/types.ts | 2 +- apps/whatsapp/src/message/message.ts | 28 ++++++++++++++-------------- 3 files changed, 24 insertions(+), 16 deletions(-) diff --git a/apps/messaging/src/routes.ts b/apps/messaging/src/routes.ts index e86099c..e8525ae 100644 --- a/apps/messaging/src/routes.ts +++ b/apps/messaging/src/routes.ts @@ -11,6 +11,7 @@ const AI_API_URL = process.env.AI_API_URL || "http://127.0.0.1:8083"; export async function handleSendMessage(req: Request) { try { + logger.info("Received message request"); // Access body directly from Elysia's parsed request const payload = messageSchema.parse(req.body); @@ -25,7 +26,13 @@ export async function handleSendMessage(req: Request) { formData.append("session_id", payload.sessionId); if (payload.audio) { - formData.append("audio", payload.audio); + const binaryStr = atob(payload.audio); + const bytes = new Uint8Array(binaryStr.length); + for (let i = 0; i < binaryStr.length; i++) { + bytes[i] = binaryStr.charCodeAt(i); + } + const audioBlob = new Blob([bytes], { type: "audio/ogg" }); // Adjust mime type as needed + formData.append("audio", audioBlob); } // Forward request to AI API @@ -51,6 +58,7 @@ export async function handleSendMessage(req: Request) { const message = error instanceof Error ? error.message : "Failed to process message"; logger.error("Error processing message", { error: message }); + console.error(error); return Response.json({ error: message }, { status: 400 }); } } diff --git a/apps/messaging/src/types.ts b/apps/messaging/src/types.ts index 2e36154..349b0e4 100644 --- a/apps/messaging/src/types.ts +++ b/apps/messaging/src/types.ts @@ -3,7 +3,7 @@ import { z } from "zod"; export const messageSchema = z.object({ message: z.string(), sessionId: z.string(), - audio: z.instanceof(Blob).optional(), + audio: z.string().optional(), // Changed from Blob to string }); export const messageResponseSchema = z.object({ diff --git a/apps/whatsapp/src/message/message.ts b/apps/whatsapp/src/message/message.ts index 0834997..e91ab7f 100644 --- a/apps/whatsapp/src/message/message.ts +++ b/apps/whatsapp/src/message/message.ts @@ -1,3 +1,4 @@ +import { logger } from "@trigger.dev/sdk/v3"; import { type WAMessage, downloadMediaMessage } from "@whiskeysockets/baileys"; import { sock } from "../client"; import { BOT_PREFIX } from "../constants"; @@ -19,7 +20,7 @@ const ERROR_MESSAGES = { interface MessagePayload { message: string; sessionId: string; - audio?: Blob; + audio?: string; // Changed from Blob to string } export async function handleMessage(message: WAMessage) { @@ -45,7 +46,11 @@ export async function handleMessage(message: WAMessage) { ""; const phoneNumber = getPhoneNumber(message); - let audioBlob: Blob | undefined; + // Initialize payload first + const payload: MessagePayload = { + message: messageContent, + sessionId: phoneNumber, + }; if (message.message?.imageMessage || message.message?.audioMessage) { const media = await downloadMediaMessage(message, "buffer", {}); @@ -67,21 +72,16 @@ export async function handleMessage(message: WAMessage) { return; } - if (isAudio && mimetype) { - audioBlob = new Blob([media], { type: mimetype }); + if (isAudio && mimetype && media instanceof Buffer) { + logger.info("Received audio message", { + sessionId: phoneNumber, + hasAudio: true, + }); + // Convert Buffer to base64 + payload.audio = media.toString("base64"); } } - // Prepare request payload - const payload: MessagePayload = { - message: messageContent, - sessionId: phoneNumber, - }; - - if (audioBlob) { - payload.audio = audioBlob; - } - // Make API request to messaging service const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 600000); From e59c509ddacdba1e9d9864d3288c5957b0ece969 Mon Sep 17 00:00:00 2001 From: Luis Otavio Date: Sun, 29 Dec 2024 04:39:59 -0300 Subject: [PATCH 18/75] Removed get endpoint in messaging --- apps/messaging/src/index.ts | 81 +----------------------------------- apps/messaging/src/routes.ts | 42 ------------------- 2 files changed, 1 insertion(+), 122 deletions(-) diff --git a/apps/messaging/src/index.ts b/apps/messaging/src/index.ts index e59feb2..e752d6b 100644 --- a/apps/messaging/src/index.ts +++ b/apps/messaging/src/index.ts @@ -1,10 +1,6 @@ import { swagger } from "@elysiajs/swagger"; import { Elysia } from "elysia"; -import { - handleGetMessages, - handleHealthCheck, - handleSendMessage, -} from "./routes"; +import { handleHealthCheck, handleSendMessage } from "./routes"; import { messageSchema } from "./types"; const PORT = process.env.PORT || 3000; @@ -75,81 +71,6 @@ const app = new Elysia() }, }, }) - .get("/api/messages/receive", handleGetMessages, { - detail: { - tags: ["messages"], - description: "Get received messages", - parameters: [ - { - in: "query", - name: "userId", - schema: { type: "string" }, - required: false, - }, - { - in: "query", - name: "limit", - schema: { type: "number" }, - required: false, - }, - { - in: "query", - name: "offset", - schema: { type: "number" }, - required: false, - }, - { - in: "query", - name: "platform", - schema: { - type: "string", - enum: ["whatsapp", "telegram", "simulator"], - }, - required: false, - }, - ], - responses: { - "200": { - description: "Messages retrieved successfully", - content: { - "application/json": { - schema: { - type: "object", - properties: { - messages: { - type: "array", - items: { - type: "object", - properties: { - id: { type: "string" }, - userId: { type: "string" }, - text: { type: "string" }, - platform: { type: "string" }, - timestamp: { type: "number" }, - }, - }, - }, - }, - }, - }, - }, - }, - "400": { - description: "Invalid request parameters", - content: { - "application/json": { - schema: { - type: "object", - properties: { - error: { type: "string" }, - }, - }, - }, - }, - }, - }, - }, - }) .get("/api/messages/health", handleHealthCheck, { detail: { tags: ["health"], diff --git a/apps/messaging/src/routes.ts b/apps/messaging/src/routes.ts index e8525ae..e006efd 100644 --- a/apps/messaging/src/routes.ts +++ b/apps/messaging/src/routes.ts @@ -63,48 +63,6 @@ export async function handleSendMessage(req: Request) { } } -export async function handleGetMessages(req: Request) { - try { - const url = new URL(req.url); - const params = queryParamsSchema.parse( - Object.fromEntries(url.searchParams), - ); - - let query = supabase - .from("received_messages") - .select("*") - .order("timestamp", { ascending: false }); - - if (params.userId) { - query = query.eq("user_id", params.userId); - } - if (params.limit) { - query = query.limit(params.limit); - } - if (params.offset) { - query = query.range( - params.offset, - params.offset + (params.limit || 10) - 1, - ); - } - if (params.platform) { - query = query.eq("meta->platform", params.platform); - } - - const { data, error } = await query; - - if (error) throw error; - - return Response.json({ messages: data }); - } catch (error) { - logger.error("Error fetching messages", { error }); - return Response.json( - { error: "Failed to fetch messages" }, - { status: 400 }, - ); - } -} - export function handleHealthCheck() { return Response.json({ status: "healthy" }); } From 53ffbeef4f8838879760ac6e8dc8e77d56e283d0 Mon Sep 17 00:00:00 2001 From: Luis Otavio Date: Sun, 29 Dec 2024 04:55:42 -0300 Subject: [PATCH 19/75] saving incoming and outgoing messages in supabase --- apps/messaging/src/routes.ts | 62 +++++++++++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/apps/messaging/src/routes.ts b/apps/messaging/src/routes.ts index e006efd..fd05702 100644 --- a/apps/messaging/src/routes.ts +++ b/apps/messaging/src/routes.ts @@ -9,6 +9,58 @@ const supabase = createClient( const AI_API_URL = process.env.AI_API_URL || "http://127.0.0.1:8083"; +async function saveMessageToSupabase( + whatsappId: string, + userMessage: string, + aiResponse: string, +) { + try { + // First check if conversation exists + const { data: existingConversation } = await supabase + .from("messages") + .select("conversation_history") + .eq("whatsapp_id", whatsappId) + .single(); + + if (existingConversation) { + // Update existing conversation + const currentHistory = existingConversation.conversation_history || []; + const updatedHistory = [ + ...currentHistory, + { + human: userMessage, + ai: aiResponse, + timestamp: new Date().toISOString(), + }, + ]; + + const { error } = await supabase + .from("messages") + .update({ conversation_history: updatedHistory }) + .eq("whatsapp_id", whatsappId); + + if (error) throw error; + } else { + // Create new conversation + const { error } = await supabase.from("messages").insert({ + whatsapp_id: whatsappId, + conversation_history: [ + { + human: userMessage, + ai: aiResponse, + timestamp: new Date().toISOString(), + }, + ], + }); + + if (error) throw error; + } + } catch (error) { + logger.error("Error saving to Supabase:", error); + throw error; + } +} + export async function handleSendMessage(req: Request) { try { logger.info("Received message request"); @@ -50,9 +102,17 @@ export async function handleSendMessage(req: Request) { throw new Error(data.message || "Failed to process message"); } - logger.info("Message processed by AI API", { + // Save message to Supabase after successful AI response + await saveMessageToSupabase( + payload.sessionId, + payload.message, + data.result, + ); + + logger.info("Message processed and saved", { sessionId: payload.sessionId, }); + return Response.json(data); } catch (error) { const message = From 14b26643b8a86cbfc0ad626dac454d3eaa1c6b07 Mon Sep 17 00:00:00 2001 From: Luis Otavio Date: Sun, 29 Dec 2024 05:35:49 -0300 Subject: [PATCH 20/75] Getting message history from supabase --- .../eda_ai_api/api/routes/classifier.py | 94 +++++++++++++++---- apps/ai_api/eda_ai_api/models/classifier.py | 11 ++- apps/messaging/src/routes.ts | 14 +++ 3 files changed, 97 insertions(+), 22 deletions(-) diff --git a/apps/ai_api/eda_ai_api/api/routes/classifier.py b/apps/ai_api/eda_ai_api/api/routes/classifier.py index f5ea23b..9f6cb37 100644 --- a/apps/ai_api/eda_ai_api/api/routes/classifier.py +++ b/apps/ai_api/eda_ai_api/api/routes/classifier.py @@ -1,3 +1,4 @@ +import json import os import uuid from typing import Any, Dict, Optional @@ -10,7 +11,7 @@ from opportunity_finder.crew import OpportunityFinderCrew from proposal_writer.crew import ProposalWriterCrew -from eda_ai_api.models.classifier import ClassifierResponse +from eda_ai_api.models.classifier import ClassifierResponse, MessageHistory from eda_ai_api.utils.audio_utils import process_audio_file from eda_ai_api.utils.memory import ZepConversationManager from eda_ai_api.utils.prompts import ( @@ -59,26 +60,42 @@ async def process_llm_response(message: str, response: str) -> str: async def process_decision( - decision: str, message: str, history: list + decision: str, + message: str, + zep_history: list, + supabase_history: list[MessageHistory] = [], ) -> Dict[str, Any]: - """Process routing decision with conversation context""" + """Process routing decision with conversation context from both sources""" logger.info(f"Processing decision: {decision} for message: {message}") + # Combine histories for context + context_parts = [] + + if supabase_history: + supabase_context = format_supabase_history(supabase_history) + context_parts.append(f"Recent conversation:\n{supabase_context}") + + if zep_history: + zep_context = "\n".join( + [f"{msg['role']}: {msg['content']}" for msg in zep_history] + ) + context_parts.append(f"Long-term memory:\n{zep_context}") + + combined_context = "\n\n".join(context_parts) + if decision == "discovery": - topics = await extract_topics(message, history) + topics = await extract_topics(message, zep_history) logger.info(f"Extracted topics: {topics}") if topics == ["INSUFFICIENT_CONTEXT"]: - context = "\n".join([f"{msg['role']}: {msg['content']}" for msg in history]) response = llm.complete( INSUFFICIENT_TEMPLATES["discovery"].format( - context=context, message=message + context=combined_context, message=message ) ) processed_response = await process_llm_response(message, response.text) return {"response": processed_response} - # Add return for successful discovery crew_result = ( OpportunityFinderCrew().crew().kickoff(inputs={"topics": ", ".join(topics)}) ) @@ -86,20 +103,20 @@ async def process_decision( return {"response": processed_response} elif decision == "proposal": - community_project, grant_call = await extract_proposal_details(message, history) + community_project, grant_call = await extract_proposal_details( + message, zep_history + ) logger.info(f"Project: {community_project}, Grant: {grant_call}") if community_project == "unknown" or grant_call == "unknown": - context = "\n".join([f"{msg['role']}: {msg['content']}" for msg in history]) response = llm.complete( INSUFFICIENT_TEMPLATES["proposal"].format( - context=context, message=message + context=combined_context, message=message ) ) processed_response = await process_llm_response(message, response.text) return {"response": processed_response} - # Add crew result and return crew_result = ( ProposalWriterCrew() .crew() @@ -124,11 +141,27 @@ async def process_decision( return {"error": f"Unknown decision type: {decision}"} +def format_supabase_history(history: list[MessageHistory]) -> str: + """Format last 10 Supabase messages into conversation format""" + if not history: + return "" + + # Get last 10 messages + limited_history = history[-10:] + + formatted = [] + for msg in limited_history: + formatted.extend([f"human: {msg.human}", f"assistant: {msg.ai}"]) + + return "\n".join(formatted[-10:]) # Take last 10 messages total + + @router.post("/classify", response_model=ClassifierResponse) async def classifier_route( message: Optional[str] = Form(default=None), audio: Optional[UploadFile] = File(default=None), session_id: Optional[str] = Form(default=None), + message_history: Optional[str] = Form(default=None), # JSON string ) -> ClassifierResponse: """Main route handler with conversation memory""" try: @@ -138,8 +171,9 @@ async def classifier_route( logger.info(f"New request - Session: {current_session_id}, User: {user_id}") + # Initialize both history sources zep = ZepConversationManager() - session_id = await zep.get_or_create_session( + zep_session_id = await zep.get_or_create_session( user_id=user_id, session_id=current_session_id ) @@ -159,10 +193,28 @@ async def classifier_route( if not combined_message: return ClassifierResponse(result="Error: No valid input provided") - # Get conversation history and make decision - history = await zep.get_conversation_history(session_id) - context = "\n".join([f"{msg['role']}: {msg['content']}" for msg in history]) + # Get both conversation histories + zep_history = await zep.get_conversation_history(zep_session_id) + supabase_history = [] + if message_history: + try: + supabase_history = [ + MessageHistory(**msg) for msg in json.loads(message_history) + ] + except json.JSONDecodeError: + logger.warning("Invalid message_history JSON format") + + # Combine both histories for context + zep_context = "\n".join( + [f"{msg['role']}: {msg['content']}" for msg in zep_history] + ) + supabase_context = format_supabase_history(supabase_history) + combined_context = f"""Recent conversation:\n{supabase_context}\n\nLong-term memory:\n{zep_context}""" + + logger.info(f"Combined context:\n{combined_context}") + + # Use combined context in router prompt router_prompt = PromptTemplate( """Previous conversation:\n{context}\n\n""" """Given the current user message, determine the appropriate service:""" @@ -171,12 +223,14 @@ async def classifier_route( ) response = llm.complete( - router_prompt.format(context=context, message=combined_message) + router_prompt.format(context=combined_context, message=combined_message) ) decision = response.text.strip().lower() - # Process decision and store conversation - result = await process_decision(decision, combined_message, history) + # Process decision using combined context + result = await process_decision( + decision, combined_message, zep_history, supabase_history + ) # Process final result if it's not already processed if isinstance(result.get("response"), str): @@ -194,8 +248,8 @@ async def classifier_route( # Log both result and character count logger.info(f"Final result ({len(final_result)} chars): {final_result}") - await zep.add_conversation(session_id, combined_message, final_result) - return ClassifierResponse(result=final_result, session_id=session_id) + await zep.add_conversation(zep_session_id, combined_message, final_result) + return ClassifierResponse(result=final_result, session_id=zep_session_id) except Exception as e: logger.error(f"Error in classifier route: {str(e)}") diff --git a/apps/ai_api/eda_ai_api/models/classifier.py b/apps/ai_api/eda_ai_api/models/classifier.py index 1bcea53..64d9b06 100644 --- a/apps/ai_api/eda_ai_api/models/classifier.py +++ b/apps/ai_api/eda_ai_api/models/classifier.py @@ -1,12 +1,19 @@ -from typing import Optional - +from typing import Optional, List, Dict from fastapi import UploadFile from pydantic import BaseModel +class MessageHistory(BaseModel): + human: str + ai: str + timestamp: str + + class ClassifierRequest(BaseModel): message: Optional[str] = None audio: Optional[UploadFile] = None + session_id: Optional[str] = None + message_history: Optional[List[MessageHistory]] = None class Config: arbitrary_types_allowed = True diff --git a/apps/messaging/src/routes.ts b/apps/messaging/src/routes.ts index fd05702..6dc5f37 100644 --- a/apps/messaging/src/routes.ts +++ b/apps/messaging/src/routes.ts @@ -87,6 +87,20 @@ export async function handleSendMessage(req: Request) { formData.append("audio", audioBlob); } + // Add message history to formData + const { data: existingConversation } = await supabase + .from("messages") + .select("conversation_history") + .eq("whatsapp_id", payload.sessionId) + .single(); + + if (existingConversation?.conversation_history) { + formData.append( + "message_history", + JSON.stringify(existingConversation.conversation_history), + ); + } + // Forward request to AI API const response = await fetch(`${AI_API_URL}/api/classifier/classify`, { method: "POST", From 37ba82103bb27dd352181d3b3c50ff89d48ae032 Mon Sep 17 00:00:00 2001 From: Luis Otavio Date: Mon, 30 Dec 2024 03:13:50 -0300 Subject: [PATCH 21/75] trigger test --- apps/whatsapp/src/message/message.ts | 36 +++++++--------------------- apps/whatsapp/src/schemas.ts | 11 +++++++++ apps/whatsapp/src/trigger/client.ts | 7 ++++++ apps/whatsapp/src/trigger/jobs.ts | 30 +++++++++++++++++++++++ apps/whatsapp/src/types.ts | 5 ++++ 5 files changed, 62 insertions(+), 27 deletions(-) create mode 100644 apps/whatsapp/src/schemas.ts create mode 100644 apps/whatsapp/src/trigger/client.ts create mode 100644 apps/whatsapp/src/trigger/jobs.ts create mode 100644 apps/whatsapp/src/types.ts diff --git a/apps/whatsapp/src/message/message.ts b/apps/whatsapp/src/message/message.ts index e91ab7f..4e4c099 100644 --- a/apps/whatsapp/src/message/message.ts +++ b/apps/whatsapp/src/message/message.ts @@ -2,6 +2,8 @@ import { logger } from "@trigger.dev/sdk/v3"; import { type WAMessage, downloadMediaMessage } from "@whiskeysockets/baileys"; import { sock } from "../client"; import { BOT_PREFIX } from "../constants"; +import { client } from "../trigger/client"; +import type { MessagePayload, MessageResponse } from "../types"; import { getPhoneNumber, react } from "../utils"; const IMAGE_ERROR_MSG = @@ -17,12 +19,6 @@ const ERROR_MESSAGES = { UNKNOWN: "Ocorreu um erro inesperado. Pode tentar novamente?", }; -interface MessagePayload { - message: string; - sessionId: string; - audio?: string; // Changed from Blob to string -} - export async function handleMessage(message: WAMessage) { await react(message, "working"); @@ -82,35 +78,21 @@ export async function handleMessage(message: WAMessage) { } } - // Make API request to messaging service - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), 600000); - - const response = await fetch("http://localhost:3001/api/messages/send", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(payload), - signal: controller.signal, + // Use trigger.dev to send message + const eventResponse = await client.sendEvent({ + name: "message.send", + payload, }); - clearTimeout(timeoutId); - - if (!response.ok) { - console.dir(response, { depth: null }); - throw new Error(`HTTP error! status: ${response.status}`); - } - - const data = await response.json(); + const result = eventResponse.payload as MessageResponse; - if (!data.result) { + if (!result?.result) { throw new Error("No result from API"); } await sock.sendMessage( chatId, - { text: data.result, edit: streamingReply.key }, + { text: result.result, edit: streamingReply.key }, { quoted: message }, ); diff --git a/apps/whatsapp/src/schemas.ts b/apps/whatsapp/src/schemas.ts new file mode 100644 index 0000000..a65206b --- /dev/null +++ b/apps/whatsapp/src/schemas.ts @@ -0,0 +1,11 @@ +import { z } from "zod"; + +export const messagePayloadSchema = z.object({ + message: z.string(), + sessionId: z.string(), + audio: z.string().optional(), +}); + +export const messageResponseSchema = z.object({ + result: z.string(), +}); diff --git a/apps/whatsapp/src/trigger/client.ts b/apps/whatsapp/src/trigger/client.ts new file mode 100644 index 0000000..741d92c --- /dev/null +++ b/apps/whatsapp/src/trigger/client.ts @@ -0,0 +1,7 @@ +import { TriggerClient } from "@trigger.dev/sdk"; + +export const client = new TriggerClient({ + id: "earth-defenders-whatsapp", + apiUrl: process.env.TRIGGER_API_URL || "http://localhost:3040", + apiKey: process.env.TRIGGER_API_KEY!, +}); diff --git a/apps/whatsapp/src/trigger/jobs.ts b/apps/whatsapp/src/trigger/jobs.ts new file mode 100644 index 0000000..d9f5d75 --- /dev/null +++ b/apps/whatsapp/src/trigger/jobs.ts @@ -0,0 +1,30 @@ +import { eventTrigger } from "@trigger.dev/sdk"; +import { messagePayloadSchema } from "../schemas"; +import type { MessagePayload, MessageResponse } from "../types"; +import { client } from "./client"; + +export const sendMessageJob = client.defineJob({ + id: "send-message-job", + name: "Send Message to API", + version: "1.0.0", + trigger: eventTrigger({ + name: "message.send", + schema: messagePayloadSchema, + }), + run: async (payload: MessagePayload): Promise => { + const response = await fetch("http://localhost:3001/api/messages/send", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(payload), + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + return { result: data.result }; + }, +}); diff --git a/apps/whatsapp/src/types.ts b/apps/whatsapp/src/types.ts new file mode 100644 index 0000000..06e4180 --- /dev/null +++ b/apps/whatsapp/src/types.ts @@ -0,0 +1,5 @@ +import type { z } from "zod"; +import type { messagePayloadSchema, messageResponseSchema } from "./schemas"; + +export type MessagePayload = z.infer; +export type MessageResponse = z.infer; From d3125eebd39ab8f3b08d855a9752a214ffe2d377 Mon Sep 17 00:00:00 2001 From: Luis Otavio Date: Sun, 5 Jan 2025 15:25:20 -0300 Subject: [PATCH 22/75] Revert "trigger test" This reverts commit 37ba82103bb27dd352181d3b3c50ff89d48ae032. --- apps/whatsapp/src/message/message.ts | 36 +++++++++++++++++++++------- apps/whatsapp/src/schemas.ts | 11 --------- apps/whatsapp/src/trigger/client.ts | 7 ------ apps/whatsapp/src/trigger/jobs.ts | 30 ----------------------- apps/whatsapp/src/types.ts | 5 ---- 5 files changed, 27 insertions(+), 62 deletions(-) delete mode 100644 apps/whatsapp/src/schemas.ts delete mode 100644 apps/whatsapp/src/trigger/client.ts delete mode 100644 apps/whatsapp/src/trigger/jobs.ts delete mode 100644 apps/whatsapp/src/types.ts diff --git a/apps/whatsapp/src/message/message.ts b/apps/whatsapp/src/message/message.ts index 4e4c099..e91ab7f 100644 --- a/apps/whatsapp/src/message/message.ts +++ b/apps/whatsapp/src/message/message.ts @@ -2,8 +2,6 @@ import { logger } from "@trigger.dev/sdk/v3"; import { type WAMessage, downloadMediaMessage } from "@whiskeysockets/baileys"; import { sock } from "../client"; import { BOT_PREFIX } from "../constants"; -import { client } from "../trigger/client"; -import type { MessagePayload, MessageResponse } from "../types"; import { getPhoneNumber, react } from "../utils"; const IMAGE_ERROR_MSG = @@ -19,6 +17,12 @@ const ERROR_MESSAGES = { UNKNOWN: "Ocorreu um erro inesperado. Pode tentar novamente?", }; +interface MessagePayload { + message: string; + sessionId: string; + audio?: string; // Changed from Blob to string +} + export async function handleMessage(message: WAMessage) { await react(message, "working"); @@ -78,21 +82,35 @@ export async function handleMessage(message: WAMessage) { } } - // Use trigger.dev to send message - const eventResponse = await client.sendEvent({ - name: "message.send", - payload, + // Make API request to messaging service + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 600000); + + const response = await fetch("http://localhost:3001/api/messages/send", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(payload), + signal: controller.signal, }); - const result = eventResponse.payload as MessageResponse; + clearTimeout(timeoutId); + + if (!response.ok) { + console.dir(response, { depth: null }); + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); - if (!result?.result) { + if (!data.result) { throw new Error("No result from API"); } await sock.sendMessage( chatId, - { text: result.result, edit: streamingReply.key }, + { text: data.result, edit: streamingReply.key }, { quoted: message }, ); diff --git a/apps/whatsapp/src/schemas.ts b/apps/whatsapp/src/schemas.ts deleted file mode 100644 index a65206b..0000000 --- a/apps/whatsapp/src/schemas.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { z } from "zod"; - -export const messagePayloadSchema = z.object({ - message: z.string(), - sessionId: z.string(), - audio: z.string().optional(), -}); - -export const messageResponseSchema = z.object({ - result: z.string(), -}); diff --git a/apps/whatsapp/src/trigger/client.ts b/apps/whatsapp/src/trigger/client.ts deleted file mode 100644 index 741d92c..0000000 --- a/apps/whatsapp/src/trigger/client.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { TriggerClient } from "@trigger.dev/sdk"; - -export const client = new TriggerClient({ - id: "earth-defenders-whatsapp", - apiUrl: process.env.TRIGGER_API_URL || "http://localhost:3040", - apiKey: process.env.TRIGGER_API_KEY!, -}); diff --git a/apps/whatsapp/src/trigger/jobs.ts b/apps/whatsapp/src/trigger/jobs.ts deleted file mode 100644 index d9f5d75..0000000 --- a/apps/whatsapp/src/trigger/jobs.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { eventTrigger } from "@trigger.dev/sdk"; -import { messagePayloadSchema } from "../schemas"; -import type { MessagePayload, MessageResponse } from "../types"; -import { client } from "./client"; - -export const sendMessageJob = client.defineJob({ - id: "send-message-job", - name: "Send Message to API", - version: "1.0.0", - trigger: eventTrigger({ - name: "message.send", - schema: messagePayloadSchema, - }), - run: async (payload: MessagePayload): Promise => { - const response = await fetch("http://localhost:3001/api/messages/send", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(payload), - }); - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - const data = await response.json(); - return { result: data.result }; - }, -}); diff --git a/apps/whatsapp/src/types.ts b/apps/whatsapp/src/types.ts deleted file mode 100644 index 06e4180..0000000 --- a/apps/whatsapp/src/types.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { z } from "zod"; -import type { messagePayloadSchema, messageResponseSchema } from "./schemas"; - -export type MessagePayload = z.infer; -export type MessageResponse = z.infer; From 800e492ac68a2e374b844282d51e9b32a4222899 Mon Sep 17 00:00:00 2001 From: Luis Otavio Date: Sun, 5 Jan 2025 21:34:58 -0300 Subject: [PATCH 23/75] Add PORT configuration to .env.example and clean up message.ts formatting --- apps/messaging/.env.example | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/messaging/.env.example b/apps/messaging/.env.example index 6451bc9..a19137b 100644 --- a/apps/messaging/.env.example +++ b/apps/messaging/.env.example @@ -1 +1,2 @@ -SUPABASE_SERVICE_ROLE_KEY=xxxxx \ No newline at end of file +SUPABASE_SERVICE_ROLE_KEY=xxxxx +PORT=3001 # Remember to modify it in the message.ts from whatsapp \ No newline at end of file From 98a73a126e45c5753697deaa592b98dc8df9f5d8 Mon Sep 17 00:00:00 2001 From: Luis Otavio Date: Sun, 5 Jan 2025 22:13:34 -0300 Subject: [PATCH 24/75] Refactor classifier and grant routes to use API calls instead of crew classes --- .../eda_ai_api/api/routes/classifier.py | 99 ++++++++++--------- apps/ai_api/eda_ai_api/api/routes/grant.py | 10 ++ .../eda_ai_api/models/grant_proposal.py | 5 + apps/messaging/src/routes.ts | 4 - 4 files changed, 67 insertions(+), 51 deletions(-) create mode 100644 apps/ai_api/eda_ai_api/models/grant_proposal.py diff --git a/apps/ai_api/eda_ai_api/api/routes/classifier.py b/apps/ai_api/eda_ai_api/api/routes/classifier.py index 9f6cb37..0b349d7 100644 --- a/apps/ai_api/eda_ai_api/api/routes/classifier.py +++ b/apps/ai_api/eda_ai_api/api/routes/classifier.py @@ -7,9 +7,7 @@ from llama_index.core import PromptTemplate from llama_index.llms.groq import Groq from loguru import logger -from onboarding.crew import OnboardingCrew -from opportunity_finder.crew import OpportunityFinderCrew -from proposal_writer.crew import ProposalWriterCrew +import httpx from eda_ai_api.models.classifier import ClassifierResponse, MessageHistory from eda_ai_api.utils.audio_utils import process_audio_file @@ -83,62 +81,69 @@ async def process_decision( combined_context = "\n\n".join(context_parts) - if decision == "discovery": - topics = await extract_topics(message, zep_history) - logger.info(f"Extracted topics: {topics}") + async with httpx.AsyncClient() as client: + if decision == "discovery": + topics = await extract_topics(message, zep_history) + logger.info(f"Extracted topics: {topics}") - if topics == ["INSUFFICIENT_CONTEXT"]: - response = llm.complete( - INSUFFICIENT_TEMPLATES["discovery"].format( - context=combined_context, message=message + if topics == ["INSUFFICIENT_CONTEXT"]: + response = llm.complete( + INSUFFICIENT_TEMPLATES["discovery"].format( + context=combined_context, message=message + ) ) + processed_response = await process_llm_response(message, response.text) + return {"response": processed_response} + + # Call discovery API instead of crew directly + api_response = await client.post( + "http://127.0.0.1:8083/api/grant/discovery", json={"topics": topics} ) - processed_response = await process_llm_response(message, response.text) + result = api_response.json() + processed_response = await process_llm_response(message, str(result)) return {"response": processed_response} - crew_result = ( - OpportunityFinderCrew().crew().kickoff(inputs={"topics": ", ".join(topics)}) - ) - processed_response = await process_llm_response(message, str(crew_result)) - return {"response": processed_response} - - elif decision == "proposal": - community_project, grant_call = await extract_proposal_details( - message, zep_history - ) - logger.info(f"Project: {community_project}, Grant: {grant_call}") + elif decision == "proposal": + community_project, grant_call = await extract_proposal_details( + message, zep_history + ) + logger.info(f"Project: {community_project}, Grant: {grant_call}") - if community_project == "unknown" or grant_call == "unknown": - response = llm.complete( - INSUFFICIENT_TEMPLATES["proposal"].format( - context=combined_context, message=message + if community_project == "unknown" or grant_call == "unknown": + response = llm.complete( + INSUFFICIENT_TEMPLATES["proposal"].format( + context=combined_context, message=message + ) ) + processed_response = await process_llm_response(message, response.text) + return {"response": processed_response} + + # Call proposal API instead of crew directly + api_response = await client.post( + "http://127.0.0.1:8083/api/grant/proposal", + json={"project": community_project, "grant": grant_call}, ) - processed_response = await process_llm_response(message, response.text) + result = api_response.json() + processed_response = await process_llm_response(message, str(result)) return {"response": processed_response} - crew_result = ( - ProposalWriterCrew() - .crew() - .kickoff(inputs={"project": community_project, "grant": grant_call}) - ) - processed_response = await process_llm_response(message, str(crew_result)) - return {"response": processed_response} - - elif decision == "heartbeat": - processed_response = await process_llm_response( - message, str({"is_alive": True}) - ) - return {"response": processed_response} + elif decision == "heartbeat": + processed_response = await process_llm_response( + message, str({"is_alive": True}) + ) + return {"response": processed_response} - elif decision == "onboarding": - result = OnboardingCrew().crew().kickoff() - processed_response = await process_llm_response(message, str(result)) - logger.info(f"Processed onboarding response: {processed_response}") - return {"response": processed_response} + elif decision == "onboarding": + # Use existing guide endpoint instead of creating new one + api_response = await client.get( + "http://127.0.0.1:8083/api/onboarding/guide" + ) + result = api_response.json() + processed_response = await process_llm_response(message, str(result)) + return {"response": processed_response} - else: - return {"error": f"Unknown decision type: {decision}"} + else: + return {"error": f"Unknown decision type: {decision}"} def format_supabase_history(history: list[MessageHistory]) -> str: diff --git a/apps/ai_api/eda_ai_api/api/routes/grant.py b/apps/ai_api/eda_ai_api/api/routes/grant.py index 9f2fe65..a97929e 100644 --- a/apps/ai_api/eda_ai_api/api/routes/grant.py +++ b/apps/ai_api/eda_ai_api/api/routes/grant.py @@ -1,7 +1,9 @@ from fastapi import APIRouter from opportunity_finder.crew import OpportunityFinderCrew +from proposal_writer.crew import ProposalWriterCrew from eda_ai_api.models.grant_discovery import GrantDiscoveryResult +from eda_ai_api.models.grant_proposal import ProposalWritingResult router = APIRouter() @@ -14,3 +16,11 @@ async def discover_grants(topics: list[str]) -> GrantDiscoveryResult: # process = run() print(crew_result) return result + + +@router.post("/proposal", response_model=ProposalWritingResult, name="proposal") +async def write_proposal(project: str, grant: str) -> ProposalWritingResult: + crew_result = ( + ProposalWriterCrew().crew().kickoff(inputs={"project": project, "grant": grant}) + ) + return ProposalWritingResult(result=str(crew_result)) diff --git a/apps/ai_api/eda_ai_api/models/grant_proposal.py b/apps/ai_api/eda_ai_api/models/grant_proposal.py new file mode 100644 index 0000000..351affd --- /dev/null +++ b/apps/ai_api/eda_ai_api/models/grant_proposal.py @@ -0,0 +1,5 @@ +from pydantic import BaseModel + + +class ProposalWritingResult(BaseModel): + result: str diff --git a/apps/messaging/src/routes.ts b/apps/messaging/src/routes.ts index 6dc5f37..a564f2f 100644 --- a/apps/messaging/src/routes.ts +++ b/apps/messaging/src/routes.ts @@ -38,8 +38,6 @@ async function saveMessageToSupabase( .from("messages") .update({ conversation_history: updatedHistory }) .eq("whatsapp_id", whatsappId); - - if (error) throw error; } else { // Create new conversation const { error } = await supabase.from("messages").insert({ @@ -52,8 +50,6 @@ async function saveMessageToSupabase( }, ], }); - - if (error) throw error; } } catch (error) { logger.error("Error saving to Supabase:", error); From 3ba2471ede398e88394e36ce6541556bda6741dc Mon Sep 17 00:00:00 2001 From: Luis Otavio Date: Thu, 9 Jan 2025 14:55:32 -0300 Subject: [PATCH 25/75] Mem0 initial cfg --- .../eda_ai_api/api/routes/classifier.py | 264 ++++-------------- apps/ai_api/eda_ai_api/models/classifier.py | 3 + apps/ai_api/eda_ai_api/utils/memory.py | 159 ++++++++--- apps/ai_api/pyproject.toml | 5 + apps/ai_api/uv.lock | 102 +++++++ deploy/neo4j-stack/docker-compose.yml | 21 ++ package.json | 1 + 7 files changed, 307 insertions(+), 248 deletions(-) create mode 100644 deploy/neo4j-stack/docker-compose.yml diff --git a/apps/ai_api/eda_ai_api/api/routes/classifier.py b/apps/ai_api/eda_ai_api/api/routes/classifier.py index 0b349d7..216baad 100644 --- a/apps/ai_api/eda_ai_api/api/routes/classifier.py +++ b/apps/ai_api/eda_ai_api/api/routes/classifier.py @@ -1,164 +1,67 @@ import json import os +from typing import Optional, List, Dict import uuid -from typing import Any, Dict, Optional - from fastapi import APIRouter, File, Form, UploadFile -from llama_index.core import PromptTemplate -from llama_index.llms.groq import Groq +from langchain_groq import ChatGroq +from langchain_core.messages import HumanMessage from loguru import logger import httpx from eda_ai_api.models.classifier import ClassifierResponse, MessageHistory from eda_ai_api.utils.audio_utils import process_audio_file -from eda_ai_api.utils.memory import ZepConversationManager +from eda_ai_api.utils.memory import SupabaseMemory from eda_ai_api.utils.prompts import ( + ROUTER_TEMPLATE, INSUFFICIENT_TEMPLATES, PROPOSAL_TEMPLATE, - RESPONSE_PROCESSOR_TEMPLATE, TOPIC_TEMPLATE, ) router = APIRouter() # Setup LLM -llm = Groq( +llm = ChatGroq( model="llama-3.3-70b-versatile", - api_key=os.environ.get("GROQ_API_KEY"), + api_key="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", temperature=0.5, ) -async def extract_topics(message: str, history: list) -> list[str]: - """Extract topics with conversation context""" - context = "\n".join([f"{msg['role']}: {msg['content']}" for msg in history]) - response = llm.complete(TOPIC_TEMPLATE.format(context=context, message=message)) - if response.text.strip() == "INSUFFICIENT_CONTEXT": - return ["INSUFFICIENT_CONTEXT"] - topics = [t.strip() for t in response.text.split(",") if t.strip()][:5] - return topics if topics else ["INSUFFICIENT_CONTEXT"] - - -async def extract_proposal_details(message: str, history: list) -> tuple[str, str]: - """Extract project and grant details with conversation context""" - context = "\n".join([f"{msg['role']}: {msg['content']}" for msg in history]) - response = llm.complete(PROPOSAL_TEMPLATE.format(context=context, message=message)) - extracted = response.text.split("|") - community_project = extracted[0].strip() if len(extracted) > 0 else "unknown" - grant_call = extracted[1].strip() if len(extracted) > 1 else "unknown" - return community_project, grant_call - - -async def process_llm_response(message: str, response: str) -> str: - processed = llm.complete( - RESPONSE_PROCESSOR_TEMPLATE.format(original_message=message, response=response) +async def process_discovery(message: str, context: str) -> Dict[str, str]: + """Process discovery requests""" + response = llm.invoke( + [HumanMessage(content=TOPIC_TEMPLATE.format(context=context, message=message))] ) - logger.info(f"Processed response: {processed.text}") - return processed.text - - -async def process_decision( - decision: str, - message: str, - zep_history: list, - supabase_history: list[MessageHistory] = [], -) -> Dict[str, Any]: - """Process routing decision with conversation context from both sources""" - logger.info(f"Processing decision: {decision} for message: {message}") - - # Combine histories for context - context_parts = [] - - if supabase_history: - supabase_context = format_supabase_history(supabase_history) - context_parts.append(f"Recent conversation:\n{supabase_context}") - - if zep_history: - zep_context = "\n".join( - [f"{msg['role']}: {msg['content']}" for msg in zep_history] - ) - context_parts.append(f"Long-term memory:\n{zep_context}") - - combined_context = "\n\n".join(context_parts) - - async with httpx.AsyncClient() as client: - if decision == "discovery": - topics = await extract_topics(message, zep_history) - logger.info(f"Extracted topics: {topics}") - - if topics == ["INSUFFICIENT_CONTEXT"]: - response = llm.complete( - INSUFFICIENT_TEMPLATES["discovery"].format( - context=combined_context, message=message - ) - ) - processed_response = await process_llm_response(message, response.text) - return {"response": processed_response} - - # Call discovery API instead of crew directly - api_response = await client.post( - "http://127.0.0.1:8083/api/grant/discovery", json={"topics": topics} - ) - result = api_response.json() - processed_response = await process_llm_response(message, str(result)) - return {"response": processed_response} - - elif decision == "proposal": - community_project, grant_call = await extract_proposal_details( - message, zep_history - ) - logger.info(f"Project: {community_project}, Grant: {grant_call}") - - if community_project == "unknown" or grant_call == "unknown": - response = llm.complete( - INSUFFICIENT_TEMPLATES["proposal"].format( - context=combined_context, message=message - ) - ) - processed_response = await process_llm_response(message, response.text) - return {"response": processed_response} - - # Call proposal API instead of crew directly - api_response = await client.post( - "http://127.0.0.1:8083/api/grant/proposal", - json={"project": community_project, "grant": grant_call}, - ) - result = api_response.json() - processed_response = await process_llm_response(message, str(result)) - return {"response": processed_response} - - elif decision == "heartbeat": - processed_response = await process_llm_response( - message, str({"is_alive": True}) - ) - return {"response": processed_response} + topics = [t.strip() for t in response.content.split(",") if t.strip()][:5] + logger.info(f"Extracted Topics: {topics}") - elif decision == "onboarding": - # Use existing guide endpoint instead of creating new one - api_response = await client.get( - "http://127.0.0.1:8083/api/onboarding/guide" + if not topics or topics == ["INSUFFICIENT_CONTEXT"]: + return { + "result": INSUFFICIENT_TEMPLATES["discovery"].format( + context=context, message=message ) - result = api_response.json() - processed_response = await process_llm_response(message, str(result)) - return {"response": processed_response} - - else: - return {"error": f"Unknown decision type: {decision}"} - + } -def format_supabase_history(history: list[MessageHistory]) -> str: - """Format last 10 Supabase messages into conversation format""" - if not history: - return "" + async with httpx.AsyncClient() as client: + logger.info("Calling discovery API...") + api_response = await client.post( + "http://127.0.0.1:8083/api/grant/discovery", json={"topics": topics} + ) + logger.info(f"Discovery API Response: {api_response.json()}") + return {"result": str(api_response.json())} - # Get last 10 messages - limited_history = history[-10:] - formatted = [] - for msg in limited_history: - formatted.extend([f"human: {msg.human}", f"assistant: {msg.ai}"]) +async def route_message(message: str, context: str) -> str: + """Route message to appropriate handler""" + logger.info(f"\n=== Routing Message ===\nContext: {context}\nMessage: {message}") - return "\n".join(formatted[-10:]) # Take last 10 messages total + response = llm.invoke( + [HumanMessage(content=ROUTER_TEMPLATE.format(message=message))] + ) + decision = response.content.lower().strip() + logger.info(f"Router Decision: {decision}") + return decision @router.post("/classify", response_model=ClassifierResponse) @@ -166,97 +69,52 @@ async def classifier_route( message: Optional[str] = Form(default=None), audio: Optional[UploadFile] = File(default=None), session_id: Optional[str] = Form(default=None), - message_history: Optional[str] = Form(default=None), # JSON string + message_history: Optional[str] = Form(default=None), ) -> ClassifierResponse: - """Main route handler with conversation memory""" + """Main route handler""" try: - # Generate a default session_id if none provided - current_session_id = session_id or str(uuid.uuid4()) - user_id = f"{current_session_id}_{uuid.uuid4().hex}" - - logger.info(f"New request - Session: {current_session_id}, User: {user_id}") - - # Initialize both history sources - zep = ZepConversationManager() - zep_session_id = await zep.get_or_create_session( - user_id=user_id, session_id=current_session_id - ) + current_session = session_id or str(uuid.uuid4()) + logger.info(f"New request - Session: {current_session}") # Process inputs - combined_parts = [] - - if audio is not None: + combined_message = [] + if audio: transcription = await process_audio_file(audio) - logger.info(f"Audio transcription: {transcription}") - combined_parts.append(f'Transcription: "{transcription}"') - + combined_message.append(f'Transcription: "{transcription}"') if message: - logger.info(f"Text message: {message}") - combined_parts.append(f'Message: "{message}"') + combined_message.append(f'Message: "{message}"') - combined_message = "\n".join(combined_parts) if not combined_message: return ClassifierResponse(result="Error: No valid input provided") - # Get both conversation histories - zep_history = await zep.get_conversation_history(zep_session_id) - supabase_history = [] + # Get conversation history + history = [] if message_history: try: - supabase_history = [ - MessageHistory(**msg) for msg in json.loads(message_history) - ] + history = [MessageHistory(**msg) for msg in json.loads(message_history)] except json.JSONDecodeError: logger.warning("Invalid message_history JSON format") - # Combine both histories for context - zep_context = "\n".join( - [f"{msg['role']}: {msg['content']}" for msg in zep_history] - ) - supabase_context = format_supabase_history(supabase_history) + # Format context and route message + context = SupabaseMemory.format_history(history) + final_message = "\n".join(combined_message) + decision = await route_message(final_message, context) + logger.info(f"Routing decision: {decision}") - combined_context = f"""Recent conversation:\n{supabase_context}\n\nLong-term memory:\n{zep_context}""" - - logger.info(f"Combined context:\n{combined_context}") - - # Use combined context in router prompt - router_prompt = PromptTemplate( - """Previous conversation:\n{context}\n\n""" - """Given the current user message, determine the appropriate service:""" - """\n{message}\n\n""" - """Return only one word (discovery/proposal/onboarding/heartbeat):""" - ) - - response = llm.complete( - router_prompt.format(context=combined_context, message=combined_message) - ) - decision = response.text.strip().lower() - - # Process decision using combined context - result = await process_decision( - decision, combined_message, zep_history, supabase_history - ) - - # Process final result if it's not already processed - if isinstance(result.get("response"), str): - final_result = await process_llm_response(combined_message, str(result)) + # Process based on route + if decision == "discovery": + response = await process_discovery(final_message, context) + elif decision == "heartbeat": + response = {"result": "*Yes, I'm here! 🟒*\n_Ready to help you!_"} else: - final_result = str(result) - - # Truncate if result exceeds character limit - if len(final_result) > 2499: - logger.warning( - f"Result exceeded 2499 characters (was {len(final_result)}). Truncating..." - ) - final_result = final_result[:2499] + response = {"result": f"Service '{decision}' not implemented yet"} - # Log both result and character count - logger.info(f"Final result ({len(final_result)} chars): {final_result}") + result = response["result"] + if len(result) > 2499: + result = result[:2499] - await zep.add_conversation(zep_session_id, combined_message, final_result) - return ClassifierResponse(result=final_result, session_id=zep_session_id) + return ClassifierResponse(result=result, session_id=current_session) except Exception as e: logger.error(f"Error in classifier route: {str(e)}") - error_msg = await process_llm_response(combined_message, f"Error: {str(e)}") - return ClassifierResponse(result=error_msg) + return ClassifierResponse(result=f"Error: {str(e)}") diff --git a/apps/ai_api/eda_ai_api/models/classifier.py b/apps/ai_api/eda_ai_api/models/classifier.py index 64d9b06..35e40d2 100644 --- a/apps/ai_api/eda_ai_api/models/classifier.py +++ b/apps/ai_api/eda_ai_api/models/classifier.py @@ -8,6 +8,9 @@ class MessageHistory(BaseModel): ai: str timestamp: str + def to_context_format(self) -> str: + return f"Human: {self.human}\nAssistant: {self.ai}" + class ClassifierRequest(BaseModel): message: Optional[str] = None diff --git a/apps/ai_api/eda_ai_api/utils/memory.py b/apps/ai_api/eda_ai_api/utils/memory.py index c821a82..e08b341 100644 --- a/apps/ai_api/eda_ai_api/utils/memory.py +++ b/apps/ai_api/eda_ai_api/utils/memory.py @@ -1,44 +1,76 @@ import uuid +import os from typing import Dict, List, Optional - +from eda_ai_api.models.classifier import MessageHistory from loguru import logger -from zep_python.client import AsyncZep -from zep_python.types import Message +from mem0 import Memory -class ZepConversationManager: - def __init__( - self, api_key: str = "abc123", base_url: str = "http://localhost:3002" - ): - self.client = AsyncZep(api_key=api_key, base_url=base_url, timeout=30) +class Mem0ConversationManager: + def __init__(self): + config = { + "version": "v1.1", # Required version field + "graph_store": { + "provider": "neo4j", + "config": { + "url": "bolt://localhost:7687", + "username": "neo4j", + "password": "password", + }, + "llm": { # Graph store specific LLM + "provider": "groq", + "config": { + "model": "llama-3.3-70b-versatile", + "api_key": os.environ.get("GROQ_API_KEY"), + "temperature": 0.0, + }, + }, + }, + "embedder": { + "provider": "huggingface", + "config": { + "model": "sentence-transformers/all-mpnet-base-v2", + "api_key": os.environ.get("HUGGINGFACE_API_KEY"), + }, + }, + "llm": { # Main LLM + "provider": "groq", + "config": { + "model": "llama-3.3-70b-versatile", + "api_key": os.environ.get("GROQ_API_KEY"), + "temperature": 0, + "max_tokens": 8000, + }, + }, + } + self.memory = Memory.from_config( + config_dict=config + ) # Changed to use config_dict parameter async def get_or_create_session( self, user_id: Optional[str] = None, session_id: Optional[str] = None ) -> str: """Get existing session or create new one""" try: + if not user_id: + user_id = uuid.uuid4().hex + if session_id: - try: - await self.client.memory.get(session_id=session_id) + # Check if session exists by trying to get memories + memories = self.memory.get_all(user_id=session_id) + if memories: logger.info(f"Found existing session: {session_id}") return session_id - except Exception as e: - logger.warning(f"Failed to get session {session_id}: {str(e)}") - # Continue to create new session - - user_id = user_id or uuid.uuid4().hex - await self.client.user.add( - user_id=user_id, metadata={"source": "classifier_api"} - ) - new_session_id = uuid.uuid4().hex - await self.client.memory.add_session( - session_id=new_session_id, user_id=user_id + # Create new session + new_session_id = session_id or uuid.uuid4().hex + # Add initial empty memory to establish session + self.memory.add( + "Session started", + user_id=new_session_id, + metadata={"type": "session_start", "original_user": user_id}, ) - - await self.client.memory.add(session_id=new_session_id, messages=[]) logger.info(f"Created new session: {new_session_id}") - return new_session_id except Exception as e: @@ -48,29 +80,66 @@ async def add_conversation( self, session_id: str, user_message: str, assistant_response: str ) -> None: """Store conversation messages""" - await self.client.memory.add( - session_id=session_id, - messages=[ - Message(role_type="user", content=user_message), - Message(role_type="assistant", content=str(assistant_response)), - ], - ) + try: + # Add user message + self.memory.add( + user_message, + user_id=session_id, + metadata={"role": "user", "timestamp": uuid.uuid1().hex}, + ) + + # Add assistant response + self.memory.add( + assistant_response, + user_id=session_id, + metadata={"role": "assistant", "timestamp": uuid.uuid1().hex}, + ) + except Exception as e: + logger.error(f"Failed to add conversation: {str(e)}") + raise async def get_conversation_history( self, session_id: str, limit: int = 5 ) -> List[Dict[str, str]]: """Get conversation history with detailed logging""" - memory = await self.client.memory.get(session_id=session_id) - - logger.info("\n===== CONVERSATION HISTORY =====") - for idx, msg in enumerate(memory.messages[-limit:], 1): - logger.info(f"\nMessage {idx}:") - logger.info(f"Role: {msg.role_type}") - logger.info(f"Content: {msg.content}") - logger.info("-" * 40) - logger.info("==============================\n") - - return [ - {"role": msg.role_type, "content": msg.content} - for msg in memory.messages[-limit:] - ] + try: + # Get all memories for the session + memories = self.memory.get_all(user_id=session_id) + + # Sort by timestamp and limit + sorted_memories = sorted( + memories, key=lambda x: x.metadata.get("timestamp", "0"), reverse=True + )[:limit] + + # Format for consistency with previous implementation + history = [ + {"role": memory.metadata["role"], "content": memory.text} + for memory in sorted_memories + ] + + # Log the history + logger.info("\n===== CONVERSATION HISTORY =====") + for idx, msg in enumerate(history, 1): + logger.info(f"\nMessage {idx}:") + logger.info(f"Role: {msg['role']}") + logger.info(f"Content: {msg['content']}") + logger.info("-" * 40) + logger.info("==============================\n") + + return history + + except Exception as e: + logger.error(f"Failed to get conversation history: {str(e)}") + return [] + + +class SupabaseMemory: + """Simple memory manager using Supabase for short-term storage""" + + @staticmethod + def format_history(history: List[MessageHistory]) -> str: + """Format conversation history for context""" + if not history: + return "" + formatted_messages = [msg.to_context_format() for msg in history[-5:]] + return "\n\n".join(formatted_messages) diff --git a/apps/ai_api/pyproject.toml b/apps/ai_api/pyproject.toml index ae15718..da74548 100644 --- a/apps/ai_api/pyproject.toml +++ b/apps/ai_api/pyproject.toml @@ -25,6 +25,11 @@ dependencies = [ "llama-index-readers-file>=0.4.1", "httpx>=0.24.0,<0.25.0", "zep-python>=2.0.2", + "mem0ai>=0.1.25", + "rank-bm25>=0.2.2", + "neo4j>=5.27.0", + "langchain>=0.3.4", + "langgraph>=0.2.35", ] [project.optional-dependencies] diff --git a/apps/ai_api/uv.lock b/apps/ai_api/uv.lock index 99189f0..5f18d42 100644 --- a/apps/ai_api/uv.lock +++ b/apps/ai_api/uv.lock @@ -823,19 +823,24 @@ dependencies = [ { name = "ffmpeg-python" }, { name = "httpx" }, { name = "joblib" }, + { name = "langchain" }, { name = "langchain-groq" }, + { name = "langgraph" }, { name = "llama-index" }, { name = "llama-index-embeddings-huggingface" }, { name = "llama-index-llms-groq" }, { name = "llama-index-readers-file" }, { name = "llama-index-vector-stores-postgres" }, { name = "loguru" }, + { name = "mem0ai" }, + { name = "neo4j" }, { name = "numpy" }, { name = "onboarding" }, { name = "opportunity-finder" }, { name = "proposal-writer" }, { name = "pydantic" }, { name = "python-multipart" }, + { name = "rank-bm25" }, { name = "requests" }, { name = "uvicorn" }, { name = "zep-python" }, @@ -862,14 +867,18 @@ requires-dist = [ { name = "httpx", specifier = ">=0.24.0,<0.25.0" }, { name = "isort", marker = "extra == 'dev'", specifier = ">=5.13.2" }, { name = "joblib", specifier = ">=1.3.2" }, + { name = "langchain", specifier = ">=0.3.4" }, { name = "langchain-groq", specifier = ">=0.2.1" }, + { name = "langgraph", specifier = ">=0.2.35" }, { name = "llama-index", specifier = ">=0.12.3" }, { name = "llama-index-embeddings-huggingface", specifier = ">=0.4.0" }, { name = "llama-index-llms-groq", specifier = ">=0.3.0" }, { name = "llama-index-readers-file", specifier = ">=0.4.1" }, { name = "llama-index-vector-stores-postgres", specifier = ">=0.3.2" }, { name = "loguru", specifier = ">=0.7.2" }, + { name = "mem0ai", specifier = ">=0.1.25" }, { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.8.0" }, + { name = "neo4j", specifier = ">=5.27.0" }, { name = "numpy", specifier = ">=1.26.2" }, { name = "onboarding", directory = "../../plugins/onboarding" }, { name = "opportunity-finder", directory = "../../plugins/grant_plugin/opportunity_finder" }, @@ -878,6 +887,7 @@ requires-dist = [ { name = "pytest", marker = "extra == 'dev'", specifier = ">=7.4.3" }, { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=4.1.0" }, { name = "python-multipart", specifier = ">=0.0.17" }, + { name = "rank-bm25", specifier = ">=0.2.2" }, { name = "requests", specifier = ">=2.31.0" }, { name = "uvicorn", specifier = ">=0.25.0" }, { name = "zep-python", specifier = ">=2.0.2" }, @@ -1971,6 +1981,32 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/da/6a/d1303b722a3fa7a0a8c2f8f5307e42f0bdbded46d99cca436f3db0df5294/langchain_text_splitters-0.3.0-py3-none-any.whl", hash = "sha256:e84243e45eaff16e5b776cd9c81b6d07c55c010ebcb1965deb3d1792b7358e83", size = 25543 }, ] +[[package]] +name = "langgraph" +version = "0.2.35" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "langchain-core" }, + { name = "langgraph-checkpoint" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3a/ea/7c40c95000bb8f416568ea9e4fc2187d854d78b4892a074af7e917b7c077/langgraph-0.2.35.tar.gz", hash = "sha256:707752f99c887570a797373584c7412f8c1d23dc6980b3e43e3341aa6122aca9", size = 90726 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/05/b817db973861da51b92191e74ecf307a33c5ed970fa9b7b9eeb4c9a591ae/langgraph-0.2.35-py3-none-any.whl", hash = "sha256:e9dfa85f05bee25b732f7e9ee26f8fc127e937d531762042c83d6c13f39898a3", size = 108658 }, +] + +[[package]] +name = "langgraph-checkpoint" +version = "2.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "langchain-core" }, + { name = "msgpack" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/64/ac32516240491107d43b62f83313b320a4d655314badb4ef3f35efc38025/langgraph_checkpoint-2.0.9.tar.gz", hash = "sha256:43847d7e385a2d9d2b684155920998e44ed42d2d1780719e4f6111fe3d6db84c", size = 33299 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d8/63/b2ecb322ffc978e6bcf27e3786a0efa3142c57d58daeb4e4397196117030/langgraph_checkpoint-2.0.9-py3-none-any.whl", hash = "sha256:b546ed6129929b8941ac08af6ce5cd26c8ebe1d25883d3c48638d34ade91ce42", size = 37318 }, +] + [[package]] name = "langsmith" version = "0.1.137" @@ -2559,6 +2595,47 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198 }, ] +[[package]] +name = "msgpack" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cb/d0/7555686ae7ff5731205df1012ede15dd9d927f6227ea151e901c7406af4f/msgpack-1.1.0.tar.gz", hash = "sha256:dd432ccc2c72b914e4cb77afce64aab761c1137cc698be3984eee260bcb2896e", size = 167260 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/5e/a4c7154ba65d93be91f2f1e55f90e76c5f91ccadc7efc4341e6f04c8647f/msgpack-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3d364a55082fb2a7416f6c63ae383fbd903adb5a6cf78c5b96cc6316dc1cedc7", size = 150803 }, + { url = "https://files.pythonhosted.org/packages/60/c2/687684164698f1d51c41778c838d854965dd284a4b9d3a44beba9265c931/msgpack-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:79ec007767b9b56860e0372085f8504db5d06bd6a327a335449508bbee9648fa", size = 84343 }, + { url = "https://files.pythonhosted.org/packages/42/ae/d3adea9bb4a1342763556078b5765e666f8fdf242e00f3f6657380920972/msgpack-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6ad622bf7756d5a497d5b6836e7fc3752e2dd6f4c648e24b1803f6048596f701", size = 81408 }, + { url = "https://files.pythonhosted.org/packages/dc/17/6313325a6ff40ce9c3207293aee3ba50104aed6c2c1559d20d09e5c1ff54/msgpack-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e59bca908d9ca0de3dc8684f21ebf9a690fe47b6be93236eb40b99af28b6ea6", size = 396096 }, + { url = "https://files.pythonhosted.org/packages/a8/a1/ad7b84b91ab5a324e707f4c9761633e357820b011a01e34ce658c1dda7cc/msgpack-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e1da8f11a3dd397f0a32c76165cf0c4eb95b31013a94f6ecc0b280c05c91b59", size = 403671 }, + { url = "https://files.pythonhosted.org/packages/bb/0b/fd5b7c0b308bbf1831df0ca04ec76fe2f5bf6319833646b0a4bd5e9dc76d/msgpack-1.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:452aff037287acb1d70a804ffd022b21fa2bb7c46bee884dbc864cc9024128a0", size = 387414 }, + { url = "https://files.pythonhosted.org/packages/f0/03/ff8233b7c6e9929a1f5da3c7860eccd847e2523ca2de0d8ef4878d354cfa/msgpack-1.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8da4bf6d54ceed70e8861f833f83ce0814a2b72102e890cbdfe4b34764cdd66e", size = 383759 }, + { url = "https://files.pythonhosted.org/packages/1f/1b/eb82e1fed5a16dddd9bc75f0854b6e2fe86c0259c4353666d7fab37d39f4/msgpack-1.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:41c991beebf175faf352fb940bf2af9ad1fb77fd25f38d9142053914947cdbf6", size = 394405 }, + { url = "https://files.pythonhosted.org/packages/90/2e/962c6004e373d54ecf33d695fb1402f99b51832631e37c49273cc564ffc5/msgpack-1.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a52a1f3a5af7ba1c9ace055b659189f6c669cf3657095b50f9602af3a3ba0fe5", size = 396041 }, + { url = "https://files.pythonhosted.org/packages/f8/20/6e03342f629474414860c48aeffcc2f7f50ddaf351d95f20c3f1c67399a8/msgpack-1.1.0-cp311-cp311-win32.whl", hash = "sha256:58638690ebd0a06427c5fe1a227bb6b8b9fdc2bd07701bec13c2335c82131a88", size = 68538 }, + { url = "https://files.pythonhosted.org/packages/aa/c4/5a582fc9a87991a3e6f6800e9bb2f3c82972912235eb9539954f3e9997c7/msgpack-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fd2906780f25c8ed5d7b323379f6138524ba793428db5d0e9d226d3fa6aa1788", size = 74871 }, + { url = "https://files.pythonhosted.org/packages/e1/d6/716b7ca1dbde63290d2973d22bbef1b5032ca634c3ff4384a958ec3f093a/msgpack-1.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:d46cf9e3705ea9485687aa4001a76e44748b609d260af21c4ceea7f2212a501d", size = 152421 }, + { url = "https://files.pythonhosted.org/packages/70/da/5312b067f6773429cec2f8f08b021c06af416bba340c912c2ec778539ed6/msgpack-1.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5dbad74103df937e1325cc4bfeaf57713be0b4f15e1c2da43ccdd836393e2ea2", size = 85277 }, + { url = "https://files.pythonhosted.org/packages/28/51/da7f3ae4462e8bb98af0d5bdf2707f1b8c65a0d4f496e46b6afb06cbc286/msgpack-1.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:58dfc47f8b102da61e8949708b3eafc3504509a5728f8b4ddef84bd9e16ad420", size = 82222 }, + { url = "https://files.pythonhosted.org/packages/33/af/dc95c4b2a49cff17ce47611ca9ba218198806cad7796c0b01d1e332c86bb/msgpack-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4676e5be1b472909b2ee6356ff425ebedf5142427842aa06b4dfd5117d1ca8a2", size = 392971 }, + { url = "https://files.pythonhosted.org/packages/f1/54/65af8de681fa8255402c80eda2a501ba467921d5a7a028c9c22a2c2eedb5/msgpack-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17fb65dd0bec285907f68b15734a993ad3fc94332b5bb21b0435846228de1f39", size = 401403 }, + { url = "https://files.pythonhosted.org/packages/97/8c/e333690777bd33919ab7024269dc3c41c76ef5137b211d776fbb404bfead/msgpack-1.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a51abd48c6d8ac89e0cfd4fe177c61481aca2d5e7ba42044fd218cfd8ea9899f", size = 385356 }, + { url = "https://files.pythonhosted.org/packages/57/52/406795ba478dc1c890559dd4e89280fa86506608a28ccf3a72fbf45df9f5/msgpack-1.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2137773500afa5494a61b1208619e3871f75f27b03bcfca7b3a7023284140247", size = 383028 }, + { url = "https://files.pythonhosted.org/packages/e7/69/053b6549bf90a3acadcd8232eae03e2fefc87f066a5b9fbb37e2e608859f/msgpack-1.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:398b713459fea610861c8a7b62a6fec1882759f308ae0795b5413ff6a160cf3c", size = 391100 }, + { url = "https://files.pythonhosted.org/packages/23/f0/d4101d4da054f04274995ddc4086c2715d9b93111eb9ed49686c0f7ccc8a/msgpack-1.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:06f5fd2f6bb2a7914922d935d3b8bb4a7fff3a9a91cfce6d06c13bc42bec975b", size = 394254 }, + { url = "https://files.pythonhosted.org/packages/1c/12/cf07458f35d0d775ff3a2dc5559fa2e1fcd06c46f1ef510e594ebefdca01/msgpack-1.1.0-cp312-cp312-win32.whl", hash = "sha256:ad33e8400e4ec17ba782f7b9cf868977d867ed784a1f5f2ab46e7ba53b6e1e1b", size = 69085 }, + { url = "https://files.pythonhosted.org/packages/73/80/2708a4641f7d553a63bc934a3eb7214806b5b39d200133ca7f7afb0a53e8/msgpack-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:115a7af8ee9e8cddc10f87636767857e7e3717b7a2e97379dc2054712693e90f", size = 75347 }, + { url = "https://files.pythonhosted.org/packages/c8/b0/380f5f639543a4ac413e969109978feb1f3c66e931068f91ab6ab0f8be00/msgpack-1.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:071603e2f0771c45ad9bc65719291c568d4edf120b44eb36324dcb02a13bfddf", size = 151142 }, + { url = "https://files.pythonhosted.org/packages/c8/ee/be57e9702400a6cb2606883d55b05784fada898dfc7fd12608ab1fdb054e/msgpack-1.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0f92a83b84e7c0749e3f12821949d79485971f087604178026085f60ce109330", size = 84523 }, + { url = "https://files.pythonhosted.org/packages/7e/3a/2919f63acca3c119565449681ad08a2f84b2171ddfcff1dba6959db2cceb/msgpack-1.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4a1964df7b81285d00a84da4e70cb1383f2e665e0f1f2a7027e683956d04b734", size = 81556 }, + { url = "https://files.pythonhosted.org/packages/7c/43/a11113d9e5c1498c145a8925768ea2d5fce7cbab15c99cda655aa09947ed/msgpack-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59caf6a4ed0d164055ccff8fe31eddc0ebc07cf7326a2aaa0dbf7a4001cd823e", size = 392105 }, + { url = "https://files.pythonhosted.org/packages/2d/7b/2c1d74ca6c94f70a1add74a8393a0138172207dc5de6fc6269483519d048/msgpack-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0907e1a7119b337971a689153665764adc34e89175f9a34793307d9def08e6ca", size = 399979 }, + { url = "https://files.pythonhosted.org/packages/82/8c/cf64ae518c7b8efc763ca1f1348a96f0e37150061e777a8ea5430b413a74/msgpack-1.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:65553c9b6da8166e819a6aa90ad15288599b340f91d18f60b2061f402b9a4915", size = 383816 }, + { url = "https://files.pythonhosted.org/packages/69/86/a847ef7a0f5ef3fa94ae20f52a4cacf596a4e4a010197fbcc27744eb9a83/msgpack-1.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7a946a8992941fea80ed4beae6bff74ffd7ee129a90b4dd5cf9c476a30e9708d", size = 380973 }, + { url = "https://files.pythonhosted.org/packages/aa/90/c74cf6e1126faa93185d3b830ee97246ecc4fe12cf9d2d31318ee4246994/msgpack-1.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4b51405e36e075193bc051315dbf29168d6141ae2500ba8cd80a522964e31434", size = 387435 }, + { url = "https://files.pythonhosted.org/packages/7a/40/631c238f1f338eb09f4acb0f34ab5862c4e9d7eda11c1b685471a4c5ea37/msgpack-1.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4c01941fd2ff87c2a934ee6055bda4ed353a7846b8d4f341c428109e9fcde8c", size = 399082 }, + { url = "https://files.pythonhosted.org/packages/e9/1b/fa8a952be252a1555ed39f97c06778e3aeb9123aa4cccc0fd2acd0b4e315/msgpack-1.1.0-cp313-cp313-win32.whl", hash = "sha256:7c9a35ce2c2573bada929e0b7b3576de647b0defbd25f5139dcdaba0ae35a4cc", size = 69037 }, + { url = "https://files.pythonhosted.org/packages/b6/bc/8bd826dd03e022153bfa1766dcdec4976d6c818865ed54223d71f07862b3/msgpack-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:bce7d9e614a04d0883af0b3d4d501171fbfca038f12c77fa838d9f198147a23f", size = 75140 }, +] + [[package]] name = "multidict" version = "6.1.0" @@ -2650,6 +2727,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 }, ] +[[package]] +name = "neo4j" +version = "5.27.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytz" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9c/b6/31ed9f3df8b948aecd31952193b803cd2007551f2f81aaa60d264e8d1950/neo4j-5.27.0.tar.gz", hash = "sha256:f82ee807cd15b178898d83f41a66372e11719a25dd487fd7bea48fd4b7323765", size = 223803 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/9a/cec819e5887804896db67b13215682b15e6f9201d9cf63890fee2b9a29e9/neo4j-5.27.0-py3-none-any.whl", hash = "sha256:929c14b9e5341267324eca170b39d1798b032bffacc26a0529eacaf678ae483f", size = 301679 }, +] + [[package]] name = "nest-asyncio" version = "1.6.0" @@ -3506,6 +3595,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ce/ac/5b1ea50fc08a9df82de7e1771537557f07c2632231bbab652c7e22597908/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bb89f0a835bcfc1d42ccd5f41f04870c1b936d8507c6df12b7737febc40f0909", size = 2822712 }, { url = "https://files.pythonhosted.org/packages/c4/fc/504d4503b2abc4570fac3ca56eb8fed5e437bf9c9ef13f36b6621db8ef00/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f0c2d907a1e102526dd2986df638343388b94c33860ff3bbe1384130828714b1", size = 2920155 }, { url = "https://files.pythonhosted.org/packages/b2/d1/323581e9273ad2c0dbd1902f3fb50c441da86e894b6e25a73c3fda32c57e/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f8157bed2f51db683f31306aa497311b560f2265998122abe1dce6428bd86567", size = 2959356 }, + { url = "https://files.pythonhosted.org/packages/08/50/d13ea0a054189ae1bc21af1d85b6f8bb9bbc5572991055d70ad9006fe2d6/psycopg2_binary-2.9.10-cp313-cp313-win_amd64.whl", hash = "sha256:27422aa5f11fbcd9b18da48373eb67081243662f9b46e6fd07c3eb46e4535142", size = 2569224 }, ] [[package]] @@ -3971,6 +4061,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/68/c0/eef4fe9dad6d41333f7dc6567fa8144ffc1837c8a0edfc2317d50715335f/qdrant_client-1.12.1-py3-none-any.whl", hash = "sha256:b2d17ce18e9e767471368380dd3bbc4a0e3a0e2061fedc9af3542084b48451e0", size = 267171 }, ] +[[package]] +name = "rank-bm25" +version = "0.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/0a/f9579384aa017d8b4c15613f86954b92a95a93d641cc849182467cf0bb3b/rank_bm25-0.2.2.tar.gz", hash = "sha256:096ccef76f8188563419aaf384a02f0ea459503fdf77901378d4fd9d87e5e51d", size = 8347 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/21/f691fb2613100a62b3fa91e9988c991e9ca5b89ea31c0d3152a3210344f9/rank_bm25-0.2.2-py3-none-any.whl", hash = "sha256:7bd4a95571adadfc271746fa146a4bcfd89c0cf731e49c3d1ad863290adbe8ae", size = 8584 }, +] + [[package]] name = "referencing" version = "0.35.1" diff --git a/deploy/neo4j-stack/docker-compose.yml b/deploy/neo4j-stack/docker-compose.yml new file mode 100644 index 0000000..0a337a4 --- /dev/null +++ b/deploy/neo4j-stack/docker-compose.yml @@ -0,0 +1,21 @@ +services: + neo4j: + image: neo4j:latest + container_name: eda-neo4j + ports: + - "7474:7474" # HTTP + - "7687:7687" # Bolt + environment: + - NEO4J_AUTH=neo4j/password + - NEO4J_PLUGINS=["apoc"] + volumes: + - neo4j_data:/data + healthcheck: + test: ["CMD-SHELL", "wget -O /dev/null -q http://localhost:7474 || exit 1"] + interval: 10s + timeout: 5s + retries: 3 + restart: always + +volumes: + neo4j_data: \ No newline at end of file diff --git a/package.json b/package.json index c3f968b..b247d8c 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "clean:workspaces": "turbo clean", "deploy:trigger": "docker compose -f deploy/trigger-stack/docker-compose.yml up -d", "deploy:langtrace": "docker compose -f deploy/langtrace-stack/docker-compose.yml up -d", + "deploy:neo4j": "docker compose -f deploy/neo4j-stack/docker-compose.yml up -d", "status": "bun status", "start:dashboard": "turbo start --filter=@eda/dashboard", "test": "turbo test --parallel", From 70b1436727b820d498e1d34e0b10a9557422adb5 Mon Sep 17 00:00:00 2001 From: Luis Otavio Date: Fri, 10 Jan 2025 21:14:40 -0300 Subject: [PATCH 26/75] Integrate Mem0ConversationManager for enhanced session management and update API key handling --- .../eda_ai_api/api/routes/classifier.py | 30 ++++++++++++---- apps/ai_api/eda_ai_api/utils/memory.py | 36 ++++++++++--------- apps/ai_api/pyproject.toml | 1 + apps/ai_api/uv.lock | 20 +++++++++++ 4 files changed, 63 insertions(+), 24 deletions(-) diff --git a/apps/ai_api/eda_ai_api/api/routes/classifier.py b/apps/ai_api/eda_ai_api/api/routes/classifier.py index 216baad..232ae45 100644 --- a/apps/ai_api/eda_ai_api/api/routes/classifier.py +++ b/apps/ai_api/eda_ai_api/api/routes/classifier.py @@ -10,7 +10,7 @@ from eda_ai_api.models.classifier import ClassifierResponse, MessageHistory from eda_ai_api.utils.audio_utils import process_audio_file -from eda_ai_api.utils.memory import SupabaseMemory +from eda_ai_api.utils.memory import SupabaseMemory, Mem0ConversationManager from eda_ai_api.utils.prompts import ( ROUTER_TEMPLATE, INSUFFICIENT_TEMPLATES, @@ -19,11 +19,12 @@ ) router = APIRouter() +mem0_manager = Mem0ConversationManager() # Setup LLM llm = ChatGroq( model="llama-3.3-70b-versatile", - api_key="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", + api_key=os.environ.get("GROQ_API_KEY"), # Use environment variable temperature=0.5, ) @@ -76,6 +77,11 @@ async def classifier_route( current_session = session_id or str(uuid.uuid4()) logger.info(f"New request - Session: {current_session}") + # Initialize/get Mem0 session + current_session = await mem0_manager.get_or_create_session( + session_id=current_session + ) + # Process inputs combined_message = [] if audio: @@ -87,7 +93,7 @@ async def classifier_route( if not combined_message: return ClassifierResponse(result="Error: No valid input provided") - # Get conversation history + # Get conversation history from both systems history = [] if message_history: try: @@ -95,15 +101,18 @@ async def classifier_route( except json.JSONDecodeError: logger.warning("Invalid message_history JSON format") - # Format context and route message - context = SupabaseMemory.format_history(history) + # Get long-term context from Mem0 + mem0_history = await mem0_manager.get_conversation_history(current_session) + + # Combine contexts + supabase_context = SupabaseMemory.format_history(history) final_message = "\n".join(combined_message) - decision = await route_message(final_message, context) + decision = await route_message(final_message, supabase_context) logger.info(f"Routing decision: {decision}") # Process based on route if decision == "discovery": - response = await process_discovery(final_message, context) + response = await process_discovery(final_message, supabase_context) elif decision == "heartbeat": response = {"result": "*Yes, I'm here! 🟒*\n_Ready to help you!_"} else: @@ -113,6 +122,13 @@ async def classifier_route( if len(result) > 2499: result = result[:2499] + # Store interaction in Mem0 + await mem0_manager.add_conversation( + session_id=current_session, + user_message=final_message, + assistant_response=result, + ) + return ClassifierResponse(result=result, session_id=current_session) except Exception as e: diff --git a/apps/ai_api/eda_ai_api/utils/memory.py b/apps/ai_api/eda_ai_api/utils/memory.py index e08b341..8127037 100644 --- a/apps/ai_api/eda_ai_api/utils/memory.py +++ b/apps/ai_api/eda_ai_api/utils/memory.py @@ -1,15 +1,28 @@ import uuid import os from typing import Dict, List, Optional +from dotenv import load_dotenv from eda_ai_api.models.classifier import MessageHistory from loguru import logger from mem0 import Memory +# Load environment variables +load_dotenv() + class Mem0ConversationManager: def __init__(self): + # Validate API keys + groq_api_key = os.getenv("GROQ_API_KEY") + huggingface_api_key = os.getenv("HUGGINGFACE_API_KEY") + + if not huggingface_api_key: + raise ValueError("HUGGINGFACE_API_KEY environment variable is not set") + if not groq_api_key: + raise ValueError("GROQ_API_KEY environment variable is not set") + config = { - "version": "v1.1", # Required version field + "version": "v1.1", "graph_store": { "provider": "neo4j", "config": { @@ -17,35 +30,24 @@ def __init__(self): "username": "neo4j", "password": "password", }, - "llm": { # Graph store specific LLM - "provider": "groq", - "config": { - "model": "llama-3.3-70b-versatile", - "api_key": os.environ.get("GROQ_API_KEY"), - "temperature": 0.0, - }, - }, }, "embedder": { "provider": "huggingface", "config": { "model": "sentence-transformers/all-mpnet-base-v2", - "api_key": os.environ.get("HUGGINGFACE_API_KEY"), + "api_key": huggingface_api_key, }, }, - "llm": { # Main LLM + "llm": { "provider": "groq", "config": { "model": "llama-3.3-70b-versatile", - "api_key": os.environ.get("GROQ_API_KEY"), - "temperature": 0, - "max_tokens": 8000, + "api_key": groq_api_key, + "max_tokens": 1000, }, }, } - self.memory = Memory.from_config( - config_dict=config - ) # Changed to use config_dict parameter + self.memory = Memory.from_config(config_dict=config) async def get_or_create_session( self, user_id: Optional[str] = None, session_id: Optional[str] = None diff --git a/apps/ai_api/pyproject.toml b/apps/ai_api/pyproject.toml index da74548..d036c1e 100644 --- a/apps/ai_api/pyproject.toml +++ b/apps/ai_api/pyproject.toml @@ -30,6 +30,7 @@ dependencies = [ "neo4j>=5.27.0", "langchain>=0.3.4", "langgraph>=0.2.35", + "anthropic>=0.42.0", ] [project.optional-dependencies] diff --git a/apps/ai_api/uv.lock b/apps/ai_api/uv.lock index 5f18d42..2154df1 100644 --- a/apps/ai_api/uv.lock +++ b/apps/ai_api/uv.lock @@ -128,6 +128,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, ] +[[package]] +name = "anthropic" +version = "0.42.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e7/7c/91b79f5ae4a52497a4e330d66ea5929aec2878ee2c9f8a998dbe4f4c7f01/anthropic-0.42.0.tar.gz", hash = "sha256:bf8b0ed8c8cb2c2118038f29c58099d2f99f7847296cafdaa853910bfff4edf4", size = 192361 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/33/b907a6d27dd0d8d3adb4edb5c9e9c85a189719ec6855051cce3814c8ef13/anthropic-0.42.0-py3-none-any.whl", hash = "sha256:46775f65b723c078a2ac9e9de44a46db5c6a4fabeacfd165e5ea78e6817f4eff", size = 203365 }, +] + [[package]] name = "anyio" version = "4.6.2.post1" @@ -819,6 +837,7 @@ name = "eda-ai-api" version = "0.1.0" source = { editable = "." } dependencies = [ + { name = "anthropic" }, { name = "fastapi" }, { name = "ffmpeg-python" }, { name = "httpx" }, @@ -859,6 +878,7 @@ dev = [ [package.metadata] requires-dist = [ + { name = "anthropic", specifier = ">=0.42.0" }, { name = "bandit", marker = "extra == 'dev'", specifier = ">=1.7.6" }, { name = "black", marker = "extra == 'dev'", specifier = ">=24.3.0" }, { name = "fastapi", specifier = ">=0.109.1" }, From 18fec064696d2b13a7aaf42d2c01d47e33c93279 Mon Sep 17 00:00:00 2001 From: Luis Otavio Date: Sat, 11 Jan 2025 17:00:00 -0300 Subject: [PATCH 27/75] Disable Mem0 memory manager and update user ID references in Supabase interactions --- .../eda_ai_api/api/routes/classifier.py | 28 ++++++++----------- apps/messaging/src/routes.ts | 10 +++---- 2 files changed, 17 insertions(+), 21 deletions(-) diff --git a/apps/ai_api/eda_ai_api/api/routes/classifier.py b/apps/ai_api/eda_ai_api/api/routes/classifier.py index 232ae45..4e1a3e2 100644 --- a/apps/ai_api/eda_ai_api/api/routes/classifier.py +++ b/apps/ai_api/eda_ai_api/api/routes/classifier.py @@ -19,7 +19,8 @@ ) router = APIRouter() -mem0_manager = Mem0ConversationManager() +# Disabled Mem0 memory manager +# mem0_manager = Mem0ConversationManager() # Setup LLM llm = ChatGroq( @@ -77,10 +78,8 @@ async def classifier_route( current_session = session_id or str(uuid.uuid4()) logger.info(f"New request - Session: {current_session}") - # Initialize/get Mem0 session - current_session = await mem0_manager.get_or_create_session( - session_id=current_session - ) + # Initialize/get Mem0 session - Disabled + # current_session = await mem0_manager.get_or_create_session(session_id=current_session) # Process inputs combined_message = [] @@ -93,7 +92,7 @@ async def classifier_route( if not combined_message: return ClassifierResponse(result="Error: No valid input provided") - # Get conversation history from both systems + # Get conversation history from Supabase history = [] if message_history: try: @@ -101,10 +100,7 @@ async def classifier_route( except json.JSONDecodeError: logger.warning("Invalid message_history JSON format") - # Get long-term context from Mem0 - mem0_history = await mem0_manager.get_conversation_history(current_session) - - # Combine contexts + # Get context from Supabase only supabase_context = SupabaseMemory.format_history(history) final_message = "\n".join(combined_message) decision = await route_message(final_message, supabase_context) @@ -122,12 +118,12 @@ async def classifier_route( if len(result) > 2499: result = result[:2499] - # Store interaction in Mem0 - await mem0_manager.add_conversation( - session_id=current_session, - user_message=final_message, - assistant_response=result, - ) + # Disabled Mem0 storage + # await mem0_manager.add_conversation( + # session_id=current_session, + # user_message=final_message, + # assistant_response=result, + # ) return ClassifierResponse(result=result, session_id=current_session) diff --git a/apps/messaging/src/routes.ts b/apps/messaging/src/routes.ts index a564f2f..180e520 100644 --- a/apps/messaging/src/routes.ts +++ b/apps/messaging/src/routes.ts @@ -10,7 +10,7 @@ const supabase = createClient( const AI_API_URL = process.env.AI_API_URL || "http://127.0.0.1:8083"; async function saveMessageToSupabase( - whatsappId: string, + userId: string, userMessage: string, aiResponse: string, ) { @@ -19,7 +19,7 @@ async function saveMessageToSupabase( const { data: existingConversation } = await supabase .from("messages") .select("conversation_history") - .eq("whatsapp_id", whatsappId) + .eq("user_id", userId) .single(); if (existingConversation) { @@ -37,11 +37,11 @@ async function saveMessageToSupabase( const { error } = await supabase .from("messages") .update({ conversation_history: updatedHistory }) - .eq("whatsapp_id", whatsappId); + .eq("user_id", userId); } else { // Create new conversation const { error } = await supabase.from("messages").insert({ - whatsapp_id: whatsappId, + user_id: userId, conversation_history: [ { human: userMessage, @@ -87,7 +87,7 @@ export async function handleSendMessage(req: Request) { const { data: existingConversation } = await supabase .from("messages") .select("conversation_history") - .eq("whatsapp_id", payload.sessionId) + .eq("user_id", payload.sessionId) .single(); if (existingConversation?.conversation_history) { From e75841bcc9fbe0dcc2d8b9b4d37f001df14111ca Mon Sep 17 00:00:00 2001 From: Luis Otavio Date: Sat, 11 Jan 2025 17:12:16 -0300 Subject: [PATCH 28/75] Add platform support to classifier and messaging routes --- .../eda_ai_api/api/routes/classifier.py | 19 ++++++++++++++++--- apps/ai_api/eda_ai_api/models/classifier.py | 1 + apps/ai_api/eda_ai_api/utils/prompts.py | 6 +++--- apps/messaging/src/routes.ts | 3 +++ apps/messaging/src/types.ts | 1 + apps/whatsapp/src/message/message.ts | 2 ++ 6 files changed, 26 insertions(+), 6 deletions(-) diff --git a/apps/ai_api/eda_ai_api/api/routes/classifier.py b/apps/ai_api/eda_ai_api/api/routes/classifier.py index 4e1a3e2..bd247e1 100644 --- a/apps/ai_api/eda_ai_api/api/routes/classifier.py +++ b/apps/ai_api/eda_ai_api/api/routes/classifier.py @@ -30,7 +30,9 @@ ) -async def process_discovery(message: str, context: str) -> Dict[str, str]: +async def process_discovery( + message: str, context: str, platform: str = "default" +) -> Dict[str, str]: """Process discovery requests""" response = llm.invoke( [HumanMessage(content=TOPIC_TEMPLATE.format(context=context, message=message))] @@ -48,7 +50,8 @@ async def process_discovery(message: str, context: str) -> Dict[str, str]: async with httpx.AsyncClient() as client: logger.info("Calling discovery API...") api_response = await client.post( - "http://127.0.0.1:8083/api/grant/discovery", json={"topics": topics} + "http://127.0.0.1:8083/api/grant/discovery", + json={"topics": topics, "platform": platform}, ) logger.info(f"Discovery API Response: {api_response.json()}") return {"result": str(api_response.json())} @@ -72,11 +75,13 @@ async def classifier_route( audio: Optional[UploadFile] = File(default=None), session_id: Optional[str] = Form(default=None), message_history: Optional[str] = Form(default=None), + platform: Optional[str] = Form(default="default"), ) -> ClassifierResponse: """Main route handler""" try: current_session = session_id or str(uuid.uuid4()) logger.info(f"New request - Session: {current_session}") + logger.info(f"Platform received: {platform}") # Add this line # Initialize/get Mem0 session - Disabled # current_session = await mem0_manager.get_or_create_session(session_id=current_session) @@ -106,9 +111,15 @@ async def classifier_route( decision = await route_message(final_message, supabase_context) logger.info(f"Routing decision: {decision}") + # Update logging when processing route decision + logger.info(f"Processing {decision} request for platform: {platform}") + # Process based on route if decision == "discovery": - response = await process_discovery(final_message, supabase_context) + response = await process_discovery( + final_message, supabase_context, platform + ) + logger.info(f"Discovery response for platform {platform}: {response}") elif decision == "heartbeat": response = {"result": "*Yes, I'm here! 🟒*\n_Ready to help you!_"} else: @@ -125,6 +136,8 @@ async def classifier_route( # assistant_response=result, # ) + response = {"result": result, "platform": platform} + return ClassifierResponse(result=result, session_id=current_session) except Exception as e: diff --git a/apps/ai_api/eda_ai_api/models/classifier.py b/apps/ai_api/eda_ai_api/models/classifier.py index 35e40d2..8491bb3 100644 --- a/apps/ai_api/eda_ai_api/models/classifier.py +++ b/apps/ai_api/eda_ai_api/models/classifier.py @@ -17,6 +17,7 @@ class ClassifierRequest(BaseModel): audio: Optional[UploadFile] = None session_id: Optional[str] = None message_history: Optional[List[MessageHistory]] = None + platform: Optional[str] = None # Add platform field class Config: arbitrary_types_allowed = True diff --git a/apps/ai_api/eda_ai_api/utils/prompts.py b/apps/ai_api/eda_ai_api/utils/prompts.py index 732b3a0..9429325 100644 --- a/apps/ai_api/eda_ai_api/utils/prompts.py +++ b/apps/ai_api/eda_ai_api/utils/prompts.py @@ -73,14 +73,14 @@ RESPONSE_PROCESSOR_TEMPLATE = PromptTemplate( """IMPORTANT: You must respond in exactly the same language as the user's - original message: + original message. User is on {platform} platform. {original_message} Process this response to: 1. MATCH THE EXACT LANGUAGE of the input message (this is crucial!) 2. Be clear and conversational in that language 3. Not exceed 2000 characters (summarize if longer) -4. Use WhatsApp formatting: +4. Use {platform} formatting: - *bold* for important terms - _italic_ for emphasis - ```code``` for technical terms @@ -98,5 +98,5 @@ Original response: {response} -Respond in the SAME LANGUAGE as the original message with WhatsApp formatting:""" +Respond in the SAME LANGUAGE as the original message with {platform} formatting:""" ) diff --git a/apps/messaging/src/routes.ts b/apps/messaging/src/routes.ts index 180e520..d54118c 100644 --- a/apps/messaging/src/routes.ts +++ b/apps/messaging/src/routes.ts @@ -72,6 +72,9 @@ export async function handleSendMessage(req: Request) { const formData = new FormData(); formData.append("message", payload.message); formData.append("session_id", payload.sessionId); + if (payload.platform) { + formData.append("platform", payload.platform); + } if (payload.audio) { const binaryStr = atob(payload.audio); diff --git a/apps/messaging/src/types.ts b/apps/messaging/src/types.ts index 349b0e4..3832f5b 100644 --- a/apps/messaging/src/types.ts +++ b/apps/messaging/src/types.ts @@ -4,6 +4,7 @@ export const messageSchema = z.object({ message: z.string(), sessionId: z.string(), audio: z.string().optional(), // Changed from Blob to string + platform: z.enum(["whatsapp", "telegram", "simulator"]).optional(), }); export const messageResponseSchema = z.object({ diff --git a/apps/whatsapp/src/message/message.ts b/apps/whatsapp/src/message/message.ts index e91ab7f..06101ed 100644 --- a/apps/whatsapp/src/message/message.ts +++ b/apps/whatsapp/src/message/message.ts @@ -21,6 +21,7 @@ interface MessagePayload { message: string; sessionId: string; audio?: string; // Changed from Blob to string + platform?: string; } export async function handleMessage(message: WAMessage) { @@ -50,6 +51,7 @@ export async function handleMessage(message: WAMessage) { const payload: MessagePayload = { message: messageContent, sessionId: phoneNumber, + platform: "whatsapp", // Add platform identifier }; if (message.message?.imageMessage || message.message?.audioMessage) { From 2290309c82c48f7c414f63d0e1089117095e893d Mon Sep 17 00:00:00 2001 From: Luis Otavio Date: Sat, 11 Jan 2025 17:31:17 -0300 Subject: [PATCH 29/75] Add Supabase ID handling for message processing and storage --- apps/messaging/src/routes.ts | 47 ++++++++++++++++++++++++++++++------ 1 file changed, 39 insertions(+), 8 deletions(-) diff --git a/apps/messaging/src/routes.ts b/apps/messaging/src/routes.ts index d54118c..410cc7e 100644 --- a/apps/messaging/src/routes.ts +++ b/apps/messaging/src/routes.ts @@ -9,6 +9,35 @@ const supabase = createClient( const AI_API_URL = process.env.AI_API_URL || "http://127.0.0.1:8083"; +// Add new function to get/create Supabase ID +async function getSupabaseId(whatsappId: string): Promise { + try { + // First check if user exists + const { data: existingUser } = await supabase + .from("messages") + .select("id") + .eq("whatsapp_id", whatsappId) + .single(); + + if (existingUser) { + return existingUser.id; + } + + // If not exists, create new user + const { data: newUser, error } = await supabase + .from("messages") + .insert([{ whatsapp_id: whatsappId }]) + .select("id") + .single(); + + if (error) throw error; + return newUser.id; + } catch (error) { + logger.error("Error getting Supabase ID:", error); + throw error; + } +} + async function saveMessageToSupabase( userId: string, userMessage: string, @@ -57,21 +86,23 @@ async function saveMessageToSupabase( } } +// Modify handleSendMessage export async function handleSendMessage(req: Request) { try { - logger.info("Received message request"); - // Access body directly from Elysia's parsed request const payload = messageSchema.parse(req.body); - logger.info("Received message request", { - sessionId: payload.sessionId, + // Get Supabase ID from WhatsApp ID + const supabaseId = await getSupabaseId(payload.sessionId); + + logger.info("Processing message", { + whatsappId: payload.sessionId, + supabaseId, hasAudio: !!payload.audio, }); - // Create FormData for AI API const formData = new FormData(); formData.append("message", payload.message); - formData.append("session_id", payload.sessionId); + formData.append("session_id", supabaseId); // Use Supabase ID instead if (payload.platform) { formData.append("platform", payload.platform); } @@ -90,7 +121,7 @@ export async function handleSendMessage(req: Request) { const { data: existingConversation } = await supabase .from("messages") .select("conversation_history") - .eq("user_id", payload.sessionId) + .eq("user_id", supabaseId) // Use Supabase ID .single(); if (existingConversation?.conversation_history) { @@ -117,7 +148,7 @@ export async function handleSendMessage(req: Request) { // Save message to Supabase after successful AI response await saveMessageToSupabase( - payload.sessionId, + supabaseId, // Use Supabase ID payload.message, data.result, ); From 96e0e5fd4bbaded91a86afaef68750cd8f2361c5 Mon Sep 17 00:00:00 2001 From: Luis Otavio Date: Thu, 16 Jan 2025 07:38:46 -0300 Subject: [PATCH 30/75] Implement API call monitoring and logging for message processing --- apps/messaging/src/routes.ts | 26 +++++++++++++++ packages/jobs/trigger/monitor-api-call.ts | 40 +++++++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 packages/jobs/trigger/monitor-api-call.ts diff --git a/apps/messaging/src/routes.ts b/apps/messaging/src/routes.ts index 410cc7e..c6266b6 100644 --- a/apps/messaging/src/routes.ts +++ b/apps/messaging/src/routes.ts @@ -1,5 +1,6 @@ import { logger } from "@eda/logger"; import { createClient } from "@supabase/supabase-js"; +import { tasks } from "@trigger.dev/sdk/v3"; import { messageSchema, queryParamsSchema } from "./types"; const supabase = createClient( @@ -88,6 +89,7 @@ async function saveMessageToSupabase( // Modify handleSendMessage export async function handleSendMessage(req: Request) { + const startTime = performance.now(); try { const payload = messageSchema.parse(req.body); @@ -141,6 +143,17 @@ export async function handleSendMessage(req: Request) { }); const data = await response.json(); + const duration = performance.now() - startTime; + + // Monitor API call using Trigger.dev + await tasks.trigger("monitor-api-call", { + endpoint: "/api/classifier/classify", + method: "POST", + statusCode: response.status, + duration, + timestamp: new Date().toISOString(), + sessionId: payload.sessionId, + }); if (!response.ok) { throw new Error(data.message || "Failed to process message"); @@ -159,6 +172,19 @@ export async function handleSendMessage(req: Request) { return Response.json(data); } catch (error) { + const duration = performance.now() - startTime; + + // Monitor failed API calls + await tasks.trigger("monitor-api-call", { + endpoint: "/api/classifier/classify", + method: "POST", + statusCode: 500, + duration, + timestamp: new Date().toISOString(), + sessionId: payload.sessionId, + error: error instanceof Error ? error.message : "Unknown error", + }); + const message = error instanceof Error ? error.message : "Failed to process message"; logger.error("Error processing message", { error: message }); diff --git a/packages/jobs/trigger/monitor-api-call.ts b/packages/jobs/trigger/monitor-api-call.ts new file mode 100644 index 0000000..63b3666 --- /dev/null +++ b/packages/jobs/trigger/monitor-api-call.ts @@ -0,0 +1,40 @@ +import { logger, task } from "@trigger.dev/sdk/v3"; +import { z } from "zod"; + +const apiCallSchema = z.object({ + endpoint: z.string(), + method: z.string(), + statusCode: z.number(), + duration: z.number(), + timestamp: z.string(), + sessionId: z.string(), + error: z.string().optional(), +}); + +export const monitorApiCallTask = task({ + id: "monitor-api-call", + run: async (payload: z.infer, { ctx }) => { + logger.info("API Call monitored", { + endpoint: payload.endpoint, + statusCode: payload.statusCode, + duration: payload.duration, + sessionId: payload.sessionId, + }); + + try { + // Here you could add logic to store metrics in your database + // or send them to another monitoring service + return { + success: true, + metrics: payload, + }; + } catch (error) { + logger.error("Error monitoring API call", { error, payload }); + return { + success: false, + error: + error instanceof Error ? error.message : "Unknown error occurred", + }; + } + }, +}); From 638c46010ad99a95f447f3bb49cb638c937fcd28 Mon Sep 17 00:00:00 2001 From: Luis Otavio Date: Sat, 18 Jan 2025 16:45:29 -0300 Subject: [PATCH 31/75] Trigger working --- apps/messaging/.env.example | 4 +- apps/messaging/src/routes.ts | 74 +++++++++++++++++------ packages/jobs/trigger.config.ts | 4 +- packages/jobs/trigger/monitor-api-call.ts | 3 +- 4 files changed, 62 insertions(+), 23 deletions(-) diff --git a/apps/messaging/.env.example b/apps/messaging/.env.example index a19137b..727d941 100644 --- a/apps/messaging/.env.example +++ b/apps/messaging/.env.example @@ -1,2 +1,4 @@ SUPABASE_SERVICE_ROLE_KEY=xxxxx -PORT=3001 # Remember to modify it in the message.ts from whatsapp \ No newline at end of file +PORT=3001 # Remember to modify it in the message.ts from whatsapp +TRIGGER_SECRET_KEY=xxx +TRIGGER_API_URL=http://localhost:3040 \ No newline at end of file diff --git a/apps/messaging/src/routes.ts b/apps/messaging/src/routes.ts index c6266b6..d8e32e3 100644 --- a/apps/messaging/src/routes.ts +++ b/apps/messaging/src/routes.ts @@ -1,7 +1,7 @@ import { logger } from "@eda/logger"; import { createClient } from "@supabase/supabase-js"; import { tasks } from "@trigger.dev/sdk/v3"; -import { messageSchema, queryParamsSchema } from "./types"; +import { type Message, messageSchema, queryParamsSchema } from "./types"; const supabase = createClient( "http://localhost:54321", // Local Supabase URL from config.toml @@ -90,50 +90,79 @@ async function saveMessageToSupabase( // Modify handleSendMessage export async function handleSendMessage(req: Request) { const startTime = performance.now(); + let payload: Message = { + message: "", + sessionId: "unknown", + }; // Initialize with default values + try { - const payload = messageSchema.parse(req.body); + logger.info("Starting message processing", { body: req.body }); + + payload = messageSchema.parse(req.body); + logger.info("Payload parsed successfully", { payload }); // Get Supabase ID from WhatsApp ID const supabaseId = await getSupabaseId(payload.sessionId); - - logger.info("Processing message", { - whatsappId: payload.sessionId, + logger.info("Supabase ID retrieved", { supabaseId, - hasAudio: !!payload.audio, + whatsappId: payload.sessionId, }); const formData = new FormData(); formData.append("message", payload.message); - formData.append("session_id", supabaseId); // Use Supabase ID instead + formData.append("session_id", supabaseId); + if (payload.platform) { formData.append("platform", payload.platform); + logger.info("Platform added to formData", { platform: payload.platform }); } if (payload.audio) { - const binaryStr = atob(payload.audio); - const bytes = new Uint8Array(binaryStr.length); - for (let i = 0; i < binaryStr.length; i++) { - bytes[i] = binaryStr.charCodeAt(i); + logger.info("Processing audio data", { hasAudio: true }); + try { + const binaryStr = atob(payload.audio); + const bytes = new Uint8Array(binaryStr.length); + for (let i = 0; i < binaryStr.length; i++) { + bytes[i] = binaryStr.charCodeAt(i); + } + const audioBlob = new Blob([bytes], { type: "audio/ogg" }); + formData.append("audio", audioBlob); + logger.info("Audio data processed successfully"); + } catch (audioError) { + logger.error("Error processing audio", { error: audioError }); + throw new Error("Failed to process audio data"); } - const audioBlob = new Blob([bytes], { type: "audio/ogg" }); // Adjust mime type as needed - formData.append("audio", audioBlob); } - // Add message history to formData - const { data: existingConversation } = await supabase + // Log conversation history retrieval + logger.info("Fetching conversation history", { userId: supabaseId }); + const { data: existingConversation, error: historyError } = await supabase .from("messages") .select("conversation_history") - .eq("user_id", supabaseId) // Use Supabase ID + .eq("user_id", supabaseId) .single(); + if (historyError) { + logger.error("Error fetching conversation history", { + error: historyError, + }); + } + if (existingConversation?.conversation_history) { formData.append( "message_history", JSON.stringify(existingConversation.conversation_history), ); + logger.info("Conversation history added to formData", { + historyLength: existingConversation.conversation_history.length, + }); } - // Forward request to AI API + // Log API request + logger.info("Making API request to classifier", { + url: `${AI_API_URL}/api/classifier/classify`, + }); + const response = await fetch(`${AI_API_URL}/api/classifier/classify`, { method: "POST", headers: { @@ -142,9 +171,18 @@ export async function handleSendMessage(req: Request) { body: formData, }); + logger.info("API response received", { + status: response.status, + ok: response.ok, + }); + const data = await response.json(); + console.log("API response parsed:", data); + const duration = performance.now() - startTime; + logger.info("Triggering monitor-api-call task"); + // Monitor API call using Trigger.dev await tasks.trigger("monitor-api-call", { endpoint: "/api/classifier/classify", @@ -181,7 +219,7 @@ export async function handleSendMessage(req: Request) { statusCode: 500, duration, timestamp: new Date().toISOString(), - sessionId: payload.sessionId, + sessionId: payload.sessionId, // Now safe to use since payload is initialized error: error instanceof Error ? error.message : "Unknown error", }); diff --git a/packages/jobs/trigger.config.ts b/packages/jobs/trigger.config.ts index 462ce78..a0b40a9 100644 --- a/packages/jobs/trigger.config.ts +++ b/packages/jobs/trigger.config.ts @@ -3,12 +3,10 @@ import { config as dotenvConfig } from "dotenv"; dotenvConfig(); -console.log("TRIGGER_API_URL", process.env.TRIGGER_API_URL); -console.log("TRIGGER_PROJECT_ID", process.env.TRIGGER_PROJECT_ID); - export const config: TriggerConfig = { project: process.env.TRIGGER_PROJECT_ID ?? "", logLevel: "log", + maxDuration: 360, retries: { enabledInDev: true, default: { diff --git a/packages/jobs/trigger/monitor-api-call.ts b/packages/jobs/trigger/monitor-api-call.ts index 63b3666..f63119b 100644 --- a/packages/jobs/trigger/monitor-api-call.ts +++ b/packages/jobs/trigger/monitor-api-call.ts @@ -1,5 +1,6 @@ -import { logger, task } from "@trigger.dev/sdk/v3"; +import { task } from "@trigger.dev/sdk/v3"; import { z } from "zod"; +import { logger } from "../../logger/src"; const apiCallSchema = z.object({ endpoint: z.string(), From 3f75e72791e735c22257defae820e415423d9895 Mon Sep 17 00:00:00 2001 From: Luis Otavio Date: Sun, 19 Jan 2025 16:24:20 -0300 Subject: [PATCH 32/75] Add configuration management with TypeScript and Python support --- .gitignore | 4 +- apps/messaging/src/index.ts | 3 + apps/whatsapp/src/constants.ts | 9 ++- bun.lockb | Bin 720408 -> 722024 bytes config.example.yaml | 47 ++++++++++++ package.json | 16 +++- packages/config/python/eda_config/config.py | 65 ++++++++++++++++ packages/config/python/package.json | 7 ++ packages/config/python/pyproject.toml | 14 ++++ packages/config/typescript/package.json | 15 ++++ packages/config/typescript/src/config.ts | 80 ++++++++++++++++++++ packages/config/typescript/tsconfig.json | 13 ++++ tsconfig.json | 8 +- turbo.json | 7 ++ 14 files changed, 279 insertions(+), 9 deletions(-) create mode 100644 config.example.yaml create mode 100644 packages/config/python/eda_config/config.py create mode 100644 packages/config/python/package.json create mode 100644 packages/config/python/pyproject.toml create mode 100644 packages/config/typescript/package.json create mode 100644 packages/config/typescript/src/config.ts create mode 100644 packages/config/typescript/tsconfig.json diff --git a/.gitignore b/.gitignore index 64e79e3..09a8582 100644 --- a/.gitignore +++ b/.gitignore @@ -68,4 +68,6 @@ media/ apps/whatsapp-store/message_db.json *.zip observations_db.json -old \ No newline at end of file +old +config.yaml +!config.example.yaml \ No newline at end of file diff --git a/apps/messaging/src/index.ts b/apps/messaging/src/index.ts index e752d6b..d827001 100644 --- a/apps/messaging/src/index.ts +++ b/apps/messaging/src/index.ts @@ -1,8 +1,11 @@ +import { ConfigLoader } from "@eda/config"; import { swagger } from "@elysiajs/swagger"; import { Elysia } from "elysia"; import { handleHealthCheck, handleSendMessage } from "./routes"; import { messageSchema } from "./types"; +const config = ConfigLoader.getConfig(); + const PORT = process.env.PORT || 3000; const app = new Elysia() .use( diff --git a/apps/whatsapp/src/constants.ts b/apps/whatsapp/src/constants.ts index 34595ce..83e6127 100644 --- a/apps/whatsapp/src/constants.ts +++ b/apps/whatsapp/src/constants.ts @@ -1,11 +1,12 @@ import dotenv from "dotenv"; import dotenvExpand from "dotenv-expand"; dotenvExpand.expand(dotenv.config()); +import { config } from "@eda/config"; -export const CMD_PREFIX = process.env.CMD_PREFIX?.trim() as string; -export const BOT_PREFIX = `${process.env.BOT_PREFIX?.trim()} ` as string; -export const BOT_NAME = process.env.BOT_NAME?.trim() as string; -export const ENABLE_REACTIONS = process.env.ENABLE_REACTIONS as string; +export const CMD_PREFIX = config.whatsapp.cmd_prefix; +export const BOT_PREFIX = config.whatsapp.bot_prefix; +export const BOT_NAME = config.whatsapp.bot_name; +export const ENABLE_REACTIONS = config.whatsapp.enable_reactions; export const ALLOWED_USERS = process.env.ALLOWED_USERS ? process.env.ALLOWED_USERS.split(",") : []; diff --git a/bun.lockb b/bun.lockb index 223eb489f3b7d6483f376a6443b950770bf79887..ad24eadcd2a4f31b6b882e6bdabcb185a23efa1d 100755 GIT binary patch delta 151370 zcmc${2Y6N0w*I~LCY$UPh!9i|L5hW@ArS6Z$BjOe8+5KjyYysE1N5S z+vkgU`(D0Z?^m;Tyz7n^iu+2xk_SmMG9A?s-|rt7|I9mBUYN;dwj4j{w_& zDKH1@3AP1y0o#CC;11x<7EDG^2Z#Wj(KF+;1NL%Vkr%ouYtUzhF(oZOFQ(aUxjZ7}#kHJ~-4z)MXC?zi`FPkK_`e7{< zvm8|M-9egEm*X(z@EeS%4tXDxdM`Sh1xoz~K>pQT&o8CB3{;0*>FOI#h1j9Mt4DxF zbtlkms%QkLit|BZz@nm})PV!M{*LbrsslE3u{~T>U0Gc|wZAtf7~P_4ZDBW@m)NLT zQFHw6wzR03t4JrS97*j`{}52M4FDzo=k8XUw;jF+O1=hE?S(Un%d0Tg=sm6WM}WxN z)qPHY9;+L)myLKoh-GxB%|)ul^`dw+ZvURv>NTLMe;$+(x`Ik~UN4*OR8Vf*ySG(u z7f|V^yZEy52}OOT7f#D@`7=TF=C3_${`bJHcz)d)0`y#67Zgx}7>FV3a`*K-Oi@=< zRZ?75O<+IIL-V?EpzJuz;VXS?H}-~0q3L}+uQS*hx$M!UpKbPBP~}&HJAiFT-x1HR z+eSj_tosy{TR#n|rGxt03=>NVtE-D9c$LMam4y>MZ&FG5IPB~79AMLR28}zEO)M#% zG`V_e(M)_9We>FS`(1ex3#+QnLS9)^Q&l{1rgsmI zc|K*UuA0i?K2va)ikh;S9w!PY3BgR<^%$kq5G92QsgG4|O# zZPN|5@~pa>5vamngZo-`J-I((bw+Q8GY+x5tK%JpS`(BMO)HZvv*Akri%XXQSM?K$ ziYodPS9v?Td@cF}duMj>Z^Eq7>Z)LLW{29V4zsMN>a3cg%9-By$%Q3TDRj(m+s4U- zRek8>Nh9IP)#Y$&*HTcGcEuCrp~oKKd5o629YI{T?pNZOJL+D8%SM0oxT>PkX+@Q| zm@G1Grsq9cU@N#6l=g#0+6v}^;^Qg{#~0BV+nhXoq*ZVssQA*N%1K2`$dR#`Mvb-S zjI!}nC52U!Y1jcM)Q$9i>?0iS)jfQa9aGnVM&tIj;(r`%@hP~b#*ee@RQM_=53H#v zo-r@@EK5ju>_RK=G%mIhl1*gTajzJs)Tm{ zH=%Ve@cMC{*BzV-%Gu^3-xWN?rF)oql>XnxGZTT25#Q50&Z`^3m}g3@>kq2MotUyT z;(btx%`B{(RE1^EoMbIE8q~l%43uRKa<~9bQU!B^#w-@&Vr!3?{c_EK+)8QKylyYl z=>on!#d=U>QB`@#v?A~BsaEafpzL`CxC6*oE1ymYUtl|FHwd}Ru$uhR@~2W;?uVdi zUxA!_wRKgKi%ao;?;6^v3g4pLYGwndg6{=opKHLK!0RgPakhpiAhW8=pJltVvZ!!^wpWE^GfNAr$4{=R8DCjgRpeb6tZv<*cKX@2(CW!WWvcfK z$ESc8sP3ZKwv=d%>o><*uREy5wFjkCX>nO`MPXG{He8K=yw=9w4XT{TUH&q^7c_a- z@^h`)bMZ127|p$NP(TGv0@eR}o@cwU6{tD*Ls0Uq!S&f)YujTAH9iZJ@6!13n9XZ( zq2*_QD(we4QC>E_qzWT?-n(#>axJL%_VX=HooAiu8F)A31@P|R4fS>~Z@tKBv6g&v zNo`#l5@__!BVtEzAGk*A2XIyBEwBZwg{#8*KzZZU4)4Fzrn|~vA*cpL>CcBNegDg? zeJ=;w!E3=Bd1W~Xc1AFb0u^x|nkmD8MOL9kQ1-267sX8Cr2<>QYUJwBJ3-~2z1Uiy z(%};1Qnp^wJJcqF>twLKu>lq({n67Sh z%;viZ)Uc=rceO3B8>pGTgot+F*4%BYP&77cG{cNQhzs4sgj`VQuhz5ejiY3_qyJyQwFM* z>dFBFF{?Mlr8@yszRPZ~c0L5Ie4RmQ_uO^1{P7$;i;L*N18BD@j!tubJw!!^rs$9r z9loMdUvvnYN;&eJNtNYi^{JXLb-~TH!m}Ngfa>t-Tde$a_zv=*9dES?j7FfANA`>+ z$jIs;a=RI+|9V2&uYVp;eD!vp{`4@od7h{217OAvL_}7AW zq+n&7R#?Mg;dxKrYYU8AxvF|*30pt!&iky#MH4QQZuQKHB5x^jjmh0t*sdC1SXxw4 zJgevu+qaK`S}_Y8<~h9J zVViDW$77(nWH-m#I6k3&pYi1-<&~Qs^n$C~?NxjDV>TUfv)2F%D=Q0UWY1lk*&cZaRQ%=0Rnd8%EL_ZHp=d_6xBJs}5Ud3yuO?mw zoB-9H9brGAZs<8;9$T6T=+!yQy?g1)edxvpQqiW~p?7lV=REx%!PMC_Pc=>DX z?DemEY>%~m-llJacP8D3aN)7**feMsTZ^D0_&5pFf}35&MW8A=+sXUBU^7m1{A+ky z;zz-iKjq||K~0x2hyT3W&hxK;npRhUUBPR>S_z(f$yRg`f}P=|;I7~?Ap6R?0iZPQ z2%3UHEihl*Wfk54Dt?8-t3WAq5vV~h9h9QeimMrNIvtLyDK44d^?DuqOM!N;+k$?3 z&EjVcUv~I7D8+68cLD>4wGO8`JkjBy4)=H1$zc+dLO;H0@uOFa_;lLu@>rY>#eOD`K|5S_dpf2+dnP72$$154r-#j-^C9BHB7rxfGX%g z`W?V5xO}PL2V2ntpyE&4Y|D8H%z{7Ykb_=TQFZW8hjz6CC_+()DvKr+%{YQ2a<_h< zirfQK741N&ay)I5{KKubZM)Mp$&aKQsdg#xDsMAf={A8F*me(E}n_#HI|?$aV>GEOKTUtL*TR`df2RN=dzOt{AJ&Uaap&q~Bhg_A*5 zSky9R(scu6fo_>GGma9V8gMjnwfrE5`+`z<2>EM!5okj|eRoV&%xbM7(AF_CAm8z0rt7wW>bie|vfvbZVDnyZ@jBcTmQ;D2$uEm%WyNYu z1uID)lT9irtD0Fh-kZWs;6MiA>B!|YyP{wZ@Y;E{YZiiP=@)Hnh34!yb((ifjxFaZ z&?i10R0F!Tvle3b7RZ%H$>GHEfVPueXz5(XoS#*yJjx zE-sr{#WR3TI|V0q>eBDUU2MH8K@EpSGoIBDsekFXV?whVL@)n5Bi4Q@O46u0*~Q^79>5I6MuM?Yiz^^ShhS+PWPP$Y@nX zB@>FcI;|+IB*+!fo>uJ==3J>e6s}GvDyuH8E-tE?P+nS7U0hOi?p`)s6)0m5Af0d@ zP)2G4YTZaukMft5SJ1VdS2%85WzjT7V7!-2xV5KMB)U(G&cqqLZ6|%{;yHsWzvu03 zD=u&?OF8TT%Dr}WnBj197c2kBVWaSP60Rm7_qosE4WPPv6)G+}zE-`m{cW!32s9_?-lbl;u5;|JvG zg-_wKa&+DqS5q~ve5U99mT&V#cQ?`9OnD`zGCn`0tv`MRs-8_?mIBWXu@Mghja}2V zYlhn5SCd86RB;BXAUy9->l|gEa#a^r_L<0hwRo6S?;TL`ikea-_u_E5|M;ke?q6f7_OmI774hoHQ4 zJSh9j0##t7;48tVZXIebDX_6Tlq)JMo#CD9cvsG^QhR4m>a_;d?5J7e23YHv6C2HX z{79QVbCj)RET~#}xKmk9d}OX(qwVUK1iNX2@y}!IYV`uBRqZZ^mxH^(t3gc5Xpca?gKv^^aYJ%=}8eAKUA5OL% zw$b4Mr`rtYg0jH0iNz&amCVG;y1N^4wd|8Ktf}8{_%x^%J_4%C1{B(gI)hTA9r@HH zEuDNq(X^7CAN;qJ4THG&?inKYS4z2mLMoK=g;SUScP7MGxh7elV}7f!Gi zn+2+6l||J$)_TVmSqmHiO8zydijSUXEqEZPF6=Ui4Tenf1PZ7NUYKNC_AscH+=*PK zd||SUpEbRht6Mf6#a4mj6kFhTaJBGACqHbewP+qFOLPOZ)haEq<)7qmNJ*{DIH%NB zc(5y=2Pg%`k1wy$L)O0Zsm9JS6cR23rT7J)TKHhOE$5z!m^mv2pqzFu5HGJwR$7I} z!TZ6-f*MT^*H&3mOa$c=*E)VSC?`A`ly?sVHG9@hvjv|HDt@Tr`+!Qflf$2Ctnchi zJ}HeI*Q zYiWA3!vIu;SGc|4xo~-Zxx+s5Z9YAot19uld*MpIC#XK2z*&ysz466;%x3g-^2yor z!L~Bp?gV72_MrM|`sCvAllxSZw!g&6CtngQ>e0i$dx7n;XM@!}I>gK)+~B(&$*~V# zY?EcV?0>kKcZs%~m=*@Qmj6TP_$UH(P39UDn6*LWis-IL8~VA<4? zq8e5!uc8kRi))I!{)=tvD*BYL8dZB&1xtJOjC~ob?%DN_=qX3^$gDiH7L6Wie0hbP z528o&o8Yq4TMl0ey7%f(yL74TsK4&2qTN-rZkzR*+|hC!J(_&(YTK@z4z>OD;Wc*f zA91bq^yfjXk7dhZ=HjCZsA=^A(lH`x>(*Uo)f!h`UZP3=CL+|aE0Alf-tOWtP{r}6w6MafDx4w5^Y*&MRuElXPbR%QJ^NPc5z9f{@XQBQUlG_& zE7NoZz0Anu?{>T2nsNtF3jTPXt)PqwrQnnmwo5(( z)fIUUSpEWB6`Vmj$%i~>8=zfEF|SjNK`u{h{}3yaT6D%kwt|(Q`uuR0vGieU>H%=| zeP>X!&tEHSgtF;lm5U7H2<16|M zNO@k%L#+j7JYg%`@kv|Y&ns=qHp6A9w?MV@MNk&`jCfgU(kfd4FTqrEN~?_CAMC!` zmeU&4D41OPv^9NH&{uGo{(A~g!N07r1%CpUGZjvmW}NNeXRN}}8#Q{prw^~wtVDs{ zq`L`}rOtcKu4L7qy5chAvcNe`(b_2l)birztww7>tyCv~GEt;J5zoJ?srvJE)(2hz z)qme|zuC-sk zZgqOs;Vp02Ud(^f;)!UXRd)oa-u>(?n?9PFqub=@U6q%KSKFr-jjJdeKh<0OuI;^x zK@FVVpvsvB(h0S7&%S4k*ZX}tMNA<={`d@Bct0pJJVrVVxaj4h>LP9#Dm`z%jW#}- zQWn6KE;{`00+&M7g*+c+n_TD=IUdZ`B$^_i*|%sK7ZX+N9sAhJspyTZj>M~XGC?Ww zuXn6l-1f2Um&KsmX~-wGSLeW0;d5}g%_(qAo`XPHat--aPU17_)g(HNfObT$6CuZc z@^d?YZUN-apu>r5&gWPa=b6`EA5&>vkzzCXZesirx*1c=V1? z^zKmfvQ6~v&)i6XZA_x7V9qwXN&RcWsDP7**9GP)zgq7-!142cvnI}gYtuXKcdOv> zpuBt}D6dcb+gi9{QXf5Ht=a_F6f>jN9-O?_5Xdyofy(%ZLo)CZXRi18&oOUr+?aAYT&5lkO3@zRK41nYU)l7Nb;8F%#V>JK+vP1AQRxyK=_0y=D!>EP zvUj%Hj1M@x7}RWCa&mu*1kyYJrD?6h z%Jy+%!V0)X#dle@z}2Am{0`Pa(JMe(;Ihb9pcMIG7aL!}lc8!}-<;afR&1Hwsqfr; ztA6~hHsf2t_5C~4Zgkb{*4ehR7L;qQ1vSB*<@kBK+4xFO?HvcI1CIlxW|Xh#oyUXh z{aADeC?K8GZRP6urIRU#iys@PdmB`N>l{An@HU4_9X{RNR#5Z$II8jU57sR*GO#dDM70?LE z?0n!Ov+|R3nFOU{IC@S(^5oNzz$NtKEdKeVXrVgdLv{|b zX6O5tA(T=r!iATh6zot~>tOWkRJ>*Tpka1?Jgd4YKB7tX+fp zIr)j3n7)Sw*>m#z&k0F0?0YhP$o4B=^2B&T!-EBcZXw+W!)MI_kN3nrOnKD)11u%_l+y#R_ z2vdEYoG12Eut(A>{Q|m*f7&8HxfEHsxr1U7%?xMEMOZo3|ZuTu}gz>^U@UL&rkdD9-fy= zBo@XE4(<`u&(DvY7px=dAhucR9(wqpSZ*+)KF#=X z1gZRB;khY)KyT+2W`tD2eu95 zFl7yMW@Tug#QqhGSdfmV_X+A3zbiv5Fv%kurV5gvuGMKJoJRJ>>3paEe( zZ;nQYQgrVpFxi@6xCi zxtWlBi0STd)zL9OSQn)I9}z1D&Nw&a_dU=iLife!eqd0)I6t;3ShqOsZzjr)DEu_u zdtgwoB;T(`q7JaCzYWuThW)6$D;}SQF>vvK(_u=<5Gv12LYx&ZP5G_pZ$-5*<8Ojv zc62O<4YftiP9?sD9UP8&A~q<=9!lDnQR{ zMXC4+2M1Y8^Zi8#)k8+;e@hUv)g^JmEdvXs9M z{is%=?zB|mEZBfx)MYG!gmTSF`@T{H*)#I|J(#72*|eAECLwAkEyGQ2aV*mef?r_d zLM6>X1L-I!Nb6t~F!dZ`u{`D9?O4mubU4h~L9Vwi_Ess_0lQ9yDPLyT*g2M)2{VgL zzqDCwl1X%tX~V)>jleHJAS>eA`B)Agz4WS?24yOs7)==Asvly05{Zmt+%5TyW8tRT z&4iNN_b^wKG6X{&@skKt0=+*cmAnb2+|f!M`#2b}JneVH(6SwcvVu*3Q7(g0a zl(YW*A9(@fvJE zI4YBYJ<3*U7XQR>7-MR7o_`S`rMLaG8YTlrJ?w8rFvb>#iAFHFY-g{*G<>a5_GGHX z=#EW+sXKi$wr+1?3s>a^{$1(BUSvE>yI6k;A?5+KjGo$s`Cgvu8;?KUF)YQZbUqB{ zwaSHcU2!rbn(u6U|6^^pM5|V!3W4!l|1Lr*maPbzkN1Q5_oU;!j|&>^$&a6MT#$8d zzQ61^x6e0o><=(0MZv73eQ*R>#ZDq;z?fB>!kw`PEx;GFD=mS^nwH({7}Ew#`xi{= zv&t}U?|)*nbs4=N6`y!wP=9~E|0O~#jU>TO(kEH>WVX61o^O~OU1SX^B+jZ_dGdhcp%>|KiM`bDO)Fhg2|Rq z*C!7<#hKfV>2;VJiTvK$VJ)m_z$O21agjoP`qZWoNL$W<$+vA?kHTc8rn)vG7$||8 zAN<~@H7~pnW?HK$bxB>^Pa;sOu4WE09dlO~u@0U?t-v5ZiGk$pjkvlT@gTo}|pmY>*V9Dhg>EO;z0@id`RO{mLw z&pXM4rV-*$_Q;^vc%_banqV@GpTPTpCcz6$p@~JFH&L?Ki-Er~9p8UqP_QyTc26*W zCA-T>o>?h#f}GNne-?}xLTzJkKMvEdva5Y;GG{&5&L(asYzT}AS7)^OPnpWXFFRY>e9Be@Tc@ zMIP;B&zKq%tjNeKC#E-C{=ID|tl}SfU z%hM)zP1?^avvo4TUY3dua!M3Z}Ky$Irh zueRO8_+^266D9|aMu6X{Cejnr_+w$}5IYeBF!effHw)csFirOqgyDCY7BsBO_fMT> z2dgoMzYM0)2E&Ekfw7*}wFz^MX1_As#zkF}n2DfonEemd2tu06GVCcDrg|b3lY?hi ze~JYDYy_%?^cS&2!G^-{TQ(~#XGSXMEHV_P_OO_>CM8TW2UGs?RP5f9&p=cHe{bgJlJcwCeCVc2|I5 zVOPVBFy%?hk74Wum5&YCpjxXlLyGoP!_Gu;X{_w_TG~&a zXUDQxwqgr|5wEBHpODE6)&&na-%dluzqtSL-$?swknKkb8jRxKJH{>q*7bs@J6Wxm z9{o4d{>#X8AZQmXEX~chP!qBC@{-Dc0d^9JI1_A0C4YfwDvDN>#NHR_l0%!f zcjwt**)D8sWTQ1d$a*_Jaq@iT>u`PB*q{tTr<*hS(0cx~+NeqICZ2(flRVkuV(aZq zWluxE@q(+ACkTx(p>~(hjxhT$3M6!@De!(mM>Q4LeF4QA`MHFSY?7~s6`1RV4wps_ z#ehvTz>YHMceyMoCuezX5+WU;n0z2MRG`gIj4;y1j zrZeKhuL$Zt%JcYgTSQM z4(EysV}2$AO#(K>azFE0J7GjQ{4)`pYzox4c+5rHCHe=Lh8_oii&Dw{%j~=sWla<# zu-p6V2^~m6*7DC%$v0pFRA+QF&AQH7-YD%K1Is7c3rD|!Pptbg9eXwKc{}-vUC0->z`a@xw3-KU^ z*jyO>$lUTIq2b8wp)lhn+k?i2$>U*iMKk_`RhRj}i0{&|vDTSNlYZNqZHJhd$X^Ci zYnbfVqkRDz24fI>kV@`*iybaaX1xG`65Esd2AC4FDxr9nTcgpzXiA(48>(xL*zLi( zAJXyIZ9&%N{6yYuYQd-tT-p*k(bPMd%rC(P8rJwyDxPtBkhLX0F&HyBuV-U&OfY{- zI`R7LI8T_pG&U%9N09Yne&Vb<(9ujsj}khRGFofej{O#_`!VgO?zD~}+o$}gFl)`m zds6W`?hLZn<7~P!D0pt5Mp+B$Ya4Y79cOD?m;CtM4+agtO~M-evP;54tHZ1W6>dbN(a=Wa#M4iQ^+*%bpJt{oZGVlBYBe^Aw_g($v_##RYr=Za ze|1e{O^usY&$#(B$ax_*2|1XI7>&!#=U|gz<}4Q9@7b^+lVU2L4YQII^YF8FVoEM) zjB&sC99tBnHLL+9<6E}FTH9<^X1uc$HikIk}O$kpEBNkYdM`RF^4H!x`#`ALM1h>G?90UK@@7Vag^(1q1@E%KDsK)Fi8EoxnpF1_Iw?w($a-0dub-dipGn9mf+IZuJIbVw zH#Rj0`H?WYBfPdvCt=oZ zjL=t*oQ(G{zcKSq{4}gbntbsyJFueVDW3m%nAL@WecYE}0hqY!OAMzc+lkGDIEC_z zyWdyQ5Y`#85T^OXo;GiU9V`}(PBS9K%n7x_*LI#^eO}Ln$k$;(H%ecC^k7{v`0o(X zs%a*I#GrraI)z%3iwSA`L<%N8L~xAGPl>+Y;P=X$Tt?{B=81kmV6{H;TMZvd@;@bH zG}FYB==zQXrd$n9P7*FU(nbp6C&|y~Jmr{u>Fm7*1 zapn(3sUR=$93gYiOO$RlHK4;fLW9V~oX={N*b*_;yu=}}0pa@38H@@wrMe9^&@lG< ziH)#9Mwd=MVo`IDJdV(DMi;ioiJM_e4CwM3p<%&-bMulzw%Vy6Iw|_~2*#K!jlXh; zfE{BC;PKqBpPJ?kCbmmq`8JNp^F^2%U}x{-&rvBjLgI87#-ZZd37r~@nwOVsxs8q> zz3INN?&X-DfrOi~d3o{W+rq4UaDuH$V~jWG7uNxa3kmUd$%T3TSCUaeJY3j?)7P)j z5;}TG%AXFiov`qiRN@I3_o0}%?Qa-dd5oD)f@w3ztc$x|36n-d1IZcX+gADIu-GEy9KUSfbxogmf@Ba)a3sYn0iG zkomA&m|cKY!%i}O)-faIoJNQ0a+qm_dGNLtruMRHWPFE-uzmo$mUqH+18CgYEo0_6 zHpi9^Qi&H}JgbefhZ&j~)*pxq+?q*Q;erDZdC4$qAgwwo8S_R_AWuPgs(f!UtVf#o z8fm3Tn3&PDiIbrfVoE9To4I2=QT9TKzhwlL3dBl=D ze;c8EQW!T&q;|&X!ckfAK}kaD5VqhvLw+8n9x>BXST}}Q0trt!mgL3r+lK{*pkrNo zOrw2G;(kILEve@lLI<0AdUc4pBj@?t42VhK-$=*~4Jr63jPn3$b=oCzfyQOI84#O% z0inL=VJ;+-Z@NTQEB@TYPj-H8g!a2=))a@I>u4rjxx%jPhf+z(3=hXXS~ z@7UR6z;bR2mH%K{AwQp1P`ASYLpP zHiqj8*h_Zo9%hXsOGS5_ghbYuYhjxIm?Btle}xST*B3B!)4SVwiz}VJu~gzbQ5&(KstN>4-U9`M<(c2-ha8@2MVEb>>kX{?2yHxziK46B5%$ zrjNJ5PBaQgxjTB=K4wOKI_0%~#G1yH;4?6_8Aqcpa(df1 zE}eMM1d>NomU%-<>>wD8#DSlJS##=UCTs7o;aJR>ySHtT+1(|oVMD|0k<8N%?H$%1 zM}nUb$u<=78Skg;)0|cRiQOMD)06&p(7`q%_Y8gZZJu!|%yxzfz2#5rW0)FiDopIx zhkW6J69IKl=G7|;})y*>EM_F(pbO$o+q4_>!D_{H{MzkxCD za9i$-?ZKx^F#9x4N2#XxQ3MaONf&JozPUZvIkzc6VN=jwK~PKZ4&lPX+0nE(*zQLh zJ>}1I1$4~z#D3nN=o!%Cx5uvdGtC?nGh01Z@>r;LSe7PkgK`iqWZzE6bZ)rtL=+mt zwWjUs98O%*2Zs&ganFa4x+#J|w_ARb+cUAB0@E~aOO7wh4;v;h2S1OPg>`veBAM2* z$g<)eL8u=|v%ZSFN)Vy_h+qo3ER{?Qu~SgfE_DO~O%6EpFT5!VlQMSp zdetRnU3rx^x2$IDDpzwM2a{&@QQgHb>(jcPTjS#F#&?^Gv$IdXp|+1=Va|mtL$G}* zh4(?ocNt967@9ttioF<)C}wqd#0!twm@yFdkM}qNd}d2AEy$ON3O29d&yT zw>yZKdDk-&rd@N>TOQ9NP$4bC#%zwehts}b!Qw$l0!oKkYzA;oR`msoIsXxIljZVM zJoAV!tBeK?Il?v&ll_{T0XZtVoA%#;xm^XbQRfjhJH^6IhN*7*Fm)MBmY{LGKld(7 zQyWVUDR(Zgy+^0BB8=S5I4^`SsYFUHWnYJ-3|n|5Z=H>d;u=|!ieYwLVG;6ggUL*` z*FS}+KAOPMpzD#&t-_o{EEPZH$grT2cFsqv0gGihX0C;~WeT?QsHivew0#21+KJ-g zH;fAFtGJUm=;*Kk^b1DEyh*k)6uA#JK4OWTj*WSRVRj93;~A0Ae+}Vw*8jMecbbW7 zdvSYkmlK<$69^7B3H(P1s_EF3NBlp*+{tF)Nlho4ZwVf2PXmXH<;+5Ix|WUf z)i4dy$mJ7_2u24B{xK+dvOR*CH=mNDPKntI6Zd*_@<9YTzD3W({H#;$Y}zthcn_;6 zY^b86x0{wD(AJ@ax#m%{rq8D6!Kc}1E}Q6~X)uijJM5ovEFLag&XjPvE!p1tFN2LU zDfPtAJHz^mIbQe&z~ta&59CjT>G*B8WIw<(6wG+@2cBt}-G$A9X}adY4o=(vliRW( zq=te*tGKbAzYKN~acE84R+xqY2j&`v_Bi7NXR~k-Vy0jYevpvPb9O%2alB36%51{M z!0g1NXC3u0`MR|L>p(PA8Jz!yxh)24zX`TX^KOj41g7C{HF_5&qsPP1cVoFCTZ(PT z1eiU4OZPirwmz|+U~;LvZ@pk05Kxo3F~rtLlNL2~q}D3;L{@T$skLNc0hjb!&Je^F*O0`&pY z5tIF^FkJ#P>oJORmW2CabPo=9XA}txLT2C)T4hsze*# zMKC!CPOv366JpNju2sqX%Kp;KIS6deQcd!Hm^`m(FTNFlR(-pq_A9sNZEgdosT!s& zHZDT9tcTgAE_@?5S-}C{C?VyCoW-R?#1j92O*W^(T`R42)?X*W>P;%fzW*J}?q*d& zW0mbxyHxb4j{ID4=fc$C)->yk?VYqKm|o5Cy%Rhz7)MHe;)#~eFzp_oSO+zr#B@(;asP9l>80^ z6HR?|V`AUA_`4qQCF=>vgQKzOzk@)AW6Ol2?QxzR(i{P6bCVER$~?!5Js0{{XsJA( z^kMcDoL;Ubcr@|6_sl{3OW3g{A~EO!&44D&bp$mn*;7pX!f2AxzTia17{DA=uZ5Z6 z5&eDICIs38m{WgZ$BXb;TjnT&Y6F`CM#{C0aYf1>izQ*IkHrf|KXzU;L&@zI!!$*- z2y?oy>D~m@1()gi#KH4P8ZKzf-kgv|t#yetFd4zDlRCa-*4x3;yXjUdYe_Amc zrghcK-|_qE!-lKbmv2UDcjNspw#~Hj`*c{-?3?@mmaDp=d2-K7Y+|hUZEhw+qqZsb z4g{*$Za?3JxkV^W>(UD%Jv5GMU|J8@Dsb=r2#jkiUP0Xwu3JW@9(^f^&8^=B3YjwT z;pF2mEeB2BvJHVWwA%N(%qBJ+=ud*FzwDNJDeO<(`91l0x4=&{O4S>okcn z(B8|4{o|I~j z5$ssvj0XpG_xXxeKUqK8|CXlPTKyvRD)(xXmqJItoEr)pN+qp@I8^Q5FV!_CHgA=Qe@?OqedC46fb$Uh{lamlM z%}ViG9}Tls)A`>a)ly;?mdhTq10ojkO^dl1FkWl9k9&4!IX38rCV^V|bdKf#{L zRYL7T;^q$-l21ezg!}|@`>HWJhm@Lj>!gHXVpNe^dDm0_PHw|vI>6YD2B4u=gT(hC;elk(@m z_J`TOg?08k!PcgRKZ%F&#E~z| zxQ+}4p2rRwwP;i z7BQoXzsrku5@4mlT-7jZF79jN54{*RtjGE-*T;ATXcVgZQwhp}m`7RP?}2e3f0=uT zmyn0+*VDSA2y*L0(7%hIreXW%hkw9a;Tw3vWJ6Pq3kdSKLOGr%Xw$H6$Fp7z>t9BJ zYrJO9{o+@?9A-5l{pe*}$V|!Y8m*J_cz$cjp99mlr7LLC`!Eeg`x@&$uh^Svo+5U` zoToxhB+}6pf2LWl(u><;&-gQa0IC;VT;9yr{zOlPs-s-&9Z(%)9Q_5V?r`+5*X?G^ z(OFP!w@d@#cfKALydIn5|Bh6q;iV)zWbzw$j(Pbf@n+M;n=61L9gC(l{{jS>QVjLl!T0ff?zvk_*;Y}8)MkJ~|npWd2-U$odV(89%C#(nk z8{f6A-XdI>$HEIUi?(^1yT^OhKg?k)KIy%%{%vx+ja1WF%P?mSIo`Jut-V#Lgqcly zIC_YUGc0KQG8J$8LD=vP`Nw=<7xzx)70x9v>w~;M5zc>?6Z?>jVZ*y5ymF(ROX%#S z{Am{~Ult1+ca7)9J`C&MBh9D}t+QZ|O>EX-T6dY`Szum(*~Tnnp-ODB7H3by>GN>d zI5Q@1Bjk>673&jDGKOVgSW*u`O`M!b4F=OFEPE+wG zJ`U?Q;?=)>Y{tbVri4$dYTQQ7PbKPLyg;&%LGmg=&6s9RPqg_|J4i(&$_eu3iqfnl zXxpyR6T5$=4V_7}jNlZTDEYZvcdQEj+fa3rqrJa~78n)bpY><@C{$zHC2sXkI}1Bn z3_YA!R{V=oiIuRtAbV+^|1%-8gqi!D_?NNZg_^;MYrkX|hNIqMy6o{)*zgIxH0dk5 zx#yZd&jn!W7p5I7*9cP~+{7$S#k+qUHhhZVzW&-wYM)|<)W6JgMg`9&$W*7`hV9bM z-EF1 zq)Fg65IhrwIR9nPQG0K(I~7OgKy7T!ZNz>6)gIKv_WIF!uch(HKZXroW6UQJ9}%wq znu=O(jr5N{wk@px7oD@uFZL-1qnsy4m;MrFeM6p4kRB0ce}nn<{k5r7xlRdmB6WIU z&a==Dq4G_}NCBST<+rflTjrPRehcft*tg-jZyD3Ge-E?1qq4QX+eh;((>zPc__tjT zlV*eug&8l`Umo~%FxzQ55dHu&4Q1lvEw|_dt3E4(nHTMn_Y%@vVy4S*)gk;LQ6z~r zZ@FIF%n*1sUpA72mD#4e8B4|93;oT^r$b_KZ(NwY83(?b7vVNzjejDV9FF=H(dc-X zwS|^E9FKbyO?l%zTZHw96Bo3I>j+3@|5JiRN}*LeQIHY$CWZ^P&q$3E8)6d>75i`F*r0_|0`1}A%~utkZi4<)g4%uCJN|oN>ML`E@xOz~g60`*uVZ(P zqbbd!!*<{&`t(A+H}Y=VT3&itT~7DlyOLcJuOQ?Bqbl2-uRFBCUh@gg`tKIeg0DN| zu_8tIBM1&5pBFZcX9BpfL)Z|H=llOgEU#d_eg_{O%=aS7E$scyY*^Ex7k_w{u%QK6 zen+Y%*>43qkZ(>@Z?G^h%BREF+Th;z5z;2w3m3*H_!F1RzE!g;-==7-%B+z*&9SJ@ z{Ll$78k>OkVLUyb!M7rG;%`^bpnz{DR9jf$Yxo`tz8rBV%-m4=3t%dmse^YglCMD3 zpr*3h@|B8;XMfC4S^~3M_=VK;4vZrJuPyJ|CGP1#joFZetNL@TgG7f7YT*{(dYDSF zmmNc1HPKQDGw{H=Q`e9$_hb{*)h4qZcDZBrzNZ1k2+4}4WBlFE`a#KVwo#nHnMp5! zNpEIgcAgDl=1wHp-G%(&3Yiob$v;10;po-Om@p}8ZZ_iGcMl6%qs`deBh!pNl)pFL zJ#3Kl3#8Iq4a-gL(KNQ0+)sz8|LsliRgU37a+Ob^as!6p`c$Gf-vh~2IM8^6hBhd80ACfUS%3k>djjXe z+)0DiTi%9A`RLN7#a=z*!N_TY{UW|sQlpvqZ6B0f0<}kS?Izxb$vo`x*zxYsD{RQ2 zo>O|o%@)&M0A2>O{mlhn?DcSdyLj4f+uPdQK94#IrncD&u6d4`?N{^~6{hf0Myq z;Dv;o13s8azUfjK{|)NC@H6&BS959|r4R+!He3X=bCu{xu zv%~mh$C{pmwCfl367xZFxMPub`LhtHpP1AJ(NA|kRW$b=sI&MW8Q)sj5gB%OyDK?!N<(!|BRf<_4&Jmt&uYd%}3aZACCB@A}#{F z41TywFdt!ya6#92&ssJ3RDOiK>0>_s8&v+&UH-pA#h>Bgh2m%O&Fw}+pb2*-RKt&Y zc)F2VRQt|zT&M!hcl>{XD)&N{??P99Gl7j!0#$I4ODN3b=TgU;qYAv-$(y5kI^aj; zEape)uHZ)>q2!@$KLJVk2$gUtKiT};%#S|*6I4&%ZgTx+l!CQ)xr9Oq?&U|C-N%nU zLh<`W`1~Ephacld=^l6Z1gMWtj zS^_N%aY+3}po)@2D9(2Ygp#*%^43l+lzay#Z{y_6(I8>|`(;wMV=| zNAcbC>o3?MJV()Tsl8nEpDc^-y=bdpW{L0AH#o11DJrxcp?$}^GJl_Z6MC;to9{%wTPZg3g?6Ka`y!=)1{_$I%k=-Uq8 z1=Z4xpe*{4lYav0KP&UN(=*QXvrF(RC@*b6Day!&jKNkQQC@3M&d3#jStmKd8jErE zGZl9O^${vRhZ~cQy|?-N3FZG1m}$W!V6$sJ%~A1eBu%;jF1|U+68w>*iO+X$}vVESM!fkeE54W<`4qj@h-Vg*-ro^(znu! zpCsZ?d@Njx{zMl)32YBP2jrh;CMJhc?>q_rf)ZU2Cpr7O-squpmmpO63tamDgi3Om zOZRsuF9=<{P~~3fxKMm4DACo@BQxx?x0~Z{a`87iyhVxl{2j`ice!|>g7-MQ&&h=f zu5etae2;>1jwe9rv(o&J1~!0Q-rt*juj1FZd_o1Eb^L#VD*rk13D-J&-jySie4XRM zw(z&(j4>(jo=fmPDE=YHKkp-cY2x_G$-e>RXPX`03i8kUS-%`A{&%>`|6+chH{{q#p1uorXF5O?S)@v?M-z;$ng{mL~rNEU=E)-wt zxKP1s_@(?efQrA-;c`&r+zjf|9L*l%79!N*J6r;x3b@nZT~6K{6@NE!t^bd@bdR}o zLdEO9)6l0mO8$hC3zM23pCO>WeZfTtrSW=D8D4U7q4)-eFM~?=ii;O2{#7S`9aOpR zIr;lez7bSce-73v@GSv-nxhK(&dL7{6}6dPO1A}+B3r>>;K7(v@qw#UIZ%NybQNVCSX4Q z36Bp-eiRKnLmxzZ?5iX8{;2u=hw)7Q>$2`&V+Nx25(pLadK zRKVS!nI=H_<{D56ZE*N5s0Mrt^3VH%UrP4_sQiC`vXH6Wq4Kvzn-0|P?IE@J{QraW z|NobYcS7-$Yry{sHG%%GGX5X($>g~xCOimKpC1Xzca8%01J4G{_&=9`63zwn5sIG& zs!!)Rd2>|!d?yzw-KC&Ji}|JaB@V9uHCmQBeif(>SZjvUG6X8%Mwei@!MwJz`#s0zMz{98~3ZgzaD9|ll2}*%1P>N-P%2%7?1UotG;BZ%myE*LUa1T)9eIHN-_5o$$gFt_VtOAvOro&oLAEEM{531Y? zKq-2Wi*Jr*12xYjsCOAIc6MsdZ;m%}~LhV6mzKbhJD0xRvE$s{{eK!~1 z92LKZOTQY>gPlC>@DLX-RQynfhdLYvs-+_wF97usN`ADH z9|Nks+GAaU=BO5oadM$F9}B9&lbu{B`Du;|Rp1$*Dw+T)eUXcAjw-*{#`o~1xCG5n z1(i6t!EpV-@vg>KD{MaREQgg2s~lE4tZ_Ka;dD?Np$kBLgsSEu$Aud0SAg=yYhC&a^`nk{QnJWz%WF+Qf}dT)HkVK+1%3ln(Z8KsCt-^9T%#+eH{NgRQi2g{C@{kUSHBHUq9aq$LGg$%t$=Y zRd$do>_4H5GsvYAs&Ty6W^f3o!Vhur!Zf@N)QRK@CvS#!Bs3F{>~9k`$yGr}LBcB? zE_Ha7!>d7kgi3df<3jOk9WL{!UxMrYANKABtg8B5_dO^msAy&;WN4%&RODM0WMpP0 zWRzqkR8(Xnq-0hWWQ1lWsAOjLK!z`w3K^-H0U0To3M#&2CS+7r7F1ML7F5*v-uQF< zU2C=1+2`!F&$;$>F|KZ(F+SrrUgsEdPR5+0>0lXEub->j@M@~Iu)qz!LREKa)gNDP zyLRtTwTH^w@K&nk{~lGHs>bhdUY_AblvB0fJyb1lpKGw+4IgmBA5eAteN0uSs`j6_ zVO1S}U$|jat-zPA9(ArN&-jWS9T@r@Q$1TAa}EBhsxA1%&EUAJzfv`y->Dkk zwP9_jT3}nMR@hfh#Vo(MM-%$F5uIGa)2NzJ7pnej?Lk$isu`T)hEJ_5(<21eg3fp2 zRW+YbH+*W+(gxaTXSG&g}rrvxT@i{3~Rb} zuKH26;vHSPf7M~Pug;%?GYgaO;T{_$~8{wLrS~dAd=c-!2NH_dXRpX-=uhodAYW|~L`%`L0 z^T8GC8rSztfFZ)j^-^hE=rz6W#E?>WSB=<`J5~B-ijhs)i@K z2~_ z%#Hut4S!A5^tDv&ncq%~&+uq;BYvl97oX1atr~XG^RBB}P*;Yv<$+Y~;`7|_MO3x# z=W2f{v+@{1RlBRH>QpuTbyTfj3{_XQJjT((e;(tTfrEysJu!)@r{>3~T0lBgovMZ( zr)m#9K~?)%ZoH}%FvpF5+SO-WeV(fJxf<5-mq(8}RqdKrsaimxb5-quwQl&-ss+8_ z{C}ou!9}#!bZ=9`b!}8XQm#%_Tl9e&{->%L9AbQX^NZrC@$9HpKF#klzreJTN8Pli zRxRus=ciUJ>wD+_R5iaJ8L#8(H>!3@gKMv1G zt7^s_TsuEkJ32p&s_xXP`JaKS`n&O}8a~^P$51ou;YO%x1$w$+RUL@I&O@B5Y6~u; z>h)=$b5%`0h^j|tBvtc|a^qFCM@CaM{g@0lVhmM(;U&4@|IVrvN@l*Q_foas6gQl4 zA3fR?4>)*`s!mlCJVe#IqFHYEDK}nK<7ZQ~g;`Y1C!4B2wqK^IQ`PuYZdg@&>{Tk$ zXLuBz=<#@ssx@2hhF_d~{?A*vcwQ?-RPR2}`FQ`M=; zzoM#Lt*hTS|DLK&RrC9msulXpjsL?9|4CJ+nxRwDN0)8>7pqpN9rMuw+q?OxYD+u1 zVO5RqMAe3!p|4O0x&qp@-Kmt-lqH0F-8Lt&t=&Dh*pd2^+B312{Qnez>ov(DB@9G-oYpGhH zj5izpq2 zsW!i6FkUMZLe-wU(6v|9^cQJZ=f8T?updsA@mbwL7(H z3$J&sss)X9!>3mHjd=6x{}|WcpQ;|qTN%$^b{>gTtzZ&WE0FA_Q`PgxROb&n|EH?y z9&znbJ2h`Ve>nr1@iaG~sFjN&;s%pp#{H0)nmAZDlepJ0qdyh{;R4cn_W9qO<$&J**Ee>n`U3`MyqP{ zPB(mN)q3u7{=ZVS+I!r5s5;>O9sH+ZO|YK{R6lSNo?5kH2XWO8T|MO5smiOUdX+iq z{9l#j>yWBvgq}Emb`$*K>Tx%Ls`kupZup<7rf+iNRkh&1-0-PY^J)HTKs7^u7PwBI zv?*$EwzGe#TF|*}{Qr}x73xKF?u5GOGfwvWcT_z@{fRbUAkm(^jHvq`)SLfDBmSN1 zY`&&>HYci{c4CMYG=}Kp8%NZsw%jA7d!PF2INYk=;b zs*d5eh}yN>Bh_+`6oc#bPS6l9pYPm+ysQ-1Q^sFStj9;udlq&O_NHR{C2y)E}hwcI1+$2UCmT+womRLeb5E%!*Z+#{v0 zOVM$!RLeb5Cw4{4JyI?ANVVJ}#kp|my;6G1&~lGd%RN#p_eh<3uawS;mV2Z+@$EL< zD$sI|RLeb5E%!*Z+#}U;kCeN!r{x~0|Lylkd7fs!XSI63CZE@4m=!K+)!vHdwDPl+ z3tEk8)!M^8_i25zrY1z&$6gzDcx)shRDx{1%DLvdh|gY1Z)Rxn~QFxVC>26A2mjtefeVJ`xadBDmSfp9w} zXb_Ce1BTf0JRpAw;JpNhu$U!4>{4K(V5oU61$>qPiA#ZDwq8&y=(-FTZVAhPq~*Xi z!3gWT9Pob$n7SMoX=Q?PL7$g^D4X&Ukh%gmC>UiyD}dmafmthnXxlHS5)6JBxWQ(; z3}mhZjtXLIz)B!&6|iU}FvgAuY6T-!0b^~!Dj;Vya9l9XhOGu7^MRGCfjB!RXb_Ce z2X3|H`9OXF;9UU3TTB5E`%0@0_C`Ug>pbr;&sPASH9&_yy-u74o(Z;ITD*qox~^fm z@s_X#NO~36CP=i-uLAysz|>cP305X37xXCvl5I*Mkop>MP%zPgUIT*H0<&HNQf$AV zN-%gWFxh6T1v1wGM+H-Cz&apoJ+NpU@Sq(L)Cxwd2d3JB^+3++z;VGNHtcmEas#mP zbs)`-2^s`rHvrRY`34~W4Z!;iAl+i#0Ae=+8wDBWxe@Sr6G+?$%&_%>VnNq8fti-@ zCXiGFY!l3~&P9NKF)+0Vn0?B*RW9vQjAq%CVw$Cv00#wgEvN(tehZjY0%Y5ML6u72aXEX+kovr*bZRPc3^`Y5!4Ds>;N{}f*n9kIdEK1WW&mV$O>R(IZ$H91Py|* z6~HE2UIFCq1iW_wr53Xjh}{Kj6l^xnU4YMSAaNH^X6psTg08!Pt(LGGNU8+33AS11 zO2B^)FtrlcVP%4HL7zQ9g-zK5r0xX{3U*o0ULbfMFl#SRY5N6Lg2DTMy*6VXkhvc? zD%fuW_5)$>1B>MjudPf_F6dJY)Y+73AoU~Qpx|2z`UnU<49xlnsOJ||fhxh^!@v(V<1moq9H z%m#c6gw+6xJ_dfaBZ6ANh#KIyEvNxpD?b4m?UUq;PW|uYs^yVA0n=7ds-T6^y6_y4r$TAg2yEF6d^%>VU{^fR%MXfE^Pw2*!Q` zbhqW-0Quhn-roX&7V|9-`yH@R(9=A>1AOX%#P5J0TQ4XUbgc(^SwcOK^gXam(8oG| z5BUE8O#L3{Yh{9RL7yLh5S#J?koqHVP;h|-{Rjjf17`gQgxY>Vm0<8Opr6e+24wyO z92E?(0Y3p@KLd+?0tVR;L9JlK&%j_?@H3F}3vgU;sSW!Dh&&Fg`~?WNV}b_3*yF$u zTYenK{}u556^O8yUxC=)fQ^Eo=J^}o(*Pv?1`M)Z(V z{|-!T1V&n!pj^=BcOc59{0^l40UQ*JvY>>{GW# z&b+g8+515k9vK<`W7F;@8*W~`(C>*=x1YWL=SLO=%(=YnE(Nyjk7UHmy6@2i~o()7zmoKyX{&?TdgE+b^hU z%LWZ@%N9QHG*slIUT4H zWSKeg;r0$U6gAWOag^u0V8Gz$~{b5P2rhB*?X> zGl2#{;hDfihItvIq z3s`B%X951*fl9$@3+N7%3(~s-1y&(QJsSu;8(3p$X9K~3K#ic#LIQy*L3SXp)~W@W zJ%ETFzIEAtyeCjA$m zC@s2J_?1AdAn!_`tJMi|h5^yTfNqvM42Zl6Xc7ci)Kx%(pztc7yEO{(hXe7$fj}!5 z4#Zx~Vmn;TVtbz07M~HcD7~5%LFPMx7R7>;5kN015hPs$1YQI5vE*w2|B*nYpsxjt z1j?I%SGraqt?VrC);C*fL5-lFg+u|F*8fJ?1T5E%_bUk8L+?sY(eph+;qqN0KP>w&^( zAi^32u{Qwm*8@YX;CjGkG~jmwFwEj^0Ez{rg5l;n8c2!(Qbq$KtVG~{BM=w^jI`t! zpj=QXh_Zkifz&ZT`i;OSs}Ka=1cZ(OqAhIAAS?lhxD$BLvhD#FvH>!fnq_aV5a%r10+oVQtkm}S&6_u2?(43%(mnSK)Ik&kYxc$Kx#6O zo&?Ob3PJF_Kxi_MZE49sm7qp2-$L#MGA9Dr_W}#7S`an~h?od0w5*9ht)O18$igQ9 zIVnKiB*3f=@XWDcDU@8xRe8aVsVugr`zSBka+N%5R9Rv%lPOEBKxLVE-cMO>aVjs_ zdX*LCJB9MHC8(^l5|vff`2osmOIFFZGL-@gc#!gnO;K566)LY<&_k3$OH+Bx_N%P5 zkg1e)HbZ5-Rja&i10JSquq>4~?1;)n3x9<2rY%q@vO1Mw8ERYwK0En(t%m#H4g~V#;Ie#BEk0@P8Z#Ob2#Y zayn2hs1#IKz~ew_29W+Zu*)h0!P9}z44~4|GJq;UjbN{ZOb0S&0NK-l{Z=gqdjg1< z0UWTb89=R|UU1ODp8#@Z0(nmWhpbKz`6LiM6R5V_nLvY}NpRSro&@q|0fkQjHP$GI zeF})51$=4+vjCsjfZtQV5sP~YC>E3ozA)d}KvE`u$GOPd2!32FpCSjb!;a~_aA7dU3sg0O5LVjl3bWz7R> z1@(gC7M=~{JPqV!1HW0FAaXts{WQ>MxlaQPf+oQq7BwHpe+DR=4>Vb$Aa(%|{|xZA z6+8p@JPY_O;H8eY3%4xbrLI^~iU7XP(j;jikn$|xZ6yN#=YYV4KwC>*2$Tyd1-=&W z9FV#QNPiA!Zxw>z=Yh~gfS;u;0;&Wxf=(9lJdkNX_VYkzs}_Xi01*asu`B~>1@(fi z7M=s-IEQwF;Msd5NM5p*cXBL#XwIhSPb~& z0e&w6K^FHSP%J1F^fKQ(AZZDZk_Ys$5`q6xAaDuL*OHe2<$_8C%{XCcdh%$I=d<-h=|7KE(;B3=RpS=LKHt)N~o*uqx;IWGfw zD}YO_P7t{gh<+Iex7?S320@cxh()ag@>cD`l?eP_0nRP}Mp|+KQ2q*&R2DEvlqIM|>KY*Z6=0NA zyaEKj3WTl!qAhI=P$j4l++ZQE0-1$C_NzdQRSUvi10o86F_u*b)C%eaV=eqOAZIO* z_Zl$H>I9MNfatYAoaL?s8U#&(TP`d^P}nuLB7d z_c~B4C>4x1-wi<08$ikiAkj(${u_b7H-HJ2{02}is1zhyz(yeTO(1jm z5vwbqMdT(R`Yj;Ma^C_P1Wkfz7PSe;e;X*=1f*M|Ahr~Ue;dfKg0}&mcL2XqV1~t& z0>y$-!A$dg2T0lsq`U*nvJ!#+79emlFx!$h1LcBBL6!w<0aD9=^ew<#s}Kag3xt*d z*_KuYR0(PX^DX3EAag5_{VuS;ss&;10TEk)g_gAys1?);7FqavK+ZNG?>)e*P7t{r zh~5U|TJAQWLC_>vY*E{R{2f5yb|B9h1+nEo{0?BL73=_fDgeK7V7bMW1I2<;!3y)O z0FrhBDHXs`Gw0RSUxQ0ug(F4VJYBs1?);Hd^>zAZH(tw-+d~Izi-qAbKBA zV!8Wx%fLGhl}#44pTav1l~QX|;T^~Ol+9M4!aI%wlroD`;T?y{R`dOU@}4EAY_k%T z?bi7qWrroJlv|lfg#~;_*=bW$c3FkWZVNg@skAheJ+@zEuZ2`m_Sp=T{Z_5=z743R z9Iz~v5A2A_K@0zg@}VtIIb?MzRW|G}rP^{;KC)vfhb`)3%Ez``rN$a57W)Z1Bff^6 z@u?Nm06w1rexCqGEbbGaSWqhX!hAmkl0E}cJ_U|iiNOB|5cnDJwIzQBlnW{abrx_0 zNc|j0KLUJf6@uU|fY8r@dQ1Bps1no&ez1@)fXpv}>@R?0RxJoS3PgMf{A^iY0=0sA z!Ep;e3gmos@+tc_s}n?iO^fKSXwhi7UjYq*Ccz&T^)--R3lx40G+Cn{whoA|1^%{z zTEOQUz^_hkCA{pGI=z*Ulp=a7@eNIqz6Dag0lck5;Qt*E_$|=ZlD`GY1(gC{3-}I5 ztq0P-1KL}KAozPAv>x!Yw0fXQP$TGMA>RX;KLFX^1D&l}5cVSw@dMDsvVH(+1@(fi z7XBlUa}3D)5$I-hg2DHqtyk$~zQ-xOEkUJ^l~Bz8H)a(0D>LeA$-m;kR;ChS0l!hsw<#(YScS@k z7SuoqwKSEBY`;oB3u&bEw;3t}tXgHD4fvfh$g(I_`v(iD|DA;lw(viIoIioQKY&ZE zP7v7yME?ndTkfAggP=(;#G;yj{J((0CLqEZ1+jkv@qYnBt>CZL;}-dNaRmI$@Gy(} zo8e-HOEo-vk*^mIzo(bqq7*N#Gb}1$(7zR<13eiYX~~{Ixu8-IWdW^#)Yd?HD`1pW z2!g$U(AGe-rL_jC1T}&iEW``Q^airMfEcS5gtY-8yn!*6IGvhybX}k7RYM@ zjI%mHq_!ivEf8n9ZGi?sli*g1@&WRFfkGc3-Wmn5?SOb+;0`O$w)(UO{MrEt7S|3a z7L*Fcn{Rs{sRNMG9!RtjfxjOR*a4Ve$sK@lL8Tzs0{no~jzGE}FwrUm!JUB6jzEf~ zbp)yeHG;_&(h0~s4an{UOtES~SZ5&OG~hwYIt{25)C;CscxNEzbRe%Y@QBq3BD(<5 zrvqu0dpgh{XcA1bs4hVM89-qdAl(`Tv0Z`qGk^>$I0NuG6Y%Q_%&@qwK(U}yFw=a` z1d_S|DQ5z+tVH1N4+M4tW?OPMpj=QX$g%)`ATC%1 zZy{#^ncacxvw#IwEeJasi0BS1w5;wxt)O18$imMCasq+8vjMX@L1YggIuOXU+(4i~ z&?H!FQ9XeCoD%fhi7XnG4K+1){HY*YMUjzh( z0y``@6et%|3Mwq%A|SONkbV)c%PIuH{ejSaK&7Si1F8fyg1r{fAIKa4WcLU5TeTo; zAP_MCIAB==fLcMl;Gl&M1abxec>{q%RwszO7>FJOR9o&Kph3_iIBZcD1Nno2!i#|# zYZSy@0>lpnKDC0ufX}6X-zC5ii@O9U7L*FUFyBjoq%a`mQsAhS2>io=z%bxzOAZ6d z1(kw23kU~NF9XuUfp4us5Ih73y$qz}rd${#OHm!-2MzJRB$& zR0@18;A$Xs1dx6;(B3Kp!PfwxBLF{38v#@aY6P7ul zjRa~1^@6Sz9tq?`0eO)?H>(pwUJFD=0Rfg91vChn1l=v_S|EQEPwx%C zKu;?e1^7e*e%Ap(7Iz&`EGQN9GT&$*>3Sd~8t7vs0{E~pfQSilWH z>S!SS2H*m#5Cq2np`(FNOB)ST32FrWEF=cVyb;Kb0R~vLAZ!c}aU(FuvTg)w1@(f# z7Cr{Zxe3S{16*o#g2=Hz^i4px<=zA|2$}>#ENU!}e=|@x7KpG$LF_mn{$^mP72FK? z!~%ZffMFIl4k#9s3Wl3+ERYljq{IRvtVG~{3lJCwjI`u9pj=QXh_ZlNfYe)o^jm;Y zRv`$!4G6szh_|H>70&s^FBmh3+0l&L| z1dF>1C>E3o#+&bWAn9%(Wjv5*B?A9MAnxfjSw1|G3G zLF7ar`d%Q-a_2~LRwD4f9|)Wb%(mpoK)Ik&kYxe)1F2Jh^!tIiRv`#}00^A|WLw%4 zph{39m~SBu0GSVZb+CC4cwMJA9fGijXc6%sEf!kmM}b;Fyw>T9(+fZ3yzG*BZ=}{Jv zlEy+-T8Y4a8Y_DCqpav^OV)aoPoqWUqqHcnfN4PLV?g>eV2xD>g42P}$ACghdkm-& z)CksENIH=DIFOwVthZ`GSOyUBIIzL89tUa#^@5ESo&n@c2l6t2BC8Wb&H$pP10|L_ z9cU0V2{u{O3?Tmrpl}9IYK?-}nLzv#z-B9W0`Pef@S6#gS=>yZSWqh1YQ9ebNwa{I zCxLBNBJh6-2%H7%u;f`lxu8-|VF6D8sk4Ffr+{5nAqdU{LT3Y&mNpxx64VIxT1Y04 znFVBL0{g965H<&h$N~;nRu)hzs23cx@Hs%vTp(`_aLDQek@JA)xj?n$&IK9-O@hM~ zH4n(o1`6i^HP$GIeHw_*20pcdY`|we;P*6e#NwU?iUp;DFU)s7kn{|YG9Nf=YXFr>p7rSP%k)c;fsKr=YhOMz;9M3h%_Mjd7#m9p9dNQO@cox%7FYFpwNIO zYZS!h0`WP(-&T+V_`Cr4(jypjJ>X=xX81fSlz(-ZG$@)d?bB0-~1#0hYTQXb?0Bx?9vsK>iA#@FgJ78U?X0 z1Mw?>o>s5|@L37?y$l3d+{-|*pj6Pyd{+WVtALc1Kp!g+_^$>6R{?!3c@=g6HtbcPL9p^wV2B+PFLdZ6n%V1y;C1N>hHwh2aB=k-9jVCs4x z%E|<(8-PBq1EXxp>p<`uz(GN@1#JMT1hX~(H`snb=0;%f8$gWBcmoJ~6F4duV*@q< zwSq+(fw6W(kW&PVcoP_B3*H1Gi-F^UI2%?3GzeA}0k_&QL4FA^wit-F<;6hkTYz^7 zaEHZ|06v?5je-R8dkVVCvgIvXu!^ z-vRoR0uyaYDGL0@Li6ApbpJ>{cM%mTv`Ow*lVo0T~wa z9^kVb*eIA`p4)(8LE<)GrmYtw?Et!N2WDBqcEGjPk|?H6Qz2n;?5thX5lfv`isQNacq@F7quSo9&V(T)gms(=xP zfFfIP2#Bl(jtfd`SQXG9SXl*ZvSWh$kASh&K&dUS24W8b-X8&*E#@P@=VM@_pv*iE z1I2>G!@yQs5Aa!t@5hvFmY}lTN>p}O=Nhl^3-?V5em2F+!hZKU&C_e)f=9YoW{p<| z@4fu*41Dd#H(nX7y7Kk3PrT0Zj_1Dq4mRa;ub-Kq-CfC(Qxf@p_664Bhm3MSk^Odt0@-?AoGFeqina`rU3n z7N`H~!qiD8<{imTy>-wMH~F=qmmfl$Udyk(wJTzsyK6!A_Rn5EtzVDxC>lD3yIv5# zhwtI1HEB4H+Us8pcmL+)<9!pqve;hso2Tt-^t#x)@*y|3z0KfdKmOtMw&(7m!au!M zcyg-6{N**-yY*A7MbV6&-m^WQzubGBVyFG-HC;e@zqB$3O zU)CvLU6!Xu7bYJ+Dfyy%*)|CLBG zP~W#{%Nh)HRcny&;6o2g4xRMiGg#*n%_eN|)K7I!pGM_eXg(3{e<1OphmsQ?xS)(Z z+WY|cenRhAjBY53iSj<&>R-dg@E2N{v!mJJtHPRhctP_j>zHY^;jn#jz4v9_6F>6w z=%|PPQyM=%nvJRcgojrf(@Wdi&i6Y8SwffZimn*r-J=ah))~0p}V+9SRxQy+XNVSG~J{-T(8?{00p@9xLM%^Pap9pO~;;Wv)j>9Fah zMc$Woe+qlWT0Pafp|RMl+WznW?A;eQSO(-i-wn>O+8 z|NWcDoxCmXWq2Yu>KG5LtVqRZaV$H z(3^kon&RV3?}YS!j92H2X_(?s(boN+k2|`C`ptz(_rnXFoN2l}&Q8O$VEv%>?^-(D z>CW`i$bUFH15>a5@0ou)>*nUCA0+l@UP})T|2Ay@$*t||Fu*m`k3yd9>?~*cKgaZR z*4>$Y270iwvoWn8Z+AS7x&`-eroC5+X?f?kX}f6uzvCJPf$G(duxxeK+ZpfNJ%$j? zysw*>H_0ADiMn7+9uT0Y2jJa@Q~j zd(>HkvvaX&&aQCQ3!Cn2sI%T!lnxTQD=}?cA99Pc;jZ0z+WuP|UhS|i4R3Qc!dWnO zr?YFEg<#{IjdXTCcDJ)gOnc!1lIZMO*X}~>VVU;-D2Ji+r@DsMIlBmJ=PcS;KkN}s z3!O&SW7<0Xc6X{f#bR8$0a%)AcO#~=VIX#BUrzshn6~OkXA$&IbM2mT(_Vr7qFqHd+u2b1S2i2_Khxor;B?nezW}7Q9!B)D zYU<`VyNdpgF`dtIoeih|AjaP*9`l^(G~9;ibk24*g8rLWL7mP|%k=nPL+&B!=DUU? z=}&GJ507V@MKYX1blxw(bZ$qHkM*oXm*d*;r=!OaOy_+rrX4bhB(ke?-Y<6TuA@I= zg7zQXiw>jdPjU_OoL!IY)b5~L;_L?c`?y`b6jSeL(w81Rk1TiX_^aQel9kc($V<*{ zq+dU;%kxNv#|npIz+$&0FFU&li@@~cveMaD`iHr8t1$Mk$IWC3rYD>NOb6jOa;Ixo z=%$Uu#yfkh+1USa;N1?_x`wx4mpWUAX(6|gzRotdcDG?OHIhy*tlCNOWSx7~+vx0e zY`wELo!x<5nE?jTQ{?bY`iHrO#m*A2tDKcMy9>M8*;~%WVapb)2&@XZK=p&fdjzd`u*_ID7AeWq9Zc!EFw= zyNPv_hhXPYcR0I`{?lE%a%YpVEI+wIy-!R9%uboKzYtG&)Yx;+lL zyx{S=J0tcwdkEwIzunyJb2gQsanAN*+Pa6ydCm^Fc8_4s={%+Tz*#E&i<}+Ia1*D2 z&pSNi8a|3Gc2?!uO~Wo>As0|Ta_t_Ye}-#!*jYN(MbE!6!H}MSmk79bE`P|tP^uOSy{lZN<6MNCwQP=KCY!)_{`jxX;^k41M z{QAe64u?;H*SLvmoz2F$e$(95Im={dnX_*&J?>fLCF~Mvy=ymz{uQp>_n020x#VSM zKkEITp7Q6Bm2TqWZsKf=i$u-cuWsU}85)A=)vUp_n@|5A&8<8<8l64Ea2`t=LjB!M zyMX=$S_8U2UAt$s`=15%YS!d%A^lmd;a|?4!;Z6=!>E5dTSWgS*i}@$7-(IeCpR;# zUT8d>8T~nABx!|d)*9B+aP#r+y$LA|K7bL|$>KhYiO?VY`d z#bbI=>EJAne*H4`4OBm8OX%1!0FCb(EqZVwu`fuv6Zgf8O~N}`xm>0T|pg4tH>-&FChM|;cEJ)VX@QzXZiFe zVtV~J%UJ>aV=%pbba(a&{byq8&UUtj{^zwY?f*cBuhPGdXSh45J)9NNzY9yC_H_0d z{qJFSQO|LkQg+A-TTIXG{hV#0-!MJ555ROBy-f~w;2*kym>0E_+`)mVr}0Z%yLafn1)HkC_0Ul^*|Xq}Dam&kO3f*iPqnTGwtDmeB#sq-T`F-Sqo8yUtl9*3ntCvprZ_Oi!`bJKIaYud^GR?ZaxdR&=8= zMtkfh-=44xj~hWfPVbZNUBjC(t@Q!&qq8_?A7J~iZ0c>8Mjs^mUAuT^A7byjcDG|% z@F8-*wY#&q9s9ouEOwaSCa%UJu&1eaIs1tICNiIlcXpWm0_Lpe`@5ZeO#iiReu>U% zuxE+7dz^hj|FMityho>Jg2PYg{|Qr<2bfaujp@}p-#yu&c3E!uLn9MA8=Mn z|0>$4d(c@O{i~fl-|X*kRZ35oh(-MAt6W+4op~ zPQ@~68m2w-1NnusM5o#`*X~F9H!xCX+GEa+(f>yC`A1K>!=LEi=o&um>}RaVS%$M; zuwpHZZaSvk<75I0)~Wl1YxgVtIzDz#XFB_h{y*8%I!&KEIl~49{&qOaHEhH@nOF^< za`rp@4YbqwJlokH^n1}xr&^}7Kk4^&mgP(bZx>8w+8k#Ye=%@|!?_Os#=1J2=S=5W zH%wi&Gf(U%qSN$gXRR3i+1Y$&tr`Bs*)z_(82;Ro=U<(>3mkfbhZO8tXKfh1g6Nc7 z=&UWn(L|lzUaObSL_J0moql?IE%W7{pE*l(R_WcftR4TPVV}`&Z8`si?fFNK)93W( zxP~1tJqPH_$aUt&aEUvrUvSnD`m zdi@>15v(J5sl(H;fy_k**)nH*R_k$==1g7g>Y#yFoOAIx4<$^mkTOT2pvwByMq2x+(6&X%Okbz_nxtI*rs7vU%l!TFR5=wfJ z-lPvXkMt$MB!rw#E+7|@vq_*Ol(*>;a3A}o4F_af;zN9OaI(LTSWS7GQ5XG6?{B1m zG?L%RALLKcM5eLYkCAlpcx$`4qRqJ(k1@-1@;J#Lx;{FCJV9nEEWM1q`z{&7@K|y) z8AoDC9Jz(uN^T?Zj!B!kFcatXPV=<4U`qzn0w9a>F3veI2`x@UYr@0a8#`HFl^YDpd0 zOZJiX$pP{tXKW^!L*|lfGN0^V+;;LP(d(IBsuFpuCy*qPOztI>Jf?ey-dyP!o+ssY z9(uE+H$sOjZg-n;8Q;_U1JUcDUjOuR7tWJ{F3xi?-s5s|1sO`NB*VyXqD$kt6n+hf zBD#z{i^qC4nLv_>uFu|2rjUoo!z7iYkw*#FWivb;qo*|!|Kxq*qU>?%ujDtdgYrnh5Zql1a zP}hCWBO&B`vY(m0PY#ehWFOf@c9UnBW*zl)vVm+QZ;~RS%e^J!ExilgMBXN)l5O*+R<5yW~Bxjco70@wS7Wa#BHdl3ipssU)v4qy0QjzE9p@ zcq4g}T+1Ocigf28{g!dxk$Uny`GKsZ%{sE4yiPWdPw~&l5%M|tmg&FK@m5dI_v9z? zGx>!aAiKzJ@&b90EFnwDa`F;cMasx#X10awB)iBuOjm9NKbMO2_ol`k+i*rTupSj_YU^PUF2?} zH^F*?tM{pTU#j<^df#b8Z!7gSQg0jeHt|xX4I|-1@A&l&zgK&Y>pgk{o;(CSs6EL! zWCkPkH^oe{R$EKflZ~X9l#orNfGj63krm`+3pvoHU&hn)&L_{11>{+>kUU2gk;_R0 zxsZgCi%37xi}WUaNH@}f_>qpJ6FH4^Cc4tBi^$)RFUeQrYw|qF(M8@|dUU0DG11lG z_3U@9{(6icy5Ji@E+a3n6Bm;g$x^b6=mPG`WEIIL1!N6*mAppQlJ(?u(w2Gn5MR=c zv?m>WINVy%bBvw$6DefjYsmpl+Jg)~&xA&DND6iznM{Hh?nN#oVWbB+pXhJ3fz(0d z64IYsKrSSF)zRZTawh3U^cP4wy~)=fY5L1g*F{E?7;+;SLvAAa(^r4i#*tfyE^X*i zhAv&)LGC08;TAApB$H&3Ib<%GN3zLNpJ^D;Hl%1yw+6&o&_o&;*cCv$PCc3bFJ-LBgM_wk2 z$%|wWd6s08!K4S#^hAbe@kwqkzEG93KC1fdCMwXM8NGQ38^dmjUxuh5AO)~oMPiNvm zJV`5Zj2DAnh%V3WC-0NJJZ*f7!#zlgr9>PcD?FXBzwkha8!_>zvK zE7SRt0P>Z7P5op`cr-xxsVZ;5nV41BJG*516jgCMliup>bWGG zTtfwz*J#s>Al8Zljm>PP=Y#GeEZ zT^wmoGX7+wt}A8nG@@@l=tD_;;CMZ`fs7{lLdqC&6Nw|YklV=JB$3=hCXghOOeT^^ z#tHzLPNiQ;; zC#0!lGI@ZckV)iTl1wJEbyLWL!LBQPftXHSr?eqz!3H8X5nU{^HlqVD2LO z$ou2~`G9;#wvugRJJ~^ABkPDR;p&oY0Xds=B%O#iX+yk-uGAhO^&G^<$ab=Wl#>dw zlVmL8pXKBwvVy!!R+3ffXX2G)6)7M&WH!kny1d(koI!fiwh#H0qy9J2Kz<@eIg6t4 z7;+OCN8-q>WDeuzwrBs(qbHlRBkf5CGJ(fvE01|7{g;y~nb{@eQqqIG!MHbxF6-XQ z8InluCh_DJaud0MoX&V%2;IW`-Xz>T;h{@_J|vf8t*g%eGdbY&$@yZEM@q>`COVIU ztS^bC@fMDUGO~%>#|jnlIKM{Lk{3uW=}yijPx08!CYdCQ%ptpZIQEbV@+Q%B$yZ4` zJ^t;l^yI#=T7E!??k$guX#JggtgOxrzB?u+sW6oWAJxDB*i-8O>i@JA;A! zJPfm#csAL~1iIE7!3;-|NTTnLc~GOdWU!I*yPV;xs3XYNEU1p?t6utQ*GJ?qd5`F8 zUE9e`v`=9clgTwCid?Jne-u5}k!UiS9N{s%leJIOZX|t~uo7Fqid|0Cmz3bgf3db?>6VX>Uc93#X zN%oMvWFOg2-X{mh2jn0b=4q=w<*%W4=-*7XkTUWv*-G9c+ej9fM`n?y$Z}TbC9;yN zCc}t6R=S*=O&({@zR1Sw%Mtn*X(5?TcH^h9N4HS*JqLZS;eA~g;h*L&G^}PX=8%8w z=Ks8xzhe)coaw#n1brXjZDwA{g7rNFeed8xGLGRzL_Yu-Lq-yPtzZ;+iL4+i$t#_B zC}+_#jUAgO!*QfJah(3KUCi-w)AExWWbA2%0p9~-u zl8eZAqUp~d`aE2pfB!sS~~7Sey5z4bFmC21r>yYu844r2HC5Q>cI*6508_i48Ud*t+zE?))AHw5C_pcAgtmF1JXJ+Ut zh^u&s(4WRk()?%gLWY0kxSC3$i2h90wNRZXdQm)$Xs;h3dITqtUQBxy@h7aB{=D&E zHXc0ZHP4q>crcl+NOA3+olV$BXL@M0w2ErjjhsokTEx*dJp%&h)!iGK?qofe{vb;` z+NQfc1N*;d`wFHz00ji@MNqK?X|WKykgyZGvAY1fJH~EZyVl0;wR;Wh z?$)*c&vVWhFCgshclY<-&py6$=6&a#ciwp?&YW`(%Fc&(CtRb`0wKsN1@J*vNuUIf z8}Id|J97XaS%QP#EwAf`Fm`Rr9H@4j7K~FrX*U z0cZoX23i44fW|-#AQGS%Wq~quOlc&<0(XN`Y&E2~jD`bYKqycZr~*_5DghON@<2JD z0>Col^S-?i&;Y0h)CIT|=gdY zBrB!?G&UM&53~c?0zH6EKu4fE&=u$mbOE{nLxCZ{U|v5cmRw5n70`?1Ka}WB_6MA0B!=8fh)i|R&oGX16%_B044wz zfm6T<;5cvy*bnRjb_2VBoxnC=2e1rS00>|#Fb$Xr@GfZ_Fa{V6i~`7~fixqbz)WBUz}2KMFc8s zE&%6&v%nd4@@XWO80*O7$koJpMY1uOMvcV|9%9>dkwq=-T?1__rM2$6@3O| zCDg^3V#SOTPKJz$?*I*srNy-N0l=0}kV3K*RK(y(+%i4*Jf-C%&lB3LKo-Cbpbnl^ zWB_>IW&o^#(#Y2XHc0d2mL7yBW0hkKTV!Mgc(ccJ3!pjB43HHyMLGni4io{n3Ka!- zOzZ>D5P59Eqvf1H4!|9726${t1Na&TU+-X=&rOxHWS+Coa_*z~42E}}ye4(i(%C^> z0p_Vc>ihnk=h8f%=GpZj$ma*m2jl^A10FzLz!P8`1b_wsG}<5Vi^V@|Awec7TOzO7 z(!|$^(^7`rKyOf%8wlxq`RuPuWGMXZdnZ&U2;FSX+Nl`J4tAKO_5C&8Ocvw^!s0365 zPCzCUv?>s;T~mg`nDhkupZ(Gp7g0b1pa#HJ(0ax|ebAaf9iTQ)3#bd!12`!)0&NIT zj@Qy7Y*gazl3q;g?Uu0+;>~}1pe@i2C=Vrf&^Ul|NNg`8Vu1a)7y#NI=nZrScox?M;MrVfpa5jj z09}!mBY-?s;to((PoN*r2VlkI^##~ara5WRu;EA#WB+qT8-fc~JQx@Xa8@Ijw*Z(A zOavwXqk&PtNMIZ=9vB0R)vl+4P6IgIP63?^OakQaBtIIw>7aslA+wQSFw6uPFf%|I zEb{>Jm_JLSb3x|-OMpedLSV5**;T86)xcWdD6k!1ckKao1G|77z!qQ|5Kp&k0)7Le zOJ$#LKpquJ#TzvlUQ?Eo6;KfslD`>9t|xIh8k3kG%iy8{sZdfXOe`qncBYUo%Sg(y zDaq|e{_ns+-~h0X{l6Cp=?+?&QbC!=7E_6=NYX>dBacQN0S;?OsS$~FZD;?p_3RgR z!ymv!fPKgQl6^(Dv5PJM#{pK%|74}-k#`0-37iH_0Fr-7%a_-b`H^P-p99V&%4DBQ zWj_Rwm)J|p=YP@wnNEP*Rp1J68R!LYIQ9UTe*?GW6O76URP z&65`%!DIk@k%w=$D6b#>0?LyZUQ^x&pif+JZ3d)o(m`=B^6ijk0(b-M1ULdbCg5>F z7N8Sk%5#a~xd9KhqmV)8I3wK-RA~Xajtb-%1C_8a6;e5mg4wFJKt7}scT!$l(@XB4 z_+NQ*C^r)Ak@f)11>~%Ue{ui}5mGYRL4bww15$YzOzowC9KaUl07HmN z49_M6FikI(zO~Nod<=Qxa6J|n1B?Pj0wct=x7OKW2O`Pg6AKyx3;_BAy#cObe3@+x zXcTC7pgqt9;0te^fL1_9paZZ1a?zkGLEC|Hytf5y3A6y30*!%UKv4jv5wSm($AZYf z|H=|d1uR?y8G%3mz;hFxpYSLXALLPvH9bM4tUKg6T6zNh$oB&{w1N`Jun~=b#;B(u zXidHaUmb}^pg16vMIaptR0FC2JaMWBQ~=5V<$$t4X`mDk43q#$0wDlp$^(@F>Ier| zhB|p-C3T0*A01HHCd*Qq3ls$3(!N$XLnLZFYS6e(ulKI zWu@tdObYh_O=+p6G8W*fBxU;RH9G*&Ai-OB>BmW8BIaVX4XIJt6Ov)mx?lN&0%#75D$ z)O5tMlH`h0Q@9DTe*kg`mfI%kpt}=$gB8*v7l7kHDs{yE*mFsXWuMaO>g{V&~}TI*ybY$4s5T8mQZ0_sfM?>~8n8;qZ=N*Z)Bct)eebaiT@BsBw4 z>N44=SSpi!ms-nMX-Y;W_MD6bsW`RKB+Jk%Xi)5rC{L*Zjsn?g>AIBU5-Uw?z~9Bt z4sI~nI$E1r@W`P^15#4>ci!P_NEv$hC*vz3a{{^`@gyVrTN;+U5SP^Tzzm=V$`1wv zPK!IPOP^LM?%PNbd#q=tb~X>teCoMi4I8R=5LhKz6_t zFadkOtBZ2kkj8kg*GQiL(j)yI^f+)HI0+mBXovwkYv2s7(}AW1o*|tEGy(KB@D$MF z`W>ha^gHM`;4AP2_zZjm{^a`q0f{@nE#NZnJ0LASjWmro3eW%=2dkCCg~UeD7_CJL zk8Qv9 zI#X(p;3e<^cn&-R=;kK?4ZRPrqQ}4^;34n;pl<3T zC8&$g(y>1-qycZhq}$l<>_6tyJ1mgAbm={9-yTI2Y7I<+PGPNK4NHa=6i8Gr;l8a+G6B7|3kI zWsu}LX++{Y%QgC6x{p?oQ3wbD3IcpJE+;S@yxgD#fc!u{zyn}jULcQ_mNKNJ0P5l` zIDe_bS(3lnkuoKa4&u)z{DD9q04NR=1AGBbfQ7t)1bCz{s2AV^u#6wTidhMjGLMxN z)vjqQe<~z(QH}|-1+55_2mVC)3ZRv= z>uAt0;5PDkSWyLO-jwr(yBg9aln({1ng-7$kyZk)F*4|8wxBlB?SXbcTc8cl5@-mt z03v`|KwTgTs1MWuY66i!IKUOrDVCCa&#^i{tt>?@PX>8S4fO!ZF`qS1rU4+=T`r%E za7_!zr!31g2buv*0qST1v<4EjqJdQ_WMoBUT|hem9RMos4BAOccL(hX&>$XBlTyAL zung!0^aQxs{}ps8uq2HLOlQa$I|%84Kpene>JL!1AJ7Nr4X_;1H&J?kmX1l3C*>v~ zZzwQWE zbOo>+=nrEkyAtXDG=A4<3Vs9K0IUT}P`*aXcSCwJ(whK2Sdenoh&fp{J7g%g9x^-+ zps^b@`Hb6UDmVrB8ZhpG-T`g` zH-L6%#dXmA03S|W15E&4BV7o*i%4Gp&I7q|eGZiK=RVNNs5BjPH^B8?T=xW8A-xB* z1h6};RT-`^-TAwBZ?WFSP&U>b$s9mV zAQ#{PzCwaZMZf)A&-LegIz+Vmp}6Jf?$?C!JRV=_SAfpbyX+;BS7|mX-k9 z(JU?ExH=MLfige{P!cE(lmaM38VoRBrb$a{*HR`N*O5Rapd!E?EC*1wyhfR39;Z>t zmr9E`W}#4kKQCgTDnMm`!n~$n1R#HcR87kxzc4)00JJW^7E5F5AWglsfm%RKfV%1f z^#IB-T`E>fGy-i1P>B?h>8M2ctdt6x0F41EWggKKP+Nt?9_i*sZwDaFipB$RKrGN7=m$_a<=MAPvr_VzX1N$GJr!ASU z2eJQ$Ai;vPn2HAjY}r6SRzf2NCAyaR%%c%hMk*^AgY;;C6^#N$0&0a=?~xu3D0Q*o z#1&CUDwkHKc1@!a%O_5c1CL%LW&=|J4#SCn^zbC4W93CDTucEb1LFZ&NmsL983R21ZDu!0Rc<{=tX*fMkO~qwS36NDsE+Ebh~URm9P-6r84##yM=D+qNSxgV`m|- z0GJQV1?B*3Aq}TK$|Sy?r{tmkWoB{#dCdx`gd>HD76CMXd5Zyhr6dpzECo0Un5IWL zQYwM|3UmV)^%a31BZmFY$%ZawAr3`WB8Q;ltb-)^6 ztp+LI3fI&VD;06Cz8M*vFh;qj!KJd$ap-XI+zH+eU^}o4h}W*qf*u3*0|x-^nGS;< z0uBPd14n?P0N1|bpeKNn0NzB4wK|2wY2XaNf|r3yz#qUB?fNPx{#RCS8dm_7aN*&` z>NXg+06rVM3wj@*488dTXbCXLd0kdZe*k?CWZ^dW9TG2rx4;|V1uzeanKu*kHRvlX z&Acke%L05yn!B@apkIN{z$ZYKqs$jv5wM11~md%fh>SQyXMg+>*)>} zYlloKr(y<0GeAZ&BiI(a2++)+49rZR837u=HNyt9Drhx;IvB{mAdih8XuvX!vO&~I zJ=*AswQ_(!8)UFj6VfrDZ2=dg2Y@<(W&<1nXDv-dY<@)(O;LdTm|U?e#EMhm%ZjCOsp(tJ z(VZ+n<<*d;FnKhR!Nz>Hie6D#q482{Km^LM0jwud(?uQi+5h#BQ5UEK)CM@l<(TIm zE8~fSGK?^0QJyzHR+Es=gQy9hqku8MaDX>j!$5}seSqFTFQ7Zn4QLKD2BHA25#PVZ ze*eykW2vSK>go&UnvXp1fvJQN@xIiiXboWAJc4s!dvP^KSf3@`wY#!@a0hy}JV4{e0b_y0@=Q~Xe3qB;6WRZ)l$q>X zbhXtgq*nsvk>;6cX@IH{tB?- z<)CV%YMm=^{R#E?0(q>lkH9s*tnnH29gq&`ub}UNx4;`Hz16s&(lwxb zQh65@-zIQ<6R<|!2GHvOhuj6wOTY<$3Fq zLGJ-`z?*_=p5MO2H4pz^fOf?7QQ$fI{~0o#0#AU)z$4%xz-{|0jWWRAAJ))PNZTk*BBb(PczzUFjnU<83 zU1OAu0vZ4{fV7%C%MgBm4f3elI|Kgl0z832VzsNGNUST8{1y|x$&?w$2>b#V0M;+< zu|}G9bA+)CtdIK1uddZ$gbedivMasDBVXDNb7B?4*kCfsgO>?{R3!7VAZ-WOCd;RT zScZIifMv6Sx&Y3A6OavX033nDIxV`;BUHk#P$d?iGI}j>9z7_HVm{0Ll}uv!Qo@^ZDV`YmrVa?$8iFi-j8Kce@X`Zjj z`&*9CAmkMR`0b)QFl-~}N63SvOq?7LP#&lVaQ+MhtqN2EssNP%^7%m(-u~P~JE@BwQK<{m;omi& zJ+*=AKno z26h4Qz)oNXunpJ_Fi%pchf@eEWF_4IsrwLQD`~$Uuk^c7{N`ck7TXq9{eobXD?`a za3Y_KYte3?O&@n_{=}3ymq2Ym?+m^*cBW(30`At_Q)EIA#J$i&daRELoaek#xLe;w zPDXG-TDE9fxP4D6^X|!g_wIXr$laR1*`&FAQf~`db?G^eG8glVcDGKW<#2YVMAzc& zKRlY{d)eKZ{>V&OP@28DSi^Of%Rd|AZe5b+am;G3v38CfYMk<*)KhnBzS+Xoj{>DB zYtQcax@hJ#le={v#{%3 z8l%p~tEiVhJds8(*5onx>hZ_6Z-Z9LjO?VH`&el zAKk3{Sz>VgKV{fiXX^T>oVqAqFF&Xk>qi<)dYd%DJFmeh&159g-U zQHsCp$O{g;BFm%d%ggr{buFACX+#XmR)Pdu-=vgPanq^mzgi^fgOin7`=*_KzJi~t zg_B(`j-#xJcRvo0STyMKi&?!wil#;5gOtW^7T@z4oCA0VVGn^u1Mgk9)@;S{zO zv-268%v@3_k#2MAF8=+ZCRrrvS&P$@;Mdydm)P-fm$DbxG()fR_VV$e%h0x<{OC4G zJK2cn{AlY?^c{7sy5bnH_kGwI>Qp;=mYA0xZQTF?CkT}CTyAW4CHo7D06%I)&Y8g9 z%IrS*I?BSiEuNuh;AaS!Ah3OAOQ(uc+U>UpI3oI7!KpPqWYC%_w{uxIu&+b`gA@KR z{nrr&XZ$JoP6a^DiC04n&ZavEO?tP$u>l_DN9mg&ZnU0xCw4+kpCOaDQ~<5amPuqN zXb27PKnyd226&a9o4@MuDJTGk_*jfPfx#fe7|zI?wJvl*Uk?z4>f~y{Ey~v>g&0vhio{H(SIr>O zcA~$hAyhU=ZJ_w#X>c*^MhMWhecx+!S@LdldX)8sH!$AM3m-3Zr7oM=VXKBtwpw(p z$XmVfI*Q~%k?i2mk*8Nwx!cYmnho~x@>dMYCgQvdzGi<&a87xVcbDDl@#X46BEYMN zS0MIS*~CdNgOh(KBM?~x} zx|l}cijA&$xy|h8Z6OILS%e+zXEj}f7dH6m7K*`z4Xt&n#FN5sfk!4`?F|n<6u(qQ zcfQt2pAN9ESuf$O!Cn_!G622hYt`3jS^%POyjbs#-d`XVc!O*d@g&E^0A^kjFK}gk zjv>J5TXoLD+aKL(=PZJKFgSBKtAq2G_8k}R8#dyqMbgJvboGI! z7DFNfbfxc9;?U#BnelpEOrV!fQLiA_BlbXt?zy<^V`yzIfWbqzWH2mS80CLxD^&%0 z1)#(HTt!1)R2B>gPVt!{?}~S`*TzA@Uybj|u3`ct^tD{Ybx^U-*WjXE_?l8IrUcO` zQP2-VFvU0wWOHLgV+j0EJIT-BqT3@@BM+T$*w5gnPoG14@q=wXXeMo&Sh)V!Jex`u zrS%w=P?F4Lrqo@Qq!W_%(_eIn)~#wsn)c^Zz4-FLfX=i3IDSsG1Ji)jaiJRpyDy6~ zB#*?v0JJQ~L(E1{m@6Ut==||BDzd^*!7))-H(5j-Yx))v zm|75*Ru6=0AXJseErJ3OhxY&Zc&dgQ8HGt?crO^UxIN1_?|6arQc0g|)KrC5m0yH?lT| z28 zmp?-W?e`LMOBkF2%0VJKB)aXWwQ`L<^D9L{Dcelr6!WQC(`DcddPEDVIPt_{gvegf zkVUsk_>?r{FlT@z95-(I+fk9vpLa$Xwle^CpWdQlNz|7eb#sxcxBGdz+1H=tQ?&Xp zzPf`$AJvIGU#-#W-*0L4p%sHfd`XTdKXI|7;bh8{C=;03U+f7+r{?t+SAq?prrfBQ zo!TLP_GT-lGz-`3XbtMIS}jVKGE~!d3=|Vep?_kqQn3ez8}iSswrB(W4uxJtaYQNh zmojXUfn}NtLn(ND^UBQ4t1p?aG*B6(E5zy$?4_y(sVm{i9Ca6d8v5PC(u%s^u;|l2 zn_X+1)#wk7wr#jCK8Ii(yCw>kHu#xF;&;&W;q)mX2hMM;>jb#~?1>R?O0PSL$)ybr zSfQ4eHssI^5ywhn@`x<1Zb>eW$n!0@!3cG6S61kj#iy0QboHR5spqLI~bD~X88$o8!yf~$g7uOt%68)lkP2>PNhK-pPMgnzyDpfSZ1+J^2I@71r;@tfQ+qs|t2nz=3HgFi{C-3g;Jbm1| z8;TQ^rXj$;L5EJ6J{;3w!l~pbd@G@mC8~;6pt?cgR%?TO4&|onV^!5fCEx5>f7CXh z4fJ5E#r`Z6Do$2{o{8c?C4_2>FjY?8Fkd+f>&^?M+3K98nN{3}nVL>bnd*AkGv*X0F(pQl_LZ-0z8GB< z^X!rc^)92qI%D0IZHGj22}7S@V%Q>bM4@FTMbnxfuf&_G*wyGGMZQo&EB!B#VnV1P zPIpMyRRi~|C|C_#giVubSVFF$Cpd8TW-0!zap3%JB_%6*oc*r`>@VMqNTBEb+MqRc4V&;xs zQuuB(HpemA%%loyyMNtlk-NIVFK|J9wP!AdRrvME_*XU(FS=!W zxdj}=A|t+}rx;fqp4cc%)v!SC7JG3!p*tq7f|&k5v5Y9TWZK-P>D-%?Mlnu&P@w8Y+K5!-S`&*iPXrz(E9$L75blq|Dj5CK>mnNEnZE7nyTYL*QaRR zOzu0;16WuTHzo;P^K8g*e=~N2_r2UjOWIrHv=Ja}-VQ-*lH<;09kHVRq^?7_?+s8^ zc6G!A+=2l1#&ONB23Kp*@)mY^QsC!)N#9KEv6LLD**8cmsDr3jASN~eSt9J}gCvs2 z#H=d&I~l#TPwpTQKMvaa!yLgaeBX&qELN|*1s*M4{0gZP*U}4N(H6olXQ(a zeJgd!c~Ui^(#T;Ui_rnRG4k=VGPM#h4WRM85DhTnEfmKZ7zXN#wiaOxu_}~rt#12D zwm#jW|L}I)5c+a&>0=cM4!6Q<+pfHDJnJq;axjTt@@&#t%tl$gc9Q~wPBp|ne{36d zpTDY;&&KHI&F7+kHjOQ8t4?sW&OP<$+sU>bIJ|p7bZ9C9i)v+dxn}@K%`xZJM-H#C>R~9s3T~+LhprhkvoKnqgWbilr)=u4~pX(n~?(syQt`OH& zxxM09Bg~B#AmD&z9sVG`c=zkmL9s>IL}+X45^-jh5p#PKmiH3vMdv82f79BlS{JqJ z*`v?52CgWCvB&scEOv51UL*d9f^#2=E+?=qcr?bka8s0OjD6jQ4(eKx`OeflIlb## zQw&n>M^)ucVhpAKA~LkaK7}!ubUdXP7^Zoh)g#b&d&ew|#?Kg(mj1(z#s?$wMiYZW zQGe_NFLzNpXwk9hede`0+ZGE}5EeWiZ>yhAN@9Gm0Qq_aEf$_lv2^0($aXlwYoch{ z6ua3uVl8s9GQSuNCeBjbW58T5zB2c)Xgm(&s_&!g4+q(FQzP(z)uS4($F?q* zlacH1hjq@en}}Tx=1(*@xh(CfhC-D;e~(W0Vo^<7-B6Uxip~_7>KL<$JIxH+ zbnC>N=IG{I;BdFo+uZi_tr1bL)gEI&Jr-vmzzycp<}mH8I5WWDm=+7%H{sR-VVwF6 zwXwTe|ER9*b~dW==n!;j0iLzH=-2|IDF-BYljz;))QpMQ1_ZFP)m?mHjZ<%BuE!aN z_T&T^s0Mh+?qUbZnyW#AZ3+Llf76b%&1OP^TPSQgnsgU0TVRu^oT564qAd-sx?7@7 zOWYxOg?e!WDQR)$uX{g$^f%rgDl z<`|ldx;Ki(t>G;+uy1Q@%Y*xhHLc+_bz5$&EI>+H5up!stwrQhI22`LMBz3V6vIXH zHi(SrVtpI*dSh`6#MG|8x<#oz^5UB8b*tEDrlA{piSKO;1=H_=0Pjatc@f&y;1ZZI zM(xx+F1fAa8#lR$LOg;-RjJ?PIL3(CZ4pE{Vnp08m@Rx_#A#fcQaMq9Z#IfF?FJo!6%kyULX^Z-`u9etR4ki~x zAfpd36}Csvy&t67dF$JWHTnEAPKN+*s?nxj28*!aM#r@NRvv>zNVLH*ySLTN!Kx>k zRBrxD&XKh^@v}=|tSVE$X30?HDRo?$VPaiJgHzVV!_**nxNKDBix;hNS066sMdPrc zr`Q{f?ie8=dx0DlcAG$EiOd~9){FJamCVSMAlF5X-XQPAz@CWh%p*jT?ud|rBShRH z&?+Ou{9Xv;_F};jNKF(Ax`50PIaV182Cp80m!Z)7IbBsN`Z*Q<)OL8yR<=6HaU7{~ zs^3}KeoC{owb3GsVxEv_EfCW_7_!(zVGib9t(K`fAbb`nwXN@g=$FbSiX@k6+oZ)> z1GJ*1g`@DkJxsgWV^O*9M*@a$FR6IaDoGP^N!_V8A!S=Mk0w#(@%PF!_ri}!nv(2N z?Te(&{^!c|R%6BUmFTl1#{GkqCbJ^hr4}G5DzEeREpIwb+cnH}>pgGI{N9!h=>!hP zat520!R9t@A1s_9;#xO^!C|p?KFCaA*BxYo82>9s(s{nV1J_wv#eVtWtJIB7 z<{b6XM!Laog1FxUi_{#^Wd=qzmT!I?+zyRg9`1T(_N23TmV}8wc~CP!RKvj2Y2;sa zeNy{>jF@CYdgR@NnO5*8TT#?Bl zp^w4W{%67RlSSvgFgihu?Pti5swR!E>nc`4uV=q0>eN=#XY%@sSB~&WD`tKkCH`E4 zYDz9?N*c`Cv{nB{mpD<{x1iD(n#fkpd#cahT?~w-hdb#b(nkFZTV)f=? z_*Uo!Akf@;3G)DhZ^X}<&Iwi1O;E1dPl`8f`nzu!&&GZD9MuOZREaH42NO=8{&p7} z-hlZ8Sz#|V7h2T~pL1aH2H5J_Pggt2I76LhO3ZH5he!D#eM_>KLix?aa4MmOI(M)*1Wd3&7G=pF? zJV}p?2Zz(a;uehuEDY^b103xU)?-mJ78}H9F*+7LjhU%xD7Ey_7vJn1`7D4t0tDFz zu^R$`sR*D7_gQKcU8`(+Ug(l1PwRQ}hs}h~ERiV=9&0H|$6;v2;MFa5W4HLA(NDG; z@r+O&kc}4Oit9sl0`fukZK#-eq414#vTdv0$%sQO5ejbQzPjXk= zDz4@$Mdta!dl2?ip7Yh7-tu8e7YCnXo%DE_2ph8iE348PXH44deeON%<3tYb-L3En zo)|EQyJZMC!i?xsyEa9f9fY%6bR!RFaqF`l0(#H+;`CtbUGgmu_XpuQe#ZssalqmN z*GKoLoWF|VM8y-DqzE01N0~q0_u+g|x$he<1`bZ7SL#=(uY5Phw z=ILSwm(QC~8#a9OJO3CoC%N^Da@5%3@61QP+DWKuW)Y27PEvZIMLRjxUP{5+B02c# zO^R=?a0g|zUp#{qrkB_+vr~f$+|K%R*{AH5Hme#`qy2bO+IcAEKJ^8k@V85F&W&P6 zjs+(KntJ@6lR<;!OMPXUoiAUi`zK>Vc~7XCZm1|Vj5a%~;bVEvpwd!xWJZd9e7h)0 zjD;LR|NC~F;`SGNc{Nrv1@AAO@@x0p=I+0$ez!fB&#i+;oh)dvcFvo1nJ72h;2ZGs zh|mTEMuZxT^_D$3TyDYfUV7c!W$HYr-Pj|P4nf6Vv_UghGhS8LZR_)p5mcv}#;8^3-+oxwqQq#u*}Rq@k5*C?t4( z;9-|$Q-K*B@rDQ|b{vSvH#5cw6W+`)&4QdQUr4Tmx&^J=x{ZKef` z=IVDDq!RUAb0ZxfCw#G#q^V|K_B7noVJyhMXuU{=nhp!P0$6`X84GB(paXad?D)cJNa*M=oYs3Ud1fJC-*1x$r zci!Y-~ut*dEhYrbHb6mbk zYi4?YqfNrq)`~*oaOBZSlQ@~L>~AM8cgwGoRhq3H%mwz>1fqIZIQ9DO=gyTc*>Bx;OW8BxmgUsX^bB&m_3>Q0B1dG25v3r9 z{Rrn<n40BRCmRG+_Rb+ygIUs?I97BQ9!{Yn?bg z9y=26b<-xmNf8j_;GLJ}($qY`ZBHw({#Y12Gq+q@13<>>=^sB~orH#=gZ4qr}>zLLXzl;%MC*v)OoWH4ijeURG<@Y~pUo69TF(uh~N5U;|cYvk09AON(m~5BuN0 zm{Y9cQp{Z|yi{egh=W96JxDk}$5s157w#{*|I(s;n^rbM6NfA5FFe9obpiAf{~NkR zoJ3hYEPOc45M`5dySg_nv|U69gOmNw76)t>{lT#bh9rBl;&w4tpqCBt;wkb?Wp=3H z{UPpKrHJEAxVz?75^cO9+@@o7xCa486dW?Y$;Fa64KWblq#eMwYHCb36f7z4=zjLJ zES|o02492bvIv7xSMynvoB=h; zQG>IlP0X8NaMQQkCHBlfGhgmjJM7e(A_w{h^yDce8cD66#hV#u#diqgLo+$}}LlB2ZVS)X&C zC^XAZAYI@-^px=VZg5C1hoB{fEp zxN=`h@A_)1+qWMBUQy##`GC4~|J*i(qX^}i1|jrF3J#DQ)e6yjwjl_vUOyYt5@wg< zvk~ovL+VJWe0W#6)#rBw>vhU)10vSp(6l**0=ax45D4wb1#N!_t{2tjAl!NmS0|58 zdD^_|(zk*YI!ZgW#JxTQ^udS4Y-n_=by!^*e{Ph*Q5yB*;3LBkk#?@3KzjEh>UdEP zg??J2EXQuzdD~&JU<3AS+KJRnF=sKZQaT6zaa{JZIr8Ku`Ej&zpqHcqef8hPo~0Px z@`zMBcm0SRZBDIUfp@r*q3lqdH8^8t|7 z8nt2^^eLUL^;gnE!2cEkT7zZxYV{>?qTc(M2w9E0QSFTPKlam8;mQXbf9Yx2;G}kF z$Ep9-9!=^0+Km6f?!P(9P5Qt`Ge;X5DMyjk12UTa*Q4HlFclCL57NI zsSNag8Q?$Wm860G7ZLtndq2tC^M7OY|J2<{o2K>pzdOPI2i^KlM%fSZQ=DG@Q9%A% zKc+m-%F(acvrD*c{l8lA{wIH={3Isn=uO(4+FYDuEdPHPtp9QbOL4CM(ffbZuK%{0 z${9l&<0*FQf2u*wS4kr!N#y?92u<1cq`j`%{(n3ZrdY*)T#1ve?*DND|Gx}E)B3LJ zEuHPhwf!^RubfNW_Tq>VZ$a}72=uV>uEhVkXsaP_PJYg&s>iLC)jyhnG#gTi(SPt1 zyZgE=FLxXCx}Sd1XRkAyQ*VP@&WTKWaRZ(Q*E~MTHmy*fYpcILw*PUP-2$*=vz#uW<@w$>*mVeP@A%6MJ5) zVcE%=#N^^nbF~{gbA(0WSB-PzeVkkW#kD3|IPvGj7nC)fh6K+Jst2DgXm+*3Pu`^; z?~B6w(d;jpz{(138+9#}ceX_!%LTP)bd!bOFHYQ6+QP|qLB#CG2Kj>!`*GJjPaK0L zbJ_&;2zS%>r>|{Xj^<8_H~W2YK#c>YglPxRj#8SyMc?@EV_(5dE*V$Wg**&&!E{jZP=wjjYzb3LD zgke)5;R*@YzTvui{V&8>BsPGIQ_{;{gb6%#{bZuSJeSc!p|o zUky};+9&Q5%5@;ha_9b1bb^4{69OD>HLv!~SI`{PO0RQ8jxBPu-$4cz!!?cgu&i0= zkjrjynhF?E2^@~d-zKitXPj`7#{xV;1*iUfk>N1H;j_qn*ig{eTB}6de1zYqcgGbU zO)T7dyHB2T;{s4jd0gvbHTJ$3ci2$CV=*Ln?vwL@dOlj|KuO22u3>ewsu4ea4@3eg zFb{x)6C}$W{8F^ku`Z(&3FTmF>H}eW1mV685)AiFGlo1|oPCe{Ngz&t4hh{Q`23V; zbOf%v0Xd%hoeq4e3p?h(hp{{$#SHmYOoV_w{X?+|3iYOkV!}yKkB8zWt^@rZs$R$x zGVQUcUyZsbNH4(1^5F15@>z?!kKT{y7pkaKUb3tCPHS$ENaO z@FQV91{(TE^uGvN=aGoObzlokQ?A$w`QJQs=j+~l9RiyAf@6n<^!6(U*U@U@x%T<}J?9mmXN^H?1Y0~hblawYB95*C3R;IL@AEZw)xpH+eH?Xq`q z`sMdnbUco>m3b^qpD{SP23Y-F#E8%{27mqB$D;E&blo;|9iKlNW=}kxYw-Prnr8R{ zof~`tnzew@-aFTN&>;W2_#Qc@-nYy@KFz-pIvGKrS zrB3DTi)3|ah(Dv2=gTvk2&x3 zb>%I1wU@bED6_{ngrE&P=KFoKYgoEb+cm!+1UJ4GB@m=~Ip1hO8<^#dI!yb0cJs)1uyIFkf^G58Vu1asjrE{2()JeiuFMGBTO0wV1EnQGE z!cX8ww#pEL^R=NRMx8eVnl?d<#?C+5+Vd6qLpqd`v!K==V$DVTB;+qbM7BgO7+U1W zY}u{aI`U(hNY?8bJ9#M?4Bl220R**UI>e1up?G}hhk$a57_m-|<->IL=_n$;># z#=j{T^8#bGe4jEiyg~R$k0$02~#RlD5_Go8KzX$evDr=b^_eLs*mI{ zI{sPz<(Qj%gsUyJfA<3|)0+0`mj270A|H>njN}ts7RA_0))Y(3a|i9p7kwku2C3W(pljVRCth$;2`r}{%T*}wW!wvvC_<@$nY#5e3Y z&8cthWK*=rbW`>=sqKr@70yYg#EYX?dl$cyO4h&0+Zd$RDR<&^8tBaN*S8F|4AOto z3n|ow1>uKB<-iC`z1Gb5Q-s{~ZTy)1%7nPNTVW5r*1{j3yQC8x?_-M`0*QQ($m8x< z#I1j=YpMi(*p3gU>y)kNu*$(z7rh z>8H8j$6cAziO$b37V1AVG_hHiUTs@qxkA?B`a_=9fX8{g;_c}21@>;)rnZ}!jtRR* z*eYBR-yh??;1T5LuP5#L4NG4vaGpi(qi75{y^RgNFl2Nxn{9N;y@yq+T?d{`=%T*p zs_Y4TY{Z&JXljIwIPwU~<9_iO1x?9c+Jl3!*Tp);$oFALG13HN@N}gHCy6 zE$Z@1udw+pBUM8J@a~gzrP_AKw2EWqKgE=-6s<-d^U{=}ClOgiWt$&~Sx->0(lQ6@ zM7m8s|K>`$uSO>*@3}j24+Ga`M>SfP+NYy4Tl2So&U)>KUt-WxgZUqrqZ*Ko1|(I0 zUyf;=sc2WbN}sgvaTY!D8T|H8?0kkBieNi+NOXw(_Punsa?c_{@aDTYxeiF+jub!avKpF2 zbbpSoVkQ5QX3kWKIX0Q zP*L`Up|!S^If-M3elHPWm+-kzn!m62&G=)XUwiPSaph3c>XumW1_DnZz^UeD#gYA7 zZw9SUtXGEC2hjlndcD2)_7WpKlf62ULW~OwuKqJP+@i?@4xM=aW!2kZ`D?YcaPr%W z&{qbhh$5QA=2e%1hV*woV38;f4#!sYg-5qcZeO{Rg;NWhtl*gQxU3z}K7g-E@ux9x zd>ebQ@s+{1-2h0iZB1;wdPE*8cGhy1I}9A&1Xx!+Ja5&N7yayiv^x3A*p?IG2`G~p zW$ZlMveuma<)Nj_JbMxG8eO3}L=1Y39pg|3G50m@QZ(=AVbfE{=<$O^>o>^HWG{+= zYGouFzu2YIf0UY*OpSr+zPGUIxVZe*a4I0o zMKwQUe)R9oe@4Gk`jJ2KuzD4t-i%#|wbQ>}SP#<|PVji}Wc5m1dxx?9Nql&RZ^ZDG z_)7Q*eYyA8Lktp)--9(s^m%WXg>TF_d@%S0>~U3NrD6ZN{hdc$xPg~``Su)C9Rr8U zK>grDO@gX)Y?qVo=;46L-|Ds)@BuF^mcU;KHA7$_D81Yzul33ex99rul|*PjuddH7 zj(-!ZT`Vm5r}U4Yf#|5#K*xG~b4R9R&8IAK9VgkH)FqX4k!ukFFKBaEiEz*(htS z0tp7>7yX2S;iqzJ&?GR6G;$NCsl78K>>+XSZu&x*CltpikfI%ZFht|*vK#p1@?xj{ z8V8n5cN5t^qS>n;!E>uBm-39O_(p89XumBYKcXx4iuNF;KOjd>4|>;d{-tiKB2_uI z{<)a<(NHiAJT3OlGGx=2$S&@GH2D6=u#tVxIEN_l$xwg-Z9W-7e+c|Y{OBzsJ|N7= zVNv#B2i&`z=TDpPVisPa^U0b^WcZADcgrR6d^Qvamb!m_6-si3c&ewB&POJuzczFo zf0x5^Ot>JkhZsj)1wF)sl~_joJ=FPR`0sVUQgLUURyt93rLP#xsy1KUW3l!t zy5ha~{VPAYluvy7iq|UB<`<#g5Fj!A)bpc@nTmGI;!w3Ry~P0V#xK)F+&AcvpUbi; zAWnWWl>6cE|MLX;cB+$cM+zqKJG^vMwCB{9Mtl&;Lp?6Bd?S+UgCEYke%^y(Q>eTH z+QU->>(Ea>?Raw8p>VMwZH!`WWt{u&&qeJsfi!e5F67OUGzQ9iPP!O zU&GI-2hDA_m00F)-QhkTk>S&sW|M1_YP_F1e*>A9p@7SAQ0vusMwB~^)(o=d76JeMqCK*Xi= zTvCLNY^ySf3ZMGKF>-a$k%z9RTiW@PgHce6W zKIvHPX(!H}AA|!-+^Zp)@vZ-9>Pd&?m#3K_%R4o5QITMTKfH16$kx^^c2U2-d3Joj zTC27~QDK`N-fjd5KJ=LIa@p|;J&*Gf*P28RjdS_)heI~c?hLhX#ugRpVvP>jVf|7F z@mX5yu|3X>nb-!;8fEd_Vq$uXj>}?YdZVB2jqr)X)e=!P4p(zTEUrWa{8+%`T~cjc zg%#hIo7b1gtW4FIVer>^L_Zs2LETsJtBo-Se#VO5%JO3nR^dI>EpEXJi@Kg^zo8+g zQOu%PXTONADB&SL%8|mCK(r@gm~TVzoe#4kz7@h@I@Gpdbo3D2Y(*76@@u7i2?ReA zO7!^-ar_r!7&c%gOj`1bLVC=05g8C!PfDo`SzK%JiW$f1H&=!{-crZk{04{n-<6{m z3>%d3L!x6Pb^g;G6k9UDEgQv?48~b0f4i6y9GMA#k)Wgp4RFGG)e%w5r2_zw)>#N@qs@z7P#E8?o?} z6B8(4TTZOVY;2{kS5{=PHR1=SsaA%Mem8-HR?7%zr<4;%n5lhX9Ji_tcF^u@ zE2`M3*)evg#ioLo&TH2S;yA9&+bgKgutEpC2of=%(Zcn>0N(to%;fDB{4PmK$vdHxF^33&% zJ^M(6*T*{f7Md9Q)5^Jgt6X&BQD|b%U1NXF9~SMbB2is zXL!LoOk{V5zf!-62@Vr;DH#?vtq=sGV2vFbXA^!b?&$n$fl$fW76x|;6Hg&%PW>2c zV3=@s!O)lt9o*0F|D)W~fe-q5KnJI0w0S8w_TY5gaO(c3q9s3p!)`;zZx0h4Q5GNM zdjcQp?u!L3Am2qih~6n&Ty(*3(8f(sXo?P3?Yz0y;oFc)ZSHb8!tWkl(0LH(Z0vq;u3nw^2 zG&G|R>PCn!uAto`!~|TMC!hphNC{7Ca_#WTzNT0t;4Ms7R;we#8c1RncMX-AA3>0> z)Rf))`P!Ks^SW6Cjgg`WRpyHng%VRO-;{buRkdEL=`U>2to>pAiaNqEJI1VbGG=a2 zS8ex)(D5&P?&ji5i2=p)C-rP8F!j^7ZYaukc6Joi9X{Lg@<&Ti?JQDOlI)mD__`U} z=)2SxvvF=|?pI%(ZanuaySgNs4~|jf2pd{ooQ6c;L`ZP?U$CTW-}t4jwv0<|=Y6c^ zg2N@~6(npSvFK^DO;u}eNW-!iz9_2>DgBQ6A{t{*cS4+D zXx|X?+z}_QQH+CR<(m%+y)V2tq8Nql#UD+?-*IDzb&t2(UF+DjZ{YAG3LH}d(IgTL z@`D6lM;bZw%K_0gB0nUwpKC8~Aok?@F`D(-CAI=Sr7(`v^2#&OWE@$}r;+Gf3+3gt z)&yLJFPBK148XXLZv93{KTuv;UzEL77SE7Ogql9hZYpGC`^Oc%pMdD-) z?8Ub>7B^7{Khtz;V{{Q)Lg228!qWrft~i6MAL4yIAg)=Xw{9xdd0-oEZmI@a!R5{G zz3;JTk>XOtaf(7`y?;~j4T3)kYRwW=a`TJa%|%>pgsr8?^rX0-+Zd*=(p&`QK`Z;V z5FPVE!LSx$ZXQrgR%S~#$?Hljg*(M-wiLzkLbc4-ii`Pq(F!e7_f%ZSi$z+>NQY^% z1p->BeV+Et^nD(shn%xCE*u^IKdlV@OJC6f@BN{4iT)Yf40c0%)4nG5S1hMUw)E+hT3i z`Q)m75IuBppK}{^i8`}x* zf{D6R3t$|x4=4z;-0_LY;$RnVFOE~Je0$Nc4XDy_zS24RkZ2K77}<5B#Z7O}4$)$M zu|zbcgNXCRHClPP5IRz}Rt^`E3#sMQ1O}QOzKMg);7F&V=`~4@}8!@xf$pcm&w(;GhQ^)>cVt z;jHZ}cA@N#_Q-a~(J9-hwcnh29a2)Y*U4<`s&=KPd1%!1@q6$9ONNt_EYM9P6o#*~ z7U~1L3Gc?RCA^z>=?^M>-m05e=Y#A1-NY*R{x58hy`#0tJg1vFBn|{s4$H2)&wYj# z(z4sPbQ4eglC*zXQJ5sFl%`6zB)^t&k}pk`*ILtXq$5dy2V~`}>yu+(I={rK!oABrW;h`HJUKE0$V* z#y%obAQ~dWUZa@R8wA2*cl)Rw+t|GIHS6r%@eoB$2-?bw7g3i7VjRozHTw$Nl9-ik zi~Ky6aG=z<2v*kPebtrK@#7$Sw~$P~K&jkNr#i?q$D30hO6pWEJ|(JHn-T*L_fsDk z@3kH@DwqA^S(bi(0ggLH=7loNU-s=ctD}XJxxa`B!l?G|uWs?y74z8ba{5xD7rG+B z$$_#p;_|jC_3R#RShVQw+Fu+;Svg`b!6<$6cU}9pVoAn^^hoN1%o18kK{+U7c5-v% z_?Kf-&P_6OWWFpf^=Z?nxnr!l$Ne_q`@wPX-T8W<=8v(lBBTU%?TaD7dCCATc`;I1FzS<57_1&2JhMp5id)FW*;K1mqq14L)5)a#@SW(FEqHf=dWRu zFML=<4iUj+u!BgdKwc*0nkNiZAC~26d&@fGM;qMb^1~NcU8Ne4p)8Kr?m>*3f8&DR ze9!5s_klNg1BU&|8&SF}%&as_G=y1_%GxyR&BxlPH>rc2TOU7~x3LS#^N)kz_Vu5) zM}2~+#{4TSygU@Estxx9RfH)q)+}7-^=54sLdD=RsnnEHDkr>3h>SUtL?f2qB^$zvKB6j z0Yt^Ji;BHqqbxBt?7`l9!x}79CANs&SiyR%SkS1bh>BfQ)EJG?r-=o{-mt_LHCRx; z-?=lFjkv4teee7HBR9*OnK^Ujv^jHT262@g_-*Z}7Rj3GQ!Snp&{zpEr^blpk0!>% zq)mRTSo*(<5*Og!`>W&K_SJ95sXG$)@w4f1W5uhIVU8GgTHONvi6hs?sHmyq&VE0X z3w9Bjf2(+CTeDVnT*MmHhf^Yu-{F?S2N$v=e+vRknk!b5s~(Q5IJDdIDH0Dv9>l9+ zM(=qd(zNc{^YmxCo>l_@$Nvy!T}*WXl!}IF^X>R$*s)RovH3MK0L-+p3#|*V$e~y$ zx{+fO#oc&nf#`H^^#ct8!}eA{o2&uAv8hzQ34(Ap0Ki-J)oLz!uRl3YCjl@mxR!Wq zGjPro+Ry~is|Nt&=vA&uZyOFtJedeUISs4m8UV~zFE~aFh+7h2^M70;Z@Fl`a#L6{ zE7uou>y4(70-Zv6gr#~Oum zU{#|2klr=L*sR2hrHOb$)tZ6C2st+cEli6R3Hc8`+@)E@`Z6ro%*2=?Ni7aiGgfrh zQOQRkO%!`_Q2XKVP!0@}&ML>}WChfzWSMt0hQh zd;M{p!T`V)az;yV=eNt~U`wS!sUFKji0l}?oOe*ie6@3_N>4ZjiFWZstgdJ*Ikm$0 zykmunlay-iwpLlg7yn3zRw&`{&R30t;&PO1I>%BH9829$S1;S3S*>7?Wq813^|p6a z7<`t~Ra7(CCY&VL398r{{^vdfVe$fjMrSS*^4b1&5fG4Jei>@XTIp%)wdu<#0U?;I zr7bO|o2{{ai(f9}FZ0cV%E9m5YoaB|dfS#$)i%J%_T-h~?yMPo7uOrf20{ADAIFLY zx5Ef*uleLE++>auj!I1GuU(_vpOv*xL9QG1t>fr1+A{YBB+GJ(Q9n-36?fn}3*=yw zu+eoN7C18{BqP~U(qJ{!L~zPvyRLMDkh;T6jdy$E+QTAgo@n+}S2j;m(u8JoP<%{3 z{3O_W-^r6}<0t%j$$|xTk1zAut@K*@y94Z;+)dV|#q<@Pre|^PKCPf3AuU!rw+2nD zO<2Emw6P<`@jW120ol}V(~2!YTX}UL#Z_{I7O*RZ%2NT%K46}O>${qMzP25}Qh}~s zN0kw;(Gbwf=Jhg;qeWk`2XM6FY*^_fzv?T-Q?t%!_8)L#J0l1$qpC*RQPp!47Nodw z685!tVQlgRpE!MQZt1n^2vv(L_EJ8xFChi>0p%S8^<>+Vl2KK4-@sena03rIpGd!wJ~i6{Q#HTvw>{N3?7JM8a{S&Hr9~)AwK2_TSXn^&cBo zJvY)pZao+fysH1Ai`Xr&^gj|MEHlN=+wETX`~rI(l3(O%*=RkE%^s^dC*Jyr z8)<)U&_@dZsZ`F2gPF!{60#Ri#&&glzBLy32pgbmxHZka5=1q4F0fmJg4vS;uR)B1h>76HIf9Tx$MJY_ST9ILK|C+BpN>i(GW?Ya(n3X z5Gc_ad&E$BJm~bQ!juNCyrGL3L4cwaO4yUX+3@MN2G7BREG2#SP|;wRn31yXk>o)I zZ&&fYVS$hil;(te#*rGnk5&XL{e3@OIBQET+ld0ts745MgM5W1r?1W~nLKg9?g1xvc_2<0aS+CI!3&bl*2?ZhY?Wd>1 zu#|ie3KizQpIpKq0`iaWR4WdL3zACR!|-`df#FI)+zlX$^#cylMg|8NS`9@TSHlz^ z>RJ(*YnC>S_nv-6+86JyNJy zgg_=S9Iotlhv1<=TvPxq7BvXVs*BPg|Ie`;)kq7vtks5 z?bO$;y{$b@EM;Nm5i(&=8ZG1?y-XfJpZ%PD^Zgp0bSc4p4o+w>_iJPsdX&bD0HPC) ziiq#C_4iu4v|YtcF^3zltd*3krW2H_Wbe(1?EjbwSHt38gnJ-qtD#}{idGJ@%!SrM z7MkXI#{@fFdpfVsjlP|3tDUL}xIIc(Haoih;j6D60mTb7s=hcLbc||8TILb=0fa>& zmh|SZKq0BkoP>KJ;hdQuk79_4GNtYpwqwqrLl+CQ$}3bFKYFGO1xZ+6eUB+mKQ02J zVP{<)DFt&iQCq@I`+NsOT>zj*{uri4Wn#mWk$2t4)LEjH5`S+2l7Ko*%Xz57wCppDzRc0U* zh-iH(EF7eT{;-9s={v{=$1R@v6`JNzxH}XIqC4u#x7sjew;DCSjHq$%zl2b?ytwSQ z=!9JyAd?}fEP89)k0iyMqZBW5z-b|-Cl_W6Svl6}9Zrp4zWHBAlyE*!*=pwlN{{Ju z5G685i`DAjQIMQ3-DA&@QGQ&IejlZH`o2P&95l3j-papU-@q!vf^@|a&AUnr@oG>a z_i6GTt+Y2s0u@$|z3*i-+FmLuPYGVPs<(yzh!PhrDLAtFt)1h#pu|s{HNb7*`$xlV zSVoRNC_T-`0mums8{)-zn_|oK*L_@*((~JOIU`n6k%5DQdWQ_vUCC^HBxq3Kv-$1% zoS_vzD9v&WW(f8A+D^kH@TVs!w35GSiRJ3C;4X_=o-E5@JGK;Hgj-3fDmcO zg^V_GkO86QAiME>EBwD8^{3OMmDIjKv8ZhP^ideE%EMLu$19`UOVL!C zMNl@UC}lN%)=j12y5 z0e6L_Pr!2SRa+sngg1^pP68=c+^QH9))*dYr zuQ@#bhz?Iwygzxa2%L(%bT&`+X+jcz%G~zLA7Qh!^Zjsx3a63hB(RLtEKxP|-=h-s znuN7Xd9=a7xjL^xEAFv>^rI>V>du)+y7%=10H-Y0n6~P&m&fZA0I)@a2(?2A({Q|7 zi~ZHF9b^T^3G>jvSLp2|1YQCGz@d@u7cPY?ZD2P-t%_U0aj|(CRh^9BLnI(LEz{}u z%)gRe&lh_aXuPU!dK!(Hj4j}KfZ)Z^+J28_H@NtwGuq%Yd2k3mhdK7G+~~$+WKQq7 zykH7AVIZ2}7|ggoj+%1ksLZ24H#~};FAEdj?Ml$#$==@VajUz-w!%XvY-z{72~i6wYmsZ@h|E#`#70Ha{YqTj&~1#@B4+ z@Dp|_m@O+`6BD|?Wo%vV?g|kCCeCnYJM4?=lG1C5Q&oY7 zJbrGxu<%PDidT;qQCu-L6)uB1rEau*s^X4w)y~r)LN#yEC|I7W-=z7n$3jtuT2+zh~Mh;a%&Ub!>htzRk3(Pfiobs`vJf) zfYH-qXST^KhQOKzAo?`gM*Cx=v@Q{?9+QIHjORbyu0_oSIhGm~xMtondVvp`3yx%YTq#}_BzTh0Ri86VQXS}a;zXRI2K8$F=i1;paa?5O{M1ZAbF=+;chIqBpzM>weYdi#q(txOxXW7w(a|1f5 z5Gv~FGD&Q5r8JI*P@f66?XmMZkYXxrKwJ@={g{M#{hzx8j!d@d6Zv*Cqt+N z)YXRw6hcYKu=xG6PS4}uErf&1rc$Lv;0fXWn&Qv`>#I!tkI!eU=~Y`jFo_ecI-L|n zlWhlBb8}M$iu|%#GoKGRv@3bJ+82&WcrhD}Q}X9l7obz=9r`t9mrcE$!y7?ul?e;$ zOSfnKDFmO zW=JQvTwoWzlSt@fkWue*7E(nYRXM{V<`r;HvAOSJ#W}CkHwW+_7zf zs*BVTku2$@Z{k4Lku)i9g>lQ_trO{!4joXHRiX4#(<$&L=#q?`qkd8**_p;0M8oSQ z4K3igWq2E@8Xz^(4P;seIi3dyj!;i-duc?&{FeYfH1EE1V_x`nm-wL z#cw_00f=F%Z>t?`Ewbpo=^r04w``ArD1qW&HSE}$Vw&N`l5MEVi|o>!;+9VP-2)4C zq`0zbs-8k;)&bGe^bjB8HNfQs+|sC0d9Li-%qPG(9}YgjQ!2I|seH(NR{`$!s%ED= z4M8f69sa^%UkqOkS40dlhq+8?s5xHA@BL~?`HshK*0bDC+A%Gnjh@MO~UxM4kf&9T=gorYR+RthFI#R)AV>)>`Vag694YfB0U_PbI$@| za8XtI<)MRUT%SJ=U5bYmq&(!b0l)jzpmH0q{;1$22Jh3g?zl3Q%EC+0$RRIC@$Kd; zX8zdICu977IgU%$)OiI(`^l1sB`F16BY*n~C9Fw-v%1Yu*c=Q1SD;n3$uYkSzlLy} zF|BIK-&m9|t&3EO{mp)pJzNZ~Sq;oG*O@#wA`mc!dTxZX|Agk^qc`WJWB98crI{O* z{!zBk4$UV>5t-mItjmt7rz``CLJ7~FQ`F8v6Dy1uY$=&5OQQO(DS3G2-#D(Q5y8g1 zf-%RYq7r9r0HOi+%?3QnWGHXss!R z^}nH`wT@`aX5}06)Ph1O*tKbNt3)YhriIE1Ub+;BGBfql_eq`Ze>i&rZ=nO0{~``m z=cnRZl$?}1Q5o->xZiw^wG++D7f42p~p)ixl!x^)1Q}qm{svJox$-rEEdo-5>+a&EvFr8<_l}1Sq)e zZ9fQG&Tut^e<$@pN&o21sAx@>zpTI%Fy3X59*PT zSw$z4P!GAyMZ3e!WWTbFJHSiEh_;B9aIc0;a^~(#QNV|F_KGIwn|Lg%2~pQ1xve|Z z-U&o{yHo6Te8b)8=?2!b|Ph!XD8l&*_^HJvUOv#`$JT5H*|l&64Yxqg1)Uv zh&jC2$7$n)Q@xXy@3A;r2m|jf63Mrz1gk!m_Jo4=SxaeBfW>g zm-hDC`hKEWjUKBTb5i1Q&$h2lgvNY7WfkqIb@x&Np9lB;+%ikI#sd=!tH%@e#ol_cYV(qh=V16=)!ya`m$n0IAA&TKeF%lAQA&H#Yjs&O^!Q}>vc z!0QO4<4RoS^X1(e5iJy@z5EK);A8eL{l!2Mwq)bVO*sh(6&#p+nV8ck$Qk zK%paseist_4}ePIFFP}iDl_be<{6A+cNzx_FV=7bJ#F%H$VbW{ zv^g@=!AKwc9CF)71`X{uFro^1L^u?iQYpe=XQc?ko=FCm59-i#!#~ioR|bzl%R=77 z^=VXWs6Cxunaj!j=pB2n4W*k*$w%d08(JGAa8Lt*h>$UfPb$5<(O&c=i+48Z1~rO@UvNMg=QMQb@b06z*2KK(&0=)^Qfh$!v<{^ zzU(&TuoLWuECBWI*aTGBs|w25h7HA+XR{@~><#~lFW0|`FSFq*d|5e_L_K!hI^)ar YM&Qdn0O8B+iinCRASf`wK!S)!B0+*E*w7FH1V|wXRV5J{L@8k- z+un{9L9t=Mii#2sBK9u!cI?>O{XT20nMAL@^Ev1LzxQ+JL-MTWUF}`(dRN^$!^ewy zUi|Ap%X{~@pnK!#>$`ls=+!YNuiP!|&_|vcnD^zgncFW;-)ooe*TjeRTJ&4Gr_bXJ zT`Z)oqJ-9trAdW>rnBKzwV|^V-2L2it-dfSKUwU>k4-xC=M|Yz;m| zeqZvE$u(0eDyFZEd0u;X2zCI^cDziy7QqQlaD+?H7gR-iI(a%M1-?u3yj{WdRG|3z z6_bqCy{S!#+=N^TT!Qv{f(yXTU61#UDn0K~5-a_rikg!0*`BuyqI7dJf`!ozwfoU1C7)4IK1F)ng_RWD9jmGM>)9L@yQF{go2?-+-_QJ&OK!1-6VFTa$oEvSy`?CL9~LJX1bWgtN1x>xqF75x)b z#gBr%`s_h<7@)`SBZBBN03@G_vWnR1k~Q)(t11ac~U^sJqVrHBL4h5d&Q!>e`DlMxjn(TQ~W>ieXzTWCXZMv&K;|}GMXOvEv zS~I<5-fYF+)X&QMyYeO%RoBc!UR5%yx^(h9uTOszA4Ua{0oEi1pvKYHL`Z@CT*h7r z+rqA(O!vCOXTS`2Y4Ay`OQ+KY+V+-CDX*x)e@m(>t4hF~gRU(*)_#1L=j~3~7eF<# z6=kcgSyiRIrr|D?v&!cc(`6l;{3om@D}4^C&UYME54J{`2FlxBLaxR??Xa}Em$A?F zIX2xRR-RthBiB~AAb7N8mlM|`mTlKK{AGyc*E)Xb;noB*N@kbKmdoKvf2PCpK~+Dg zq@=P}X|;E;%XePS;ILME`3GQDX|-oi)T(3co<~|%Qay84N!2{Bcxus%=@j}Za9q%04}I|{&tvS=tpah~x&@#nl5r!fjsEI!)g@)KOR8`& zS!Cio&l{9)6*&}?_78)qAa=Cn6RV1fOX!R<;F50yrC|G!HomN+YDx)SnKU-jsIhjW zz{Xe4D5{=H!|q10y~(!_#rO7#y}H9j*)g>bXf$Swse*3DSR4x1)HoS;(o}dnC=Z-9 zt8^0Od9!9%TsqoX=D0%Zk$K?Gq&qEmHQuH6sxdZpDVT}$TvDrw^nH z#m{qCe7y7L>RxlE7S(vA$fZT$I6I;b2gUn3oH*WUI|f7^%<((bjwg@mD4APQT!iLR zPPCey2-2v!QJ^%gEUKB>i;+6i@dUUN@?M}Uvo9#!SPE)N%Sybm(z4RxUX#m*Q@$)T zsbuD?q8Xl7NSl~J>v|w()~c;L0D%%rnc#WM$aP17>WkgLcHqu7Vcn*&wkr-ouIcJK zct>y&S~Hc_?aLg~6^wy$wixo=!M9wx!>C8;&w_Uc2NU1jJJzdvj2>f3ty>SO#aA*1 zviVj0Rymoyd8EST zbvIG{Uf}p?)`O}_sw-y9F7f(Kw`z9>WzSBaT$!;}F^3YSV>@a0F!|(>!$4^{zs#0f z46614$jMh*S3R|~4FC7K&`wo&5*6i0=5VDskF<%5Ng4@>jr>QWVe~N zA<5-8xtloI$%m9oDmr*psdr&|i+r_Hqm zl{ubNpm=J<^wLR1Rg)gFDiEw{3?s;Guprfj}y^SlGk zu%2HtwWM4sv~_$>5S!H(8~k?RmDeS;Winif2?~PtThGSLu6$s%`Tki+h3cvpI|HIR5Q? zJ1&o_x8wU}xU|S8AKg}4w-JE`bT-%yd<)b7ZU9x`ofp~y=EGItBv1}I%3;Y;o36lN zdr%EZrcc0?{=;R~zDF##%%b*%{?DPzlF^>WXZK zcZF7=8$o$k0AjkjnuyIe9@K~!3@W~QR%Ow|qUw^Nmsn4{0#tmK!|Y3~ye~Ld5$_XF zfzP{$E?`^u!DLY1=3MSFfU2Ofs-n2pjEX6{1(7z1+UKvZc6#$-tNyc~Qf&ZbmqV|% z>Tkcw*77=BwRXM6)>2c|ZvbZX_H^mupz;m9*4lZ8%eNKGL_QBx`Nf<{OH1g%56CRL zB@cLiJy#{qQFZj1X6~y&H8*(>OrFd3!hY)5DOD9SdsR=GKI8^l;lU2Ofa>t>H(Gfn zytO=N69OrahCrGn?U|f2lZLZO6z2P%Rz>s^Y^z zm7fKwUyKFzV4eErW}9#m1u5a|qFH8cO$*-1>{$E7I_po#nU&eKW?p59_d0ToxJh@| zjwmiFE16L`zvLO@s^&pZ)!ztq03W^6mR?;^Ue3zG#+;o>RZaC1cUko>yW4i$FQDvv zp~b`M&UAt&*V}~UjvoQ4-KRQU==h{VdlgsAsHo~6+}^f(?c)1vDx_v>0TxwN70t`L z-&&!Zcv%B^3108r6etOh?|{m;yFY=*nT)x5nQw7u9ClvX!_n5VXG z4-%+*lNpjH$mCS`<88L!Lmsy6{uthd^l6V+`~8Sq6+eeu6+HpUzNPF9O6JyhlODDH z=x?<0^~CQ&{0(3m`PU>CAofz#mm!cMD?qil3fvnk0af65hr_`3@NrMrZEPN>78RFG znvRFyuy$rTm~^{BM?GoNw}bBi-{&cdE5Y4s5%>f;fxq2qTktWcj4yzy=piSsYP1<| za=iD`))LF$%0JJ^CxDs?k8pSpxEs7Zs44TiwRR173p95--ZQqM%?MQSZQ$kcdmoXX+PVMqx?RX$1G~UCIJ^Q>0cU}GfD@g3xZ?xC4CGg0>dxS~t_!MM7i|5&_Wi66 zt&^Ygi5)$)+X?JKhF?Ci5mPEE%^_7K*O;t8Zj9sRTS4$m6n(EL#_(<1ZC=&;}h_1 zIoAVe5mVu6P!--7iYOr2LF*VKG63hob%USzvw7^s#W*v3|9 z4vy1jdn@2F{TrYU)`2R2Vy3lFE!#j7@hSpoI;(0%uW1#f-s*N% z(Se{W6oTrqC)?W!s!OMomQSwm&PT2m4+Eu`t~g34ZysC4Q2)%!p0igiYpZOIZ#L{5`tVUO-6WdEY&) zB{D!cWuGn)w>N0LKTfpUPU$6sK7hkitwRBlg)pRa$z1NV-i8ndC!{Jq+ zEL*gX&F?NrYwM0fAd^;?%$QWdt!iaa6~U5uk$tV&Gnk*H?m6A8ML2$!)|8f1PpT-J zRZ}{n`Z1U8Zctq@n{>i5P?jnLwZ`OAk9wfIqLPmHyrPK{t4d}wV-D+X6J~)@Bze`C zJR1)`z;syhlrJZ}*irWT1?b*H4;)AVv>yWCHQ zxBfrctJK!*`~3g6y^7fdWBtwp?bH!*_;FUm%-&la-UDiqy~^PtP<~tmcJL16;Uxh% z>XD$jy=suf!{D-4A5a6M-(btT2Mt|19-fn9QzdU~k~cCHRh-5649vCO*9%ms-9VMn zYKWC5f={~S*2ad~9+M-g=c+jyRT6GQE)Q4-DtAp$RjLxQ*v(@ za``&fQ*+80)R&P@wVisTb=i@i2Hmfq(pA~XqSi}3_Rz&r8B_lJ;kJ?H$x_k8(lHb) zMf!p&U>{H}c|8@#I1hj-FsWesJlmj&MO7uectmCp&7VQ8^gOOBDJq-mJ!Uw^l%jmw zl5wCk9tFx0$(By+XYFaucC_@jqiy=Uk+$N?Ko!h$pQ;MtlNOvd+OCcH;NHX^1ZpYE z0JY=%@)(doU-ujV?P=G8njS87SO;oJnhMH)3PJfzHmG@TKZk8V&40g)viK1wcY5C8 z!wzo+H5>fvcx%BE;k8{6+&#`}bOor24ktp}58cM}8qm)>!0|Sq;%A*`65Qp1!TdR;HN{iegTSRgeyJ_+5Kzro>g4B7v-Ud^l&4Mvwd=ZV zx~94)C_)4}o3f@(l#!%5y=P?$KGc;HyD- zmYz0;RI#7fD*t2X` z@$|XYWVrJLtLgZK*5o&W8XG(SnO=%B24~v>vqAOcd{CA;4V05TcaC+^{^we(ci0`2 zBIbleImeP-UHU6sEk!>ra2mgeKouspg^$7Ie78HSSY$JDiknkCgUdR&(oY7}$CEhZ zQNCAP+RN-quO^>nmpV|+JqeT|<3V-RoT;V7Q+rjGjrT9G5vwlOY z4Xm6#rI)salS|7d_2MdE&*1C*yPWV-z0EYzm3BC&5kA1-K@JaCZfoBiRPEDMO2uUF zamPnC*j9BVy&Pq?ptO6J9#5{YZ2F86JuvVpd+`W)R*5$&ux+jEHG{RO23>CN-aXPY zc(r?%)T6TGbB)_qSpz4ZYxIOm!{h_|ZgAOccZY32X)vjD@?_aDxop3FvF*^mZn2VE zEw`-K{%`Af@)>6QGTZdyhuRL^=W;tc)Poue=~vha)`9yzLccsn$FVNR4@+tvEYys87vq7!QD=1J4+$>OIsWlbL_4dEkDt4>G>p)o` z0OdL7{ln&;3)i%kTtQPz$O#O6kU-x@fK}>P@-9R*<~V zUQK$9g=22CE&UMG-O$sZD!dWY5V+4_+0E7^uOy!u*mJd2^n9>Z8h0h2KHhYT9pxS2 zYS9C5;kGrlfXmieOYDBDwan$93OozB2J>W43S4%Zoox;#zbrTm`99!dlp}ARc)PX4 z=-WBpOVb1b4VraiP=Raiuw8P%oz|3Rg6g6SP!(K5I(5;)yKGCeXDQ{qtBaA#Imh2^ z8*~k*^8WzkGmBlm+tyo4&%T9OLONL;;lhhFvhi+8sjb_JKm{f50#1ZWp_(Edo3d;Ehf`z~sBWDH zYWB5n33%Q+SWF5eU+p@Hc=b*mC`AqiH5YyMuI+`FKs99Hd+JrX`w;}HF#f)+;0m~A z&jq0D7IOt1^?|MUUQk=6j>zTs-bZ!>ee$98x91$L1huJL2WqnW=2fe}Hy_)2W|ddY zm{rT=SJfvr;|daJC?;>fk_X`AZAtPKg;PGY7VGpc+maTb8t}zumcIguKMHC9ta0*7 zL0N?7#Z$O>t=0o+Gu6(*yV$>`OXk0n zcwKh3|HV3QjpLi(vhcBR?TWAamFZp@uKLv`Tms7L=ly0)Tsftep3PQw``wNice3(2 zz-5^@sL}B=>4l`{ZO<&P2NfI$PXnds_8;t?ARYm2KO6FGb z48csg-rP1ezAvcdpsb=old-pseDeB|iRETLdLvwlUTKi=Kbne^p+o6Pj|2uO1=;@prf@&FVG)E zf_}T%1eLvL3om%Cg{xxA{`Xz`s!p~Ay9bXR+Of8)tM1g!ww;fHaz%d+Tk4&TKLM8= z?gG`^>p*qjMWED7=1aX4InZ95CEt6xkaV(JfvaZ_NIA822iZVfXHW(1E&t5n z>kiYp+6rsz1)H}5u7Xbh`c0Zc0Z7mQCt!diqra zDo7bicmz^hHP75bC$D?H!)0ZellF_6L7xZ8BD?HwD?G_zWzqBs6*!Q1H7NO)2zt+k zY15lHYw~0*Wd{S+hepWYG<+6>q&vPaNkK$l3jAaB4i(f4}?mk#J1Y#(q_yw`yD zK09hy?W1=tN<5n$@Avc0501O&_6c{6A9_>l&Nd@P?|tXgsjs93Lu%sDk0L>Oc@77` zyz*TC(WvK*LXT*$?d7br7M?dUC@9a4okS?#gsvwv!h}92G~9#^P29 zGQ+G$T999v6MHn~d830Zm4nh+dfwQepfWpFLTGeQFteG+Z)H*;B4AUkB}Db4|H(uu zlov-oBMS+QQ7HOJJV>w3iRSo0eOXSd3^N`cWK?HI??F(V6I+knPY9M&XUBG-Cyq0r zNrVbb=x##Cm{1zMX+y^nDloFE2_3Ca^s}9V`dK-#r4b%lhLI1_4mGxWCL>6nofFwL z$eW!T{l0Zj4>>N_47q2QAbn0w^oL!7{5d(16N1Haa-&zY2^x|8t4)wTHz#snkT*9s zGBH>@H#d4+X3#h{C-wqkt6#8WPPQM%NBd#Ov>@~9ERnGyVd3nEVPi}eHOx-U_^px> zo3bYEpNk}iG%dny$3(Is?SsbCb0Td6|BPIJILrDGMC}}8o{@+y-ZiK{BPaUMu0bR4 z>#jlinK_Z1An(jve>rPQUlOJVndtT?Ofz4rVB6e8^x@rt^aVN5FLw*_7vw|^4HhrR z^=nv_l>+ZMJ>joQvBKxG;z<$~wvJ}SnUaSl6UL^&a*Wk(QYa<=5Qc-+X8S!^|3^w5 zHmr(drNM><1#`1w_Y)doQv6IvRmXzD+JrxV0e%Q9Di)aDwK^*ea$Jyc@t{cWAboj` zfAYb0T5b_!E=ojK9~{&p>_BH08f^+M&x%71gS3##M%VQT@*8sEyD<8sZy51gG668z zAKf>oX9`-@H%MQRAmQH+Q*IpMj6^)z&-Q?uh-%`IA;IF6x&B<_%99opo}Gx@sZ>G6 z8QHO~3FU?v<&i3%XyQyukD`?auK7~_}z zf5)Yy*D4eK5C()+I83=L;V*@$lon;_!Oo_V|Lhq%xYq-%z!u(Ml?!oBkn$!R7^AMTbKqC^<&7s1jSmK$*h8) zGYI)^gQ*k@f{{OSDXC#)BA!Lhm{sZP!CIAS;_(ZR$k5?6YplfIfBy7mWiZGgL9X$3kY*3I< zne7ia%36>TnD>8zO|a#`iZSij6uSpTHq}MkZ!o$^!*x-@ADm}xYNzWtFr_quDcYD9 z)Zdij|BjGZpl&C#H8_erj+7(G>5166u)~6aAlu(eNF7HL7AIn@k2Z3)r6UMw6k7K` z52jSiR5<6eFd4xtXa1gS6V$!Lu||%ADIaPw;4XlvRpY#!TZ!(cLT zQ=iX9a=a;1P5iRF+u&hoakF)g2r_@e%E8|?vrj|5}pN7+Dyaq-m)}3E;<1vIe%6$cLpy zU3{U7b1I!~5}RRYJY8$Spm59ekwL8p$t+D3%|;^sNX-@g8YE3czYjC5O-{10KBs8O zDEP;qGyb;DA@#a zrkWI^CM83k5SlDmFC^!$z1|8rEi|L5^2D{IY2n3$Z;Cy*n*P-!%0x8*`%HOb;42=J<&j))`r~<|JZsVH{>^Im{3mA7sqQ)+YF|T))1|)@dfM=yPR3 z{^L3R0p)h!u^KStm%!vRW@(FE20Pw#;@5<%rN;$}pUCxBR9Lq*GhB3gMUcKJCo(w5 z+m!3Cth7;V)*nnn->nSNf5?e;of+i+Fkr<(KgfGBH+t2~p#I66==PaGiEjB}KPagi}EJHCdH>Pe{@{|bpbCnfQF&rL2*g*RlyAw!9# z8}Bj1s-Q#3cSNaKW_j}up67fn$h{T%Kc8X&I z_9aX@v-&YtcdvE5X*LB5V5*Nz9m~KYE{^$_elMxBDH)V(i?4zWGNo+0G%M~1#}=YG zo|T+yRY3_%ePs8EH^bCFzS;MF|f-CoS~zd@$KYF4SpuwXN?)n{8X zm{XU(1E%HMxK-rHVDU?wUC!~m-bO}8_}4iW3mYzA{(uc5j$_K%iAaZF@yogX+2`7M ztgSKsM%WOugYSG^N+af><6z^-$*9|!h_8idu4!7x-b6Ce?9i4jva7HuFZ$e~p#D|P zPK%k7$$+BlpF-G#U`gGeIDylwm@b}E&)a0lMDbT)>?3R)VcjlKKYl@~wOG0Wc6>6m z{{&1eGZpy$h4vIMf~ zT{cPVPi8t62@6kRhvmPU>qjoKUTOzgF3b%rS{}fh&6g#jKV1~0znA0BUuC<6_S7a~ zkHFCEob2fDtAfV&a_~z3{an8>OqH`N5$PT*em^%dE!fQOiJY2tN(4j7;<5Y)mltlP zhW3PJHeG%0xHxG1Ajd!U65BHjVb<0gVP+_XnFE;}#k4Vxj0`q^nCpj^+E{am;Qt7d zM=>#MNyJB7X3I&fdj1L|>O00DR|s#x4uhqRO7UYZcR5mrqf3xz%4kaSIucDVDT&|i z3idI!RK`&m%uYaB-LHrBCXN++Q6l~!Y(O&aiaY%HmB|UV;npmX{zS5g`!Eq-4C|+) z$-R~TAd=xotnw|cvYnW^hlv~>bFHOoCWLMDMvdsNazj`wD|4M+FyIEdu|$Tz}DO>s8dm zmH4}>gZl4sV*B2Lu4ek0LFfny+F4gMk=ug2?{oc6k;!LFXZU-qNgB6d-N9z`t*4$g zsg7Y{+5AJUfATt>N05o#%+`e8;&!|LWIn?i`omNpXM3iZQkZ(nOkvTRZx7Oc%JH`& zRD;Zwk>BACJBOL!7@rN(aB~BYqXzD+ttztirqyr5xw!k-P3R`yDD z8!Q)QVL_Jesx#S_)Ctz?rxh81?Zo2TC99lxx6F`3oF>#tq&T1 z&GBDEIM^i7;@Lsd9VTgZy;6YGWeFF=>xVEyQnjw=R6SBM@jP#(y>Zo zavt=co;gKJ9}M$bL~^3{KN!}7zW?8Ucs+}Y4B6PMh?<2O$;c{%KQy% zNU-F-Y`^s3)W~NIy6xdGKNjJEShzSA$&DTN2yPM<#3I?zRgZ+}Evd2Vqjo5;y(axx zkB0R~WA`B)ZJOUCxeK%|j>p_3l$C3usCbs~VgHB8voP@4~9TsHV zGARByCq~5P$QG@9JZy|p#9fbv={`mLM64#Kc(^SR;STT#yUTHO=S|k$j*f<^)yx2R z=xwm$4P%!cVc(P<$&JoEy&$xFstNQG?l3tif4es*dvuN2d5c zQ~Ln2P5Tpj2&OGgB-~cUTE2zp!Axf337u?}nwyB-m6ErA#w|JK=oW{mA=HyWZWs@1 z*~#r7G+uJEJsG)`MubZ?apy`%?ut?HukT?z<|X~GXaCe_SgO&n_r*-A!?vYNN;~Rd z8bZt~3lgzsV7a;(kL(uu?dY+(=feDUINez&vP5(x%|;O~Idm%edYiw8HlfD}6`9a}+ldV`I!AINQ;gsf1SKXk>K*HV-CX}tA^)0KEA7kv`ugZ`JFxG@!G_QEkgWu)lR#IGjQ$2uUV)_=f;8pg~W`vAt& zLaHvG**QECHoVR7fn|}331~|q+VEMJ-xa^wsx-RO@b~za8$vo_#umWF2TSH;$6q2e zl6{>!7wlX^w z|3+!0>}`ao1T}X0*6s)G;&xPuX@xu+){hcx+&zC{pTP#0IL;5Tt~<07Qv**S#PvE? z+ZPc!*671l+y5ALyja+97v1+Ajvf>&%#JT4q(-K8nf^bKn3Tb`UwNeeeQJi_Zu&f! zGYRv>voNhAX72G@|6p5U=lpz_DJ9IjkPe5bLjrG@UAj!Cd8w6Vc^fSbqpZ_63)QhP=r0%1Fen zXWHk+mcY0`q|Vm~nOQ$)h*v=IJQd}s_BX9U_7pj>-tmZ6WfI2jCzxf5 ziQVZ(yh&DiRC>glV1w5a93C!7M6&(Q2p&dZTsN~f*l(vuau>>CdJ^n3Th7Y_Pqdl) z?i}%k*x*cp!%R7`y9o}pMS2+#bGvOG@5c^-+2no|L0OMyaoBP-OkQT69(@Iqom0cG z#h})a@aPti!O{7x!~8+C`<~Xe+-SJ%RYow39RP-jL~@9>G_f6oc*sg! zeLARNs%sjdLnx1dz}?Q}Fq6XnfRME0EWt*&dq?UC3x+e66_PyMmJ!WLgW2?33H3p4 zo@B;5?e-VF{9}-)hht_oIv=JMT5CN4v;EB+6Wgg1alw+;2c;3PN?t`s2^e^1C!!y8 z3LA$5bABRdBx@9P&EwL4{-0ehZ8B;=-wbN)jxqie8@{p=1{E^+4k<{#JDrZh*x z=#tK1{RkYX5vi|*xks3ukIm1- zIhs!N@qv3r%ygT2gdV+g&#;k%{)jFSbIwI!R*e9bM`0Y`nXBID64oD$kG2apAI%=~ zq`kxRkz~1JZ~TNr){=f#{0GKm8Ef@m*syTR(G1_S6jWJZVc$q1_C!;RzfZ(FoEWTm zLsn#;u(5zrpGKJK13$AHUPKc6_)jXI~x8wgo$5KA-NA^$ll6pBCHjdO* zyU$_jWzM=BF~@he`Y?&|cGV)7Y}HiK14z`Tct2N-Ef27*#xQiou`tEiC*Dh7+A#7w zf*lXU8bpIV(j#S0?juI_2pf;Zp!1QcNhrd0Z#|3)-;qq#KlTXg$CBW%1Fdz~>9Gl! z`zN;kPi#k$nXdE?IVfVT7>t+r6)+ju7PA&+yF-P3@+Y<**R5)hO<4(3J_hm#PQ^XL z{BbDtyOT0;@u+HaFS{O)%p$4k7(g@y9d;FK!CH(-b_ge=2P?!J}-k_csM2 z15(lvO~GXZkF;rDXbN^pq@)v?g8xVb{qG2B-ftZ?j9_bX_OslGMDSdf%Y1 zaT1>SBT_d-EK9^s9c;Zfb^YyMfkczNEi?N3;IOfTnK(VitgqK+$5_$XZfwb*O@#WA zG(Fr_z|^<_$~yCIw*MU=P3BBP%M3^Q_WVyB@3c+G1#k)dJZG;H_UP`lA&k=ULUKf*S`&e7MxRE%ATx5LyVu2Oi} z`+#9Kr7?hguLp?+w0%GGVwmP>CY19Mu{U7sWZ3hzIntJr78Y{N6NhOhVHy~}6{g*c zd8fp_yfVC*;bNm;J%fUavi)-jnSGB=9`wcnJ@KU^Y%`-Yqb#s%8h_e+HASZO3;%+SW%`bJbo1 zGp-e8wu~gAR~#MYSJRJABbGnl0h~Dfk;&q?lj6m)WM}Ak{|zuz%%T`1qVJCk>ub2M zIDJ&u2>KU}j(Af{)mrGjg%zjR@r4nuD9K{?z)lUfJk3-#MxiCMG;b!O{pS##Bo=O~ zV=sFwOh@O~B0}TCEj7%n+X;@f!G2?z6m0N3f}?HlrKaHi$EoW~wwk8kMuH=)wDs|+ z;E7Ga8wd_J3H)COs@<58R}BV^OPz6^C+N;NeaA};oe%u837$xDb4HH82~#gO4fu8^ zM9llpcDc zKEiCW<6Ta+Q)tucR<%eJ&l3{*>0X!ygc<*_mZ!*88E4Tmgg9WG%fv=#qD`@$<#B@b z7Al{U6^F>X%}&U_2{s1Co%CxAgHx^RFb8nPpX-?2i#-6-%+1>~dQty0>zC|_sNh1_ zSmL5#!?~0K8xKQ0;J>1P((LaZgGm=WpMCicFf9g*yFaqx6Kyt@bT(uc z!<5iWNB%!y8a`>}_BdW_zVR9Vz(C{liKG}MM%~}RiPg{%L2Gfq+oGRk`OtH34 zc}ctmiEP~T1oCktItcJ*F7(J=Q*C~f4_+IKo*#pG-FXI2~{SFug$Frzt}3Ud{( z4}JlrW|)-meaozJwo&{kLn{P-Zsj9LZ1-w(?0{)9ZrZ?QmD_!RT~e!IM-a~)0hO(X z=@0|M+jgl)D$+owwy)&WZxqq^=Jb9+o&t8w+1gkfxRKOusb~2_j zF@L*i+qJZdxC+=vsN*%s-I zrmpjw%`$!kOr2u7AbuxIO=@z~*G`XkHC6@A3!~1k4QcAadyzO5alzfs0?tu>7JeTr`PZOU1Wd^q9C}Y5cAel#lk$A}VD>rt{ z*<=X09?JILB&bD$KhR+C_B@C2V#(K{ePop%UuLaf_5}W^FuR|T zrLI^O=3k4Y-f&V%<`OV#x!ZOa^XmasDW@I4Ln2+Z7vRtK8f?6ss7`>XYwlVxQtOs<-m2K+Th)B#v;RlkW&C4Y@YXP7rMF<9#l16((O(isy(C-`8eQc8oksxLUP87k3ph3o7{A% zOM_7vcF)1oQFf~9aGA|d9Pf&pcv;xEmJ7*5=--Y;W?Zlcqx$h0BQN<=`CQL3vvDaj!LHdUUUAb_4;f64MJ+nr}jdq-y z&h$@(sa`vw`#0Gw6L#c1a)^{CiUIT@OuNjKiDF%EMxvc!>^wpzDiqy*bC`ZFE|-P>H}_@7_qy9UUsLL0Bxb@&4mkf# zBvRWrZ+zGFZqTJ>jZ!4?*i;#@$C2i3p@0RX#RK<5f}Iu* zjz#ZPQ@5d*ItA*)KAFEgM7Y!T2{Ku)c=E0Qr>5t4{vOcpi z&l<)(6!FSbT9`SKdzr@{3iF>JOSgxu!%@)tiRj4>hmA<1FFqWmZvxMFB+Lh6cRj)c zA1tZQ_TMLD*4S{{`OG7a+Hq~B+~`w}hWSsDI(wt;TMC|JlDd?jaXQWS;gHXHWR65y zrP&A5BObG!#pp%JMKHUb7p_bAjj)4BYX9tP=f`bl*$a;oVP^JAb;W8Vnv2cuBla%r z^l-})%$cV=5%Es2!FLGqw_U9Lhi^(6o{1uMKI}M?B>M3tV}e-!C(RAJ3V4KIo>@IR zJ(YY!qpN}OFdcQ-4)Hg~*T9CD@BMg((4mrtd|^758Jq1*Ow_bE7CcZxaAodHgXFQHn^N1_g6^$F9~Y0Hk%Ot@Mn^xZ{r1+vtcgB zlLQBZOSUrCZ%rm)iI0xl8rE+^0ZtBH(}Tq5Yg@zg=Ww`vpH1$j_2M{04$q%_{lK5K zz%-O`_*b*y+iZ8^n7mY52GhNF>#(pZCcP4>2WO6c_h<IeN1x_L@J_@1c5w#^ud> z{!jFBXkkcJ!@R z!p2uvzuLWOt2eW)`683Zt6~1D4C4*2hV`KT!E4s(86sS}9`t%@JHj*K5}2IC9OR-m zzaG}VMvl&J*kNY>tbI02voCWkbI@HdEeb3cY+2gAnPi2ZC8A^A3>#l3|HVkP4d`s& zFMSzmJ(1m0WWR9n8yx!=z7;mULE6{evQrDgp)3*W@HTbnky~`^+hP5i#4kf4XFIn4n4b}{XFqA&LG!Zfj&JpN@c z4Q-C57x6wR%sD1?^?BENX2cx9qQAcz)^Eq(bKcX?ShAfFbRj|M#;qA|x^0B@*V*yPY6!#3+sbfix2$Qu&|>i?k>G;a zgJW+a9Tyh7%64t)r(xp<^wiCt+A+>Ecs*r+=_tYUgZ0{dW((nB_@YF#__MI_LyXt^ zUuJUqkV$DCL7rzSxT#5+`FTn@fgsO5m0&GF=0pX5B*?==1qXjY6=6Yl>_kWlw%s>A z3{w{yXO8^>t1x=UrhRG70I!huc7oYqhJybhIM4?7`zj^9fgrEh2HCM+3GuxgO1k)K zvqM+#O@hO%wA(i+=^TRjR{AKxqfOA?`CEI>!;4`tX0;hmofsT_Bo&*vn%KQ}B>UVX zo(9$Evx%dtcZ7|fV@U5i`aaz9Ihu?hX!Vc&`eRuC1szlNlYL&o5aI#UvpjQREbqymJ8<5wc)xB3);K5<^`?deUm*%2VfuHp>3-BmP{JEgW%+F3+4N}#ggT2`w;Ukf|?mWEPRrOyYE9M zo8n{nEt7d->j=&WGG_BWEni^hI)@fTm$wQVe?+TiBo3E+NBRBu21Bi0dl(Dc1~YwY z{s8Dpm`0_nb8tB1=g0_uIPV|Y9aIBH=6XMD{F%o8!I#cy3TF&4=@;=OhUz%;bk4sD zrb4*kyDlpZarP!*r=4wKlwXmR1{oY=EXwwm64Dst8iCg!8e!^d?!Nf*gIIgM2yuK^ z@EZ??2yy=YjYe!BnA&2;cj7w>wP#B8yFVIxolJ+Y}UCIT83JBDvloM79ymY|M(_5r~+Fm;qY zj~~+B8j}03(-Q}*gw+uj^VnVSll)9WejxI_e`>jOSzTt=pij+ivG#l~<7iviq*U;L zrwPvg4~huaHAXt6@zsq?XX=Ox2o511oiz!6`=CRZo<{qI@FkAswWV;|>o{>OR1RYA zf*ypayRpdptVqW&Jw}dPzTHuNX1@t-4$Ll=EZwpDU~Fq}?{5id!tla|DCO?YS3WA4 zeLd%7m^CV!qxe;hrFzJJ)k(OfqtGsVe-MBeF$x93|Q)u5)r#~@JxHpnb!FT?CEp5-mJ$DR~yUV}cFkRJ1-4!`~t zNOahvn%YGCJ(xy;GUqg_&M4FR<^iZ?gCXc5-82S|m6ta2axri*pC5O@3tGFg+6u zYxc9V0pp78?_)5#snZ)Xo%!ZVnJje;(PGN}(O{=JgZ&$c(V%AG=!v#3L(S&O%&A?v zTjQ{sVb@#IJxp&$MOPrzKGR-VZ*ys=mQ8QRaC5t;zLd?{pSwElzm~()ID5I;=$P4q z#nXCN^ECP2AS5cDS&cuWt4y+R+ezqm9aJYJ_658=>Awh7yHEvR-IXuew1cw&b}UT2 zVovX|%M@qsmR=>KJVv*8r-N*&lpn{AL&BHfaDrW?P%@>z8D?g&F!K=Z271~qQt4T( zAVW!*I^4$3M`DIm@+q$Wgp-&n>v)S^uGz_(Vm}*+ohW3onJ}Hb7^Uef9XCMx>5m6u zpA$Mb*z#C*yk~FQRVmBGYLFaaUi7&`A!Rqd@EuI9X06owV5?fxJkN{6+`R(Fj_Y7e zO1$RcNckpL)_t7HsV0p@Vtb0YA->45l#lrjAW>&A#|@^lK8LDsdp_&dH|f><$wu_J zzG35@_|{cO<&LOY$vcKHJ4TpNqQCX!?|!25^&gb#`*_Vq*pi<;E&Z=BLY}?&N$2MP zm;XP&T9foYNa(d7ZLh zhRjDOg^sh-pe%5_x%!|2vF?1$#%k*X~M^N`7RZ zDwkZS_APK+sN!7n|0`6vXSs6z25pgwPz4KJLSZX@7CYV?Rp13q-W+A%rTnOzW&9{z z13&r*C0`-JrwOTF30Lxy!Ou1P==0y8dir{k>))dkyvd~#ir=F4OS3in=pz(gE5hgR zP(FM&KT5aW;XR-}LKS>JKZ@VLj~e(OKl=O?_iHXthKEeDKVvj}bDwC(TI~$^>swRq zQ{evme8G=C%~ADzWuz9R&$o^XRm~25lDh$fxqyh*(f6N z7gR+pkSi+9#S0~G>Ex}PTqwEk?Duc+eQBg!z+kpA@sbiF1k5Ni*8OXRB&I%g^_TJN|*I|xab32w83!C z{pe+4y@OnIPZuqeaSw4^sM`9067_fT{{)qEfJ-k_Ia!W_>88Me2qNKw(nnE)T)G^I z;ko+dP{AQiez=nhrSDOoM$0jv>M3;bLIuY-Jl5e@3Hb<>|3v=J9wQ*&$^B0SRZx*j zC{%Ew!(xY%B;?Z^B`R_8lU;msRJl`;YbB`$Ro~pmu{Lm~%ecU0{5xz-h6`PMb5y}g zkqeiroUm7S3@bla>5>Z-yvXst!%oEC>eAijaGk^39o_-zBa|1fcU-9I?*-Ma2a|>+ zU_L@6c-Zm3LyfwpTzqqsXtR_5C#a;2{AWn!)95n(9cl^M=Hi74KF2R9`hvrkK(+KW zP!@gD$=?RmAzU6BSNhh)e-Fwff3GE=j8RGxa{FQ`;F85;$N;4P$8TdY&Yh-W&XDFK zR6fpNCLN~-^Z7e8L*B)+Z8x9VWI(6{Y?2MK^E994C`AUi_<;@wxpd8uHhF`Q3v*n2 zbCkkEtvr0VM|4lKri?(K(KgCuXpSoA7$}upne~rUyqi(t)U_Q-J7QV^Jg$k~AxW>ta3f}6t zQ2FixRpC9LD!kXl3nky+_=e~~us;c!xDr0(5(=Z?=Lbc*gufmXZKGdt4G8a1B7_?q zKISqAC4by;VH@}hpbCE3#lPbC>mdKUH~FP`<3lI^1k?odh2vj?{PVs|nvQ@;@FS?E z|Lzi~7A1^=lE*=f?+zgUJT0rjJ@m_=((mQuT|x0~E?%ex?F&kw9xa%V6xWkqZNVH+ zoqr@KZ_88UUr?fv{89mhpb8x0(g~ICcqbnRs=NuH6gkcDDWE=6%z~gmQUI!e>7YdA zPX6yu-aXspn-8j@Ge9YJCdfYyp_ZQoD*kK<`P3!@%~35|9#0d3!(x_ z@QjOi7F5A6JNYY4{u-!?-*xyYs84fLIiETC-=U(u;MXwl5X@)%uRj5qWQgqfKcF%m z;o^nj!yW&3D20x4>4a)|9;i!*W7BBA#&Zz@C7kFIHb=>eom?nB$?@hWd5M$%9V&jZ zixD_uMa)1D2?zIeTk&$2t~n}Mg_8@FzXp^c^k2j1Ba|I^ZgS~vcKL1r^%2U0)`MF89|Ps8PlKvw3n-U<0n`feKB&u%uR!Jd1ys7< zz+J$dWnG5~X2$Ro?S&77se%M3&p8s5Vn>5geFCV7ywb%l0JZ&C3G&aogkQ>k1E`8` z2jz+zKq>U3!{zuqf zD*h}d7pk1|z?Pb+FC?G@OB^l*HT)YKUjgbP#F4#KpbEIm#b5653WryMQv7PiuL1QD zDtM!l-vqYON^zS4;CfKbwh>fCn?M!x6sXU?Lp5NtOaC+|{!fQn96sZ4E2#3H2a7-x zP>WweppQ@$zXGbIZ-R>7?(jWO6}<2GN1zJ)%<(TA|3(;oKETbtzeAPn4^SVWxEinO zqM)jYfhtb_!It>W4tH_b*5R%WcXQYo)adR8s=WO`*|Tpevml#*DX^c*kN{Otw&OXD z4|O;kl!8Zr(zyWCJa#fDyA-+jiH=VJRo-+^>1R5eW4al^9GCD6Pz9d}O4C|UpXR9e zIv2mtr9a#8b3rMx5|kpBfci8?<-gR)FIB

`G7-UJWYlMt;e}t3efXn}iM}zr%5% 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ʞd@WE~RBkx4_GJDA`?xwuV=)g$68?^B=h%9h*qBXe`Nqt2zT~Mhewfx5MM)cf zF5Pzc^w{X>kYgGcL_z(ES@3*_jlrFva?1azJ4>98dCW-EL0P#ko{jnJ7xM>mWnGY$ z&G;sL?>F*chXD>`Ef*l;k^}@& z=mAB=qvJZ5Op%M?;&3c06tgi4kaoxh28A^?zm@)#`FuHFvq0rdRQbhh&jPIf76OAN zM#!BS946-C=62G|D_G&3@!v_zkqu9885w)xTxyYVG zA?B~h>Mz7}UZf7S?E?6^%KOiRpYO5aWub5k5T14%+W>=*tyVAA@#0k3_WNTC-@l zXL*<%*yU)c8WtkEFNSJ`U1QqCke>K!CMq}dJ1JoK%f$896Xlx_x&%?V5y98{~DXQ2z-yY&Lh7G*6!05Z`nDF9*CFMV^q=&EQM$&Jw1h7H< z+btFq1FLZN77tCDt-D<>Iqfw9Ep%2^mBwuC5~#B3HV^HlEl60{v$O`%YzhKzQ)8U$ zcZpJ(MCy_%7US{(Qvc=gP=@&9y;pku*Bw4g&nHLzdA)W*RsB^6WjRvn-&cN%lc!+?E z${H92Qd6_r%XJ|u(yV2Hk}W>orA=FgkrXErP1>=m%fMFt%o4L-j#=CpH`Zi1EYhm` zT-`3`kL_1-eZ!Foa6_K*smo3GS=e$&WiBur(M({4%Iaz>YO3`Z=K!HOZ=Wg)w$A>s z$>(~=!~5(6%4$si;1~y1+YWCW&erNNsF7JmGTFaOJ$ZvEV&)nO$kXi0Dkl)Ezd3nQ$gPbSZ;pI4(2%0Nx z=*{n?bUSS2oYpdIB_ObhG-b193s*uH{d%Zm+cjoUH(g1mr^|RznjSc@Tp+0N-2BCr zayYRl5%~3hPrkHYy5#@e@L9pOCiBoZc^;0Bu;U|m9KqRmMQrh49DCQX%c~%J|Jvi} z_7!0*

BbsDm{@tK|>HLz-crkM8bVcJ|z;-h=17s!_ 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 Config: + if cls._instance is None: + config_path = Path(__file__).parents[4] / "config.yaml" + with open(config_path) as f: + config_data = yaml.safe_load(f) + cls._instance = Config(**config_data) + return cls._instance + +# Usage example: +# from eda_config import ConfigLoader +# config = ConfigLoader.get_config() +# groq_key = config.api_keys.groq \ No newline at end of file diff --git a/packages/config/python/package.json b/packages/config/python/package.json new file mode 100644 index 0000000..32cb961 --- /dev/null +++ b/packages/config/python/package.json @@ -0,0 +1,7 @@ +{ + "name": "@eda/config-python", + "private": true, + "scripts": { + "build": "uv pip install -e ." + } +} diff --git a/packages/config/python/pyproject.toml b/packages/config/python/pyproject.toml new file mode 100644 index 0000000..71a94f7 --- /dev/null +++ b/packages/config/python/pyproject.toml @@ -0,0 +1,14 @@ +[project] +name = "eda-config" +version = "0.1.0" +dependencies = [ + "pyyaml>=6.0.1", + "pydantic>=2.0.0" +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["eda_config"] \ No newline at end of file diff --git a/packages/config/typescript/package.json b/packages/config/typescript/package.json new file mode 100644 index 0000000..233b2c1 --- /dev/null +++ b/packages/config/typescript/package.json @@ -0,0 +1,15 @@ +{ + "name": "@eda/config", + "version": "0.1.0", + "main": "dist/config.js", + "types": "dist/config.d.ts", + "type": "module", + "scripts": { + "build": "tsc && bun build ./src/config.ts --outdir ./dist --target node", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "yaml": "^2.3.4", + "zod": "^3.22.4" + } +} diff --git a/packages/config/typescript/src/config.ts b/packages/config/typescript/src/config.ts new file mode 100644 index 0000000..0f0fe7a --- /dev/null +++ b/packages/config/typescript/src/config.ts @@ -0,0 +1,80 @@ +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { parse } from "yaml"; +import { z } from "zod"; + +const ApiKeysSchema = z.object({ + groq: z.string(), + huggingface: z.string(), + serper: z.string(), + langtrace: z.string(), + trigger: z.string(), +}); + +const SupabaseConfigSchema = z.object({ + url: z.string(), + service_key: z.string(), +}); + +const DatabaseConfigSchema = z.object({ + host: z.string(), + port: z.number(), + user: z.string(), + password: z.string(), + database: z.string(), +}); + +const AIModelConfig = z.object({ + provider: z.string(), + model: z.string(), + temperature: z.number(), + description: z.string(), +}); + +const ReactionsSchema = z.object({ + queued: z.string(), + working: z.string(), + done: z.string(), + error: z.string(), +}); + +const WhatsAppSchema = z.object({ + bot_prefix: z.string(), + cmd_prefix: z.string(), + bot_name: z.string(), + enable_reactions: z.boolean(), + reactions: ReactionsSchema, +}); + +const ConfigSchema = z.object({ + ports: z.record(z.string(), z.number()), + api_keys: ApiKeysSchema, + databases: z.object({ + supabase: SupabaseConfigSchema, + postgres: DatabaseConfigSchema, + }), + ai_models: z.object({ + premium: AIModelConfig, + standard: AIModelConfig, + basic: AIModelConfig, + }), + whatsapp: WhatsAppSchema, +}); + +type Config = z.infer; + +function getConfig(): z.infer { + const configPath = join(__dirname, "../../../..", "config.yaml"); + const configFile = readFileSync(configPath, "utf8"); + const configData = parse(configFile); + return ConfigSchema.parse(configData); +} + +// Export the config instance and type +export const config = getConfig(); +export type { Config }; + +// Usage example: +// import { ConfigLoader } from '@eda/config'; +// const config = ConfigLoader.getConfig(); +// const groqKey = config.api_keys.groq; diff --git a/packages/config/typescript/tsconfig.json b/packages/config/typescript/tsconfig.json new file mode 100644 index 0000000..39b86ec --- /dev/null +++ b/packages/config/typescript/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "@eda/typescript/base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "declaration": true, + "target": "es2022", + "module": "esnext", + "moduleResolution": "bundler" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/tsconfig.json b/tsconfig.json index ce91b34..732c2af 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,3 +1,9 @@ { - "extends": "@eda/typescript/base.json" + "extends": "@eda/typescript/base.json", + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@eda/config": ["../../packages/config/typescript/dist"] + } + } } diff --git a/turbo.json b/turbo.json index 4cc85d6..da7695c 100644 --- a/turbo.json +++ b/turbo.json @@ -63,6 +63,13 @@ "seed": { "cache": false, "dependsOn": ["migrate"] + }, + "build:config": { + "dependsOn": ["^build"], + "outputs": [ + "packages/config/typescript/dist/**", + "packages/config/python/dist/**" + ] } } } From 2660b85894137e155e1cb48a4b5279dea0da3e2b Mon Sep 17 00:00:00 2001 From: Luis Otavio Date: Sun, 19 Jan 2025 18:54:11 -0300 Subject: [PATCH 33/75] Refactor WhatsApp constants and configuration for improved structure and clarity --- apps/whatsapp/src/constants.ts | 23 +- apps/whatsapp/src/utils.ts | 22 +- config.example.yaml | 184 ++++++++++-- packages/config/python/eda_config/config.py | 177 ++++++++++-- packages/config/typescript/src/config.ts | 292 +++++++++++++++++--- 5 files changed, 571 insertions(+), 127 deletions(-) diff --git a/apps/whatsapp/src/constants.ts b/apps/whatsapp/src/constants.ts index 83e6127..17ea52c 100644 --- a/apps/whatsapp/src/constants.ts +++ b/apps/whatsapp/src/constants.ts @@ -1,17 +1,10 @@ -import dotenv from "dotenv"; -import dotenvExpand from "dotenv-expand"; -dotenvExpand.expand(dotenv.config()); import { config } from "@eda/config"; -export const CMD_PREFIX = config.whatsapp.cmd_prefix; -export const BOT_PREFIX = config.whatsapp.bot_prefix; -export const BOT_NAME = config.whatsapp.bot_name; -export const ENABLE_REACTIONS = config.whatsapp.enable_reactions; -export const ALLOWED_USERS = process.env.ALLOWED_USERS - ? process.env.ALLOWED_USERS.split(",") - : []; -export const BLOCKED_USERS = process.env.BLOCKED_USERS - ? process.env.BLOCKED_USERS.split(",") - : []; -export const IGNORE_MESSAGES_WARNING = process.env - .IGNORE_MESSAGES_WARNING as string; +export const CMD_PREFIX = config.services.whatsapp.cmd_prefix; +export const BOT_PREFIX = config.services.whatsapp.bot_prefix; +export const BOT_NAME = config.services.whatsapp.bot_name; +export const ENABLE_REACTIONS = config.services.whatsapp.enable_reactions; +export const ALLOWED_USERS = config.access.allowed_users; +export const BLOCKED_USERS = config.access.blocked_users; +export const IGNORE_MESSAGES_WARNING = + config.services.whatsapp.ignore_messages_warning; diff --git a/apps/whatsapp/src/utils.ts b/apps/whatsapp/src/utils.ts index 2d2b2f4..187c218 100644 --- a/apps/whatsapp/src/utils.ts +++ b/apps/whatsapp/src/utils.ts @@ -43,27 +43,9 @@ export async function react( reaction: keyof typeof REACTIONS, ) { switch (ENABLE_REACTIONS) { - case "false": + case false: break; - case "true": - await sock.sendMessage(message.key.remoteJid!, { - react: { - text: REACTIONS[reaction], - key: message.key, - }, - }); - break; - case "dms_only": - if (isGroupMessage(message)) return; - await sock.sendMessage(message.key.remoteJid!, { - react: { - text: REACTIONS[reaction], - key: message.key, - }, - }); - break; - case "groups_only": - if (!isGroupMessage(message)) return; + case true: await sock.sendMessage(message.key.remoteJid!, { react: { text: REACTIONS[reaction], diff --git a/config.example.yaml b/config.example.yaml index a58d7fa..2922dee 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -1,47 +1,181 @@ -# Service Ports +# Service Ports (all port configurations in one place) ports: - whatsapp: 4000 - messaging: 3001 - ai_api: 8000 - langtrace: 3003 - trigger: 3040 + messaging: 3001 # WhatsApp message handling service + ai_api: 8000 # FastAPI service for AI operations + langtrace: 3003 # LangTrace monitoring service + trigger: 3040 # Trigger.dev job processing + remix: 3030 # Trigger.dev remix app + whatsapp: 4000 # WhatsApp bot service + dashboard: 8080 # Admin dashboard + landingpage: 8081 + docs: 8082 + db: + postgres: 5432 + redis: 6379 + neo4j: + http: 7474 # Neo4j browser interface + bolt: 7687 # Neo4j database connection + clickhouse: 8123 -# API Keys +# API Keys (unified section for all API keys) api_keys: - groq: "xxx" - huggingface: "xxx" - serper: "xxx" - langtrace: "" - trigger: "xxx" + groq: "xxx" # LLM API + huggingface: "xxx" # Embeddings API + serper: "xxx" # Search API + langtrace: "xxx" # LLM monitoring + trigger: "xxx" # Background jobs + resend: "xxx" # Email service + openai: "xxx" + supabase: + service_key: "xxx" + anon_key: "xxx" + openpanel: + client_id: "xxx" + secret: "xxx" + dub: "xxx" + sentry: + auth_token: "xxx" -# Database Configs +# Database Settings databases: supabase: url: "xxx" - service_key: "" - postgres: - host: "xxx" + project_id: "xxx" + + langtrace_postgres: + host: "langtrace-postgres" port: 5432 user: "xxx" password: "xxx" - database: "xxx" + database: "langtrace" + + langtrace_clickhouse: + host: "http://langtrace-clickhouse:8123" + user: "xxx" + password: "xxx" + database: "langtrace_traces" + + trigger_postgres: + host: postgres + port: "${ports.db.postgres}" + user: "xxx" + password: "xxx" + database: postgres + + redis: + host: redis + port: "${ports.db.redis}" + tls_disabled: true + + neo4j: + host: localhost + auth: + user: "xxx" + password: "xxx" + plugins: ["apoc"] + healthcheck: + interval: 10 + timeout: 5 + retries: 3 -# AI Models +# Service Configurations +services: + ai_api: + debug: false + + whatsapp: + bot_prefix: "*[BOT]:*" + cmd_prefix: "!" + bot_name: "Bot" + enable_reactions: true + reactions: + queued: "πŸ”" + working: "βš™οΈ" + done: "βœ…" + error: "⚠️" + puppeteer_path: "/snap/bin/chromium" + ignore_messages_warning: false + mongodb_uri: "xxx" + + resend: + from_email: "xxx" + reply_to: "xxx" + + langtrace: + api: + host: "http://localhost:3000/api/trace" + key: "${api_keys.langtrace}" + admin: + email: "admin@example.com" + password: "xxx" + enable_login: true + telemetry: + enabled: false + posthog: + host: "xxx" + + trigger: + project_id: "xxx" + api_url: "http://localhost:3040" + environment: "development" + runtime: "docker-compose" + v3_enabled: true + concurrency: + org_execution_limit: 300 + env_execution_limit: 100 + auth: + magic_link_secret: "xxx" + session_secret: "xxx" + encryption_key: "xxx" + provider_secret: "xxx" + coordinator_secret: "xxx" + deployment: + worker: + http_port: 9020 + coordinator_host: "127.0.0.1" + coordinator_port: 9020 + docker: + publish_ip: "127.0.0.1" + sentry: + auth_token: "xxx" + dsn: "xxx" + org: "xxx" + project: "xxx" + + dashboard: + auth: + nextauth_secret: "xxx" + azure: + client_id: "xxx" + client_secret: "xxx" + tenant_id: "xxx" + google: + client_id: "xxx" + secret: "xxx" + + upstash: + redis_url: "xxx" + redis_token: "xxx" + +# AI Models Configuration ai_models: premium: provider: "groq" model: "llama-3.3-70b-versatile" temperature: 0.5 - description: "High-performance model" - + description: "High-performance model for complex reasoning and generation" standard: - provider: "groq" + provider: "groq" model: "mixtral-8x7b-v1" temperature: 0.7 - description: "Balanced model" - + description: "Balanced model for general purpose tasks" basic: provider: "groq" - model: "llama-2-7b-chat" + model: "llama-2-7b-chat" temperature: 0.8 - description: "Fast model" \ No newline at end of file + description: "Fast model for simple tasks and quick responses" + +# Access Control +access: + allowed_users: [] # List of allowed phone numbers + blocked_users: [] # List of blocked phone numbers \ No newline at end of file diff --git a/packages/config/python/eda_config/config.py b/packages/config/python/eda_config/config.py index 71510ad..c21bd1f 100644 --- a/packages/config/python/eda_config/config.py +++ b/packages/config/python/eda_config/config.py @@ -1,25 +1,76 @@ from pathlib import Path -from typing import Optional, Union +from typing import List, Optional, Union, Dict import yaml from pydantic import BaseModel +class Neo4jAuth(BaseModel): + user: str + password: str + +class Neo4jHealthcheck(BaseModel): + interval: int + timeout: int + retries: int + +class Neo4jConfig(BaseModel): + host: str + auth: Neo4jAuth + plugins: List[str] + healthcheck: Neo4jHealthcheck + +class RedisConfig(BaseModel): + host: str + port: Optional[int] = None # Make port optional + tls_disabled: bool + +class DatabaseConfig(BaseModel): + host: str + port: Optional[int] = None # Make port optional + user: str + password: str + database: str + +class SupabaseKeys(BaseModel): + service_key: str + anon_key: str + +class OpenPanelKeys(BaseModel): + client_id: str + secret: str + +class SentryKeys(BaseModel): + auth_token: str + class ApiKeys(BaseModel): groq: str huggingface: str serper: str langtrace: str trigger: str + resend: str + openai: str + supabase: SupabaseKeys + openpanel: OpenPanelKeys + dub: str + sentry: SentryKeys -class SupabaseConfig(BaseModel): - url: str - service_key: str +class DbPorts(BaseModel): + postgres: int + redis: int + neo4j: Dict[str, int] + clickhouse: int -class DatabaseConfig(BaseModel): - host: str - port: int - user: str - password: str - database: str +class Ports(BaseModel): + messaging: int + ai_api: int + langtrace: int + trigger: int + remix: int + whatsapp: int + dashboard: int + landingpage: int + docs: int + db: DbPorts class AIModelConfig(BaseModel): provider: str @@ -27,25 +78,110 @@ class AIModelConfig(BaseModel): temperature: float description: str -class Reactions(BaseModel): +class WhatsappReactions(BaseModel): queued: str working: str done: str error: str -class WhatsApp(BaseModel): +class WhatsappConfig(BaseModel): bot_prefix: str cmd_prefix: str bot_name: str enable_reactions: bool - reactions: Reactions + reactions: WhatsappReactions + puppeteer_path: str + ignore_messages_warning: bool + mongodb_uri: str + +class TriggerAuth(BaseModel): + magic_link_secret: str + session_secret: str + encryption_key: str + provider_secret: str + coordinator_secret: str + +class TriggerWorker(BaseModel): + http_port: int + coordinator_host: str + coordinator_port: int + +class TriggerDeployment(BaseModel): + worker: TriggerWorker + docker: Dict[str, str] + +class TriggerConfig(BaseModel): + project_id: str + api_url: str + environment: str + runtime: str + v3_enabled: bool + concurrency: Dict[str, int] + auth: TriggerAuth + deployment: TriggerDeployment + sentry: Dict[str, str] + +class ResendConfig(BaseModel): + from_email: str + reply_to: str + +class LangTraceApiConfig(BaseModel): + host: str + +class LangTraceAdminConfig(BaseModel): + email: str + password: str + enable_login: bool + +class LangTraceTelemetryConfig(BaseModel): + enabled: bool + +class LangTracePosthogConfig(BaseModel): + host: str + +class LangTraceConfig(BaseModel): + api: LangTraceApiConfig + admin: LangTraceAdminConfig + telemetry: LangTraceTelemetryConfig + posthog: LangTracePosthogConfig + +class DashboardAuthAzure(BaseModel): + client_id: str + client_secret: str + tenant_id: str + +class DashboardAuthGoogle(BaseModel): + client_id: str + secret: str + +class DashboardAuth(BaseModel): + nextauth_secret: str + azure: DashboardAuthAzure + google: DashboardAuthGoogle + +class DashboardConfig(BaseModel): + auth: DashboardAuth + +class UpstashConfig(BaseModel): + redis_url: str + redis_token: str + +class ServicesConfig(BaseModel): + ai_api: Dict[str, bool] + whatsapp: WhatsappConfig + trigger: TriggerConfig + resend: ResendConfig + langtrace: LangTraceConfig + dashboard: DashboardConfig + upstash: UpstashConfig class Config(BaseModel): - ports: dict[str, int] + ports: Ports api_keys: ApiKeys - databases: dict[str, Union[SupabaseConfig, DatabaseConfig]] - ai_models: dict[str, AIModelConfig] - whatsapp: WhatsApp + databases: Dict[str, Union[DatabaseConfig, RedisConfig, Neo4jConfig]] + services: ServicesConfig + ai_models: Dict[str, AIModelConfig] + access: Dict[str, List[str]] class ConfigLoader: _instance: Optional[Config] = None @@ -57,9 +193,4 @@ def get_config(cls) -> Config: with open(config_path) as f: config_data = yaml.safe_load(f) cls._instance = Config(**config_data) - return cls._instance - -# Usage example: -# from eda_config import ConfigLoader -# config = ConfigLoader.get_config() -# groq_key = config.api_keys.groq \ No newline at end of file + return cls._instance \ No newline at end of file diff --git a/packages/config/typescript/src/config.ts b/packages/config/typescript/src/config.ts index 0f0fe7a..bb61e5e 100644 --- a/packages/config/typescript/src/config.ts +++ b/packages/config/typescript/src/config.ts @@ -3,78 +3,282 @@ import { join } from "node:path"; import { parse } from "yaml"; import { z } from "zod"; -const ApiKeysSchema = z.object({ - groq: z.string(), - huggingface: z.string(), - serper: z.string(), - langtrace: z.string(), - trigger: z.string(), -}); - -const SupabaseConfigSchema = z.object({ - url: z.string(), - service_key: z.string(), -}); - +// Database Configs const DatabaseConfigSchema = z.object({ host: z.string(), - port: z.number(), + port: z.number().optional(), // Make port optional user: z.string(), password: z.string(), database: z.string(), }); -const AIModelConfig = z.object({ - provider: z.string(), - model: z.string(), - temperature: z.number(), - description: z.string(), +const Neo4jAuthSchema = z.object({ + user: z.string(), + password: z.string(), +}); + +const Neo4jConfigSchema = z.object({ + host: z.string(), + auth: Neo4jAuthSchema, + plugins: z.array(z.string()), + healthcheck: z.object({ + interval: z.number(), + timeout: z.number(), + retries: z.number(), + }), }); -const ReactionsSchema = z.object({ - queued: z.string(), - working: z.string(), - done: z.string(), - error: z.string(), +const RedisConfigSchema = z.object({ + host: z.string(), + port: z.number().optional(), // Make port optional + tls_disabled: z.boolean(), }); -const WhatsAppSchema = z.object({ +// Service Configs +const WhatsAppConfigSchema = z.object({ bot_prefix: z.string(), cmd_prefix: z.string(), bot_name: z.string(), enable_reactions: z.boolean(), - reactions: ReactionsSchema, + reactions: z.object({ + queued: z.string(), + working: z.string(), + done: z.string(), + error: z.string(), + }), + puppeteer_path: z.string(), + ignore_messages_warning: z.boolean(), + mongodb_uri: z.string(), +}); + +const TriggerConfigSchema = z.object({ + project_id: z.string(), + api_url: z.string(), + environment: z.string(), + runtime: z.string(), + v3_enabled: z.boolean(), + concurrency: z.object({ + org_execution_limit: z.number(), + env_execution_limit: z.number(), + }), + auth: z.object({ + magic_link_secret: z.string(), + session_secret: z.string(), + encryption_key: z.string(), + provider_secret: z.string(), + coordinator_secret: z.string(), + }), + deployment: z.object({ + worker: z.object({ + http_port: z.number(), + coordinator_host: z.string(), + coordinator_port: z.number(), + }), + docker: z.object({ + publish_ip: z.string(), + }), + }), + sentry: z.object({ + auth_token: z.string(), + dsn: z.string(), + org: z.string(), + project: z.string(), + }), +}); + +const ResendConfigSchema = z.object({ + from_email: z.string(), + reply_to: z.string(), +}); + +const LangTraceConfigSchema = z.object({ + api: z.object({ + host: z.string(), + }), + admin: z.object({ + email: z.string(), + password: z.string(), + enable_login: z.boolean(), + }), + telemetry: z.object({ + enabled: z.boolean(), + }), + posthog: z.object({ + host: z.string(), + }), }); +const DashboardAuthConfigSchema = z.object({ + nextauth_secret: z.string(), + azure: z.object({ + client_id: z.string(), + client_secret: z.string(), + tenant_id: z.string(), + }), + google: z.object({ + client_id: z.string(), + secret: z.string(), + }), +}); + +const DashboardConfigSchema = z.object({ + auth: DashboardAuthConfigSchema, +}); + +const UpstashConfigSchema = z.object({ + redis_url: z.string(), + redis_token: z.string(), +}); + +// API Keys +const ApiKeysSchema = z.object({ + groq: z.string(), + huggingface: z.string(), + serper: z.string(), + langtrace: z.string(), + trigger: z.string(), + resend: z.string(), + openai: z.string(), + supabase: z.object({ + service_key: z.string(), + anon_key: z.string(), + }), + openpanel: z.object({ + client_id: z.string(), + secret: z.string(), + }), + dub: z.string(), + sentry: z.object({ + auth_token: z.string(), + }), +}); + +// AI Models +const AIModelConfigSchema = z.object({ + provider: z.string(), + model: z.string(), + temperature: z.number(), + description: z.string(), +}); + +// Root Config const ConfigSchema = z.object({ - ports: z.record(z.string(), z.number()), + ports: z.object({ + messaging: z.number(), + ai_api: z.number(), + langtrace: z.number(), + trigger: z.number(), + remix: z.number(), + whatsapp: z.number(), + dashboard: z.number(), + landingpage: z.number(), + docs: z.number(), + db: z.object({ + postgres: z.number(), + redis: z.number(), + neo4j: z.object({ + http: z.number(), + bolt: z.number(), + }), + clickhouse: z.number(), + }), + }), api_keys: ApiKeysSchema, databases: z.object({ - supabase: SupabaseConfigSchema, - postgres: DatabaseConfigSchema, + supabase: z.object({ + url: z.string(), + project_id: z.string(), + }), + langtrace_postgres: DatabaseConfigSchema, + langtrace_clickhouse: z.object({ + host: z.string(), + user: z.string(), + password: z.string(), + database: z.string(), + }), + trigger_postgres: DatabaseConfigSchema, + redis: RedisConfigSchema, + neo4j: Neo4jConfigSchema, + }), + services: z.object({ + ai_api: z.object({ + debug: z.boolean(), + }), + whatsapp: WhatsAppConfigSchema, + trigger: TriggerConfigSchema, + resend: ResendConfigSchema, + langtrace: LangTraceConfigSchema, + dashboard: DashboardConfigSchema, + upstash: UpstashConfigSchema, }), ai_models: z.object({ - premium: AIModelConfig, - standard: AIModelConfig, - basic: AIModelConfig, + premium: AIModelConfigSchema, + standard: AIModelConfigSchema, + basic: AIModelConfigSchema, + }), + access: z.object({ + allowed_users: z.array(z.string()), + blocked_users: z.array(z.string()), }), - whatsapp: WhatsAppSchema, }); type Config = z.infer; -function getConfig(): z.infer { - const configPath = join(__dirname, "../../../..", "config.yaml"); - const configFile = readFileSync(configPath, "utf8"); - const configData = parse(configFile); - return ConfigSchema.parse(configData); +function formatZodError(error: z.ZodError) { + const errors = error.errors.map((err) => { + const path = err.path.join("."); + let details = ""; + + if ("code" in err) { + switch (err.code) { + case "invalid_type": + details = `\nReceived: ${(err as z.ZodInvalidTypeIssue).received}\nExpected: ${ + (err as z.ZodInvalidTypeIssue).expected + }`; + break; + case "invalid_string": + details = `\nValidation: ${(err as z.ZodInvalidStringIssue).validation}`; + break; + case "unrecognized_keys": + details = `\nKeys: ${(err as z.ZodUnrecognizedKeysIssue).keys.join(", ")}`; + break; + default: + details = ""; + } + } + + return `[${path}]: ${err.message}${details}`; + }); + + return `Configuration Error:\n${errors.join("\n\n")}`; +} + +function getConfig(): Config { + try { + const configPath = join(__dirname, "../../../..", "config.yaml"); + const configFile = readFileSync(configPath, "utf8"); + const configData = parse(configFile); + + // Pre-process template literals + const processedConfig = JSON.parse( + JSON.stringify(configData).replace(/\${ports\.db\.[^}]+}/g, (match) => { + const path = match.slice(2, -1).split("."); + let value = configData; + for (const key of path) { + value = value[key]; + } + return value; + }), + ); + + return ConfigSchema.parse(processedConfig); + } catch (error) { + if (error instanceof z.ZodError) { + throw new Error(formatZodError(error)); + } + throw error; + } } -// Export the config instance and type export const config = getConfig(); export type { Config }; - -// Usage example: -// import { ConfigLoader } from '@eda/config'; -// const config = ConfigLoader.getConfig(); -// const groqKey = config.api_keys.groq; From ae3bf953e4d6ba776f1e396ce832820d6f8d02af Mon Sep 17 00:00:00 2001 From: Luis Otavio Date: Sun, 19 Jan 2025 18:59:19 -0300 Subject: [PATCH 34/75] Refactor configuration schemas into separate types file for better organization and maintainability --- packages/config/typescript/src/config.ts | 222 +---------------------- packages/config/typescript/src/types.ts | 221 ++++++++++++++++++++++ 2 files changed, 222 insertions(+), 221 deletions(-) create mode 100644 packages/config/typescript/src/types.ts diff --git a/packages/config/typescript/src/config.ts b/packages/config/typescript/src/config.ts index bb61e5e..167345f 100644 --- a/packages/config/typescript/src/config.ts +++ b/packages/config/typescript/src/config.ts @@ -2,227 +2,7 @@ import { readFileSync } from "node:fs"; import { join } from "node:path"; import { parse } from "yaml"; import { z } from "zod"; - -// Database Configs -const DatabaseConfigSchema = z.object({ - host: z.string(), - port: z.number().optional(), // Make port optional - user: z.string(), - password: z.string(), - database: z.string(), -}); - -const Neo4jAuthSchema = z.object({ - user: z.string(), - password: z.string(), -}); - -const Neo4jConfigSchema = z.object({ - host: z.string(), - auth: Neo4jAuthSchema, - plugins: z.array(z.string()), - healthcheck: z.object({ - interval: z.number(), - timeout: z.number(), - retries: z.number(), - }), -}); - -const RedisConfigSchema = z.object({ - host: z.string(), - port: z.number().optional(), // Make port optional - tls_disabled: z.boolean(), -}); - -// Service Configs -const WhatsAppConfigSchema = z.object({ - bot_prefix: z.string(), - cmd_prefix: z.string(), - bot_name: z.string(), - enable_reactions: z.boolean(), - reactions: z.object({ - queued: z.string(), - working: z.string(), - done: z.string(), - error: z.string(), - }), - puppeteer_path: z.string(), - ignore_messages_warning: z.boolean(), - mongodb_uri: z.string(), -}); - -const TriggerConfigSchema = z.object({ - project_id: z.string(), - api_url: z.string(), - environment: z.string(), - runtime: z.string(), - v3_enabled: z.boolean(), - concurrency: z.object({ - org_execution_limit: z.number(), - env_execution_limit: z.number(), - }), - auth: z.object({ - magic_link_secret: z.string(), - session_secret: z.string(), - encryption_key: z.string(), - provider_secret: z.string(), - coordinator_secret: z.string(), - }), - deployment: z.object({ - worker: z.object({ - http_port: z.number(), - coordinator_host: z.string(), - coordinator_port: z.number(), - }), - docker: z.object({ - publish_ip: z.string(), - }), - }), - sentry: z.object({ - auth_token: z.string(), - dsn: z.string(), - org: z.string(), - project: z.string(), - }), -}); - -const ResendConfigSchema = z.object({ - from_email: z.string(), - reply_to: z.string(), -}); - -const LangTraceConfigSchema = z.object({ - api: z.object({ - host: z.string(), - }), - admin: z.object({ - email: z.string(), - password: z.string(), - enable_login: z.boolean(), - }), - telemetry: z.object({ - enabled: z.boolean(), - }), - posthog: z.object({ - host: z.string(), - }), -}); - -const DashboardAuthConfigSchema = z.object({ - nextauth_secret: z.string(), - azure: z.object({ - client_id: z.string(), - client_secret: z.string(), - tenant_id: z.string(), - }), - google: z.object({ - client_id: z.string(), - secret: z.string(), - }), -}); - -const DashboardConfigSchema = z.object({ - auth: DashboardAuthConfigSchema, -}); - -const UpstashConfigSchema = z.object({ - redis_url: z.string(), - redis_token: z.string(), -}); - -// API Keys -const ApiKeysSchema = z.object({ - groq: z.string(), - huggingface: z.string(), - serper: z.string(), - langtrace: z.string(), - trigger: z.string(), - resend: z.string(), - openai: z.string(), - supabase: z.object({ - service_key: z.string(), - anon_key: z.string(), - }), - openpanel: z.object({ - client_id: z.string(), - secret: z.string(), - }), - dub: z.string(), - sentry: z.object({ - auth_token: z.string(), - }), -}); - -// AI Models -const AIModelConfigSchema = z.object({ - provider: z.string(), - model: z.string(), - temperature: z.number(), - description: z.string(), -}); - -// Root Config -const ConfigSchema = z.object({ - ports: z.object({ - messaging: z.number(), - ai_api: z.number(), - langtrace: z.number(), - trigger: z.number(), - remix: z.number(), - whatsapp: z.number(), - dashboard: z.number(), - landingpage: z.number(), - docs: z.number(), - db: z.object({ - postgres: z.number(), - redis: z.number(), - neo4j: z.object({ - http: z.number(), - bolt: z.number(), - }), - clickhouse: z.number(), - }), - }), - api_keys: ApiKeysSchema, - databases: z.object({ - supabase: z.object({ - url: z.string(), - project_id: z.string(), - }), - langtrace_postgres: DatabaseConfigSchema, - langtrace_clickhouse: z.object({ - host: z.string(), - user: z.string(), - password: z.string(), - database: z.string(), - }), - trigger_postgres: DatabaseConfigSchema, - redis: RedisConfigSchema, - neo4j: Neo4jConfigSchema, - }), - services: z.object({ - ai_api: z.object({ - debug: z.boolean(), - }), - whatsapp: WhatsAppConfigSchema, - trigger: TriggerConfigSchema, - resend: ResendConfigSchema, - langtrace: LangTraceConfigSchema, - dashboard: DashboardConfigSchema, - upstash: UpstashConfigSchema, - }), - ai_models: z.object({ - premium: AIModelConfigSchema, - standard: AIModelConfigSchema, - basic: AIModelConfigSchema, - }), - access: z.object({ - allowed_users: z.array(z.string()), - blocked_users: z.array(z.string()), - }), -}); - -type Config = z.infer; +import { type Config, ConfigSchema } from "./types"; function formatZodError(error: z.ZodError) { const errors = error.errors.map((err) => { diff --git a/packages/config/typescript/src/types.ts b/packages/config/typescript/src/types.ts new file mode 100644 index 0000000..b28706e --- /dev/null +++ b/packages/config/typescript/src/types.ts @@ -0,0 +1,221 @@ +import { z } from "zod"; + +// Database Schemas +export const DatabaseConfigSchema = z.object({ + host: z.string(), + port: z.number().optional(), + user: z.string(), + password: z.string(), + database: z.string(), +}); + +export const Neo4jAuthSchema = z.object({ + user: z.string(), + password: z.string(), +}); + +const Neo4jConfigSchema = z.object({ + host: z.string(), + auth: Neo4jAuthSchema, + plugins: z.array(z.string()), + healthcheck: z.object({ + interval: z.number(), + timeout: z.number(), + retries: z.number(), + }), +}); + +const RedisConfigSchema = z.object({ + host: z.string(), + port: z.number().optional(), // Make port optional + tls_disabled: z.boolean(), +}); + +// Service Configs +const WhatsAppConfigSchema = z.object({ + bot_prefix: z.string(), + cmd_prefix: z.string(), + bot_name: z.string(), + enable_reactions: z.boolean(), + reactions: z.object({ + queued: z.string(), + working: z.string(), + done: z.string(), + error: z.string(), + }), + puppeteer_path: z.string(), + ignore_messages_warning: z.boolean(), + mongodb_uri: z.string(), +}); + +const TriggerConfigSchema = z.object({ + project_id: z.string(), + api_url: z.string(), + environment: z.string(), + runtime: z.string(), + v3_enabled: z.boolean(), + concurrency: z.object({ + org_execution_limit: z.number(), + env_execution_limit: z.number(), + }), + auth: z.object({ + magic_link_secret: z.string(), + session_secret: z.string(), + encryption_key: z.string(), + provider_secret: z.string(), + coordinator_secret: z.string(), + }), + deployment: z.object({ + worker: z.object({ + http_port: z.number(), + coordinator_host: z.string(), + coordinator_port: z.number(), + }), + docker: z.object({ + publish_ip: z.string(), + }), + }), + sentry: z.object({ + auth_token: z.string(), + dsn: z.string(), + org: z.string(), + project: z.string(), + }), +}); + +const ResendConfigSchema = z.object({ + from_email: z.string(), + reply_to: z.string(), +}); + +const LangTraceConfigSchema = z.object({ + api: z.object({ + host: z.string(), + }), + admin: z.object({ + email: z.string(), + password: z.string(), + enable_login: z.boolean(), + }), + telemetry: z.object({ + enabled: z.boolean(), + }), + posthog: z.object({ + host: z.string(), + }), +}); + +const DashboardAuthConfigSchema = z.object({ + nextauth_secret: z.string(), + azure: z.object({ + client_id: z.string(), + client_secret: z.string(), + tenant_id: z.string(), + }), + google: z.object({ + client_id: z.string(), + secret: z.string(), + }), +}); + +const DashboardConfigSchema = z.object({ + auth: DashboardAuthConfigSchema, +}); + +const UpstashConfigSchema = z.object({ + redis_url: z.string(), + redis_token: z.string(), +}); + +// API Keys +const ApiKeysSchema = z.object({ + groq: z.string(), + huggingface: z.string(), + serper: z.string(), + langtrace: z.string(), + trigger: z.string(), + resend: z.string(), + openai: z.string(), + supabase: z.object({ + service_key: z.string(), + anon_key: z.string(), + }), + openpanel: z.object({ + client_id: z.string(), + secret: z.string(), + }), + dub: z.string(), + sentry: z.object({ + auth_token: z.string(), + }), +}); + +// AI Models +const AIModelConfigSchema = z.object({ + provider: z.string(), + model: z.string(), + temperature: z.number(), + description: z.string(), +}); + +export const ConfigSchema = z.object({ + ports: z.object({ + messaging: z.number(), + ai_api: z.number(), + langtrace: z.number(), + trigger: z.number(), + remix: z.number(), + whatsapp: z.number(), + dashboard: z.number(), + landingpage: z.number(), + docs: z.number(), + db: z.object({ + postgres: z.number(), + redis: z.number(), + neo4j: z.object({ + http: z.number(), + bolt: z.number(), + }), + clickhouse: z.number(), + }), + }), + api_keys: ApiKeysSchema, + databases: z.object({ + supabase: z.object({ + url: z.string(), + project_id: z.string(), + }), + langtrace_postgres: DatabaseConfigSchema, + langtrace_clickhouse: z.object({ + host: z.string(), + user: z.string(), + password: z.string(), + database: z.string(), + }), + trigger_postgres: DatabaseConfigSchema, + redis: RedisConfigSchema, + neo4j: Neo4jConfigSchema, + }), + services: z.object({ + ai_api: z.object({ + debug: z.boolean(), + }), + whatsapp: WhatsAppConfigSchema, + trigger: TriggerConfigSchema, + resend: ResendConfigSchema, + langtrace: LangTraceConfigSchema, + dashboard: DashboardConfigSchema, + upstash: UpstashConfigSchema, + }), + ai_models: z.object({ + premium: AIModelConfigSchema, + standard: AIModelConfigSchema, + basic: AIModelConfigSchema, + }), + access: z.object({ + allowed_users: z.array(z.string()), + blocked_users: z.array(z.string()), + }), +}); + +export type Config = z.infer; From 210ade8e6e49270d2b35ef86e249af68b72efbb1 Mon Sep 17 00:00:00 2001 From: Luis Otavio Date: Sun, 19 Jan 2025 19:01:11 -0300 Subject: [PATCH 35/75] Update example configuration with local database settings and enable debugging for services --- config.example.yaml | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/config.example.yaml b/config.example.yaml index 2922dee..bd21336 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -39,8 +39,8 @@ api_keys: # Database Settings databases: supabase: - url: "xxx" - project_id: "xxx" + url: http://127.0.0.1:54321 + project_id: "create-eda" langtrace_postgres: host: "langtrace-postgres" @@ -57,14 +57,12 @@ databases: trigger_postgres: host: postgres - port: "${ports.db.postgres}" user: "xxx" password: "xxx" database: postgres redis: host: redis - port: "${ports.db.redis}" tls_disabled: true neo4j: @@ -81,16 +79,16 @@ databases: # Service Configurations services: ai_api: - debug: false + debug: true whatsapp: - bot_prefix: "*[BOT]:*" - cmd_prefix: "!" - bot_name: "Bot" + bot_prefix: "*[BOT]:*" # Message prefix for bot responses + cmd_prefix: "!" # Command prefix for users + bot_name: "Sydney" enable_reactions: true reactions: queued: "πŸ”" - working: "βš™οΈ" + working: "βš™οΈ" done: "βœ…" error: "⚠️" puppeteer_path: "/snap/bin/chromium" @@ -104,13 +102,12 @@ services: langtrace: api: host: "http://localhost:3000/api/trace" - key: "${api_keys.langtrace}" admin: - email: "admin@example.com" + email: "xxx" password: "xxx" enable_login: true telemetry: - enabled: false + enabled: true posthog: host: "xxx" From a9e0463be3e8317fdce7193bb7e39d4de7041f01 Mon Sep 17 00:00:00 2001 From: Luis Otavio Date: Tue, 21 Jan 2025 01:32:43 -0300 Subject: [PATCH 36/75] Initial python config package --- .../eda_ai_api/api/routes/classifier.py | 5 + apps/ai_api/package.json | 2 +- apps/ai_api/pyproject.toml | 8 +- apps/ai_api/uv.lock | 19 +- package.json | 2 +- packages/config/python/eda_config/__init__.py | 6 + packages/config/python/eda_config/config.py | 209 +++--------------- packages/config/python/eda_config/types.py | 196 ++++++++++++++++ packages/config/python/package.json | 3 +- packages/config/python/pyproject.toml | 4 +- packages/config/scripts/bump-version.ts | 56 +++++ packages/config/typescript/package.json | 5 +- turbo.json | 6 +- 13 files changed, 325 insertions(+), 196 deletions(-) create mode 100644 packages/config/python/eda_config/__init__.py create mode 100644 packages/config/python/eda_config/types.py create mode 100644 packages/config/scripts/bump-version.ts diff --git a/apps/ai_api/eda_ai_api/api/routes/classifier.py b/apps/ai_api/eda_ai_api/api/routes/classifier.py index bd247e1..e5a3003 100644 --- a/apps/ai_api/eda_ai_api/api/routes/classifier.py +++ b/apps/ai_api/eda_ai_api/api/routes/classifier.py @@ -17,6 +17,11 @@ PROPOSAL_TEMPLATE, TOPIC_TEMPLATE, ) +from eda_config.config import ConfigLoader # Add this import + +config = ConfigLoader.get_config() # Add this line + +print(config.services.ai_api) router = APIRouter() # Disabled Mem0 memory manager diff --git a/apps/ai_api/package.json b/apps/ai_api/package.json index ed8d6f9..4da52f8 100644 --- a/apps/ai_api/package.json +++ b/apps/ai_api/package.json @@ -2,7 +2,7 @@ "name": "@eda/ai-api", "version": "0.0.1", "scripts": { - "dev": "uv run uvicorn --reload eda_ai_api.main:app --port 8083", + "dev": "uv venv && . .venv/bin/activate && uv pip install -e . && uv run uvicorn --reload eda_ai_api.main:app --port 8083", "start": "uv run uvicorn eda_ai_api.main:app", "lint": "bash ./scripts/linting.sh", "test": "bash ./scripts/test.sh" diff --git a/apps/ai_api/pyproject.toml b/apps/ai_api/pyproject.toml index d036c1e..e63b319 100644 --- a/apps/ai_api/pyproject.toml +++ b/apps/ai_api/pyproject.toml @@ -16,6 +16,7 @@ dependencies = [ "opportunity-finder", "proposal_writer", "onboarding", + "eda-config", "ffmpeg-python>=0.2.0", "python-multipart>=0.0.17", "llama-index>=0.12.3", @@ -30,7 +31,7 @@ dependencies = [ "neo4j>=5.27.0", "langchain>=0.3.4", "langgraph>=0.2.35", - "anthropic>=0.42.0", + "anthropic>=0.42.0" ] [project.optional-dependencies] @@ -86,7 +87,4 @@ ignore_missing_imports = true opportunity-finder = { path = "../../plugins/grant_plugin/opportunity_finder" } proposal_writer = { path = "../../plugins/grant_plugin/proposal_writer" } onboarding = { path = "../../plugins/onboarding" } - -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" +eda-config = { path = "../../packages/config/python" } diff --git a/apps/ai_api/uv.lock b/apps/ai_api/uv.lock index 2154df1..08fd327 100644 --- a/apps/ai_api/uv.lock +++ b/apps/ai_api/uv.lock @@ -835,9 +835,10 @@ wheels = [ [[package]] name = "eda-ai-api" version = "0.1.0" -source = { editable = "." } +source = { virtual = "." } dependencies = [ { name = "anthropic" }, + { name = "eda-config" }, { name = "fastapi" }, { name = "ffmpeg-python" }, { name = "httpx" }, @@ -881,6 +882,7 @@ requires-dist = [ { name = "anthropic", specifier = ">=0.42.0" }, { name = "bandit", marker = "extra == 'dev'", specifier = ">=1.7.6" }, { name = "black", marker = "extra == 'dev'", specifier = ">=24.3.0" }, + { name = "eda-config", directory = "../../packages/config/python" }, { name = "fastapi", specifier = ">=0.109.1" }, { name = "ffmpeg-python", specifier = ">=0.2.0" }, { name = "flake8", marker = "extra == 'dev'", specifier = ">=6.1.0" }, @@ -913,6 +915,21 @@ requires-dist = [ { name = "zep-python", specifier = ">=2.0.2" }, ] +[[package]] +name = "eda-config" +version = "0.1.6" +source = { directory = "../../packages/config/python" } +dependencies = [ + { name = "pydantic" }, + { name = "pyyaml" }, +] + +[package.metadata] +requires-dist = [ + { name = "pydantic", specifier = ">=2.0.0" }, + { name = "pyyaml", specifier = ">=6.0.1" }, +] + [[package]] name = "embedchain" version = "0.1.124" diff --git a/package.json b/package.json index 3968b7c..6b27668 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "lint:repo": "bunx sherif@latest -r unordered-dependencies -r multiple-dependency-versions", "lint:repo:fix": "bunx sherif@latest -r unordered-dependencies -r multiple-dependency-versions --fix", "typecheck": "turbo typecheck", - "build:config": "turbo run build --filter=@eda/config" + "build:config": "turbo run build --filter=@eda/config --filter=@eda/config-python" }, "devDependencies": { "@biomejs/biome": "1.9.4", diff --git a/packages/config/python/eda_config/__init__.py b/packages/config/python/eda_config/__init__.py new file mode 100644 index 0000000..1335b4c --- /dev/null +++ b/packages/config/python/eda_config/__init__.py @@ -0,0 +1,6 @@ +from .config import ConfigLoader +from .types import Config + +config = ConfigLoader.get_config() + +__all__ = ["Config", "config"] \ No newline at end of file diff --git a/packages/config/python/eda_config/config.py b/packages/config/python/eda_config/config.py index c21bd1f..3552d98 100644 --- a/packages/config/python/eda_config/config.py +++ b/packages/config/python/eda_config/config.py @@ -1,187 +1,7 @@ from pathlib import Path -from typing import List, Optional, Union, Dict +from typing import Optional import yaml -from pydantic import BaseModel - -class Neo4jAuth(BaseModel): - user: str - password: str - -class Neo4jHealthcheck(BaseModel): - interval: int - timeout: int - retries: int - -class Neo4jConfig(BaseModel): - host: str - auth: Neo4jAuth - plugins: List[str] - healthcheck: Neo4jHealthcheck - -class RedisConfig(BaseModel): - host: str - port: Optional[int] = None # Make port optional - tls_disabled: bool - -class DatabaseConfig(BaseModel): - host: str - port: Optional[int] = None # Make port optional - user: str - password: str - database: str - -class SupabaseKeys(BaseModel): - service_key: str - anon_key: str - -class OpenPanelKeys(BaseModel): - client_id: str - secret: str - -class SentryKeys(BaseModel): - auth_token: str - -class ApiKeys(BaseModel): - groq: str - huggingface: str - serper: str - langtrace: str - trigger: str - resend: str - openai: str - supabase: SupabaseKeys - openpanel: OpenPanelKeys - dub: str - sentry: SentryKeys - -class DbPorts(BaseModel): - postgres: int - redis: int - neo4j: Dict[str, int] - clickhouse: int - -class Ports(BaseModel): - messaging: int - ai_api: int - langtrace: int - trigger: int - remix: int - whatsapp: int - dashboard: int - landingpage: int - docs: int - db: DbPorts - -class AIModelConfig(BaseModel): - provider: str - model: str - temperature: float - description: str - -class WhatsappReactions(BaseModel): - queued: str - working: str - done: str - error: str - -class WhatsappConfig(BaseModel): - bot_prefix: str - cmd_prefix: str - bot_name: str - enable_reactions: bool - reactions: WhatsappReactions - puppeteer_path: str - ignore_messages_warning: bool - mongodb_uri: str - -class TriggerAuth(BaseModel): - magic_link_secret: str - session_secret: str - encryption_key: str - provider_secret: str - coordinator_secret: str - -class TriggerWorker(BaseModel): - http_port: int - coordinator_host: str - coordinator_port: int - -class TriggerDeployment(BaseModel): - worker: TriggerWorker - docker: Dict[str, str] - -class TriggerConfig(BaseModel): - project_id: str - api_url: str - environment: str - runtime: str - v3_enabled: bool - concurrency: Dict[str, int] - auth: TriggerAuth - deployment: TriggerDeployment - sentry: Dict[str, str] - -class ResendConfig(BaseModel): - from_email: str - reply_to: str - -class LangTraceApiConfig(BaseModel): - host: str - -class LangTraceAdminConfig(BaseModel): - email: str - password: str - enable_login: bool - -class LangTraceTelemetryConfig(BaseModel): - enabled: bool - -class LangTracePosthogConfig(BaseModel): - host: str - -class LangTraceConfig(BaseModel): - api: LangTraceApiConfig - admin: LangTraceAdminConfig - telemetry: LangTraceTelemetryConfig - posthog: LangTracePosthogConfig - -class DashboardAuthAzure(BaseModel): - client_id: str - client_secret: str - tenant_id: str - -class DashboardAuthGoogle(BaseModel): - client_id: str - secret: str - -class DashboardAuth(BaseModel): - nextauth_secret: str - azure: DashboardAuthAzure - google: DashboardAuthGoogle - -class DashboardConfig(BaseModel): - auth: DashboardAuth - -class UpstashConfig(BaseModel): - redis_url: str - redis_token: str - -class ServicesConfig(BaseModel): - ai_api: Dict[str, bool] - whatsapp: WhatsappConfig - trigger: TriggerConfig - resend: ResendConfig - langtrace: LangTraceConfig - dashboard: DashboardConfig - upstash: UpstashConfig - -class Config(BaseModel): - ports: Ports - api_keys: ApiKeys - databases: Dict[str, Union[DatabaseConfig, RedisConfig, Neo4jConfig]] - services: ServicesConfig - ai_models: Dict[str, AIModelConfig] - access: Dict[str, List[str]] +from .types import Config class ConfigLoader: _instance: Optional[Config] = None @@ -189,8 +9,31 @@ class ConfigLoader: @classmethod def get_config(cls) -> Config: if cls._instance is None: - config_path = Path(__file__).parents[4] / "config.yaml" + def find_project_root(start_path: Path) -> Optional[Path]: + """Find project root by looking for config.yaml""" + current = start_path + while current != current.parent: + if (current / "config.yaml").exists(): + return current + current = current.parent + return None + + # Start searching from the current file's directory + start_path = Path(__file__).resolve().parent + project_root = find_project_root(start_path) + + if not project_root: + raise FileNotFoundError( + f"Could not find config.yaml from {start_path}.\n" + f"Ensure config.yaml is located in the project root." + ) + + config_path = project_root / "config.yaml" + print(f"DEBUG: Loading config from {config_path}") # Debugging line + with open(config_path) as f: config_data = yaml.safe_load(f) + print(f"DEBUG: Loaded config data: {config_data}") # Debugging line + cls._instance = Config(**config_data) return cls._instance \ No newline at end of file diff --git a/packages/config/python/eda_config/types.py b/packages/config/python/eda_config/types.py new file mode 100644 index 0000000..de6936a --- /dev/null +++ b/packages/config/python/eda_config/types.py @@ -0,0 +1,196 @@ +from typing import List, Optional, Union, Dict +from pydantic import BaseModel + +class Neo4jAuth(BaseModel): + user: str + password: str + +class Neo4jHealthcheck(BaseModel): + interval: int + timeout: int + retries: int + +class Neo4jConfig(BaseModel): + host: str + auth: Neo4jAuth + plugins: List[str] + healthcheck: Neo4jHealthcheck + +class RedisConfig(BaseModel): + host: str + port: Optional[int] = None # Make port optional + tls_disabled: bool + +class DatabaseConfig(BaseModel): + host: str + port: Optional[int] = None # Make port optional + user: str + password: str + database: str + +class SupabaseKeys(BaseModel): + service_key: str + anon_key: str + +class OpenPanelKeys(BaseModel): + client_id: str + secret: str + +class SentryKeys(BaseModel): + auth_token: str + +class ApiKeys(BaseModel): + groq: str + huggingface: str + serper: str + langtrace: str + trigger: str + resend: str + openai: str + supabase: SupabaseKeys + openpanel: OpenPanelKeys + dub: str + sentry: SentryKeys + +class DbPorts(BaseModel): + postgres: int + redis: int + neo4j: Dict[str, int] + clickhouse: int + +class Ports(BaseModel): + messaging: int + ai_api: int + langtrace: int + trigger: int + remix: int + whatsapp: int + dashboard: int + landingpage: int + docs: int + db: DbPorts + +class AIModelConfig(BaseModel): + provider: str + model: str + temperature: float + description: str + +class WhatsappReactions(BaseModel): + queued: str + working: str + done: str + error: str + +class WhatsappConfig(BaseModel): + bot_prefix: str + cmd_prefix: str + bot_name: str + enable_reactions: bool + reactions: WhatsappReactions + puppeteer_path: str + ignore_messages_warning: bool + mongodb_uri: str + +class TriggerAuth(BaseModel): + magic_link_secret: str + session_secret: str + encryption_key: str + provider_secret: str + coordinator_secret: str + +class TriggerWorker(BaseModel): + http_port: int + coordinator_host: str + coordinator_port: int + +class TriggerDeployment(BaseModel): + worker: TriggerWorker + docker: Dict[str, str] + +class TriggerConfig(BaseModel): + project_id: str + api_url: str + environment: str + runtime: str + v3_enabled: bool + concurrency: Dict[str, int] + auth: TriggerAuth + deployment: TriggerDeployment + sentry: Dict[str, str] + +class ResendConfig(BaseModel): + from_email: str + reply_to: str + +class LangTraceApiConfig(BaseModel): + host: str + +class LangTraceAdminConfig(BaseModel): + email: str + password: str + enable_login: bool + +class LangTraceTelemetryConfig(BaseModel): + enabled: bool + +class LangTracePosthogConfig(BaseModel): + host: str + +class LangTraceConfig(BaseModel): + api: LangTraceApiConfig + admin: LangTraceAdminConfig + telemetry: LangTraceTelemetryConfig + posthog: LangTracePosthogConfig + +class DashboardAuthAzure(BaseModel): + client_id: str + client_secret: str + tenant_id: str + +class DashboardAuthGoogle(BaseModel): + client_id: str + secret: str + +class DashboardAuth(BaseModel): + nextauth_secret: str + azure: DashboardAuthAzure + google: DashboardAuthGoogle + +class DashboardConfig(BaseModel): + auth: DashboardAuth + +class UpstashConfig(BaseModel): + redis_url: str + redis_token: str + +class ServicesConfig(BaseModel): + ai_api: Dict[str, bool] + whatsapp: WhatsappConfig + trigger: TriggerConfig + resend: ResendConfig + langtrace: LangTraceConfig + dashboard: DashboardConfig + upstash: UpstashConfig + +# Add this new type for Supabase config +class SupabaseDbConfig(BaseModel): + url: str + project_id: str + +class DatabasesConfig(BaseModel): + supabase: SupabaseDbConfig + langtrace_postgres: DatabaseConfig + langtrace_clickhouse: DatabaseConfig + trigger_postgres: DatabaseConfig + redis: RedisConfig + neo4j: Neo4jConfig + +# Update the main Config class to use the new DatabasesConfig +class Config(BaseModel): + ports: Ports + api_keys: ApiKeys + databases: DatabasesConfig # Changed from Dict to DatabasesConfig + services: ServicesConfig + ai_models: Dict[str, AIModelConfig] + access: Dict[str, List[str]] \ No newline at end of file diff --git a/packages/config/python/package.json b/packages/config/python/package.json index 32cb961..710c3ad 100644 --- a/packages/config/python/package.json +++ b/packages/config/python/package.json @@ -2,6 +2,7 @@ "name": "@eda/config-python", "private": true, "scripts": { - "build": "uv pip install -e ." + "build": "bun run bump && uv venv && . .venv/bin/activate && uv pip install -e .", + "bump": "bun ../scripts/bump-version.ts" } } diff --git a/packages/config/python/pyproject.toml b/packages/config/python/pyproject.toml index 71a94f7..9fba146 100644 --- a/packages/config/python/pyproject.toml +++ b/packages/config/python/pyproject.toml @@ -1,6 +1,8 @@ [project] name = "eda-config" -version = "0.1.0" +version = "0.1.7" +description = "Configuration management for EDA" +requires-python = ">=3.11" dependencies = [ "pyyaml>=6.0.1", "pydantic>=2.0.0" diff --git a/packages/config/scripts/bump-version.ts b/packages/config/scripts/bump-version.ts new file mode 100644 index 0000000..f21b333 --- /dev/null +++ b/packages/config/scripts/bump-version.ts @@ -0,0 +1,56 @@ +import { readFileSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; + +function bumpVersion(version: string): string { + const parts = version.split(".").map(Number); + if (parts.length !== 3 || parts.some(Number.isNaN)) { + throw new Error(`Invalid version format: ${version}`); + } + const [major, minor, patch] = parts as [number, number, number]; + return `${major}.${minor}.${patch + 1}`; +} + +function updatePythonVersion() { + const pyprojectPath = join(__dirname, "../python/pyproject.toml"); + const content = readFileSync(pyprojectPath, "utf8"); + const versionMatch = content.match(/version = "(\d+\.\d+\.\d+)"/); + + if (!versionMatch?.[1]) { + throw new Error("Could not find valid version in pyproject.toml"); + } + + const newVersion = bumpVersion(versionMatch[1]); + const newContent = content.replace( + /version = "(\d+\.\d+\.\d+)"/, + `version = "${newVersion}"`, + ); + writeFileSync(pyprojectPath, newContent); + return newVersion; +} + +function updateTypeScriptVersion() { + const packagePath = join(__dirname, "../typescript/package.json"); + const pkg = JSON.parse(readFileSync(packagePath, "utf8")) as { + version: string; + }; + + if (!pkg.version) { + throw new Error("No version field found in package.json"); + } + + const newVersion = bumpVersion(pkg.version); + pkg.version = newVersion; + writeFileSync(packagePath, `${JSON.stringify(pkg, null, 2)}\n`); + return newVersion; +} + +try { + const pyVersion = updatePythonVersion(); + const tsVersion = updateTypeScriptVersion(); + console.log("βœ… Versions bumped successfully:"); + console.log(`Python: ${pyVersion}`); + console.log(`TypeScript: ${tsVersion}`); +} catch (error) { + console.error("❌ Failed to bump versions:", error); + process.exit(1); +} diff --git a/packages/config/typescript/package.json b/packages/config/typescript/package.json index 233b2c1..d656ab5 100644 --- a/packages/config/typescript/package.json +++ b/packages/config/typescript/package.json @@ -1,11 +1,12 @@ { "name": "@eda/config", - "version": "0.1.0", + "version": "0.1.1", "main": "dist/config.js", "types": "dist/config.d.ts", "type": "module", "scripts": { - "build": "tsc && bun build ./src/config.ts --outdir ./dist --target node", + "bump": "bun ../scripts/bump-version.ts", + "build": "bun run bump && tsc && bun build ./src/config.ts --outdir ./dist --target node", "typecheck": "tsc --noEmit" }, "dependencies": { diff --git a/turbo.json b/turbo.json index da7695c..426dde7 100644 --- a/turbo.json +++ b/turbo.json @@ -66,9 +66,13 @@ }, "build:config": { "dependsOn": ["^build"], + "inputs": ["config.yaml", "packages/config/**/src/**"], "outputs": [ "packages/config/typescript/dist/**", - "packages/config/python/dist/**" + "packages/config/python/.venv/**", + "packages/config/python/config.yaml", + "packages/config/typescript/package.json", + "packages/config/python/pyproject.toml" ] } } From c797d76e79e3d618fc72838c11db5d047985b9b7 Mon Sep 17 00:00:00 2001 From: Luis Otavio Date: Tue, 21 Jan 2025 01:44:28 -0300 Subject: [PATCH 37/75] Python package cleanup --- apps/ai_api/eda_ai_api/api/routes/classifier.py | 2 -- apps/ai_api/package.json | 2 +- packages/config/python/eda_config/config.py | 2 -- packages/config/python/pyproject.toml | 2 +- packages/config/typescript/package.json | 2 +- 5 files changed, 3 insertions(+), 7 deletions(-) diff --git a/apps/ai_api/eda_ai_api/api/routes/classifier.py b/apps/ai_api/eda_ai_api/api/routes/classifier.py index e5a3003..da647a8 100644 --- a/apps/ai_api/eda_ai_api/api/routes/classifier.py +++ b/apps/ai_api/eda_ai_api/api/routes/classifier.py @@ -21,8 +21,6 @@ config = ConfigLoader.get_config() # Add this line -print(config.services.ai_api) - router = APIRouter() # Disabled Mem0 memory manager # mem0_manager = Mem0ConversationManager() diff --git a/apps/ai_api/package.json b/apps/ai_api/package.json index 4da52f8..ed8d6f9 100644 --- a/apps/ai_api/package.json +++ b/apps/ai_api/package.json @@ -2,7 +2,7 @@ "name": "@eda/ai-api", "version": "0.0.1", "scripts": { - "dev": "uv venv && . .venv/bin/activate && uv pip install -e . && uv run uvicorn --reload eda_ai_api.main:app --port 8083", + "dev": "uv run uvicorn --reload eda_ai_api.main:app --port 8083", "start": "uv run uvicorn eda_ai_api.main:app", "lint": "bash ./scripts/linting.sh", "test": "bash ./scripts/test.sh" diff --git a/packages/config/python/eda_config/config.py b/packages/config/python/eda_config/config.py index 3552d98..9cb26a1 100644 --- a/packages/config/python/eda_config/config.py +++ b/packages/config/python/eda_config/config.py @@ -29,11 +29,9 @@ def find_project_root(start_path: Path) -> Optional[Path]: ) config_path = project_root / "config.yaml" - print(f"DEBUG: Loading config from {config_path}") # Debugging line with open(config_path) as f: config_data = yaml.safe_load(f) - print(f"DEBUG: Loaded config data: {config_data}") # Debugging line cls._instance = Config(**config_data) return cls._instance \ No newline at end of file diff --git a/packages/config/python/pyproject.toml b/packages/config/python/pyproject.toml index 9fba146..6add981 100644 --- a/packages/config/python/pyproject.toml +++ b/packages/config/python/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "eda-config" -version = "0.1.7" +version = "0.1.10" description = "Configuration management for EDA" requires-python = ">=3.11" dependencies = [ diff --git a/packages/config/typescript/package.json b/packages/config/typescript/package.json index d656ab5..e48432d 100644 --- a/packages/config/typescript/package.json +++ b/packages/config/typescript/package.json @@ -1,6 +1,6 @@ { "name": "@eda/config", - "version": "0.1.1", + "version": "0.1.4", "main": "dist/config.js", "types": "dist/config.d.ts", "type": "module", From 9e71e0e1bf2f6861d10cd7084f4ffb04ffb5ae21 Mon Sep 17 00:00:00 2001 From: Luis Otavio Date: Tue, 21 Jan 2025 02:30:50 -0300 Subject: [PATCH 38/75] Replace some .env with config --- apps/ai_api/.env.example | 7 --- .../eda_ai_api/api/routes/classifier.py | 11 ++--- apps/ai_api/eda_ai_api/core/config.py | 5 --- apps/ai_api/eda_ai_api/main.py | 25 +++++++++-- apps/ai_api/package.json | 4 +- config.example.yaml | 2 +- packages/config/python/eda_config/types.py | 43 ++++++++++++++++++- packages/config/python/pyproject.toml | 2 +- packages/config/typescript/package.json | 2 +- .../src/opportunity_finder/crew.py | 15 ++++--- .../src/opportunity_finder/main.py | 43 +++++++++++-------- .../src/proposal_writer/crew.py | 12 ++++-- .../src/proposal_writer/main.py | 39 ++++++++++------- plugins/onboarding/src/onboarding/crew.py | 12 ++++-- 14 files changed, 149 insertions(+), 73 deletions(-) delete mode 100644 apps/ai_api/.env.example diff --git a/apps/ai_api/.env.example b/apps/ai_api/.env.example deleted file mode 100644 index 86ac70e..0000000 --- a/apps/ai_api/.env.example +++ /dev/null @@ -1,7 +0,0 @@ -IS_DEBUG=True -API_KEY=sample_api_key # NOT USED -OPENAI_API_KEY=sk-proj-xxxx # NOT USED -CEREBRAS_API_KEY=csk-xxxx # NOT USED -SERPER_API_KEY=xxxx # Needed to use Serper tool -LANGTRACE_API_KEY=xxxx -GROQ_API_KEY=xxxx # API key for the LLM model \ No newline at end of file diff --git a/apps/ai_api/eda_ai_api/api/routes/classifier.py b/apps/ai_api/eda_ai_api/api/routes/classifier.py index da647a8..a7ecbff 100644 --- a/apps/ai_api/eda_ai_api/api/routes/classifier.py +++ b/apps/ai_api/eda_ai_api/api/routes/classifier.py @@ -25,11 +25,11 @@ # Disabled Mem0 memory manager # mem0_manager = Mem0ConversationManager() -# Setup LLM +# Update LLM configuration llm = ChatGroq( - model="llama-3.3-70b-versatile", - api_key=os.environ.get("GROQ_API_KEY"), # Use environment variable - temperature=0.5, + model=config.ai_models["premium"].model, + api_key=config.api_keys.groq, + temperature=config.ai_models["premium"].temperature, ) @@ -52,8 +52,9 @@ async def process_discovery( async with httpx.AsyncClient() as client: logger.info("Calling discovery API...") + discovery_port = config.ports.ai_api api_response = await client.post( - "http://127.0.0.1:8083/api/grant/discovery", + f"http://127.0.0.1:{discovery_port}/api/grant/discovery", json={"topics": topics, "platform": platform}, ) logger.info(f"Discovery API Response: {api_response.json()}") diff --git a/apps/ai_api/eda_ai_api/core/config.py b/apps/ai_api/eda_ai_api/core/config.py index e1fa3a8..4aec1d6 100644 --- a/apps/ai_api/eda_ai_api/core/config.py +++ b/apps/ai_api/eda_ai_api/core/config.py @@ -4,8 +4,3 @@ APP_VERSION = "0.0.1" APP_NAME = "AI API for Earth Defenders Assistant" API_PREFIX = "/api" - -config = Config(".env") - -API_KEY: Secret = config("API_KEY", cast=Secret) -IS_DEBUG: bool = config("IS_DEBUG", cast=bool, default=False) diff --git a/apps/ai_api/eda_ai_api/main.py b/apps/ai_api/eda_ai_api/main.py index 2efa4d9..47acaf2 100644 --- a/apps/ai_api/eda_ai_api/main.py +++ b/apps/ai_api/eda_ai_api/main.py @@ -1,12 +1,18 @@ from fastapi import FastAPI - +from eda_config import ConfigLoader from eda_ai_api.api.routes.router import api_router -from eda_ai_api.core.config import API_PREFIX, APP_NAME, APP_VERSION, IS_DEBUG +from eda_ai_api.core.config import API_PREFIX, APP_NAME, APP_VERSION from eda_ai_api.core.event_handlers import start_app_handler, stop_app_handler +config = ConfigLoader.get_config() + def get_app() -> FastAPI: - fast_app = FastAPI(title=APP_NAME, version=APP_VERSION, debug=IS_DEBUG) + fast_app = FastAPI( + title=APP_NAME, + version=APP_VERSION, + debug=config.services.ai_api.debug, + ) fast_app.include_router(api_router, prefix=API_PREFIX) fast_app.add_event_handler("startup", start_app_handler(fast_app)) @@ -16,3 +22,16 @@ def get_app() -> FastAPI: app = get_app() + + +def run_server(): + """Run the API server using config settings""" + import uvicorn + + uvicorn.run( + "eda_ai_api.main:app", host="0.0.0.0", port=config.ports.ai_api, reload=True + ) + + +if __name__ == "__main__": + run_server() diff --git a/apps/ai_api/package.json b/apps/ai_api/package.json index ed8d6f9..c46f123 100644 --- a/apps/ai_api/package.json +++ b/apps/ai_api/package.json @@ -2,8 +2,8 @@ "name": "@eda/ai-api", "version": "0.0.1", "scripts": { - "dev": "uv run uvicorn --reload eda_ai_api.main:app --port 8083", - "start": "uv run uvicorn eda_ai_api.main:app", + "dev": "uv run python -m eda_ai_api.main", + "start": "uv run python -m eda_ai_api.main", "lint": "bash ./scripts/linting.sh", "test": "bash ./scripts/test.sh" } diff --git a/config.example.yaml b/config.example.yaml index bd21336..221b1ec 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -1,7 +1,7 @@ # Service Ports (all port configurations in one place) ports: messaging: 3001 # WhatsApp message handling service - ai_api: 8000 # FastAPI service for AI operations + ai_api: 8083 # FastAPI service for AI operations langtrace: 3003 # LangTrace monitoring service trigger: 3040 # Trigger.dev job processing remix: 3030 # Trigger.dev remix app diff --git a/packages/config/python/eda_config/types.py b/packages/config/python/eda_config/types.py index de6936a..170887a 100644 --- a/packages/config/python/eda_config/types.py +++ b/packages/config/python/eda_config/types.py @@ -1,26 +1,31 @@ from typing import List, Optional, Union, Dict from pydantic import BaseModel + class Neo4jAuth(BaseModel): user: str password: str + class Neo4jHealthcheck(BaseModel): interval: int timeout: int retries: int + class Neo4jConfig(BaseModel): host: str auth: Neo4jAuth plugins: List[str] healthcheck: Neo4jHealthcheck + class RedisConfig(BaseModel): host: str port: Optional[int] = None # Make port optional tls_disabled: bool + class DatabaseConfig(BaseModel): host: str port: Optional[int] = None # Make port optional @@ -28,17 +33,21 @@ class DatabaseConfig(BaseModel): password: str database: str + class SupabaseKeys(BaseModel): service_key: str anon_key: str + class OpenPanelKeys(BaseModel): client_id: str secret: str + class SentryKeys(BaseModel): auth_token: str + class ApiKeys(BaseModel): groq: str huggingface: str @@ -52,12 +61,14 @@ class ApiKeys(BaseModel): dub: str sentry: SentryKeys + class DbPorts(BaseModel): postgres: int redis: int neo4j: Dict[str, int] clickhouse: int + class Ports(BaseModel): messaging: int ai_api: int @@ -70,18 +81,21 @@ class Ports(BaseModel): docs: int db: DbPorts + class AIModelConfig(BaseModel): provider: str model: str temperature: float description: str + class WhatsappReactions(BaseModel): queued: str working: str done: str error: str + class WhatsappConfig(BaseModel): bot_prefix: str cmd_prefix: str @@ -92,6 +106,7 @@ class WhatsappConfig(BaseModel): ignore_messages_warning: bool mongodb_uri: str + class TriggerAuth(BaseModel): magic_link_secret: str session_secret: str @@ -99,15 +114,18 @@ class TriggerAuth(BaseModel): provider_secret: str coordinator_secret: str + class TriggerWorker(BaseModel): http_port: int coordinator_host: str coordinator_port: int + class TriggerDeployment(BaseModel): worker: TriggerWorker docker: Dict[str, str] + class TriggerConfig(BaseModel): project_id: str api_url: str @@ -119,53 +137,71 @@ class TriggerConfig(BaseModel): deployment: TriggerDeployment sentry: Dict[str, str] + class ResendConfig(BaseModel): from_email: str reply_to: str + class LangTraceApiConfig(BaseModel): host: str + class LangTraceAdminConfig(BaseModel): email: str password: str enable_login: bool + class LangTraceTelemetryConfig(BaseModel): enabled: bool + class LangTracePosthogConfig(BaseModel): host: str + class LangTraceConfig(BaseModel): api: LangTraceApiConfig admin: LangTraceAdminConfig telemetry: LangTraceTelemetryConfig posthog: LangTracePosthogConfig + class DashboardAuthAzure(BaseModel): client_id: str client_secret: str tenant_id: str + class DashboardAuthGoogle(BaseModel): client_id: str secret: str + class DashboardAuth(BaseModel): nextauth_secret: str azure: DashboardAuthAzure google: DashboardAuthGoogle + class DashboardConfig(BaseModel): auth: DashboardAuth + class UpstashConfig(BaseModel): redis_url: str redis_token: str + +# Update AIApiConfig class +class AIApiConfig(BaseModel): + debug: bool + + +# Update ServicesConfig class class ServicesConfig(BaseModel): - ai_api: Dict[str, bool] + ai_api: AIApiConfig # Change from Dict to AIApiConfig whatsapp: WhatsappConfig trigger: TriggerConfig resend: ResendConfig @@ -173,11 +209,13 @@ class ServicesConfig(BaseModel): dashboard: DashboardConfig upstash: UpstashConfig + # Add this new type for Supabase config class SupabaseDbConfig(BaseModel): url: str project_id: str + class DatabasesConfig(BaseModel): supabase: SupabaseDbConfig langtrace_postgres: DatabaseConfig @@ -186,6 +224,7 @@ class DatabasesConfig(BaseModel): redis: RedisConfig neo4j: Neo4jConfig + # Update the main Config class to use the new DatabasesConfig class Config(BaseModel): ports: Ports @@ -193,4 +232,4 @@ class Config(BaseModel): databases: DatabasesConfig # Changed from Dict to DatabasesConfig services: ServicesConfig ai_models: Dict[str, AIModelConfig] - access: Dict[str, List[str]] \ No newline at end of file + access: Dict[str, List[str]] diff --git a/packages/config/python/pyproject.toml b/packages/config/python/pyproject.toml index 6add981..33e5fe7 100644 --- a/packages/config/python/pyproject.toml +++ b/packages/config/python/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "eda-config" -version = "0.1.10" +version = "0.1.13" description = "Configuration management for EDA" requires-python = ">=3.11" dependencies = [ diff --git a/packages/config/typescript/package.json b/packages/config/typescript/package.json index e48432d..a83bcb6 100644 --- a/packages/config/typescript/package.json +++ b/packages/config/typescript/package.json @@ -1,6 +1,6 @@ { "name": "@eda/config", - "version": "0.1.4", + "version": "0.1.8", "main": "dist/config.js", "types": "dist/config.d.ts", "type": "module", diff --git a/plugins/grant_plugin/opportunity_finder/src/opportunity_finder/crew.py b/plugins/grant_plugin/opportunity_finder/src/opportunity_finder/crew.py index 8e1d7bc..271350d 100644 --- a/plugins/grant_plugin/opportunity_finder/src/opportunity_finder/crew.py +++ b/plugins/grant_plugin/opportunity_finder/src/opportunity_finder/crew.py @@ -1,15 +1,20 @@ -import os import datetime +import os +from eda_config import ConfigLoader from crewai import Agent, Crew, Process, Task, LLM from crewai.project import CrewBase, agent, crew, task from crewai_tools import ScrapeWebsiteTool, SerperDevTool -search_tool = SerperDevTool() +# Get config +config = ConfigLoader.get_config() + +search_tool = SerperDevTool(api_key=config.api_keys.serper) scrape_tool = ScrapeWebsiteTool() + llm = LLM( - model="groq/llama-3.3-70b-versatile", # Replace with your chosen Cerebras model name, e.g., "cerebras/llama3.1-8b" - api_key=os.environ.get("GROQ_API_KEY"), # Your Cerebras API key - temperature=0.5, + model=config.ai_models["premium"].model, + api_key=config.api_keys.groq, + temperature=config.ai_models["premium"].temperature, ) diff --git a/plugins/grant_plugin/opportunity_finder/src/opportunity_finder/main.py b/plugins/grant_plugin/opportunity_finder/src/opportunity_finder/main.py index db13864..a14ab5b 100644 --- a/plugins/grant_plugin/opportunity_finder/src/opportunity_finder/main.py +++ b/plugins/grant_plugin/opportunity_finder/src/opportunity_finder/main.py @@ -1,55 +1,61 @@ #!/usr/bin/env python import os import sys +from eda_config import ConfigLoader from opportunity_finder.crew import OpportunityFinderCrew from langtrace_python_sdk import langtrace + +# Get config +config = ConfigLoader.get_config() + + # This main file is intended to be a way for you to run your # crew locally, so refrain from adding unnecessary logic into this file. # Replace with inputs you want to test with, it will automatically # interpolate any tasks and agents information def init_langtrace(): - """Initialize langtrace with environment variables.""" + """Initialize langtrace with config values.""" langtrace.init( - api_key=os.environ.get('LANGTRACE_API_KEY'), - api_host=os.environ.get('LANGTRACE_API_HOST', 'http://localhost:3000/api/trace'), + api_key=config.api_keys.langtrace, + api_host=config.services.langtrace.api.host, ) + def run(): """ Run the crew with default topics. """ init_langtrace() - inputs = { - 'topics': 'Climate Justice, Indigenous Land Defense, Sociobiodiveristy' - } + inputs = {"topics": "Climate Justice, Indigenous Land Defense, Sociobiodiveristy"} OpportunityFinderCrew().crew().kickoff(inputs=inputs) + def main(topics: str): """ Run the crew with specified topics. - + Args: topics: Comma-separated string of topics to search for opportunities """ init_langtrace() - inputs = { - 'topics': topics - } + inputs = {"topics": topics} return OpportunityFinderCrew().crew().kickoff(inputs=inputs) + def train(): """ Train the crew for a given number of iterations. """ - inputs = { - 'topics': 'Climate Justice, Indigenous Land Defense, Sociobiodiveristy' - } + inputs = {"topics": "Climate Justice, Indigenous Land Defense, Sociobiodiveristy"} try: - OpportunityFinderCrew().crew().train(n_iterations=int(sys.argv[1]), filename=sys.argv[2], inputs=inputs) + OpportunityFinderCrew().crew().train( + n_iterations=int(sys.argv[1]), filename=sys.argv[2], inputs=inputs + ) except Exception as e: raise Exception(f"An error occurred while training the crew: {e}") + def replay(): """ Replay the crew execution from a specific task. @@ -60,15 +66,16 @@ def replay(): except Exception as e: raise Exception(f"An error occurred while replaying the crew: {e}") + def test(): """ Test the crew execution and returns the results. """ - inputs = { - 'topics': 'Climate Justice, Indigenous Land Defense, Sociobiodiveristy' - } + inputs = {"topics": "Climate Justice, Indigenous Land Defense, Sociobiodiveristy"} try: - OpportunityFinderCrew().crew().test(n_iterations=int(sys.argv[1]), openai_model_name=sys.argv[2], inputs=inputs) + OpportunityFinderCrew().crew().test( + n_iterations=int(sys.argv[1]), openai_model_name=sys.argv[2], inputs=inputs + ) except Exception as e: raise Exception(f"An error occurred while replaying the crew: {e}") diff --git a/plugins/grant_plugin/proposal_writer/src/proposal_writer/crew.py b/plugins/grant_plugin/proposal_writer/src/proposal_writer/crew.py index 1d36402..5727c72 100644 --- a/plugins/grant_plugin/proposal_writer/src/proposal_writer/crew.py +++ b/plugins/grant_plugin/proposal_writer/src/proposal_writer/crew.py @@ -1,14 +1,18 @@ import datetime import os from typing import List +from eda_config import ConfigLoader from crewai import LLM, Agent, Crew, Process, Task from crewai.project import CrewBase, agent, crew, task -# Initialize LLM +# Get config +config = ConfigLoader.get_config() + +# Initialize LLM using config llm = LLM( - model="groq/llama-3.3-70b-versatile", # Replace with your chosen Cerebras model name, e.g., "cerebras/llama3.1-8b" - api_key=os.environ.get("GROQ_API_KEY"), # Your Cerebras API key - temperature=0.5, + model=config.ai_models["premium"].model, + api_key=config.api_keys.groq, + temperature=config.ai_models["premium"].temperature, ) diff --git a/plugins/grant_plugin/proposal_writer/src/proposal_writer/main.py b/plugins/grant_plugin/proposal_writer/src/proposal_writer/main.py index fd1b915..f58b7ba 100644 --- a/plugins/grant_plugin/proposal_writer/src/proposal_writer/main.py +++ b/plugins/grant_plugin/proposal_writer/src/proposal_writer/main.py @@ -2,28 +2,35 @@ import os import sys import json +from eda_config import ConfigLoader from proposal_writer.crew import ProposalWriterCrew from langtrace_python_sdk import langtrace +# Get config +config = ConfigLoader.get_config() + # This main file is intended to be a way for you to run your # crew locally, so refrain from adding unnecessary logic into this file. # Replace with inputs you want to test with, it will automatically # interpolate any tasks and agents information + def run(): """ Run the crew. """ - langtrace.init(api_key=os.environ.get('LANGTRACE_API_KEY')) + langtrace.init(api_key=config.api_keys.langtrace) print("Running the ProposalWriterCrew") - with open('tests/project_example_cache.json', 'r', encoding='utf-8') as f: + with open("tests/project_example_cache.json", "r", encoding="utf-8") as f: project = json.loads(f.read()) - with open('tests/teia.json', 'r', encoding='utf-8') as f: + with open("tests/teia.json", "r", encoding="utf-8") as f: grant = json.loads(f.read()) - inputs = [{ - 'community_project': project, # Details about the community project/initiative - 'grant_call': grant, # Information about the grant/funding opportunity - }] + inputs = [ + { + "community_project": project, # Details about the community project/initiative + "grant_call": grant, # Information about the grant/funding opportunity + } + ] print("inputs:", inputs) crew = ProposalWriterCrew(community_project=project, grant_call=grant) crew.crew().kickoff(inputs=inputs) @@ -33,15 +40,16 @@ def train(): """ Train the crew for a given number of iterations. """ - inputs = { - "topic": "AI LLMs" - } + inputs = {"topic": "AI LLMs"} try: - ProposalWriterCrew().crew().train(n_iterations=int(sys.argv[1]), filename=sys.argv[2], inputs=inputs) + ProposalWriterCrew().crew().train( + n_iterations=int(sys.argv[1]), filename=sys.argv[2], inputs=inputs + ) except Exception as e: raise Exception(f"An error occurred while training the crew: {e}") + def replay(): """ Replay the crew execution from a specific task. @@ -52,15 +60,16 @@ def replay(): except Exception as e: raise Exception(f"An error occurred while replaying the crew: {e}") + def test(): """ Test the crew execution and returns the results. """ - inputs = { - "topic": "AI LLMs" - } + inputs = {"topic": "AI LLMs"} try: - ProposalWriterCrew().crew().test(n_iterations=int(sys.argv[1]), openai_model_name=sys.argv[2], inputs=inputs) + ProposalWriterCrew().crew().test( + n_iterations=int(sys.argv[1]), openai_model_name=sys.argv[2], inputs=inputs + ) except Exception as e: raise Exception(f"An error occurred while replaying the crew: {e}") diff --git a/plugins/onboarding/src/onboarding/crew.py b/plugins/onboarding/src/onboarding/crew.py index c894407..a07ba80 100644 --- a/plugins/onboarding/src/onboarding/crew.py +++ b/plugins/onboarding/src/onboarding/crew.py @@ -1,12 +1,16 @@ import os +from eda_config import ConfigLoader from crewai import Agent, Crew, Process, Task, LLM from crewai.project import CrewBase, agent, crew, task -# Configure LLM +# Get config +config = ConfigLoader.get_config() + +# Configure LLM using config settings llm = LLM( - model="groq/llama-3.3-70b-versatile", - api_key=os.environ.get("GROQ_API_KEY"), - temperature=0.5, + model=config.ai_models["premium"].model, + api_key=config.api_keys.groq, + temperature=config.ai_models["premium"].temperature, ) From ccb0577962b3fa2dc17a18ad3b2a6aa8f047a04a Mon Sep 17 00:00:00 2001 From: Luis Otavio Date: Tue, 21 Jan 2025 02:37:18 -0300 Subject: [PATCH 39/75] Update Astro configuration to use dynamic server port from config --- apps/docs/astro.config.mjs | 7 ++++++- apps/docs/package.json | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/apps/docs/astro.config.mjs b/apps/docs/astro.config.mjs index 99b38e6..38bc15d 100644 --- a/apps/docs/astro.config.mjs +++ b/apps/docs/astro.config.mjs @@ -1,9 +1,14 @@ +import starlight from "@astrojs/starlight"; +import { config } from "@eda/config"; // @ts-check import { defineConfig } from "astro/config"; -import starlight from "@astrojs/starlight"; // https://astro.build/config export default defineConfig({ + server: { + port: config.ports.docs, + host: false, + }, integrations: [ starlight({ title: "My Docs", diff --git a/apps/docs/package.json b/apps/docs/package.json index 3ac9526..8d5d1e5 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -3,7 +3,7 @@ "type": "module", "version": "0.0.1", "scripts": { - "dev": "astro dev --port 8082", + "dev": "astro dev", "start": "astro dev", "build": "astro check && astro build", "preview": "astro preview", From d9be9c4ffadc9c71b472426c42574874021420c5 Mon Sep 17 00:00:00 2001 From: Luis Otavio Date: Tue, 21 Jan 2025 02:55:09 -0300 Subject: [PATCH 40/75] Removed env from dashboard --- apps/dashboard/.env.example | 24 ------------------ apps/dashboard/next.config.mjs | 2 ++ apps/dashboard/package.json | 5 ++-- apps/dashboard/src/env.mjs | 44 ++++++++++++++++----------------- apps/dashboard/src/server.ts | 28 +++++++++++++++++++++ bun.lockb | Bin 722024 -> 732240 bytes 6 files changed, 54 insertions(+), 49 deletions(-) delete mode 100644 apps/dashboard/.env.example create mode 100644 apps/dashboard/src/server.ts diff --git a/apps/dashboard/.env.example b/apps/dashboard/.env.example deleted file mode 100644 index a31ad30..0000000 --- a/apps/dashboard/.env.example +++ /dev/null @@ -1,24 +0,0 @@ -# Supabase -NEXT_PUBLIC_SUPABASE_URL=http://127.0.0.1:54321 -NEXT_PUBLIC_SUPABASE_ANON_KEY= -SUPABASE_SERVICE_KEY= - -# Resend -RESEND_API_KEY= - -# Upstash -UPSTASH_REDIS_REST_URL= -UPSTASH_REDIS_REST_TOKEN= - -# Dub -DUB_API_KEY= - -# OpenPanel -NEXT_PUBLIC_OPENPANEL_CLIENT_ID= -OPENPANEL_SECRET_KEY= - -# Sentry -SENTRY_AUTH_TOKEN= -NEXT_PUBLIC_SENTRY_DSN= -SENTRY_ORG= -SENTRY_PROJECT= \ No newline at end of file diff --git a/apps/dashboard/next.config.mjs b/apps/dashboard/next.config.mjs index 7b8c51a..d586cbb 100644 --- a/apps/dashboard/next.config.mjs +++ b/apps/dashboard/next.config.mjs @@ -1,4 +1,5 @@ import "./src/env.mjs"; +import { config } from "@eda/config"; import { withSentryConfig } from "@sentry/nextjs"; /** @type {import('next').NextConfig} */ @@ -16,4 +17,5 @@ export default withSentryConfig(nextConfig, { hideSourceMaps: true, disableLogger: true, tunnelRoute: "/monitoring", + authToken: config.api_keys.sentry.auth_token, }); diff --git a/apps/dashboard/package.json b/apps/dashboard/package.json index 32c8c1a..be42767 100644 --- a/apps/dashboard/package.json +++ b/apps/dashboard/package.json @@ -2,12 +2,12 @@ "name": "@eda/dashboard", "version": "0.1.0", "scripts": { - "dev": "next dev -p 8080", + "dev": "tsx src/server.ts", "build": "next build", "clean": "git clean -xdf .next .turbo node_modules", "lint": "biome lint", "format": "biome format --write .", - "start": "next start", + "start": "NODE_ENV=production tsx src/server.ts", "typecheck": "tsc-files --noEmit" }, "dependencies": { @@ -24,6 +24,7 @@ "nuqs": "^1.18.0", "react": "^18.3.1", "react-dom": "^18.3.1", + "tsx": "^4.19.2", "zod": "^3.23.8" }, "devDependencies": { diff --git a/apps/dashboard/src/env.mjs b/apps/dashboard/src/env.mjs index 00c2241..58d5c96 100644 --- a/apps/dashboard/src/env.mjs +++ b/apps/dashboard/src/env.mjs @@ -1,3 +1,4 @@ +import { config } from "@eda/config"; import { createEnv } from "@t3-oss/env-nextjs"; import { z } from "zod"; @@ -7,33 +8,30 @@ export const env = createEnv({ .string() .optional() .transform((v) => (v ? `https://${v}` : undefined)), - PORT: z.coerce.number().default(3000), + PORT: z.coerce.number().default(config.ports.dashboard), }, server: { - OPENPANEL_SECRET_KEY: z.string(), - RESEND_API_KEY: z.string(), - SUPABASE_SERVICE_KEY: z.string(), - UPSTASH_REDIS_REST_TOKEN: z.string(), - UPSTASH_REDIS_REST_URL: z.string(), + OPENPANEL_SECRET_KEY: z.string().default(config.api_keys.openpanel.secret), + RESEND_API_KEY: z.string().default(config.api_keys.resend), + SUPABASE_SERVICE_KEY: z + .string() + .default(config.api_keys.supabase.service_key), + UPSTASH_REDIS_REST_TOKEN: z + .string() + .default(config.services.upstash.redis_token), + UPSTASH_REDIS_REST_URL: z + .string() + .default(config.services.upstash.redis_url), }, client: { - NEXT_PUBLIC_OPENPANEL_CLIENT_ID: z.string(), - NEXT_PUBLIC_SUPABASE_ANON_KEY: z.string(), - NEXT_PUBLIC_SUPABASE_URL: z.string(), - }, - runtimeEnv: { - NEXT_PUBLIC_OPENPANEL_CLIENT_ID: - process.env.NEXT_PUBLIC_OPENPANEL_CLIENT_ID, - NEXT_PUBLIC_SENTRY_DSN: process.env.NEXT_PUBLIC_SENTRY_DSN, - NEXT_PUBLIC_SUPABASE_ANON_KEY: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY, - NEXT_PUBLIC_SUPABASE_URL: process.env.NEXT_PUBLIC_SUPABASE_URL, - OPENPANEL_SECRET_KEY: process.env.OPENPANEL_SECRET_KEY, - PORT: process.env.PORT, - RESEND_API_KEY: process.env.RESEND_API_KEY, - SUPABASE_SERVICE_KEY: process.env.SUPABASE_SERVICE_KEY, - UPSTASH_REDIS_REST_TOKEN: process.env.UPSTASH_REDIS_REST_TOKEN, - UPSTASH_REDIS_REST_URL: process.env.UPSTASH_REDIS_REST_URL, - VERCEL_URL: process.env.VERCEL_URL, + NEXT_PUBLIC_OPENPANEL_CLIENT_ID: z + .string() + .default(config.api_keys.openpanel.client_id), + NEXT_PUBLIC_SUPABASE_ANON_KEY: z + .string() + .default(config.api_keys.supabase.anon_key), + NEXT_PUBLIC_SUPABASE_URL: z.string().default(config.databases.supabase.url), }, + runtimeEnv: process.env, skipValidation: !!process.env.CI || !!process.env.SKIP_ENV_VALIDATION, }); diff --git a/apps/dashboard/src/server.ts b/apps/dashboard/src/server.ts new file mode 100644 index 0000000..f9acaaa --- /dev/null +++ b/apps/dashboard/src/server.ts @@ -0,0 +1,28 @@ +import { createServer } from "node:http"; +import { config } from "@eda/config"; +import next from "next"; +import { env } from "./env.mjs"; + +const dev = process.env.NODE_ENV !== "production"; +const app = next({ dev }); +const handle = app.getRequestHandler(); + +async function startServer() { + try { + await app.prepare(); + const port = env.PORT || config.ports.dashboard; + + createServer(handle).listen(port, () => { + console.log(`> Ready on http://localhost:${port}`); + }); + } catch (error) { + console.error("Error starting server:", error); + process.exit(1); + } +} + +if (require.main === module) { + startServer(); +} + +export default startServer; diff --git a/bun.lockb b/bun.lockb index ad24eadcd2a4f31b6b882e6bdabcb185a23efa1d..c11c4aeb0d44582d27d1a38b41f494c67e4f8652 100755 GIT binary patch delta 33381 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 From 19711d3a12ca44a7e279edcd8eaec64b86fe4637 Mon Sep 17 00:00:00 2001 From: Luis Otavio Date: Tue, 21 Jan 2025 03:02:31 -0300 Subject: [PATCH 41/75] Fix error in landingpage + remove .env --- apps/landingpage/.env.example | 1 - .../{tailwind.config.js => tailwind.config.mjs} | 0 apps/landingpage/vite.config.js | 8 ++++---- 3 files changed, 4 insertions(+), 5 deletions(-) delete mode 100644 apps/landingpage/.env.example rename apps/landingpage/{tailwind.config.js => tailwind.config.mjs} (100%) diff --git a/apps/landingpage/.env.example b/apps/landingpage/.env.example deleted file mode 100644 index 285b27c..0000000 --- a/apps/landingpage/.env.example +++ /dev/null @@ -1 +0,0 @@ -VITE_OPENPANEL_CLIENT_ID= \ No newline at end of file diff --git a/apps/landingpage/tailwind.config.js b/apps/landingpage/tailwind.config.mjs similarity index 100% rename from apps/landingpage/tailwind.config.js rename to apps/landingpage/tailwind.config.mjs diff --git a/apps/landingpage/vite.config.js b/apps/landingpage/vite.config.js index 7e60593..e92efdb 100644 --- a/apps/landingpage/vite.config.js +++ b/apps/landingpage/vite.config.js @@ -1,20 +1,20 @@ import { resolve } from "node:path"; import { URL, fileURLToPath } from "node:url"; +import { config } from "@eda/config"; import react from "@vitejs/plugin-react"; import { defineConfig } from "vite"; -// https://vitejs.dev/config/ export default defineConfig({ server: { host: "::", - port: "8081", + port: config.ports.landingpage, }, define: { "process.env.VITE_OPENPANEL_CLIENT_ID": JSON.stringify( - process.env.VITE_OPENPANEL_CLIENT_ID, + config.api_keys.openpanel.client_id, ), }, - base: process.env.BASE_URL || "/", + base: "/", plugins: [react()], resolve: { alias: [ From 4b4e4debc6a949bf5dd982d3672528c8bd8093cd Mon Sep 17 00:00:00 2001 From: Luis Otavio Date: Tue, 21 Jan 2025 03:07:45 -0300 Subject: [PATCH 42/75] Fix: Variable error --- apps/landingpage/index.html | 4 ++-- apps/landingpage/vite.config.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/landingpage/index.html b/apps/landingpage/index.html index 0152bc5..2b06364 100644 --- a/apps/landingpage/index.html +++ b/apps/landingpage/index.html @@ -12,7 +12,7 @@ - + \ No newline at end of file diff --git a/apps/landingpage/vite.config.js b/apps/landingpage/vite.config.js index e92efdb..c2cf0df 100644 --- a/apps/landingpage/vite.config.js +++ b/apps/landingpage/vite.config.js @@ -10,7 +10,7 @@ export default defineConfig({ port: config.ports.landingpage, }, define: { - "process.env.VITE_OPENPANEL_CLIENT_ID": JSON.stringify( + VITE_OPENPANEL_CLIENT_ID: JSON.stringify( config.api_keys.openpanel.client_id, ), }, From 77989c7a10998762503de0b4813c4ac427a0b755 Mon Sep 17 00:00:00 2001 From: Luis Otavio Date: Tue, 21 Jan 2025 22:40:18 -0300 Subject: [PATCH 43/75] Using config for message api --- apps/ai_api/uv.lock | 2 +- apps/api/package.json | 2 +- apps/messaging/src/index.ts | 6 ++---- apps/messaging/src/routes.ts | 14 +++++++++----- bun.lockb | Bin 732240 -> 732688 bytes 5 files changed, 13 insertions(+), 11 deletions(-) diff --git a/apps/ai_api/uv.lock b/apps/ai_api/uv.lock index 08fd327..b398338 100644 --- a/apps/ai_api/uv.lock +++ b/apps/ai_api/uv.lock @@ -917,7 +917,7 @@ requires-dist = [ [[package]] name = "eda-config" -version = "0.1.6" +version = "0.1.13" source = { directory = "../../packages/config/python" } dependencies = [ { name = "pydantic" }, diff --git a/apps/api/package.json b/apps/api/package.json index bf67703..c61d2ea 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -10,7 +10,7 @@ "generate": "supabase gen types --lang=typescript --local --schema public > ../../packages/supabase/src/types/db.ts" }, "dependencies": { - "supabase": "^1.207.9" + "supabase": "^2.6.8" }, "devDependencies": { "@snaplet/copycat": "^5.1.0", diff --git a/apps/messaging/src/index.ts b/apps/messaging/src/index.ts index d827001..53a0754 100644 --- a/apps/messaging/src/index.ts +++ b/apps/messaging/src/index.ts @@ -1,12 +1,10 @@ -import { ConfigLoader } from "@eda/config"; +import { config } from "@eda/config"; import { swagger } from "@elysiajs/swagger"; import { Elysia } from "elysia"; import { handleHealthCheck, handleSendMessage } from "./routes"; import { messageSchema } from "./types"; -const config = ConfigLoader.getConfig(); - -const PORT = process.env.PORT || 3000; +const PORT = config.ports.messaging; const app = new Elysia() .use( swagger({ diff --git a/apps/messaging/src/routes.ts b/apps/messaging/src/routes.ts index d8e32e3..0f15e68 100644 --- a/apps/messaging/src/routes.ts +++ b/apps/messaging/src/routes.ts @@ -1,14 +1,17 @@ +import { config } from "@eda/config"; import { logger } from "@eda/logger"; import { createClient } from "@supabase/supabase-js"; import { tasks } from "@trigger.dev/sdk/v3"; import { type Message, messageSchema, queryParamsSchema } from "./types"; +// Replace Supabase client initialization const supabase = createClient( - "http://localhost:54321", // Local Supabase URL from config.toml - process.env.SUPABASE_SERVICE_ROLE_KEY!, // Get this from Supabase Studio + config.databases.supabase.url, + config.api_keys.supabase.service_key, ); -const AI_API_URL = process.env.AI_API_URL || "http://127.0.0.1:8083"; +// Replace AI API URL constant +const AI_API_URL = `http://localhost:${config.ports.ai_api}`; // Add new function to get/create Supabase ID async function getSupabaseId(whatsappId: string): Promise { @@ -16,12 +19,12 @@ async function getSupabaseId(whatsappId: string): Promise { // First check if user exists const { data: existingUser } = await supabase .from("messages") - .select("id") + .select("user_id") .eq("whatsapp_id", whatsappId) .single(); if (existingUser) { - return existingUser.id; + return existingUser.user_id; } // If not exists, create new user @@ -143,6 +146,7 @@ export async function handleSendMessage(req: Request) { .single(); if (historyError) { + console.error(historyError); logger.error("Error fetching conversation history", { error: historyError, }); diff --git a/bun.lockb b/bun.lockb index c11c4aeb0d44582d27d1a38b41f494c67e4f8652..e264504dc2a124cae8522e3b87e362f6bc1e803b 100755 GIT binary patch delta 15815 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�BT+UY%}Q>097vnjta;&)h+@K2$*^ikYf%C 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 From 5977a87bd8b69b14c3b33b0ea4807f0da7e64c24 Mon Sep 17 00:00:00 2001 From: Luis Otavio Date: Tue, 21 Jan 2025 22:44:59 -0300 Subject: [PATCH 44/75] Use config in whatsapp --- apps/messaging/.env.example | 4 ---- apps/whatsapp/.env.example | 37 ------------------------------------- apps/whatsapp/src/utils.ts | 11 ++++------- 3 files changed, 4 insertions(+), 48 deletions(-) delete mode 100644 apps/messaging/.env.example delete mode 100644 apps/whatsapp/.env.example diff --git a/apps/messaging/.env.example b/apps/messaging/.env.example deleted file mode 100644 index 727d941..0000000 --- a/apps/messaging/.env.example +++ /dev/null @@ -1,4 +0,0 @@ -SUPABASE_SERVICE_ROLE_KEY=xxxxx -PORT=3001 # Remember to modify it in the message.ts from whatsapp -TRIGGER_SECRET_KEY=xxx -TRIGGER_API_URL=http://localhost:3040 \ No newline at end of file diff --git a/apps/whatsapp/.env.example b/apps/whatsapp/.env.example deleted file mode 100644 index 4359b14..0000000 --- a/apps/whatsapp/.env.example +++ /dev/null @@ -1,37 +0,0 @@ -TRIGGER_SECRET_KEY= -TRIGGER_API_URL= -PORT=4000 -PUPPETEER_EXECUTABLE_PATH="/snap/bin/chromium" - -# Whatsapp - -# This is how the bot will prefix its messages when answering to commands -# or when replying to itself (e.g. when you run the bot in your own personal whatsapp account) -# Note: must be different from CMD_PREFIX and cannot be empty -BOT_PREFIX="*[BOT]:*" - -# This is how the user should prefix their messages when issuing commands to the bot -CMD_PREFIX="!" - -# The assistant's name. Call it whatever you want. -BOT_NAME="Sydney" - -# Accepted values are "true", "dms_only", "groups_only" or "false" -ENABLE_REACTIONS="true" - -# The bot will only reply to these users. Leave this commented to allow everyone to use the bot. -# See the readme.md file to learn how this works. -#ALLOWED_USERS="" # Example: "5511999999999,14155551111" where 55 is the country code, 11 is the area code, and the rest is the phone number. - -# The bot will ignore these users. Leave this commented to allow everyone to use the bot. -# See the readme.md file to learn how this works. -#BLOCKED_USERS="" # Example: "5511999999999,14155551111" where 55 is the country code, 11 is the area code, and the rest is the phone number. - -# The "Too many unread messages..." warning when the bot starts. -IGNORE_MESSAGES_WARNING="false" # Accepted values are "true" or "false" - -# Change to your liking -QUEUED_REACTION="πŸ”" -WORKING_REACTION="βš™οΈ" -DONE_REACTION="βœ…" -ERROR_REACTION="⚠️" \ No newline at end of file diff --git a/apps/whatsapp/src/utils.ts b/apps/whatsapp/src/utils.ts index 187c218..deb20a3 100644 --- a/apps/whatsapp/src/utils.ts +++ b/apps/whatsapp/src/utils.ts @@ -1,3 +1,4 @@ +import { config } from "@eda/config"; // Add this import import type { proto } from "@whiskeysockets/baileys"; import { stripIndents } from "common-tags"; import { sock } from "./client"; @@ -29,12 +30,8 @@ async function getGroupName(groupJid: string): Promise { } } -export const REACTIONS = { - queued: process.env.QUEUED_REACTION || "πŸ”", - working: process.env.WORKING_REACTION || "βš™οΈ", - done: process.env.DONE_REACTION || "βœ…", - error: process.env.ERROR_REACTION || "⚠️", -}; +// Replace REACTIONS definition with config values +export const REACTIONS = config.services.whatsapp.reactions; export type Reaction = keyof typeof REACTIONS; @@ -194,7 +191,7 @@ export async function shouldIgnoreUnread( warningMessage = `Too many unread messages (${unreadCount}) since I've last seen this chat. I'm ignoring them. If you need me to respond, please message me again.`; } - if (IGNORE_MESSAGES_WARNING === "true") { + if (IGNORE_MESSAGES_WARNING) { await sock.sendMessage(chatJid, { text: BOT_PREFIX + warningMessage }); } From ff86feacb1e34555163d16f43a707ff65d7fb339 Mon Sep 17 00:00:00 2001 From: Luis Otavio Date: Tue, 21 Jan 2025 23:44:15 -0300 Subject: [PATCH 45/75] Refactor: Remove .env.example and generate env file from config during deployment --- deploy/trigger-stack/.env.example | 97 ------------------------- deploy/trigger-stack/deploy.sh | 8 ++ deploy/trigger-stack/docker-compose.yml | 48 ++++++------ deploy/trigger-stack/export-config.ts | 55 ++++++++++++++ package.json | 2 +- 5 files changed, 88 insertions(+), 122 deletions(-) delete mode 100644 deploy/trigger-stack/.env.example create mode 100755 deploy/trigger-stack/deploy.sh create mode 100644 deploy/trigger-stack/export-config.ts diff --git a/deploy/trigger-stack/.env.example b/deploy/trigger-stack/.env.example deleted file mode 100644 index 8fe1367..0000000 --- a/deploy/trigger-stack/.env.example +++ /dev/null @@ -1,97 +0,0 @@ -PORT=3040 -REMIX_APP_PORT=3030 - -NODE_ENV=production -RUNTIME_PLATFORM=docker-compose - -V3_ENABLED=true -# TRIGGER_TELEMETRY_DISABLED=1 -INTERNAL_OTEL_TRACE_DISABLED=1 -INTERNAL_OTEL_TRACE_LOGGING_ENABLED=0 - -POSTGRES_USER=postgres -POSTGRES_PASSWORD=postgres -POSTGRES_DB=postgres -DATABASE_HOST=postgres:5432 -DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${DATABASE_HOST}/${POSTGRES_DB} - -# This sets the URL used for direct connections to the database and should only be needed in limited circumstances -# See: https://www.prisma.io/docs/reference/api-reference/prisma-schema-reference#fields:~:text=the%20shadow%20database.-,directUrl,-No -DIRECT_URL=${DATABASE_URL} - -REDIS_HOST=redis -REDIS_PORT=6379 -REDIS_TLS_DISABLED=true - -# If this is set, emails that are not specified won't be able to log in -# WHITELISTED_EMAILS="authorized@yahoo\.com|authorized@gmail\.com" - -# Accounts with these emails will become admins when signing up and get access to the admin panel -# ADMIN_EMAILS="admin@example\.com|another-admin@example\.com" - -# If this is set, your users will be able to log in via GitHub -# AUTH_GITHUB_CLIENT_ID= -# AUTH_GITHUB_CLIENT_SECRET= - -# E-mail settings -# -# - Ensure the FROM_EMAIL matches what you setup with Resend.com -# - If these are not set, emails will be printed to the console -# -# FROM_EMAIL= -# REPLY_TO_EMAIL= -# RESEND_API_KEY= - -# Concurrency limits -# -# - If these are too high, you may run out of resources on your worker. A simple fix is to either re-deploy -# with lower limits, or set appropriate limits in your trigger.config.ts or directly on your tasks. -# -DEFAULT_ORG_EXECUTION_CONCURRENCY_LIMIT=300 -DEFAULT_ENV_EXECUTION_CONCURRENCY_LIMIT=100 - -# Secrets -# -# - You MUST change these in production! -# -# generate these with `openssl rand -hex 16` -MAGIC_LINK_SECRET=secret # used to encrypt magic link tokens -SESSION_SECRET=secret # used to encrypt session cookies -ENCRYPTION_KEY=ae13021afef0819c3a307ad487071c06 # used to encrypt permanent data -# -# generate these with `openssl rand -hex 32` -PROVIDER_SECRET=provider-secret -COORDINATOR_SECRET=coordinator-secret - -# Worker settings -# -HTTP_SERVER_PORT=9020 -COORDINATOR_HOST=127.0.0.1 -COORDINATOR_PORT=${HTTP_SERVER_PORT} -REGISTRY_HOST=${DEPLOY_REGISTRY_HOST} -REGISTRY_NAMESPACE=${DEPLOY_REGISTRY_NAMESPACE} -# FORCE_CHECKPOINT_SIMULATION=0 # only uncomment if you are willing to try EXPERIMENTAL docker features - expect bugs - -# Docker -# -# RESTART_POLICY= -# WEBAPP_PUBLISH_IP= -# TRIGGER_IMAGE_TAG= -# POSTGRES_IMAGE_TAG= -# REDIS_IMAGE_TAG= -# ELECTRIC_IMAGE_TAG= - -# Registry settings -# -# - Images will be pushed to: host/namespace/project:version -# -# DEPLOY_REGISTRY_HOST=docker.io -# DEPLOY_REGISTRY_NAMESPACE=trigger # you should change this, for example to your Docker Hub username - -# Domain settings -# -# - Should be uncommented unless you're just testing locally -# - Required for the split webapp / worker setup -# -# TRIGGER_PROTOCOL=https -# TRIGGER_DOMAIN=.ngrok-free.app \ No newline at end of file diff --git a/deploy/trigger-stack/deploy.sh b/deploy/trigger-stack/deploy.sh new file mode 100755 index 0000000..2ba1354 --- /dev/null +++ b/deploy/trigger-stack/deploy.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +# Generate fresh env file from config +bun run export-config.ts > .env + +sleep 2 +# Run docker compose with generated env file +docker compose --env-file .env up -d diff --git a/deploy/trigger-stack/docker-compose.yml b/deploy/trigger-stack/docker-compose.yml index ae0fb4f..d13b79a 100644 --- a/deploy/trigger-stack/docker-compose.yml +++ b/deploy/trigger-stack/docker-compose.yml @@ -1,12 +1,12 @@ x-webapp-env: &webapp-env - LOGIN_ORIGIN: &trigger-url ${TRIGGER_PROTOCOL:-http}://${TRIGGER_DOMAIN:-localhost:3040} + LOGIN_ORIGIN: &trigger-url ${TRIGGER_PROTOCOL}://${TRIGGER_DOMAIN} APP_ORIGIN: *trigger-url - DEV_OTEL_EXPORTER_OTLP_ENDPOINT: &trigger-otel ${TRIGGER_PROTOCOL:-http}://${TRIGGER_DOMAIN:-localhost:3040}/otel + DEV_OTEL_EXPORTER_OTLP_ENDPOINT: &trigger-otel ${TRIGGER_PROTOCOL}://${TRIGGER_DOMAIN}/otel ELECTRIC_ORIGIN: http://electric:3000 x-worker-env: &worker-env - PLATFORM_HOST: webapp - PLATFORM_WS_PORT: 3030 + PLATFORM_HOST: webapp + PLATFORM_WS_PORT: ${REMIX_APP_PORT} SECURE_CONNECTION: "false" OTEL_EXPORTER_OTLP_ENDPOINT: *trigger-otel @@ -19,15 +19,15 @@ networks: services: webapp: - image: ghcr.io/triggerdotdev/trigger.dev:${TRIGGER_IMAGE_TAG:-v3} + image: ghcr.io/triggerdotdev/trigger.dev:${TRIGGER_IMAGE_TAG} container_name: trigger-webapp - restart: ${RESTART_POLICY:-unless-stopped} + restart: ${RESTART_POLICY} env_file: - .env environment: <<: *webapp-env ports: - - ${WEBAPP_PUBLISH_IP:-127.0.0.1}:${PORT}:3030 + - ${DOCKER_PUBLISH_IP}:${PORT}:${REMIX_APP_PORT} depends_on: - postgres - redis @@ -35,9 +35,9 @@ services: - webapp postgres: - image: postgres:${POSTGRES_IMAGE_TAG:-16} + image: postgres:${POSTGRES_IMAGE_TAG} container_name: trigger-postgres - restart: ${RESTART_POLICY:-unless-stopped} + restart: ${RESTART_POLICY} volumes: - postgres-data:/var/lib/postgresql/data/ env_file: @@ -45,26 +45,26 @@ services: networks: - webapp ports: - - ${DOCKER_PUBLISH_IP:-127.0.0.1}:5433:5432 + - ${DOCKER_PUBLISH_IP}:${POSTGRES_PORT}:5432 command: - -c - wal_level=logical redis: - image: redis:${REDIS_IMAGE_TAG:-7} + image: redis:${REDIS_IMAGE_TAG} container_name: trigger-redis - restart: ${RESTART_POLICY:-unless-stopped} + restart: ${RESTART_POLICY} volumes: - redis-data:/data networks: - webapp ports: - - ${DOCKER_PUBLISH_IP:-127.0.0.1}:6389:6379 + - ${DOCKER_PUBLISH_IP}:${REDIS_PORT}:6379 docker-provider: - image: ghcr.io/triggerdotdev/provider/docker:${TRIGGER_IMAGE_TAG:-v3} + image: ghcr.io/triggerdotdev/provider/docker:${TRIGGER_IMAGE_TAG} container_name: trigger-docker-provider - restart: ${RESTART_POLICY:-unless-stopped} + restart: ${RESTART_POLICY} volumes: - /var/run/docker.sock:/var/run/docker.sock user: root @@ -73,17 +73,17 @@ services: depends_on: - webapp ports: - - ${DOCKER_PUBLISH_IP:-127.0.0.1}:9021:9020 + - ${DOCKER_PUBLISH_IP}:${HTTP_SERVER_PORT}:${HTTP_SERVER_PORT} env_file: - .env environment: <<: *worker-env - PLATFORM_SECRET: $PROVIDER_SECRET + PLATFORM_SECRET: ${PROVIDER_SECRET} coordinator: - image: ghcr.io/triggerdotdev/coordinator:${TRIGGER_IMAGE_TAG:-v3} + image: ghcr.io/triggerdotdev/coordinator:${TRIGGER_IMAGE_TAG} container_name: trigger-coordinator - restart: ${RESTART_POLICY:-unless-stopped} + restart: ${RESTART_POLICY} volumes: - /var/run/docker.sock:/var/run/docker.sock user: root @@ -92,17 +92,17 @@ services: depends_on: - webapp ports: - - ${DOCKER_PUBLISH_IP:-127.0.0.1}:9020:9020 + - ${DOCKER_PUBLISH_IP}:${COORDINATOR_PORT}:${COORDINATOR_PORT} env_file: - .env environment: <<: *worker-env - PLATFORM_SECRET: $COORDINATOR_SECRET + PLATFORM_SECRET: ${COORDINATOR_SECRET} electric: - image: electricsql/electric:${ELECTRIC_IMAGE_TAG:-latest} + image: electricsql/electric:${ELECTRIC_IMAGE_TAG} container_name: trigger-electric - restart: ${RESTART_POLICY:-unless-stopped} + restart: ${RESTART_POLICY} environment: DATABASE_URL: ${DATABASE_URL}?sslmode=disable networks: @@ -110,4 +110,4 @@ services: depends_on: - postgres ports: - - ${DOCKER_PUBLISH_IP:-127.0.0.1}:3061:3000 \ No newline at end of file + - ${DOCKER_PUBLISH_IP}:3061:3000 \ No newline at end of file diff --git a/deploy/trigger-stack/export-config.ts b/deploy/trigger-stack/export-config.ts new file mode 100644 index 0000000..269e5d0 --- /dev/null +++ b/deploy/trigger-stack/export-config.ts @@ -0,0 +1,55 @@ +//@ts-ignore +import { config } from "@eda/config"; + +const envVars = { + // Core settings + PORT: config.ports.trigger, + REMIX_APP_PORT: config.ports.remix, + NODE_ENV: "production", + RUNTIME_PLATFORM: "docker-compose", + V3_ENABLED: config.services.trigger.v3_enabled, + + // Database settings + POSTGRES_USER: config.databases.trigger_postgres.user, + POSTGRES_PASSWORD: config.databases.trigger_postgres.password, + POSTGRES_DB: config.databases.trigger_postgres.database, + DATABASE_HOST: "postgres:5432", + DATABASE_URL: `postgresql://${config.databases.trigger_postgres.user}:${config.databases.trigger_postgres.password}@postgres:5432/${config.databases.trigger_postgres.database}`, + DIRECT_URL: `postgresql://${config.databases.trigger_postgres.user}:${config.databases.trigger_postgres.password}@postgres:5432/${config.databases.trigger_postgres.database}`, // Add this line + + // Add database ports + POSTGRES_PORT: config.ports.db.postgres, + REDIS_PORT: config.ports.db.redis, + + // Redis settings + REDIS_HOST: "redis", + REDIS_TLS_DISABLED: config.databases.redis.tls_disabled, + + // Auth settings + MAGIC_LINK_SECRET: config.services.trigger.auth.magic_link_secret, + SESSION_SECRET: config.services.trigger.auth.session_secret, + ENCRYPTION_KEY: config.services.trigger.auth.encryption_key, + PROVIDER_SECRET: config.services.trigger.auth.provider_secret, + COORDINATOR_SECRET: config.services.trigger.auth.coordinator_secret, + + // Hardcoded values (not in config) + INTERNAL_OTEL_TRACE_DISABLED: "1", + INTERNAL_OTEL_TRACE_LOGGING_ENABLED: "0", + RESTART_POLICY: "unless-stopped", + TRIGGER_IMAGE_TAG: "v3", + POSTGRES_IMAGE_TAG: "16", + REDIS_IMAGE_TAG: "7", + ELECTRIC_IMAGE_TAG: "latest", + DOCKER_PUBLISH_IP: "127.0.0.1", + TRIGGER_PROTOCOL: "http", + TRIGGER_DOMAIN: `localhost:${config.ports.trigger}`, + + // Worker settings + HTTP_SERVER_PORT: config.services.trigger.deployment.worker.http_port, + COORDINATOR_HOST: config.services.trigger.deployment.worker.coordinator_host, + COORDINATOR_PORT: config.services.trigger.deployment.worker.coordinator_port, +}; + +for (const [key, value] of Object.entries(envVars)) { + console.log(`${key}=${value}`); +} diff --git a/package.json b/package.json index 6b27668..ee60c03 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "dev:docs": "turbo dev --filter=@eda/docs", "build:landingpage": "turbo build --filter=@eda/landingpage", "clean:workspaces": "turbo clean", - "deploy:trigger": "docker compose -f deploy/trigger-stack/docker-compose.yml up -d", + "deploy:trigger": "cd deploy/trigger-stack && chmod +x deploy.sh && ./deploy.sh", "deploy:langtrace": "docker compose -f deploy/langtrace-stack/docker-compose.yml up -d", "deploy:neo4j": "docker compose -f deploy/neo4j-stack/docker-compose.yml up -d", "status": "bun status", From a01eda5ebdb8f77bc96d26b386f1065f69661967 Mon Sep 17 00:00:00 2001 From: Luis Otavio Date: Wed, 22 Jan 2025 00:28:01 -0300 Subject: [PATCH 46/75] Fixed trigger related issues --- apps/messaging/src/routes.ts | 56 +++++++++++++++++---------- apps/whatsapp/package.json | 2 +- apps/whatsapp/src/message/message.ts | 3 +- bun.lockb | Bin 732688 -> 732000 bytes packages/jobs/package.json | 6 +-- packages/simulator/package.json | 2 +- 6 files changed, 42 insertions(+), 27 deletions(-) diff --git a/apps/messaging/src/routes.ts b/apps/messaging/src/routes.ts index 0f15e68..b6b945c 100644 --- a/apps/messaging/src/routes.ts +++ b/apps/messaging/src/routes.ts @@ -1,7 +1,7 @@ import { config } from "@eda/config"; import { logger } from "@eda/logger"; import { createClient } from "@supabase/supabase-js"; -import { tasks } from "@trigger.dev/sdk/v3"; +import { configure, tasks } from "@trigger.dev/sdk/v3"; import { type Message, messageSchema, queryParamsSchema } from "./types"; // Replace Supabase client initialization @@ -10,6 +10,11 @@ const supabase = createClient( config.api_keys.supabase.service_key, ); +configure({ + secretKey: config.api_keys.trigger, + baseURL: config.services.trigger.api_url, +}); + // Replace AI API URL constant const AI_API_URL = `http://localhost:${config.ports.ai_api}`; @@ -187,15 +192,19 @@ export async function handleSendMessage(req: Request) { logger.info("Triggering monitor-api-call task"); - // Monitor API call using Trigger.dev - await tasks.trigger("monitor-api-call", { - endpoint: "/api/classifier/classify", - method: "POST", - statusCode: response.status, - duration, - timestamp: new Date().toISOString(), - sessionId: payload.sessionId, - }); + try { + // Monitor API call using Trigger.dev + await tasks.trigger("monitor-api-call", { + endpoint: "/api/classifier/classify", + method: "POST", + statusCode: response.status, + duration, + timestamp: new Date().toISOString(), + sessionId: payload.sessionId, + }); + } catch (triggerError) { + logger.error("Error triggering monitoring task:", triggerError); + } if (!response.ok) { throw new Error(data.message || "Failed to process message"); @@ -216,16 +225,23 @@ export async function handleSendMessage(req: Request) { } catch (error) { const duration = performance.now() - startTime; - // Monitor failed API calls - await tasks.trigger("monitor-api-call", { - endpoint: "/api/classifier/classify", - method: "POST", - statusCode: 500, - duration, - timestamp: new Date().toISOString(), - sessionId: payload.sessionId, // Now safe to use since payload is initialized - error: error instanceof Error ? error.message : "Unknown error", - }); + try { + // Monitor failed API calls + await tasks.trigger("monitor-api-call", { + endpoint: "/api/classifier/classify", + method: "POST", + statusCode: 500, + duration, + timestamp: new Date().toISOString(), + sessionId: payload.sessionId, + error: error instanceof Error ? error.message : "Unknown error", + }); + } catch (triggerError) { + logger.error( + "Error triggering monitoring task for failed call:", + triggerError, + ); + } const message = error instanceof Error ? error.message : "Failed to process message"; diff --git a/apps/whatsapp/package.json b/apps/whatsapp/package.json index a869bce..3521806 100644 --- a/apps/whatsapp/package.json +++ b/apps/whatsapp/package.json @@ -17,7 +17,7 @@ "typescript": "^5.0.0" }, "dependencies": { - "@trigger.dev/sdk": "3.2.1", + "@trigger.dev/sdk": "3.3.11", "@types/common-tags": "^1.8.4", "@whiskeysockets/baileys": "^6.7.9", "common-tags": "^1.8.2", diff --git a/apps/whatsapp/src/message/message.ts b/apps/whatsapp/src/message/message.ts index 06101ed..65a8317 100644 --- a/apps/whatsapp/src/message/message.ts +++ b/apps/whatsapp/src/message/message.ts @@ -1,7 +1,6 @@ -import { logger } from "@trigger.dev/sdk/v3"; +import { logger } from "@eda/logger"; import { type WAMessage, downloadMediaMessage } from "@whiskeysockets/baileys"; import { sock } from "../client"; -import { BOT_PREFIX } from "../constants"; import { getPhoneNumber, react } from "../utils"; const IMAGE_ERROR_MSG = diff --git a/bun.lockb b/bun.lockb index e264504dc2a124cae8522e3b87e362f6bc1e803b..8e88040775e4abf98a5435b2019eb8cca3c45675 100755 GIT binary patch delta 112909 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≻(1tzbHb|s0{U#s@^l*JLJOX-3uBYOa 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 VRVGS{tv%lo@M|5 diff --git a/packages/jobs/package.json b/packages/jobs/package.json index a29d3cb..86fec0a 100644 --- a/packages/jobs/package.json +++ b/packages/jobs/package.json @@ -5,7 +5,7 @@ "scripts": { "clean": "rm -rf .turbo node_modules", "lint": "biome lint ./trigger ./lib", - "dev": "bunx trigger.dev dev", + "dev": "bunx trigger.dev dev --skip-update-check", "format": "biome format --write .", "typecheck": "tsc-files --noEmit", "test": "vitest" @@ -14,10 +14,10 @@ "@eda/supabase": "workspace:*", "@eda/types": "workspace:*", "@getzep/zep-cloud": "^2.1.1", - "@trigger.dev/sdk": "3.2.1" + "@trigger.dev/sdk": "3.3.11" }, "devDependencies": { - "@trigger.dev/build": "3.2.1", + "@trigger.dev/build": "3.3.11", "vitest": "^2.1.5" } } diff --git a/packages/simulator/package.json b/packages/simulator/package.json index dec9896..9a2acde 100644 --- a/packages/simulator/package.json +++ b/packages/simulator/package.json @@ -13,7 +13,7 @@ "@cerebras/cerebras_cloud_sdk": "^1.8.0", "@eda/logger": "workspace:*", "@eda/types": "workspace:*", - "@trigger.dev/sdk": "3.2.1", + "@trigger.dev/sdk": "3.3.11", "dotenv": "^16.4.5", "groq-sdk": "^0.7.0" }, From 0cee3f729e7748942a432d8bc73e8766a1ddfe41 Mon Sep 17 00:00:00 2001 From: Luis Otavio Date: Wed, 22 Jan 2025 00:31:31 -0300 Subject: [PATCH 47/75] Refactor: Update Neo4j deployment to use environment variables and add deployment script --- deploy/neo4j-stack/deploy.sh | 8 ++++++++ deploy/neo4j-stack/docker-compose.yml | 14 +++++++------- deploy/neo4j-stack/export-config.ts | 16 ++++++++++++++++ package.json | 2 +- 4 files changed, 32 insertions(+), 8 deletions(-) create mode 100755 deploy/neo4j-stack/deploy.sh create mode 100644 deploy/neo4j-stack/export-config.ts diff --git a/deploy/neo4j-stack/deploy.sh b/deploy/neo4j-stack/deploy.sh new file mode 100755 index 0000000..e10df33 --- /dev/null +++ b/deploy/neo4j-stack/deploy.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +# Generate fresh env file from config +bun run export-config.ts > .env + +sleep 2 +# Run docker compose with generated env file +docker compose --env-file .env up -d \ No newline at end of file diff --git a/deploy/neo4j-stack/docker-compose.yml b/deploy/neo4j-stack/docker-compose.yml index 0a337a4..7317da7 100644 --- a/deploy/neo4j-stack/docker-compose.yml +++ b/deploy/neo4j-stack/docker-compose.yml @@ -3,18 +3,18 @@ services: image: neo4j:latest container_name: eda-neo4j ports: - - "7474:7474" # HTTP - - "7687:7687" # Bolt + - "${NEO4J_HTTP_PORT}:7474" + - "${NEO4J_BOLT_PORT}:7687" environment: - - NEO4J_AUTH=neo4j/password - - NEO4J_PLUGINS=["apoc"] + - NEO4J_AUTH=${NEO4J_AUTH} + - NEO4J_PLUGINS=${NEO4J_PLUGINS} volumes: - neo4j_data:/data healthcheck: test: ["CMD-SHELL", "wget -O /dev/null -q http://localhost:7474 || exit 1"] - interval: 10s - timeout: 5s - retries: 3 + interval: ${HEALTHCHECK_INTERVAL}s + timeout: ${HEALTHCHECK_TIMEOUT}s + retries: ${HEALTHCHECK_RETRIES} restart: always volumes: diff --git a/deploy/neo4j-stack/export-config.ts b/deploy/neo4j-stack/export-config.ts new file mode 100644 index 0000000..7d0bb4f --- /dev/null +++ b/deploy/neo4j-stack/export-config.ts @@ -0,0 +1,16 @@ +//@ts-ignore +import { config } from "@eda/config"; + +const envVars = { + NEO4J_AUTH: `${config.databases.neo4j.auth.user}/${config.databases.neo4j.auth.password}`, + NEO4J_PLUGINS: JSON.stringify(config.databases.neo4j.plugins), + HEALTHCHECK_INTERVAL: config.databases.neo4j.healthcheck.interval, + HEALTHCHECK_TIMEOUT: config.databases.neo4j.healthcheck.timeout, + HEALTHCHECK_RETRIES: config.databases.neo4j.healthcheck.retries, + NEO4J_HTTP_PORT: config.ports.db.neo4j.http, + NEO4J_BOLT_PORT: config.ports.db.neo4j.bolt, +}; + +for (const [key, value] of Object.entries(envVars)) { + console.log(`${key}=${value}`); +} diff --git a/package.json b/package.json index ee60c03..b7d42c5 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "clean:workspaces": "turbo clean", "deploy:trigger": "cd deploy/trigger-stack && chmod +x deploy.sh && ./deploy.sh", "deploy:langtrace": "docker compose -f deploy/langtrace-stack/docker-compose.yml up -d", - "deploy:neo4j": "docker compose -f deploy/neo4j-stack/docker-compose.yml up -d", + "deploy:neo4j": "cd deploy/neo4j-stack && chmod +x deploy.sh && ./deploy.sh", "status": "bun status", "start:dashboard": "turbo start --filter=@eda/dashboard", "test": "turbo test --parallel", From 1deaa50e77af29caf8f4d641a4b1fbc2bee5e166 Mon Sep 17 00:00:00 2001 From: Luis Otavio Date: Wed, 22 Jan 2025 00:35:26 -0300 Subject: [PATCH 48/75] Refactor: Update langtrace deployment to use a script for environment variable generation --- deploy/langtrace-stack/deploy.sh | 8 ++++ deploy/langtrace-stack/export-config.ts | 51 +++++++++++++++++++++++++ package.json | 2 +- 3 files changed, 60 insertions(+), 1 deletion(-) create mode 100755 deploy/langtrace-stack/deploy.sh create mode 100644 deploy/langtrace-stack/export-config.ts diff --git a/deploy/langtrace-stack/deploy.sh b/deploy/langtrace-stack/deploy.sh new file mode 100755 index 0000000..e10df33 --- /dev/null +++ b/deploy/langtrace-stack/deploy.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +# Generate fresh env file from config +bun run export-config.ts > .env + +sleep 2 +# Run docker compose with generated env file +docker compose --env-file .env up -d \ No newline at end of file diff --git a/deploy/langtrace-stack/export-config.ts b/deploy/langtrace-stack/export-config.ts new file mode 100644 index 0000000..a86f1bf --- /dev/null +++ b/deploy/langtrace-stack/export-config.ts @@ -0,0 +1,51 @@ +//@ts-ignore +import { config } from "@eda/config"; + +const envVars = { + // Core settings + PORT: config.ports.langtrace, + + // Postgres settings + POSTGRES_HOST: config.databases.langtrace_postgres.host, + POSTGRES_USER: config.databases.langtrace_postgres.user, + POSTGRES_PASSWORD: config.databases.langtrace_postgres.password, + POSTGRES_DATABASE: config.databases.langtrace_postgres.database, + POSTGRES_DB: config.databases.langtrace_postgres.database, + POSTGRES_URL: `postgres://${config.databases.langtrace_postgres.user}:${config.databases.langtrace_postgres.password}@${config.databases.langtrace_postgres.host}/${config.databases.langtrace_postgres.database}`, + POSTGRES_PRISMA_URL: `postgres://${config.databases.langtrace_postgres.user}:${config.databases.langtrace_postgres.password}@${config.databases.langtrace_postgres.host}/${config.databases.langtrace_postgres.database}?pgbouncer=true&connect_timeout=15`, + POSTGRES_URL_NO_SSL: `postgres://${config.databases.langtrace_postgres.user}:${config.databases.langtrace_postgres.password}@${config.databases.langtrace_postgres.host}/${config.databases.langtrace_postgres.database}`, + POSTGRES_URL_NON_POOLING: `postgres://${config.databases.langtrace_postgres.user}:${config.databases.langtrace_postgres.password}@${config.databases.langtrace_postgres.host}/${config.databases.langtrace_postgres.database}`, + POSTGRES_IMAGE_TAG: "16", + + // App settings + NEXT_PUBLIC_APP_NAME: config.services.langtrace.api.host, + NEXT_PUBLIC_ENVIRONMENT: "production", + NEXT_PUBLIC_HOST: "http://localhost:3000", + NEXTAUTH_SECRET: config.services.dashboard.auth.nextauth_secret, + NEXTAUTH_URL: "http://localhost:3000", + NEXTAUTH_URL_INTERNAL: "http://localhost:3000", + + // Clickhouse settings + CLICK_HOUSE_HOST: config.databases.langtrace_clickhouse.host, + CLICK_HOUSE_USER: config.databases.langtrace_clickhouse.user, + CLICK_HOUSE_PASSWORD: config.databases.langtrace_clickhouse.password, + CLICK_HOUSE_DATABASE_NAME: config.databases.langtrace_clickhouse.database, + + // Admin settings + ADMIN_EMAIL: config.services.langtrace.admin.email, + ADMIN_PASSWORD: config.services.langtrace.admin.password, + NEXT_PUBLIC_ENABLE_ADMIN_LOGIN: config.services.langtrace.admin.enable_login, + + // Azure AD settings + AZURE_AD_CLIENT_ID: config.services.dashboard.auth.azure.client_id, + AZURE_AD_CLIENT_SECRET: config.services.dashboard.auth.azure.client_secret, + AZURE_AD_TENANT_ID: config.services.dashboard.auth.azure.tenant_id, + + // Additional settings + POSTHOG_HOST: config.services.langtrace.posthog.host, + TELEMETRY_ENABLED: config.services.langtrace.telemetry.enabled, +}; + +for (const [key, value] of Object.entries(envVars)) { + console.log(`${key}=${value}`); +} diff --git a/package.json b/package.json index b7d42c5..1718f86 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "build:landingpage": "turbo build --filter=@eda/landingpage", "clean:workspaces": "turbo clean", "deploy:trigger": "cd deploy/trigger-stack && chmod +x deploy.sh && ./deploy.sh", - "deploy:langtrace": "docker compose -f deploy/langtrace-stack/docker-compose.yml up -d", + "deploy:langtrace": "cd deploy/langtrace-stack && chmod +x deploy.sh && ./deploy.sh", "deploy:neo4j": "cd deploy/neo4j-stack && chmod +x deploy.sh && ./deploy.sh", "status": "bun status", "start:dashboard": "turbo start --filter=@eda/dashboard", From 60e659ab18c3d0111a555550fea1b905b285edf4 Mon Sep 17 00:00:00 2001 From: Luis Otavio Date: Wed, 22 Jan 2025 00:49:06 -0300 Subject: [PATCH 49/75] Refactor: Remove example environment files and update Supabase client configuration to use centralized config --- deploy/langtrace-stack/.env.example | 39 ------------------- packages/jobs/.env.example | 4 -- packages/jobs/lib/supabase.ts | 12 ++---- .../jobs/schemas/monitor-api-calls.schema.ts | 13 +++++++ packages/jobs/trigger.config.ts | 6 +-- packages/jobs/trigger/monitor-api-call.ts | 14 +------ 6 files changed, 20 insertions(+), 68 deletions(-) delete mode 100644 deploy/langtrace-stack/.env.example delete mode 100644 packages/jobs/.env.example create mode 100644 packages/jobs/schemas/monitor-api-calls.schema.ts diff --git a/deploy/langtrace-stack/.env.example b/deploy/langtrace-stack/.env.example deleted file mode 100644 index 9167c64..0000000 --- a/deploy/langtrace-stack/.env.example +++ /dev/null @@ -1,39 +0,0 @@ -PORT=3003 -# Postgres Variables -POSTGRES_HOST="langtrace-postgres:5432" -POSTGRES_USER="ltuser" -POSTGRES_PASSWORD="ltpasswd" -POSTGRES_DATABASE="langtrace" -POSTGRES_DB="${POSTGRES_DATABASE}" -POSTGRES_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}/${POSTGRES_DATABASE}" -POSTGRES_PRISMA_URL="${POSTGRES_URL}?pgbouncer=true&connect_timeout=15" -POSTGRES_URL_NO_SSL="${POSTGRES_URL}" -POSTGRES_URL_NON_POOLING="${POSTGRES_URL}" - -# Application Variables -NEXT_PUBLIC_APP_NAME="Langtrace" -NEXT_PUBLIC_ENVIRONMENT="production" -NEXT_PUBLIC_HOST="http://localhost:3000" -NEXTAUTH_SECRET="difficultsecret" -NEXTAUTH_URL="${NEXT_PUBLIC_HOST}" -NEXTAUTH_URL_INTERNAL="${NEXT_PUBLIC_HOST}" - -# Clickhouse Variables -CLICK_HOUSE_HOST="http://langtrace-clickhouse:8123" -CLICK_HOUSE_USER="lt_clickhouse_user" -CLICK_HOUSE_PASSWORD="clickhousepw" -CLICK_HOUSE_DATABASE_NAME="langtrace_traces" - -# Admin login -ADMIN_EMAIL="admin@langtrace.ai" -ADMIN_PASSWORD="langtraceadminpw" -NEXT_PUBLIC_ENABLE_ADMIN_LOGIN="true" - -# Azure AD Variables -AZURE_AD_CLIENT_ID="" -AZURE_AD_CLIENT_SECRET="" -AZURE_AD_TENANT_ID="" - -POSTHOG_HOST="https://us.i.posthog.com" - -TELEMETRY_ENABLED="true" \ No newline at end of file diff --git a/packages/jobs/.env.example b/packages/jobs/.env.example deleted file mode 100644 index ab55d18..0000000 --- a/packages/jobs/.env.example +++ /dev/null @@ -1,4 +0,0 @@ -SUPABASE_PROJECT_URL=http://127.0.0.1:54321 -SUPABASE_SERVICE_ROLE_KEY= -TRIGGER_PROJECT_ID=proj_your_triger_project_id -TRIGGER_API_URL=http://localhost:3040 diff --git a/packages/jobs/lib/supabase.ts b/packages/jobs/lib/supabase.ts index 19bcc50..0a6b8c6 100644 --- a/packages/jobs/lib/supabase.ts +++ b/packages/jobs/lib/supabase.ts @@ -1,13 +1,7 @@ -// TODO: should be moved to @eda/supabase, but currently having error: SyntaxError: Cannot use import statement outside a module +import { config } from "@eda/config"; import { createClient } from "@supabase/supabase-js"; -// Generate the Typescript types using the Supabase CLI: https://supabase.com/docs/guides/api/rest/generating-types -// import type { Database } from "database.types"; -// Create a single Supabase client for interacting with your database -// 'Database' supplies the type definitions to supabase-js -// export const supabase = createClient( export const supabase = createClient( - // These details can be found in your Supabase project settings under `API` - process.env.SUPABASE_PROJECT_URL as string, // e.g. https://abc123.supabase.co - replace 'abc123' with your project ID - process.env.SUPABASE_SERVICE_ROLE_KEY as string, // Your service role secret key + config.databases.supabase.url, + config.api_keys.supabase.service_key, ); diff --git a/packages/jobs/schemas/monitor-api-calls.schema.ts b/packages/jobs/schemas/monitor-api-calls.schema.ts new file mode 100644 index 0000000..32d5e5e --- /dev/null +++ b/packages/jobs/schemas/monitor-api-calls.schema.ts @@ -0,0 +1,13 @@ +import { z } from "zod"; + +export const apiCallSchema = z.object({ + endpoint: z.string(), + method: z.string(), + statusCode: z.number(), + duration: z.number(), + timestamp: z.string(), + sessionId: z.string(), + error: z.string().optional(), +}); + +export type ApiCall = z.infer; diff --git a/packages/jobs/trigger.config.ts b/packages/jobs/trigger.config.ts index a0b40a9..a06e293 100644 --- a/packages/jobs/trigger.config.ts +++ b/packages/jobs/trigger.config.ts @@ -1,10 +1,8 @@ +import { config as edaConfig } from "@eda/config"; import type { TriggerConfig } from "@trigger.dev/sdk/v3"; -import { config as dotenvConfig } from "dotenv"; - -dotenvConfig(); export const config: TriggerConfig = { - project: process.env.TRIGGER_PROJECT_ID ?? "", + project: edaConfig.services.trigger.project_id, logLevel: "log", maxDuration: 360, retries: { diff --git a/packages/jobs/trigger/monitor-api-call.ts b/packages/jobs/trigger/monitor-api-call.ts index f63119b..2411a1e 100644 --- a/packages/jobs/trigger/monitor-api-call.ts +++ b/packages/jobs/trigger/monitor-api-call.ts @@ -1,20 +1,10 @@ import { task } from "@trigger.dev/sdk/v3"; -import { z } from "zod"; import { logger } from "../../logger/src"; - -const apiCallSchema = z.object({ - endpoint: z.string(), - method: z.string(), - statusCode: z.number(), - duration: z.number(), - timestamp: z.string(), - sessionId: z.string(), - error: z.string().optional(), -}); +import type { ApiCall } from "../schemas/monitor-api-calls.schema"; export const monitorApiCallTask = task({ id: "monitor-api-call", - run: async (payload: z.infer, { ctx }) => { + run: async (payload: ApiCall, { ctx }) => { logger.info("API Call monitored", { endpoint: payload.endpoint, statusCode: payload.statusCode, From 7a2d4279f90279008d76a01a1aa45c71a744bf34 Mon Sep 17 00:00:00 2001 From: Luis Otavio Date: Wed, 22 Jan 2025 00:53:19 -0300 Subject: [PATCH 50/75] Refactor: Replace environment variables with centralized config for API keys and AI model --- packages/simulator/.env.example | 5 - packages/simulator/src/utils.ts | 224 +++++++++++++++----------------- 2 files changed, 105 insertions(+), 124 deletions(-) delete mode 100644 packages/simulator/.env.example diff --git a/packages/simulator/.env.example b/packages/simulator/.env.example deleted file mode 100644 index 9b4da8c..0000000 --- a/packages/simulator/.env.example +++ /dev/null @@ -1,5 +0,0 @@ -TRIGGER_SECRET_KEY=tr_dev_xxxxx -TRIGGER_API_URL=http://localhost:3040 -LANGTRACE_API_KEY=your_langtrace_api_key_here -GROQ_API_KEY=your_groq_api_key_here -AI_MODEL=llama-3.1-70b-versatile diff --git a/packages/simulator/src/utils.ts b/packages/simulator/src/utils.ts index 7efbcbb..08574bc 100644 --- a/packages/simulator/src/utils.ts +++ b/packages/simulator/src/utils.ts @@ -1,153 +1,139 @@ +import { createHash } from "node:crypto"; +import fs from "node:fs"; import os from "node:os"; import path from "node:path"; -import fs from "node:fs"; -import { createHash } from "node:crypto"; import Cerebras from "@cerebras/cerebras_cloud_sdk"; -import { Groq } from "groq-sdk"; +import { config } from "@eda/config"; import { logger } from "@eda/logger"; +import { Groq } from "groq-sdk"; import type { Messages } from "./types"; const DEFAULT_APP_FOLDER = path.join( - os.homedir(), - ".earth-defenders-assistant", + os.homedir(), + ".earth-defenders-assistant", ); -const cerebrasKey = process.env.CEREBRAS_API_KEY; -const groqKey = process.env.GROQ_API_KEY; -let client: Cerebras | Groq; -let defaultModel: string; -if (cerebrasKey) { - client = new Cerebras({ - apiKey: cerebrasKey, - }); - defaultModel = "llama3.1-8b"; -} else if (groqKey) { - client = new Groq({ - apiKey: groqKey, - }); - defaultModel = "llama-3.1-8b-instant"; -} else { - throw new Error( - "Neither CEREBRAS_API_KEY nor GROQ_API_KEY is set in the environment.", - ); -} -const MODEL = process.env.AI_MODEL || defaultModel; +// Replace environment variables with config +const groqKey = config.api_keys.groq; + +// Initialize client with Groq by default +const client = new Groq({ + apiKey: groqKey, +}); +// Use the AI model from config +const MODEL = config.ai_models.standard.model; interface CachedItem { - data: T; - timestamp: number; + data: T; + timestamp: number; } interface CacheOptions { - duration?: number; // Duration in milliseconds - subfolder?: string; // Subfolder within DEFAULT_APP_FOLDER + duration?: number; // Duration in milliseconds + subfolder?: string; // Subfolder within DEFAULT_APP_FOLDER } // Default cache duration is 1 hour const DEFAULT_CACHE_DURATION = 60 * 60 * 1000; function createCache(options: CacheOptions = {}) { - const { - duration = DEFAULT_CACHE_DURATION, - subfolder = 'simulator/.cache' - } = options; - - const cacheDir = path.join(DEFAULT_APP_FOLDER, subfolder); - logger.debug(`Cache directory: ${cacheDir}`); - - async function get(key: string): Promise { - try { - const cacheFile = path.join(cacheDir, `${key}.json`); - logger.debug(`Attempting to read cache file: ${cacheFile}`); - const data = await fs.promises.readFile(cacheFile, 'utf8'); - const cached = JSON.parse(data) as CachedItem; - - if (Date.now() - cached.timestamp < duration) { - logger.debug(`Cache hit for key: ${key}`); - return cached.data; - } - logger.debug(`Cache expired for key: ${key}`); - return null; - } catch (error) { - console.error(error); - logger.debug(`Cache miss for key: ${key}`, { error }); - return null; - } - } + const { duration = DEFAULT_CACHE_DURATION, subfolder = "simulator/.cache" } = + options; + + const cacheDir = path.join(DEFAULT_APP_FOLDER, subfolder); + logger.debug(`Cache directory: ${cacheDir}`); - async function set(key: string, data: T): Promise { - const item: CachedItem = { - data, - timestamp: Date.now() - }; - - logger.debug(`Creating cache directory: ${cacheDir}`); - await fs.promises.mkdir(cacheDir, { recursive: true }); - - const cacheFile = path.join(cacheDir, `${key}.json`); - logger.debug(`Writing to cache file: ${cacheFile}`); - await fs.promises.writeFile( - cacheFile, - JSON.stringify(item) - ); + async function get(key: string): Promise { + try { + const cacheFile = path.join(cacheDir, `${key}.json`); + logger.debug(`Attempting to read cache file: ${cacheFile}`); + const data = await fs.promises.readFile(cacheFile, "utf8"); + const cached = JSON.parse(data) as CachedItem; + + if (Date.now() - cached.timestamp < duration) { + logger.debug(`Cache hit for key: ${key}`); + return cached.data; + } + logger.debug(`Cache expired for key: ${key}`); + return null; + } catch (error) { + console.error(error); + logger.debug(`Cache miss for key: ${key}`, { error }); + return null; } + } + + async function set(key: string, data: T): Promise { + const item: CachedItem = { + data, + timestamp: Date.now(), + }; + + logger.debug(`Creating cache directory: ${cacheDir}`); + await fs.promises.mkdir(cacheDir, { recursive: true }); + + const cacheFile = path.join(cacheDir, `${key}.json`); + logger.debug(`Writing to cache file: ${cacheFile}`); + await fs.promises.writeFile(cacheFile, JSON.stringify(item)); + } - return { get, set }; + return { get, set }; } // Create cache instance for message generation const messageCache = createCache<{ - id: string; - timestamp: number; - [key: string]: unknown; + id: string; + timestamp: number; + [key: string]: unknown; }>(); export async function generateWhatsAppMessage(messages: Messages) { - const cacheKey = createHash('md5') - .update(JSON.stringify(messages)) - .digest('hex'); - - logger.debug(`Generated cache key: ${cacheKey}`); + const cacheKey = createHash("md5") + .update(JSON.stringify(messages)) + .digest("hex"); + + logger.debug(`Generated cache key: ${cacheKey}`); + + try { + // Try to get from cache first + logger.debug("Attempting to retrieve from cache"); + const cached = await messageCache.get(cacheKey); + if (cached) { + logger.debug("Found message in cache"); + return cached; + } - try { - // Try to get from cache first - logger.debug('Attempting to retrieve from cache'); - const cached = await messageCache.get(cacheKey); - if (cached) { - logger.debug('Found message in cache'); - return cached; - } - - logger.debug('Cache miss, generating new message'); - const params = { - messages, - model: MODEL, - response_format: { - type: "json_object", - }, - }; - - logger.debug('Calling AI service', { model: MODEL }); - const generatedMessage = await client.chat.completions.create(params); - if (!generatedMessage.choices[0]?.message?.content) { - logger.error('Invalid response from AI service'); - throw new Error("Invalid response from OpenAI"); - } - - const content = generatedMessage.choices[0].message.content; - const payload = { - ...JSON.parse(content), - id: crypto.randomUUID(), - timestamp: Date.now(), - }; - - logger.debug('Caching generated message'); - await messageCache.set(cacheKey, payload); - - return payload; - } catch (error) { - logger.error("Error in message generation:", { error }); - throw error; + logger.debug("Cache miss, generating new message"); + const params: ChatCompletionCreateParamsNonStreaming = { + messages: messages.map((msg) => ({ + role: msg.role, + content: msg.content, + })), + model: MODEL, + response_format: { type: "json_object" }, + }; + + logger.debug("Calling AI service", { model: MODEL }); + const generatedMessage = await client.chat.completions.create(params); + if (!generatedMessage.choices[0]?.message?.content) { + logger.error("Invalid response from AI service"); + throw new Error("Invalid response from OpenAI"); } + + const content = generatedMessage.choices[0].message.content; + const payload = { + ...JSON.parse(content), + id: crypto.randomUUID(), + timestamp: Date.now(), + }; + + logger.debug("Caching generated message"); + await messageCache.set(cacheKey, payload); + + return payload; + } catch (error) { + logger.error("Error in message generation:", { error }); + throw error; + } } From 2399e2766c878fe3c9f3b96d7060e1214313bc28 Mon Sep 17 00:00:00 2001 From: Luis Otavio Date: Wed, 22 Jan 2025 00:57:53 -0300 Subject: [PATCH 51/75] Refactor: Remove unused example environment files from grant_plugin and onboarding --- plugins/grant_plugin/opportunity_finder/.env.example | 7 ------- plugins/grant_plugin/proposal_writer/.env.example | 7 ------- plugins/onboarding/.env.example | 1 - 3 files changed, 15 deletions(-) delete mode 100644 plugins/grant_plugin/opportunity_finder/.env.example delete mode 100644 plugins/grant_plugin/proposal_writer/.env.example delete mode 100644 plugins/onboarding/.env.example diff --git a/plugins/grant_plugin/opportunity_finder/.env.example b/plugins/grant_plugin/opportunity_finder/.env.example deleted file mode 100644 index c109415..0000000 --- a/plugins/grant_plugin/opportunity_finder/.env.example +++ /dev/null @@ -1,7 +0,0 @@ -OPENAI_API_KEY=sk-xxxx # NOT USED -CEREBRAS_API_KEY=csk-xxxx # NOT USED -SERPER_API_KEY=xxxx # Needed to use Serper tool -LANGTRACE_API_KEY=xxxx -LANGTRACE_API_HOST=http://localhost:3000/api/trace -SAMBANOVA_API_KEY=xxxx # NOT USED -GROQ_API_KEY=xxxx # API key for the LLM model \ No newline at end of file diff --git a/plugins/grant_plugin/proposal_writer/.env.example b/plugins/grant_plugin/proposal_writer/.env.example deleted file mode 100644 index c109415..0000000 --- a/plugins/grant_plugin/proposal_writer/.env.example +++ /dev/null @@ -1,7 +0,0 @@ -OPENAI_API_KEY=sk-xxxx # NOT USED -CEREBRAS_API_KEY=csk-xxxx # NOT USED -SERPER_API_KEY=xxxx # Needed to use Serper tool -LANGTRACE_API_KEY=xxxx -LANGTRACE_API_HOST=http://localhost:3000/api/trace -SAMBANOVA_API_KEY=xxxx # NOT USED -GROQ_API_KEY=xxxx # API key for the LLM model \ No newline at end of file diff --git a/plugins/onboarding/.env.example b/plugins/onboarding/.env.example deleted file mode 100644 index 5991433..0000000 --- a/plugins/onboarding/.env.example +++ /dev/null @@ -1 +0,0 @@ -GROQ_API_KEY=xxxx # LLM Used for the onboarding crew From 8b0f467dcd3569104266b64b7800ef8868167356 Mon Sep 17 00:00:00 2001 From: Luis Otavio Date: Wed, 22 Jan 2025 01:17:30 -0300 Subject: [PATCH 52/75] Refactor: Update example configuration with specific database credentials and URLs --- config.example.yaml | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/config.example.yaml b/config.example.yaml index 221b1ec..e550cff 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -39,37 +39,37 @@ api_keys: # Database Settings databases: supabase: - url: http://127.0.0.1:54321 + url: "http://localhost:54321" project_id: "create-eda" langtrace_postgres: host: "langtrace-postgres" port: 5432 - user: "xxx" - password: "xxx" + user: "ltuser" + password: "ltpasswd" database: "langtrace" langtrace_clickhouse: host: "http://langtrace-clickhouse:8123" - user: "xxx" - password: "xxx" + user: "lt_clickhouse_user" + password: "clickhousepw" database: "langtrace_traces" trigger_postgres: - host: postgres - user: "xxx" - password: "xxx" - database: postgres + host: "postgres" + user: "postgres" + password: "postgres" + database: "postgres" redis: - host: redis + host: "redis" tls_disabled: true neo4j: - host: localhost + host: "localhost" auth: - user: "xxx" - password: "xxx" + user: "neo4j" + password: "password" plugins: ["apoc"] healthcheck: interval: 10 @@ -96,20 +96,20 @@ services: mongodb_uri: "xxx" resend: - from_email: "xxx" - reply_to: "xxx" + from_email: "onboarding@resend.dev" + reply_to: "xxx@example.com" langtrace: api: host: "http://localhost:3000/api/trace" admin: - email: "xxx" + email: "admin@langtrace.ai" password: "xxx" enable_login: true telemetry: enabled: true posthog: - host: "xxx" + host: "https://us.i.posthog.com" trigger: project_id: "xxx" @@ -130,7 +130,7 @@ services: worker: http_port: 9020 coordinator_host: "127.0.0.1" - coordinator_port: 9020 + coordinator_port: 9021 docker: publish_ip: "127.0.0.1" sentry: From 72938f8d58823e36dbda875c31881b7b27ab64c8 Mon Sep 17 00:00:00 2001 From: Luis Otavio Date: Wed, 22 Jan 2025 01:32:09 -0300 Subject: [PATCH 53/75] Refactor: Add README documentation for Messaging API with features, installation, and usage instructions --- apps/messaging/README.md | 106 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 apps/messaging/README.md diff --git a/apps/messaging/README.md b/apps/messaging/README.md new file mode 100644 index 0000000..db502a5 --- /dev/null +++ b/apps/messaging/README.md @@ -0,0 +1,106 @@ +# Earth Defenders Assistant - Messaging API + +![Badge](https://img.shields.io/badge/Built%20with-Elysia-purple) +![Badge](https://img.shields.io/badge/Documentation-Swagger-green) +![Badge](https://img.shields.io/badge/Runtime-Bun-black) + +A robust messaging API service built with Elysia.js for handling cross-platform messaging in the Earth Defenders Assistant ecosystem. This API provides seamless integration for WhatsApp, Telegram, and Simulator platforms with AI-powered message processing capabilities. + +## Features + +- **Cross-Platform Support**: Handle messages from WhatsApp, Telegram, and Simulator +- **AI Integration**: Process messages through integrated AI services +- **Audio Message Support**: Handle and process audio messages +- **Conversation History**: Track and maintain conversation history using Supabase +- **Real-time Monitoring**: Monitor API calls and performance using Trigger.dev +- **Swagger Documentation**: Built-in API documentation +- **Health Monitoring**: Endpoint for service health checks + +## Prerequisites + +- Bun Runtime +- Supabase Account +- Required API Keys (configured in config.yaml) + +## Installation + +1. Clone the repository +2. Install dependencies: +```bash +bun install +``` + +## Usage + +### Development +```bash +bun run dev +``` + +### Production Build +```bash +bun run build +``` + +### Start Production Server +```bash +bun run start +``` + +## API Endpoints + +### Send Message +```http +POST /api/messages/send +``` + +Request Body: +```json +{ + "message": "Hello world", + "sessionId": "5515991306053", + "audio": "base64-encoded-audio-string", // Optional + "platform": "whatsapp" // Optional: "whatsapp" | "telegram" | "simulator" +} +``` + +### Health Check +```http +GET /api/messages/health +``` + +## Development + +### Available Scripts + +- `bun run dev` - Start development server with hot reload +- `bun run build` - Build for production +- `bun run start` - Start production server +- `bun run test` - Run tests +- `bun run lint` - Run linting +- `bun run format` - Format code +- `bun run typecheck` - Check types + +### Project Structure + +``` +src/ +β”œβ”€β”€ index.ts # Main application entry +β”œβ”€β”€ routes.ts # API route handlers +└── types.ts # TypeScript type definitions +``` + +## Error Handling + +The API implements comprehensive error handling with: +- Input validation using Zod +- Detailed error logging +- Standardized error responses +- Monitoring of failed API calls + +## Documentation + +Swagger documentation is automatically generated and available at: +``` +http://localhost:3001/swagger +``` From 9ade4ec21703e5af806e1f7f632d812c41af42a0 Mon Sep 17 00:00:00 2001 From: Luis Otavio Date: Wed, 22 Jan 2025 02:05:29 -0300 Subject: [PATCH 54/75] Lint --- .../eda_ai_api/api/routes/classifier.py | 110 +++++++++--------- apps/ai_api/eda_ai_api/core/config.py | 3 - apps/ai_api/eda_ai_api/core/security.py | 7 +- apps/ai_api/eda_ai_api/main.py | 16 ++- apps/ai_api/eda_ai_api/models/classifier.py | 3 +- apps/ai_api/eda_ai_api/utils/memory.py | 8 +- apps/ai_api/tox.ini | 2 +- package.json | 4 +- 8 files changed, 83 insertions(+), 70 deletions(-) diff --git a/apps/ai_api/eda_ai_api/api/routes/classifier.py b/apps/ai_api/eda_ai_api/api/routes/classifier.py index a7ecbff..53f5227 100644 --- a/apps/ai_api/eda_ai_api/api/routes/classifier.py +++ b/apps/ai_api/eda_ai_api/api/routes/classifier.py @@ -1,23 +1,22 @@ import json -import os -from typing import Optional, List, Dict import uuid +from typing import Dict, Optional + +import httpx +from eda_config.config import ConfigLoader # Add this import from fastapi import APIRouter, File, Form, UploadFile -from langchain_groq import ChatGroq from langchain_core.messages import HumanMessage +from langchain_groq import ChatGroq from loguru import logger -import httpx from eda_ai_api.models.classifier import ClassifierResponse, MessageHistory from eda_ai_api.utils.audio_utils import process_audio_file -from eda_ai_api.utils.memory import SupabaseMemory, Mem0ConversationManager +from eda_ai_api.utils.memory import SupabaseMemory from eda_ai_api.utils.prompts import ( - ROUTER_TEMPLATE, INSUFFICIENT_TEMPLATES, - PROPOSAL_TEMPLATE, + ROUTER_TEMPLATE, TOPIC_TEMPLATE, ) -from eda_config.config import ConfigLoader # Add this import config = ConfigLoader.get_config() # Add this line @@ -34,9 +33,12 @@ async def process_discovery( - message: str, context: str, platform: str = "default" + message: str, context: str, platform: Optional[str] = "default" ) -> Dict[str, str]: """Process discovery requests""" + # Ensure platform is never None + actual_platform = platform or "default" + response = llm.invoke( [HumanMessage(content=TOPIC_TEMPLATE.format(context=context, message=message))] ) @@ -55,7 +57,7 @@ async def process_discovery( discovery_port = config.ports.ai_api api_response = await client.post( f"http://127.0.0.1:{discovery_port}/api/grant/discovery", - json={"topics": topics, "platform": platform}, + json={"topics": topics, "platform": actual_platform}, ) logger.info(f"Discovery API Response: {api_response.json()}") return {"result": str(api_response.json())} @@ -73,6 +75,30 @@ async def route_message(message: str, context: str) -> str: return decision +async def process_input_messages( + message: Optional[str], audio: Optional[UploadFile] +) -> list[str]: + combined_message = [] + if audio: + transcription = await process_audio_file(audio) + combined_message.append(f'Transcription: "{transcription}"') + if message: + combined_message.append(f'Message: "{message}"') + return combined_message + + +async def process_message_history( + message_history: Optional[str], +) -> list[MessageHistory]: + history = [] + if message_history: + try: + history = [MessageHistory(**msg) for msg in json.loads(message_history)] + except json.JSONDecodeError: + logger.warning("Invalid message_history JSON format") + return history + + @router.post("/classify", response_model=ClassifierResponse) async def classifier_route( message: Optional[str] = Form(default=None), @@ -90,60 +116,38 @@ async def classifier_route( # Initialize/get Mem0 session - Disabled # current_session = await mem0_manager.get_or_create_session(session_id=current_session) - # Process inputs - combined_message = [] - if audio: - transcription = await process_audio_file(audio) - combined_message.append(f'Transcription: "{transcription}"') - if message: - combined_message.append(f'Message: "{message}"') - + combined_message = await process_input_messages(message, audio) if not combined_message: return ClassifierResponse(result="Error: No valid input provided") - # Get conversation history from Supabase - history = [] - if message_history: - try: - history = [MessageHistory(**msg) for msg in json.loads(message_history)] - except json.JSONDecodeError: - logger.warning("Invalid message_history JSON format") - - # Get context from Supabase only + history = await process_message_history(message_history) supabase_context = SupabaseMemory.format_history(history) + final_message = "\n".join(combined_message) decision = await route_message(final_message, supabase_context) - logger.info(f"Routing decision: {decision}") - - # Update logging when processing route decision logger.info(f"Processing {decision} request for platform: {platform}") - # Process based on route - if decision == "discovery": - response = await process_discovery( - final_message, supabase_context, platform - ) - logger.info(f"Discovery response for platform {platform}: {response}") - elif decision == "heartbeat": - response = {"result": "*Yes, I'm here! 🟒*\n_Ready to help you!_"} - else: - response = {"result": f"Service '{decision}' not implemented yet"} - - result = response["result"] - if len(result) > 2499: - result = result[:2499] - - # Disabled Mem0 storage - # await mem0_manager.add_conversation( - # session_id=current_session, - # user_message=final_message, - # assistant_response=result, - # ) - - response = {"result": result, "platform": platform} + response = await process_route_decision( + decision, final_message, supabase_context, platform + ) + result = response["result"][:2499] # Truncate if needed return ClassifierResponse(result=result, session_id=current_session) except Exception as e: logger.error(f"Error in classifier route: {str(e)}") return ClassifierResponse(result=f"Error: {str(e)}") + + +async def process_route_decision( + decision: str, message: str, context: str, platform: Optional[str] +) -> Dict[str, str]: + if decision == "discovery": + response = await process_discovery(message, context, platform or "default") + logger.info(f"Discovery response for platform {platform}: {response}") + elif decision == "heartbeat": + response = {"result": "*Yes, I'm here! 🟒*\n_Ready to help you!_"} + else: + response = {"result": f"Service '{decision}' not implemented yet"} + + return response diff --git a/apps/ai_api/eda_ai_api/core/config.py b/apps/ai_api/eda_ai_api/core/config.py index 4aec1d6..e26cee8 100644 --- a/apps/ai_api/eda_ai_api/core/config.py +++ b/apps/ai_api/eda_ai_api/core/config.py @@ -1,6 +1,3 @@ -from starlette.config import Config -from starlette.datastructures import Secret - APP_VERSION = "0.0.1" APP_NAME = "AI API for Earth Defenders Assistant" API_PREFIX = "/api" diff --git a/apps/ai_api/eda_ai_api/core/security.py b/apps/ai_api/eda_ai_api/core/security.py index ef1815e..982e410 100644 --- a/apps/ai_api/eda_ai_api/core/security.py +++ b/apps/ai_api/eda_ai_api/core/security.py @@ -1,13 +1,14 @@ import secrets from typing import Optional +from eda_config import ConfigLoader from fastapi import HTTPException, Security from fastapi.security.api_key import APIKeyHeader from starlette.status import HTTP_400_BAD_REQUEST, HTTP_401_UNAUTHORIZED -from eda_ai_api.core import config from eda_ai_api.core.messages import AUTH_REQ, NO_API_KEY +config = ConfigLoader.get_config() api_key = APIKeyHeader(name="token", auto_error=False) @@ -16,7 +17,9 @@ def validate_request(header: Optional[str] = Security(api_key)) -> bool: raise HTTPException( status_code=HTTP_400_BAD_REQUEST, detail=NO_API_KEY, headers={} ) - if not secrets.compare_digest(header, str(config.API_KEY)): + if not secrets.compare_digest( + header, str(config.api_keys.groq) + ): # Using GROQ API key as an example raise HTTPException( status_code=HTTP_401_UNAUTHORIZED, detail=AUTH_REQ, headers={} ) diff --git a/apps/ai_api/eda_ai_api/main.py b/apps/ai_api/eda_ai_api/main.py index 47acaf2..648b210 100644 --- a/apps/ai_api/eda_ai_api/main.py +++ b/apps/ai_api/eda_ai_api/main.py @@ -1,5 +1,6 @@ -from fastapi import FastAPI from eda_config import ConfigLoader +from fastapi import FastAPI + from eda_ai_api.api.routes.router import api_router from eda_ai_api.core.config import API_PREFIX, APP_NAME, APP_VERSION from eda_ai_api.core.event_handlers import start_app_handler, stop_app_handler @@ -24,13 +25,18 @@ def get_app() -> FastAPI: app = get_app() -def run_server(): +def run_server() -> None: """Run the API server using config settings""" import uvicorn - uvicorn.run( - "eda_ai_api.main:app", host="0.0.0.0", port=config.ports.ai_api, reload=True - ) + # Use localhost for development, configure via config for production + host = "127.0.0.1" # Default to localhost + if config.services.ai_api.get( + "allow_external", False + ): # Only if explicitly enabled + host = "0.0.0.0" # nosec B104 # Explicitly allowed in config + + uvicorn.run("eda_ai_api.main:app", host=host, port=config.ports.ai_api, reload=True) if __name__ == "__main__": diff --git a/apps/ai_api/eda_ai_api/models/classifier.py b/apps/ai_api/eda_ai_api/models/classifier.py index 8491bb3..0833aa4 100644 --- a/apps/ai_api/eda_ai_api/models/classifier.py +++ b/apps/ai_api/eda_ai_api/models/classifier.py @@ -1,4 +1,5 @@ -from typing import Optional, List, Dict +from typing import List, Optional + from fastapi import UploadFile from pydantic import BaseModel diff --git a/apps/ai_api/eda_ai_api/utils/memory.py b/apps/ai_api/eda_ai_api/utils/memory.py index 8127037..31705e1 100644 --- a/apps/ai_api/eda_ai_api/utils/memory.py +++ b/apps/ai_api/eda_ai_api/utils/memory.py @@ -1,17 +1,19 @@ -import uuid import os +import uuid from typing import Dict, List, Optional + from dotenv import load_dotenv -from eda_ai_api.models.classifier import MessageHistory from loguru import logger from mem0 import Memory +from eda_ai_api.models.classifier import MessageHistory + # Load environment variables load_dotenv() class Mem0ConversationManager: - def __init__(self): + def __init__(self) -> None: # Validate API keys groq_api_key = os.getenv("GROQ_API_KEY") huggingface_api_key = os.getenv("HUGGINGFACE_API_KEY") diff --git a/apps/ai_api/tox.ini b/apps/ai_api/tox.ini index c9147da..755a562 100644 --- a/apps/ai_api/tox.ini +++ b/apps/ai_api/tox.ini @@ -60,5 +60,5 @@ exclude = max-complexity = 10 import-order-style = google application-import-names = flake8 -max-line-length = 90 +max-line-length = 100 ignore = B008 \ No newline at end of file diff --git a/package.json b/package.json index 1718f86..74e91c1 100644 --- a/package.json +++ b/package.json @@ -34,8 +34,8 @@ "start:dashboard": "turbo start --filter=@eda/dashboard", "test": "turbo test --parallel", "format": "biome format --write .", - "lint:repo": "bunx sherif@latest -r unordered-dependencies -r multiple-dependency-versions", - "lint:repo:fix": "bunx sherif@latest -r unordered-dependencies -r multiple-dependency-versions --fix", + "lint:repo": "bunx sherif@latest -r unordered-dependencies -r multiple-dependency-versions -r packages-without-package-json -r root-package-dependencies", + "lint:repo:fix": "bunx sherif@latest -r unordered-dependencies -r multiple-dependency-versions -r packages-without-package-json -r root-package-dependencies --fix", "typecheck": "turbo typecheck", "build:config": "turbo run build --filter=@eda/config --filter=@eda/config-python" }, From ce1e95bcacbdfefb51d530d501fc14a12c1cb360 Mon Sep 17 00:00:00 2001 From: Luis Otavio Date: Wed, 22 Jan 2025 02:20:05 -0300 Subject: [PATCH 55/75] Refactor: Update GitHub Actions workflow to use Bun for dependency management and environment configuration --- .github/workflows/stacks.yml | 39 ++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/.github/workflows/stacks.yml b/.github/workflows/stacks.yml index d0b3d37..ebe8f30 100644 --- a/.github/workflows/stacks.yml +++ b/.github/workflows/stacks.yml @@ -15,6 +15,14 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Install dependencies + run: bun install + - name: Create Docker network run: docker network create eda-network || true @@ -25,8 +33,8 @@ jobs: echo "Processing $dir" cd "$dir" - # Copy environment file - cp .env.example .env + # Generate env file from config + bun run export-config.ts > .env # Start containers docker compose up -d @@ -48,32 +56,23 @@ jobs: # Extract the service name from directory path service_name=$(basename "$dir") + port=$(bun run @eda/config getPorts $service_name) - # First check for HEALTHCHECK URL in .env file - if [ -f "$dir/.env" ]; then - healthcheck_url=$(grep '^HEALTHCHECK=' "$dir/.env" | cut -d '=' -f2) - if [ -n "$healthcheck_url" ]; then - test_url="$healthcheck_url" - else - # If no HEALTHCHECK, fall back to PORT - port=$(grep '^PORT=' "$dir/.env" | cut -d '=' -f2) - if [ -z "$port" ]; then - echo "No PORT found in .env for $service_name, using default 3000" - port=3000 - fi - test_url="http://localhost:${port}/" - fi - else - echo "No .env file found for $service_name, using default 3000" - test_url="http://localhost:3000/" + # Default to 3000 if no port found + if [ -z "$port" ]; then + echo "No port found for $service_name in config, using default 3000" + port=3000 fi + test_url="http://localhost:${port}/" + # Test the endpoint curl --fail "$test_url" || { echo "Failed to reach $test_url for $service_name" exit 1 } done < $GITHUB_WORKSPACE/deploy_dirs.txt + - name: Cleanup if: always() run: | @@ -81,4 +80,4 @@ jobs: cd "$dir" docker compose down cd $GITHUB_WORKSPACE - done < $GITHUB_WORKSPACE/deploy_dirs.txt + done < $GITHUB_WORKSPACE/deploy_dirs.txt \ No newline at end of file From 882884508351568a6a54fb609ec5ae82cc66438b Mon Sep 17 00:00:00 2001 From: Luis Otavio Date: Wed, 22 Jan 2025 02:25:02 -0300 Subject: [PATCH 56/75] Update stacks.yaml --- .github/workflows/stacks.yml | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/.github/workflows/stacks.yml b/.github/workflows/stacks.yml index ebe8f30..9df7e08 100644 --- a/.github/workflows/stacks.yml +++ b/.github/workflows/stacks.yml @@ -23,6 +23,9 @@ jobs: - name: Install dependencies run: bun install + - name: Build config package + run: bun run build:config + - name: Create Docker network run: docker network create eda-network || true @@ -33,10 +36,7 @@ jobs: echo "Processing $dir" cd "$dir" - # Generate env file from config - bun run export-config.ts > .env - - # Start containers + # Start containers directly with config docker compose up -d # Store the directory name for later use @@ -54,11 +54,9 @@ jobs: while IFS= read -r dir; do echo "Testing endpoints for $dir" - # Extract the service name from directory path service_name=$(basename "$dir") port=$(bun run @eda/config getPorts $service_name) - # Default to 3000 if no port found if [ -z "$port" ]; then echo "No port found for $service_name in config, using default 3000" port=3000 @@ -66,7 +64,6 @@ jobs: test_url="http://localhost:${port}/" - # Test the endpoint curl --fail "$test_url" || { echo "Failed to reach $test_url for $service_name" exit 1 From 8c80db657a639681effa23fb72adf9359a59a9f1 Mon Sep 17 00:00:00 2001 From: Luis Otavio Date: Wed, 22 Jan 2025 02:33:40 -0300 Subject: [PATCH 57/75] Refactor: Update GitHub Actions workflow to install UV and activate virtual environment --- .github/workflows/stacks.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/stacks.yml b/.github/workflows/stacks.yml index 9df7e08..f23321e 100644 --- a/.github/workflows/stacks.yml +++ b/.github/workflows/stacks.yml @@ -15,13 +15,19 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Install UV + uses: astral-sh/setup-uv@v3 + - name: Setup Bun uses: oven-sh/setup-bun@v2 with: bun-version: latest - name: Install dependencies - run: bun install + run: | + bun install + uv venv + . .venv/bin/activate - name: Build config package run: bun run build:config From a8f6587104a509a244069d13e10500a64c10c2d9 Mon Sep 17 00:00:00 2001 From: Luis Otavio Date: Wed, 22 Jan 2025 02:37:31 -0300 Subject: [PATCH 58/75] Refactor: Improve GitHub Actions workflow by reorganizing steps and adding error handling --- .github/workflows/stacks.yml | 37 +++++++++++++++++++++++------------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/.github/workflows/stacks.yml b/.github/workflows/stacks.yml index f23321e..f4e3022 100644 --- a/.github/workflows/stacks.yml +++ b/.github/workflows/stacks.yml @@ -15,37 +15,45 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Install UV - uses: astral-sh/setup-uv@v3 - - name: Setup Bun uses: oven-sh/setup-bun@v2 with: bun-version: latest + - name: Install UV + uses: astral-sh/setup-uv@v3 + - name: Install dependencies run: | bun install uv venv . .venv/bin/activate - - name: Build config package - run: bun run build:config + - name: Setup config + run: | + cp config.example.yaml config.yaml + bun run build:config - name: Create Docker network run: docker network create eda-network || true - name: Find and process deploy directories run: | + # Create dirs file + touch $GITHUB_WORKSPACE/deploy_dirs.txt + for dir in deploy/*/; do if [ -d "$dir" ]; then echo "Processing $dir" cd "$dir" - # Start containers directly with config + # Generate env from config + bun run export-config.ts > .env + + # Start containers docker compose up -d - # Store the directory name for later use + # Store directory echo "${dir%/}" >> $GITHUB_WORKSPACE/deploy_dirs.txt cd $GITHUB_WORKSPACE @@ -69,8 +77,9 @@ jobs: fi test_url="http://localhost:${port}/" + echo "Testing URL: $test_url" - curl --fail "$test_url" || { + curl --fail --retry 3 --retry-delay 5 "$test_url" || { echo "Failed to reach $test_url for $service_name" exit 1 } @@ -79,8 +88,10 @@ jobs: - name: Cleanup if: always() run: | - while IFS= read -r dir; do - cd "$dir" - docker compose down - cd $GITHUB_WORKSPACE - done < $GITHUB_WORKSPACE/deploy_dirs.txt \ No newline at end of file + if [ -f "$GITHUB_WORKSPACE/deploy_dirs.txt" ]; then + while IFS= read -r dir; do + cd "$dir" + docker compose down + cd $GITHUB_WORKSPACE + done < $GITHUB_WORKSPACE/deploy_dirs.txt + fi \ No newline at end of file From 29bdd3bf2abca60a4d8d8c328ed8e8ab6c73a581 Mon Sep 17 00:00:00 2001 From: Luis Otavio Date: Wed, 22 Jan 2025 02:47:08 -0300 Subject: [PATCH 59/75] Refactor: Update CI workflow and configuration for PostgreSQL and ClickHouse ports --- .github/workflows/ci.yml | 9 +++++++-- config.example.yaml | 2 +- deploy/langtrace-stack/docker-compose.yml | 4 ++-- deploy/langtrace-stack/export-config.ts | 2 ++ 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 430106c..aae3037 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,8 +21,13 @@ jobs: - name: Install dependencies run: bun install - - name: Run builds - run: bun run build + - name: Setup config + run: | + cp config.example.yaml config.yaml + bun run build:config + + - name: Build remaining packages + run: turbo run build --filter=!@eda/config --filter=!@eda/config-python # - name: Run tests # run: bun run test diff --git a/config.example.yaml b/config.example.yaml index e550cff..29e783c 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -44,7 +44,7 @@ databases: langtrace_postgres: host: "langtrace-postgres" - port: 5432 + port: 5435 user: "ltuser" password: "ltpasswd" database: "langtrace" diff --git a/deploy/langtrace-stack/docker-compose.yml b/deploy/langtrace-stack/docker-compose.yml index f32a30b..15e3e0b 100644 --- a/deploy/langtrace-stack/docker-compose.yml +++ b/deploy/langtrace-stack/docker-compose.yml @@ -26,7 +26,7 @@ services: env_file: - .env ports: - - "5432:5432" + - "${POSTGRES_PORT}:5432" volumes: - postgres-data:/var/lib/postgresql/data @@ -39,7 +39,7 @@ services: - CLICKHOUSE_USER=${CLICK_HOUSE_USER} - CLICKHOUSE_DB=${CLICK_HOUSE_DATABASE_NAME} ports: - - "8123:8123" + - "${CLICKHOUSE_PORT}:8123" - "9000:9000" healthcheck: test: wget --no-verbose --tries=1 --spider http://langtrace-clickhouse:8123/ping || exit 1 diff --git a/deploy/langtrace-stack/export-config.ts b/deploy/langtrace-stack/export-config.ts index a86f1bf..94d4259 100644 --- a/deploy/langtrace-stack/export-config.ts +++ b/deploy/langtrace-stack/export-config.ts @@ -16,6 +16,7 @@ const envVars = { POSTGRES_URL_NO_SSL: `postgres://${config.databases.langtrace_postgres.user}:${config.databases.langtrace_postgres.password}@${config.databases.langtrace_postgres.host}/${config.databases.langtrace_postgres.database}`, POSTGRES_URL_NON_POOLING: `postgres://${config.databases.langtrace_postgres.user}:${config.databases.langtrace_postgres.password}@${config.databases.langtrace_postgres.host}/${config.databases.langtrace_postgres.database}`, POSTGRES_IMAGE_TAG: "16", + POSTGRES_PORT: config.ports.db.postgres, // App settings NEXT_PUBLIC_APP_NAME: config.services.langtrace.api.host, @@ -30,6 +31,7 @@ const envVars = { CLICK_HOUSE_USER: config.databases.langtrace_clickhouse.user, CLICK_HOUSE_PASSWORD: config.databases.langtrace_clickhouse.password, CLICK_HOUSE_DATABASE_NAME: config.databases.langtrace_clickhouse.database, + CLICKHOUSE_PORT: config.ports.db.clickhouse, // Admin settings ADMIN_EMAIL: config.services.langtrace.admin.email, From 6d860be605231391e837664e157f5934e3d4b8d0 Mon Sep 17 00:00:00 2001 From: Luis Otavio Date: Wed, 22 Jan 2025 02:52:29 -0300 Subject: [PATCH 60/75] Refactor: Update CI workflow to set up UV and streamline dependency installation; remove unused Zep stack files --- .github/workflows/ci.yml | 10 +++- apps/ai_api/pyproject.toml | 1 - bun.lockb | Bin 732000 -> 730424 bytes deploy/zep-stack/.env.example | 12 ----- deploy/zep-stack/docker-compose.yml | 81 ---------------------------- deploy/zep-stack/zep.yaml | 43 --------------- packages/jobs/package.json | 1 - 7 files changed, 8 insertions(+), 140 deletions(-) delete mode 100644 deploy/zep-stack/.env.example delete mode 100644 deploy/zep-stack/docker-compose.yml delete mode 100644 deploy/zep-stack/zep.yaml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index aae3037..7bf39d7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,8 +18,14 @@ jobs: with: bun-version: latest + - name: Setup UV + uses: astral-sh/setup-uv@v3 + - name: Install dependencies - run: bun install + run: | + bun install + uv venv + . .venv/bin/activate - name: Setup config run: | @@ -30,4 +36,4 @@ jobs: run: turbo run build --filter=!@eda/config --filter=!@eda/config-python # - name: Run tests - # run: bun run test + # run: bun run test \ No newline at end of file diff --git a/apps/ai_api/pyproject.toml b/apps/ai_api/pyproject.toml index e63b319..da5b939 100644 --- a/apps/ai_api/pyproject.toml +++ b/apps/ai_api/pyproject.toml @@ -25,7 +25,6 @@ dependencies = [ "llama-index-vector-stores-postgres>=0.3.2", "llama-index-readers-file>=0.4.1", "httpx>=0.24.0,<0.25.0", - "zep-python>=2.0.2", "mem0ai>=0.1.25", "rank-bm25>=0.2.2", "neo4j>=5.27.0", diff --git a/bun.lockb b/bun.lockb index 8e88040775e4abf98a5435b2019eb8cca3c45675..3eef1a4c7712ee5dff5b9b8fd6bd07b1cf58a010 100755 GIT binary patch delta 75625 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 RKk~cWN0)UilA*5s{{n4~`@aAH delta 76598 zcmeFadwkFJ|NsAdzP1;ym-A?H$YC<38W!^!Ga8m5X)z|0;l<{#qcOxRsY#{vYVMBJ zDuwE;B{|1p4oOU5NQ@RLWobgu?{<4W9-Dc8`n-Ri%lG^F{^xn=)$MV=KF;UI^Z9&v zZNnE2oIdivD{b3+Jvd}{+y_5?A(y1hz+H#BJ^NAAuVa5X+F)Srsw;hWKHu)(ebo(K zXS0VivmN~^FFmqRMrd8r@G*?pX(Kq|AZLF+-=_?m_jni@YYK4EO4aRIyz?02Cxph>Zl7F*Gr7G`4x2n^*dsP#-6DG$` zj(4YyNt~26IeublD}NQP0hDcj4B@B;p=_E@p!YyOL`H1#gv2EDv|+@K8kORnVi@nW zX1EM=IvniG8YmefdS)nH6`=ZTf%cD2u)4@t(MDzbYg1LTZ?*1&aRc1^(b%a`JzdyYCpGA z<*V1ubG^xxj9HycqY*-l(b^B%5cY#w8$sFOHmyH}sE$7jWrycOnd?d@o9S>@rORQn zlpZL@mxL+%1=uWAQex`l(W$A`jZtZ-qY|eXMs7FH^kz*PE$goO=@lr`dk)I@;~(;T z-0b0uxE?BcKDvs@q{hXjB*7inQ#DdOD5Fh|O$i)}>D;-Oik<-_PfDA_;Kq9h$KfzK zE^%rC2K-H=%WOW6P>s3<%5nMB!zx^gnn0eZ&3!UXf~g@h#vK?tDt?{L`!y)z%!RU} z98iX924!C5Wt!SY)nQa@iaRhd)vy$W4eG1Hr%q0B$4*K&T4{SHlx5!pMHw?@=fT0s zmsf67$5__M7@asVk&}1iBPzlNP-c7+%7VlqW&O$kO_CYyFK7n#ocpb{Yk)<^i%2nY>t$m>!G@Z1z(CP!_ z92qb~wcyrAO~Vg%gBVpH1KJ=1;2n5yl#Yr`bq99pXw27kDwG8&dO{WKa(^`}w!Sb~*zCe*wH_Uz!nb--wZN3I@e?z+q+82Tuwi_T0Ib=(r&LqV z&^ivv8jgT+Tpb&!rwfz?S&w+^lJ_+q_YybBLTxPOdlvSv`$EX6$;VGZLNY_1*`HP3ddjDLr+1&>47 zg*(Qon#Q64?1H3ms$#>TtVk3%(@BcUPywb-jh{RkCG(0`1^5uk1aqLQVQHM=KaN*T zdK$`>D1bJG)}5#_xG_O#36$~fouo26rQ`2|vH+t;C#EH!LWeWZrySKiQ6g$5D0kKS zpse{5i7KH%Nv3fx><}nd`Y*sa%0En21*-}k1p6j9XVdUh)e_aA91}gX?FZ!`{5?et z?u-*~u%<1isD!4ux1g4?u7|so4 z#*woX?*p4H^dOW4uLtEi@M)&%suFB{3}4?WDCYm>XH=6{o1;pY8W%qy9)s};Y-aR2 zlzn*;%9h#zIy#N%i`tJjI$dTecmva6=X z#gC2)Oq#TLzA9kN7fr+SbgP!t2EL^FZ2WxB>QlyIlUKaeOc@LS5z0z zgtEdD;uFRMCXO{)XDfRRlofv#;W;E`^!IdW-L%nt9%W9L=uYDTpA?v!=1y}PMIKK| z>rS=>IiA(6+h*J)sl5>sL+Mz_VP zm;U`=QGUQE-=c4A%~s(vW+x`NC&y268xxkQ+HAz8%HEl~Oil5NP!8-#%T)&TUN?=F zuy?@bB6=VExyB^Cp$a-GF>xYS_`cv=m1Dpee}MMy3MFq3t-*ci7!u^pTZRO=ffYeH z;FFO6$I-mERKa}SR$3Lx7Ptb=VQ>L9MvdT^DRKqlgG zJT6$R3i_kgXQ52D%?GBD!6`c(fEhLepmzLFC2$kUmYDf3)iQrWnc(-}oSA!}EWph* zYUz0OeHHFKa2^p_K{;Rx@>EN%g|eVapq%0LJ}@(^1Z%HVeKH@)raZ0fBq%fRM#0ES zK2bH{nIt}8obe|z7DHKscLLj86;tA#+^#awAe`#IYxGYvtW5pW?b`g z)%4}*yaJmUFN8nSePf48cOGnxrP%RPn2eDze5WdLRH$06$D(Zm(^BF`p#W_VurHKN z6|hS!_YI+J(vIM4ffl-;wV|wetv#wh6QP`z*LSN5lo!A~X_B>?zPQ&K0~xcY!oeDx zLqI05ZoCnIO*!&_N_Zh`mV7muhfO#+RZYwEgQ^L4Lh0{L9hDY8ag6chH-=S^frXw% zfx9vSwRdAEdvMq>Rj&l=WMvrtTlY@o_eA5|@qFB1j7b~C4)r|M*7@MfldA3$wT67B zM#BNEf1#?J_`g6|#i^$~OWHQgh&`>EYN~rwQtapnhW}aB*Y`j<%{HA;!K=b%mks`2 zHT5Pa7nDEY&%qmAqO>QJP0&yK4}Ms;s2jKCDTcA_2j#EVpANH|o>K``+%6*k`)+b< zQtYUSZsRv(z=D)OIR^He@r-ZRG-EYP_Q)bA^P2_b`qbqo)%5;QmbVbfo~{jLvO|7W z$^87IT2S{x*(`%}ItwqV^{?tL>PR#eoO5BouWC|-KrsY+&h}G|*=G@uXQIEMTmZun zfnz$nRArQwkTfw3kA3oCbLL}EN_Y*F zlYG-PwW_=gWri0{Df;1_B0CWR<~YxI;PDER@?N94{Dlm!LV5;&e2{rYn1KK zwy(F%T9e*_%_Hz-*i5Yi%F*~Ol)b#iZnK6}(zw9M(~{h&DOGG%wbCbBH#3GCHmflN z%6NUWB7A(pPmdRi1QKZT6n;WZ6ADbG8g)k*{(mkq*<5Lq8aDVL1g+I$U9Lh39Y7K2_ zjj4>;odH;xjXJ>>ptK)vrW&Ik78fo;TVZq1e*$GeHaFL!C=gp{$~2>+pUPNSm5;@0 zZE4a%#VbGXPD)Fi96NeYoI5eeoxmrmD|G>WxL4IYvZd<8SD-9F7i~Y{ulz%yY=Qfs z?7e%SEO=x~&+(2;GwQ))Ils}#?SL})$6DWlGTN(JpV6A4b(GfmZB(+8)w3$2;e9II z)8K566|gzk@mM%z47T8bh=*d_JA1Ki$Z;u&$$_b3Cj4-}3OH#@u+_sOAMh**Zu;QU z2*z1` z+rG(2op>CSrQO(9^=ADJD%uFm2SFLFpVso>#KT!&T9UD?qvx$o-CDfeNi|xcH7^Wf zT>Loe)PbjIR-ImVd2FsnZBMz@KJFUZsQC{gj-CCg-P-CeCAT;|(i~~OJAKaBNy{(q zA9|!=!NhIPty;SwYWpwFN9vkJ59Dq0WWM5Zz6>h_mbWK!rpvhtRv@e@p6~*f^Rf-4NdN@^Qc+6=o z$3$42U`h7FUhO@X5*+roKptW$wllucrH9$g*|NTAJZPnsc{IcuMprAlSK;bnW%s2H z;N{7m?Q-5^2xceYE6otc-Ktt%R$=GC>SV>h) z&E-1O>FbBlflzjSBg^?MtX{Beo^TB7=B<=f zF|3_qK-kaTR)4+@YX~e=vd3DRMt4}ICw!;NxfvGAQk6aA_z4yp%1TKt9W|Yf0F@F( zJL(t@i#0~ip({SseokxJUV?=ipV`$c>;5(s^~VGs1534L{y~>*O&i%X5UqIu=`~CAOu(r#HKMk;pZ{4Z?hWSU)gU+xn*^vGY4jv6%m8Pd8pje zN~X>O<$b|KAGyuSf~A%V`h5+H*<-wChM0lUWeCa?8>k@GDooq8upUE@ia~4lYHM}b z5R9%+I8`r%&kQl!%Jdk-Kc-<7Pd+-bX*<>YHjnwS%lW9bOv&`Og;a&b>83iPeQ(q0 z4bn-r4n*2l;M5~(HI}*dDkze<;&P6M#h$=uTOQ)jMs=&ar*%M;X3|`AaFB|s*51b| zEE5wpD@ZmCGrQThgAS0b!^}{7?FUVxzj6-0{kdF7U~X9rAN74~H~`zYtpTh8Vk zD`@;J=f}64f8KKT3a$wI(kG-7Ud~$yZmKKHB`~dSmCP6vq`2I)R^xlZWYZW7uVG;o>jdWA=djcS;>K!p zQ|r1)-qxv`Y;vP_CjnzmeK*wpAzTl6!p?Mcz!8L;t4V%;tcy*%V<3BGo(*-zz{RB- z%TTt`EX{uI9T+vmSX4Y+D|P(M_{SlnhTPnhiN>3kk;IEc7(;n%r`~8IizmRPJNaFg?StNOJpuJA0cHJ=%gZ6&k;p`ffAj9ZQ;%~7OnrR7 z8k7%<@sTd9zhN*&Rv%(to$P`XV<>v zX@9QG* zNS9PpwCy8m2%vE@LL4yk29<#l!czTVzUXo~`^d8Q_*{m znZ`Jk8%i=b$~5rKnR&z72n!GAwbkfx(|D@FnpSQ}{%jo7V2!Bo3m#}1!^$oDE3`aY z=Y-mSf-6S0PR5Gba}bsUA;Cxikqqm&?gRxF1ntaQ-^_J5) z1pRFVa6SnqI~&zXblI1|8Y44ZC`P9k(|A%jUxV{;%jx_9&L7oOI4y9tLg^2|QXw%7?X5;xi^uFx=M!*o7r+!ms12|f zr@D0>`;(!p)OqXEZnPTwYWYiqML!kqZCE_#sd9V`i)mJo@Xt}VF={zbrJ4(}v^wuyAgu_9FJ_w7wq878d<9IU{ z_I%f>aBxGc7$J_MAl3=1BB9oC>JC61G&5oGq>n==n$0(!&9-$~sIw1TtfDnE9J^p~ z99zYak80!GGhTHC3c$SyR)6@{wAT0=uy_w?l|tITh|m*Mr+P{Fb7(MFPr_dfhSn2R znUyuI!U~@k;()>74KM7*u1TsYxK~6XRbjD!6)oiWTKiWJ=e-H#{mbbw1Qy5Ot#kfT z5FQECmU;wMPgpqWA-9H!>dal;8fz)AR7p6BrY4z20z#@Hm%tiXZrLM}O(RZj1Ykjg zizQTJrh1BLyij4~z~a)ZwzMg!s&A1VX22m>dL&~JZ#B8xFF!ZL0b_i5R?eSc4X&_y zq*YpLoDWUWg_2C(Si`y%uiDf~1$K^ybt{$aw^{Ym)pWN>epNHXOqVXN;4X1U ze}yl{mKu`Xryr@`l97|xa-*3gZ4#K9aAoj;HO=GOw$ijgZt|vV+ zXNEf3J)_etU)Y@)AYAjTx$oQqi*p-O5>uSnmdbGlu4CY6tpFn2+? z$Xm|>>>FTpQHlNrr|N#rmmbe6OC2*`g~cNhwla+9KVYfVinFJ4mRc-u5NVEs_;WBh z?X3Q{pMuqc4`ZBu^U4=sc6=NxYfZ7r#k;wxAe=(Cru!w3_9~@hJcfBe?Y7n^vkiVh zHZ8-LT?A^Ka3nt)BM}xyff|YT%&)W{JLbR&xh?&@APjAY`92n)7ymu~$3U1rQp3cZ z4a?e|B-0z!{2nHcd&;xXOKO)eB|I9BRAAw$R(hyy@k_Gl4fOBtAe}s0r-wT4ds)Ym z@JwV1i@StgT3e!fKv+ywg(Et&)tSy33sl#sqjxu0){wWFFzH{23&O@g(_``f9G3MQ z%x1hIUEV@tb$>)rnvNEc&Z;atkbu0t`Zo zmz%IS=5fn{!)cdSRb%2@g`N0WSgaGumF%)_q2<|nB-HskTx>(6x6Nhm=wYkjIOt4* zi$m0^hx0914CRpU40JIp4ih{M#Yk-YT1ELVx1wOF(;Q2*K>J|?!z$1g&M2?A?0>?- zO&*^0J&|J?cp5#srsThf)M2p&t!Frn6R^13Tk~J=onxnCp(+_#P+ZP%ZJ|sbgwTrx zQ%8euU@;-wPff>@G{I!@gkc{}hl`J!s(ElH;8+i+HA zS~#5Vz+!WINoIc(8x}_@8XwJFXR*2~b$IgM4{^ZY%*E4>7hMhyEGxtE+beq^h$@=j z5;f!1%A5?#8VltQhaKxcm}SLb#96AtpiR;G!An(Ns8w$oEXIc)o)~UjDw{sURtb!$PH55g`|nf~@=hx0|?^HfcErVs1Sv|@- z0+T^aF<-~>u|iMbiql{w$SC-$WA0g49Br6L*x+36>eFO-mpWeqv8I-FDEbCgTc*vs z7rX5}ObiKYfd`PT_vHFItRG20obgtl+CPUCX+3A3xDw|lwz=~-Tu-Vba6$`N^>1f7 z`?DkrX}6pvu4M@IoeyV z$6&PP=`^^kqd>*5+zk?g_}J%Ob9pt}V0vb}5N3C8;LHwN6Y5wCmsQ$|{BMBhmYnUf z_1`Gh^D!E7HmcP@?Lr+lsmY?Or(rP-W$l8+@+qsv=E@-l!Q!k|ev4pnPe7lc-Os?% z&z93%wuiS!mz~J>=+=r+i0AF~K2z>s$(G}^(!&Vx-a z5f;14nlO&7u-NbwL&;gQpklMfAtVCU4B1+MrE@!+LzT1FE-duc5#SxTRKsJ*wEqYz z+6rRpx7%t0`=Z^}!*(Wc?;hNPSi9)caH*Fz`8a@XCbvqIKhiU*nR~jQacdp?EZy%)rRMk*poknHH0-PfAQw5xlg@uv*nmL+tIL! z=-dh?mj}GNK$|uEvb@c(dfJA4DVx4RAG{ChmM|3GS!ciM18mDU5)6XXSGInIdaa>{ za$bfLPqaXtkAJ25)G1~>c6eB-N5TtT_8(yB8225(JXJ9!!KuPvtGBH=Akz!+gtyN@ zxn777_Uu8`!&urjyBy!ZsyO_~r4Sr#3f1`*r(r>B#lvJbYjZtJ-ux=l?sW*J+&F@} zV!A3koJ%Xrd_iNWV22UxYc<_;ax-Bvduc(`(4 zdm2r-aR{lv#X+WX^Ez^y+2?k1!R_W@n7wYrZFsb@X&*sTwz8zRc{odMGi_~(*|6VO|csj^C1X$er)cWsLQod}4VXz+Uu*2LV4$xd|p)Nt?Dyx0!uma<|mJ%V1h(R+ZFs zn7!ePW7tc015>Itcsz~4tegjnw;MPi&vbbem71PEr-s?v{)YJ{VW)9YUjLhH`WfAH z6^J)v_$~xq{{&oC9fOr0O+E@16Tzd{g)ZC2mu1sMwBF3$t$pPp_Jcie;-xa3zGW43 zB%GRl2q#`ZGr(WB0`$IOy`%+oz6d9?z_`Fj*a?emWgXj{wf<0DfXDNAzSI|1qE&YL zMmTXJ_8n3+uA0`B%`ka*)Mo9!%6-k%@J)oN`rQh=5hjPP3T(Ufmu&hS zjXB^3I$XB?j*rvC{v9baaR zldwO4&N$^dkTbHH%}BZxc|$ds{ue^luWsXwJ0jbs!0Gnfc&n>79AjnXUtr-iEN?We zJy$sy(yYkO&iYZ_<}1Jb-U#6K#H)~kJzm6@T(QRB&#h#4IXSpz9SIEYLELiM&!`d=zOYvnqZ(BFJQ6JqdVcxb- zXAd}e?zXle$E&ccFN>9r0_RZ>uF+}%O2SuWxgO$f0VUc1i>-k#8l899G8@QsJ8HBS zU%zEJF)Q$PxNbw6b>>GG;GwP~943>i*qKv={IVm}7b*0c5JP-oRfYEa@}jDg$_ z7S}>lIm2a}(MYaWMRw~z*+_VW6oG>xtiD#w>__lz+$U8fJK~$XeD|i_;7o(XZbOyv zbY&wfHYA=3&2|MeZel~pQ9;A{cL4amx=Io1KJez(s=T~lwvV6ZbV39BQUrcXW%g5r zGvIN29eDcxG>I<}_+eGZy7)HmLx_jI8iUa{v6*zKj{5Bewr)`*{|tt76?|8iBg8J@ zxflhotZmRb+{|t+U1}i8E>Kpfn)Nj1CM>mwVmG%x4k(pgvnrN zYb^C=84MPlY2j9;RSRXQK5D<&>CgZVPqB);3kwfor-$0U!x#E$p#=5t&0W?7J3bP- z3abY!>v6MlFur`uq_Jz_`!h9Z!K!$JI*QqTFlwM z!s5VQ0*eK=9!lAM#Mk)l!X#~hZ$o$C*GO%{?~^XRC~6*vDgb9+DJ(V5xaPOUSE4x* zRN+Qz3uB3c=3g*5AaEEw;IjV)3tvaVL*xe^z)aJTLL4xxe(~hra5>+GWu-1=TZ}VU z+%*jA`wG?Z#c29f+}YZ~@h!dvDB+**;fjE%g+p;)eBYY4bXXDbt){nNvGf%WbG&|O zYxB%V4|BF{rzSS`#XywyNto);mH#rbhQ-D~ui-5Gb35tM2pP3(Z?m4BsAst`uvE|U zDExYRxy~@hK-uKj>hSR4?jTh?_1HT~Th`&sF&`G&=hhh90m7#<)^{kKf0SF2KMV!D z|3RB|7lp$DzTWB_4wDr_Sqc! zF_aq1AARri2IyivQ>foj6b3bjv$tDPsT08h~1 zVqaCXoc&Lb2R$?K^{haAnTUf1-H*YL1dC%$wbCkBYC2%mb)JL8f?xvRW~5n2bC85SZZ8j%-arj zmg_Asa2j<{;}J#0Yo@-i+RBYuW>>Sz`c3|}j~iuFF%09&s)j*51m#6Fp%XYV-}tW{D#N;nzyj5SvgE!x0V;WYC~MLX$^tgg{*{&f&2@M`s4whJ z+ID5Y!Gt?Qc~O~g7tOnB?WX;y^zW|qA+6z1*7RX*_lEMKl0Tw(e<<@Cp#3w-9aPp} zp!T4$ISr$0)CVDgXy_k@o-J zP`2oConE<)po1RYYLCjwv3LrcO?z7VQ#lq(pp178>V#g>{*~2~1P$v7Q=2T|KJcu078qm9-Ove|>q2Cb7a804Ss7$B1(~u4i+k7&Z_`TYb z%E&FXO=Thh+WseH_%_=Af1*q;5aAiGEef%ACJI60L7iDAo!Ea;7P_+zM`evdwT3~N zUN`Me#SMTl6IvhY(Y%u4&g7px{wtT4dh4EBh;*2bXkDb!rIIh!HkI}gtxL5o)4CkW zjqq)4zXN4`-qrSdP+pZZga0hg*r)?;hO$9E)An{KGt7tbqSAk-<^`Hlnejd-Gu*HJ zztZ+WZ5Kj0#EwCE9dlxFV1yGIP&vjCaB#Z!gfc)c zyAPBXmHvI9KG0!M*8C~WM{13QvS6dNx}kMor$Kp9S)p{OHxE!xTMh#^$tuBT zb%5ug%rFbei%NSQlwGm_%7k8pvgMXSnelQcFDeJy3Mgy58p`d(S!oh%_ zL0O~Ep&U$mwY?w81P(!YQJL{!&5vt61!erRP+nAq`vHo7dTv~XGTrM?R)7N|mL;}v zglMJh4Vw|GYOMxk$*b#dm6f~(I1{X^{VS`tY<Sp|sm;J4ow;T01~_{ZEwHyL5Oe z)9tKnDr?lGx*=ZA+8TDI5eCRQJfs8n(19x}6X~Tn)szwa(Hs-|+Z@(T@-P@P?h9pF z{dL&N%Apgj{bRHa)8Q&B%QRf`e^UC7(EiX2Yfz8XfXc$gY5Sj)3B>Dg6QHyw>hRPC zu%Ffb&uRbXp{(G1ZNCWRWXh?InZbkQJKAFfloyo=zpM2<&Hs}!{7Qsl1y)1ZVjpYW zq~ljshTD>%J+}M@Wy!b0pQC#>lwI_N4o{{3SK6ksAP2NfWk!Wi#xK&G%GsWAQU^Gt z!S_%mPy%Jk{I314X#NM3O@74DGP=87nKS0fwF{;LOJaRLGhn4QrlypY>9X%SKet*W-uMfi%NS2lr5A2W%y^bKh@5a z`FR~+zSad=Jy1qisO=?CUR1WsDkw8ptvQt~@PW1~D=U(x`E5G#zwuzkpCAGwZiL0N|B$Jg&r zHcFWePi0xI*1(%fCiE8oGcvJPFrbZlhF0>b+NLsLrw&(5Yjw?QLJbLg9BVO?^`)tm zc0(PkvF21}caOHI9CiMhx6+)-^0t9;)V0%`%J}V}yq$`GGF)U$3?X{-(E+KPAF4gNQkEjtJ4QlEt~gd<4qtpOpSZ+W$W*`kw(9;kb@KrTwk8sVw;!ZBto;v)ZQ8 z{$AVvq)hJz__IP6bU3Pm^m!P{i^`h!)i#y!`$HLT0F>beLz(dqXiMk}D8tRtnpq3=XTU5S za2}Ki%!e|A1)9I2?bo2Zs4VyrD8nyrFZGQ@7xQ$R2WV7bmH2+*{f#!RmEYMfl&Nu`IBYvax2$TsP(*aIuE!O^L zp-kXBlm)q<{Vzf}_{ucD0_9a%S&{2H{7vQG&7h+dJX+#dzyJX}9%!ZgfVTfprRGrV zVGJIqgH=}cY)8$hEN?KB$##aa-l5w6KPkh7>2Uv`>KOho0x*N#P&Qy+9f8UO9?>?H z;rc@v?lH}&EZ86@8+a%bXFOvJl;Pb_UX_&x9=&xNwUZ)?61 z%9i;+^M64ZKTm7MC)%+d$_zI^c~NO^)OKZM2Ajc|@pdTBGkc-r`=HF=D=4r3qzF>q zIH?0tnZPNk>>q|F`%M1_?Mu3$e&}XIy|^u`al_>uGV@w0+lV}tL=YM#&4|s zsZ8e{ZC6&N(^T`O>-|R9dfs^lh$g{x=N;gkcYx}=bLSo4e|gn+=N+J0B<{Qe{HJ$; z+#NgfjG&d9Oek#Xop*p}2=2yr-T`{y;LMh|^A7ONJHR{d0Pnm5r_FA4eF;m$k22lV@f zf7Cng0Pnm5H150u)Ki4-4)44JyzL#}^9^6Kt#A06*SF0x0v9jpG?Aan= zlb>VN@9W=rHK}7-mw3N}?{$5r`{M)8zu2Ux=@HL+qdUfIY0YbLj}LD@nAdP& z(4{tKn@>IbenTVDyK#d9HNrl7LF@zzUYJ4TdE{^HX*qq_O7TYI|Mly`U6#m^6X>eWEXKJn^j_LuN= zUPI*Sr(QK>#X5iiQcAFLJwWuQ08x_rDL~8y0Q-7?ffBVIz_Afv9YM618vr&CxHkX{ zkvxL9O#r?d0ftKKMt}yJ0SX9)iO(j0Jp^f+07ghYLHZT||IGkTN%CfZfUN+B31Y=> z3qTRU+${j3rH~-&Gl1Z&0B*_L3J|glpoCzY1bqf@o*?HlfOsh;ShO7=VjI8&$=(JK znGaA#FiFC<16(Cou^k{$N(oke4iKFWkSw|R05LlN?4JXq@}CZ)J$3@DBS;f-2fzjb z_YQ!ml1C6%0N}e5V4B441Zc1epnzb8_!I!_AxJ9#m?`-L>AL~^cL8Kb@-BdYJphLZ zGR1EkRWR>K=2-bxstgDAmj^x5`yO?XfMEdf}Fho^Q4$y(LR8PF97CC z_7?z=Ujmd7yd>fK0Im|O*axscN(ol(2Z;U>AX{?31c>0elYtESA^<01XNO3J8{p&q07a1Zf8WmPm5xuonTWm8c>B$8msl z1na~+<~79pREAR4OCDu|IFCa%N-SlQY@}=!pA(QR5>MGG`IOJ3;kS@&l1$kydnx(i zcM|fsOrz|OLds5Q^Btr>GAX;{7-hEvor3Idh zV#)z2r5uz#XCQ@=OF1OhC|^s|S;#lCigH-Y?;%HIDCMZ+QHsP_0y!qJl;g6IazcE5 zK-26who(vU0ZnsK@(I$<1NffyF00J%m9406hzw-b^1ar>=oRva?tRDe_F94KC z<^_O|p8!e-&PmXZ0OtvEegwE6#RQ9f28j3x;3vuc2_W(!KpDYB3I7@3D#41M0e+QI zf|b8`)sR6Ky&f@3CHEpg%&!Px{{=T426Z2Pq4VMtY{VPKJA$bIG zr2xK{0Io^wC4dIM0TdAYDL$nDdkE4>0d7b>LHcC?|K9-qmgL_60)7WL3}7?8rNw2h z!BRvr_cDl=w;UqLDgz1r9g*yk`8y(oTmdK{s478a0OtvE$^e{FOt9z=fQTyq)g}81 zK;%_`GJ={C{s+KSf)#%N)Rt0$mDd2GuLAf;?p1)8>j3s^0QDs58i3 zK;XU(aJS?U#Qg=}`zJs{iTxAfqLFN*G!~z~AooZ-rHSN2B>g4=``>1hajygKp1|H6(HRS;O_wFF3ApnfNB7T z3BtwC2~b2Z*9p*53JJ2R0|ZwCh>*-`03kI1N(g#OP<4Rw1Uc0K`baUsqM85^H30fa zb`5~YS^#AP{Uy96z*T}3H30@lDZ$Fx0MWGoq9nH#KujF~du@P$5>*?(;RCRaAX>~i z02>J0bpVD)9zk4P0AC+~p%Uu@(4Zbb0l_fwsSB`&AgwOI2+1c%zYD;>9>7zQTn`|? z7vL~KtoYppP((2IE`ZTeNRU+@AlMhcEt$RmA$J3m5R8+c`T*w%a_R%bOEJNs1^^Lv z15A+Yy8$8_0+bO&1aVCOeD496Cb9PbG-wJ?KrlmmngHw}NNWNxQ}PMYn*sPY1;~)( zrT_uW0S*&nieEE;B7(Wi0Om*`L6#ptaC3mUlGz*}qy<0;!SfR22XLMs#}8nh6ca4E z7a*bqzrWJtQAHX9~{s4~F z0P6^H#B2qyfxz7gKqQYKE&#x{HNawtZ4J<%4L||GQt=4@*h7#O0I*#03DWNa@NWa~ zh9tKE2)G~MFhQ>P-3L%aF!w%yx22FE>j8k^`vF!+=KTO6fdC~0?@7=D0OtvE9spP+ z#RQAm0z?D?yf4{-0Fmtg$_PG`@U{R~30AZPSR+sl0Ba?x zJ%HmufOQ1x#KbBk8wlJ%0P7`>Ag%*|?}GpvCH6sp1|0zk2sVpP2Y@{UX&nHzNJHB0>Q6Np=W8WEX%kg8dTS8Q?0xip~HBq?BM~C_r=jvN(25?Mb!vGp|2PhyoAwJyz_7J3X12`%9 z1nCa}_;&|5CCS|Z0>S|f6BLWzLjXkta~}dYD}@AEJph8k0ZJq@93Z48KncM)3F-lG zo*<_Ozy&ELSkwz3q9?#llHC&^G6JBC;G%^00=P=Bq8GrgQcAG$VSwlefKthg0Ep=g zV1F3kvP3-$;D`iRM^GkaZ-5O1?%n`@NFG649{}G-fNK&P3DBS~Kmoy@;?oCU4?$WV zfE$ueklqi#zc0YwlH3;{;1Phs05%*TTJ-bA8G~eQKM*gw93si;4-)(cBH1PL5kv}k z6rhBlss!~1I8Tt%AHXTa1d9d$L_7*mU9ukqh_C79(EtSm zjm2jWz#f9MK>$r8pCElOfPXYVGf9pH2p9rzn7~i`1_Klk%pDAHuM`qw#Q+2k0q~d1 zApjvm0ZIs3OHd5Jd4il6fHqQ0u;>YZh@k-YOZHHJ$YB6w1c4I%1i)2-6;A-PlTw0} z!vUg)0R&0zFo2j50QTVk9VBWvfa6Jkbp*j;jsVy|;2r_sl01UArvQAP1n4ZWPXaU; z2~a=~Dn3sE>>)^d3Ls4K3DRQ${6_+Gm*kNE0iys86NHOjEI<*#+*p8~Qb>?B8X$NS zK!jwD0tgucP(sjKf<^@fh5V*$zt`b)SQ;3~liH^2ZX z1u!3zK4T$Kl1q79u2BX`)Hui>Sw)E!GY&FXhEj$|9wkPc@sObs3y}sBkZeIbk{u>K z;{o;%q>Tp{A^8O969N1u06Znh6957x0URcX6~Bo9MFevv0*sbIf~*99;7I^($(#fb zk_b>jFiwIJ0L~NSBml%qF~OoFfQUqZ36h-%5Sa{6MleajlK`#~tVjY#lv09~DFD&Q z0LhY@3=oqFU{3)^m8cW|$7FzY1ZiTX0&E~~rvglsJc7710N=>~(K!Yg&1q3t1 zCkln>+r)@>=OKBLOZix?QPxUS7UUCIMOi22 zJO~~&P}WNx1&|ehuKLi~{7 z0oDVQmV)?N>vG30ddGYic^Xq zvgkcz6!9)HsxH~@0z|F^C?lvT;qL)lC0OwuKy4`{Sh)%ydL@95i{+oxYq%=B#$6&J%I100G%cFQ-B5=015~~#b-Uh9)h&> z0AZ3(kiHSXe*-{wN!|buunFKWLAdyB1SleyyAhzL6cS`@1_<5+5FweH07A9^lo0fm zpv?g1334_A^pRqMMO(29_1c1EsGnqS0f_tzA0P#{xuxJlJ#BP8IlD!)saxXv`!6XUa18|jK#U6k} zDJ59>1wiy(fMm(t3lOsp!2Sh5sziMO;P?_?9YLCy`v5i&xc31}l{|vD{Q$mS0!)+G zF98~S1yDdRLwxoF>>)_o4=_{m3DOS$_1RMl7Opqyl2LOr)<{ki;BZUN6 zg#f_^0p?2PL4c4$03`&^OHd)ed4ilmfO%3(u;^=mh(iGLCHoLSj-khJOZ$RzLy%Skuw3#9(vJi99|L$pl8*rdoB%jXkSl)20g4Fb9tU_^ z3JJ2l1qeO?utG9V0ECF zvjDzl05(eO8Gr`g0~8Q!7N4^KdkE6b0&JChg7gvq|L*~|N%Hpq0Y3m7Cde1R5`ZFt zxg`KQq>v!%96<0700oly13<`mfD(e;5_AsWJVDMmfW1;ou;>Cn#Cd>yl6@W^@<)I& zg8dSH0pKdZiVFY-q?BOgPXN(B0u)N_j{q@01K588_*$ZV0&rXeSVwSJ%%1@^5V(H^ zI4XGralZigUIaKMu@?av{0dM&a6){30oX&3_6xvC$tOs^gmtXtuUN-UN%F7QAy3O* zO0oD|f}G*vHb82BZ|{n~ZRWk+QR+Q1Tt?QlKSom~K4^E?gX@__H7R|{-p+mv&U)*^ z>)M|;>0k3{*!b5zv)^xM8a4UH)E8LMK8QlJRgF!f_ImS*cTX#p0& z{%)#_tZDa_EiU^#_K#Yd_-!4gUvnv(_{Ze4hX$BN9md6x8~IdoPl}D7h_~yXKd^pQ zXZsUgUj0I>bS9~!o7b^B)(`4tA8A$(?1`92VN`sASCdC_KDI zPmFInK2`l=t-W|j?i5t$Yb)UC0rqc9UGmJ~_OEShif`#<8}P>ZglPLSrhVSKDDHa8 z*E2ieE_%x8p{Vdf>*#?(SG7u2GbwhQdu)8dm^m9wqpl?1^v>}6ax;RU2uHQ@iwUu* zlT#7{C#D8(F%2ITtj2bezfv^2ITSyDGn>JZQrwd#$GcMwg4MQKtJ)m=#V`C!@$BXK z>t_wOAGg)K1x%xM`(OBjIkf}@KV?;B zxEqxjkH1h;P1UkuIMiH%zgmKS(lq-WXf5Tr9u4$}muyL}2iWX&*Z+`UKV;gi>aiuN z+pHA7OtCv`OIoZyl47r4#TIr@eo42#U@yS0KUd?RxtoD%CQM73#G-~B#28>{wn?4s z_Mr9n-!oPC{|~G4|5K~-{~hZy<4CXn|Nobo4DT7mTI{?Pb*Al>`+t(m|9u!As~~?n z{ypE1O+f*zdRDI0d$#Ik^8@@>?)b&361n(AmG0KR-oN>`y2u^y=D#n~-<7VgIbPHc zYT%a)bQpdCgumR$s~Q-?@Mjdd>c};8IQ~Mx4gTvCTs3t#2!H9BKZ%Xs>w?8V{I!W) z{2#8nB!6F(dKtbt4gSl>ecG$OPJ_Q8@rV8vN(0UK?_sWL)(DJF{+7gFnl;hsv4}S` zYpUb2ezjNuT+LOu4C}YV8fxgLBlF+>-=kRz&G=JB4{CO=PLPdrMrY!$8I}uUD;OKO zwT{c55Bp4q3jm{&zuC0IOaJo5{TlKIU-5SVtpIIxWd0^ncYJuY17k1nC$4(o!-Rss z*l-Q;(Z7jegy^_z{=ap%b_Qek#`xIvcfGpux6N77d+@2LVVGu3!0Kq$O|zz8bv5e_ z#=X4|Cw) z>J7%)wZdm07>8S5Ft%Q6e1_<7u{v%5Sd3Huy2U6B+W-#L0Y__gAJ_=Z#%Ojw*htOX znmqs(tJzpEwoD*Cqcn@t;o5?wY8KC*24(ZN!zWF{@j768u&SC(&@2dyzt#=Yn27(e z#`r;dBTdh~L>;aJ*c2Tu35>G_znX7M)og|i7tHaOuHn;wY%cs>zA;NP4;X9W!l(S- zx94aU0#^Qm5CX=@fxlK@9KeTDcPSVv*9D)$nl00DImnOT!~BicbwK=01cL*E*BhFJ z!99s3!1bn%jK3scB!F?=zol7sxM%2a@94M>f!)MF;ILT%#>$1`Q_8`CYfBa6&l2{) z=PeDl>c~C8W@`4CW}I@D*ygylX%+$ZAY{yWzg@G3;XaITocH;f^@h6ujPw3;%_3QU zM&`WVpUh>?ah4>&;Ox>jzMyVPqpgE;vOx;9$6q=$ahUYzWveFfKyJHH(4!DIM+v z7<*?ZK3l+=LB9jzzRvypIrp_kwvHH!t?XEC(@Pa2K_d;+iq^k>aR!#zw#zNpz4u;H5hqL~}) zNzH!MY%JJQnqAUt9N0+BO3B#&arlhV@HZWhgKey4mo*y?HcqqOHJbo7Ub8aICW0kv zc11G|)Kty>0OR;bz$as}hSxMq1e~JTpE`09SWmEfp?_(X4EF;%+zriA!1!x|yl#TA zCsOfwM>7*sgyVKHKJO}qzgwVT8sHv1`n@!p0=7moZ_TEHmH#rYT{E7S2kCHCz*xI! z`0$sFc{y~r>2PPm&C6*qwEqkkuWDEg%E(XSTt8b_`Ak|(Aqj2 zyL6TgS4XqiV0H9f;-gt6SY7@!IX9ZRfXw(AeE6pTc-^Jh9Jqf34+;SA$tz;`tC2jr5!0G|~)@_jn;D`2>d zv#$G9WFs3^C>VFRKppN?xc{_5;b#gp^T1w?+_;&w({W#eJKM(2!WElTeElJ{#?h6)a*U5 z9sG4O2JEBZO1R5^;;kF)&tKsHv&S3*F9@Xr9xcP&3+&>0r_5s}d zkv#4nk7@QH+#SJqMO8)rb4>jUpM}f~*W)_i8n}PN3O5=$P_vKVE&<~nFi5jJxW5B) zL!&kO81Ca>W1)jJTMPG5uyN2Kntj6Y#~*NvgT`pM4sQPHV?1=IW}m{%Uxefy@PuaT z;r=4{e&7Rfl zYcTvtVe6U;X8n@?H!$&+f353zK=#vNeDJq=t!q9QTj2;k_$#~C^|EG1Vd3eI!R47- zE4_>G!QUOXu2(fX28;jrhT*ugqFlyt*8giAFh>VG0gnxuE!6B=urM&L_oCTJxNqR| zDn8tn8Ra{CRv~4s_lq?<1$SSa9>)%yr}1GttNoX1SPZjv9sCk3beU#n;I0G4Yq@4; z;m*PWvJ(3G|JU1Dz*UvCj~b2$$3hfQl%pbcfQX3L-HJIg4i*+RVz*$oqK+&BySv5i zPV831Zj2dqbnrg!*?SAaH_ZRL_jm8*=i^%IUGM7HYpuQa*~p|Mk6?-}TcqnfMy9B? z4Va5{y(ie^7iVRdE)_j#|EG`@p^V98df;c+<;o^Qa=9*hjy*GaG9*{%vKQFpdLTn` zr7nAkU4E=t{#NO-SJ)5fvemlmHL@`ImGlEx*66}F*stqjcC9XZi|mH3w@#PEBkQc| zt=DDmka;o`Wk_y7CN=XO-Z4r}U~ba&K49O9=Sivm&ARX-!fg_Qzb(4#ckJ6WMVqa< z>9SPFvLTbd!@A548Q-tA4$~vLEH!RUeyy^jy38IopQ_gO|HpKp z1A;h^v3p#XnQ>nS#;MH-U6uxSMUWwRQkSL0-2mk8lrBq$yAj++c3PLE$1T4f8i(wR zCJVF4fKV}XWLU{Z{&3nj@}IPm454$nEF&_R17u{J*JVz)FYCSff-cL1>^>b+I&BOx zsie%v?xw}U-z8no8Cn1Iw7>M^%epWNLiwJF^yDkL%mo>rm9&vgc2$>UMaHLZZKN5l z>9TCtrDi0f*L9gIcKI%f#Ct=RRhAtrf6MF;a2L&fRM;5GFc##emOsH~_yRdla|8L= z{Sx2ueu(?S02l~^U@#1Up&&oR-woQrahl>L z$Y%*^LM@QbM{EcAC`u0qQ8^#m7yoGp)}b&AhJ$>8BpgP-NRZEeRfTF$1LSTK@JBqf)hv&m!5iE zU3_BiRyY>x1Bio%@CY8m6Od6TqfSOtO!V_7_MJ?s*>ii}8uF!qLbPyUC;~-6emM9O zd;vN6`U(kP%Q$aMeYSxhXbbJ3gF5%zzFe66UU59U1Nm*^Hy|%QzknGG7Wu*E7`OdC%899!TF7AEKo2T z0n=dy$S<6h1^KjM z5hx19z#Z~{2go-c(?D8C2k9XLID#Bf$@$ZBcmNOK5y+=UXPLRX6NyC*nP!6=ENy1y zmUE-3AZJBop|pDZ#=cq!JFKa}9vr|7Y2Z6We*XOwHa{YM^R zad~IIG%P2xh=m~Xv*dPQ4-U|onv~CQ{R-#lX?zFUdSWpXdmN+kAv}V|AV29YCl}qJ z2XuwQunTs>R@ek$oq^TK9xQvI2o!~4;0C$D9rAz& zIKp3yr3Cm6Z{Qus{`)*!fU`92Bf9WJm;_VcGd1`pe1*Rt0p!MxEtsqyjlyCFslgu7 zL1qHFKvsAtk-{YcUWRLM9nQcxSO$mSFdTuSa15>!<_5$-8R97q6`&Gq#eW-Q-_C!# zK)$FbUo%S$1)!ih_rX3}SYfQCLH7T-z>HcNm`9v7=nCaAbA$XIaB1*|;@}OQkQZ7~ z-h7z(;S*g=&In}t{{&XRYM2kpfX6+o9}#Yay^Wc>MnPC=K^>?I^`N{O^wGYk<0j%= zMz3BD864D-kM<)~q2KL?s%^j9R}Pa+Wh|X66i$PDl3@}=zzo<1+hGUzL22*@`8>%& za0NH8Ku*X7xxpRsfHTPR1oA1H$M6DP!E1OUA8m@qau@DHEIa`Dkk4s22#4SZWQTN+ z9!!u5Y(e&j_uvK9{RWP}Q8)(2;RMLX85h7pSOkk<2`o)Z$6id}C9o7$z$_RIV?j2S z@@2J5kcR*sARq7i2*1Nyh@-vbCu#$rF*Jjpp*f7hZ#+zZiQoWcNCTZ{!y|I(EP_M6 zDJVbwToQc24P+C#17t(liS}#<^2O*t_yrn6eYN?MeXTIr^cA7th zf+Y*MKvu{GUnzrpRPrjfaH(3wmhpltG)?Etm!vwi6b%gKj)m1zpWt3gfh z1i6iD3+||&VApaIPQhvDN;IkQ^HC+f*gF>pA$U_#9tv`YT<(4k0J*s>H?uRrc?T8z z#Xca@Er!HxI0|x0_PE;j#lC2b@6=E$GAK7j<({Y9yL^dEZcPSawt&5GmfXIDK#N#>Zq$ZZyFbpID`Tmof z6wAr5Bcuj7{gv-A%10mNgjaqlR}OXat2}?%yOk+~bu86hkSccpIiHnZs{ah}@C#KL zg}EH&fmCg4%$lm-U-o^%!m8O?W->X5^=YaoYbcR?~J znU%U&KwWrZ=A<6vE8cQQ_8#P5OwPRIjH^BF4zS9k;u7p9HkOa9$N@^EWLPi6D)i*6 zqycH?1vw*;?}bU_Tp|zW;W=qP0y!7y4|2p&96qRh-|gLOgVoLN_6@7bCtW0m(h_oz zag~s7h{VwfcQfo=pfhxW zU}z74&<+BiDf|pAp*j2nO`u^ao76T9u&4*+9bCgiwJ|gTiB^J%uiU&8w?y#^h@QwL z3DNl#^ym7%A0k^Va7z}&Uvwp0TL^+SAaS&Yj*z4nVKyBQ$YE4Z%Fi1p$ zU;y+7iBuSv#6Co~4^?gRI5>wz;FJe1M(EF48FLe?2ifSZ)8)CbZ^OP7Y9!IK zr+*k>bD<-88_}7DDM@b8^><>%zy&xD=in@yfzzqDtmJj5Qb8Vgd;qa<5AK3oO>Yy~ zEzFy61Fpk0xC&R`GF(!-@;bPM-NAYvq*dKv0Q854$Yx0)+%OZaDq(W0$IQrM4k;qmPtO@ z`j|-MvDYVhLRUSNL)lDVgt zd*KNv{2ya(K>iT(0fgP-zq`lCe zqH;^)SKJ41_k|AF_hXiYeXs-aAlr_)6}G@;J1W^9OKB(tK9CcPc9-_{hT`A_o~lOy zhf2I@V(%VS0=Mk@vEkrVi%cAZlW)42~!v9 zfP|?HwLtu;+tG1FF#zg=DAfZaaB*tpSKL2?l-S73PuL~mX3!LxfJELLegV-FyCfoJ z3kU>>!_cwXrQ^0t;voSfg4WOqBvO$HZNSJ_JM3+Bxzw1*g$^JQ8+v`POW}G$FX#eB zA%n4pb>zR!&wk$|FXBx;x<6MuFVDW57sEODHiOBW`OZ zlX@DtHWVaN5~<-%YM+c;s!^B=GeL&oG%%_-B8gq9dj?F0DIl4Ynhuk8l|CR5Nk^Ik zvq3>5%!1!Qs?n&a|=!r%`3(N@xK(k@bMQc|%S0i^RRf`zaE zqF^3K2_@qaq39%a&rc>x?ytFNe@Rdxk)a|HEe6Se$d-WAN;zl%%RqXx*riHkq}0S* z4!uBneGN!DVq~(B8kR6J6eSH~2-XV2A)c#16gI&cSPd(|kc(-!*Xs6-m>Xantk*I0 zJK#jf=LhK^h%_u#o3c0x4l(%mtbr{EAA23Z>&!#oN{-~=3p zlOPwl)0k)AEZoL@4)Z)*010;;uEABfA?+`Mo4`NoWmd=JpXp02=3z;m# zzF~faFYp<(fSS&qxD((z6ebM&AZ!2l74sRg4%D-(6C{I8k=4bNj26Kz{VT703pFP= zfE&nglv1R{o&z$&6x=3E>D?AcgW0a!9%O3zG>nGPfaa?vnj8ogB_k5PM(&;vq2BA1?=8#5PpKpt2{$=xw) z;3t{Phh5Bqn1w(JCK)P_n)c71#Iqej@hlG#K-@+^sg=TD$YlNa694M36sAIP{5|0c zc3HYfjTOZ$Ro@8wL7J`@GO4LU^0bPKO%X^!O)wjSl-M7$5y*P50cL%u3pJq{R0MAr zfUYdRByp)NFU)F~lDK3}5-N+V1W2a0K^g3R5LSx+d|)I>Ql*j^NqjU&RhPuC2d4N* z#8okUF-6a4Ybl|jBmUwi`i7oSn9}G;GbV{Q%&KSv62)W+e@ocN+&@HQ)ss$Ffketf zIf%e*)U5O=iQnj$nw<7GdaW`48wy6xm8z5srWBDhn`EpK35sdx$*7V9t0I@Clzx>o zbJkH6hOjc8T`^^7Rna4nc9XW02&*HLNNa%6#?pqha7#ZB|D^i0af`mRbsbD2^O6~9 zDrrX1m$<{~;SgomlR64Uu+7&=rC~ltj5Bh;j#L51pV3bOyt(8+IdNc_=s(cTWfbBNMWJ zH1zu-?*o#CWZ(_y|4<$NT?}x3h88$&zI@JbDIc1Rpu^HGA+04S638RqxhPfWr z!CF|Ork8SP8n_YbMWUAqO!_A>BRV<2*@B)te`uTu?Zch?OlU8%J+K>grB)UF9I~no zehyi}Br?g5JU4t8&qE-I9l=bR&{5ofl86sf<}6oU1+EA3>TpKL4cWj696??|K0}z( z(2OvrFi(KQc@k5~{fLfYeGc#&fL8gW~_&>q^801>@5Hk)g zfUNQ!V8((Rr`^ZA4EG=g&cit{bZ=n4CWA?oF2Yhh@k`j{xacaTJaKkvARa7)G{6H=Hr@ETsib9g2LwfcT5Pp(p<0C+@!`ltf45qW1;Dq?!dOt(0aYIX{Q}E64$g_4RVvvLb%+ z2xtQGzd)Kx<~tkwzGI({RZa@!yfrN{IlPxw*h6ri1~YaCu!q!O2dU)MPZJhd(Wlc* z>0r{Kq!S65!Ek57E5>N6%9Wu3 zIQVF1;~F2|+DHdY2(Q)Eq`F*JhrWbPF_fk$uw*(buIYMm2heB?zgi;kAE>NtCVhUoHKtgZmtJ}2*G`CdV7~}&%%D-K&(5lG8aSwyhFbami zU>F1g{rOLn<^F^yOBXW&$gQQJ_{$|ydctsMK}sVqN2(GP9rA`P!8#A-!W@_l3L;?^ z{08G70w%(Em;f_j2FU#s=~yC@4Z7$}gQ+kDCc`8!bf#l}guWaC5H{>@5lbKmU?e1N z!=5w)|D=~RoDn8z_$cC93=3gCEPzG!1rrmH7FdQ*YF~PtRH|GT3FI*>NofVl}ag1knt4Kt3OxD~S~ zafy!HcOolc)?WcQgooVe3dJ;f*=g*j;3S-Zv+#)gOT))vzYi;@@lYbWg#98k!!FlJ zxlt?E(Vmzw$R*5O%sU|A?!j%iXdk2nx{Tu%+=Odz1FpjrxC$aOOd}$h4G zKR|b24ee(9=x5RID?6+?ougc4exrz9{G`>*hv$oLP3K|T+Q+MeR|$G%R628xY_;=o zwZ?zioLZgVyR9j1vi0@y_VQL$E}647^TWz@a_$_i?C3=Nv3$|)oT)~dY<;{+dwEmd zUVZqFzib0X1^1Z$;hf3V&#OcUFCPYV@9v&mdv*z#(#s<@$Tldo7Oy1R_<}iGs*+y* z%J#E^o7$1iyu;MTS%suGyZRYFPIY)-?4C3A|LP|RmGCM>0%N#E&a(#Lx&224Tn!oR zp6Cl!=O z-S8hh)ct6p#&djlDk8kelGD*MyVdEL=<~%{O(v`uAnlD(s>n9L+vI3P{rT9L@Fhg8KZZ08Q>{Nx905Dk_^zc@H$ZsJj`>WBx_ms9B>% z+wxkAlq6M~>>sXVTvx#)UCLX!RH&;u>tsG}dY@fIWg@GQZYm}dwY1PJ`b8$Q#a0An zXFNLQROOv%oY0)M@&g*-ffbJj`PU8UO7EoedHML-jLWG)oy|q^JwU^PMlEifOQO9B z+OORbx4_3_D~cuPgg@s}`-#gh7!9dL&(Cg!X{Zsp`c?2XZIL8I=C0#j>!P}>t@A4lDZPt%UKR~=h=cDeWv zKOHkTw`xsT%TU5f{g3>;<)RyX)-~3``lB&3x0>N%E@FC+Q@Oj*gqSy6XgS@T)@T>I zJjyGprWcr%-k3yR!u@E!^&Z*)UYcv{QTIMqJm@scLcYuvJ3UlFR#G_Xpi=?*3 ztPY)UaFNJbXEVE-w&hjf*-(ketL~g8+c%j4WlWDd)%;uDyyZXWJ&=+;&8yC5GY4Bb z??y9=YFD+*xriXkPZ3=}7xrcDA%Vle@RgOMIjYULTotbDGP= z>=J!UWmM1tE#Vwxw4c?x`SsKBw67nINmTMFqZ;KWfqH0UCz?)E`dywpy>ob?MhAR& zlPGT9BWz&n}~~E;I)#{{rS5ma9C?mmc++Yre0xY_6Ko+H*?C zWl&X6AqD8_ODd?j1JiDPpyxe24~nY(Zh+$S8F=Bbh?O2 zswH)9iK?V^+_jSyj~$ueQwMABU?O8FrL~HwYh|^jkolRZb5*tCuGupWgQTR*+NxTx z?Q{MtVw149poi9*eOVb*RdI#Q+bjua$R#@cwaC7gz3<+)CT*RQ&DE55X|qdeW*g^f z>TD5mk-M4}o$Ip%uX@w^9Y>kVw&cRIT4YgLQ4f|46Clpo{WZ#HPl2;U+Me%~Z*EStnvIspa) zJXd0v>P3hFb<|-@%K#otkxTK6sX3}t-@IU>o@2^VpssQ%PVq|ARYi&uVsl;98n@qW z7SK|8gU+3Jm32tta6;(Q@KJoEGi3VdmlZ4WWRq&8L8PXLYn>iPY;p1*R9|^{n|<8Y z)z|XtkflwQARiucMU)Ud#M}I9D$5Wa8<+gmn^mz+rh{E4TjQio*^@L-@g<3C zY6Df#m$J`6Qx-ju)pI1&DYNagH7;7p-)30@)!Ub{ZbU=!H*iwlea^4`oPtJ4t#yoR z{GJBtmM?MY*ZSNv%LxKWm0CWBr`vX5-3S8d-Slb$Vb z)YUIjLoG+Khg=dy-{}0vju0g!aT^bOB-JNPUa$FHFpUYcgFt83I7U|YBG zhdWM*8m;k>3+Lg_#XfI5`FcsBPoIYBEMYC<(UAFU$g6ho=|`j~*j$UJr#7u|2RIW95exkf6N zgwV}@(bVJm5x0?+p0-=cxzn<5YRK>7mZj&}G*>^1>DpW^E6dE5HBe10M<(3^wNdAJ zaMkVQ*-Ef5ryE-5!Xkl@gUF*_pt>W9)liiF`el9jiV2UqWH8z4Gk}fWUoDN%a6)70 zt9IM#{k$cW^f2r4&_?gMrnZ5qH9gy}H=3EzoV((~#A8mML(r4~!|)u9k25~g&i^^B zV7wO#JY%9$fodILEo;z_Ht)M9(}ADP+)Y43Hb@NIgMlhmG;W|FmzoJ9q{TsYZQR(CnyW zc5$%IQHNWp@QT#@b$lu%30a`EN~oK}ZN#YQ`4}-7DXD`!*?zQYqoTSKs8buYs3yrA zAB}KG8|7Y^f=_6pHIYlztS7vhj`PwhFR?VLdu7!1ESlB^sq*8A>rjx|OK>Aj)6F0i zFK$DRj7iZ{`6^s7R(sH#_^42E!eM|Co(l2M7evgH;pok(A> zdpP@WzAB?HCr1CVqv}wd=#3DDc_dh!t^W7YTl-AQPAa4?l^ohho0T5__4Jny`3^h% zXYVq4lTm+$8QNLxs6mQ`n-twg;MUv0a;CF3S1ulY$D`J8JBe9F1zb4&H-C2~9=s4?o&2RTs5)1a0=Jra6b-tS;JI7ud}TGZ(d$JF-aqndVg0TwP^ffU6p$S>L#|U+FuLvMOPIx`bQ%t*5%`Fs&Z}X z6ne3~IlC!ycNH(e3^Q*J71Ri~o`$+rJ4vrJ2H3mKsz@C=l;IBSsoVnzqzBfzf#GR1 z-szqyydK3aR@a=v?L|+mKF{XKUODIS9yF?J2PUSK1}ANqQJEbDt|4d-bY(|Y!_gS8S7y2iTrV0IF>4Fx<*-!k zqYa0n{&fO!**?ja7@^U$E&Hf@5|NQFs@uCU`7#nnBDc0-a<`#pw6T#v!=1Ezrl@}E zU;quUi|3EzGTpZ9_gWRM-{Szw=%7Y9WBMsa$=1_;Dxnvq5v`G2s{U$GQ{?&js|eBi zp9}mC1+^B)h&g$|jGX)*_**M5rEn?9w+xJILP3lkZkR&`sQ~1b34^pM+t$9*UHe>p zWtXE*7W#F2(I6GogoZN$ei@`J;S3Gy^l1pSDJZ+!VC}-OvanmL%~yZTlo(mB!Kyp; zYDsx4={5f^gGj#ws8>xH=0?iKB-wq4D&LF*ejB1KmabN9A3u1=JXsRxbJVsWDijUB zn`q>ulWZ!R??8^28^MVhf8dh`pQhmjgDbs$ENgi^Y__3lKVg3i3!^vxPmTFs=>2U} z7?qIn_)5~9tRao@V91l@=wF7uF;b1W$>>0a+=$UgL7zDNoWpG8$6&(~#(zIHbx+?v z5^Ko^9~pk-KU}K*Vv!H0#76zq2~+i((>fi|kY#-F@v9C+EvujsH3o&La5PMM_cGd7 zoo!BI8*}zQde#nCjeaF$3Q8%-C1!nPl{}5x2IIb2X(Mr5xFane@xDiAz=0miQ;X98PGX2DeHQk9MK= z`HzK`p|@a^+R%!4wRBx_v&q;oN;|=Mv3!x=y4w}A$|kEJ)#tKplR-4T zY=Kxl%*IEKC8oabRN_$K(Qm||EEiET$Wj(Un>28siFQ{)rqw6HKtSy`UJ-Wucx(@$OiZER zbQRm4?y_;Z%F=-ouD&x=wt_Yz z_pdrxv$q^b>H8)Q&f+^U1^>grnLg1j^wcJZUMud}^e>pbGhuZf4m>M%CiRMQRP)Z- z!RJA7bHsM5Gr8zCPZjFI-3nu8qxTT$rF{3VSXWfF@$4d^3{ffwkzbA|tri{qa|Y~P zxSC$i<*$U7A9s3mpN~zl7s@!{++loVf5_glY?Rs{(dpMCOA32U##Y@Z^$kr+E8@yR zO*`+i@fs6+NG`I{U+AK}qLgi3+Hhq3KP>?I&wi|veSDKfXOjP(CpIrE4 zsMTd@s=W1PB>FfnPyw=G)S^(~-5CyARr{E3E>JP}SyHYtWB10^C07qL+p@KmFSKQ~8DJN)K+tHC(8jzkU1jg;z*I&ho^h zcbMk*$Ob=Zex@t)-o$*tM_<_(&64s)DP*Av4I#DU+o7aO5EUOn9T)rr&p+Fy&3F!?OL|*zK9bUY&9`k+6}iCJRpPm z#B0=l+Er=+=O%s@o=K4{ZoSg$7j5pc#W)&bdQG|2^-H)MlzS1JAL|QGKYMix%dz|1 zhBFeXvr4(Me>bvZ(i1cp`}&llH&zNpR`n3cW3r^&abs4iiv37CSx7appE=;aIEfp) zMmfsC?C3SxNwjmwnV4rY^RKjy4C;|*Q?#?{A4kT9EB`(Xae6s6shn!wP`p}8V3McMgKVYuoPIYjl?=R;yv&5&nhQ0dJ?|t zmEB--ealKTJV+yTUX{M?%Uu468mHE)?t{&)ZjaEAGpmJruDHd9*4vDRUKt5$;b6`! zj7`42!dI^bbMrLivjTnPYZ>3iOZ@g^VBMk%jrN0zqV)$O6&Ix>2I z^(0d7T1gKItovue#11nTQB&8M&BkGk7D0m_9U@_)w%L0T?il)gWpBP*YLw8Jb^iDB ztfiYcZ~gDbkfhXH*`>t=ONhR89VO_!C%>T`FqrQ)Zx6KllS zJ0%}%#wO<`kqM)%e`z<-SNe@#4EKLX!Bk~t#dR&{G611EWo z^u3TJbgMRDw|M+>#EYNu%%b4>fH7u|lqcr@z7mtI2P~6FRhC+57WEJL9yKzaROKEQ zGpV+R^D)J4Rlkj9QT%wDHg^pl_xZx|4>{y^gFYCw68V^z8aj-jyX@Jn&Cc1jj#yQ$ z(kC8{GG;kr9(ue@JsHDQ(Qbzd7)#@8F=SVlGn=zGST|JdcBm-vNmB1IW2rr3u<2b? z1y*GaZZwYi%^a<|kJI9^YU;J7-)ZCkSd{b@F$S={7)zX>|36i+KIt3VWTTK&b>9h8 z;JIDeRA2V$*1{8;PwpZ2%cyJaw8idHcP21Oen&$_NnDNi+G&e*@0O^MX18*hC{z4y zZ7qFtU$<u~)mG|E$srGb`9~&AEd}OWkTd}j=ZEJOMNc8dDr|M58 z`MPMxb#+4P#U)uRQ6`&F*r=%T~+t432W zXYW@rGtF7;d~6Jj^as@f@u5zFrW0|@6fS5J4r&$i)xr5#<#gNZtrb&BDmU_=a-T}+ zHlra)KeNfaJKdXtPto9kNa-;~5sRdU`^Aqa`hW5~GaRUou}VWh&FBExGaG2{3Zwcv5-KNNjUc_mir_ z493(y)SnOe|B+T!9s)LMGIj=yYILFjry^&P;^b5MJ(DX=ug%#~*R`ft!i%TrNFe|X zNl8q<(WkYs>D1&@p#nXZt+r}fC!D$X$PJ+}O+VMLty8h8wGQdgJpZh9eaq4_+8XU< z@wB&Y1#FOqLj02E@<%QI$X=3B@J2?1(M5e^uIMfjI^up>Q1|)K8!Z3XG5z&74R6jrJ8i?xXVzM_KBQyxRK2#<$4IlU+Z)tnlY4IV zryh&L9seq1+nPp|zi-jil)1F6$t^|=olOQjV^rj9j+6$*sNgwVJ&(pj&z-}gVy31S zRs1}2Yg6D2)hLR3dwE0KI`nVby-T~EeQoZ3Ykagzx9nH4*nYgBMn{?J%l)$`bN1Bs zH&L`c4Vl(zB^x}4<~X0kTHI8H=5tZk9?)~O{MS$C1eGVu9i6vGG*r$vCx6LOhOB$Ct5k zcuxq~__n%!t7lYo+d+iz@$!?C&acXOxw)jD!$a-*=y>Vd`)yxlZ9;)30%cyX;3Gl5 zjbAzA#`#_@)}YqJ@;_8Vms4N=njMb0ecbBtrj$GvH?>udE_US~R+KnO(-x{`6R#U%|Qa>L;q}N^^a`15dQtjC2aGQnJ;C8my7z z(M2C`n_N$|Bu0PVzTm5?iyQ{&Ye*K|uU7J)qEFSWj4b8y(a9N9}d$t@F}qoCq0zEl+n;r9z~gGqnN>YcsU zpEbHov?gr5G(^7E&R};oFLU*`y*6JH!y2jSaTvL@1fsAG$NI6}?vh>lu72oTGQFH#f3%d;US|FZa*&Y7zf% z)k!;4OIn{Q|EltAq~e@Ds`4ADmVzI(3;a(mleUZ<fx+wD66cfTA@+Y(hc|IM=jvzmW)qLm z>$96>;ume9@^VParN1w}kvTDjrC-!Kng4cwQLU5AgGSmW!#{aS$>z-;`pNbF2eS~Y(BX)XhS-H9y6{?L8)+rHD@$Fl8^V!TQ0h$e;;S{ zv6eFKyK;;+yOuWdmZ)^x!VUp1*5>WSc^V$GnrY^DJA!K~4Ic#cU zQmvxR*>Vmvo!h zOml6c9e0_V+nU;^Qk{1*2wdz`{e#4JhfgZWWn)iUO zey~$x_b|8Lu~Qy<8RaKatI)j+?>hWMqFmjMj=yp~@MBDN>ps|8kH6Tf{d;Mpj%dh~ z`#9ju>PrhAZ~``5=6YXw!BoB9OD?pygH0vVMBmwG4l$9fXtI?ymFZ%}=;lYvCR-B^ zJGbOku;r4ImeHTawsq{fW9umW!a+{8o5ens+i1u$+oxXqasU0~#aCp?lnXd-68(LEf-AJchpr}(bjqxv2y5z-WGOI;sH2R8LHgCa$ z-V|Cq@)vnFQ<_9Nn6u{7u+~DqXU&Vr6Zur0_~gY$P8?DndDdxb%|)#eV=d>b%%_;g z=^CM@$oo%dO6lHr4Vd9R?%_c>I4DK^^QyA$^X~UzYsT6w{vtkQtUkS*)qV+^ydj*> zplJLZBdpZOlc*f6(`B@uO3qm5NyA=oR_RYuyLX*cfzvEQg0iT<({$<&eAY`!?c1Wt zhytDknWl_-FkW=Z&quzb+nQKS@_hw2GmOdz@Kkjg#%v=Rf? zEH&ANA`zcGn6kWBRQ-9zw@25%k?2(GbLO(f@=G3=?qN~q&rwj_44coMMV5#Dbx*(X znU`aro!lz6-l0#qtSS4XT%vNhv=LfnL$zv7rpeRvYMxYRQW-7a_g zHV01DF3cO#s7r_Q9~tXAb$(TzLRp1GrmKCqRoWOjub!0trbkx0k~X}m)H{ZmcaOVr zyvWS!n@6?0h$c^sL|mj@a(bvW7tPNsP4jA5DZeA1M}Y~ZBi5m7z1FqMt2SI>aP>yR ziL}=5zTesQ#D%em8Y6X|6Nl8Mp7{z%H)o+K!+u6y^^LHWCAvnotzS0;4yopA4Qst7 zZON;=FBAI_G-P55eDkVd(^F24iLqb7M+VXSkO{9Z|IzMkV%R5n)$q%drEEU+<|<{W zolk{cBejTpCTkxnI&GfKdXHIB7JY(p&L4S&(H4V$**Fr5LHn=+%AO<9y|So+wc4Op`p*pN;6| zOnPJ{Er@H1{>j3rYu7lWPbU1YBTFf*Z%`x4i)dZ$ zO9qeaE|nK?k7SN6Nfv{udDHCc_n)19VNtE8?b0ta-EZybxFumUmqPY;98mH4mi0ZQ z`lcsEqZ4k*LsCJ-l;t)r(v9a^LNWk$)yZ5r$DeJvw9^`;2X=0P>} z9?wG@@Ks0e;osITy4ZaRXo+;w#(ak?>(cG0UtMk|={Gr+`l$}Fta93vj(!p=YncV5 z)r$wbsl(gaSK`d!X=9%85d!|=UqrWlXs&7-_Im;CSY&qOjL8lAPTy`@P}}bGEVH2d ziei0cWy38OwhE2&2M12Lvn3yHS2~VELWR?*HV@4?qIHOtmIsC;G(bZn(O;^wMe*pvfPg(!~ diff --git a/deploy/zep-stack/.env.example b/deploy/zep-stack/.env.example deleted file mode 100644 index 8a0a009..0000000 --- a/deploy/zep-stack/.env.example +++ /dev/null @@ -1,12 +0,0 @@ -# Zep Stack Environment Variables - remember to change zep.yaml as well -PORT=3002 -HEALTHCHECK=http://localhost:8003/healthcheck -OPENAI_API_KEY=your_openai_api_key - -# Database configuration -POSTGRES_USER=postgres -POSTGRES_PASSWORD=postgres - -# Neo4j configuration -NEO4J_USER=neo4j -NEO4J_PASSWORD=zepzepzep diff --git a/deploy/zep-stack/docker-compose.yml b/deploy/zep-stack/docker-compose.yml deleted file mode 100644 index 7dfcb89..0000000 --- a/deploy/zep-stack/docker-compose.yml +++ /dev/null @@ -1,81 +0,0 @@ -name: zep-ce - -services: - zep: - image: zepai/zep:latest - container_name: zep-ce - ports: - - ${PORT}:8000 - volumes: - - ./zep.yaml:/app/zep.yaml - environment: - - ZEP_CONFIG_FILE=zep.yaml - depends_on: - graphiti: - condition: service_healthy - db: - condition: service_healthy - db: - image: pgvector/pgvector:pg17 - container_name: zep-ce-postgres - restart: on-failure - shm_size: "128mb" # Increase this if vacuuming fails with a "no space left on device" error - environment: - - POSTGRES_USER=${POSTGRES_USER} - - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} - healthcheck: - test: ["CMD", "pg_isready", "-q", "-d", "postgres", "-U", "postgres"] - interval: 5s - timeout: 5s - retries: 5 - volumes: - - zep-db:/var/lib/postgresql/data - ports: - - "5434:5432" - graphiti: - image: zepai/graphiti:0.3 - container_name: zep-ce-graphiti - ports: - - "8003:8003" - env_file: - - .env - healthcheck: - test: - [ - "CMD", - "python", - "-c", - "import urllib.request; urllib.request.urlopen('http://localhost:8003/healthcheck')", - ] - interval: 10s - timeout: 5s - retries: 3 - depends_on: - neo4j: - condition: service_healthy - environment: - - OPENAI_API_KEY=${OPENAI_API_KEY} - - MODEL_NAME=gpt-4o-mini - - NEO4J_URI=bolt://neo4j:7687 - - NEO4J_USER=${NEO4J_USER:-neo4j} - - NEO4J_PASSWORD=${NEO4J_PASSWORD:-zepzepzep} - - PORT=8003 - neo4j: - image: neo4j:5.22.0 - container_name: zep-ce-neo4j - healthcheck: - test: wget http://localhost:7687 || exit 1 - interval: 1s - timeout: 10s - retries: 20 - start_period: 3s - ports: - - "7474:7474" # HTTP - - "7687:7687" # Bolt - volumes: - - neo4j_data:/data - environment: - - NEO4J_AUTH=${NEO4J_USER}/${NEO4J_PASSWORD} -volumes: - neo4j_data: - zep-db: diff --git a/deploy/zep-stack/zep.yaml b/deploy/zep-stack/zep.yaml deleted file mode 100644 index 3e0ac4b..0000000 --- a/deploy/zep-stack/zep.yaml +++ /dev/null @@ -1,43 +0,0 @@ -log: - # debug, info, warn, error, panic, dpanic, or fatal. Default = info - level: info - # How should logs be formatted? Setting to "console" will print human readable logs - # whie "json" will print structured JSON logs. Default is "json". - format: json -http: - # Host to bind to. Default is 0.0.0.0 - host: 0.0.0.0 - # Port to bind to. Default is 8000 - port: 8000 - max_request_size: 5242880 -postgres: - user: postgres - password: postgres - host: db - port: 5432 - database: postgres - schema_name: public - read_timeout: 30 - write_timeout: 30 - max_open_connections: 10 -# Carbon is a package used for dealing with time - github.com/golang-module/carbon -# It is primarily used for generating humand readable relative time strings like "2 hours ago". -# See the list of supported languages here https://github.com/golang-module/carbon?tab=readme-ov-file#i18n -carbon: - locale: en -graphiti: - # Base url to the graphiti service - service_url: http://graphiti:8003 -# In order to authenicate API requests to the Zep service, a secret must be provided. -# This secret should be kept secret between the Zep service and the client. It can be any string value. -# When making requests to the Zep service, include the secret in the Authorization header. -api_secret: abc123 -# In order to better understand how Zep is used, we can collect telemetry data. -# This is optional and can be disabled by setting disabled to true. -# We do not collect any PII or any of your data. We only collect anonymized data -# about how Zep is used. -telemetry: - disabled: false - # Please provide an identifying name for your organization so can get a better understanding - # about who is using Zep. This is optional. - organization_name: Awana Digital diff --git a/packages/jobs/package.json b/packages/jobs/package.json index 86fec0a..88945f1 100644 --- a/packages/jobs/package.json +++ b/packages/jobs/package.json @@ -13,7 +13,6 @@ "dependencies": { "@eda/supabase": "workspace:*", "@eda/types": "workspace:*", - "@getzep/zep-cloud": "^2.1.1", "@trigger.dev/sdk": "3.3.11" }, "devDependencies": { From 16e93d27a78f5edf44c5c7e6bdb9a2ec766dbbf3 Mon Sep 17 00:00:00 2001 From: Luis Otavio Date: Wed, 22 Jan 2025 03:05:51 -0300 Subject: [PATCH 61/75] Refactor: Simplify CI workflow and update package versions; streamline task triggering in API --- .github/workflows/ci.yml | 9 +++------ apps/dashboard/src/app/api/actions.ts | 8 +++----- packages/config/python/pyproject.toml | 2 +- packages/config/typescript/package.json | 2 +- 4 files changed, 8 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7bf39d7..4570cfd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,18 +22,15 @@ jobs: uses: astral-sh/setup-uv@v3 - name: Install dependencies - run: | - bun install - uv venv - . .venv/bin/activate + run: bun install - name: Setup config run: | cp config.example.yaml config.yaml bun run build:config - - name: Build remaining packages - run: turbo run build --filter=!@eda/config --filter=!@eda/config-python + - name: Run builds + run: bun run build # - name: Run tests # run: bun run test \ No newline at end of file diff --git a/apps/dashboard/src/app/api/actions.ts b/apps/dashboard/src/app/api/actions.ts index dbcf44f..ebdec5f 100644 --- a/apps/dashboard/src/app/api/actions.ts +++ b/apps/dashboard/src/app/api/actions.ts @@ -1,14 +1,12 @@ "use server"; -import type { helloWorldTask } from "@eda/jobs/trigger/example"; +import { helloWorldTask } from "@eda/jobs/trigger/example"; import { tasks } from "@trigger.dev/sdk/v3"; export async function myTask() { try { - const handle = await tasks.trigger( - "hello-world", - "James", - ); + // Remove task option and pass payload directly + const handle = await tasks.trigger("hello-world", "James"); return { handle }; } catch (error) { diff --git a/packages/config/python/pyproject.toml b/packages/config/python/pyproject.toml index 33e5fe7..b0e5856 100644 --- a/packages/config/python/pyproject.toml +++ b/packages/config/python/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "eda-config" -version = "0.1.13" +version = "0.1.16" description = "Configuration management for EDA" requires-python = ">=3.11" dependencies = [ diff --git a/packages/config/typescript/package.json b/packages/config/typescript/package.json index a83bcb6..3c569e7 100644 --- a/packages/config/typescript/package.json +++ b/packages/config/typescript/package.json @@ -1,6 +1,6 @@ { "name": "@eda/config", - "version": "0.1.8", + "version": "0.1.11", "main": "dist/config.js", "types": "dist/config.d.ts", "type": "module", From 26b7f45e586a408acf05d243bd0636a33be2e43d Mon Sep 17 00:00:00 2001 From: Luis Otavio Date: Wed, 22 Jan 2025 03:20:15 -0300 Subject: [PATCH 62/75] Refactor: Update configuration for database ports and remove unused dependencies; enhance API server settings --- apps/ai_api/eda_ai_api/main.py | 11 +++++++---- apps/ai_api/uv.lock | 18 +----------------- config.example.yaml | 6 ++++-- deploy/trigger-stack/export-config.ts | 2 +- packages/config/python/eda_config/types.py | 2 ++ packages/config/python/pyproject.toml | 2 +- packages/config/typescript/package.json | 2 +- packages/config/typescript/src/types.ts | 2 ++ 8 files changed, 19 insertions(+), 26 deletions(-) diff --git a/apps/ai_api/eda_ai_api/main.py b/apps/ai_api/eda_ai_api/main.py index 648b210..3ba7007 100644 --- a/apps/ai_api/eda_ai_api/main.py +++ b/apps/ai_api/eda_ai_api/main.py @@ -31,12 +31,15 @@ def run_server() -> None: # Use localhost for development, configure via config for production host = "127.0.0.1" # Default to localhost - if config.services.ai_api.get( - "allow_external", False - ): # Only if explicitly enabled + if ( + hasattr(config.services.ai_api, "allow_external") + and config.services.ai_api.allow_external + ): host = "0.0.0.0" # nosec B104 # Explicitly allowed in config - uvicorn.run("eda_ai_api.main:app", host=host, port=config.ports.ai_api, reload=True) + uvicorn.run( + "eda_ai_api.main:app", host=host, port=config.ports.ai_api, reload=True + ) if __name__ == "__main__": diff --git a/apps/ai_api/uv.lock b/apps/ai_api/uv.lock index b398338..e0e7c07 100644 --- a/apps/ai_api/uv.lock +++ b/apps/ai_api/uv.lock @@ -863,7 +863,6 @@ dependencies = [ { name = "rank-bm25" }, { name = "requests" }, { name = "uvicorn" }, - { name = "zep-python" }, ] [package.optional-dependencies] @@ -912,12 +911,11 @@ requires-dist = [ { name = "rank-bm25", specifier = ">=0.2.2" }, { name = "requests", specifier = ">=2.31.0" }, { name = "uvicorn", specifier = ">=0.25.0" }, - { name = "zep-python", specifier = ">=2.0.2" }, ] [[package]] name = "eda-config" -version = "0.1.13" +version = "0.1.19" source = { directory = "../../packages/config/python" } dependencies = [ { name = "pydantic" }, @@ -5303,20 +5301,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/93/86/f1305e1ab1d6dc27d245ffc83d18d88f2bebf6c6488725ee82dffb3eda7a/yarl-1.17.0-py3-none-any.whl", hash = "sha256:62dd42bb0e49423f4dd58836a04fcf09c80237836796025211bbe913f1524993", size = 44053 }, ] -[[package]] -name = "zep-python" -version = "2.0.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "httpx" }, - { name = "pydantic" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b5/64/1b0c4afa6d0e8f35e1072eacdfe8ec216871be5f2dfd569942f69232482f/zep_python-2.0.2.tar.gz", hash = "sha256:919fd635ad5801f30d9ef6da0bb99e854ed4c3cd1357dc8ada4a39b8171a41bc", size = 22273 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1c/d4/09a6bc0c32f91b7b5b93322513d51299ca9b35bdd6b0560b39104c296f39/zep_python-2.0.2-py3-none-any.whl", hash = "sha256:d7b2cadf1eb15007825362cae5ddff9c9d0ada5b4f75cf91e5b87986459f7817", size = 40011 }, -] - [[package]] name = "zipp" version = "3.20.2" diff --git a/config.example.yaml b/config.example.yaml index 29e783c..072a814 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -10,7 +10,9 @@ ports: landingpage: 8081 docs: 8082 db: - postgres: 5432 + postgres: 5439 + trigger_postgres: 5440 + langtrace_postgres: 5441 redis: 6379 neo4j: http: 7474 # Neo4j browser interface @@ -114,7 +116,7 @@ services: trigger: project_id: "xxx" api_url: "http://localhost:3040" - environment: "development" + environment: "production" runtime: "docker-compose" v3_enabled: true concurrency: diff --git a/deploy/trigger-stack/export-config.ts b/deploy/trigger-stack/export-config.ts index 269e5d0..7277f9b 100644 --- a/deploy/trigger-stack/export-config.ts +++ b/deploy/trigger-stack/export-config.ts @@ -18,7 +18,7 @@ const envVars = { DIRECT_URL: `postgresql://${config.databases.trigger_postgres.user}:${config.databases.trigger_postgres.password}@postgres:5432/${config.databases.trigger_postgres.database}`, // Add this line // Add database ports - POSTGRES_PORT: config.ports.db.postgres, + POSTGRES_PORT: config.ports.db.trigger_postgres, // Use trigger-specific port REDIS_PORT: config.ports.db.redis, // Redis settings diff --git a/packages/config/python/eda_config/types.py b/packages/config/python/eda_config/types.py index 170887a..6a3090f 100644 --- a/packages/config/python/eda_config/types.py +++ b/packages/config/python/eda_config/types.py @@ -64,6 +64,8 @@ class ApiKeys(BaseModel): class DbPorts(BaseModel): postgres: int + trigger_postgres: int # Add new port + langtrace_postgres: int # Add new port redis: int neo4j: Dict[str, int] clickhouse: int diff --git a/packages/config/python/pyproject.toml b/packages/config/python/pyproject.toml index b0e5856..a0842e9 100644 --- a/packages/config/python/pyproject.toml +++ b/packages/config/python/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "eda-config" -version = "0.1.16" +version = "0.1.19" description = "Configuration management for EDA" requires-python = ">=3.11" dependencies = [ diff --git a/packages/config/typescript/package.json b/packages/config/typescript/package.json index 3c569e7..face18a 100644 --- a/packages/config/typescript/package.json +++ b/packages/config/typescript/package.json @@ -1,6 +1,6 @@ { "name": "@eda/config", - "version": "0.1.11", + "version": "0.1.14", "main": "dist/config.js", "types": "dist/config.d.ts", "type": "module", diff --git a/packages/config/typescript/src/types.ts b/packages/config/typescript/src/types.ts index b28706e..5692fb8 100644 --- a/packages/config/typescript/src/types.ts +++ b/packages/config/typescript/src/types.ts @@ -171,6 +171,8 @@ export const ConfigSchema = z.object({ docs: z.number(), db: z.object({ postgres: z.number(), + trigger_postgres: z.number(), // Add new port + langtrace_postgres: z.number(), // Add new port redis: z.number(), neo4j: z.object({ http: z.number(), From b5080b0044fcb9bf339c2330a91e1b973cfdfd44 Mon Sep 17 00:00:00 2001 From: Luis Otavio Date: Wed, 22 Jan 2025 03:49:38 -0300 Subject: [PATCH 63/75] Refactor: Enhance CI workflow by skipping config packages during endpoint tests; update Flake8 configuration and linting scripts --- .github/workflows/stacks.yml | 8 +++++++- apps/ai_api/.flake8 | 4 +++- apps/ai_api/eda_ai_api/main.py | 9 ++++----- apps/ai_api/package.json | 2 +- apps/ai_api/setup.cfg | 6 ++++++ apps/messaging/package.json | 2 +- biome.json | 28 ++++++++++++++++------------ 7 files changed, 38 insertions(+), 21 deletions(-) diff --git a/.github/workflows/stacks.yml b/.github/workflows/stacks.yml index f4e3022..cebd31f 100644 --- a/.github/workflows/stacks.yml +++ b/.github/workflows/stacks.yml @@ -66,9 +66,15 @@ jobs: - name: Test endpoints run: | while IFS= read -r dir; do + service_name=$(basename "$dir") echo "Testing endpoints for $dir" - service_name=$(basename "$dir") + # Skip config packages + if [[ "$service_name" == *"config"* ]]; then + echo "Skipping config package: $service_name" + continue + fi + port=$(bun run @eda/config getPorts $service_name) if [ -z "$port" ]; then diff --git a/apps/ai_api/.flake8 b/apps/ai_api/.flake8 index 5e366e4..65a2615 100644 --- a/apps/ai_api/.flake8 +++ b/apps/ai_api/.flake8 @@ -87,6 +87,7 @@ hang-closing = True # E133 - closing bracket is missing indentation (conflicts with black) # E203 - whitespace before β€˜:’ (conflicts with black) # W503 - line break before binary operator +# W504 - line break after binary operator # F401 - module imported but unused # F403 - β€˜from module import *’ used; unable to detect undefined names # @@ -95,7 +96,8 @@ hang-closing = True ignore = E133, E203, - W503 + W503, + W504 # Specify the list of error codes you wish Flake8 to report. select = E, diff --git a/apps/ai_api/eda_ai_api/main.py b/apps/ai_api/eda_ai_api/main.py index 3ba7007..92fb3cc 100644 --- a/apps/ai_api/eda_ai_api/main.py +++ b/apps/ai_api/eda_ai_api/main.py @@ -31,15 +31,14 @@ def run_server() -> None: # Use localhost for development, configure via config for production host = "127.0.0.1" # Default to localhost - if ( + allow_external = ( hasattr(config.services.ai_api, "allow_external") and config.services.ai_api.allow_external - ): + ) + if allow_external: host = "0.0.0.0" # nosec B104 # Explicitly allowed in config - uvicorn.run( - "eda_ai_api.main:app", host=host, port=config.ports.ai_api, reload=True - ) + uvicorn.run("eda_ai_api.main:app", host=host, port=config.ports.ai_api, reload=True) if __name__ == "__main__": diff --git a/apps/ai_api/package.json b/apps/ai_api/package.json index c46f123..977da37 100644 --- a/apps/ai_api/package.json +++ b/apps/ai_api/package.json @@ -4,7 +4,7 @@ "scripts": { "dev": "uv run python -m eda_ai_api.main", "start": "uv run python -m eda_ai_api.main", - "lint": "bash ./scripts/linting.sh", + "lint": "uvx flake8 eda_ai_api --config=setup.cfg", "test": "bash ./scripts/test.sh" } } diff --git a/apps/ai_api/setup.cfg b/apps/ai_api/setup.cfg index 1616126..46802d3 100644 --- a/apps/ai_api/setup.cfg +++ b/apps/ai_api/setup.cfg @@ -7,3 +7,9 @@ branch = True [coverage:report] precision = 2 + +[flake8] +ignore = E133,E203,W503,W504 +max-line-length = 100 +extend-ignore = W503 +per-file-ignores = __init__.py:F401 diff --git a/apps/messaging/package.json b/apps/messaging/package.json index 59da657..611acbc 100644 --- a/apps/messaging/package.json +++ b/apps/messaging/package.json @@ -8,7 +8,7 @@ "start": "bun run dist/index.js", "test": "bun test", "clean": "rm -rf .turbo node_modules dist", - "lint": "biome lint", + "lint": "biome lint ./src", "format": "biome format --write .", "typecheck": "tsc-files --noEmit" }, diff --git a/biome.json b/biome.json index f849c6c..0c6f157 100644 --- a/biome.json +++ b/biome.json @@ -4,22 +4,26 @@ "enabled": true }, "files": { - "ignore": ["src/components/ui"] + "ignore": [ + "src/components/ui", + "**/dist/**", + "dist", + "**/dist", + "apps/messaging/dist" + ], + "maxSize": 3145728 }, "linter": { - "ignore": ["node_modules", ".next", "packages/tsconfig"], + "ignore": [ + "node_modules", + ".next", + "packages/tsconfig", + "**/dist/**", + "dist" + ], "enabled": true, "rules": { - "recommended": true, - "a11y": { - "noSvgWithoutTitle": "off" - }, - "style": { - "noNonNullAssertion": "off" - }, - "correctness": { - "useExhaustiveDependencies": "off" - } + "recommended": true } }, "formatter": { From 96a14e200f3db2f4831b6baafb2ddd6140f4de43 Mon Sep 17 00:00:00 2001 From: Luis Otavio Date: Wed, 22 Jan 2025 04:00:14 -0300 Subject: [PATCH 64/75] Refactor: Update endpoint testing to skip config packages and improve service name extraction; modify Redis client configuration to use centralized config --- .github/workflows/stacks.yml | 7 ++++--- packages/kv/src/index.ts | 9 +++++---- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/.github/workflows/stacks.yml b/.github/workflows/stacks.yml index cebd31f..feacf70 100644 --- a/.github/workflows/stacks.yml +++ b/.github/workflows/stacks.yml @@ -66,11 +66,12 @@ jobs: - name: Test endpoints run: | while IFS= read -r dir; do - service_name=$(basename "$dir") - echo "Testing endpoints for $dir" + # Extract service name by removing "deploy/" and "-stack" + service_name=$(basename "$dir" | sed 's/-stack//') + echo "Testing endpoints for service: $service_name" # Skip config packages - if [[ "$service_name" == *"config"* ]]; then + if [[ "$service_name" == "config" ]]; then echo "Skipping config package: $service_name" continue fi diff --git a/packages/kv/src/index.ts b/packages/kv/src/index.ts index ef00d55..3a0acf0 100644 --- a/packages/kv/src/index.ts +++ b/packages/kv/src/index.ts @@ -1,8 +1,9 @@ -import "server-only"; - import { Redis } from "@upstash/redis"; +import "server-only"; +//@ts-ignore +import { config } from "@eda/config"; export const client = new Redis({ - url: process.env.UPSTASH_REDIS_REST_URL!, - token: process.env.UPSTASH_REDIS_REST_TOKEN!, + url: config.services.upstash.redis_url, + token: config.services.upstash.redis_token, }); From 5161803115bdd1a9e1646aac0473622d49a7722b Mon Sep 17 00:00:00 2001 From: Luis Otavio Date: Thu, 23 Jan 2025 06:16:51 -0300 Subject: [PATCH 65/75] Fix lint errors, stack.yml --- .github/workflows/stacks.yml | 3 +- .../src/components/custom/AboutSection.jsx | 5 +- .../src/components/custom/ContactSection.jsx | 7 +- .../src/components/custom/FeaturesSection.jsx | 88 +++++++++---------- .../src/components/custom/HeroSection.jsx | 9 +- .../components/custom/HowItWorksSection.jsx | 17 ++-- .../components/custom/ProsAndConsSection.jsx | 74 +++++++++++----- .../src/components/custom/whatsapp-button.jsx | 5 +- 8 files changed, 126 insertions(+), 82 deletions(-) diff --git a/.github/workflows/stacks.yml b/.github/workflows/stacks.yml index feacf70..e0d64ef 100644 --- a/.github/workflows/stacks.yml +++ b/.github/workflows/stacks.yml @@ -76,7 +76,8 @@ jobs: continue fi - port=$(bun run @eda/config getPorts $service_name) + # Use the built config module through node + port=$(node -e "import('@eda/config').then(c => console.log(c.config.ports['${service_name}']))") if [ -z "$port" ]; then echo "No port found for $service_name in config, using default 3000" diff --git a/apps/landingpage/src/components/custom/AboutSection.jsx b/apps/landingpage/src/components/custom/AboutSection.jsx index c0dc04d..746116a 100644 --- a/apps/landingpage/src/components/custom/AboutSection.jsx +++ b/apps/landingpage/src/components/custom/AboutSection.jsx @@ -1,5 +1,5 @@ -import React from "react"; import { motion } from "framer-motion"; +import React from "react"; const AboutSection = ({ theme, themeColors }) => { return ( @@ -37,7 +37,10 @@ const AboutSection = ({ theme, themeColors }) => { preserveAspectRatio="none" className="relative block w-full h-20" style={{ transform: "scaleY(-1)" }} + role="img" + aria-labelledby="aboutWaveTitle" > + About Section Wave { return ( @@ -11,7 +11,10 @@ const ContactSection = ({ theme, themeColors }) => { preserveAspectRatio="none" className="relative block w-full h-16" style={{ transform: "scaleY(-1)" }} + role="img" + aria-labelledby="contactWaveTitle" > + Contact Section Wave { const { t } = useTranslation(); + const features = [ + { + id: "mapeo", + icon: , + title: t("mapeoIntegration"), + description: t("mapeoIntegrationDesc"), + }, + { + id: "terrastories", + icon: , + title: t("terrastoriesConnection"), + description: t("terrastoriesConnectionDesc"), + }, + { + id: "toolkit", + icon: , + title: t("earthDefendersToolkit"), + description: t("earthDefendersToolkitDesc"), + }, + ].map((feature) => ( + + {feature.icon} +

+ {feature.title} +

+

{feature.description}

+ + )); + return (
{ {t("featuresTitle")}
- {[ - { - icon: ( - - ), - title: t("mapeoIntegration"), - description: t("mapeoIntegrationDesc"), - }, - { - icon: ( - - ), - title: t("terrastoriesConnection"), - description: t("terrastoriesConnectionDesc"), - }, - { - icon: ( - - ), - title: t("earthDefendersToolkit"), - description: t("earthDefendersToolkitDesc"), - }, - ].map((feature, index) => ( - - {feature.icon} -

- {feature.title} -

-

{feature.description}

-
- ))} + {features}
@@ -71,7 +64,10 @@ const FeaturesSection = ({ theme, themeColors }) => { preserveAspectRatio="none" className="relative block w-full h-20" style={{ transform: "scaleY(-1)" }} + role="img" + aria-labelledby="featuresWaveTitle" > + Features Section Wave { return ( @@ -67,7 +67,10 @@ const HeroSection = ({ theme, themeColors, t }) => { preserveAspectRatio="none" className="relative block w-full h-20" style={{ transform: "scaleY(-1)" }} + role="img" + aria-labelledby="heroWaveTitle" > + Decorative Wave Pattern { @@ -8,21 +8,25 @@ const HowItWorksSection = ({ theme, themeColors }) => { const features = [ { + id: "voice", icon: , title: t("voiceFirstTech"), description: t("voiceFirstDesc"), }, { + id: "offline", icon: , title: t("offlineFirst"), description: t("offlineFirstDesc"), }, { + id: "language", icon: , title: t("languagePreservation"), description: t("languagePreservationDesc"), }, { + id: "territory", icon: , title: t("territoryDefense"), description: t("territoryDefenseDesc"), @@ -48,12 +52,12 @@ const HowItWorksSection = ({ theme, themeColors }) => { {t("howItWorksTitle")}
- {features.map((feature, index) => ( + {features.map((feature) => ( {feature.icon} @@ -74,7 +78,10 @@ const HowItWorksSection = ({ theme, themeColors }) => { viewBox="0 0 1200 120" preserveAspectRatio="none" className="relative block w-full h-16" + role="img" + aria-labelledby="howItWorksWaveTitle" > + How It Works Section Wave { @@ -19,21 +19,21 @@ const ProsAndConsSection = ({ theme, themeColors }) => { }; const objectives = [ - t("enhanceAccessibility"), - t("implementOffline"), - t("trainAI"), - t("developTools"), - t("supportSustainable"), - t("integrateWhatsApp"), + { id: "obj-accessibility", text: t("enhanceAccessibility") }, + { id: "obj-offline", text: t("implementOffline") }, + { id: "obj-ai", text: t("trainAI") }, + { id: "obj-tools", text: t("developTools") }, + { id: "obj-sustainable", text: t("supportSustainable") }, + { id: "obj-whatsapp", text: t("integrateWhatsApp") }, ]; const outcomes = [ - t("empoweredCommunities"), - t("languageCulturalPreservation"), - t("increasedAccessibility"), - t("dataSovereignty"), - t("effectiveProjectManagement"), - t("simplifiedCommunication"), + { id: "out-communities", text: t("empoweredCommunities") }, + { id: "out-preservation", text: t("languageCulturalPreservation") }, + { id: "out-accessibility", text: t("increasedAccessibility") }, + { id: "out-sovereignty", text: t("dataSovereignty") }, + { id: "out-management", text: t("effectiveProjectManagement") }, + { id: "out-communication", text: t("simplifiedCommunication") }, ]; return ( @@ -63,8 +63,8 @@ const ProsAndConsSection = ({ theme, themeColors }) => { {t("projectObjectives")}
    - {objectives.map((objective, index) => ( -
  • {objective}
  • + {objectives.map((objective) => ( +
  • {objective.text}
  • ))}
@@ -84,8 +84,8 @@ const ProsAndConsSection = ({ theme, themeColors }) => { {t("expectedOutcomes")}
    - {outcomes.map((outcome, index) => ( -
  • {outcome}
  • + {outcomes.map((outcome) => ( +
  • {outcome.text}
  • ))}
@@ -100,9 +100,21 @@ const ProsAndConsSection = ({ theme, themeColors }) => { y: ["-30px", "30px"], }} transition={{ - rotate: { repeat: Number.POSITIVE_INFINITY, duration: 20, ease: "linear" }, - x: { repeat: Number.POSITIVE_INFINITY, duration: 5, yoyo: Number.POSITIVE_INFINITY }, - y: { repeat: Number.POSITIVE_INFINITY, duration: 7, yoyo: Number.POSITIVE_INFINITY }, + rotate: { + repeat: Number.POSITIVE_INFINITY, + duration: 20, + ease: "linear", + }, + x: { + repeat: Number.POSITIVE_INFINITY, + duration: 5, + yoyo: Number.POSITIVE_INFINITY, + }, + y: { + repeat: Number.POSITIVE_INFINITY, + duration: 7, + yoyo: Number.POSITIVE_INFINITY, + }, }} /> { y: ["50px", "-50px"], }} transition={{ - rotate: { repeat: Number.POSITIVE_INFINITY, duration: 20, ease: "linear" }, - x: { repeat: Number.POSITIVE_INFINITY, duration: 6, yoyo: Number.POSITIVE_INFINITY }, - y: { repeat: Number.POSITIVE_INFINITY, duration: 8, yoyo: Number.POSITIVE_INFINITY }, + rotate: { + repeat: Number.POSITIVE_INFINITY, + duration: 20, + ease: "linear", + }, + x: { + repeat: Number.POSITIVE_INFINITY, + duration: 6, + yoyo: Number.POSITIVE_INFINITY, + }, + y: { + repeat: Number.POSITIVE_INFINITY, + duration: 8, + yoyo: Number.POSITIVE_INFINITY, + }, }} />
@@ -124,10 +148,14 @@ const ProsAndConsSection = ({ theme, themeColors }) => { viewBox="0 0 1200 120" preserveAspectRatio="none" className="relative block w-full h-16" + style={{ transform: "scaleY(-1)" }} initial={{ y: 20 }} animate={{ y: 0 }} transition={{ duration: 1, ease: "easeOut" }} + role="img" + aria-labelledby="prosConsWaveTitle" > + Pros and Cons Section Wave { const handleClick = () => { @@ -25,7 +25,10 @@ const WhatsAppButton = ({ text, theme }) => { className="w-6 h-6 ml-2" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg" + role="img" + aria-labelledby="whatsappIconTitle" > + WhatsApp Icon Date: Thu, 23 Jan 2025 06:30:14 -0300 Subject: [PATCH 66/75] Fix whatsapp lint --- apps/whatsapp/src/client.ts | 40 +++++---- apps/whatsapp/src/utils.ts | 172 ++++++++++++++++++++---------------- 2 files changed, 118 insertions(+), 94 deletions(-) diff --git a/apps/whatsapp/src/client.ts b/apps/whatsapp/src/client.ts index 2d1fc79..a9918f4 100644 --- a/apps/whatsapp/src/client.ts +++ b/apps/whatsapp/src/client.ts @@ -12,6 +12,7 @@ import { handleCommand } from "./commands/commands_index"; import { CMD_PREFIX } from "./constants"; import { handleMessage } from "./message/message"; import { + getRemoteJid, isGroupMessage, react, shouldIgnore, @@ -99,26 +100,29 @@ async function startSock() { const upsert = events["messages.upsert"]; //console.log("recv messages ", JSON.stringify(upsert, undefined, 2)); + // Update the message processing code if (upsert.type === "notify") { for (const message of upsert.messages) { - const chatId = message.key.remoteJid!; - const unreadCount = unreadCounts[chatId] || 0; - - if (await shouldIgnore(message)) return; - if (!(await shouldReply(message))) return; - if (await shouldIgnoreUnread(message, unreadCount)) return; - - await react(message, "queued"); - - // Add the message to the queue - if (!messageQueue[chatId]) { - messageQueue[chatId] = []; - } - messageQueue[chatId].push(message); - - // Process the queue if not already processing - if (!isProcessingMessage) { - await processMessageQueue(chatId); + try { + const chatId = getRemoteJid(message); + const unreadCount = unreadCounts[chatId] || 0; + + if (await shouldIgnore(message)) continue; + if (!(await shouldReply(message))) continue; + if (await shouldIgnoreUnread(message, unreadCount)) continue; + + await react(message, "queued"); + + if (!messageQueue[chatId]) { + messageQueue[chatId] = []; + } + messageQueue[chatId].push(message); + + if (!isProcessingMessage) { + await processMessageQueue(chatId); + } + } catch (error) { + console.error("Error processing message:", error); } } } diff --git a/apps/whatsapp/src/utils.ts b/apps/whatsapp/src/utils.ts index deb20a3..87d0032 100644 --- a/apps/whatsapp/src/utils.ts +++ b/apps/whatsapp/src/utils.ts @@ -12,6 +12,15 @@ import { IGNORE_MESSAGES_WARNING, } from "./constants"; +// Add this helper function at the top +export function getRemoteJid(message: proto.IWebMessageInfo): string { + const jid = message.key.remoteJid; + if (!jid) { + throw new Error("Message has no remote JID"); + } + return jid; +} + export function isGroupMessage(message: proto.IWebMessageInfo) { return message.key.remoteJid?.endsWith("@g.us") ?? false; } @@ -39,19 +48,18 @@ export async function react( message: proto.IWebMessageInfo, reaction: keyof typeof REACTIONS, ) { - switch (ENABLE_REACTIONS) { - case false: - break; - case true: - await sock.sendMessage(message.key.remoteJid!, { - react: { - text: REACTIONS[reaction], - key: message.key, - }, - }); - break; - default: - break; + if (!ENABLE_REACTIONS) return; + + try { + const jid = getRemoteJid(message); + await sock.sendMessage(jid, { + react: { + text: REACTIONS[reaction], + key: message.key, + }, + }); + } catch (error) { + console.error("Failed to send reaction:", error); } } @@ -73,56 +81,63 @@ ${helpStatement}`; } export async function shouldIgnore(message: proto.IWebMessageInfo) { - if (ALLOWED_USERS.length === 0 && BLOCKED_USERS.length === 0) { - return false; - } - - const senderNumber = getPhoneNumber(message); - - if (isGroupMessage(message)) { - // Check if this message came from a blocked user - if (BLOCKED_USERS.includes(senderNumber)) { - console.warn( - `Ignoring message from blocked user "${message.pushName}" <${senderNumber}>`, - ); - return true; + try { + if (ALLOWED_USERS.length === 0 && BLOCKED_USERS.length === 0) { + return false; } - // Fetch group metadata - const groupJid = message.key.remoteJid!; - try { - const groupMetadata = await sock.groupMetadata(groupJid); + const senderNumber = getPhoneNumber(message); - // Check if any allowed users are in the group - const allowedInGroup = groupMetadata.participants.some((participant) => { - const participantNumber = participant.id.split("@")[0]; - return ALLOWED_USERS.includes(participantNumber); - }); + if (isGroupMessage(message)) { + // Check if this message came from a blocked user + if (BLOCKED_USERS.includes(senderNumber)) { + console.warn( + `Ignoring message from blocked user "${message.pushName}" <${senderNumber}>`, + ); + return true; + } - if (!allowedInGroup) { + // Fetch group metadata + const groupJid = getRemoteJid(message); + try { + const groupMetadata = await sock.groupMetadata(groupJid); + + // Check if any allowed users are in the group + const allowedInGroup = groupMetadata.participants.some( + (participant) => { + const participantNumber = participant.id.split("@")[0]; + return ALLOWED_USERS.includes(participantNumber); + }, + ); + + if (!allowedInGroup) { + console.warn( + `Ignoring message from group "${groupMetadata.subject}" because no allowed users are in it`, + ); + return true; + } + } catch (error) { + console.error("Error fetching group metadata:", error); + return true; // Ignore the message if we can't fetch group data + } + } else { + // It's a private message, so just check if the user is blocked or isn't in the allowed list + if ( + BLOCKED_USERS.includes(senderNumber) || + !ALLOWED_USERS.includes(senderNumber) + ) { console.warn( - `Ignoring message from group "${groupMetadata.subject}" because no allowed users are in it`, + `Ignoring message from blocked/not allowed user "${message.pushName}" <${senderNumber}>`, ); return true; } - } catch (error) { - console.error("Error fetching group metadata:", error); - return true; // Ignore the message if we can't fetch group data - } - } else { - // It's a private message, so just check if the user is blocked or isn't in the allowed list - if ( - BLOCKED_USERS.includes(senderNumber) || - !ALLOWED_USERS.includes(senderNumber) - ) { - console.warn( - `Ignoring message from blocked/not allowed user "${message.pushName}" <${senderNumber}>`, - ); - return true; } - } - return false; + return false; + } catch (error) { + console.error("Error in shouldIgnore:", error); + return true; + } } export async function shouldReply(message: proto.IWebMessageInfo) { @@ -168,34 +183,39 @@ export async function shouldIgnoreUnread( unreadCount: number, ) { if (unreadCount > 1) { - const chatJid = message.key.remoteJid!; + try { + const chatJid = getRemoteJid(message); - // Mark messages as read - await sock.readMessages([message.key]); + // Mark messages as read + await sock.readMessages([message.key]); - const isGroup = chatJid.endsWith("@g.us"); - let warningMessage = ""; + const isGroup = chatJid.endsWith("@g.us"); + let warningMessage = ""; - if (isGroup) { - const groupName = await getGroupName(chatJid); - console.warn( - `Too many unread messages (${unreadCount}) for group chat "${groupName}". Ignoring...`, - ); - warningMessage = `Too many unread messages (${unreadCount}) since I've last seen this chat. I'm ignoring them. If you need me to respond, please @mention me or quote my last completion in this chat.`; - } else { - console.warn( - `Too many unread messages (${unreadCount}) for chat with user "${getPhoneNumber( - message, - )}". Ignoring...`, - ); - warningMessage = `Too many unread messages (${unreadCount}) since I've last seen this chat. I'm ignoring them. If you need me to respond, please message me again.`; - } + if (isGroup) { + const groupName = await getGroupName(chatJid); + console.warn( + `Too many unread messages (${unreadCount}) for group chat "${groupName}". Ignoring...`, + ); + warningMessage = `Too many unread messages (${unreadCount}) since I've last seen this chat. I'm ignoring them. If you need me to respond, please @mention me or quote my last completion in this chat.`; + } else { + console.warn( + `Too many unread messages (${unreadCount}) for chat with user "${getPhoneNumber( + message, + )}". Ignoring...`, + ); + warningMessage = `Too many unread messages (${unreadCount}) since I've last seen this chat. I'm ignoring them. If you need me to respond, please message me again.`; + } - if (IGNORE_MESSAGES_WARNING) { - await sock.sendMessage(chatJid, { text: BOT_PREFIX + warningMessage }); - } + if (IGNORE_MESSAGES_WARNING) { + await sock.sendMessage(chatJid, { text: BOT_PREFIX + warningMessage }); + } - return true; + return true; + } catch (error) { + console.error("Error in shouldIgnoreUnread:", error); + return true; + } } return false; From 57343e5a93f8829a5a01ada48b71addacef15330 Mon Sep 17 00:00:00 2001 From: Luis Otavio Date: Thu, 23 Jan 2025 06:46:10 -0300 Subject: [PATCH 67/75] Fixed analytics lint --- packages/analytics/package.json | 2 +- packages/analytics/src/client.tsx | 5 ++--- packages/analytics/src/server.ts | 7 +++---- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/packages/analytics/package.json b/packages/analytics/package.json index 284b91a..aa42abf 100644 --- a/packages/analytics/package.json +++ b/packages/analytics/package.json @@ -7,7 +7,7 @@ "scripts": { "clean": "rm -rf .turbo node_modules", "lint": "biome check .", - "format": "biome --write .", + "format": "biome format --write .", "typecheck": "tsc-files --noEmit" }, "dependencies": { diff --git a/packages/analytics/src/client.tsx b/packages/analytics/src/client.tsx index 8700400..fc78e84 100644 --- a/packages/analytics/src/client.tsx +++ b/packages/analytics/src/client.tsx @@ -1,3 +1,4 @@ +import { config } from "@eda/config"; import { logger } from "@eda/logger"; import { OpenPanelComponent, @@ -9,7 +10,7 @@ const isProd = process?.env.NODE_ENV === "production"; const Provider = () => ( { if (!isProd) { logger.info("Track", options); - return; } const { event, ...rest } = options; - openTrack(event, rest); }; diff --git a/packages/analytics/src/server.ts b/packages/analytics/src/server.ts index 8e84a76..8eb3f98 100644 --- a/packages/analytics/src/server.ts +++ b/packages/analytics/src/server.ts @@ -1,3 +1,4 @@ +import { config } from "@eda/config"; import { logger } from "@eda/logger"; import { OpenPanel, type PostEventPayload } from "@openpanel/nextjs"; import { waitUntil } from "@vercel/functions"; @@ -11,8 +12,8 @@ export const setupAnalytics = async (options?: Props) => { const { userId, fullName } = options ?? {}; const client = new OpenPanel({ - clientId: process.env.NEXT_PUBLIC_OPENPANEL_CLIENT_ID!, - clientSecret: process.env.OPENPANEL_SECRET_KEY!, + clientId: config.api_keys.openpanel.client_id, + clientSecret: config.api_keys.openpanel.secret, }); if (userId && fullName) { @@ -31,12 +32,10 @@ export const setupAnalytics = async (options?: Props) => { track: (options: { event: string } & PostEventPayload["properties"]) => { if (process.env.NODE_ENV !== "production") { logger.info("Track", options); - return; } const { event, ...rest } = options; - waitUntil(client.track(event, rest)); }, }; From 313f459fe94203df76255297822f000ced8bd0aa Mon Sep 17 00:00:00 2001 From: Luis Otavio Date: Thu, 23 Jan 2025 06:52:48 -0300 Subject: [PATCH 68/75] Lint supabase --- packages/supabase/src/clients/client.ts | 5 +++-- packages/supabase/src/clients/middleware.ts | 6 +++--- packages/supabase/src/clients/server.ts | 5 +++-- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/supabase/src/clients/client.ts b/packages/supabase/src/clients/client.ts index 8875eb0..1fada05 100644 --- a/packages/supabase/src/clients/client.ts +++ b/packages/supabase/src/clients/client.ts @@ -1,8 +1,9 @@ +import { config } from "@eda/config"; import { createBrowserClient } from "@supabase/ssr"; import type { Database } from "../types"; export const createClient = () => createBrowserClient( - process.env.NEXT_PUBLIC_SUPABASE_URL!, - process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, + config.databases.supabase.url, + config.api_keys.supabase.anon_key, ); diff --git a/packages/supabase/src/clients/middleware.ts b/packages/supabase/src/clients/middleware.ts index b01a3f0..ffd6897 100644 --- a/packages/supabase/src/clients/middleware.ts +++ b/packages/supabase/src/clients/middleware.ts @@ -1,3 +1,4 @@ +import { config } from "@eda/config"; import { createServerClient } from "@supabase/ssr"; import type { NextRequest, NextResponse } from "next/server"; @@ -6,8 +7,8 @@ export const updateSession = async ( response: NextResponse, ) => { const supabase = createServerClient( - process.env.NEXT_PUBLIC_SUPABASE_URL!, - process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, + config.databases.supabase.url, + config.api_keys.supabase.anon_key, { cookies: { getAll() { @@ -26,7 +27,6 @@ export const updateSession = async ( }, ); - // This is to ensure the session is updated const { data: { user }, } = await supabase.auth.getUser(); diff --git a/packages/supabase/src/clients/server.ts b/packages/supabase/src/clients/server.ts index b070e8c..4d3262e 100644 --- a/packages/supabase/src/clients/server.ts +++ b/packages/supabase/src/clients/server.ts @@ -1,3 +1,4 @@ +import { config } from "@eda/config"; import { createServerClient } from "@supabase/ssr"; import { cookies } from "next/headers"; import type { Database } from "../types"; @@ -6,8 +7,8 @@ export const createClient = () => { const cookieStore = cookies(); return createServerClient( - process.env.NEXT_PUBLIC_SUPABASE_URL!, - process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, + config.databases.supabase.url, + config.api_keys.supabase.service_key, { cookies: { getAll() { From 2a42d19bae8b062b5dd813cb390f9801abb37dc2 Mon Sep 17 00:00:00 2001 From: Luis Otavio Date: Thu, 23 Jan 2025 07:35:49 -0300 Subject: [PATCH 69/75] Build and lint errors --- apps/dashboard/next.config.mjs | 11 +++++++++++ biome.json | 5 ++++- packages/config/python/pyproject.toml | 2 +- packages/config/typescript/package.json | 2 +- packages/config/typescript/src/config.ts | 4 ++-- 5 files changed, 19 insertions(+), 5 deletions(-) diff --git a/apps/dashboard/next.config.mjs b/apps/dashboard/next.config.mjs index d586cbb..08615df 100644 --- a/apps/dashboard/next.config.mjs +++ b/apps/dashboard/next.config.mjs @@ -8,6 +8,17 @@ const nextConfig = { experimental: { instrumentationHook: process.env.NODE_ENV === "production", }, + webpack: (config, { isServer }) => { + // Handle node: protocol imports + if (!isServer) { + config.resolve.fallback = { + ...config.resolve.fallback, + fs: false, + path: false, + }; + } + return config; + }, }; export default withSentryConfig(nextConfig, { diff --git a/biome.json b/biome.json index 0c6f157..c5e2d28 100644 --- a/biome.json +++ b/biome.json @@ -23,7 +23,10 @@ ], "enabled": true, "rules": { - "recommended": true + "recommended": true, + "style": { + "useNodejsImportProtocol": "off" + } } }, "formatter": { diff --git a/packages/config/python/pyproject.toml b/packages/config/python/pyproject.toml index a0842e9..dd6febf 100644 --- a/packages/config/python/pyproject.toml +++ b/packages/config/python/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "eda-config" -version = "0.1.19" +version = "0.1.23" description = "Configuration management for EDA" requires-python = ">=3.11" dependencies = [ diff --git a/packages/config/typescript/package.json b/packages/config/typescript/package.json index face18a..2854def 100644 --- a/packages/config/typescript/package.json +++ b/packages/config/typescript/package.json @@ -1,6 +1,6 @@ { "name": "@eda/config", - "version": "0.1.14", + "version": "0.1.18", "main": "dist/config.js", "types": "dist/config.d.ts", "type": "module", diff --git a/packages/config/typescript/src/config.ts b/packages/config/typescript/src/config.ts index 167345f..3203ff0 100644 --- a/packages/config/typescript/src/config.ts +++ b/packages/config/typescript/src/config.ts @@ -1,5 +1,5 @@ -import { readFileSync } from "node:fs"; -import { join } from "node:path"; +import { readFileSync } from "fs"; +import { join } from "path"; import { parse } from "yaml"; import { z } from "zod"; import { type Config, ConfigSchema } from "./types"; From 5949a6990c97a6c741694fdbbac9140016d6c123 Mon Sep 17 00:00:00 2001 From: Luis Otavio Date: Thu, 23 Jan 2025 07:51:23 -0300 Subject: [PATCH 70/75] Enhance service readiness checks in CI workflow and add get-port script --- .github/workflows/stacks.yml | 70 ++++++++++++++++++++++++++++++++---- tooling/scripts/get-port.ts | 26 ++++++++++++++ 2 files changed, 89 insertions(+), 7 deletions(-) create mode 100644 tooling/scripts/get-port.ts diff --git a/.github/workflows/stacks.yml b/.github/workflows/stacks.yml index e0d64ef..123565f 100644 --- a/.github/workflows/stacks.yml +++ b/.github/workflows/stacks.yml @@ -61,12 +61,44 @@ jobs: done - name: Wait for services to be ready - run: sleep 50 + run: | + while IFS= read -r dir; do + service_name=$(basename "$dir" | sed 's/-stack//') + echo "Waiting for $service_name..." + + # Get port from config + port=$(bun run -b --bun ./tooling/scripts/get-port.ts ${service_name}) + + if [ -z "$port" ]; then + echo "No port configured for $service_name, skipping..." + continue + fi + + # Wait for container health check + timeout 120 bash -c "until docker ps --filter name=${service_name} --format '{{.Status}}' | grep -q 'healthy'; do + echo 'Waiting for ${service_name} health check...' + sleep 5 + done" || { + echo "Service $service_name failed to become healthy" + docker logs ${service_name} + exit 1 + } + + # Wait for port to be available + timeout 60 bash -c "until nc -z localhost $port; do + echo 'Waiting for port $port...' + sleep 2 + done" || { + echo "Service $service_name failed to expose port $port" + docker logs ${service_name} + exit 1 + } + + done < $GITHUB_WORKSPACE/deploy_dirs.txt - name: Test endpoints run: | while IFS= read -r dir; do - # Extract service name by removing "deploy/" and "-stack" service_name=$(basename "$dir" | sed 's/-stack//') echo "Testing endpoints for service: $service_name" @@ -76,19 +108,43 @@ jobs: continue fi - # Use the built config module through node - port=$(node -e "import('@eda/config').then(c => console.log(c.config.ports['${service_name}']))") + # Get port using bun script + port=$(bun run -b --bun ./tooling/scripts/get-port.ts ${service_name}) if [ -z "$port" ]; then - echo "No port found for $service_name in config, using default 3000" - port=3000 + echo "No port found for $service_name in config, skipping..." + continue fi test_url="http://localhost:${port}/" echo "Testing URL: $test_url" - curl --fail --retry 3 --retry-delay 5 "$test_url" || { + curl --fail --retry 5 --retry-delay 10 --retry-connrefused "$test_url" || { echo "Failed to reach $test_url for $service_name" + docker logs ${service_name} || true + exit 1 + } + done < $GITHUB_WORKSPACE/deploy_dirs.txt + + - name: Check service health + run: | + while IFS= read -r dir; do + service_name=$(basename "$dir" | sed 's/-stack//') + echo "Checking health for $service_name..." + + # Get port from config + port=$(bun run -b --bun ./tooling/scripts/get-port.ts ${service_name}) + + # Skip if no port found + if [ -z "$port" ]; then + echo "No port configured for $service_name, skipping..." + continue + fi + + # Wait for port to be available + timeout 60 bash -c "until nc -z localhost $port; do sleep 2; done" || { + echo "Service $service_name not ready on port $port" + docker logs ${service_name} || true exit 1 } done < $GITHUB_WORKSPACE/deploy_dirs.txt diff --git a/tooling/scripts/get-port.ts b/tooling/scripts/get-port.ts new file mode 100644 index 0000000..3e8e567 --- /dev/null +++ b/tooling/scripts/get-port.ts @@ -0,0 +1,26 @@ +//@ts-ignore +import { config } from "@eda/config"; + +const serviceName = process.argv[2]; + +if (!serviceName) { + console.error("Service name required"); + process.exit(1); +} + +// Get port from config +const port = config.ports[serviceName as keyof typeof config.ports]; + +if (port) { + console.log(port); + process.exit(0); +} + +// Handle nested ports in db section +const dbPort = config.ports.db[serviceName as keyof typeof config.ports.db]; +if (dbPort) { + console.log(dbPort); + process.exit(0); +} + +process.exit(1); From b56cf03c84d3f4b1930fd16e8ef922c5030554ac Mon Sep 17 00:00:00 2001 From: Luis Otavio Date: Thu, 23 Jan 2025 08:53:03 -0300 Subject: [PATCH 71/75] Fixed langtrace configuration --- deploy/langtrace-stack/docker-compose.yml | 2 +- deploy/langtrace-stack/export-config.ts | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/deploy/langtrace-stack/docker-compose.yml b/deploy/langtrace-stack/docker-compose.yml index 15e3e0b..d3852a4 100644 --- a/deploy/langtrace-stack/docker-compose.yml +++ b/deploy/langtrace-stack/docker-compose.yml @@ -26,7 +26,7 @@ services: env_file: - .env ports: - - "${POSTGRES_PORT}:5432" + - "${POSTGRES_PORT}" volumes: - postgres-data:/var/lib/postgresql/data diff --git a/deploy/langtrace-stack/export-config.ts b/deploy/langtrace-stack/export-config.ts index 94d4259..20919bd 100644 --- a/deploy/langtrace-stack/export-config.ts +++ b/deploy/langtrace-stack/export-config.ts @@ -6,25 +6,25 @@ const envVars = { PORT: config.ports.langtrace, // Postgres settings - POSTGRES_HOST: config.databases.langtrace_postgres.host, + POSTGRES_HOST: `${config.databases.langtrace_postgres.host}:5432`, // Add port to host POSTGRES_USER: config.databases.langtrace_postgres.user, POSTGRES_PASSWORD: config.databases.langtrace_postgres.password, POSTGRES_DATABASE: config.databases.langtrace_postgres.database, POSTGRES_DB: config.databases.langtrace_postgres.database, - POSTGRES_URL: `postgres://${config.databases.langtrace_postgres.user}:${config.databases.langtrace_postgres.password}@${config.databases.langtrace_postgres.host}/${config.databases.langtrace_postgres.database}`, - POSTGRES_PRISMA_URL: `postgres://${config.databases.langtrace_postgres.user}:${config.databases.langtrace_postgres.password}@${config.databases.langtrace_postgres.host}/${config.databases.langtrace_postgres.database}?pgbouncer=true&connect_timeout=15`, - POSTGRES_URL_NO_SSL: `postgres://${config.databases.langtrace_postgres.user}:${config.databases.langtrace_postgres.password}@${config.databases.langtrace_postgres.host}/${config.databases.langtrace_postgres.database}`, - POSTGRES_URL_NON_POOLING: `postgres://${config.databases.langtrace_postgres.user}:${config.databases.langtrace_postgres.password}@${config.databases.langtrace_postgres.host}/${config.databases.langtrace_postgres.database}`, + POSTGRES_URL: `postgres://${config.databases.langtrace_postgres.user}:${config.databases.langtrace_postgres.password}@${config.databases.langtrace_postgres.host}:5432/${config.databases.langtrace_postgres.database}`, + POSTGRES_PRISMA_URL: `postgres://${config.databases.langtrace_postgres.user}:${config.databases.langtrace_postgres.password}@${config.databases.langtrace_postgres.host}:5432/${config.databases.langtrace_postgres.database}?pgbouncer=true&connect_timeout=15`, + POSTGRES_URL_NO_SSL: `postgres://${config.databases.langtrace_postgres.user}:${config.databases.langtrace_postgres.password}@${config.databases.langtrace_postgres.host}:5432/${config.databases.langtrace_postgres.database}`, + POSTGRES_URL_NON_POOLING: `postgres://${config.databases.langtrace_postgres.user}:${config.databases.langtrace_postgres.password}@${config.databases.langtrace_postgres.host}:5432/${config.databases.langtrace_postgres.database}`, POSTGRES_IMAGE_TAG: "16", - POSTGRES_PORT: config.ports.db.postgres, + POSTGRES_PORT: config.ports.db.langtrace_postgres, // App settings - NEXT_PUBLIC_APP_NAME: config.services.langtrace.api.host, + NEXT_PUBLIC_APP_NAME: `http://localhost:${config.ports.langtrace}/api/trace`, NEXT_PUBLIC_ENVIRONMENT: "production", - NEXT_PUBLIC_HOST: "http://localhost:3000", + NEXT_PUBLIC_HOST: `http://localhost:${config.ports.langtrace}`, NEXTAUTH_SECRET: config.services.dashboard.auth.nextauth_secret, - NEXTAUTH_URL: "http://localhost:3000", - NEXTAUTH_URL_INTERNAL: "http://localhost:3000", + NEXTAUTH_URL: `http://localhost:${config.ports.langtrace}`, + NEXTAUTH_URL_INTERNAL: `http://localhost:${config.ports.langtrace}`, // Clickhouse settings CLICK_HOUSE_HOST: config.databases.langtrace_clickhouse.host, From 6d7648866c9648abe8477309da965c3b608710d7 Mon Sep 17 00:00:00 2001 From: Luis Otavio Date: Thu, 23 Jan 2025 08:57:13 -0300 Subject: [PATCH 72/75] Remove redundant service readiness checks from deployment workflow --- .github/workflows/stacks.yml | 39 ------------------------------------ 1 file changed, 39 deletions(-) diff --git a/.github/workflows/stacks.yml b/.github/workflows/stacks.yml index 123565f..c20908d 100644 --- a/.github/workflows/stacks.yml +++ b/.github/workflows/stacks.yml @@ -1,9 +1,6 @@ name: Deployment Stacks on: - push: - paths: - - 'deploy/**' pull_request: paths: - 'deploy/**' @@ -60,42 +57,6 @@ jobs: fi done - - name: Wait for services to be ready - run: | - while IFS= read -r dir; do - service_name=$(basename "$dir" | sed 's/-stack//') - echo "Waiting for $service_name..." - - # Get port from config - port=$(bun run -b --bun ./tooling/scripts/get-port.ts ${service_name}) - - if [ -z "$port" ]; then - echo "No port configured for $service_name, skipping..." - continue - fi - - # Wait for container health check - timeout 120 bash -c "until docker ps --filter name=${service_name} --format '{{.Status}}' | grep -q 'healthy'; do - echo 'Waiting for ${service_name} health check...' - sleep 5 - done" || { - echo "Service $service_name failed to become healthy" - docker logs ${service_name} - exit 1 - } - - # Wait for port to be available - timeout 60 bash -c "until nc -z localhost $port; do - echo 'Waiting for port $port...' - sleep 2 - done" || { - echo "Service $service_name failed to expose port $port" - docker logs ${service_name} - exit 1 - } - - done < $GITHUB_WORKSPACE/deploy_dirs.txt - - name: Test endpoints run: | while IFS= read -r dir; do From 9bdac0ca8a49e399fd16ada8d7a35010dd6c6fe6 Mon Sep 17 00:00:00 2001 From: Luis Otavio Date: Thu, 23 Jan 2025 09:05:30 -0300 Subject: [PATCH 73/75] Add special handling for Neo4j in get-port script --- tooling/scripts/get-port.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tooling/scripts/get-port.ts b/tooling/scripts/get-port.ts index 3e8e567..375a995 100644 --- a/tooling/scripts/get-port.ts +++ b/tooling/scripts/get-port.ts @@ -8,6 +8,12 @@ if (!serviceName) { process.exit(1); } +// Special handling for Neo4j +if (serviceName === "neo4j") { + console.log(config.ports.db.neo4j.http); + process.exit(0); +} + // Get port from config const port = config.ports[serviceName as keyof typeof config.ports]; From fc9288aa810bd41689ce6c89a481a7b700e834dc Mon Sep 17 00:00:00 2001 From: Luis Otavio Date: Thu, 23 Jan 2025 09:39:19 -0300 Subject: [PATCH 74/75] Enhance service endpoint testing with detailed logging and network checks --- .github/workflows/stacks.yml | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/.github/workflows/stacks.yml b/.github/workflows/stacks.yml index c20908d..5c6eeef 100644 --- a/.github/workflows/stacks.yml +++ b/.github/workflows/stacks.yml @@ -61,7 +61,15 @@ jobs: run: | while IFS= read -r dir; do service_name=$(basename "$dir" | sed 's/-stack//') - echo "Testing endpoints for service: $service_name" + echo "=== Testing endpoints for service: $service_name ===" + + # Show container status + echo "Docker container status:" + docker ps -a | grep $service_name || true + + # Show container logs + echo "Container logs:" + docker logs $service_name 2>&1 || true # Skip config packages if [[ "$service_name" == "config" ]]; then @@ -69,22 +77,36 @@ jobs: continue fi - # Get port using bun script + # Get and verify port port=$(bun run -b --bun ./tooling/scripts/get-port.ts ${service_name}) + echo "Retrieved port: $port" if [ -z "$port" ]; then echo "No port found for $service_name in config, skipping..." continue fi + # Network connectivity check + echo "Testing network connectivity:" + nc -zv localhost $port 2>&1 || true + test_url="http://localhost:${port}/" echo "Testing URL: $test_url" - curl --fail --retry 5 --retry-delay 10 --retry-connrefused "$test_url" || { + # More verbose curl + curl -v --fail --retry 5 --retry-delay 10 --retry-connrefused "$test_url" || { + echo "=== Error Details ===" echo "Failed to reach $test_url for $service_name" - docker logs ${service_name} || true + echo "Container status:" + docker ps -a | grep $service_name || true + echo "Latest logs:" + docker logs --tail 50 ${service_name} 2>&1 || true + echo "Network status:" + netstat -tulpn | grep $port || true exit 1 } + + echo "=== Test completed for $service_name ===" done < $GITHUB_WORKSPACE/deploy_dirs.txt - name: Check service health From 5abc0d6319664c210211a898ce1b72b2cac1fb3c Mon Sep 17 00:00:00 2001 From: Luis Otavio Date: Thu, 23 Jan 2025 09:48:02 -0300 Subject: [PATCH 75/75] Add health check for containers in deployment workflow --- .github/workflows/stacks.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.github/workflows/stacks.yml b/.github/workflows/stacks.yml index 5c6eeef..5415ed7 100644 --- a/.github/workflows/stacks.yml +++ b/.github/workflows/stacks.yml @@ -53,6 +53,19 @@ jobs: # Store directory echo "${dir%/}" >> $GITHUB_WORKSPACE/deploy_dirs.txt + # Wait for containers to be fully up + echo "Waiting for containers to initialize..." + sleep 15 + + # Check if all containers are healthy + docker compose ps --format "{{.Name}} {{.Status}}" | while read -r line; do + if [[ ! $line =~ "(healthy)" ]] && [[ ! $line =~ "Up" ]]; then + echo "Container not healthy: $line" + docker compose logs + exit 1 + fi + done + cd $GITHUB_WORKSPACE fi done