From cd597a361e74e05aeec1d10319fd875518719cc3 Mon Sep 17 00:00:00 2001 From: Timothy Carambat <rambat1010@gmail.com> Date: Tue, 23 Jul 2024 12:42:53 -0700 Subject: [PATCH] OBDC Support (#1933) * add possibility to connect to SQL Base by ODBC --------- Co-authored-by: suchaudn <nicolas.suchaud@legrand.fr> Co-authored-by: nicho2 <nicho2@laposte.net> --- .vscode/settings.json | 1 + .../SQLConnectorSelection/DBConnection.jsx | 2 + .../NewConnectionModal.jsx | 33 +++++++++- .../SQLConnectorSelection/icons/odbc.png | Bin 0 -> 19645 bytes server/package.json | 3 +- .../plugins/sql-agent/SQLConnectors/ODBC.js | 60 ++++++++++++++++++ .../plugins/sql-agent/SQLConnectors/index.js | 5 +- server/yarn.lock | 18 +++++- 8 files changed, 116 insertions(+), 6 deletions(-) create mode 100644 frontend/src/pages/Admin/Agents/SQLConnectorSelection/icons/odbc.png create mode 100644 server/utils/agents/aibitat/plugins/sql-agent/SQLConnectors/ODBC.js diff --git a/.vscode/settings.json b/.vscode/settings.json index 5e26e4778..60ff747fd 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -31,6 +31,7 @@ "Mintplex", "moderations", "numpages", + "odbc", "Ollama", "Oobabooga", "openai", diff --git a/frontend/src/pages/Admin/Agents/SQLConnectorSelection/DBConnection.jsx b/frontend/src/pages/Admin/Agents/SQLConnectorSelection/DBConnection.jsx index 9d7b35b0a..d7361baea 100644 --- a/frontend/src/pages/Admin/Agents/SQLConnectorSelection/DBConnection.jsx +++ b/frontend/src/pages/Admin/Agents/SQLConnectorSelection/DBConnection.jsx @@ -1,12 +1,14 @@ import PostgreSQLLogo from "./icons/postgresql.png"; import MySQLLogo from "./icons/mysql.png"; import MSSQLLogo from "./icons/mssql.png"; +import ODBCLogo from "./icons/odbc.png"; import { X } from "@phosphor-icons/react"; export const DB_LOGOS = { postgresql: PostgreSQLLogo, mysql: MySQLLogo, "sql-server": MSSQLLogo, + odbc: ODBCLogo, }; export default function DBConnection({ connection, onRemove, setHasChanges }) { diff --git a/frontend/src/pages/Admin/Agents/SQLConnectorSelection/NewConnectionModal.jsx b/frontend/src/pages/Admin/Agents/SQLConnectorSelection/NewConnectionModal.jsx index e5f4c3016..f6b1c21e3 100644 --- a/frontend/src/pages/Admin/Agents/SQLConnectorSelection/NewConnectionModal.jsx +++ b/frontend/src/pages/Admin/Agents/SQLConnectorSelection/NewConnectionModal.jsx @@ -11,6 +11,7 @@ function assembleConnectionString({ host = "", port = "", database = "", + driver = "", }) { if ([username, password, host, database].every((i) => !!i) === false) return `Please fill out all the fields above.`; @@ -21,6 +22,9 @@ function assembleConnectionString({ return `mysql://${username}:${password}@${host}:${port}/${database}`; case "sql-server": return `mssql://${username}:${password}@${host}:${port}/${database}`; + case "odbc": + if (!driver) return `Please fill out the driver field.`; + return `Driver={${driver}};Server=${host};Port=${port};Database=${database};UID=${username};PWD=${password}`; default: return null; } @@ -33,6 +37,7 @@ const DEFAULT_CONFIG = { host: null, port: null, database: null, + driver: null, }; export default function NewSQLConnection({ isOpen, closeModal, onSubmit }) { @@ -48,12 +53,14 @@ export default function NewSQLConnection({ isOpen, closeModal, onSubmit }) { function onFormChange() { const form = new FormData(document.getElementById("sql-connection-form")); + setConfig({ username: form.get("username").trim(), password: form.get("password"), host: form.get("host").trim(), port: form.get("port").trim(), database: form.get("database").trim(), + driver: form.get("driver")?.trim(), }); } @@ -74,7 +81,7 @@ export default function NewSQLConnection({ isOpen, closeModal, onSubmit }) { // to the parent container form so we don't have nested forms. return createPortal( <ModalWrapper isOpen={isOpen}> - <div className="relative w-full md:w-1/3 max-w-2xl max-h-full md:mt-8"> + <div className="relative w-full md:w-fit max-w-2xl max-h-full md:mt-8"> <div className="relative bg-main-gradient rounded-xl shadow-[0_4px_14px_rgba(0,0,0,0.25)] max-h-[85vh] overflow-y-scroll no-scroll"> <div className="flex items-start justify-between p-4 border-b rounded-t border-gray-500/50"> <h3 className="text-xl font-semibold text-white"> @@ -114,7 +121,7 @@ export default function NewSQLConnection({ isOpen, closeModal, onSubmit }) { <label className="text-white text-sm font-semibold block my-4"> Select your SQL engine </label> - <div className="grid md:grid-cols-4 gap-4 grid-cols-2"> + <div className="flex flex-wrap gap-x-4 gap-y-4"> <DBEngine provider="postgresql" active={engine === "postgresql"} @@ -130,6 +137,11 @@ export default function NewSQLConnection({ isOpen, closeModal, onSubmit }) { active={engine === "sql-server"} onClick={() => setEngine("sql-server")} /> + <DBEngine + provider="odbc" + active={engine === "odbc"} + onClick={() => setEngine("odbc")} + /> </div> </div> @@ -224,6 +236,23 @@ export default function NewSQLConnection({ isOpen, closeModal, onSubmit }) { spellCheck={false} /> </div> + + {engine === "odbc" && ( + <div className="flex flex-col"> + <label className="text-white text-sm font-semibold block mb-3"> + Driver + </label> + <input + type="text" + name="driver" + className="border-none bg-zinc-900 text-white placeholder:text-white/20 text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5" + placeholder="the driver to use eg: MongoDB ODBC 1.2.0 ANSI Driver" + required={true} + autoComplete="off" + spellCheck={false} + /> + </div> + )} <p className="text-white/40 text-sm"> {assembleConnectionString({ engine, ...config })} </p> diff --git a/frontend/src/pages/Admin/Agents/SQLConnectorSelection/icons/odbc.png b/frontend/src/pages/Admin/Agents/SQLConnectorSelection/icons/odbc.png new file mode 100644 index 0000000000000000000000000000000000000000..c287558874843bd0f534117b2cb6dea95904cb1e GIT binary patch literal 19645 zcmdtKcR1H?|36HG%m^V{J3C~{2rVlKAv2q|kr_f(G8-C7QK=*=BxTQtR6<5(D6@gg z`|+yldws9#`rh|_+{f?t`|JK3KF9UB-d^w5I?wa@e5_N9o{km+Ef*~r85zStZ4Co5 zGV)a7Um7ZWGO5ZghyU3?(l+rTBcrw^{w0?UxZX`hM)}pz(AeAf=n;7vcUMs>TX$<a zQ9oA_jwT~h^pnSby4rbL@%y>DxOvI@Ded@kggpM6_*iU*GA;i<{!#R_JuPpbq4|$P z@&A-|ICy)L<i*5%eSJlJ_lmlE+KWlZ$;pX{ONvQKir@$lFMl_0D?br8FCm=i-%q7s z=VjyRNb+`ccjG5cZDs9###?E}j(;BY_m*sJ{&^YF8Bdo#7qPVwvvaX?wR7|K5|a>> z__w>XwUPIB^meiPj~iEY@&1obTpWofk$16jvsc>TCt_=N+Uksp_YP%U4K;Os5q?QU zG2*UV9RKBOe?Iy5Gl~7*T<_0e|MGbMi%+iV?d|Dk{m-X&^AffC^T`?Fm5E|B#jI?J z=NJFC=l}aHc~x7lf4OnuNsifh{KqGF1H>;#R-RsVhW;cwr5y%#UhZc+ZS4Ma*#B`t zbx%7hZ#!FMNpVRj5%GN@l6w`!{^M8w967~*{NjM0_d#1_8A-W)lKZUeME35LvlEfB zl9CaTm9UZ!k&=<Hww0B(!<3Qw=ehoV<$s(>(;6p|mXwy1k(QH?lGrCFEi3-d@BI1Y z|M-fb`xzSt;<1!5l>fM)|NiX15C4A{=6`=(|8;_Yf35$A2AcFQONkieKWmg2U;F~& zyZ7J57oW+ip7C~Y_f$4?bmdpK^0acbw(?da4*suS{Kv!n?@vWkiGQ1i|GYy#E7Jd% zfjC(H&unq`G<0`&QC7Ed^ReRJX<+AOYv*a_xl2*(Kaco-yWjsRKmTzI3emsJX0d-3 zGUc6_%BYK4{s%Qw4gFFldIJqF+YM6Bh^=GXToQAKa_|21J8txhnl~;OpMEf++WYkB z&GwJC^Ls@Uu4GPlcORO)b2ad1C$DR^j;pDo!)XhawhuO*#}DOtbMPP9Z+k8@H1RXv z>a})_2>U+&w34$bqs`3reNFQdMyL9GZ_oR`e1U)VYiJBxuGmuZ3ktSYR0#+Q9?vC3 zM{BbQa|;R@tfz~LjyA0P|I5b_2@_-*@gF{Xpp30u7h4;4^(wouF#EHjqECGV#=^O# z*<)j4&xhMTe%yXmhmVhMVR6x7?wx}a4Gqon2{sN6zs6RLsS6S6qO~qE8AV0x3=9n7 zH{TSUICgAP^Xa`eZr*I`?4&8^y7=*<QI|&qJ%>1%s;Vl@h7FbH$SF2*bDwS8vvn)w z-Me?AFI))I%t+!%dD!u12yCPHUl<1kDec~~NB{Woh#NP!K79Q6$-*e>xQ}8c3kOGZ zbTo~Irl$2we`WN=i|389B6Kp<_4E=a{4`Q52NI=5TW*Cn+N#;v-JfiI{CIcDD@hL- zT=}h2VRUS)y0^EKw>%Gi)~#D-t0lx#TO|;Mr+xVF;eI>2k!>UIK781!a6^krmc;KZ ze{1rPc2>JuR(pEL+G>7A(oLIzL~D5k1$}dKhD(j-FI=F#c4Rw#KD4(t#%p%G%{iq) z&nZbMe~U3MFRw^NNJwa4WMrEHZ^;IB#;*HWS#)e{Y&&=DQuXkV2woVV_V)H}h(AC} zLr-7Bw>m>T_AB7ty?dB>YCc;%J%qWxUTSI4NmcY`v9q(2^%$gl{ra_^bF=#Ss3@D7 zXlCt0htv!VSnwn{!4GwfWxbEH$JQDN@6buDNKjdgo}2UO%4^s_Nkt_nA<<^Ef5V0i zGrxXa;fYP<po<cH|LM~OoBH^>xw#u7A|f8<<fQP#nC-{Ap{Jt2kLE=Va(8$4uB3*> zToMhL-pP}(%gbknM@KWBJ-ht5I?;T2Wkn+3r^CGm53GNVwVmA_O_obiG&*r&Gwv}l zEiEBKD<S>y<3o6kV-^+`95OB(2FH#`oNneA^Oo;Q(q>_gk8*T%t*s2FPD)9M;tO6N zUoKDkFg!Fw5ng%j;HB-Amf{ulQ3cNaIFfqo>sPgd2RC?nd3g*sFb9w8>Im@jlMU4; zXlZIvMKbY;Dlk4e7co@l<m5E-X5VMa&t*Xl4i1StQDYaZt*yoVzuO+UA+<q3Kp=l8 zF+7*lqv!80C*$6C5s%a-DU>q!?OW{6pWe2oPg^%6630{-%eoCN|NbHCIZRXJ)_dXm zch4@5=!wCRkq8N!y8fr7Q&rK-Lbmk+5xFEYGkg0T-@kv)wQFKuUjEUVKeTa|sDJ=@ z!Ra?uar^ojt}~c%)72j^$u^qkd$r|cvD--a^<n1pjEtn~*JE(ipWixkeYjaFsyrl& zve>$d-RnPQpOlN@^uH&d9%<Q<DtArRL(qONNkW)=b7i`~3)a~BU$|-o-rVf!()5py zocc_IgM%}jHVIapxmNiZZ>4idsm5o{NcRQJo1a}<na|JIC?_o@CU!3~)28pG|JnHZ zm^@Odo|YE1j9X81_Q~S(f`Vf{+=4008#kU)SsB;8CTT~j^-NBHJ^s=zQPH{=XTIqj zJ0_$+o7BW5@4fT3-{1xv)~~k4vO4LH9&Py6ky9T^L2aug;7Qll*LO|MYx`8$lA9>~ z=vEwY2lLDM<CB52_8N>EH*bzwPsh$LDyrkdnowVr^zi=u^=rRdLlSS^+=H{9{4qCI z$G5hi)qF!bR{h~N>j7;86O$OWJw|u(^9_7h1rw4kUp}9CJAigzU?4xkZ!3E|rGoEN zO(esX=b{Wrl%b)aev9LocRRk>Y0b(CbF<x23E`bCUsEny{(iViKU(odO3G&5voosA z*Cf<&2US)2G3`zVe0*eR>FDgV1k%^`+0@Z1F7$_=J*~A3-@o?e^vmC0ZZ3>xrs46@ zKXdML<6fOBF83IFMk`xi9A5bB8ULO=v3Bai_ZWHP&tKVd!qbR}ZHBf!)YQ~;=JlSO z3J0mz@oE|!iC3>iM$m2c?0iNsOIxps`jL0`x93%cq*!8xVZ3)JM4q%6kZmAdwSj@4 z0u}SG%BSX~xn83>T@pR)boJ!KGfaFcJnTa+$dVhp62QvF))+r<)mWBR0fj+%p`W5l zB3fGybJ6IDap;Q|@?H8pjC5M6zkdBPE^^`2D&1HjiwbYobhXl{@OAv45xyBPddt^R z=B8aezu@{*`7^Y9K|hlwINTy4G7@`w%tY589X{<d(HVLyQwNul!518yoT`?l%f)bi zyC$$cj7xn)TGCX+6fT^!9N5|aURUM7v16AeI5dXIGIegLSy<f0=|UrHGVAIz8AG>w zdZh&~?vj_6x2-4n3v(+3eQ(^cOr6y5W@YhP&RKp#a(rf-ZDirJ$v;{taPHhO-A_>w zHc>%Ka~`S6Nl_1<JQ>zDRLCPyo;h=7AS<UVnI}Bju%_E-&T4?h$<_71nKOmB_Pz8= zJ9J;i_rEii4a30ttE4nkqknwgd{iNqM9HHPZ018Ln0)8iGeaNBl-eq0Lw$V#g>}pa zD-N*L;@sY6&Q#h;HpZ)Ju;h|z{(j&gE0q)_@Ac*41dV8HgwVNfy&L^STZ9GUBZT^k zKL=mp360)rH4tqxu#x$fD%KTW%*L83=Ki4}A>za+FLYRbT@q$2bXxn%gO}6d8jh`} zq-==q&tEvDLylFm92MW7en3+bx1EpMMs={$;uqY2b#1FfG7;uhot>S{7uuLaDa?(w zpc?SXf_MhY4-fV8g`Nwu$KTP<(Db-Rn-moj9o;|aO+4s&Dyr{ATuD(S1Cd*B5=Xq- zo`7%J#91!9d;i|<??*&k<cib8tTIqNa)e$po_m)<<YAkEPSuPy%kRUD`8FGqXx++J ze1hGbcd*Cb`qF%z;?${Am}?E8f<i+6bmqjMMMOpp7InK_!gz3xO_ff65xYu@31K-m z(M3Z`OAHe?Ry`vpCuhFR^Y!fU>(tcLW=2)kOo;0mmj|!R)NImi_%p}WVLpX)1>(@N z>#jGvIdW4byMA=?Id41%d;GaQA`&aZmvjZUZq*o((io<}Te*e`U^iX->>26V4SwPf zww61psPO}8eu05msnOAwSy9zS(?WPxmgdVR_Quv$?cr7x<|Z#+ncD)061eO#+>mrl zC8X>{XtcI|tKtv(s_N>%<tGmwM4pfh!(d$09AZ<sDdWl&b!bz`LoL1_*2c!h6U#jg zQr~H{HqvRmZKtIs<MkhA-nwnuqfL>~tRCI2#a3TGJUY?)<>&bO-S<6IFl6-!eC1z9 zG1SJ-9GEvd3wko6`_|edDEYjZJap)g-{GUgsS0VSpXZ*h+>`S%tfoeTjfFYs;j?E_ zioZS_>*#55kg9T#$x3Wc!lX3G{%DcXGFThy9~jqg(9YJjzso*tvVH6wS{m6|MlDvl zdd9%HiSc%y;NW26mwpm;aa%_FH8ehK-g~-v?ER5-r9PAE$odR6)l}tGqkO*w2+VSo z=3=-Vyy%GfSY7!jddzh3*SDzOKSpg<mw&$!O$ZBH_o1UhLH2qe=7(c_JkP7Xk4;T< zKSo>Yg%T92uG`0K-2fnyejs*nx#^USPW+@jE>K;S_kQ!Kv*fY0?=piP9K9KN|8NTM zl>AOM=3iTP?b^`Q)y1I{;GH(75WFmV=<s3JkWmq7G&==!OqafwC9O-2g{H#hFD}2o zz3I^~CRg3JobupWPRgfWs+m+?4o|<VVy>>zued65l7!i*JUxnTjy7Xi8Ma=jrAtF& zjt+<u--!>LD$#OS!!#JmObdQ7tYk3|Z8fmd_Se_;s-@F-gW1J>VLF+|#F?4Le+Agu z+taCBwahb~=?k3RmHOH%kHl(VYI*@*r<=}sKb~vb$U^j~f}@+db^(keC@=1`I6Hfw z^eVmo^sD&*?wHpnoveu>JKyUIv?%hjJSRtt*RP-4*3Qlf=#6%}(06V}M@Qol&utHM zZaH;i=b-2gfB7=E!0G-dX7;P~@p%(3uf4nhV|RH~VlP}!Gcp3eSe{VUb#oI{{yn@2 zn6oQLSkSlQmE{eKoZ=<p9lEz`=)_)<YFqcY^)$R=?iW%xSNiDDBUQAxKlhe!B`GO% z!=~;1cpY_E6D@M7(E%IT*dhX_Uv1v^`D}sUHqzXL3GQ%L;>F;Eh#|5A2M(YgGKstQ zDV8RT3=M^I?mgZA@uYigJdZ-+rAzO>bumQunAqBGoq9EI0f4c`ry01D=$f*=Q$5Fv zhuY(?aKnd(hxuh>jPgP_@dSwgK927n<oPvZFz_mJcztQEym#=@+nlugQ|e-;%D4fM zFYsPkjfxoBpJkLCmX^l%?%g|9g)g<W1?Yz^pUZCjP)O2xKK0_8c;K81Dp;dH{i#x) zY*!fvPkKhKjif?V3MGFa0goSV_9<*VjGj0*{W(G8WKpNIT6}{E1CKl_(3zU4>5XbP zyKK`!T7Q54SADm`scL3uwbt)3da_?jD@r|hFYt&a0H&m*Bx&refJ=7~!^p_Ug|sMb zSDU(vmFB*^5lKQ?@;?>v4FZnw1Telh7m<-1vhwZJ!>p|8R5>qhpLma<I;)=N9y`Uv z>WL}(b4*|Pj;3gBN5|&z@$rV{=9)wSb?yLB^kyuKL3G#YA7w9JFjt>+TDKT*c+$GK z$CHjXWZ5HP(=XH{s=)cF+OnV}VwjG%9Na3neS4kHeYe(TvRB7Xol5Ai2xL%MTP}FJ zih^Yzn`Cb}K$DwEI!AZxo|xJ5jaF7xv=3h7W5o_F%no&y`ijMK%Y|XUj<+24mZqSh zuQ<P9QzGWbUG=EPK7E6O)-PA)x_#ol+Y(EKCbt-8irv4mG940CX{opK_%x{E;M=?E zoyG1z@B`F+o<hfeFDz78idzoW#AqKqdOS#=ieZb4Wp|OQ4_{P-P-KMA{+Oz9nV?1Q z+W7Vp04Ky(8F>{O?3o(wf^-nG({g!X7L3SnW!b(ZwKCtX$x@1oO^#RVQ4H=uFCX{d zwO?tz$BRSCA<?>iBJ0y9QxFp&6&1ew_wR$TCSHv&ODiu|F)%Plym5mTeB#a>en%7H zQIBUC-ObKEnrDO3O;4wF?p*|h*)^RYl!^<=TX(;9DolU)kOp&pr?BvF`27!i+S=O0 zIq0<y9#r-8%%6~TaCff<+9Y6EZNi&9+$dAz@jFx9h*FMX^yJgIus(6FxScz9?)UJ> zd%MxVQ<p1_Dnm1_YOF0I>Bfx<Ag*0WE*D~B^L!>v0ZQbD7ygtl=Kh8%a#6F%=35Gz z0k_U{%4rl97T&#guf}Kn@WYV1jdJ7O@_*T(CEC@@tjANW-#2dDAdn!pEUDt|DBuU) z4#|7K`~A50^XalB(ooz!`DL?okZ6y=nhlq)T)F$?34QtU_t**Tdv`iJjkj*!4r0iR zyHoS?dp@D9ly92d`QpqTPfyRcpFVw^`7wL-2G%ZFA#LVK(9AC_xAwdgExiF||GKGZ zb|^t*5Zz7u`Vq7JEeA9X91xI^VMjkEm>vrYOaJt=n8HWt$<92AE&JRI&&+wV6CIqa zkS_D4MNU12FMOanDl6D65L2kiRuYiw?&HU_ni<U7FKu}9=1qQv|GnH?@iFfghb=lz zg}j<D4w`E}amuRcs))hqwtjSomNOJrpFV#6oTIk3R$E7>?$vzXH_6(e>1p>;5wAS2 z(H7#A`T6hW?2jM6vb$JAQB_ZG<1<qcV5*HgJg$3E&YdIcYMaOpTJmw}%3lxdB0ocq zFOOuV2d0UREbjDSXY*~P?p=|0F3>PA9B^<Lv-|z++c(#dW5<q#17Kr$Q(=&xk|e*A z&#b6`aHIk1zD_-oUKIrqmx6ro!T!|ckD%H)<*Qs+1nM?6856Qwqa%i@(CYNjPf&?@ z398tcXJe;jpyB5y3;2<gob1_Uf4YDZ=YN=;eZbuO*5v5aiNl9u&)39nVqvk~b6GW< z3fk1VaB8Nyickt>Hn#O~nnDWc>et_#cIkb=22^yu);ftNYNBSEMA@*&IBJ@no=yVb z2Wq206HDRIy0N$zASEM1khr5qk3zX|aC3`ncPf1Ksk=LHxnya2jx_~6ONxsDKa2Cz zOmUhNDk>`7OTE)))+VN>6UZs3_+@1|Dl032^3+JA++#|y_lwd(RypA#G&D7Ny*3eB z;Q2G|_9CfnkYKLKpV|FDH!Z%u-x7p60Mo;!v&eM|EiJ9BR>^Kmkv;=2b==ItL_zBe z5RKJDXWlhM|NO572Ah5v+1LopP4&hS+~m^K7h~CVW{%4%m_{B8Ggjv#BL$U|cmVx1 zj~ux;VXPs0c0nBYGdw+AuuESXbdJE;`9eRtO1wtVxx!OZxohg{h47tCJ9G@~&KyX0 z14gS~U0EVdm%>BYKSd$U9TOo`!A3V~ovE8vH~u~|&#IaV3Qhlmn{HJ{_w^1NRXa5L zV`&XweWQ}4om5m9`r0bd2=sqvZg)id2c-?8p9-BN&MuI;7q@wMctApmb#iuAx3Utz zL(la3elF?N`}Ov%UKg_&JDpadP9~JAWFB>WbQ+>%)boX0oS7kA<W=IL3MU8fd>Oa@ z#BqpX0AsA|?1L!d`9rdXE-p|0QWJ<(FU$wbo|KjGmikWDP4$+NlkMvv5t7c&0HtT9 z*~8vjZ-Vqfe?kY3M4f2KU>~<3xC_mpBS&h$X7(sVIuXA^Q6oR+%6{(hWmZ(4lxzCe zCVUrvI<juwoRORRHe>VP(n{*N4}oWAj_E;cxE=6ySC>SpnweRut7i+qD+IdN@fto( zD4C6ycDR1yzNP4I&_2F)>-hftWHK(D{0fmxgKY74#>O1zxn=cUe;8}21HyHX8pM*I zBBP|Hwt<`x9T!*lMz6V>)RI+F(pBV>)My*l=1}wKSf*{g=b=A5D5VY%mgrlTE(s|_ zzL&W9rlGY}D>!&{+jKn!7!A^BKlC_eX69sr0N$+j3x7pB!xJZ}Q6bXP!wSUaEAbu) zM)dOj>8Mj7D{Q*Tzwb?Oh#t*o18iZJkdT0OSk!@qW~b$4GC4U3D*8-h_U)^_jVCM$ zb&ZWV2(1MKk1K~XA?`P0i5G_V)D%CM|HLTERd%ixa@pPY;~P0S9dF()UmAV*aOXkC zTUzSs6qpPiUz$0)15`n3A3lDp=IWX~;pa9!*X^FdQ#k^;KnLO<5PaSQ2k#Dk{tB5} zA@h5rq*$SLq#XUh$IU(QQ)s;9wh~=I7kxpO%SofVSkG~BaYgf})LdJ(_|qK;Lveay z@#;naeP_%GSs#qE#-T&BJNON+*II|xT33pdFTeX1R8>{wvW=CM)pn^QQ`!kj{_NSs z)f_EIdY?XhdfV3*+;u|%mAPo}`9f<^QIT?5)Q2nAuQ%rFlhJ9B&wmb4p{rjnB_;K| z*39DGQ`GK1Dhqw_TU{SAU((924x3NU90@u}u{<Ic;*s@xlbtot_f$_w>W74eH(Ph? z5E2nN>O)^#S;ail?Otv^cN@r$UD%jkfPq;~O;1lRAwg+IVITM}`K?>Gl*-bQlf#y# zN_GD*L9v%J>+k@gfuE0Wj`yO%F+4mxD9uX2GWIRp7Gax8o|muw%F))=9-G~(Jh~Ok z{b7Fo_X}w&LwsxdQ86w!Mr-f#?eU;~aOApYZR?)hyDRE9*MdizyNaZuDA>#n*3vLC z8c8rScR8a4`z;Ja<-CY%b1>X-af|f;&5Mr#{E(&mR)3Gmr6xBVsHv;7Zc9JV(K7`| z7nPEdLL1Y=M5m?a&ij*F`K+u(@Nc_?-@hLvMn`L)feoOyz52qc4M6np>C-PF(qO2n z7{phm*7E4l7@++3ts@79AEC9*d~(ha5Eq}h(ap}z{y4EAD33G&1+6=4lK5ra7WbJ- zJ;5E9Qd2LUu<*a%QN9sHprbOTXA3AHYQog7BGYX4!&ikX&vclNR)7+oBp{|m!1y)? zsT&rqr;Z=DJE<A|nZOgBcMA*IKA-(@G^dz}iK+gi`>TlIVO#14g6YoJ6?{bjOJ|iQ zy=K3bEi(d6)$5?uPrRQaCnxtCzn?;I2I_5s>3cTdt6SV(hBh}hD^L1@qaAk3oF(vA zy$%5Y3!P7-&EJG<cvaXW3;>Dxo?!at!-o%7e;;W=;ha&PUTV|gGym@Um#<mAe{fW7 z(;@-l3F^4_^hF8-X8mF<UVq9~<)tTY1)Wq@7lg`I=1x-I`>rU=P0Y!DpcVE{9iKn* zwW_Qsf>8-X9Annk(4f33cFL_I;CR-FoU#d}KV((EVQ#Y9<LifA11qTc@lT!zL$F?d zjM@JwYL1|S!WPqPqXmvrXJ>1Vj~=~O1_J5u`}>Gb_7!G6zJ!Shm*$)MayrzdW5x`C z9sPj8X2voSKK-irh3+vs5XHCq+C`AU#a|t#@~mrVOB42beL3>PIBy-Q?Qwp#B>S29 z`TCv`uRPC@Lw7^TH}Adq#yH=$kxgav4mr?f1RE=BfBD)<PH#Pwo{GJ#is86J2oK<E z4N8tuT!e=G=7w|-@6_3Chf_pry9(^t<k(jdVDwCl6|^d=ySNBL3b3Eg8yp&%h3u9A zz>8t4L5*h9)*j}adOpP1e0?u9#PSO~KPHPm7rog@+Qu!#9~m*k-;^R&4?GSbH?v`u zfY(@p$;m7(pPp@i>!3IMT;pjSh)r>?&z94zx3eAE$Tm}L06yK{X7g?>m&84mwzj;( zB5+drz^LwF9UV<iPf1kcT>l?moS!zS4~wGcVEKia6rA2=Aw`xOvbJi$!~*eu@OV5~ zj%e^wyFCift6;b7@QrfH7I#A@<4L~%@+J4peeQK_8CpD^X9c^$!otK|KS>=)Q|1Aq zAvMUA{r<A$iAlkOj&Ap!=Sh(7B82XUMNp6dxA^@YN#R+%c=Y&jwz7r6SPQZ1`-dl} z7`dab@3psneR&sS&=E_xNelDkYrUC4#<C#v>lRIAD<TAL#<Gi4H6#h~T>css8A){( zC+d6^xDUcF$ef@S3Qff=-@cgHp?Za7Gro{z3FWorAIj6kj_<cq-}4ATff9^(?>bNw zap9sx>D8hlnTf6fYKSsw5HYJ;wQO@@HtqOx2W$7AbSq513@3z#$tFVUV7Am9AgjRf z9uW8UTpRnUb4xzrRp9&}`V%0W(d0{wj^xC|a~M(^(6GlM)qQW@o`X95NxDC>_(Khf zTIWewGI4+sVUR*pE!z8EZ>dKy3W$j%gwt!^K=HP<wY7YA?;zurBVOCquV3#6;5bp% z+x(aE9Aaf84FulWk#?2^0tW59uQy>FVwlOmqdU$v#5Gu;1aK($2t#L-0F2(n35h$3 zN5N;Or|_ReOr!Uh^x|lmviaBVl=iK+YkfZlMG=P4!-v|YjGkgiNlD<22b`RC{|tEb zj2j@h;q_~@eZz@sf7#L<?*X#S4|4sdN=C&U-tr$v=sp4O1a*{PUctf2Ew>bQNHdjO zWfsx|7sZckt-%(pc<#!_$FnAG)V|sw{158J-CreAM9u40@w7Jm@@Xk9ePiQjAS>93 zDI0+K(Ix)?r&n(;+=zeM*%|%oTgOm;WjLdpGM!fQ8aWx++S+8VzaguAnevR*`O}{r z^}6&`H4g0W39m6<mt^+lcHo)h#^Ku>UOOZ*t~(?vc(_SQoUoVNyWO{Lt39_gC(o5@ z90#N}YS+c_WRPprKG-0*@#<-3@x`U;Q}b<a8G75>k4q`0c`cG1H~$qpu#?HX{p560 z1#^XIXvSWj31h{jiGuJ!u^NRlK0fJScf=dmwQIoTMfHc8m`%*^Tfp<+7B-pd@Da8k zIzjd#Hy4E#2UR@TM@H^jgyc|3&4)vx+PhiQ?i&ysakMo}%g$~ay8f_e%oBprY}$SV zJ(Awq#)f)w@Eo8iigwD@a7$T-{lu9J)-#gH%E<{xOS1t7l-suMqB(xzL=<qAp`l?v z$~e17$<gmKv$Lw8H{c%<R#lW2x9nr2jZmA)U%YSdDA&0|^dR-i+$bS>3N5l-6k7Ff z-Y{Tlfp~{rx^(HD>&bN3%T9${C!U&x9=acP=)PJ|(5oIhhQOAV7J{6SNaB!`H?pvV z&CJZGu4%ow^}$J+;0o_Ebr1K+1j<&4lPD8~NZviYz2M<aUX@;%w{P=4G~5l{bN_t< z)odg6`v$YO6WMmM(w;wmo`2^~8iLl~)bN#unYd#QW*ZG%(p9cRLAA>YRpPedkX_fn zDJ$>h=0>pXT$e7%=PzEoHjOn#hJf_z>+y%}PQ<yhjWWv0ZYv3H@(TzMgiZ*${chfL z9R&r<Hhy2n^b+rJ7U&;4o!^fAef)Ek8)fC=<KqdPc&dCY7&3N~d3?K+v^1P@a?n%< z7Z;7ghv_gpQvnRMyve1d3W|z~Klzjt75hg<*2AaYdF2p(8Z)DkOu=OdYs$TR#c0al zC&!=nz;PthE0a+CAV8%*6?byR9l!nfkt$9zgGn$>GaPylNcq>|&%gC|<Nf-sZ_uqC zZqaKCQclIt5Bz9V*?cCFEKi(tet_k@I=D9@Bm=_SgH(OwwxY>Y^48Ib33_{b`vTEz z5O06}{COuclNyb4ZE?1q?~Pjs<WG{f_xX$rAy(Rm)eMo|iz&x|GvoPG;)S&0?>u^R zaLV(dbtg@A1!ow<5`8nXi=art#<^sWeG=NQe&M0@CRhFb=*bfeXrZu$@3uRQh}!Qj zcI&ksqp3LTv<}9)C4^ni6sxP~CIWJDoX{pbQ`9K?=YH-5JB6Em{tW~Dz)oqa&T8fg z*z8j*%|2-RgbxP88n%LHa&j`&$g{0d&+rTc*++HTDI#*ElslP&LhBr)zt`qrmLeh| z1k+^XQ@#k53EiKmz`ixIyu7^Ue%nSP1A{R1DO%LH1McoQJtM}p&zZjDQeu(^F5R8A zIhNJlSzymZ&{RNlLXc5b<^^u>Owp9l0r!BU1#=!3EsR-`eBkNpn^H=7Q(7w6>rit` z3*({tESL_SDS|hkfI&<eg#Q+im9+;jkQj~U&z}qYu-s(=y2u7SL(j}i@!p9f21drK zI7w{<^G+!#Z|(Ihx8TPm!h(PKQju)Gq2Xoavx0(xwol`!=z2Zbe*gZxQ&jZo&+FUe z;PK#j-&pt4NTKq8Cp4HxZuQ=~e+>Zo+|8RegY&W%7W^2Cc~29u4JaFUeFD9)<E_s< zhK5BePkR_Y+kfTCmColL89jClZXov2@$q%wUCcZ@O7}85IO8EJ>BH|u*+^CJWi&T8 z_e|MZ+FR=TqeLVWgS1mpk_FSTdR3(#Ws5ZaZre&nSy>svBUL!dulId{{{D@|0@okE zc)_*%3T=CPJ0cw%MM~FJF)6QG*^pDkuP6KmNHL~Ul2lh7JlKwC#rLO$!?+YPFK>Kq zZf@y=#?cTne<<tY_Cp`vv}7AR8WwHZ4lH3l#p1#x_Q<5abh@nCIp@`OUrw3Bj|}?? zH4mfs@M*H?tE-1A8+(6aderWO8Q)-j<EX(-Rzf_N6}!E@vpSCmS76=ipFFw8cbCk; z;-3Bb`fcXjXPjSv>sEjMd`g934Kjk=WA7O!H@7n9E~-M)?2t3QzD?$RiIM=*k#P|? zjUHzF>hh>clX+e4z~p4yxpU`AAAB$Oy2Wwg#EF%TqJT!7;*SY#2iNyX(!OZz?(S|w z^M_nja+t@6_#V@v^^?ODyzvoV-rS73c#(e2bL+dFo{L|;oW9iet-t@ReGsSmx3->= z%jc%N4)gj<e*G%>ya%RcROK%}OqI9q-lealQl-JpmUPj;d1!Rfl;aR0cx_I_Y!VtC zPEq9kxkX@2F!z;X0`*<sC`i<rQ<BjQw1o8@8K=!5E9gJhWj`Vs8m%FN2#MlM1%+pd zge@wx__Lq65N?Tt0q0`@L1%xksU?@8T+abiTZrh@{c|HtDP9MTrV9+hl$Vip|3peN zuVJC*CclUt^m3+(4#M%}TrNSDlfahjViqo>Q}9q^KOQ3b6v{)GX|~Ms*2cySIWND{ z@@*8#*9X`zE;t?6wt_h$q<JwhH~nsSsE}TWm+tSSC7YGSUo1xI8rDe8)WVf}Hy!xY z;?-__X>}$FtvkT3!SlLsP|Fu;#kM|<j20dl91J^gD(Fl_RB%&M)3(PBV^;k%hNh<S z{ekQvrbKq(P?%G61IyE=PZ!>}2k-4)=|Jf*G&1_m8^52PI}~a_+ow<R{YzIx%`e#B z_SX!%5Uovvgf5&gPN?@3WyNUFWPVIq{Fu#0%<1fbApHP?o|rK(UvH|frv?(%gc5gf zdaa{;bv@a{08LFrUPYcw{gL45m;OIyzGmfonxL=KP(Mnj7Qsudm-uWP5X1pJAx8sZ zrKn%m@zngiZ{AS}yVtDCNIQ%qH)%-Tgm8z%jLXNh+UDjXE2B7Xbw%D=KtSt{OVw3X zxvt$ZpuDD!1O>mgf^EgeZP`}^xoX%3VFx0#v(oYRD|%344+?#p8lIYpKYCNf3Pt+k zt_#tNdb_XqEln0D-M;NMmlwszqh@cPx#~PeI&ak<{fEAJ_XN%NM#03XXl>OUz7Rd8 zgWg_W<l4QFnb~SC55z#$Z{`l$W3FIKu;*ugOXlb2Q}S%Vy2j+RhV~$M;(!L(%#XA+ z?()U<oaPtjeKKL%J#3Y+FxV6sDwJ$q>T~!0{R40FL`6lRJW&MC6{LBvZb{O{qMdC~ zn6_+8-WJxXZ)Eg<@4DtyoBrr~rKMY7_1!)&3Y=1jy3j<vUV~k4C#&B?Uj0mz%5vqs zD7iRCXXk2AN`|SSKcGQyHxe~{uL4EnqoA$IxPCgn*}^Zh=@ZL!M_9%XLvQZP@-F+) zq5yq?PvhYQ?}-sn%ZFX%Au1@#iPx?<wE2Tt#Q}y45p)jr6x7-Sj}slnI03kgaxCKD zphSC?l|yAoz4xA0v&Z`LTNg-H3tg?jr@hP7?p8oti9@f_|MYEQ0xBI<Jg*XMw$Yx~ zaxsx%iHRE(7soQv8DBKQnnEtCb2@Mp5uFPk`$$z4%$9d5$lXqHb?4jB0dr`$&I1h8 zgLsuRi!eB2eiCNp>E>$!U;SY>or7>6R533>j3JHXp?<RB6O3me1l`r!so}l`+X(KL zhOck&!_n<r%@A*9P%Et$e~zc8oxE>g1)9HoMWWdKvod2G`?*k|bB{tP$n^E~JD)jT z>bECbeBad-1JXPYD^@1%_=r;Qq_oGN22o3(F+V=ua?0F1Zf$jXO%s*t;Iuu{AISTK zl97x6o;k1PxKeN3$~Mq-cNfbqDDZ&22bsY5K7*hf(S;!jqbwLXXTXwhe>odzlJbc2 z4|EdWM>^M?vJSZ^hhNvzb#E)iKv3X@9&6+p)nLj>yFU5Sr6h!X5XUen%my`Ho?ctc z=?eGoJ9w0+Kx*r#pDQKy7j{@wUz4zTqSxLliuY6<#RT{2^H*2qn9A<%EwP)$>XNq| z?w^`+`!$EVBVt~3@>l<2P8tSoGLnx^-C?m;=XV`@5N&qFDmyzHV!JwU81iph9WOO- z1A-9T8dk?vXRp@VN((>V>#pZ(<&76dr4-)4eG9oW#npu&!Q-|!5RioFOo*;BzrXBE zlD^2N!h7)GL4*s#Vq;@%CuO9it>9b`iszW(pEb<9{~~6A3ePLIUU}e5=Est{s;cCO zz(J!s*Vkv!*qu3Gk7&yC(c6KEsj2iKAt8<3DuHvtaMG3ZS39l$4u<u5MpEE|F=^+I z#v>)`jv;q508UFVI7i1_d^@-4M*xQ-NQa3~@Kw7Y^z2L}wXDy?HHT4~eiR?({mm6^ zxKZYnR5ilf`tKlPDR;gqOtb@PYU==nY{y@Hegx!vvd9GzYvIGuk!{ihcsCXzkrCVm z=`>Ys?dY*l_VlMu83_g@EUaN-a=mBdE&qMP-Gr<ITmzT+;Lq#Q=jDASH~`mdu2?Ai zB^9|w{bM~LYb%5vNc|?nj0{RKtZ#0_APJ8au)WcorEw?B?=>#r5dLujr@^W!@Uz?> z0#Jj(bFPqrnhdshu2cK&=2W@&k0)u4BW{_WFG&~+NLjs{-&+G1Xaz1$<X1t&xVgE9 zMIR+ReJTo`&MzcX{na(^AKYo3)(%BQZbH9y_;%Jf)-+r5z=4R>Un}#Uo6K1@3{Om4 zY-rHNsu$e7o0gd4H?JO~S5i_Ua%jpcKaVv=E*1Qfh1o*^A)d>QXWS+p7c+Ymz_$C! z*QcA));P@w3d6#Z9s*nlUbBBH|A-H~ZEi)sjex|-MNOD&X0z`#$3#!n*3}K(jpF6{ zSviEuqdff_zb*|r12=p3l|yU#ph%gSS`Q9IQQtR+K>-3B`lb>Y5z%OV)$#@JQ7^9o z;mN%&C?cKL-n}!peD$g|Dy6ort`=n5o^96&D;+p6=)v`zoScp*y`x7NJ~$PQshdO* zZWT@@^q0&nretJf^xQXj0xkll2hq{Ui6)Pp7iVJc3XxINYhJm0`Odw2lt`7<Q!pu> zgN0FYIA0rP1#-fRXZhetAyZlb1puk$Xrvx9{?aix{g)n@{sv7G0=>hHpp)ST8R>J- zukn^fL|b<QI1$c2l;3M9wyxaM12p{gN~xig$mXKfpictDKZ1x7L?#VGuB3$1)z$T_ zgA{(yn<F$cIDhWR2nY<U?(OB_<>dv(JqOnJ^5siJ%rcnRopsw)Ix*7*W(KNdgO{Io zHBlRH!8Nc*EXAqf;}a5?AlBvj%?_$6<!ckj8k2PKgsv`AKtRCT-rnrB<(42sM#M`Q z6ciM|fMw<7`+l_nC4r%rAD#Eax~cMY>V9fg5~i!N!it>9<x<uR*+YM7TDTm^o12Fx z&$4ozu<0`zxO|=|>QZ;WC%9FDxxpAcQzZG($On|T$z2BcZWv|Nf;*9b31FC7B3eXn zt+n(Asw0Rq5h-V#+lq2yVu5{)?7`IZny6S46BA$%!YN{>>Hz;{Ay`;`d}2bB7nk5f z@~)@Au0+5@OpFOq5iZUGl49u$l8Y)Z@Zr%hlCN()2wWo4qtN4;%-I<{@D61Czi&m+ zA`MinFKNn)28HR_)+vl4JvUdhX7k=bifB)#PjAQ4+=wDjQ&XedTEB5aJK!`zR>)u8 zMTBt5Q=_`+T~*hryAJ0?ZaEgn0SF1Cl(Oo>Sry!~ht9;r1XQ=?@~&gVNzR|A{KMyY zyAtU(i{62I4RK8`3p~|^4FcGIF*9Ld`p>~j**$jAI$7<4V1eLaWNsm8;IOCwTYhee zPW}o0uV-j@Z@Sjo$LF<qa{IsVGee5e%*R<-NFBw&AFgU|XM-(TH{5th0=Wx6uq1Qd z#(toH112V%px03&Kh+@udZt(wEO5u~NBqI>TXylVsJam(5TfE38nQxZ9hjOD^*w02 z9^LM_->e`C+zjO5PakINoEM;RFSwgylqqjWIVSgPYtY%(_Mio9Mk09G58(hJ0fW?j zVxBw;VK;-D!Q3Q7CQ*x*(P$YZO<Yp&y*XvrE&FNi>FpE;_C)n-G=F_ZA4(GF%kap^ zcyS!(Ud{5BLqM(&c_VvMEyLSxePaXRGAr?1zx#@laZh-jhMHO!22u+680Jw$1!n~` zsRYD;d7<ZjboU~W_B%K{45kh09mQgmG?JGBy${kg+$?$`$Kr}B59i5!A**Lg4(B?) zjIy~)Xq?DLPDw^mLeGFQ)Khph78E5=W4nW+qbeL}2u*0a2*}f2IfOBwDmlDGlAxK0 zy?`MfwPC7P+qG*KvNkzAcJv!C?}&{Cc*ki$FV0wEZXqd=lU8?X-<9&r%+bUN0Zidv z{)NTG-%@Ol%GbgbrVF(BVU2-+@>0~RK^B8jEf(3Vekc4mYU9C_P^`Xb+Vr&Gsi~>p zj?vq<Z?hPNoJ8tLiYqSo6>o1n1f7)^N-y-<vPad!tBH94s8rIF>rc;3CnqmYlpq*G zS4&x|CIi`DtnWH9Lh!&_ToGyl4zwAFtoIf^Xu0)mI&m^!FlNWFsFk*5f3ziR56H4r zD2OAX5tL(-laM&XQpC20gZRtVoZ4EXFgeTr14jO9^L5NI&9$}bVY6H<y(j>Bm3-|Q z2V9FraK@Oi#vN(H;kN~?`=iG|>NG7a@ATMhOrimlk^t*1Y2q*dqKCv(l@qe!44tvj zoR-<d5;Kb6ZFs42^xR2^)Arv_u}CgOJi4L2K8nfDbOS{_EfmW<wAGR(e!;!FSZQB= zf4#>np!yJptWolf8?RL<HDtcjZay`<&e;eoDZS9SgTSAjDXJRV2~s7y<=GXF?>ZY^ z0*=cD&OIS+i<tn{67UppU*9=oS-Q%C6v`IA?!hI}(~OJVcuu#Z?(*fVUPqy&Mr5^M z9gc{GN0S2CLop*r209xgNL#Pox)q06c?z^RQW|mEmvf(-2`8)PKK&kgZqCaoDK>yy zW@nPLsj!V?A!lt-Ovglsf-3$98p%w($|^G^5Y#LwmnTTCA>jx9>>@$3y^3TVlghFl zab77j(^D&7PtY(irIcQ_iVNLKL#z@41|fvfbAKa)96_LvPLQh~;Kw@IO2pEUhEdK@ z-^xNwO%2DG3s4l(VsVW_HXNpq{R}vX=YH~}^?0wd&3jw-KQer6PTL!bbHly1nxE>$ zj7TnJ6}&k(J|5Gab;1VrF7A^`oJ1vo<$+K)Y3r6YveeX6B(pA{nE$YN!(UgCM@}e1 zC0;uq9bqy#yghjIXzc~IJ^f$4Z1>$IMMf3RuxrnrYUJMZ5Z3nHrIGJRB2`pH&=DRQ zgw@yPjMt+>ZNiC~)Ect7=}*&9+qw%+LA0c`Irp`?J9(<~+*^EBm;Hg6i1hcALG+W? zn3Z@&O;b}Y@R?!JC`#*odUB}DmB{mXAnj0cSb*h9X66oTUdcn^Q&vHt$(+$7I&|-O zL}C9hFvu~8RrHK(+`x{VNgqBK!c)<kGSJqbPZfBWkwJ0&`gPfWpF2%Yo*WU4YOuYu z{V0NmTyWp;pij<)?yb26X%UJfU?-ZYVED^duZV39&{wR1q0!ASH95D#S0Y3@gjS#{ zmoy1fVK^jEr)p|zu^bMb58X=-$z5pQzRj>}8E`b7B{B&fk{}|spaWm{&spKFn*nnA z5!=FS7Djad#@)!u$r%HzLnxOB$l!XC*8PcnwXvHCFQlqU4LJ!sa@S6jKM;Juzkqz` z^4@#(6$0Tz=#S`xFcD#sse{b*Y-{6ZXJtJP%#3@pf`^1thO<&c9WgL6iUcDf4lgP) zn)1{>t)ruJP0Eo4>DLR$gI0EQoR~^(z+7o?0fZ!AIMi!cIZ^TPj3tK!H=G9s2J#~^ zMA+4X2Xxs;5hZMdE(ghQ<r~7vo+%pap6Ira91*pMLCONXXV!w=<YK6Lx-(P`Jl0)< zM}#~NEPEwofJy?|3K1Pb#|3bqp`)utBxyu6Hj<x;@!%I&7ii88A3geq=|C(TJBBT| zZ$tq;mjAUJ)*;(payY{(EL1)J>(o>v%!$7cboB>-Ny*^?6WWM%#K~!C-%%4{;pId3 zAr6bzSKL*7R8X)9Q21}5&sZHoG4+`IBwf;U=@#~?Tt2i7KY*Y8qodSdQA7#|&YEY6 zmMxy##Q-}EfC3QKqlIdZNJgmz(uU|(<srE$TEml*40t#{Kfj9V>Y=BtA&b&!A9tLr zt+;EYp3!y(7e+#23<>}m1z`ZZe;<zVSI&V~*)|~q$Yts7&J>|W9vB!XD=S-3SxJt^ zz>||aJjQ?EB|oe3JuI~#PV9|j@$uZeUqhpsf&#lEw^mjln0D~*FX*vpGMVy!`Qy$e zb20`dg5RRi5cLOnsR4{LaB*&NF-$9JVhDJ6;*d%pOzXM1s#V7?U%sRxpmc8@!btV| z_nVZJm1nTA2!_}IHe@_BzXiOpJgyre`Ry9v55j4Jkd8pFD)=JyKRD|1sGK3Hgq__0 zjQHp9XRw^!wzsd}w{IW769Hw24N`<chiq@mCz&M1&6Yx0yG2B5r$g2PCy=%nw!2+_ zku&_%^5AClC<b)-W+(URNQTaQyVyhnD|`DWP<LW)m)~%bmbmYfSwU-CUmqieOLg)a zG0JETD#Ye2<m4(x$0sLi;oQv<b${EoZQJ?wA@)pYoyK|A>yeoicpj9VhDCdvpwI}C z)Iy4ig{My=U;j`)BlpZVBgMH+o9%oy2m6;l5E|h3CNZ1wclWpR32F9^;gJS(Ll!p? zqH95d2-GPeRRDxrIhlFyo+V=bL}%*v+SnU2Ptx{QS)QRljyV#d8ZF}%vgbaNTtqtA zrRO=b=d$_bn>S;z#b^MnSPh29@FX^7^rOGmK!9wPpMqjljT;cA-9knX&M&QL3BUxV z-n@c5RE#l)fOP>yN=i!7Zy~w;xn^y-buHRQ*~=AyMqntAmgM5z?(SG(H^)BrKB@7Q zQ^CKt89g=Ifb8R;CFI5l{Rx~#LynKZom*;ux+xlR+3KQm#!`4#7~#>OgjKb+G9dwj z=*a@|53?gFr?CI+eZL2M0eNUwp$B5wH#~+aV1iM8ZQhL>CsF0V+tZ&td-l1n6Ge3f z`}~H)LVm~Ind9N+Cao-<311@cIP4x+*_)9|37R`$WF)wE?<U;$O3_`TtXb^O<D87c zudFccgt>>QOZatIx{zQ9ssvgk<!1&drO2f#4B9Bej5*7%1iW-9Y^JKhTx3|4o`S%< zug{Odx#MYs+FfzKvnVwmb513MED!Nf&DYn|5F75`9%?#^`!J0XFOdLE8TEw-Y7=jt zn4w6fZ3+7Pd3A0d(8KGoV4Hao1O=*zZOaQo3C1tHcj=_cw%LD%KR~@4u?A?*wbHe1 z`}RRBzFOocF7l=!K8Y<9Tq&aFUkh{}84kX?e>lB2Eqx#+<%70V`R_OG^pSBlZ*C!+ z$1~qPax!coL%pm6mJY*CkhcC+1ki1uGN-o(?$FiMU0y0*Q~3Vl$1LV0ZtSLenPXef zqgBwUcVqKUjk1=MitgO0aOfn$3K(#^aw`h5`My9xxMSwx3h3}bYf}8tyq82Gjm`U| zhl-Q5SvQNCi3fNELOU{kPP*GTN^Er_b_;|r^@Xejss6g|L}a)?bBF|W_|l#|d-9+( zW)<=w8cZt9%5pBRhe{JjRlEcf0Jpk*{s_ivDj?ngDljauI=r0$RGoKn(HE`y6L(^R zfjcP>UU6Ibr?|X=f@=JpVG1cSHGlt-u3y)`6_>AwY(IL#mh>4NE&JosyuSD7G;zG| zQFlSoxO$3lBNw@(Uq9i(ZihcIHIW*T+`AXqUb~O88g;~DMY{P=RH!p4Z5=Z>0)Y>b zF#XhC?*w%hMn=Yk*0p8b@Fj#n><X>kVaj|hc+=IziOmV(0nOOl5&rSxN9AtJ*XDr8 zaUBJpiHeK_<=tb(f;Jqowt4UkxPrb^ryh3n#e1!W?8Gp&_smzW!%9Ejs#6s^OL1j= zAfYu8TRVb^u*cvB6b|u#)V#DHqHZSeikJpzTpi4H$j8>x!nT3tI?=<|iQNhPbC}+h zqf$`@k80r4c_2rpyab>oj^NX!R!k2OZh7y9er2o!paWu$ELNuP<VirgbFKWGkD17} z>)v+p4AL;L>`$yU+<m2T5&}K@oAswB%7`6*{a!cr5SJ1A2}7@-#cQaJ0sByHE(h<} zv1574x4aq%*UG+QErP~oFF8l))`MF&TNnt%f?;jxnHnE=Z1eB!>7jSq{B2jjF^(l( zYzfQ$^i=Kc{=mS}Hu3UHd1>Y7Go5zKCg(%d*}sjAi9hds7eyJzlhfv~_2$Vp#{H-Y z#)Z8|@DMR5n71GS=H>6daxDF3lDd3D+lih#|5{$5voKGi-2}*gVB>=~`(zaMhZOKc zKZ1imq{0s!y3n>{TNk{F=vs#KVIfJVH;J_u%@^SZ6^y<7_`=Yh(dKh_adEN9s0RqF zzJ5~M3fec(oRBN>n&>XhX}!pR;<A8jWekyY42tKWPvMO@f^Hnkd2(9;f_yOGN$z*@ z^4j`+z&TV$Hx1&|P!-?YB3n@*G)I)adfq^KVlxXS#-~w%w@B<*PJSv3(oExA@`pZt z&0Hu+dN!HMq`5P>26|BiKq_TgM`xaaNa)!gUyf$9c0<uJ&F)3kXfR2OSK|3Jte~la zH=Pb;Sn{!^o0INiu1R2HIpzBGoIM?%o0}*ke4ZE{%Z8uKaW;(aTw6(B4XBwDDG-P$ z%@xsXsYQ!p9c{lK5xZ&@mOihwEbZUA8{I-sPOhup$Hwa{!@aybX4HlIl|hh$!&)4l zf&#tmpM%bsLB5CGrb&M7XQf`nLaRl#5uF9iG^Ega<bVi^96L)J^9Oe`DA9AXMC3)Z z!7?Hs#`T~wJo=r|($ZF<wq%rB)_PC%$YE}F_bcCxt+-y@87-yWqyYVj<CV%YaHfLp zr&(Eq8FQ{%B?_(yT4r+v)||~;N%T}rg@8U0@jG-szjY4aLlGf{)7b*Y;B^=G1vK@S z5jM+LCMk<j9|m2;+4GgB7U$;Xu(vk*csKRM)`TykqxPhwW4cOqq$O<CYqrp_f-M<T zHjgHKiC1mgS0;)mje)OUFQ6uW>MuhGQSp0&kmj0G`Fm{M%kv&L2v-l7oS2yD@oF_O za}I&ua{-|UM17k3SFyX=c!k#h=5^&jE7_v(rR;)$UrrgD&cf&9qGsgg$nAvyU^P@1 zM-}LUV1#km+27|z`Ttz=2MYGQiHN}{f@cJfhuyNu1R@wTw|VpCAz*;2XO8blBd^)Q zNBzD&%1lg5<bPNMR|6}i=htJ18`y@AUK9G}<~|{$T+u2uN1e7vJFy1Omww*h^qCdP zgBtd(5!NaG2t@d_gpXV+b{rCxOl1(H6r$Y|X-;C-+_<Y#p($dSn~A-gm0gd?`y)16 z^f#}!&0{Y1+n1=}J<%zFzg938xZw@56To|+ka2(&^iG`E*Kd0bS~vv3;d8D1E}glA zS-@-<7=pG)$hHWc-1$6DRE%56qcAa~1BgBUqUJAbe|8CxT~iK{hX?NJeKi6_)OzIg zmBl5oI>0Icq!b~>I3Vs1r3)DQVi`=lb%1c}y2mwYs8tRc5;0u-;fJVTv7p|U{+p0+ zaw`0E&ubA)XKi&QS$W2P2iOI&5;hgNoJiWl_uC-*oafqI6rA~!XftRg1YroLD;XaP z@Ox0Sgn-Rn7j4#;_cYT_0($yP^=!ak#I^z)MIefDsHo5!+|%Rx@WCf$B?edLLc5z3 zCwfXYVHY0f%qu8JfZCGIyC`j-E|dBz$ceq6gI>(!sA9=$3khqeJ<Nc<yT|;nGG0Q7 zA`Ba_frfSl7m)o<7?o({c8;g)v8hVd>kB>M`d>=$@bV&-hw|c>P~|t~UZjs;SydxT zO39;ZOM`0HP9$61LzcE+o0hfrelpThvYxfIAkhmy9MiP|6+T^T*P_IY0*XIFQU#|Q z>fJSQt967MiY*=1*fe&!nBiv#ll>i2aA+bL0saR!&l*CNrq=!IfR}?rf(uJ1@z$;0 zX>C<tj~?JPzvbC_?6|SkFV%=dlQjS#Tfjb3J?y80a@GkJ0ok39LSte=5#;XK#%Kck z2#tY=>pMH|c~M%5)klkg`s=GWRkR<75&lbqW?DJ_w;0g>5gPUX^(3=vWWU~S@Sf;a S${_y0*ueuj8c)?M!~QRV4(|;B literal 0 HcmV?d00001 diff --git a/server/package.json b/server/package.json index 4f07e68b5..0b9982402 100644 --- a/server/package.json +++ b/server/package.json @@ -66,6 +66,7 @@ "mysql2": "^3.9.8", "node-html-markdown": "^1.3.0", "node-llama-cpp": "^2.8.0", + "odbc": "^2.4.8", "ollama": "^0.5.0", "openai": "4.38.5", "pg": "^8.11.5", @@ -101,4 +102,4 @@ "nodemon": "^2.0.22", "prettier": "^3.0.3" } -} +} \ No newline at end of file diff --git a/server/utils/agents/aibitat/plugins/sql-agent/SQLConnectors/ODBC.js b/server/utils/agents/aibitat/plugins/sql-agent/SQLConnectors/ODBC.js new file mode 100644 index 000000000..d4f58464e --- /dev/null +++ b/server/utils/agents/aibitat/plugins/sql-agent/SQLConnectors/ODBC.js @@ -0,0 +1,60 @@ +const odbc = require("odbc"); +const UrlPattern = require("url-pattern"); + +class ODBCConnector { + #connected = false; + database_id = ""; + constructor( + config = { + connectionString: null, + } + ) { + this.connectionString = config.connectionString; + this._client = null; + this.database_id = this.#parseDatabase(); + } + + #parseDatabase() { + const regex = /Database=([^;]+)/; + const match = this.connectionString.match(regex); + return match ? match[1] : null; + } + + async connect() { + this._client = await odbc.connect(this.connectionString); + this.#connected = true; + return this._client; + } + + /** + * + * @param {string} queryString the SQL query to be run + * @returns {import(".").QueryResult} + */ + async runQuery(queryString = "") { + const result = { rows: [], count: 0, error: null }; + try { + if (!this.#connected) await this.connect(); + const query = await this._client.query(queryString); + result.rows = query; + result.count = query.length; + } catch (err) { + console.log(this.constructor.name, err); + result.error = err.message; + } finally { + await this._client.close(); + this.#connected = false; + } + return result; + } + + getTablesSql() { + return `SELECT table_name FROM information_schema.tables WHERE table_schema = '${this.database_id}'`; + } + + getTableSchemaSql(table_name) { + return `SHOW COLUMNS FROM ${this.database_id}.${table_name};`; + } +} + +module.exports.ODBCConnector = ODBCConnector; diff --git a/server/utils/agents/aibitat/plugins/sql-agent/SQLConnectors/index.js b/server/utils/agents/aibitat/plugins/sql-agent/SQLConnectors/index.js index 9cf1e1ff4..2e153b7e7 100644 --- a/server/utils/agents/aibitat/plugins/sql-agent/SQLConnectors/index.js +++ b/server/utils/agents/aibitat/plugins/sql-agent/SQLConnectors/index.js @@ -2,7 +2,7 @@ const { SystemSettings } = require("../../../../../../models/systemSettings"); const { safeJsonParse } = require("../../../../../http"); /** - * @typedef {('postgresql'|'mysql'|'sql-server')} SQLEngine + * @typedef {('postgresql'|'mysql'|'sql-server'|'odbc')} SQLEngine */ /** @@ -36,6 +36,9 @@ function getDBClient(identifier = "", connectionConfig = {}) { case "sql-server": const { MSSQLConnector } = require("./MSSQL"); return new MSSQLConnector(connectionConfig); + case "odbc": + const { ODBCConnector } = require("./ODBC"); + return new ODBCConnector(connectionConfig); default: throw new Error( `There is no supported database connector for ${identifier}` diff --git a/server/yarn.lock b/server/yarn.lock index 3c5484d4b..96df39c4a 100644 --- a/server/yarn.lock +++ b/server/yarn.lock @@ -673,7 +673,7 @@ "@langchain/core" "~0.1" js-tiktoken "^1.0.11" -"@mapbox/node-pre-gyp@^1.0.11": +"@mapbox/node-pre-gyp@^1.0.11", "@mapbox/node-pre-gyp@^1.0.5": version "1.0.11" resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz#417db42b7f5323d79e93b34a6d7a2a12c0df43fa" integrity sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ== @@ -1588,7 +1588,7 @@ arrify@^2.0.0: resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa" integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug== -async@^3.2.3, async@^3.2.4: +async@^3.0.1, async@^3.2.3, async@^3.2.4: version "3.2.5" resolved "https://registry.yarnpkg.com/async/-/async-3.2.5.tgz#ebd52a8fdaf7a2289a24df399f8d8485c8a46b66" integrity sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg== @@ -4813,6 +4813,11 @@ node-abort-controller@^3.1.1: resolved "https://registry.yarnpkg.com/node-abort-controller/-/node-abort-controller-3.1.1.tgz#a94377e964a9a37ac3976d848cb5c765833b8548" integrity sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ== +node-addon-api@^3.0.2: + version "3.2.1" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161" + integrity sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A== + node-addon-api@^5.0.0: version "5.1.0" resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-5.1.0.tgz#49da1ca055e109a23d537e9de43c09cca21eb762" @@ -5065,6 +5070,15 @@ octokit@^3.1.0: "@octokit/request-error" "^5.0.0" "@octokit/types" "^12.0.0" +odbc@^2.4.8: + version "2.4.8" + resolved "https://registry.yarnpkg.com/odbc/-/odbc-2.4.8.tgz#56e34a1cafbaf1c2c53eec229b3a7604f890e3bf" + integrity sha512-W4VkBcr8iSe8hqpp2GoFPybCAJefC7eK837XThJkYCW4tBzyQisqkciwt1UYidU1OpKy1589y9dMN0tStiVB1Q== + dependencies: + "@mapbox/node-pre-gyp" "^1.0.5" + async "^3.0.1" + node-addon-api "^3.0.2" + ollama@^0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/ollama/-/ollama-0.5.0.tgz#cb9bc709d4d3278c9f484f751b0d9b98b06f4859" -- GitLab