From dd58b7c2412834d9cdf423077fae2323d43245ba Mon Sep 17 00:00:00 2001 From: ogzhanolguncu Date: Tue, 7 May 2024 14:43:45 +0300 Subject: [PATCH] feat: move core functions to base class --- bun.lockb | Bin 218283 -> 218660 bytes package.json | 5 +- src/client-factory.ts | 18 +-- src/clients/vector/index.test.ts | 6 +- src/clients/vector/index.ts | 2 + src/rag-chat-base.ts | 96 +++++++++++++ src/rag-chat.test.ts | 233 +++++++++++++++++++++++++++++++ src/rag-chat.ts | 106 +++----------- src/services/retrieval.ts | 8 ++ 9 files changed, 369 insertions(+), 105 deletions(-) create mode 100644 src/rag-chat-base.ts create mode 100644 src/rag-chat.test.ts diff --git a/bun.lockb b/bun.lockb index 2b5b12c9cc04ef68776c09a26802522d1ffec1a7..5fa88c752ea35199bc10cbaaf99d537add2e42d8 100755 GIT binary patch delta 38171 zcmeIb33yFc`}cqL=8%J!H6mi3Nn}70A&1r+sWBx8LPA0!k;D)h2{q4bx!mW@BLlx`@gRLb@g2No_pQjb+5JVHLrcn zKH1y0dO!7?_wvBzEyvfdG08oDmr-Nb>ZQHPRIa(ObKul>ZqMDF_Q1n?+)b{miY?`k zSICM%bz6SlQa2Urt%}2uKOtd4dP<_hk&t3>)6!9ReZQihZe9>ct7CK;DaLheIjOY6 zQ5F4Xax0I#i0+L%W6Puo*(up6Nm(D-dL`0TCVplaE1%QIO6a4rva)kBk{o}LkK~_` zo|O&fxKP&NDCcn0y5rquhzc0I4RL1UIST#pQ7vdFM8IL%%sV*&&Qfog{<_{ zl*E8>S(Wf#s+5(KnLaro)!}K^!`P8k{vM=wkwd$YRbKw+4mWHbsw3fO{yw5dW$ zTg*g0fSibww!4n4ROcEUB7Ksb6_A!PH7WBe6e;)wQYuu2MA8@s(Ivmd=#u}-w%xe& ztR#oSIH#A(``#`xJtJv?^#8(|R@V7QX~7mWh{z12WR;!SrX5~5vWS*wj6>Kr_&~7WhabI zP71(YQh(CGs@#N0X#rX3nc2Y&ExR}1rNeh1C0*}ER{m>j*@*PwbBo5N%*(G!Kr*aF zgtS@}+n|hH&@PA7x?@rkvI#miG_f+6oRXcC1yeb|D(G2TW+o+!&JIY)`oq?91FhCg zO`pV+aX7Trmi;>7D>_O$^2-Jo-z)2=g1-d?j1I8DYhLkE}}kNu=2AN6HL%&em5WWhCd>d@54%9c;&ULsoS} zI`TtoqBc@8a3RI;mvE~~zeGv_?;$1rHCunm=v=05=bNppe#}mpkdu{>DW^_ghP-p(&CXBF;9 z!tAU`8B#@8jFoZLq|uoPEGK8sr9m@NG7}igjkC@TYnxY z4jm<373A2|^ig7;-Pf{DOPP?8k&u=32D;0U=g1##1|0cI`&k}-jg-C^W%G~rxB4g@ zDH$gZupEp;isN1bZHX=|Re&x-X(>`#qD(zY&Wg3tA4f_$GoL&Hf0vOb1J@wSAx}pA zajY`w?A0Ovc5f@gzgto>CQZm0ot~I+mR99d?m;DgD_ImOH;88O?e50d|7KluR?HgCng1y5g7=z&2u>d7l;;06?}S* zU`>%JnUs~0nZ#=KHaW@AEQS{kccV+y-kNUdo6)PIC#H|i&P3op`NhkS!gTy{B|HLdS!>pvV$w~B%wbaQxPfbnXI8cnCbilZ*q^!|2$_KW7$d;at z;FORUkTHI&N6(*Q-Spl^C zSWo+TR66Rmh4+JV>8HyIovjwWS{^C5ISF@<)2cT%6&mR5x!Aen?Au8fq)L>?xa;TdCHe zd!sK!O73|pEI-PuwEU)d6H*frlWxF^-M2^?H^sKyEYgYJqf-J>CX7jUkgZu>)GAYs z%V+eOU0i3iRh_Shs7Zo9kg`yOJZ@!h5WOBcJB2B`D$DK_q!j!lvMRC}QmPq%U0vj@ zH4aA$Fqby;Zxl}mZn=L!feAdZG z8h@|tNYT?)wI6uKa-_#Bw{M^3PhHE=9_) z_zWrSIU89HnVp=OK51;SWBfL&hF7*)6Mq`%WtwcoF)8>^gIYd6K7Q`O5q;`5YH;x6 zqD}AZjhGpJ=|n)guP=3f*}v-EDy}M;!_fv)r%}=-OzUbCH+1V2bfZ?IC_Pd)hBS)O z<{Jf#+}c}4G4Fj1-^Om|;!+MrZ{uvENasaD-ONzy(w@*fLOo3GH$v?tt3{MH)QD^0b}e(U-c!166g3Ic zelm(%xb@(2My-}nu4y!9d!kAk^&`S?s-UIY)s!VH$TSyjkm1|Ptvf3iLs~_-I@24S zh$>~2kkd?~xRu-WI-Jz4l#$RP%=H&q4^tzts~dAJ+{~(^MVL0jC=PHtcM4||Hj31) z8*zbdS4$R*F4$Nuq#MP7Zf%?4+uE&Hsbmak9pxHI*LSlr2@KPA8O22TR5oe_MY-Y` zH6eG>E;9;(+}cT_ILPg+!#wO}6t3)0nm{|_Y^z9Ztx*u{c71>zMG}Wmzh#(Suc}ci zB+9vf(L$fKigX?)6k?nWjMPh2Gd>H6at&Z4_r{*CYaOO-Fycbpu8TJ3FiKj5x!N-M zq){Bw0NNzOw~gE7WOeQfS4x`P6^ka#P{t@~6s9dPircug_YB{*ZdWCStJVG`%fh`- z9Rt|Odo)NdN52lyC6+C^#cMnOBbw#6vs{a3>`%&o0A;=D{|E7^n zMJUSf4~lesN~o3T1a08bYTw(%L(qDeB_(uXXt#H}uEE?LQ(cU>NH;^JAkyvH#quOQ zsTn10!<-dZPAMrO($$xcw2#>*GT}Oysgg?840Q3MqHFz+i4W={=HEg<#siyPp6Y%86zPo%$0#A4w;?dd|5QJ*eir& zYC;1{?gF8BLLExub&5$nPbk3*RcdQRMmkdojWxMX2ql@JaQb_M8B%vbzY&t`BAo+h zS1a}s#$l~57WZ6|^ zs#&p*5VB&|6JnDf?e}-M5N7hjCYMjhihZAuoeSg5N;`y*mEZb1+;@bm6d`U;im8Np znfV~5VPzaA!PeP$js066(Q+Qb2ZbecQ$JEkJ7psasAz{Nu8}ZV~vh&w#HZv z3UijAF^Ic#@M5%69eW7HqscTgR~hGeG}a5|^O<|CoHZF`K}KPCq$`aO)sP9-Hq5mR zO;WQOGz@c`tAHqJST9*GT&>sw#m7=+0n^Z|YD*@&(4@FB zX50^GR<&`(*`%kmGrNy#452%`YrXKKG3(_zhbHb@xz}X%m71BI;2MG^9c20OIGU7P zR+`ay3yr;vb)j{%XIe`GB%o1GJNH-6BtJ8E=Wl4?#@U9EdSGv3NPLv*aW+HQMZC}Rt~T%%9L*cz3is>{%%s&p%jeg@%!?x`VB<%%;}j zjJUCG*E+a6B|5)FW8+Kd;1%Z?`cmx#G--O>kp3$`vwBP>(C28DTT)@aA=XTw2u_Td zMqG;9`6L|sQ^QE-8A5GLS1J#6I9SW`%bV4XLzCHLHQ^#Ob^$u9h_c_bHTF8n{uxc0 zi^&_$4mZqlp3)c+&!9=1qvoduMs=Nx%wEgK!? zibiAUmK<7zITxaZ8~&`6M+u1!=Aqc-Ot5^g4jm7nN#oD}Opr-v)^aG#eaN=tfXEFxMAotT*|g<^plc@SW~YBhuX=AkBW4?Nr*pL;3?Fdb!VP1f+nF^BTwSCqggE{bLbkH z%r7=@3T~Y(#Zj=9V)*8}ozJ32$_dByEg`Yv5X`FGaFSI8c*>o98z)|nlx@%qlo$Y6q=+n zYwf%&apJwJ_EgW3C29JiS@oCQXuhqnoDC0i?ng6EO7^9A~4+ zjK_0!$YXY#wG)+n)Vf}^T5u4W_-l>rWoRL2j9T`Y57DIe$c36!%e5@cbXv6GyTI+- z0@ubo@0}sk7hA442Znpia5#pcm{Y_#AFYqMAf6%=3vV`|-YU;nyD-XGkmqpplS=B} z=NYx8N4e_eTh2437$!5&I7qXFy>Ig@cxegRyKIi3MZgE|+YF#$p%U2hQ*FU;#aSKwT} z^hBfnOq%6r(m_;>j{O8pa;2o8Fulq=W5|jqXVN@sZ^UyQ{|uoJBR(b4RZOUhnPth+ zaIg9Ibh0xRt*?9n;@V6|a-%JR!nCgqU&HNcyui~U9L}8yXk#R6*I_~fv1Q$f2=`j( zaEvq)GRmE&(E7_>#>IQVDz|IJqC00W&ef;TtOl1+RDZG6#EeruaT;k9tadv;fEy!M z6V5hEDBL*PCeoEbsH;_6RG9NsG!75!slO3=5T52}9j1pbHEO9SK5dFqZddVAPY=m7 zue8jnn{`%>L6cTAmt)r)H0euo(RS@clR?jDT^{a*BAsrX;hHYDqRbD$Tybc`R946Y{A} z9PizYf+yV0r3P(fURQlU=W=&Wd=6Y}qQ~OTp z6=*}zs+p@!(*lQs1w?YD-(NtJQD9!yIIp8|slmlh&{}IeSbQRy^?}WLLNdXsnWwMY zXh~>RXHIy+I{#NQYj6oIl(;G~qFlY!SxsKUC|VTmg%V^YlZ(*3XwrT3I)^D|p=Xem zFj}I~WDHo>Qn_dy(W;q;=yPa%ikTMadSJbk###f0p|!!v`V?t7S})T ze@2t8B0pSgzrpHS>(H5tCIg3s6U#TzqR_00-m*0s`{{76jn)vhmZL1Rb{JZ&Y(=vi zmWAcfA~G>Q*V{v=uTeNW(pBzBYf-T>PDPVqtgGUe(b}WY2P^~E4c{$pSMyEQc%uC| zt-gTP!`%CSCnRT3Ya(@h%Gwk0B0Ss+CD4qL_T7sn1Iz3{z4B(`v#n9i{+rFkyh)^U zBcZ zgf2zvK^$I^=gB*oxLIebCr-36cQxk5^JsTVZ}P0eF`Qhi7F>&VXFR(uqRFhF1u3ER zHqR1T!so~{jDqLgu2N_#dp>Q{19c&6h|?%|c@$J9M2embq;8z!%-6kA;&Xt+KPrw$hPgmq_mg6n zFOj_R&G-K-#o;+X@|_FH8n<@%7^@Cd&O>pq1(`9+ZOJZTx@;ijB~o%=xi#%rOdVdJ z2*^vMRAG}XHzVbBuatPEjQP4pmb*h}M!;J>zTzNb+kAtNgbUg^Xt9-ah_ zWIJADRrG1L{(nJQ?eRa!$OlJO*pB?qrB(j_g^cj#f8vl4akyHF)WWcq;4EL>|>lc>?JVQy{&fay;4T%Hh3xJd8CY& zmu$OxrL3lVg*OUcs~aqVeYVwqN0u_Kyk@PI2klh%lQP;)*zqEz#-H2zy;6KV2QN`y z*z!x;?tW56_qQ6eQ7U`hHoR8~b-~W)q8%?%>@V56NSU42kmA5ENQwGY-fSuSbz8qz z3Uxzc^hw|k+d!lwxP=tzFPj%Bye6gpjg+WTcDzVQS{f%sc}_1{(e%L)`xg8@s&5@>@J_qQfzHIl}OS3ZCS_WMM|)q&DXbikqq7U>eiQ3 zjcxW`DMK+(cq4bWwWI~vR`-*#a&))j{~yRvWdHxP0Ed^6yT_V4{p@Opl;8kc#@hV- zWEJej5Z?s3*tQcX9lO%jMM`iLZ_?H z+FCoaC&UK5(2QNO*QcMP+$fqYMK3~^!TS_a^vy_dahq)?Quya={a&dXg~#feOZy9U z?7dP_y=e0yrIeR#U8Kanf)wgCTi+=PFOd?z3n|ndTi+`RFOf2X-qcxK#BjeI@t>qP zaLBg1S4tbcZ}TE0_<^m9l>9%m^?RjI$90b4V(_tTAd-X9K5O87YP0uBapSbjiPGwMsSf0aD}ta7|9^zs8Ht|3{bR*qfK= zwv-AQK?f`Jl)yh%>ofvG)O`O}Dg81BNHfg^|6Hw`SM4%v-?E$KZKS;JCuNxabG2?Z z&;73QtrhT}tMz}b)*b&`t^aeiZk`6}v9`)?BUkD268X>7y4@J^@xnh>>x_zjuGY;- za_?2TOcv2)sQh!aF0JveSM9Q>{ByN#jgNn>*3IEzUcJk>`2WGxdQ+?C?!8KvI*2Yc zxO25`6n~uEY2)i%M<@HAUii$CcS0gEZVnr{;k6ap6L00GPX1u!>4#SSlDU7zkas`t zGpP08RbQX+KjL%jrBA%3C8qD_{AbG@&EuzRHkN!+(}?*bS}SD~d@{)Bal+fUh2}JR zo)~0YN85fPS}SYZK-==Ew-NtowB|Clemck)eA3(SJ{hf*H{wnXGQ3WC8~f0_4cDnb z#xAt9Q_)%_V>epzX>Y^-bhK8*NI5;os9Ef797C&S_!bW`-bKqTj@D`zN6@Bz=4}Lg z7OnXhIiC$OntkqVoJI3BntwjXIE}XC^JvY_C`Oxq#@pz4CR+117MvMmgrD^`uAtR5 zBF+vnE}#{jjn?WL7tz+7^EP76MQaU>f^*dG3+ji~*y#BM^+VhKMYPt`xPi9iOX~M! zwAS3%`X%)%p?)RNT1z9Yg!+9&{m=po*H_dJE$yplt+lZmE%|He_jR-uY@~cm{l1}o zXrYGhH`EU;_nT;~t#Jfx>bKPI+h{G!$oZD~eMkM!B8=wWQ9raL-$iSYMlss_^VIKr zwARsBaGv^Ipnhm>BjN(}Lo2)x&94$JqOG||{Vqmp4;cj)so(e153Q@w^Ly%tw*C8P z?xnhcw&fD_yA-YUG`3!%ewV4=W4PK@V!d?&~mRvYq7==w5dN*zaOLdjb+Y{)bASgLyI$-U!#6#ORh!p z6V76^`9D#=pQ8D_-h!W~-_O(!ZG;i=Gxb9&{5e`nFfO94`GxxZ63x$23Vxw}zfwQ6 zB%|lA)DLa@uhHDYeFJUFZ`AL%XzsM#`WyB8o%;PA%@4NXey4udsUKRJ;kr)!(9*6) zYw5;rwB#Gq??$vX(MY*L{r;eSXjz8uAJh*m_m60P1$hK*>Yvo_&uDFmk@F|@yGi}f za*XCTsUO;so6*{IqZn=eE$VkGTFW&S+@gMeQ9raiBjPXWhgSGkG`}Idh_>c7^}8Ld zJ!TZ#9<;g#Gw2rDoYg(GL94H8-mAB3(b~M#H?%=&i>7(2cny7l+Nwbe)*-xgh(#(+ zhwv%|u}_F4%2f(tmk?>CAeO1!5ZZE8r8HuNNZO7AvUQ35l^XWA~vg@vJXUQoM5yr`;FLhMi}B3@DlM7*qgD#Mi2z9z>7&5VwT*R`sk8ab1Y*^&!ry8$xVp01@8+;-cEx0Ag@M2=9gv zmsDIs2(LyE`-HfnT#X=h36a(a;;Py$L~>&Y|Hcs4R7zurnoS^%3GuV?Z36MG5V=ht zepN?=nA#K~pee-fDyJz#vt|%yg}9-bH-k7W#FAzZf2v|3<~N7v*c{@PTF@LKyamJ+ zA#SUP77!PNC~N_tsf$9aX$cY25~7qUXbI7y6~rweoT_Ili0eXZZv|0S-4J3+07QHM zgiCD=fEXMI;T;H3Ud07Mc(sPuCxo|hwT9RwL|SWzN@}+d$w3hQK@e3`N)SZNV2EQv zR8zje5bp|+8w^oH9T8$`2t+^#gpbMzfoK*AaaIUl)jSm9v=B=|A^cRa5cAtWbZi6R zuNJg{2yY8RnQKiM;OE{AsVZm{CZzq7h-!D zL{oJ`h%Mm|@!=57)z)x`!4VMN5fCj^Tm*zydx(8P1SnT~h+RUYwTEb}b_|J z5v)=oA!>GjI3`4>^6dcet`NB$Alj-ULQL%l5zrALOyzWhXch%=R)`42e=ndtgjf;< z5vhtHv<@oRjp(Qrh=@`pBHSvX6QYw^A)>RoDB?lYxijJ+RUo2^x+bEl>iHm|n<^5~ zUEL7TL-l(I(Nk>|(MxGv5YZ}5L~pf2M2vEEMLet$MD$U+Mf6owx*__h6cPQ^0TBa~ zZ+EF{cd2W49FJ8;gqX^E>MBH>YTgs#v=B>rLJU>KLd@?4(Xkgq zyjsu;B3wq}6(L5b2+4>O41Lc5L`zVdSm^uS5HYKg2O1vXpOshNyzVx)9q3L(Ee*gxC@X5g!M!Ky8hK7(4{RdkDlL6*mOJYbeA%A(klDP>5YZ zqz#2wrgjUFJPg8r7{m&dG7O?-Jj5{}4CNaS@vacL@er%k5h11yhX@!Bp;XRrh-M=o z&I(bWnvZ}uEyR)$5KpLLA?A;S=r|IhP%Rh<5uN~XMTiY5A_3xp5QPa4Me3puYeqrD zjDpyt3PwTn7!7eth|Q|!Xo%}VY#$BrjJhGjmPCm7M2M|wYa+zpBna;$h;1q^3Bqd( z#6BUmE7ur^T|%Udfp|gf79x2pg#TEG9V%rkM9pM~V?w;Fe3K#G6(Tno;#GA-h^Z+M z0VxnWRZa>-vvCk-h1ji{kApZZ#FB9kdsVRz^T$JU91pQiEf^0Go(geAh&NS4D#QgL z3R58tsEb0ZNrQ+ zgm(tSQ5BZ~;WZIrpAg5CYa+xhA<`y7e5iH{k(>$Pp9yhXrDQ_X%z`*3#3#x(3*ucN zalRY!!Fnhg<<4RK24WJ5HY1aVe~V%2;S#AzXxOoI4a6$>$cGDOG85NFkb$q?aF zAg&1Ug^HL0abb#9QLUe%^^&uN5NoDl5i=EwuT{ZRh#ol*w}kjs^~`~|F2wd6i1X@( z5L>1}#7~2`sJ2dn7(5-qdpg7=6*nEi>rse(LR?XcZVBO3 zJ!eB)7h?Nth_dR25L@Oz#Lt0nsjYJ$2G51?o(oZ4#m$BAng_8@2yf+@2eC_tw0RJf z)NUb?=R^3nXP{MCZR5aCN8t_V?AMJ$21AVlF3i2CZH5Nnn~#4Lqq zs0x-s^jHRQONhp*=Q4=vLTq0K(Nx_KV#{)f_~j7I)z;+@gI7R!uYhQ&;#NR-t%TSo zM1XRwgxDoS+DeGlYPS%{286!>5v)=Su46xHSX$Lyqj@SsVLlZ;hg4wFILxfX@839N*JA^PRr8Oi(9pg!fN|=PTZvZthv7K zW&L-nD=KZX_L%O9Q9o_gK5$m%rxBHRCcLVBtx1|Ql4j@pt=ehb$|_IS5>?6bTBP>l zPX8CQmYOp-i#FJ~W1SYN?abeyO)cfD^_a9AjuWx?$VZAnZq4X*KyuR&J`6f4Am% z*RJuOwxJX%w>!#Rl{am!CgGE|o#(D=xmUF%oV14AdCi~vpGZeV+wQi_`N5r(NO=(_ z!P>w-o{%(~1}TO5gWua+k_MhjCHYyk4AWpnii=TRYP6hE7*PdFY<8l^TI ze~t!VpxtP4>$K$85Nwi2UUhA*5n=OITVy?p%QJ8Ok~<&8Kl#R15;p{K1!MZD^0lX5$q+%_w5osguFpW!(!+FWOwi-eQAXT|SSxZiMrle z@6kjCkgP#nfvh{-fvhz>K`+n&>~N?tL3)cSEeV_dsvlVaj8R2FdP6Nky&a^t=3?j9 zAib{tJPKI=7J@|}77PZm2n|ue!Ma}!S!U!{1#)}HAs|c4J1QYquj~4dp!_hYomvpA zyJLG0Jb{Ow0$EH>fzzNEyaHYYuYsLlH`oLAg4e-nFc-`N^T7hJ5G+>yA-Z2LnXNKl z&Dn^o1LU^NIbbf3r6viC0V!Y{7!T5bEIETg9Ozt1%?;7(hV&g^Baj>A*XjJ& zuz-L+SOZo91FQy1z*4XR$eL^3os1K*q#XwXfZT%61M~vDK@50UO$ybA*6&8J0`La1 zx>W{MfERFqWOXf6ch}D$=TfNh1DsEREPAKFX;2J4Q~lcLezBVfZUxT*xpi|3QkFgW z<f z81OLY4Vr*ew8Ls3_vbDE3qcN;2BrhKQFs!N8;IrJuZSHZ*vXFlxy;x=P#t`weht%y<}D|_2Wg%LMPMfB zWJ4VRj1)YHGg&T;!4P+0LJx}(yAAsy@vX99g_YLvi zf>GFW9ExcJ!B{3^-&MmuNvgZaFBFKa8jro zXJv?w0J4Ti0}TTgi7yN0fLUN1kcs~gkQ^~_JP102PC!P9jE?po0!Z&R0CFVr15Qwy zk-RBFuhN;yNROLGhziIWKu)XWfJ^}?Sa=sGV}~V9xbmQ~x)#CosBTlT{YsRa*+fI2!qWU$qNGi}6P91#QoDKG>`p>kUCI3Od#PZld_0XfBl z*)j?#bE*T7-jQCB-sl9R!P|pq&=YhA-9T3$ab0XF!@C!dcnS9g<3KX#1H@LA|5B81 zE)cSE4FhptFo*^HfQ)t7Gj8*e6;Jk#LEsTE1jx!450bzLkO)SBkstw#24ifbAZLPn zAmxfPd5`jz4YEKwNCo48$HNJP(?BN3024tDmaS>ap%Sk`e04*UOM&y(Qkph;1u{6$Y|IDe86$=A@~3s1IxgB;1Jjk z-UR!=QZN@t`z4vB^jv}}gJ}_%2NnQnmib^ISPWhVQnaTy$^J=E!TI1I!K@$en+whiIl2S>q2 z-~{*toV3G|Zz~`YKF>U@euYv3z64)@bKng43>1UY;B#;moCn{6ufaFqJ1|7EnqweR znxq3V6!?K!zy~}4q**E>FCo7Nw{Pc3qg$`C5HblXVekv`XYd2K0xpAV;3seu{AlYH zvHg?qb?_Vb75r|8Zy^5wx4=zM4qFX)f!pY1k$=e;FKtH%DeG-1paapJ$TGkiNCuMF z1uB5@wl1wFtyBfQ2B-$Ag6hB*)C8VNh7b+{bwM4F=g(Vh&>A!cO@MIqKm$-8Gz5)6 zW6%^d1Fb*{&=N@601ya*fjAV2Y!4!UbZ0oS9cT;MfG|K;mf~EczUr(G$%})K0>yzr zKnjwxik!J*D2pS5ZBD{Nf%NkM@C?`pz6M_bImaympMlfh6gUZ91TTQ?;8`FCYS|D; zpJ$$`C6f)H5Uc~zB2NGXRspG*3^0SRoVu5TrC;88FcOafUT6G(GU1X8gK zFae~2(I8bmV@V;93=)9!{YYdI7z@ULL?FhafbgP=90z2ejYp<~Y#@b91yeu{mi@H~~Hd#o!Dm0bhbIr2o$nI0wWLi4;d<&-~Vwmyj30MQ|QS z(@ARxCoLn=!}}3#3S?n=7WqAp1kZbL-pd66PaJd{W59Hu~khEMZ=9wP{$Q6KO zBA&^0yIj9l0;zBnk=1~BEOFADRgsE!RYHX(QJ} z(gk8G-6c{gXKh)X>2L>|>?Wb9Ko~j|OYQ1C`tIQhFuvyBJ zerat#*0rF(pukWfXQ~f->b`yogDM!jH1@TfqtaSFt7{>FVSzyyY*8fxbzg0d^6!N> ztipQfq1wl4d@ntQ?<;qU_C2>l=p(e7DkfSV>yoR#3S{-O`XWa6Z6_C8lJoYWF6|O> zzg(khp@D4zL)zINwh`m?e5*q%t31BvZcN=$szPtQrXEyEHR-K~@b#l)rJYu{9@c%; z;@)~Ko6>DdkHsYQ0v3G1TPa2l_47QC^I+b?CFx)69jVD>~QBZibUU|_KGmgA1KYRGE?~ad7e%hHC%uF#8s~^bghLoU@1;gs1E)0T7I4fthW4g zQ|9sUFAc+~VEU2bEU(EP-vY}7GpPB7&veNuSpC#rZ{z2As_Dp{qsuo~5c?VFsgjKD zmsIBg_*JC(Owk{x+m;Qiym_+D|F`;wv~lQ_)$##)O{<}NN(DKJ)S&@1NQwG=fF7e) zs-n6LWT=i(IRjyH)OKc#-`~$Wb6SORn@NpWJ+$p%x`~qWfBbXGhVVJ*kI4LNV@^NM zgG&QewtGKneDW%D42Me8L6sXz5uPWX9`z0PyM8*bk=ZyQ;Y{wY)Q(vC=$g7s5q_R0 zqXwled9l%+wU@AC8cIfsRm>y0Z|%R&sLTUu)+2gvZgBbV5i@h1H)y~7rP6!Nrj~AT3{)Ei>Ar0}Pe~nFbzbA=S5BL)Yr!lu za*X#pS2gaW@7t+w&fRJ{7%EZu>Iz9~dmgwNx8>5|E=BK8G4l?QVbH=?H5iP887h4+ zWpt`#&Ed>myMJhPI$)cWVft|A8LxM<=!dPWR*TiX!Sq{Cbxp(t)i_QMt?hXNYo94C zHnctd!e5jqIfZjl_E*_)dWhd&{^lnd`PHY+@N2a3%XPXo!!C=@ZQ~ePmZ&|iuGLP1 z`mb~Rb@h+;%$(Ya?`7(#%0u*;ZL8F?Eao{^Hv2sC^UrmSCDMG=BBmTJ9%*;GC@?Xp ztyIVCIEUFG57fnwf|NcgqX{_Jo@=DTA)GdXiLe!l<6j~^dXU1oEe z@V0?LU#Yso*>SX{)>E*q)~#B4WurxpmgkoQ*%2 zv(&5(Bb9b+p@K%y9U)jqh4;PxYB#TfZbMBAmhT`(XJV=oGyUAU-(stsUO-GpU|Z>q zSd}{pFVZlOA$IN8R?p5YU33G32y54vu6B%Kp!zwE7UO-33yWquh;Qzg@cQ+?Q%s ziasjc((e^4(R>ihzOI0UPui36K7LxYLwN8_t-uC?-(gugLWB3QR zCPo(GA4d+D{CdUYHzY<5ihMHBQl*ks>#pV`>SI~Bt|a2q7*#e&pXrxwC!4moe@e{E zjqD|gLzEU20{Tg*GiHx37<;PID=apN;PL(x=rrWO$ zj3Mql^|^>M>Q{+drJ9e$f%R&|SV-%Pqu$TZYuo2Eb$u+S70*+RpFi~ExlQiN&);3< zUQq$bKT7XZl@(n`*;E zPJsdHuVe;UWk#_#HSFZ|WXtW>qx#&n^AAuVDSC@?0a(bY&?!KTNzp^v_C%L9snV!! zwSntyGC@5>4<<(Xr%d$W4bOdcb^6_yb?R7(*%WfTdVCzkKdb!4v8;QZ&|Nrn`k2AF zk3KB*ji4r>vbFUcr-#(`JiEJYq5fr?s;`{Kf>U@H`zW`Ttske4@w8u^@%kz~AV9q` zUazOu3{oe@>-ik+2Bp&Y8`Yvz8pZRx?N@JK{;6E8_ZpYh+5`rh2Z&SZO$^$39?)GT z`{Tsa4}R9~8h9Sz{c_{d@j(~nRJ$7!8mua&v9@>~`n_n`t#vyaR{8#}MYigkM(vdv zitzJ1G(4>7_VAEDKk9bZ?o+i4gLa;$j1N5bX3@M~X4blE;CT+YyKKKPne&e%-i-+g zQRl=7&y&k5eRVvz)y<j&Did(xDb-TMYwAG0Uac(44s+x^H3F{PmmCFT&8Ydu zAHn_KYnN1-scso~9@x9yFKEvv7e^1d8{>*l&re__@;oox+o#LL_0LZ}eAmMBMDc@; zAG$ZWsCT*>(@p&;t_)TyX5MinRkcoM)$u&1d|S7a_)K@xb$1hao@9Q!sIvcC%`W!2 z8}ppXC8^*32y5w19r@(#lPh+2zH4w&?ZAKo!BG)6Rl+R2nwM3Nk;< z=I;&6`ReP};~{I>l^Wsu^NC8&bz~l}A<2{OD_7Z+7k6)hwSMcTx@;ifp1^ zs!G{3u;+QxCm-(i%%x=|Z7|@HLLB)`UCU$t(tD`k+00zeGuPkfpK?h5>yzaeGTp?` zw}*Nzo0)q;RhvYkJ);sP;n+KB`XoM%IH95^qn%eHo|G%r$$G77o=1Rxb9qm#{q=4g z*SYxKHd(JPcT;{anN_T6jCBy5Gwj`wGkcf&UeOwp2R5v9!mPGgn^CGr zl8sk;r_z$y>Z)jOC|3@d9PVqK29~OhIker^{j5E1P`?u~@jo3YArt%fmaZmZpgpP< zA=-Hju*$xbs=b)epo?5|a^9D@+LD;cOw#LrOiw;Ic-3ZIs}3Wl$;bg}=E{kyB5y@}5TKb5OlegUemJ)S_GU(z-SgMrM8)E(FRWA8vi{N#FMl zRX2Og>{_n@YS=Uy+Ve1X{{|%u_q!VG#zHnk`qXQnT8>3K&tu&$Ck}}{+hKnfEM)v( z;dyfWbLqjar2a5^Ju&vc!S!>b^_vxIc{XZdqmG@*R|>~M&c$@)aaCnHLyJwJ6?LqA z-}W&L?N(^(d2IayRgg`XrHA96{$)2j8O^n8XIEh0s!N7?ARs@tOVRiU~1SUq5v zT9?a3O2=Vp|6ILB=}^9VR3GMIFj$qE!9~31G4!z`FW>%SdBSgI)%l=-o{P73)s8>! z-|=4Mw#jz6#0*vAXE0{At2yN0=XoG~xx$jqW|#e%1%bvh&#V{KCo}XIKhJ~b7bULR z@%m>+_`F0Km$M94YmrP`Kl>5&`H^*d44<~Lmt14X#Ui8bpi0N!Kd%z@IQUZSlf3S; zN=Rw6*4^v zJ_X!wx3cVhQI%#g%w22*)yOF5!L=iHmZ`*E?#bwWo0XL`Bti|JNndv(tvHdgyTg_? zWzxR8o7UW|)#Gpj{aeki!;>!Q0E-EW_=lA8NeHPvaB zUdHcx+mZ8+Xa7{>{%!Bw^ydEMt5<5T9-YOExZl=gW!JX7dJzl1p6#vn@TwBka+kL1 z{kvHWQ(waIk?R3%uw2F zQmt2h2)#zCYB!rhVn`aFYE$p;`c5kq6I&p=2HmQ$67yoU zSIoarR}r=ETckPTLnf%Gxm+EXMf>uL!HIK89jg{0YI`1N-#$2cV6B;7JVzO(ax)u z^KmprjS%s>+E<_lmNi{-q^o!5Ge63wtD7Qy(^b#{WK_D^zY&+m^V<x zG+DaLg>-bfwPwT?m$a-}H+mXg*lXOk>h=N_i>ny8Xundg; z26~3I&rIF@YnZcrPd)}SmtwAz%^ZCFuG>X~KOV5E{-LTz%nyysj~y+8c1!zN2bQ=u ze{illccqzGg!!q`-|vDEnX1krw&x<%ZxK$!O|ts=k$y)XPN@9KVY0ASnMAc=5iM`1 zD~qW87pm-HeI~#5Td){%ygId*#$2V!FVV;P1?E_Jq&2TI>e*GJxWJK3hfZ{iP)n9z zSF8>!A&{CIns`hqO+>&8;N%vIf&5j|c_U&b+Pgz{N#YSO8-$LCv9wa@nBQ?8uy zxE0p#LO}8@XG(`(e0hy?5EEqix6vyj#I}a zO}@G$!b(%ty6&8->aFB6%_?)Pw2=#+JQGs?hJ4}|!gl~TW*)hG{ochu=I(D39-mdc z+|vy)kfBMjv8re#xmsUa=pkw9q-6NNe=Xsuscp!aAj)7uY*t4fC;0(2(V$=MbCd|S zKJ7ZL-ZYphehbxShTf%Ni-p!eZ;z~vUr+p4ZQG$U1^hz^^K^n=uc^+f$oQZdim2Uu zku|K#x>FbKsrN>rSwXYI!xyRLt8m0J)IF+FXG!O`d9gK;9(ytA!xj;J%PiFIm>QSp6SC}7E2i8*lSL#DW_J6E0?~2Rc>VNj#PhaZF;f3pS z>2pV&)vC@KJw&~txaPO4{obBnjqdZ$=d2j?)x_U%M!p5(NMsr4R-!S?jX7($unBow z?I904xgA^6FNAD-sbb0e&$D+t{GJ!taCspk-Ez*anX-oBik*iJr8Eehf7h$7DxF2s z&MCxNc0;YD)GV&aS`H%j-8IYw?n$+T%KN=S{?f+lC%+z$^06}$2g3M_jq}UDw2tJ&e1gheRvkCez_-*FPf*mR0;_|+J^JXXfeAbKV$ocUY2mA? z+B#N~vnoNvR`tR6YHvJXm&zs)Kui^?{>|Bbhf}WjTVIZvXH6~?J}Bhk zFA59!M6To5j}jl9H?EPSmFv}D2Nt#0bKtZ0DlJ*{Sx*)B6j~Q4quabTwf%>K%2=LB zcUny$F4f}8*`4e4C+@c~aJ-Nam#ES

l>VZv*XNUR?WX->IvRZS^ASv;D%in>^EE z!TaCdEut1Net7Fx^4pu%Ac>c zim*KO^!}r&3fI7V%i0p*w_%gjt1ryo5ZHa!cgM^U+R5?UyjBkN^xbn$(opvKVwEZ( zsqWsaZaztw(^TtCG{o|!t?rw7?ov{pPaUPL-q#Spwu(gv_93b!_WtgMIH3S1UVjfM!mm@h4J2ww8FK2CQ)Uw zd;ic^Mc*8KjxVlgHFLWtwM9Mr6xWm0wpbRMGO8b*Ja3WgXYwgRFn2@PG3{cvefUC& zPj@rMoS>mw)H>37n(>PI@F{9t{#mP&uU0NrC#L4Vfjyt5OCQcArW{^mJYMNSh1A*H zz`)uZDyN~P%6~KC=rc8bGabQ#v{`T2FngP2{`a%euIc5?^n@7>Ew}#}o%N7Hwx}*o z>kq2=PwPSaPmA48>vijoV>g#emG^#D9cLWKxtfbEr?cO~mOtJ6Y2VPf>IP=*(_*Ya z_<8K~rv`1S=Q|Z$=GaW1*owESw>dW(T^jT4h!F$aliQzqaf?;t;@F-2pV3=ro+p@M6as-u>^pjYNUc?7viHQcGT=+&S5Uu+(VnE(I) delta 38466 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 export class ClientFactory { @@ -44,23 +44,19 @@ export class ClientFactory { redis: TInit["redis"] extends true ? Redis : undefined; vector: TInit["vector"] extends true ? Index : undefined; }> { - let redis: Redis | undefined; - let vector: Index | undefined; + let redisPromise: Promise = Promise.resolve(undefined); + let vectorPromise: Promise = Promise.resolve(undefined); if (options.redis) { - redis = await this.createRedisClient(); - if (!redis) { - throw new InternalUpstashError("Couldn't initialize Redis client"); - } + redisPromise = this.createRedisClient(); } if (options.vector) { - vector = await this.createVectorClient(); - if (!vector) { - throw new InternalUpstashError("Couldn't initialize Vector client"); - } + vectorPromise = this.createVectorClient(); } + const [redis, vector] = await Promise.all([redisPromise, vectorPromise]); + return { redis, vector } as { redis: TInit["redis"] extends true ? Redis : undefined; vector: TInit["vector"] extends true ? Index : undefined; diff --git a/src/clients/vector/index.test.ts b/src/clients/vector/index.test.ts index baf9e06..837643d 100644 --- a/src/clients/vector/index.test.ts +++ b/src/clients/vector/index.test.ts @@ -22,7 +22,7 @@ describe("Vector Client", () => { await upstashSDK.deleteVectorIndex(DEFAULT_VECTOR_DB_NAME); }, - { timeout: 20_000 } + { timeout: 30_000 } ); test( @@ -38,7 +38,7 @@ describe("Vector Client", () => { await upstashSDK.deleteVectorIndex("test-name"); }, - { timeout: 20_000 } + { timeout: 30_000 } ); test( @@ -62,6 +62,6 @@ describe("Vector Client", () => { await upstashSDK.deleteVectorIndex(indexName); }, - { timeout: 20_000 } + { timeout: 30_000 } ); }); diff --git a/src/clients/vector/index.ts b/src/clients/vector/index.ts index b76802d..627f517 100644 --- a/src/clients/vector/index.ts +++ b/src/clients/vector/index.ts @@ -3,6 +3,7 @@ import { Index } from "@upstash/sdk"; import type { PreferredRegions } from "../../types"; import { DEFAULT_VECTOR_DB_NAME } from "../../constants"; +import { delay } from "../../utils"; export const DEFAULT_VECTOR_CONFIG: CreateIndexPayload = { name: DEFAULT_VECTOR_DB_NAME, @@ -94,6 +95,7 @@ export class VectorClient { }); } } + await delay(); if (index?.name) { const client = await this.upstashSDK.newVectorClient(index.name); diff --git a/src/rag-chat-base.ts b/src/rag-chat-base.ts new file mode 100644 index 0000000..7870114 --- /dev/null +++ b/src/rag-chat-base.ts @@ -0,0 +1,96 @@ +import type { Callbacks } from "@langchain/core/callbacks/manager"; +import type { AIMessage, BaseMessage } from "@langchain/core/messages"; +import { RunnableSequence, RunnableWithMessageHistory } from "@langchain/core/runnables"; +import { StreamingTextResponse, LangChainStream } from "ai"; + +import type { PrepareChatResult, ChatOptions } from "./types"; +import { sanitizeQuestion, formatChatHistory } from "./utils"; +import type { BaseLanguageModelInterface } from "@langchain/core/language_models/base"; +import type { PromptTemplate } from "@langchain/core/prompts"; +import type { HistoryService, RetrievePayload } from "./services"; +import type { RetrievalService } from "./services/retrieval"; + +type CustomInputValues = { chat_history?: BaseMessage[]; question: string; context: string }; + +export class RAGChatBase { + protected retrievalService: RetrievalService; + protected historyService: HistoryService; + + #model: BaseLanguageModelInterface; + #template: PromptTemplate; + + constructor( + retrievalService: RetrievalService, + historyService: HistoryService, + config: { model: BaseLanguageModelInterface; template: PromptTemplate } + ) { + this.retrievalService = retrievalService; + this.historyService = historyService; + + this.#model = config.model; + this.#template = config.template; + } + + protected async prepareChat({ + question: input, + similarityThreshold, + topK, + }: RetrievePayload): Promise { + const question = sanitizeQuestion(input); + const facts = await this.retrievalService.retrieveFromVectorDb({ + question, + similarityThreshold, + topK, + }); + return { question, facts }; + } + + protected streamingChainCall = ( + chatOptions: ChatOptions, + question: string, + facts: string + ): StreamingTextResponse => { + const { stream, handlers } = LangChainStream(); + void this.chainCall(chatOptions, question, facts, [handlers]); + return new StreamingTextResponse(stream, {}); + }; + + protected chainCall( + chatOptions: ChatOptions, + question: string, + facts: string, + handlers?: Callbacks + ): Promise { + const formattedHistoryChain = RunnableSequence.from([ + { + chat_history: (input) => formatChatHistory(input.chat_history ?? []), + question: (input) => input.question, + context: (input) => input.context, + }, + this.#template, + this.#model, + ]); + + const chainWithMessageHistory = new RunnableWithMessageHistory({ + runnable: formattedHistoryChain, + getMessageHistory: (sessionId: string) => + this.historyService.getMessageHistory({ + sessionId, + length: chatOptions.includeHistory, + }), + inputMessagesKey: "question", + historyMessagesKey: "chat_history", + }); + + return chainWithMessageHistory.invoke( + { + question, + context: facts, + }, + { + callbacks: handlers ?? undefined, + configurable: { sessionId: chatOptions.sessionId }, + } + ) as Promise; + } +} diff --git a/src/rag-chat.test.ts b/src/rag-chat.test.ts new file mode 100644 index 0000000..f614530 --- /dev/null +++ b/src/rag-chat.test.ts @@ -0,0 +1,233 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import { ChatOpenAI } from "@langchain/openai"; +import { RAGChat } from "./rag-chat"; +import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import type { AIMessage } from "@langchain/core/messages"; +import { delay } from "./utils"; +import { Index, Ratelimit, Redis, Upstash } from "@upstash/sdk"; +import type { StreamingTextResponse } from "ai"; +import { DEFAULT_REDIS_DB_NAME, DEFAULT_VECTOR_DB_NAME } from "./constants"; +import { RatelimitUpstashError } from "./error"; +import { PromptTemplate } from "@langchain/core/prompts"; + +describe("RAG Chat with advance configs and direct instances", async () => { + const ragChat = await RAGChat.initialize({ + email: process.env.UPSTASH_EMAIL!, + token: process.env.UPSTASH_TOKEN!, + model: new ChatOpenAI({ + modelName: "gpt-3.5-turbo", + streaming: true, + verbose: false, + temperature: 0, + apiKey: process.env.OPENAI_API_KEY, + }), + vector: new Index({ + token: process.env.UPSTASH_VECTOR_REST_TOKEN!, + url: process.env.UPSTASH_VECTOR_REST_URL!, + }), + redis: new Redis({ + token: process.env.UPSTASH_REDIS_REST_TOKEN!, + url: process.env.UPSTASH_REDIS_REST_URL!, + }), + }); + + beforeAll(async () => { + await ragChat.addContext( + "Paris, the capital of France, is renowned for its iconic landmark, the Eiffel Tower, which was completed in 1889 and stands at 330 meters tall." + ); + }); + + test("should get result without streaming", async () => { + const result = (await ragChat.chat( + "What year was the construction of the Eiffel Tower completed, and what is its height?", + { stream: false } + )) as AIMessage; + expect(result.content).toContain("330"); + }); + + test("should get result with streaming", async () => { + const result = (await ragChat.chat("Which famous artworks can be found in the Louvre Museum?", { + stream: true, + })) as StreamingTextResponse; + expect(result).toBeTruthy(); + }); +}); + +describe("RAG Chat with basic configs", async () => { + const ragChat = await RAGChat.initialize({ + email: process.env.UPSTASH_EMAIL!, + token: process.env.UPSTASH_TOKEN!, + model: new ChatOpenAI({ + modelName: "gpt-3.5-turbo", + streaming: false, + verbose: false, + temperature: 0, + apiKey: process.env.OPENAI_API_KEY, + }), + region: "eu-west-1", + }); + + const upstashSDK = new Upstash({ + email: process.env.UPSTASH_EMAIL!, + token: process.env.UPSTASH_TOKEN!, + }); + + afterAll(async () => { + await upstashSDK.deleteRedisDatabase(DEFAULT_REDIS_DB_NAME); + await upstashSDK.deleteVectorIndex(DEFAULT_VECTOR_DB_NAME); + }); + + test( + "should get result without streaming", + async () => { + await ragChat.addContext( + "Paris, the capital of France, is renowned for its iconic landmark, the Eiffel Tower, which was completed in 1889 and stands at 330 meters tall." + ); + + // Wait for it to be indexed + // eslint-disable-next-line @typescript-eslint/no-magic-numbers + await delay(3000); + + const result = (await ragChat.chat( + "What year was the construction of the Eiffel Tower completed, and what is its height?", + { stream: false } + )) as AIMessage; + + expect(result.content).toContain("330"); + }, + { timeout: 30_000 } + ); +}); + +describe("RAG Chat with ratelimit", async () => { + const redis = new Redis({ + token: process.env.UPSTASH_REDIS_REST_TOKEN!, + url: process.env.UPSTASH_REDIS_REST_URL!, + }); + const ragChat = await RAGChat.initialize({ + email: process.env.UPSTASH_EMAIL!, + token: process.env.UPSTASH_TOKEN!, + model: new ChatOpenAI({ + modelName: "gpt-3.5-turbo", + streaming: false, + verbose: false, + temperature: 0, + apiKey: process.env.OPENAI_API_KEY, + }), + vector: new Index({ + token: process.env.UPSTASH_VECTOR_REST_TOKEN!, + url: process.env.UPSTASH_VECTOR_REST_URL!, + }), + redis, + ratelimit: new Ratelimit({ + redis, + limiter: Ratelimit.tokenBucket(1, "1d", 1), + prefix: "@upstash/rag-chat-ratelimit", + }), + }); + + afterAll(async () => { + await redis.flushdb(); + }); + + test("should throw ratelimit error", async () => { + await ragChat.chat( + "What year was the construction of the Eiffel Tower completed, and what is its height?", + { stream: false } + ); + + const throwable = async () => { + await ragChat.chat("You shall not pass", { stream: false }); + }; + + expect(throwable).toThrowError(RatelimitUpstashError); + }); +}); + +describe("RAG Chat with instance names", async () => { + const ragChat = await RAGChat.initialize({ + email: process.env.UPSTASH_EMAIL!, + token: process.env.UPSTASH_TOKEN!, + redis: "my-fancy-redis-db", + vector: "my-fancy-vector-db", + model: new ChatOpenAI({ + modelName: "gpt-3.5-turbo", + streaming: false, + verbose: false, + temperature: 0, + apiKey: process.env.OPENAI_API_KEY, + }), + region: "eu-west-1", + }); + + afterAll(async () => { + const upstashSDK = new Upstash({ + email: process.env.UPSTASH_EMAIL!, + token: process.env.UPSTASH_TOKEN!, + }); + await upstashSDK.deleteRedisDatabase("my-fancy-redis-db"); + await upstashSDK.deleteVectorIndex("my-fancy-vector-db"); + }); + + test( + "should get result without streaming", + async () => { + await ragChat.addContext( + "Paris, the capital of France, is renowned for its iconic landmark, the Eiffel Tower, which was completed in 1889 and stands at 330 meters tall." + ); + + // Wait for it to be indexed + // eslint-disable-next-line @typescript-eslint/no-magic-numbers + await delay(3000); + + const result = (await ragChat.chat( + "What year was the construction of the Eiffel Tower completed, and what is its height?", + { stream: false } + )) as AIMessage; + + expect(result.content).toContain("330"); + }, + { timeout: 30_000 } + ); +}); + +describe("RAG Chat with custom template", async () => { + const ragChat = await RAGChat.initialize({ + email: process.env.UPSTASH_EMAIL!, + token: process.env.UPSTASH_TOKEN!, + vector: new Index({ + token: process.env.UPSTASH_VECTOR_REST_TOKEN!, + url: process.env.UPSTASH_VECTOR_REST_URL!, + }), + redis: new Redis({ + token: process.env.UPSTASH_REDIS_REST_TOKEN!, + url: process.env.UPSTASH_REDIS_REST_URL!, + }), + template: PromptTemplate.fromTemplate("Just say `I'm a cookie monster`. Nothing else."), + model: new ChatOpenAI({ + modelName: "gpt-3.5-turbo", + streaming: false, + verbose: false, + temperature: 0, + apiKey: process.env.OPENAI_API_KEY, + }), + }); + + test( + "should get result without streaming", + async () => { + await ragChat.addContext("Ankara is the capital of Turkiye."); + + // Wait for it to be indexed + // eslint-disable-next-line @typescript-eslint/no-magic-numbers + await delay(3000); + + const result = (await ragChat.chat("Where is the capital of Turkiye?", { + stream: false, + })) as AIMessage; + + expect(result.content).toContain("I'm a cookie monster"); + }, + { timeout: 30_000 } + ); +}); diff --git a/src/rag-chat.ts b/src/rag-chat.ts index dd5eb76..893fc8a 100644 --- a/src/rag-chat.ts +++ b/src/rag-chat.ts @@ -1,34 +1,25 @@ -import type { Callbacks } from "@langchain/core/callbacks/manager"; -import type { BaseMessage } from "@langchain/core/messages"; -import { RunnableSequence, RunnableWithMessageHistory } from "@langchain/core/runnables"; -import { LangChainStream, StreamingTextResponse } from "ai"; import type { BaseLanguageModelInterface } from "@langchain/core/language_models/base"; +import type { AIMessage } from "@langchain/core/messages"; import type { PromptTemplate } from "@langchain/core/prompts"; +import type { StreamingTextResponse } from "ai"; import { HistoryService } from "./services/history"; -import { RetrievalService } from "./services/retrieval"; import { RateLimitService } from "./services/ratelimit"; -import type { RetrievePayload } from "./services/retrieval"; +import { RetrievalService } from "./services/retrieval"; import { QA_TEMPLATE } from "./prompts"; import { UpstashModelError } from "./error/model"; import { RatelimitUpstashError } from "./error/ratelimit"; -import type { ChatOptions, PrepareChatResult, RAGChatConfig } from "./types"; import { ClientFactory } from "./client-factory"; import { Config } from "./config"; -import { appendDefaultsIfNeeded, formatChatHistory, sanitizeQuestion } from "./utils"; - -type CustomInputValues = { chat_history?: BaseMessage[]; question: string; context: string }; +import { RAGChatBase } from "./rag-chat-base"; +import type { ChatOptions, RAGChatConfig } from "./types"; +import { appendDefaultsIfNeeded } from "./utils"; -export class RAGChat { - private retrievalService: RetrievalService; - private historyService: HistoryService; - private ratelimitService: RateLimitService; - - private model: BaseLanguageModelInterface; - private template: PromptTemplate; +export class RAGChat extends RAGChatBase { + #ratelimitService: RateLimitService; constructor( retrievalService: RetrievalService, @@ -36,37 +27,16 @@ export class RAGChat { ratelimitService: RateLimitService, config: { model: BaseLanguageModelInterface; template: PromptTemplate } ) { - this.retrievalService = retrievalService; - this.historyService = historyService; - this.ratelimitService = ratelimitService; - - this.model = config.model; - this.template = config.template; - } - - private async prepareChat({ - question: input, - similarityThreshold, - topK, - }: RetrievePayload): Promise { - const question = sanitizeQuestion(input); - const facts = await this.retrievalService.retrieveFromVectorDb({ - question, - similarityThreshold, - topK, - }); - return { question, facts }; + super(retrievalService, historyService, config); + this.#ratelimitService = ratelimitService; } - async chat( - input: string, - options: ChatOptions - ): Promise> { + async chat(input: string, options: ChatOptions): Promise { // Adds chat session id and ratelimit session id if not provided. const options_ = appendDefaultsIfNeeded(options); //Checks ratelimit of the user. If not enabled `success` will be always true. - const { success, resetTime } = await this.ratelimitService.checkLimit( + const { success, resetTime } = await this.#ratelimitService.checkLimit( options_.ratelimitSessionId ); @@ -89,53 +59,11 @@ export class RAGChat { : this.chainCall(options_, question, facts); } - private streamingChainCall = ( - chatOptions: ChatOptions, - question: string, - facts: string - ): StreamingTextResponse => { - const { stream, handlers } = LangChainStream(); - void this.chainCall(chatOptions, question, facts, [handlers]); - return new StreamingTextResponse(stream, {}); - }; - - private chainCall( - chatOptions: ChatOptions, - question: string, - facts: string, - handlers?: Callbacks - ) { - const formattedHistoryChain = RunnableSequence.from([ - { - chat_history: (input) => formatChatHistory(input.chat_history ?? []), - question: (input) => input.question, - context: (input) => input.context, - }, - this.template, - this.model, - ]); - - const chainWithMessageHistory = new RunnableWithMessageHistory({ - runnable: formattedHistoryChain, - getMessageHistory: (sessionId: string) => - this.historyService.getMessageHistory({ - sessionId, - length: chatOptions.includeHistory, - }), - inputMessagesKey: "question", - historyMessagesKey: "chat_history", - }); - - return chainWithMessageHistory.invoke( - { - question, - context: facts, - }, - { - callbacks: handlers ?? undefined, - configurable: { sessionId: chatOptions.sessionId }, - } - ); + /** Context can be either plain text or embeddings */ + async addContext(context: string | number[]) { + const retrievalService = await this.retrievalService.addEmbeddingOrTextToVectorDb(context); + if (retrievalService === "Success") return "OK"; + return "NOT-OK"; } /** diff --git a/src/services/retrieval.ts b/src/services/retrieval.ts index 063daee..d6b860e 100644 --- a/src/services/retrieval.ts +++ b/src/services/retrieval.ts @@ -3,6 +3,7 @@ import { formatFacts } from "../utils"; import type { RAGChatConfig } from "../types"; import { ClientFactory } from "../client-factory"; import { Config } from "../config"; +import { nanoid } from "nanoid"; const SIMILARITY_THRESHOLD = 0.5; const TOP_K = 5; @@ -51,6 +52,13 @@ export class RetrievalService { return formatFacts(facts); } + async addEmbeddingOrTextToVectorDb(input: string | number[]) { + if (typeof input === "string") { + return this.index.upsert({ data: input, id: nanoid(), metadata: { value: input } }); + } + return this.index.upsert({ vector: input, id: nanoid(), metadata: { value: input } }); + } + public static async init(config: RetrievalInit) { const clientFactory = new ClientFactory( new Config(config.email, config.token, {