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