From 6c833e277354cdac29b991998663f40a5eae7c1b Mon Sep 17 00:00:00 2001
From: ajaysi
&D#)FWX(LNMh zdYWmef=^FJSX!UNS{3X`z2HotyF!_I5$9HCRj3K{^b2(7)6fZve= z&kUZ^r}VTM-?j5xZ-3VrIz8-MOdC0uNoz6vTqH(lJ~hmLg;P_7isI4YV3`bn&G^%2 zk<3a<0m)ynSidgUIyPTFe#!p}e|^rsJ|C!BDw8T(ZdS;174kyGhJ2tF1%ZZ}{`NV4 zdv3$t1^>fux7_SKGuL}2_tY~By=-CP;?0Sfxrv!iC27*uXa7WeElJWL`$_vU{W@j; zIM8;@zYypo_^s&pHpU=DDKS5^S`Pf({%7`kpDCM^raJti*cHbc&eu_!K5LyI^Nfj; znAkHdvDUO()R%BN&wI^$@!M=;o(-k!0@x$7CDx^_Go-i`&*$seCOxkd&z9mHlN8^d z+7$mA0h2j==D2OssC!`x`Y#8KsW3-bJh?cdF4U>A>NMQ5PPJ91Y3({wt_~?(X+GO1 zv28Q<61rBNt~Q_jKvII!cBB=rxXg9J&eWODmC9o2SxIrt*q^mexy1LRZBn}EI18ls zf7;u|6?3aGW@Ay@-+nfs+UytPeqyvT9XyNHb;>3`l!=gWUL=D!f`DZrF bSwhWGIX^_WGnP(o)r1!}C)ie=ClO}h{Bci|@Zl^r1 zCZeiY98H3bCej8?6up^kOL~*Rp2Y$f>=RVF=QVr5m8P*2oN4u9df-=dW(zJZR%8w% zzx3+&UzhoiO^~uap3KA)^e#uza!ifrX*oTal(~oiu96rF%O|K9oPtfsk+F!D2xn^t z)tDMh%f?`3Vg)@R2Lzk41jL3&EL%6gzq?{;Vhr@&sE7(qf|;#4hO~=BjFLxzzKbc9 zLEuWH0im_@m)s@y_RGCo&XCh;B+f#Z&T9^~iDp (jF0~gDBJ7vBsJ#xY3`xWs3ww01?NHSe_H|Ko79IH(1g$5)9pB_0>&WN!y zqIm@e%Ona8OdLEZ71Pp;BnB3sgid|I+jlXlriiT;$~o^a_oCngn@Te}{;U=W &BV972uf6>0|a>&)6mPs}BFLr!;(~?^%Zw1$?lyeoE^Obc!sc8Grf!vla+}yHn zZp*&;EsxG`?0x&>jg5!!?BmMy`R4X~(+EIRnIOKiX{jBoXL*AZT%Qkayk&P*`j&iB zRnu~XCs_7rrR4T~E&2_0sY-JBmue+X@Jizichi#H>8)K1Y|L$X;KRV~C6DB*yt4a- z*DP<#$=g2+>=5PqZg|^``mMS3;SU4dOD@T`;n&MGQf1RE$xeW(a XBKY<0Z1)kXQ8-h=l!-rLiDu-)-~y9@dE zL*Bl6$NM{*`>Gs&TjfGN3Ru>M o#y&$Fq(irwvsWY%=S>wq!e2@(G!ar91%b3DiEK#I%H} z_hcaONC#Ct$~2H)60CUz$gf%yBE6Q7cXd4=DDet*5C;vm_=&6t09s KSG_*%% z8_~f4+l-V!xREmbNH`DR9hr3J^YDOU#2Gqeh~#oKl8}{TIGo*LaDgP##xSC2GLs-4 zksK`!MCAhsm4WcvL7-iODU6+dOe6~KCp9&lh{Wk<>O#SDB60yjdW1jLKs@^s(6i~G zq88kTRj`%B6#b0i*@1B_5n%^tVx35`+l--*ekz*_7>gvvG9b| k1jek37hp`4~tB>MsW(^z8Tp*cVH z5Sr*tB;W-GGYFc>I_G HT-8g>4p;S}r}?FauXnyx{-Nj4uNVEbw ufyxy3=T?CtD-0x_ zX$h)>_mxs%dkyo$jBP3a@)pEu@DCJ{rWHcFj|=VoQaDgH0$kY$i0={-Se|{z5YH|0 zLh`r*2}~VXDWj)I1Wu&o2#QllmJSQj`H&!;gWR|!A2lRxNT{*oWRHA2$yE7qE QYa wD)sEhb?xY99X}`oR;qQ>Sym zCvJM4obx=%NzECXpf&GoZ0>zTdgl@E{&vT^o{IhJ9Ph4kA-{swko3=K%_-2DZ@R8Z z5KnA>YMUuFXu`}gE#a(FJ$z +GW>e*H^8XL*S||8ex +*_a&ihwOJY_D)8a(sf2l&~cUrqZ(Jb4{6-KDfed5u${9n0X!-< z9f2SMsU`};!Kxa=qDI}J2$WPEeIyxx#Dnrv zNk}me@8lCnHO+`2qq~aJ<4lXiU?dPDW`sH@uZIK?Q<$3Atjqh8$qU9Pvvq5hqe1l` zY`|jJM>26t06nV$R?U7bImX~2;FB`?88x0A#Bh@-4RYO+nhw1*!lu}mF^1!jG2Y~q z3Xg>zH&<#LkF(u|)CHq2D_L_|;1ep-0Z0a& I!=8NcTg`v_R~RLJHb?7Nrp9DjZJ}w6T#)kpz2|=ZZjFA4W0@Z6ULIj z4^1yx7f6Ip!0>YSFuF!HIYGz}&fc(wOSXAT9QKLvBEGF0Yo`pqjuy?tk{KPtF>OY) z!YMM Dq%RwmG=%@;0gZ&34 z7)a6J+NoG1ifnqEB{O5=v^xxzec1B(l1yQVpvjDdv-#qjU^t2?T_bcLXc&Mup3ya~ zJVaS!l#k6?Vd_5?*U1Y&W*Tl*ffGoJV%}3}fvZu}NoO!~_&P8oHHFjj46HqAb7GPB zh(^{FV=H)lp-%C=(Bg5-5DNiYpg6Pe*+noXAfCBkn4_^0n`QDsLAl^=k8pOrII}n= zfinvAM$hO#9>QY`h)-rLvkGxiv1FRh6+R5ig<5Lph0id9aiw5BBg51rc3!MC=9sXi zq{WTdTu{}NIX2V%qAJj7gqEc-LZUR*MunoofFcA&pvM^PTvE;?7;q!Tb+R o!m}{24aWCGXP1Z zvIORceK98mN)|Dv#kCR+i|-L(Tntb-IDuPYgJ5|C23rfDPbQNzyKwgJic }m `x45bry}fvd_D 6pW1M8K&Uei~WS1flVoqDLJN%s>ZM* z)*%8BQdyd;CIpVWk=Qe=ClNIuyaFDL_S7j+5`5_x3TQh{0yjp3lba+Eh8Y?z!Qlf0 zU0y{Ce^#7g6m&B|wj4e`>I4TBy-iRbpc_fPrv`VSCBW*%z?YJU6X0EN^9eu2!0=3* z$dZ`8IAcUcKvv*AFs2NiP=co;d`g11U TGTUlXRZAgt>+PZ&`QatEfT5TejBDVA_C3fC~`xCwo<=t2O&VGugxQBWX(u#v zAOm*Sn3?FxB=O@B@C%$^zEPM!49t~*kCbjNQ+Wq;Z1JBC54nPA*I?u!loh5pj5VD7 zZ^b=JWq^-Uz&vaRXGH)`+XC4Kt{e1foMKR5;2()fVtuFsuznV?im~7ZFDf|)G(rRM zx&$K9VsW~3_h}o6f!`8fZZ9Mc(Ig!jJ{-(|U>Q6OBp8&EKpFc4#><5pkS3}|OezVc z1%f=GhC_ZX=`s>#xy;JtQ!b8j*^?>sfFwsoA|jUs8A*ImXfM`>^b|5+-6o6GpV*E{ zHW;F(rKjOOCEZ1lNNc!kCwC} jGKgxT~pJ0l0xz-w^i6zt*c z1-Bs;7V32FUs9D}qhuI%Le+40%yM_f9q(T9d|gbjecAP#ors8&@Lmdqi0S*n*7F<< zl3YT+{;$%nXQdnR`1LbyAO4{CoyT&g#^w)>{itD19{-=WJ*e@G@?nc`{woaH3 zN(d&=U_M}OA1DM5_Z{ya=pXJq(La2&?`*c8>)SmPzrlkY4kwdiIL3!n99vCBEz;Y8 z&W}TMh-eUSxLy!Lz(y< r5DPKAf;M!1%;hh7)}b6s~5OXVa;@y$3?IdAnIfzgZ3`Vj|v}? zzhtG^G#>s1)mlP=g}vnrRF=&iSzgEeK9A(fPv$Ecmfg-^+3X=05?ec7ee8RW{oq*6 z*@Q@k9bb09`E*~F(hbv;wlYeapQdzOn$nK_PNZ!-l^> ILuHTw(?8=4rz4gpoxHs3eKUaSsUl+=4ePpig zQ5Xl>R#+9v*DVJnkMEmPUz^HRwJ*3gz|@#)*`C{Z_=C0&J!d~Ao59%y&leXxflDX< ziJP@!(xj+aA6hFq6SuG9loU05X!>?&`Zjz!@LlTacPg$o%08D?4GsOX0vPlMOSE@) zQ8O+HEBO8OWqba?qK;422Usn9rq$x}+6Ks8F>U*t?OaOvOj${v!RfMT`{!)yE{2m# zJEon}u4(t{PO)dOGf1I|fshjh{qkNmmf_5vC}j4D6+hv9J_P 840FeIHpLURI`tk{J1w -8X{g} X#hSEww4gH(l}+d{Qf*LtKAhv(c3iQs1e0i7=)V}FF_ ze~&-INp{QWswta2dMoH^1gojNRq1TPcOBS7P207na}S+es2;jm{iV6;FU?nfdG=^N z*mN`4HWzGL2)2`jIoPw{*|S)`;d*QC;inhszjU*HWUhW>zFxWHN3=pkv#9)GuzlGl zwQPH{Eq6Gb-}&&e!`AZ1&mY N-x`~CBumEB^CG2AcX6m(KZ_OyfKP)RXkSkS6wNj(h zzU-ZGTrPv5+;WRuDubcC4u*1DiJ@FFS@eu^+Q~XfOw;w3%iv;bcm?eFav3+0H=1=n zYcw;iDX< E0kz!Nwv17;e zurK5V@vb?nrop6m%$kgNH_}wtpP*Yjt31PFu8JXY?7QMYOUZ-dP~T*Wy(AOts;E;9 zi!qN)7x8L`dQa-?B?B%j{KvEe%r4OAPb6XF{+@VHan^{8KAq&P*!PWcH0a}Wn|I9E z4~pf3C9VSYvMBeS%xLNEJ_^Qq1tr;a27v^4MwV$KC<{C~34`uI%cxDiNzVqC mRhRMnw zi6hc1jL7Zo?7ouK<$UVi5I7fF-Tqif`_;R1Z&0vP!<4K?S3}!(_debm$Y|u5)$Nb0 z-fY9xaBm>AzJwa9Aw9ARJMIl(D&0k~bgKb8yb6u(4S;i%wVIb=+r67}%irqe>n)^2 z!@i3Vx=hj|TZPaU>Dcij#?KHxE&OEgW598qBRI$D->)XmE>PDoO5#XXDC+DPlw@0Z za3UQLeGLu$D3}go)Lf^^Zk*rKI33yQPVSLHs4qGH={kWBs^F{^dawx9hkgw6K#2^( z<&q4+Bc%*i1n#5!BqbLqnW7|1$yX@hsxHND5|OlYU0-p??1y+*jzQ|Tkb$bZ=q&qY z|JV9ou70)QdkxnP-)P!)dF;))1?Pi#|C6%^^G;7Luzk+C-N+uAa~?9XXXcz|W)Chp zy_YsVcl4ICu96hyPEwdF!}*FQ@(7Z=hr%qYw*0zt`OO(H{I+4_NAoS`@-0L7kBFK2 z-JgW&ghm|xERDD`zpf*<@u6HtPp;=!uK9Sr2@3Loxu%0F_2Ewt1G*04MPMC76;JEk z@&c_EmlviX9h6E7#d|p`qYq7`6fSSeEiF`H#rcNI u}QN*G~r6)lH5 `=Za2oFgXA0vdcttpH z{mU@28L|TTG2rNY`js?K%jjDcltZ )2D7v}97Xhec84J) z7)D$GTn(aA79fo8UD Rd*u*PYbhb=)u zWNMEojKD>lCZhz{m9aMAh}Nwnqk1=tlw@Qej~XLqCkNBjCHpe%=mWy%YjW3CQ$6Mz zuqB#IO?8uJTMfw>3WTTludQ3b@u*V*(35qBVfrlCA_d#SJaj2T(eJ-}`46~_71a?a zprAKwBUKbP%(8AH+CSS|jD#{4Pk6t;OG5#{Lg=J2ief2LAEyN;rQEnLht(v;!zSpd zi z2jP(k6Kuf)$hgl5sr?}U(u*9*&|WJzRuu18vVVzcgE)?QFOpg5W2ZOg+qU5BT&!xy zHSV3SdU*D+kE_<-Xgir3jxJOwvyXuh<$Mn>IQK19*5?`y=LVl%(7rOS%`8+tJKO&Y zcgMA !IAPiCl1U z!E @IJ|VpZLh z(U+q0RU4KhdwI{b@qA_D)!>W4Yn!h%U2nhs*!8_{vRqrwLgk*t%DP 4ajOo}CiKDm>gTyHYiR0h9EX!8Z4C)c4t Zq9>eBCo$!7un$1dif6WUI zUw-(efBl?){k&h!Z5f*L4=q-==e9n%P< y z;Rv}9;RpQ6Id`YSiyj42q|1Q9wGHWu?xUj|k!@s%Cadx&tocbq o$}@qAoSA& zL;00}Hd_{=QcFF_0q6<=*4NR^K*34+WXN{s++ny>C=|t*3)Tq1hzwN6SVRpGVGx-h zKJiQP p~tfjiGJ&lPVW zz#{>lY;QUS0C*zqg3I&~#Cc7jxY)U{VMXX_6UDSyi$;3@Qzm9m@ uh3noFaCNY2fu6SU`RZA~6KNjELAjgi9eN4{|B5i%SnF zibX5-bjK3`f|8<={oyLq;EGIqieFppG=+Ut-X#} _uETSC=>+Ijr5!!Hi|H1A< z{ZAe5>yghOR0)a|F!%znJd;9 3&=T}jmU897QXz}ibszV?dE2V5Me%I12 zVy#v_dI#-7E+Ise6CoHj2|=OCICHoVV(LKwvxsO;p ^$1d3Vc86F1zO@{N<1%k$p4o8Fc=Z_B*5^;+u &YG|9E^xZ&NAZ=ASP&IcU+(Aj;rEeOlPKlheq=kR=O zX_jOhL=FAc((GqNLy$AbkammQcMWOR$$pI9*}tdcKTz@?kz@xAf35Hz;?xVN)<7e1 z)t!46I^id_xJ)OUD}u}LH?XYa4`KGN06F9oW=QfAF+yS?`#RXWlz)$s1tf;?RH%4d z>ddyTbq*}S_Z7m;eP{{6x1sIo%!@NO8@A3hY@KiDyxFjGu3_hV!~K^6d4EH$?ev`g zG#7&j^J;qS2;3=03SsyOLl|a%OXJ{{+;+-wF_*TFQSvf%Ws12Dpe{!S{wRva)(6%2 z=k|Jht=4Z8JG309;y~(}a^fWCP-$)K_<%S6?Xd&0rjri<$$65wSAEucAqmW1#<2B3 zQPNdxZ&>ADx8YSLmg%nu@*XW6r7gLBm8~U0hr4hHf=qz%^0(3m akR7E#9*SPDM3*6uxaJy=*3CxIt-Cf+#5% RUrEy<74tF0iGi3N-!XkDzLQ>@}5y2jNwa~F-+hag{8tFlgrNjyg!t)JpocH`ZP zd-q-ucIq2YeOCHq^YEpLd{gVqrtNc0+uv-OZ`yMyn6GHP)|RW-p7U(Kd*#qfSP!W8 ztBAkEO0b_H&yPCpFmp7Z36yHy?B^(0Yjk(7!C%wph}zCdOF_KXh!0^PRL1@_b@Fd0 zp~Eb+Xqq&Ky_(PvCE(=X&j=hDN1l^z* nOSmMsADx+;y+k z!|&CuxStDixy0|)8s8;mZr?zO`gg=s|47b(LIW!=?uhaCfsxi=L|u`rXS@im8 P z=vE7)I% w`6&9Sk60Fb|-^Fq-MWc*sg11a2&+%Ng~obz!*BAu9XP8ivN zIp@GyF3QE~#$40s`RXTTk1p9At|8o=ZGGv&f@dp2cwPQqFS`sgDm{Q5_3#Idw=*9c z%>|#j={cM8oXtCZc+rXt?e6Gg41det>y+McdVBA4yi;D$+wOR$-GzMU0pZVe6ZgTp z)|dlz3jyeQxYms;CoqXKqFjYWLw+Yzx`2QOuHHp6c(ogyK=CRCqC$n>%8EkIXj)lF zFP0;330YdWzOnkbVOpf;6d%ResEEpbllpMd6$-wag>4u#&~L@%X562lS2c>LkCL}F zGV=cwcCX!n!YKG_9HE|SQM_*vDs+s46*$FFrqE_K=B@@k495X~Vbd^F7t(OWi-xwZ zs)hJ}H9BtA3G hzg-zmZkTgNwBY#IxVbiLL<|`+R_ivbW$!%HK zcz0beUV3XJd~K6v)mo~$gR0WXVYkQ_!1ORz#|(E3c7s-$$4Hp%>OOzF6#;+s)b6o+ z^oK m`H|HAk+kdQ z(xV?q5ByXLd?eNXgY?)((o-Kv2S1Wdd?fAX8KlVLzj~{z)>eHh9JbZo8nJaFtJrF7 zXKc4RciAd$C2U)4W42pQ*$&w7{FuGPwsU!(!}gTzrKV+xzHbHEZQGZR*qpXr`%4ck ZOZ2@Qth2Se AAykoPz^{#Jr zHwk39+?`H3=P8Z)5NNN0K9wr>;71<%5UFY#sV|EhDb_S6QB|sRZ&7KbDt+n9+G{w1 z+petn|C#@rZ~yt{pMS@H^839Me&FBUO8?eJQGX>J`*Bs3-JfISF(ps}Em89oNzc<( z-!bpN(jhr>4f72&u{kAI&OPs@DF+4I*?>*9C!bW+Dxzi2VrziTw%Yoh6dDBA*X}3I z$_Rc};}_JtcZrUBz9mDmaT+yT<+VjD-I9{Y0C;10azp~Rfpq4cC00h>u5^u&3 irW(eD zF`+{--WtYTmxrlg8td}-Y8X#l9)AtfWH^lm!)3UQM#Ez?r5mqPLcod&k7pkim4MJ( zo9(TOX{lkDx|m=M db5t!nK*rbup0| zrUhdT+q*WQy@m{zlTI*suYnV`7Otgk+Gn$PSBWSc5A)_trzD_Om^if)yon1#M z7^Nw#-R@^QY;8{pUH9o =G* zTI;sC{x7CjeWp9qUFRHihmQC1f5F9tg@6}02s9naTuPDCVun+JRn@#Key(9NJI%`( z9TzorMiGF-9-p3`iL-eg@;RUZsIqCO
OL|}&r2Y|j fU|08hdOVZYxI9#D zivoZHY>upFfYm@Q53tiZ1bB?U%W29-KvuCq$#eMuRvy4h1Rv`_PAzeHAoG&86tAxA z8ZW9~pF0zH(X)y+q3E)35kdtf*vWLAC9$H)%8JIO33%v0cyN4G0EfQA0Rfr9%Pb(w zWMQhk(8EGcV*M?t+la~Gm{j+7*`;gg!BhCN=fMk71k8ZEl9a2O5kqt(Pqt7+zWo71 zE-PS&*%vV6-3u6USph?|jDl|j47J_DUs*1-i1Q|e(?(P^F{Pp=YgAKYfLbh7068TC z`TU}ovW-i5fc%zGs%9c;wYrL~<#mm#d|i?64!DI=Sp_^yEn?4s!bwWX%0<*%8RcpO zjI8GfSeG;vc{MQyIEcvsY8K^`qzix(6cRKQF{+poB_47*l$K-T)*0EK;(J?|V3*&v z_RBg|WM>YfpLI|wJ7t}Y1UtJN8 mVi7QN6r(+ zp+@YrBHae45$`{cvHQLXWhG;fuj1lWD}PV}IpD~d0UGk)CM_wvhTK>aWeo-2$X0S0 zp^(eUFXXZogPJbhO#v$(;t>?6yvr^Uh^h9;;YIFB2a;O^Bv&~|zJ-un*&zAGLvl@n z (miq@}*a2UUuEJq!| z=jv1HRri1^@G=tp>eSEV7m ym_p^JolW~VcJ*EY%}q4M{M1@=G_=A4wX8_3f<*k`{U@N=<3Yo zd!^uTAzW_nd3^uT{f(7kuGD^^&{d9fKh8bMZOjx$N|AGgD8ctW>fPumHkN{?Fy0?} zxbR?Mb#$|@ ?k z`!+t=_P@JR@yF~tz4d;n >5J`zy3N>7v1x16oJyLZr0GlU_`6o|I|p~V zj;trv5*u<+E_GcjOqXLv*RyNc&6s(5sua6ixKfU?>w{~9n}IEVDLPTOT<+{$kFG^G zW{cCM&auMOp%ePo`Zs=3Tqs4y3zv67y&Jv7x3-p_oi&pgGn6rXnO7kyF!Zl~L?~a+ z-zf*)QM^OG2R^g?-Qq{Z1v5PU+;gGq@xNr+Uobt}%mL8EQ}rn@!_&__GZ^&mmrUD1 zl2q|SGyMK@&j_)a0BcnsTa*Ev$mr`6OoV*|) zx2g%aTv`eI8)_F%r66(PwPyqubzK5y;3)2r{~PK#+`MwqG`;JgXz!mX{0n_Ub$mm0 vmMQ Dict[str, Any]: + """ + Research a topic using web crawling and AI analysis. + + Args: + topic (str): The topic to research + + Returns: + Dict[str, Any]: Research results including overview, findings, and recommendations + """ + try: + logger.info(f"[research_topic] Starting research for topic: {topic}") + + # Initialize web crawler + async def analyze_topic(): + async with AsyncWebCrawlerService() as crawler: + # Perform web research + search_results = await crawler.crawl_website(topic) + + if not search_results.get('success'): + return { + 'success': False, + 'error': search_results.get('error', 'Research failed') + } + + # Analyze content with LLM + analysis = await crawler.analyze_content_with_llm( + search_results['content'], + api_key=None, # Should be passed from config + gpt_provider="google" # Should be configurable + ) + + # Structure the response + return { + 'success': True, + 'data': { + 'research': { + 'overview': { + 'topic': topic, + 'scope': analysis.get('topics', []), + 'methodology': 'Web crawling and AI analysis' + }, + 'data_quality': { + 'is_reliable': bool(analysis.get('seo_score', 0) > 0.7) + }, + 'analysis_quality': { + 'is_thorough': bool(len(analysis.get('key_insights', [])) > 5) + }, + 'recommendations': analysis.get('recommendations', []), + 'next_steps': analysis.get('priority_areas', []) + } + } + } + + # Run the async analysis + results = asyncio.run(analyze_topic()) + + if not results.get('success'): + error_msg = results.get('error', 'Research failed') + logger.error(f"[research_topic] Research failed: {error_msg}") + return { + 'success': False, + 'error': error_msg + } + + logger.info("[research_topic] Research completed successfully") + return results + + except Exception as e: + error_msg = f"Research failed: {str(e)}" + logger.error(f"[research_topic] {error_msg}") + return { + 'success': False, + 'error': str(e) + } \ No newline at end of file diff --git a/lib/utils/alwrity_sidebar.py b/lib/utils/alwrity_sidebar.py new file mode 100644 index 00000000..8665bead --- /dev/null +++ b/lib/utils/alwrity_sidebar.py @@ -0,0 +1,244 @@ +import streamlit as st +import logging + +from .config_manager import save_config + +# Configure logging +logging.basicConfig( + level=logging.DEBUG, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + handlers=[ + logging.StreamHandler(), # Output to console + #logging.FileHandler('alwrity.log') # Output to file + ] +) +logger = logging.getLogger(__name__) + +# Sidebar configuration +def sidebar_configuration(): + """Configure the sidebar with all necessary options.""" + try: + # Configure sidebar styling + st.sidebar.markdown(""" + + """, unsafe_allow_html=True) + + logger.info("Initializing sidebar configuration") + st.sidebar.title("π οΈ Personalization & Settings ποΈ") + + with st.sidebar.expander("**π· Content Personalization**"): + logger.debug("Setting up content personalization options") + blog_length = st.text_input("**Content Length (words)**", value="2000", + help="Approximate word count for blogs. Note: Actual length may vary based on GPT provider and max token count.") + + blog_tone_options = ["Casual", "Professional", "How-to", "Beginner", "Research", "Programming", "Social Media", "Customize"] + blog_tone = st.selectbox("**Content Tone**", + options=blog_tone_options, + help="Select the desired tone for the blog content.") + logger.debug(f"Selected blog tone: {blog_tone}") + + if blog_tone == "Customize": + custom_tone = st.text_input("Enter the tone of your content", help="Specify the tone of your content.") + if custom_tone: + blog_tone = custom_tone + logger.debug(f"Custom tone set to: {custom_tone}") + else: + logger.warning("Custom tone not specified") + st.warning("Please specify the tone of your content.") + + blog_demographic_options = ["Professional", "Gen-Z", "Tech-savvy", "Student", "Digital Marketing", "Customize"] + + blog_demographic = st.selectbox("**Target Audience**", + options=blog_demographic_options, + help="Select the primary audience for the blog content.") + if blog_demographic == "Customize": + custom_demographic = st.text_input("Enter your target audience", + help="Specify your target audience.", + placeholder="Eg. Domain expert, Content creator, Financial expert etc..") + if custom_demographic: + blog_demographic = custom_demographic + else: + st.warning("Please specify your target audience.") + + blog_type = st.selectbox("**Content Type**", + options=["Informational", "Commercial", "Company", "News", "Finance", "Competitor", "Programming", "Scholar"], + help="Select the category that best describes the blog content.") + + blog_language = st.selectbox("**Content Language**", + options=["English", "Spanish", "German", "Chinese", "Arabic", "Nepali", "Hindi", "Hindustani", "Customize"], + help="Select the language in which the blog will be written.") + if blog_language == "Customize": + custom_lang = st.text_input("Enter the language of your choice", help="Specify the content language.") + if custom_lang: + blog_language = custom_lang + else: + st.warning("Please specify the language of your content.") + + blog_output_format = st.selectbox("**Content Output Format**", + options=["markdown", "HTML", "plaintext"], + help="Select the format for the blog output.") + + with st.sidebar.expander("**π©» Images Personalization**"): + image_generation_model = st.selectbox("**Image Generation Model**", + options=["stable-diffusion", "dalle2", "dalle3"], + help="Select the model to generate images for the blog.") + number_of_blog_images = st.number_input("**Number of Blog Images**", value=1, help="Specify the number of images to include in the blog.") + + with st.sidebar.expander("**π€ LLM Personalization**"): + gpt_provider = st.selectbox("**GPT Provider**", + options=["google", "openai", "minstral"], + help="Select the provider for the GPT model.") + model = st.text_input("**Model**", value="gemini-1.5-flash-latest", help="Specify the model version to use from the selected provider.") + temperature = st.slider( + "Temperature", + min_value=0.1, + max_value=1.0, + value=0.7, + step=0.1, + format="%.1f", + help="""Temperature controls the 'creativity' or randomness of the text generated by GPT. + Greater determinism with higher values indicating more randomness.""" + ) + + top_p = st.slider( + "Top-p", + min_value=0.0, + max_value=1.0, + value=0.9, + step=0.1, + format="%.1f", + help="Top-p sampling controls the level of diversity in the generated text." + ) + + # Selectbox for max tokens + max_tokens_options = [500, 1000, 2000, 4000, 16000, 32000, 64000] + max_tokens = st.selectbox( + "Max Tokens", + options=max_tokens_options, + index=max_tokens_options.index(4000), + help="Max tokens determine the maximum length of the output sequence generated by a model." + ) + n = st.number_input("N", + value=1, + min_value=1, + max_value=10, + help="Defines the number of words or characters grouped together in a sequence when analyzing text.") + frequency_penalty = st.slider( + "Frequency Penalty", + min_value=0.0, + max_value=2.0, + value=1.0, + step=0.1, + format="%.1f", + help="Influences word selection during text generation, promoting diversity with higher values." + ) + + presence_penalty = st.slider( + "Presence Penalty", + min_value=0.0, + max_value=2.0, + value=1.0, + step=0.1, + format="%.1f", + help="Encourages the use of diverse words by discouraging repetition." + ) + + with st.sidebar.expander("**π΅οΈ Search Engine Personalization**"): + geographic_location = st.selectbox("**Geographic Location**", + options=["us", "in", "fr", "cn"], + help="Select the geographic location for tailoring search results.") + search_language = st.selectbox("**Search Language**", + options=["en", "zn-cn", "de", "hi"], + help="Select the language for the search results.") + number_of_results = st.number_input("**Number of Results**", + value=10, + max_value=20, + min_value=1, + help="Specify the number of search results to retrieve.") + time_range = st.selectbox("**Time Range**", + options=["anytime", "past day", "past week", "past month", "past year"], + help="Select the time range for filtering search results.") + include_domains = st.text_input("**Include Domains**", value="", + help="List specific domains to include in search results. Leave blank to include all domains.") + similar_url = st.text_input("**Similar URL**", value="", help="Provide a URL to find similar results. Leave blank if not needed.") + + # Storing collected inputs in a dictionary + config = { + "Blog Content Characteristics": { + "Blog Length": blog_length, + "Blog Tone": blog_tone, + "Blog Demographic": blog_demographic, + "Blog Type": blog_type, + "Blog Language": blog_language, + "Blog Output Format": blog_output_format + }, + "Blog Images Details": { + "Image Generation Model": image_generation_model, + "Number of Blog Images": number_of_blog_images + }, + "LLM Options": { + "GPT Provider": gpt_provider, + "Model": model, + "Temperature": temperature, + "Top-p": top_p, + "Max Tokens": max_tokens, + "N": n, + "Frequency Penalty": frequency_penalty, + "Presence Penalty": presence_penalty + }, + "Search Engine Parameters": { + "Geographic Location": geographic_location, + "Search Language": search_language, + "Number of Results": number_of_results, + "Time Range": time_range, + "Include Domains": include_domains, + "Similar URL": similar_url + } + } + + # Writing the configuration to a file whenever a change is made + save_config(config) + except Exception as e: + logger.error(f"Error configuring sidebar: {str(e)}") + st.error(f"Error configuring sidebar: {str(e)}") \ No newline at end of file diff --git a/lib/utils/alwrity_utils.py b/lib/utils/alwrity_utils.py index b37d623b..dd80f33a 100644 --- a/lib/utils/alwrity_utils.py +++ b/lib/utils/alwrity_utils.py @@ -8,7 +8,7 @@ from lib.ai_writers.keywords_to_blog_streamlit import write_blog_from_keywords from lib.ai_writers.speech_to_blog.main_audio_to_blog import generate_audio_blog from lib.ai_writers.long_form_ai_writer import long_form_generator from lib.ai_writers.ai_news_article_writer import ai_news_generation -from lib.ai_writers.ai_agents_crew_writer import ai_agents_writers +#from lib.ai_writers.ai_agents_crew_writer import ai_agents_writers from lib.ai_writers.ai_financial_writer import write_basic_ta_report from lib.ai_writers.facebook_ai_writer import facebook_post_writer from lib.ai_writers.linkedin_ai_writer import linked_post_writer @@ -24,8 +24,8 @@ import tiktoken import openai from lib.gpt_providers.text_to_image_generation.main_generate_image_from_prompt import generate_image from lib.utils.voice_processing import record_voice -from lib.content_planning_calender.content_planning_agents_alwrity_crew import ai_agents_content_planner -from ..gpt_providers.text_generation.main_text_generation import llm_text_gen +#from lib.content_planning_calender.content_planning_agents_alwrity_crew import ai_agents_content_planner +from lib.gpt_providers.text_generation.main_text_generation import llm_text_gen def is_youtube_link(text): @@ -292,9 +292,9 @@ def ai_agents_team(): if plan_keywords and len(plan_keywords.split()) >= 2: with st.spinner("Get Content Plan..."): try: - plan_content = ai_agents_content_planner(plan_keywords) - st.success(f"Successfully generated content plan for: {plan_keywords}") - st.markdown(plan_content) + #plan_content = ai_agents_content_planner(plan_keywords) + st.success(f"Coming soon: Content plan for: {plan_keywords}") + #st.markdown(plan_content) except Exception as err: st.error(f"Failed to generate content plan: {err}") else: diff --git a/lib/utils/api_key_manager.py b/lib/utils/api_key_manager.py deleted file mode 100644 index 8be43195..00000000 --- a/lib/utils/api_key_manager.py +++ /dev/null @@ -1,69 +0,0 @@ -import os -import streamlit as st -from dotenv import load_dotenv - -def check_all_api_keys(): - """ - Checks if all required API keys are present in the environment variables. - Prompts the user to enter missing keys and saves them in the .env file. - This includes general API keys and the LLM provider key. - """ - # Load environment variables from .env (MUST COME FIRST) - load_dotenv() - - api_keys = { - "METAPHOR_API_KEY": "https://dashboard.exa.ai/login", - "TAVILY_API_KEY": "https://tavily.com/#api", - "SERPER_API_KEY": "https://serper.dev/signup", - "STABILITY_API_KEY": "https://platform.stability.ai/", - "FIRECRAWL_API_KEY": "https://www.firecrawl.dev/account" - } - - # Check for missing keys AFTER loading environment variables - missing_keys = { - key: url for key, url in api_keys.items() if os.getenv(key) is None - } - - gpt_provider = os.getenv("GPT_PROVIDER") - supported_providers = { - 'google': "GEMINI_API_KEY", - 'openai': "OPENAI_API_KEY", - 'mistral': "MISTRAL_API_KEY" - } - if not gpt_provider or gpt_provider.lower() not in supported_providers: - gpt_provider = st.selectbox( - "Select your LLM Provider", options=list(supported_providers.keys()) - ) - os.environ["GPT_PROVIDER"] = gpt_provider - try: - with open(".env", "a") as env_file: - env_file.write(f"GPT_PROVIDER={gpt_provider}\n") - except IOError as e: - st.error(f"Failed to write GPT_PROVIDER to .env file: {e}") - st.success(f"GPT Provider set to {gpt_provider}") - - api_key_var = supported_providers[gpt_provider.lower()] - if not os.getenv(api_key_var): - missing_keys[api_key_var] = '' - - # If there are missing keys, prompt the user to enter them - if missing_keys: - st.warning(f"API keys not found: {', '.join(missing_keys)}. Please provide them below. Restart the app after saving the keys.") - with st.form(key='api_keys_form'): - # Gather all missing keys in one go - for key, url in missing_keys.items(): - if url: - st.text_input(f"{key}: π[Get it here]({url})π", type="password", key=key) - else: - st.text_input(f"{key}:", type="password", key=key) - - # Save all keys at once when the button is clicked - if st.form_submit_button("Save Keys"): - with open(".env", "a") as env_file: - for key in missing_keys: - key_value = st.session_state[key] - env_file.write(f"{key}={key_value}\n") - st.success("API keys saved successfully! Please restart the application.") - st.stop() - return False - return True diff --git a/lib/utils/api_key_manager/README.md b/lib/utils/api_key_manager/README.md new file mode 100644 index 00000000..1a6f9b26 --- /dev/null +++ b/lib/utils/api_key_manager/README.md @@ -0,0 +1,159 @@ +# ALwrity Setup Guide: API Key Manager + +## What is the API Key Manager? + +The API Key Manager is a crucial component of ALwrity that helps you set up and configure all the necessary API keys and settings for your content creation workflow. It provides a user-friendly wizard interface to guide you through the setup process step by step. + +## Setup Wizard Steps + +### 1. Website Setup +- **Purpose**: Configure your website's basic information +- **Features**: + - Website URL configuration + - Site structure setup + - Basic SEO settings + - Content organization preferences + +### 2. AI Research Setup +- **Purpose**: Set up AI-powered research capabilities +- **Features**: + - Research parameters configuration + - Data collection preferences + - Analysis settings + - Research depth options + +### 3. AI Providers Configuration +- **Purpose**: Configure AI service providers +- **Supported Providers**: + - OpenAI (GPT models) + - Google (Gemini Pro) + - Anthropic (Claude) + - DeepSeek +- **Features**: + - API key management + - Model selection + - Usage preferences + - Cost optimization settings + +### 4. Personalization Setup +- **Purpose**: Customize your content creation experience +- **Features**: + - Writing style preferences + - Tone settings + - Content structure templates + - Brand voice configuration + +### 5. ALwrity Integrations +- **Purpose**: Set up additional tools and services +- **Features**: + - Third-party service connections + - Plugin configurations + - API integrations + - Workflow automation settings + +### 6. Final Setup +- **Purpose**: Complete and verify your configuration +- **Features**: + - Configuration review + - Settings verification + - Test connections + - Setup completion + +## How to Use the Setup Wizard + +### 1. Starting the Setup +1. Launch ALwrity +2. Navigate to the Setup section +3. Begin the wizard process + +### 2. Navigation +- Use the step indicator to track progress +- Navigate between steps using buttons +- Save progress automatically +- Return to previous steps if needed + +### 3. Configuration Process +1. **Enter Information**: Fill in required details +2. **Verify Settings**: Review your inputs +3. **Test Connections**: Ensure everything works +4. **Complete Setup**: Finalize your configuration + +## Managing API Keys + +### 1. Key Storage +- Secure storage of API keys +- Environment variable management +- Key rotation support +- Access control + +### 2. Key Validation +- Automatic key verification +- Usage monitoring +- Error handling +- Expiration tracking + +### 3. Security Features +- Encrypted storage +- Access logging +- Permission management +- Secure transmission + +## Progress Tracking + +### 1. Setup Progress +- Visual progress indicator +- Step completion tracking +- Overall setup status +- Remaining tasks + +### 2. Status Monitoring +- API key status +- Connection status +- Configuration status +- Error reporting + +## Best Practices + +### 1. Before Setup +- Gather all necessary API keys +- Review provider documentation +- Plan your configuration +- Backup existing settings + +### 2. During Setup +- Follow the wizard steps +- Verify each configuration +- Test connections +- Save progress regularly + +### 3. After Setup +- Review all settings +- Test functionality +- Document configurations +- Monitor usage + +## Troubleshooting + +### 1. Common Issues +- Invalid API keys +- Connection problems +- Configuration errors +- Setup interruptions + +### 2. Solutions +- Key verification +- Connection testing +- Error logging +- Support resources + +## Need Help? + +If you encounter any issues during setup: +1. Check the error messages +2. Review the documentation +3. Verify your API keys +4. Contact ALwrity support + +--- + +*Note: Keep your API keys secure and never share them. The API Key Manager helps you manage these keys safely while setting up ALwrity for optimal content creation.* \ No newline at end of file diff --git a/lib/utils/api_key_manager/__init__.py b/lib/utils/api_key_manager/__init__.py new file mode 100644 index 00000000..5c04ffa0 --- /dev/null +++ b/lib/utils/api_key_manager/__init__.py @@ -0,0 +1,37 @@ +"""API key manager package.""" + +from .manager import APIKeyManager +from .api_key_manager import ( + initialize_wizard_state, + update_progress, + check_all_api_keys, + render, + render_navigation +) +from .components import ( + render_website_setup, + render_ai_research_setup, + render_ai_providers, + render_final_setup, + render_personalization_setup, + render_alwrity_integrations, + render_navigation_buttons, + render_step_indicator +) + +__all__ = [ + 'APIKeyManager', + 'initialize_wizard_state', + 'update_progress', + 'check_all_api_keys', + 'render', + 'render_navigation', + 'render_website_setup', + 'render_ai_research_setup', + 'render_ai_providers', + 'render_final_setup', + 'render_personalization_setup', + 'render_alwrity_integrations', + 'render_navigation_buttons', + 'render_step_indicator' +] \ No newline at end of file diff --git a/lib/utils/api_key_manager/ai_research.py b/lib/utils/api_key_manager/ai_research.py new file mode 100644 index 00000000..ec929e38 --- /dev/null +++ b/lib/utils/api_key_manager/ai_research.py @@ -0,0 +1,42 @@ +"""AI research functionality for API key manager.""" + +from loguru import logger +import asyncio +from typing import Dict, Any, Optional + +async def research_topic(topic: str, api_keys: Dict[str, str]) -> Dict[str, Any]: + """ + Research a topic using available AI services. + + Args: + topic (str): The topic to research + api_keys (Dict[str, str]): Dictionary of API keys for different services + + Returns: + Dict[str, Any]: Research results and metadata + """ + try: + logger.info(f"Starting research on topic: {topic}") + + # TODO: Implement actual research functionality using available API keys + # This is a placeholder implementation + results = { + "topic": topic, + "status": "success", + "data": { + "summary": f"Research summary for {topic}", + "key_points": ["Point 1", "Point 2", "Point 3"], + "sources": ["Source 1", "Source 2"] + } + } + + logger.info("Research completed successfully") + return results + + except Exception as e: + logger.error(f"Error during research: {str(e)}") + return { + "topic": topic, + "status": "error", + "error": str(e) + } \ No newline at end of file diff --git a/lib/utils/api_key_manager/api_key_manager.py b/lib/utils/api_key_manager/api_key_manager.py new file mode 100644 index 00000000..9dac54ca --- /dev/null +++ b/lib/utils/api_key_manager/api_key_manager.py @@ -0,0 +1,165 @@ +"""API key manager for handling various API keys.""" + +from typing import Dict, Any, Optional +from loguru import logger +import streamlit as st +import os +import json +import sys +from datetime import datetime +from dotenv import load_dotenv +from .components.website_setup import render_website_setup +from .components.ai_research_setup import render_ai_research_setup +from .components.ai_providers import render_ai_providers +from .components.final_setup import render_final_setup +from .components.personalization_setup import render_personalization_setup +from .components.alwrity_integrations import render_alwrity_integrations +from .components.base import render_navigation_buttons, render_step_indicator +from .wizard_state import initialize_wizard_state, get_current_step, next_step, previous_step +from .manager import APIKeyManager +from .validation import check_all_api_keys + +# Configure logger to output to both file and stdout +logger.remove() # Remove default handler +logger.add("logs/api_key_manager.log", + format=" {time:YYYY-MM-DD HH:mm:ss} |{level: <8} |{name} :{function} :{line} -{message} ", + level="DEBUG") +logger.add(sys.stdout, + format="{time:YYYY-MM-DD HH:mm:ss} |{level: <8} |{name} :{function} :{line} -{message} ", + level="INFO") + +def initialize_wizard_state(): + """Initialize or get the wizard state from session""" + logger.debug("Initializing wizard state") + if 'wizard_state' not in st.session_state: + st.session_state.wizard_state = { + 'current_step': 0, + 'total_steps': 0, + 'completed_steps': set(), + 'api_keys_status': {}, + 'setup_progress': 0 + } + logger.info("Created new wizard state") + +def update_progress(): + """Update the overall setup progress""" + logger.debug("Updating setup progress") + try: + # Get the API key manager instance from session state + api_key_manager = st.session_state.get('api_key_manager') + if not api_key_manager: + logger.warning("API key manager not found in session state") + return + + total_keys = sum(len(keys) for keys in api_key_manager.api_key_groups.values()) + configured_keys = sum(1 for status in st.session_state.wizard_state['api_keys_status'].values() + if status.get('configured', False)) + progress = (configured_keys / total_keys) * 100 + st.session_state.wizard_state['setup_progress'] = progress + logger.info(f"Updated progress to {progress:.1f}%") + except Exception as e: + logger.error(f"Error updating progress: {str(e)}", exc_info=True) + +def render(api_key_manager: APIKeyManager) -> Dict[str, Any]: + """ + Render the API key manager interface. + + Returns: + Dict[str, Any]: Current state + """ + try: + logger.info("[render] Rendering API key manager interface") + + # Initialize session state for current step if not exists + if "current_step" not in st.session_state: + st.session_state.current_step = 1 + logger.info("[render] Initialized current_step to 1") + + # Display step indicator + render_step_indicator(st.session_state.current_step, 6) + + # Render appropriate step based on current_step + if st.session_state.current_step == 1: + logger.info("[render] Rendering AI providers setup") + return render_ai_providers(api_key_manager) + elif st.session_state.current_step == 2: + logger.info("[render] Rendering website setup") + return render_website_setup(api_key_manager) + elif st.session_state.current_step == 3: + logger.info("[render] Rendering AI Research setup") + return render_ai_research_setup(api_key_manager) + elif st.session_state.current_step == 4: + logger.info("[render] Rendering personalization setup") + return render_personalization_setup(api_key_manager) + elif st.session_state.current_step == 5: + logger.info("[render] Rendering ALwrity integrations setup") + return render_alwrity_integrations(api_key_manager) + elif st.session_state.current_step == 6: + logger.info("[render] Rendering final setup") + return render_final_setup(api_key_manager) + + except Exception as e: + error_msg = f"Error in API key manager: {str(e)}" + logger.error(f"[render] {error_msg}") + st.error(error_msg) + return {"current_step": st.session_state.current_step, "error": error_msg} + +def render_navigation(self): + """Render navigation buttons with proper state handling""" + st.markdown(""" + ", unsafe_allow_html=True) diff --git a/lib/utils/api_key_manager/components.py b/lib/utils/api_key_manager/components.py new file mode 100644 index 00000000..9b18a82e --- /dev/null +++ b/lib/utils/api_key_manager/components.py @@ -0,0 +1,76 @@ +"""API key manager components.""" + +import asyncio +import streamlit as st +import os +from loguru import logger +from .styles import API_KEY_MANAGER_STYLES +from .config import FEATURE_PREVIEWS, API_KEY_CONFIGS +from .wizard_state import ( + get_current_step, + next_step, + previous_step, + set_selected_providers, + get_selected_providers, + set_website_url, + get_website_url, + set_api_key, + get_api_key, + can_proceed_to_next_step, + get_api_keys +) +from .health_monitor import APIKeyHealthMonitor +from .key_rotation import KeyRotationManager +from ...utils.website_analyzer import analyze_website +from .api_key_tests import ( + test_openai_api_key, + test_gemini_api_key, + test_anthropic_api_key, + test_deepseek_api_key, + test_mistral_api_key +) +from .components.base import render_step_indicator, render_navigation_buttons, render_success_message +from .components import ( + render_ai_providers, + render_website_setup, + render_health_monitoring, + render_ai_research_setup, + render_final_setup +) + +def render_wizard(): + """Render the main wizard interface.""" + st.title("API Key Setup Wizard") + + # Get current step + current_step = get_current_step() + + # Render step indicator + render_step_indicator() + + # Render current step content + if current_step == 1: + render_ai_providers() + elif current_step == 2: + render_website_setup() + elif current_step == 3: + render_ai_research_setup() + elif current_step == 4: + render_final_setup() + elif current_step == 5: + render_health_monitoring() + + # Render navigation buttons + render_navigation_buttons() + +__all__ = [ + 'render_wizard', + 'render_step_indicator', + 'render_navigation_buttons', + 'render_success_message', + 'render_ai_providers', + 'render_website_setup', + 'render_ai_research_setup', + 'render_health_monitoring', + 'render_final_setup' +] \ No newline at end of file diff --git a/lib/utils/api_key_manager/components/README.md b/lib/utils/api_key_manager/components/README.md new file mode 100644 index 00000000..ce3dac14 --- /dev/null +++ b/lib/utils/api_key_manager/components/README.md @@ -0,0 +1,178 @@ +# ALwrity Setup Components Guide + +## Overview + +The ALwrity Setup Components are the building blocks that guide you through setting up your content creation environment. Each component is designed to help you configure specific aspects of ALwrity for optimal content creation. + +## Core Components + +### 1. Website Setup (`website_setup.py`) +**Purpose**: Configure your website's basic information and analyze its current state + +**Features**: +- **URL Configuration**: Set up your website's URL +- **Analysis Options**: + - Basic Analysis: Quick overview of your website + - Full Analysis with SEO: Comprehensive website and SEO analysis +- **Analysis Results**: + - Basic Metrics: Status, content type, title, meta description + - Content Analysis: Word count, headings, images, links + - SEO Analysis: SEO score, meta tags, content quality + - Technical SEO: Mobile friendliness, page speed, technical issues + - Strategy Recommendations: Actionable improvements + +### 2. AI Research Setup (`ai_research_setup.py`) +**Purpose**: Configure AI-powered research tools for content creation + +**Features**: +- **Traditional Search**: + - SerpAPI integration for real-time search results + - Access to structured data and knowledge graphs + - News articles and related questions + +- **AI Deep Research**: + - Tavily AI for semantic understanding + - Metaphor/Exa for neural search capabilities + - Advanced research features + +### 3. AI Providers (`ai_providers.py`) +**Purpose**: Set up your preferred AI content generation services + +**Supported Providers**: +- **OpenAI (GPT models)** + - Advanced language models + - Creative content generation + - Context-aware responses + +- **Google (Gemini Pro)** + - Balanced content creation + - Factual accuracy + - Multilingual support + +- **Anthropic (Claude)** + - Professional writing + - Detailed analysis + - Ethical considerations + +- **DeepSeek** + - Technical content + - Specialized knowledge + - Efficient processing + +### 4. Personalization Setup (`personalization_setup.py`) +**Purpose**: Customize your content creation experience + +**Features**: +- **Writing Style**: + - Tone preferences + - Voice settings + - Content structure + +- **Brand Configuration**: + - Brand voice + - Style guidelines + - Content templates + +### 5. ALwrity Integrations (`alwrity_integrations.py`) +**Purpose**: Connect additional tools and services + +**Features**: +- **Third-party Services**: + - Analytics integration + - Social media tools + - Content management systems + +- **Workflow Automation**: + - Publishing tools + - Content scheduling + - Distribution channels + +### 6. Final Setup (`final_setup.py`) +**Purpose**: Complete and verify your configuration + +**Features**: +- **Configuration Review**: + - Settings verification + - Connection testing + - Setup completion + +- **Validation**: + - API key verification + - Service connectivity + - System readiness + +## Base Components + +### 1. Navigation (`base.py`) +**Purpose**: Provide consistent navigation throughout the setup process + +**Features**: +- Step indicators +- Navigation buttons +- Progress tracking +- Back/forward controls + +## How to Use the Components + +### 1. Starting the Setup +1. Launch ALwrity +2. Navigate to the Setup section +3. Follow the guided wizard process + +### 2. Component Navigation +- Use the step indicator to track progress +- Navigate between components using buttons +- Save progress automatically +- Return to previous steps if needed + +### 3. Configuration Process +1. **Enter Information**: Fill in required details +2. **Verify Settings**: Review your inputs +3. **Test Connections**: Ensure everything works +4. **Complete Setup**: Finalize your configuration + +## Best Practices + +### 1. Before Setup +- Gather all necessary API keys +- Review provider documentation +- Plan your configuration +- Backup existing settings + +### 2. During Setup +- Follow the wizard steps +- Verify each configuration +- Test connections +- Save progress regularly + +### 3. After Setup +- Review all settings +- Test functionality +- Document configurations +- Monitor usage + +## Troubleshooting + +### 1. Common Issues +- Invalid API keys +- Connection problems +- Configuration errors +- Setup interruptions + +### 2. Solutions +- Key verification +- Connection testing +- Error logging +- Support resources + +## Need Help? + +If you encounter any issues during setup: +1. Check the error messages +2. Review the documentation +3. Verify your API keys +4. Contact ALwrity support + +--- + +*Note: Each component is designed to help you set up a specific aspect of ALwrity. Follow the setup wizard in order to ensure all components are properly configured for optimal content creation.* \ No newline at end of file diff --git a/lib/utils/api_key_manager/components/__init__.py b/lib/utils/api_key_manager/components/__init__.py new file mode 100644 index 00000000..97061d65 --- /dev/null +++ b/lib/utils/api_key_manager/components/__init__.py @@ -0,0 +1,20 @@ +"""API key manager components package.""" + +from .website_setup import render_website_setup +from .ai_research_setup import render_ai_research_setup +from .ai_providers import render_ai_providers +from .final_setup import render_final_setup +from .personalization_setup import render_personalization_setup +from .alwrity_integrations import render_alwrity_integrations +from .base import render_navigation_buttons, render_step_indicator + +__all__ = [ + 'render_website_setup', + 'render_ai_research_setup', + 'render_ai_providers', + 'render_final_setup', + 'render_personalization_setup', + 'render_alwrity_integrations', + 'render_navigation_buttons', + 'render_step_indicator' +] \ No newline at end of file diff --git a/lib/utils/api_key_manager/components/ai_providers.py b/lib/utils/api_key_manager/components/ai_providers.py new file mode 100644 index 00000000..c55aae6c --- /dev/null +++ b/lib/utils/api_key_manager/components/ai_providers.py @@ -0,0 +1,225 @@ +"""AI providers setup component.""" + +import streamlit as st +from loguru import logger +from typing import Dict, Any +from ..manager import APIKeyManager +from .base import render_navigation_buttons, render_step_indicator, render_tab_style +from ..wizard_state import next_step, update_progress +from datetime import datetime + +def validate_api_key(key: str) -> bool: + """Validate if an API key is properly formatted.""" + if not key: + return False + # Basic validation - check if key is not empty and has minimum length + return len(key.strip()) > 0 + +def render_ai_providers(api_key_manager: APIKeyManager) -> Dict[str, Any]: + """Render the AI providers setup step.""" + logger.info("[render_ai_providers] Starting AI providers setup") + try: + # Store API key manager in session state for update_progress + st.session_state['api_key_manager'] = api_key_manager + + # Main content + st.markdown(""" +++ """, unsafe_allow_html=True) + + # Create tabs for different AI providers + tabs = st.tabs(["Primary Providers", "Additional Providers"]) + + # Track if any changes were made + changes_made = False + has_valid_key = False + validation_message = "" + + with tabs[0]: + st.markdown("### Primary AI Providers") + st.markdown("Configure the main AI providers for content creation") + + # Create a grid layout for AI provider cards + col1, col2 = st.columns(2) + + with col1: + # OpenAI Card + with st.container(): + st.markdown(""" +π€ AI Providers Setup
+Configure your AI service providers for content generation
++", unsafe_allow_html=True) + + with col2: + # Google Card + with st.container(): + st.markdown(""" ++ ++OpenAI++Power your content with GPT-4 and GPT-3.5 models
++ """, unsafe_allow_html=True) + + openai_key = st.text_input( + "OpenAI API Key", + type="password", + key="openai_key", + help="Enter your OpenAI API key" + ) + + if openai_key: + if validate_api_key(openai_key): + st.markdown(""" ++ β API key configured ++ """, unsafe_allow_html=True) + else: + st.markdown(""" ++ β οΈ Invalid API key format ++ """, unsafe_allow_html=True) + + with st.expander("π How to get your OpenAI API key", expanded=False): + st.markdown(""" + **Step-by-step guide:** + 1. Go to [OpenAI's website](https://platform.openai.com) + 2. Sign up or log in to your account + 3. Navigate to the API section + 4. Click "Create new secret key" + 5. Copy the generated key and paste it here + + **Note:** Keep your API key secure and never share it publicly. + """) + + st.markdown("+", unsafe_allow_html=True) + + with tabs[1]: + st.markdown("### Additional AI Providers") + st.markdown("Configure additional AI providers for enhanced capabilities") + + # Create a grid layout for additional provider cards + col1, col2 = st.columns(2) + + with col1: + # Anthropic Card (Coming Soon) + with st.container(): + st.markdown(""" ++ ++Google Gemini++Leverage Google's powerful Gemini models
++ """, unsafe_allow_html=True) + + google_key = st.text_input( + "Google API Key", + type="password", + key="google_key", + help="Enter your Google API key" + ) + + if google_key: + if validate_api_key(google_key): + st.markdown(""" ++ β API key configured ++ """, unsafe_allow_html=True) + else: + st.markdown(""" ++ β οΈ Invalid API key format ++ """, unsafe_allow_html=True) + + with st.expander("π How to get your Google API key", expanded=False): + st.markdown(""" + **Step-by-step guide:** + 1. Visit [Google AI Studio](https://makersuite.google.com/app/apikey) + 2. Sign in with your Google account + 3. Click "Create API key" + 4. Copy the generated key and paste it here + + **Note:** Make sure to enable the Gemini API in your Google Cloud Console. + """) + + st.markdown("++ """, unsafe_allow_html=True) + st.info("Anthropic integration will be available in the next update") + + with col2: + # Mistral Card (Coming Soon) + with st.container(): + st.markdown(""" ++ ++Anthropic Coming Soon+++Access Claude for advanced content generation
+++ """, unsafe_allow_html=True) + st.info("Mistral integration will be available in the next update") + + # Track changes and validate keys + if any([openai_key, google_key]): + changes_made = True + # Check if at least one valid API key is provided + if validate_api_key(openai_key) or validate_api_key(google_key): + has_valid_key = True + validation_message = "β At least one AI provider configured successfully" + else: + validation_message = "β οΈ Please provide at least one valid API key" + else: + validation_message = "β οΈ Please configure at least one AI provider to continue" + + # Display validation message + if validation_message: + if "β " in validation_message: + st.success(validation_message) + else: + st.warning(validation_message) + + # Navigation buttons + if render_navigation_buttons(1, 6, changes_made): + if has_valid_key: + # Store the API keys in a separate session state key + st.session_state['api_keys'] = { + 'openai': openai_key if validate_api_key(openai_key) else None, + 'google': google_key if validate_api_key(google_key) else None + } + + # Update progress and move to next step + st.session_state['current_step'] = 2 # Set the next step explicitly + update_progress() + st.rerun() # Rerun to apply the changes + else: + st.error("Please configure at least one valid AI provider to continue") + + return {"current_step": 1, "changes_made": changes_made} + + except Exception as e: + error_msg = f"Error in AI providers setup: {str(e)}" + logger.error(f"[render_ai_providers] {error_msg}") + st.error(error_msg) + return {"current_step": 1, "error": error_msg} \ No newline at end of file diff --git a/lib/utils/api_key_manager/components/ai_providers_setup.py b/lib/utils/api_key_manager/components/ai_providers_setup.py new file mode 100644 index 00000000..1ab045f6 --- /dev/null +++ b/lib/utils/api_key_manager/components/ai_providers_setup.py @@ -0,0 +1,114 @@ +"""AI providers setup component for API key manager.""" + +from typing import Dict, Any +from loguru import logger +import streamlit as st +import os +import sys + +def render_ai_providers_setup(api_key_manager) -> Dict[str, Any]: + """ + Render the AI providers setup component. + + Args: + api_key_manager: API key manager instance + + Returns: + Dict[str, Any]: Component state + """ + try: + logger.info("[render_ai_providers_setup] Rendering AI providers setup") + + # Display section header + st.header("Step 1: Select AI Providers") + st.markdown(""" + Configure your AI providers to enable advanced content generation capabilities. + Choose and set up the AI services you want to use. + """) + + # Create columns for different providers + col1, col2 = st.columns(2) + + with col1: + st.subheader("OpenAI") + st.markdown(""" + OpenAI's GPT models provide powerful natural language processing capabilities. + + Get your API key from: [OpenAI Dashboard](https://platform.openai.com/account/api-keys) + """) + + openai_key = api_key_manager.get_api_key("openai") + openai_input = st.text_input( + "OpenAI API Key", + value=openai_key if openai_key else "", + type="password", + key="openai_key_input" + ) + + with col2: + st.subheader("Google Gemini") + st.markdown(""" + Google's Gemini models offer advanced AI capabilities. + + Get your API key from: [Google AI Studio](https://makersuite.google.com/app/apikey) + """) + + gemini_key = api_key_manager.get_api_key("gemini") + gemini_input = st.text_input( + "Gemini API Key", + value=gemini_key if gemini_key else "", + type="password", + key="gemini_key_input" + ) + + # Optional AI Provider + st.subheader("Additional AI Provider (Optional)") + col1, col2 = st.columns(2) + + with col1: + st.markdown(""" + Mistral AI provides an alternative model for content generation. + + Get your API key from: [Mistral Platform](https://console.mistral.ai/api-keys/) + """) + + mistral_key = api_key_manager.get_api_key("mistral") + mistral_input = st.text_input( + "Mistral API Key (Optional)", + value=mistral_key if mistral_key else "", + type="password", + key="mistral_key_input" + ) + + # Add a note about saving + st.info(""" + Note: At least one AI provider (OpenAI or Google Gemini) is required. + Click Continue to save your keys and proceed. + """) + + # Save keys if they've changed when proceeding to next step + if st.session_state.get('wizard_current_step', 1) > 1: + if openai_input != openai_key: + api_key_manager.save_api_key("openai", openai_input) + logger.info("[render_ai_providers_setup] OpenAI API key saved") + + if gemini_input != gemini_key: + api_key_manager.save_api_key("gemini", gemini_input) + logger.info("[render_ai_providers_setup] Gemini API key saved") + + if mistral_input != mistral_key: + api_key_manager.save_api_key("mistral", mistral_input) + logger.info("[render_ai_providers_setup] Mistral API key saved") + + # Validate that at least one required provider is configured + if not (openai_input or gemini_input): + st.error("Please configure at least one AI provider (OpenAI or Google Gemini) to proceed.") + return {"current_step": 1, "can_proceed": False} + + return {"current_step": 1, "can_proceed": bool(openai_input or gemini_input)} + + except Exception as e: + error_msg = f"Error in AI providers setup: {str(e)}" + logger.error(f"[render_ai_providers_setup] {error_msg}") + st.error(error_msg) + return {"current_step": 1, "error": error_msg} diff --git a/lib/utils/api_key_manager/components/ai_research.py b/lib/utils/api_key_manager/components/ai_research.py new file mode 100644 index 00000000..09e6c5ac --- /dev/null +++ b/lib/utils/api_key_manager/components/ai_research.py @@ -0,0 +1,137 @@ +"""AI Research setup component.""" + +import streamlit as st +from typing import Dict, Any +from loguru import logger +from ..manager import APIKeyManager +from .base import render_navigation_buttons, render_step_indicator + +def render_ai_research(api_key_manager: APIKeyManager) -> Dict[str, Any]: + """Render the AI Research setup step.""" + try: + st.markdown(""" ++ ++Mistral Coming Soon+++Use Mistral's efficient language models
+++ """, unsafe_allow_html=True) + + # Create tabs for different sections + tabs = st.tabs(["User Information", "Research Preferences"]) + + changes_made = False + has_valid_info = False + validation_message = "" + + with tabs[0]: + st.markdown("### User Information") + st.markdown("Please provide your details for personalized research experience") + + # User Information Card + with st.container(): + st.markdown(""" + + """, unsafe_allow_html=True) + + # User Input Fields with Streamlit Components + full_name = st.text_input("Full Name", key="full_name", + help="Enter your full name as you'd like it to appear") + + email = st.text_input("Email Address", key="email", + help="Enter your business email address") + + company = st.text_input("Company/Organization", key="company", + help="Enter your company or organization name") + + role = st.selectbox("Role", + ["Content Creator", "Marketing Manager", "Business Owner", "Other"], + help="Select your primary role") + + with tabs[1]: + st.markdown("### Research Preferences") + st.markdown("Configure how AI assists with your research") + + # Research Preferences Card + with st.container(): + st.markdown(""" +π AI Research Configuration
+Configure your research preferences and provide user information
+++ """, unsafe_allow_html=True) + + # Research Preferences Settings + research_depth = st.select_slider( + "Research Depth", + options=["Basic", "Standard", "Deep", "Comprehensive"], + value="Standard", + help="Choose how detailed you want the AI research to be" + ) + + st.markdown("#### Content Types") + content_types = st.multiselect( + "Select content types to focus on", + ["Blog Posts", "Social Media", "Technical Articles", "News", "Academic Papers"], + default=["Blog Posts", "Social Media"], + help="Choose what types of content you want to research" + ) + + auto_research = st.toggle( + "Enable Automated Research", + help="Automatically start research when content topics are added" + ) + + # Validate inputs + if all([full_name, email, company]): + changes_made = True + has_valid_info = True + validation_message = "β User information completed successfully" + else: + validation_message = "β οΈ Please fill in all required fields to continue" + + # Display validation message + if validation_message: + if "β " in validation_message: + st.success(validation_message) + else: + st.warning(validation_message) + + # Navigation buttons + if render_navigation_buttons(3, 6, changes_made): + if has_valid_info: + # Store user information in session state + st.session_state['user_info'] = { + 'full_name': full_name, + 'email': email, + 'company': company, + 'role': role, + 'research_preferences': { + 'depth': research_depth, + 'content_types': content_types, + 'auto_research': auto_research + } + } + + # Update progress and move to next step + st.session_state['current_step'] = 4 + st.rerun() + else: + st.error("Please complete all required fields to continue") + + return {"current_step": 3, "changes_made": changes_made} + + except Exception as e: + error_msg = f"Error in AI research setup: {str(e)}" + logger.error(f"[render_ai_research] {error_msg}") + st.error(error_msg) + return {"current_step": 3, "error": error_msg} \ No newline at end of file diff --git a/lib/utils/api_key_manager/components/ai_research_setup.py b/lib/utils/api_key_manager/components/ai_research_setup.py new file mode 100644 index 00000000..6eeaf853 --- /dev/null +++ b/lib/utils/api_key_manager/components/ai_research_setup.py @@ -0,0 +1,349 @@ +"""AI research setup component for the API key manager.""" + +import streamlit as st +from loguru import logger +from typing import Dict, Any +from ..manager import APIKeyManager +from .base import render_navigation_buttons +import os +from dotenv import load_dotenv +import sys + +# Configure logger +logger.remove() # Remove default handler +logger.add( + "logs/ai_research_setup.log", + rotation="500 MB", + retention="10 days", + level="DEBUG", + format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {message}" +) +logger.add( + sys.stdout, + level="INFO", + format="+ ++Research Settings+{time:YYYY-MM-DD HH:mm:ss} |{level: <8} |{message} " +) + +def render_ai_research_setup(api_key_manager: APIKeyManager) -> Dict[str, Any]: + """Render the AI research setup step.""" + logger.info("[render_ai_research_setup] Rendering AI research setup component") + + st.markdown(""" +++ """, unsafe_allow_html=True) + + # Create two columns for different search types + col1, col2 = st.columns(2) + + with col1: + st.markdown("### The Usual") + + # SerpAPI Card + st.markdown(""" +π AI Research Setup
+Configure your AI research providers for content analysis and research
++", unsafe_allow_html=True) + + # Firecrawl Card + st.markdown(""" ++ ++SerpAPI++ Access search engine results for research +++ """, unsafe_allow_html=True) + + serpapi_key = st.text_input( + "SerpAPI Key", + type="password", + key="serpapi_key", + help="Enter your SerpAPI key" + ) + + if serpapi_key: + st.markdown(""" ++ β API key configured ++ """, unsafe_allow_html=True) + + st.markdown(""" +++ """, unsafe_allow_html=True) + + st.markdown("++π How to get your SerpAPI key
+++Step-by-step guide:
++
+- Visit SerpAPI
+- Create an account
+- Go to your dashboard
+- Copy your API key
+- Paste it here
+Note: SerpAPI provides real-time search results from multiple engines.
++", unsafe_allow_html=True) + + with col2: + st.markdown("### AI Deep Research") + + # Tavily Card + st.markdown(""" ++ ++Firecrawl++ Web content extraction and analysis +++ """, unsafe_allow_html=True) + + firecrawl_key = st.text_input( + "Firecrawl API Key", + type="password", + key="firecrawl_key", + help="Enter your Firecrawl API key" + ) + + if firecrawl_key: + st.markdown(""" ++ β API key configured ++ """, unsafe_allow_html=True) + + st.markdown(""" +++ """, unsafe_allow_html=True) + + st.markdown("++π How to get your Firecrawl API key
+++Step-by-step guide:
++
+- Visit Firecrawl
+- Create an account
+- Go to your dashboard
+- Generate your API key
+- Copy and paste it here
+Note: Firecrawl provides powerful web content extraction and analysis capabilities.
++", unsafe_allow_html=True) + + # Metaphor/Exa Card + st.markdown(""" ++ ++Tavily AI++ AI-powered search with semantic understanding +++ """, unsafe_allow_html=True) + + tavily_key = st.text_input( + "Tavily API Key", + type="password", + key="tavily_key", + help="Enter your Tavily API key" + ) + + if tavily_key: + st.markdown(""" ++ β API key configured ++ """, unsafe_allow_html=True) + + st.markdown(""" +++ """, unsafe_allow_html=True) + + st.markdown("++π How to get your Tavily API key
+++Step-by-step guide:
++
+- Visit Tavily
+- Create an account
+- Go to API settings
+- Generate a new API key
+- Copy and paste it here
+Note: Tavily provides AI-powered semantic search capabilities.
++", unsafe_allow_html=True) + + # Disabled Options Expander + with st.expander("π Coming Soon - More Search Options", expanded=False): + st.markdown(""" ++ ++Metaphor/Exa++ Neural search engine for deep research +++ """, unsafe_allow_html=True) + + metaphor_key = st.text_input( + "Metaphor/Exa API Key", + type="password", + key="metaphor_key", + help="Enter your Metaphor/Exa API key" + ) + + if metaphor_key: + st.markdown(""" ++ β API key configured ++ """, unsafe_allow_html=True) + + st.markdown(""" +++ """, unsafe_allow_html=True) + + st.markdown("++π How to get your Metaphor/Exa API key
+++Step-by-step guide:
++
+- Visit Metaphor/Exa
+- Create an account
+- Navigate to API settings
+- Generate your API key
+- Copy and paste it here
+Note: Metaphor/Exa provides neural search capabilities for deep research.
+++ """, unsafe_allow_html=True) + + # Track changes + changes_made = bool(serpapi_key or tavily_key or metaphor_key or firecrawl_key) + + # Navigation buttons with correct arguments + if render_navigation_buttons(3, 5, changes_made): + if changes_made: + try: + # Load existing .env file if it exists + load_dotenv() + + # Create or update .env file with new API keys + with open('.env', 'a') as f: + if serpapi_key: + f.write(f"\nSERPAPI_KEY={serpapi_key}") + logger.info("[render_ai_research_setup] Saved SerpAPI key") + if tavily_key: + f.write(f"\nTAVILY_API_KEY={tavily_key}") + logger.info("[render_ai_research_setup] Saved Tavily API key") + if metaphor_key: + f.write(f"\nMETAPHOR_API_KEY={metaphor_key}") + logger.info("[render_ai_research_setup] Saved Metaphor API key") + if firecrawl_key: + f.write(f"\nFIRECRAWL_API_KEY={firecrawl_key}") + logger.info("[render_ai_research_setup] Saved Firecrawl API key") + + # Store the API keys in session state + st.session_state['api_keys'] = { + 'serpapi': serpapi_key, + 'tavily': tavily_key, + 'metaphor': metaphor_key, + 'firecrawl': firecrawl_key + } + + # Update progress and move to next step + st.session_state['current_step'] = 4 + st.rerun() + except Exception as e: + error_msg = f"Error saving API keys: {str(e)}" + logger.error(f"[render_ai_research_setup] {error_msg}") + st.error(error_msg) + else: + st.error("Please configure at least one research provider to continue") + + # Detailed Information Section + st.markdown(""" + --- + ### Understanding Your Research Options + + #### The Usual: Traditional Search + **SerpAPI** + - Real-time search results from multiple search engines + - Access to structured data from search results + - Great for gathering general information and market research + - Includes features like: + - Web search results + - News articles + - Knowledge graphs + - Related questions + + #### AI Deep Research: Advanced Search Capabilities + + **Tavily AI** + - AI-powered search with semantic understanding + - Automatically summarizes and analyzes search results + - Perfect for: + - Deep research tasks + - Academic research + - Fact-checking + - Real-time information gathering + + **Metaphor/Exa** + - Neural search engine that understands context and meaning + - Specialized in finding highly relevant content + - Ideal for: + - Technical research + - Finding similar content + - Discovering patterns in research + - Understanding topic landscapes + + #### Choosing the Right Tool + + 1. **For General Research:** + - Start with SerpAPI for broad coverage and structured data + + 2. **For Deep Analysis:** + - Use Tavily AI when you need AI-powered insights + - Choose Metaphor/Exa for neural search and pattern discovery + + 3. **For Comprehensive Research:** + - Combine multiple tools to get the most complete picture + - Use SerpAPI for initial research + - Follow up with AI tools for deeper insights + + > **Pro Tip:** Configure multiple providers to ensure you have backup options and can cross-reference results for better accuracy. + """) + + return {"current_step": 3, "changes_made": changes_made} diff --git a/lib/utils/api_key_manager/components/alwrity_integrations.py b/lib/utils/api_key_manager/components/alwrity_integrations.py new file mode 100644 index 00000000..981b2f21 --- /dev/null +++ b/lib/utils/api_key_manager/components/alwrity_integrations.py @@ -0,0 +1,176 @@ +"""ALwrity integrations setup component.""" + +import streamlit as st +from loguru import logger +from typing import Dict, Any +from ..manager import APIKeyManager +from .base import render_navigation_buttons, render_step_indicator, render_tab_style + +def render_alwrity_integrations(api_key_manager: APIKeyManager) -> Dict[str, Any]: + """Render the ALwrity integrations setup step.""" + try: + # Apply enhanced tab styling + render_tab_style() + + st.markdown(""" +Bing Search API
+Microsoft's powerful search API with web, news, and image search capabilities.
+ +Google Search API
+Google's programmable search engine with customizable search parameters.
+ +These integrations are under development and will be available soon!
+++ """, unsafe_allow_html=True) + + # Create tabs for different integration types + tabs = st.tabs(["Website Platforms", "Social Media", "Analytics Tools"]) + + changes_made = False + has_valid_integrations = False + validation_message = "" + + with tabs[0]: + st.markdown(""" +π ALwrity Integrations
+Connect your content platforms and tools
+++ """, unsafe_allow_html=True) + + # Website Platforms Grid + col1, col2 = st.columns(2) + + with col1: + # WordPress Card (Coming Soon) + with st.container(): + st.markdown(""" +Website Platforms
+Connect your website platforms for seamless content publishing
+++ """, unsafe_allow_html=True) + st.info("WordPress integration will be available in the next update") + + with col2: + # Wix Card (Coming Soon) + with st.container(): + st.markdown(""" ++ ++WordPress Coming Soon+++Connect your WordPress site for direct content publishing.
+++ """, unsafe_allow_html=True) + st.info("Wix integration will be available in the next update") + + with tabs[1]: + st.markdown(""" ++ ++Wix Coming Soon+++Connect your Wix site for direct content publishing.
+++ """, unsafe_allow_html=True) + + # Social Media Grid + col1, col2 = st.columns(2) + + with col1: + # Facebook Card (Coming Soon) + with st.container(): + st.markdown(""" +Social Media
+Connect your social media accounts for content distribution
+++ """, unsafe_allow_html=True) + st.info("Facebook integration will be available in the next update") + + with col2: + # Instagram Card (Coming Soon) + with st.container(): + st.markdown(""" ++ ++Facebook Coming Soon+++Connect your Facebook account for content sharing.
+++ """, unsafe_allow_html=True) + st.info("Instagram integration will be available in the next update") + + with tabs[2]: + st.markdown(""" ++ ++Instagram Coming Soon+++Connect your Instagram account for content sharing.
+++ """, unsafe_allow_html=True) + + # Google Search Console Card (Coming Soon) + with st.container(): + st.markdown(""" +Analytics Tools
+Connect your analytics tools for content performance tracking
+++ """, unsafe_allow_html=True) + st.info("Google Search Console integration will be available in the next update") + + # Validate integrations + changes_made = True # Always allow proceeding since integrations are coming soon + has_valid_integrations = True + validation_message = "β Website platform integrations will be available in the next update" + + # Display validation message + if validation_message: + if "β " in validation_message: + st.success(validation_message) + else: + st.warning(validation_message) + + # Navigation buttons + if render_navigation_buttons(5, 6, changes_made): + if has_valid_integrations: + # Store integration settings in session state + st.session_state['integrations'] = { + 'coming_soon': { + 'wordpress': True, + 'wix': True, + 'facebook': True, + 'instagram': True, + 'google_search_console': True + } + } + + # Update progress and move to next step + st.session_state['current_step'] = 6 + st.rerun() + else: + st.error("Please configure at least one integration to continue") + + return {"current_step": 5, "changes_made": changes_made} + + except Exception as e: + error_msg = f"Error in ALwrity integrations setup: {str(e)}" + logger.error(f"[render_alwrity_integrations] {error_msg}") + st.error(error_msg) + return {"current_step": 5, "error": error_msg} \ No newline at end of file diff --git a/lib/utils/api_key_manager/components/base.py b/lib/utils/api_key_manager/components/base.py new file mode 100644 index 00000000..953df736 --- /dev/null +++ b/lib/utils/api_key_manager/components/base.py @@ -0,0 +1,185 @@ +"""Base components for the API key manager.""" + +import streamlit as st +from typing import Dict, Any +from loguru import logger +from ..styles import API_KEY_MANAGER_STYLES +from ..wizard_state import ( + get_current_step, + next_step, + previous_step, + can_proceed_to_next_step +) + +def render_step_indicator(current_step: int, total_steps: int) -> None: + """Render the step indicator.""" + try: + st.markdown(""" + + """, unsafe_allow_html=True) + + steps = [ + ("π", "AI LLM", 1), + ("π€", "Website Setup", 2), + ("π€", "AI Research", 3), + ("π¨", "Personalization", 4), + ("π", "Integrations", 5), + ("β ", "Complete", 6) + ] + + html = '+ ++Google Search Console Coming Soon+++Connect your Google Search Console for SEO insights.
+' + for i, (icon, title, step) in enumerate(steps): + step_class = "active" if step == current_step else "completed" if step < current_step else "" + line_class = "active" if step == current_step else "completed" if step < current_step else "" + + html += f''' +' + + st.markdown(html, unsafe_allow_html=True) + + except Exception as e: + logger.error(f"Error rendering step indicator: {str(e)}") + st.error("Error displaying step indicator") + +def render_navigation_buttons(current_step: int, total_steps: int, changes_made: bool = False) -> bool: + """Render the navigation buttons with modern glassmorphic styling. + + Args: + current_step (int): Current step number + total_steps (int): Total number of steps + changes_made (bool): Whether changes were made in the current step + + Returns: + bool: True if next/complete button was clicked, False otherwise + """ + col1, col2, col3 = st.columns([1, 2, 1]) + + with col1: + if current_step > 1: + if st.button("**β Back**", use_container_width=True, key="back_button"): + st.session_state['current_step'] = current_step - 1 + st.rerun() + + with col3: + if current_step < total_steps: + next_text = "**Continue β**" + if st.button(next_text, use_container_width=True, disabled=not changes_made, key="next_button"): + return True + else: + if st.button("**Complete Setup β**", use_container_width=True, type="primary", key="complete_button"): + # Save the configuration + st.success("β Setup completed successfully!") + return True + + return False + +def render_tab_style() -> None: + """Render enhanced tab styling.""" + st.markdown(""" + + """, unsafe_allow_html=True) + +def render_success_message(): + """Render the success message with glassmorphic design.""" + st.markdown(""" + + """, unsafe_allow_html=True) diff --git a/lib/utils/api_key_manager/components/final_setup.py b/lib/utils/api_key_manager/components/final_setup.py new file mode 100644 index 00000000..5f9eb06c --- /dev/null +++ b/lib/utils/api_key_manager/components/final_setup.py @@ -0,0 +1,146 @@ +"""Final setup component for the API key manager.""" + +import streamlit as st +from loguru import logger +import sys +import json +import os +from typing import Dict, Any +from ..manager import APIKeyManager +from ..validation import check_all_api_keys + +# Configure logger to output to both file and stdout +logger.remove() # Remove default handler +logger.add( + "logs/final_setup.log", + rotation="500 MB", + retention="10 days", + level="DEBUG", + format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {message}" +) +logger.add( + sys.stdout, + level="INFO", + format="+ + {step} + {title} ++ ''' + if i < len(steps) - 1: + html += f'' + html += '{time:YYYY-MM-DD HH:mm:ss} |{level: <8} |{message} " +) + +def load_main_config() -> Dict[str, Any]: + """Load the main configuration file.""" + config_path = os.path.join("lib", "workspace", "alwrity_config", "main_config.json") + try: + with open(config_path, 'r') as f: + return json.load(f) + except Exception as e: + logger.error(f"Error loading main_config.json: {str(e)}") + return {} + +def render_final_setup(api_key_manager: APIKeyManager) -> Dict[str, Any]: + """Render the final setup step. + + Args: + api_key_manager (APIKeyManager): The API key manager instance + + Returns: + Dict[str, Any]: Current state + """ + logger.info("[render_final_setup] Rendering final setup component") + + st.markdown("### Step 5: Final Setup") + + # Load main config + main_config = load_main_config() + + # Display configuration summary + st.markdown("#### Configuration Summary") + + # Blog Content Characteristics + st.markdown("##### Blog Content Characteristics") + blog_settings = main_config.get("Blog Content Characteristics", {}) + st.write(f"- Blog Length: {blog_settings.get('Blog Length', '2000')}") + st.write(f"- Blog Tone: {blog_settings.get('Blog Tone', 'Professional')}") + st.write(f"- Blog Demographic: {blog_settings.get('Blog Demographic', 'Professional')}") + st.write(f"- Blog Type: {blog_settings.get('Blog Type', 'Informational')}") + + # LLM Options + st.markdown("##### LLM Options") + llm_settings = main_config.get("LLM Options", {}) + st.write(f"- GPT Provider: {llm_settings.get('GPT Provider', 'google')}") + st.write(f"- Model: {llm_settings.get('Model', 'gemini-1.5-flash-latest')}") + st.write(f"- Temperature: {llm_settings.get('Temperature', 0.7)}") + st.write(f"- Max Tokens: {llm_settings.get('Max Tokens', 4000)}") + + # Personalization Settings + st.markdown("##### Personalization Settings") + personalization = main_config.get("personalization", {}) + st.write(f"- Writing Tone: {personalization.get('writing_tone', 'Professional')}") + st.write(f"- Target Audience: {personalization.get('target_audience', 'General')}") + st.write(f"- Content Type: {personalization.get('content_type', 'Blog Posts')}") + + # Navigation buttons + col1, col2 = st.columns(2) + + with col1: + if st.button("β Back to Personalization"): + logger.info("[render_final_setup] User clicked back to personalization") + st.session_state.current_step = 4 + st.session_state.next_step = "personalization_setup" + st.rerun() + + with col2: + if st.button("Complete Setup β"): + logger.info("[render_final_setup] User clicked complete setup") + try: + # Verify all required API keys are present and valid + is_valid, missing_keys, impact_messages = check_all_api_keys(api_key_manager) + + if not is_valid: + st.error("β οΈ Some required API keys are missing") + st.markdown("### Missing API Keys and Impact") + + # Display impact messages in a structured way + for message in impact_messages: + if message.startswith("β οΈ"): + st.error(message) + else: + st.warning(message) + + st.markdown(""" +++ """, unsafe_allow_html=True) + return {"current_step": 6, "changes_made": True} + + # Save final configuration + if not os.path.exists("lib/workspace/alwrity_config"): + os.makedirs("lib/workspace/alwrity_config") + + config_path = os.path.join("lib", "workspace", "alwrity_config", "main_config.json") + with open(config_path, 'w') as f: + json.dump(main_config, f, indent=4) + + # Show success message with HTML formatting + st.markdown(""" +Required Keys:
++
+- At least one AI provider (OpenAI, Google Gemini, Anthropic Claude, or Mistral)
+- At least one research provider (SerpAPI, Tavily, Metaphor, or Firecrawl)
+Please configure the required keys before proceeding.
+++ """, unsafe_allow_html=True) + + # Set setup completion flag in session state + st.session_state['setup_completed'] = True + + # Redirect to main application + st.switch_page("alwrity.py") + + except Exception as e: + error_msg = f"Error completing setup: {str(e)}" + logger.error(f"[render_final_setup] {error_msg}") + st.error(error_msg) + + return {"current_step": 5, "changes_made": True} diff --git a/lib/utils/api_key_manager/components/health_monitor.py b/lib/utils/api_key_manager/components/health_monitor.py new file mode 100644 index 00000000..bb4e4475 --- /dev/null +++ b/lib/utils/api_key_manager/components/health_monitor.py @@ -0,0 +1,39 @@ +"""Health monitoring component for the API key manager.""" + +import streamlit as st +from loguru import logger +from ..health_monitor import APIKeyHealthMonitor +from ..key_rotation import KeyRotationManager +from ..wizard_state import get_api_keys + +def render_health_monitoring(): + """Render the API key health monitoring dashboard.""" + st.header("API Key Health & Rotation") + + # Initialize managers + health_monitor = APIKeyHealthMonitor() + rotation_manager = KeyRotationManager() + + # Create tabs for different views + health_tab, rotation_tab = st.tabs(["Health Monitor", "Key Rotation"]) + + with health_tab: + health_monitor.get_health_dashboard() + + with rotation_tab: + rotation_manager.display_rotation_dashboard() + + # Manual rotation controls + st.subheader("Manual Controls") + key_type = st.selectbox( + "Select Key Type", + options=[k.split('_')[0] for k in get_api_keys()] + ) + + if key_type: + if st.button("Force Rotation"): + new_key = rotation_manager.rotate_if_needed(key_type) + if new_key: + st.success(f"Rotated to new key: {new_key}") + else: + st.warning("No suitable key available for rotation") diff --git a/lib/utils/api_key_manager/components/personalization.py b/lib/utils/api_key_manager/components/personalization.py new file mode 100644 index 00000000..7d97dba9 --- /dev/null +++ b/lib/utils/api_key_manager/components/personalization.py @@ -0,0 +1,188 @@ +"""Personalization setup component.""" + +import streamlit as st +from typing import Dict, Any +from loguru import logger +from ..manager import APIKeyManager +from .base import render_navigation_buttons, render_step_indicator + +def render_personalization(api_key_manager: APIKeyManager) -> Dict[str, Any]: + """Render the personalization setup step.""" + try: + st.markdown(""" +β Setup Completed Successfully!
+Your configuration has been saved and you're ready to use ALwrity.
+++ """, unsafe_allow_html=True) + + # Create tabs for different sections + tabs = st.tabs(["Content Style", "Brand Voice", "Advanced Settings"]) + + changes_made = False + has_valid_settings = False + validation_message = "" + + with tabs[0]: + st.markdown("### Content Style") + st.markdown("Define your preferred content style and tone") + + # Content Style Card + with st.container(): + st.markdown(""" +π¨ Personalization Settings
+Customize your content generation experience
+++ """, unsafe_allow_html=True) + + # Style Settings + writing_style = st.selectbox( + "Writing Style", + ["Professional", "Casual", "Technical", "Conversational", "Academic"], + help="Select your preferred writing style" + ) + + tone = st.select_slider( + "Content Tone", + options=["Formal", "Semi-Formal", "Neutral", "Friendly", "Humorous"], + value="Neutral", + help="Choose the tone for your content" + ) + + content_length = st.select_slider( + "Content Length", + options=["Concise", "Standard", "Detailed", "Comprehensive"], + value="Standard", + help="Select your preferred content length" + ) + + with tabs[1]: + st.markdown("### Brand Voice") + st.markdown("Configure your brand's unique voice and personality") + + # Brand Voice Card + with st.container(): + st.markdown(""" ++ ++Writing Style+++Choose how you want your content to be written.
+++ """, unsafe_allow_html=True) + + # Brand Settings + brand_personality = st.multiselect( + "Brand Personality Traits", + ["Professional", "Innovative", "Friendly", "Trustworthy", "Creative", "Expert"], + default=["Professional", "Trustworthy"], + help="Select traits that best describe your brand" + ) + + brand_voice = st.text_area( + "Brand Voice Description", + help="Describe how your brand should sound in content" + ) + + keywords = st.text_input( + "Brand Keywords", + help="Enter key terms that should be used in your content" + ) + + with tabs[2]: + st.markdown("### Advanced Settings") + st.markdown("Fine-tune your content generation preferences") + + # Advanced Settings Card + with st.container(): + st.markdown(""" ++ ++Brand Identity+++Define your brand's personality and voice.
+++ """, unsafe_allow_html=True) + + # Advanced Settings + seo_optimization = st.toggle( + "Enable SEO Optimization", + help="Automatically optimize content for search engines" + ) + + readability_level = st.select_slider( + "Readability Level", + options=["Simple", "Standard", "Advanced", "Expert"], + value="Standard", + help="Choose the complexity level of your content" + ) + + content_structure = st.multiselect( + "Content Structure", + ["Introduction", "Key Points", "Examples", "Conclusion", "Call-to-Action"], + default=["Introduction", "Key Points", "Conclusion"], + help="Select required content sections" + ) + + # Validate settings + if all([writing_style, tone, content_length, brand_personality]): + changes_made = True + has_valid_settings = True + validation_message = "β Personalization settings completed successfully" + else: + validation_message = "β οΈ Please complete all required settings to continue" + + # Display validation message + if validation_message: + if "β " in validation_message: + st.success(validation_message) + else: + st.warning(validation_message) + + # Navigation buttons + if render_navigation_buttons(4, 6, changes_made): + if has_valid_settings: + # Store personalization settings in session state + st.session_state['personalization'] = { + 'content_style': { + 'writing_style': writing_style, + 'tone': tone, + 'content_length': content_length + }, + 'brand_voice': { + 'personality': brand_personality, + 'voice_description': brand_voice, + 'keywords': keywords + }, + 'advanced_settings': { + 'seo_optimization': seo_optimization, + 'readability_level': readability_level, + 'content_structure': content_structure + } + } + + # Update progress and move to next step + st.session_state['current_step'] = 5 + st.rerun() + else: + st.error("Please complete all required settings to continue") + + return {"current_step": 4, "changes_made": changes_made} + + except Exception as e: + error_msg = f"Error in personalization setup: {str(e)}" + logger.error(f"[render_personalization] {error_msg}") + st.error(error_msg) + return {"current_step": 4, "error": error_msg} \ No newline at end of file diff --git a/lib/utils/api_key_manager/components/personalization_setup.py b/lib/utils/api_key_manager/components/personalization_setup.py new file mode 100644 index 00000000..f4f773c4 --- /dev/null +++ b/lib/utils/api_key_manager/components/personalization_setup.py @@ -0,0 +1,702 @@ +"""Personalization setup component for the API key manager.""" + +import streamlit as st +from loguru import logger +import sys +import json +from typing import Dict, Any +from ..manager import APIKeyManager +from ....web_crawlers.async_web_crawler import AsyncWebCrawlerService +from ....personalization.style_analyzer import StyleAnalyzer +from pages.style_utils import ( + get_analysis_section, + get_glass_container, + get_info_section, + get_example_box +) +from .base import render_navigation_buttons +from .alwrity_integrations import render_alwrity_integrations +import asyncio +import os +from pathlib import Path +import yaml + +# Configure logger to output to both file and stdout +logger.remove() # Remove default handler +logger.add( + "logs/personalization_setup.log", + rotation="500 MB", + retention="10 days", + level="DEBUG", + format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {message}" +) +logger.add( + sys.stdout, + level="INFO", + format="+ ++Advanced Options+++Configure advanced content generation settings.
+{time:YYYY-MM-DD HH:mm:ss} |{level: <8} |{message} " +) + +def load_main_config() -> Dict[str, Any]: + """Load the main configuration file.""" + config_path = os.path.join("lib", "workspace", "alwrity_config", "main_config.json") + try: + with open(config_path, 'r') as f: + return json.load(f) + except Exception as e: + logger.error(f"Error loading main_config.json: {str(e)}") + return {} + +def save_main_config(config: Dict[str, Any]) -> bool: + """Save the main configuration file.""" + try: + config_path = os.path.join("lib", "workspace", "alwrity_config", "main_config.json") + os.makedirs(os.path.dirname(config_path), exist_ok=True) + with open(config_path, 'w') as f: + json.dump(config, f, indent=4) + return True + except Exception as e: + logger.error(f"Error saving main_config.json: {str(e)}") + return False + +def display_style_analysis(analysis_results: dict): + """Display the style analysis results in a structured format.""" + try: + # Writing Style Section + writing_style = analysis_results.get("writing_style", {}) + writing_style_content = f""" ++
+ """ + st.markdown(get_analysis_section("Writing Style", writing_style_content), unsafe_allow_html=True) + + # Target Audience Section + target_audience = analysis_results.get("target_audience", {}) + target_audience_content = f""" +- Tone: {writing_style.get("tone", "N/A")}
+- Voice: {writing_style.get("voice", "N/A")}
+- Complexity: {writing_style.get("complexity", "N/A")}
+- Formality: {writing_style.get("formality", "N/A")}
++
+ """ + st.markdown(get_analysis_section("Target Audience", target_audience_content), unsafe_allow_html=True) + + # Content Type Section + content_type = analysis_results.get("content_type", {}) + content_type_content = f""" +- Demographics: {', '.join(target_audience.get("demographics", ["N/A"]))}
+- Expertise Level: {target_audience.get("expertise_level", "N/A")}
+- Industry Focus: {target_audience.get("industry_focus", "N/A")}
+- Geographic Focus: {target_audience.get("geographic_focus", "N/A")}
++
+ """ + st.markdown(get_analysis_section("Content Type", content_type_content), unsafe_allow_html=True) + + # Recommended Settings Section + recommended = analysis_results.get("recommended_settings", {}) + recommended_content = f""" +- Primary Type: {content_type.get("primary_type", "N/A")}
+- Secondary Types: {', '.join(content_type.get("secondary_types", ["N/A"]))}
+- Purpose: {content_type.get("purpose", "N/A")}
+- Call to Action: {content_type.get("call_to_action", "N/A")}
++
+ """ + st.markdown(get_analysis_section("Recommended Settings", recommended_content), unsafe_allow_html=True) + + except Exception as e: + logger.error(f"Error displaying style analysis: {str(e)}") + st.error(f"Error displaying analysis results: {str(e)}") + +def render_personalization_setup(api_key_manager: APIKeyManager) -> Dict[str, Any]: + """Render the personalization setup step.""" + logger.info("[render_personalization_setup] Rendering personalization setup component") + + st.markdown(""" +- Writing Tone: {recommended.get("writing_tone", "N/A")}
+- Target Audience: {recommended.get("target_audience", "N/A")}
+- Content Type: {recommended.get("content_type", "N/A")}
+- Creativity Level: {recommended.get("creativity_level", "N/A")}
+- Geographic Location: {recommended.get("geographic_location", "N/A")}
+++ """, unsafe_allow_html=True) + + # Load main config + main_config = load_main_config() + + # Create tabs for different personalization methods + tab1, tab2 = st.tabs([ + "Manual Settings", + "ALwrity Personalization" + ]) + + with tab1: + st.markdown("### Manual Settings Configuration") + + # Add container for better width control + st.markdown(""" +β¨ Personalization Setup
+Configure your content generation preferences and writing style
++ """, unsafe_allow_html=True) + + # Create two columns for settings and explanations (1:2 ratio) + settings_col, info_col = st.columns([1, 2]) + + with settings_col: + st.markdown(""" +", unsafe_allow_html=True) + + # Add some spacing before the save button + st.markdown("", unsafe_allow_html=True) + + if st.button("Save Manual Settings", type="primary", use_container_width=True): + # Update main config with new values + main_config["Blog Content Characteristics"] = { + "Blog Length": blog_length, + "Blog Tone": blog_tone, + "Blog Demographic": blog_demographic, + "Blog Type": blog_type, + "Blog Language": blog_language, + "Blog Output Format": blog_format + } + + main_config["Blog Images Details"] = { + "Image Generation Model": image_model, + "Number of Blog Images": num_images + } + + main_config["LLM Options"] = { + "GPT Provider": gpt_provider, + "Model": model, + "Temperature": temperature, + "Top-p": top_p, + "Max Tokens": max_tokens, + "Frequency Penalty": frequency_penalty, + "Presence Penalty": presence_penalty + } + + main_config["Search Engine Parameters"] = { + "Geographic Location": geo_location, + "Search Language": search_language, + "Number of Results": num_results, + "Time Range": time_range + } + + if save_main_config(main_config): + st.success("β Your personalization settings have been saved successfully!") + else: + st.error("Unable to save settings. Please try again.") + + with tab2: + st.markdown("#### ALwrity Personalization") + + # Create two columns for the layout + col1, col2 = st.columns([2, 1]) + + with col1: + # Website URL input + st.markdown("### Website URL") + url = st.text_input( + "Enter your website URL", + placeholder="https://example.com", + help="Provide your website URL to analyze your content style. Leave empty if you want to provide written samples instead." + ) + logger.debug(f"Website URL input value: {url}") + + # Alternative: Written samples + if not url: + st.markdown("### Written Samples") + st.markdown(""" ++ """, unsafe_allow_html=True) + + # Blog Content Characteristics + st.markdown("#### Blog Content Characteristics") + blog_settings = main_config.get("Blog Content Characteristics", {}) + + blog_length = st.text_input( + "Blog Length", + value=blog_settings.get("Blog Length", "2000"), + placeholder="e.g., 2000", + help="Target word count for your blog posts" + ) + + blog_tone = st.selectbox( + "Blog Tone", + options=["Professional", "Casual", "Technical", "Conversational"], + index=["Professional", "Casual", "Technical", "Conversational"].index(blog_settings.get("Blog Tone", "Professional")), + help="The overall tone of your content" + ) + + blog_demographic = st.selectbox( + "Target Demographic", + options=["Professional", "General", "Technical", "Academic"], + index=["Professional", "General", "Technical", "Academic"].index(blog_settings.get("Blog Demographic", "Professional")), + help="Your primary audience demographic" + ) + + blog_type = st.selectbox( + "Content Type", + options=["Informational", "Educational", "Entertainment", "Technical"], + index=["Informational", "Educational", "Entertainment", "Technical"].index(blog_settings.get("Blog Type", "Informational")), + help="The primary type of content you create" + ) + + blog_language = st.selectbox( + "Content Language", + options=["English", "Spanish", "French", "German", "Other"], + index=["English", "Spanish", "French", "German", "Other"].index(blog_settings.get("Blog Language", "English")), + help="Primary language for your content" + ) + + blog_format = st.selectbox( + "Output Format", + options=["markdown", "html", "plain text"], + index=["markdown", "html", "plain text"].index(blog_settings.get("Blog Output Format", "markdown")), + help="Format of the generated content" + ) + + # Blog Images Details + st.markdown("#### Blog Images") + image_settings = main_config.get("Blog Images Details", {}) + + image_model = st.selectbox( + "Image Generation Model", + options=["stable-diffusion", "dall-e", "midjourney"], + index=["stable-diffusion", "dall-e", "midjourney"].index(image_settings.get("Image Generation Model", "stable-diffusion")), + help="AI model for generating images" + ) + + num_images = st.number_input( + "Number of Images", + min_value=1, + max_value=5, + value=image_settings.get("Number of Blog Images", 1), + help="Number of images to generate per blog post" + ) + + # LLM Options + st.markdown("#### AI Generation Settings") + llm_settings = main_config.get("LLM Options", {}) + + gpt_provider = st.selectbox( + "AI Provider", + options=["google", "openai", "anthropic"], + index=["google", "openai", "anthropic"].index(llm_settings.get("GPT Provider", "google")), + help="Choose your preferred AI provider" + ) + + model = st.text_input( + "Model", + value=llm_settings.get("Model", "gemini-1.5-flash-latest"), + placeholder="e.g., gemini-1.5-flash-latest", + help="The specific AI model to use" + ) + + temperature = st.slider( + "Creativity Level", + min_value=0.0, + max_value=1.0, + value=float(llm_settings.get("Temperature", 0.7)), + help="Higher values = more creative, lower values = more focused" + ) + + top_p = st.slider( + "Output Diversity", + min_value=0.0, + max_value=1.0, + value=float(llm_settings.get("Top-p", 0.9)), + help="Controls diversity of generated content" + ) + + max_tokens = st.number_input( + "Maximum Length", + min_value=100, + max_value=8000, + value=int(llm_settings.get("Max Tokens", 4000)), + help="Maximum length of generated content" + ) + + frequency_penalty = st.slider( + "Frequency Penalty", + min_value=-2.0, + max_value=2.0, + value=float(llm_settings.get("Frequency Penalty", 1.0)), + help="Reduces repetition of the same words" + ) + + presence_penalty = st.slider( + "Presence Penalty", + min_value=-2.0, + max_value=2.0, + value=float(llm_settings.get("Presence Penalty", 1.0)), + help="Encourages discussion of new topics" + ) + + # Search Engine Parameters + st.markdown("#### Search Settings") + search_settings = main_config.get("Search Engine Parameters", {}) + + geo_location = st.text_input( + "Geographic Location", + value=search_settings.get("Geographic Location", "us"), + placeholder="e.g., us, uk, ca", + help="Target geographic location for search results" + ) + + search_language = st.selectbox( + "Search Language", + options=["en", "es", "fr", "de", "other"], + index=["en", "es", "fr", "de", "other"].index(search_settings.get("Search Language", "en")), + help="Language for search results" + ) + + num_results = st.number_input( + "Number of Results", + min_value=1, + max_value=50, + value=search_settings.get("Number of Results", 10), + help="Number of search results to analyze" + ) + + time_range = st.selectbox( + "Time Range", + options=["anytime", "day", "week", "month", "year"], + index=["anytime", "day", "week", "month", "year"].index(search_settings.get("Time Range", "anytime")), + help="Time range for search results" + ) + + st.markdown("", unsafe_allow_html=True) + + with info_col: + st.markdown(""" ++ """, unsafe_allow_html=True) + + st.markdown(""" +", unsafe_allow_html=True) + + # Close the container + st.markdown("+ ### Understanding Your Settings + + #### Blog Content Settings + + **Blog Length** + - Determines the target word count for your posts + - Affects content depth and detail level + - Impacts reader engagement and SEO performance + - Recommended: 1500-2500 words for comprehensive coverage + + **Blog Tone** + - Professional: Formal, business-oriented, authoritative + - Casual: Friendly, conversational, approachable + - Technical: Detailed, precise, industry-specific + - Conversational: Engaging, relatable, personal + + **Target Demographic** + - Professional: Business audience, decision-makers + - General: Broad readership, general public + - Technical: Specialized audience, industry experts + - Academic: Research-focused, scholarly readers + + **Content Type** + - Informational: Facts, insights, and analysis + - Educational: Teaching, tutorials, how-to guides + - Entertainment: Engaging, fun, light content + - Technical: Detailed analysis, specifications + + **Content Language** + - Select your primary content language + - Affects grammar, idioms, and cultural context + - Impacts SEO and audience reach + + **Output Format** + - Markdown: Best for most platforms + - HTML: For web publishing + - Plain Text: For simple content + + #### Image Generation Settings + + **Image Generation Model** + - Stable Diffusion: Best for general content + - DALL-E: Great for creative concepts + - Midjourney: Excellent for artistic content + + **Number of Images** + - Consider your content type and platform + - More images = better engagement but higher cost + - Recommended: 1-2 images per post + + #### AI Generation Settings + + **AI Provider** + - Google: Balanced, reliable, cost-effective + - OpenAI: Creative, nuanced, versatile + - Anthropic: Precise, ethical, focused + + **Model Selection** + - Latest models offer best performance + - Specialized models for specific needs + - Consider cost vs. quality trade-offs + + **Creativity Level (Temperature)** + - 0.0: Focused, consistent, predictable + - 0.5: Balanced creativity and coherence + - 1.0: Maximum creativity, more varied + + **Output Diversity (Top-p)** + - Controls variety in word choices + - Higher values = more diverse vocabulary + - Lower values = more focused terminology + + **Maximum Length** + - Affects content completeness + - Consider platform limits + - Balance detail vs. readability + + **Frequency & Presence Penalties** + - Reduce repetition of words + - Encourage topic diversity + - Fine-tune content variety + + #### Search Settings + + **Geographic Location** + - Target specific regions + - Affects local SEO + - Influences content relevance + + **Search Language** + - Match your content language + - Affects result relevance + - Impacts SEO performance + + **Number of Results** + - More results = better analysis + - Consider processing time + - Balance quality vs. speed + + **Time Range** + - Anytime: All available content + - Recent: Latest information + - Historical: Past content + + ### Best Practices + + 1. **Start Conservative** + - Begin with moderate settings + - Adjust based on results + - Monitor performance + + 2. **Consider Your Audience** + - Match tone to reader expectations + - Adjust complexity appropriately + - Focus on value delivery + + 3. **Optimize for Platform** + - Consider platform limitations + - Match format requirements + - Optimize for engagement + + 4. **Regular Review** + - Monitor content performance + - Adjust settings as needed + - Stay updated with trends ++ """, unsafe_allow_html=True) + + st.markdown("++ """, unsafe_allow_html=True) + samples = st.text_area( + "Paste your content samples here", + help="Paste 2-3 samples of your best content. This helps ALwrity understand your writing style." + ) + logger.debug(f"Sample text length: {len(samples) if samples else 0}") + + # ALwrity Style button + st.markdown("", unsafe_allow_html=True) + if st.button("π¨ ALwrity Style", use_container_width=True): + if url: + with st.status("Starting style analysis...", expanded=True) as status: + try: + logger.info(f"Starting style analysis for URL: {url}") + + # Step 1: Initialize crawler + status.update(label="Step 1/4: Initializing web crawler...", state="running") + crawler_service = AsyncWebCrawlerService() + + # Step 2: Crawl website + status.update(label="Step 2/4: Crawling website content...", state="running") + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + result = loop.run_until_complete(crawler_service.crawl_website(url)) + loop.close() + + if result.get('success', False): + content = result.get('content', {}) + + # Step 3: Initialize style analyzer + status.update(label="Step 3/4: Analyzing content style...", state="running") + style_analyzer = StyleAnalyzer() + + # Step 4: Perform style analysis + status.update(label="Step 4/4: Generating style recommendations...", state="running") + style_analysis = style_analyzer.analyze_content_style(content) + + if style_analysis.get('error'): + status.update(label="Analysis failed", state="error") + st.error(f"Style analysis failed: {style_analysis['error']}") + else: + status.update(label="Analysis complete!", state="complete") + # Display style analysis results + display_style_analysis(style_analysis) + + # Display original content in tabs + tab1, tab2, tab3 = st.tabs(["Content", "Metadata", "Links"]) + + with tab1: + st.markdown("### Main Content") + st.markdown(content.get('main_content', 'No content found')) + + with tab2: + st.markdown("### Metadata") + st.markdown(f""" + **Title:** {content.get('title', 'No title found')} + + **Description:** {content.get('description', 'No description found')} + + **Meta Tags:** + {content.get('meta_tags', {})} + """) + + with tab3: + st.markdown("### Links") + for link in content.get('links', []): + st.markdown(f"- [{link.get('text', '')}]({link.get('href', '')})") + + else: + status.update(label="Crawling failed", state="error") + st.error(f"Failed to analyze website: {result.get('error', 'Unknown error')}") + + except Exception as e: + logger.error(f"Error during style analysis: {str(e)}") + st.error(f"Analysis failed: {str(e)}") + elif samples: + with st.spinner("Analyzing content samples..."): + try: + # Initialize style analyzer + style_analyzer = StyleAnalyzer() + + # Analyze content samples + style_analysis = style_analyzer.analyze_content_style({"main_content": samples}) + + if style_analysis.get('error'): + st.error(f"Style analysis failed: {style_analysis['error']}") + else: + # Display style analysis results + display_style_analysis(style_analysis) + + except Exception as e: + logger.error(f"Error analyzing samples: {str(e)}") + st.error(f"Analysis failed: {str(e)}") + else: + st.warning("Please provide either a website URL or content samples") + + with col2: + st.markdown(""" + ### How ALwrity Discovers Your Style + + **AI-Powered Style Analysis** + + ALwrity AI analyzes your existing content to understand your unique writing style and preferences. This helps us generate content that matches your voice perfectly. + + **Step 1: Content Analysis** + + We'll analyze your website content or written samples to understand: + + - Writing tone and voice + - Vocabulary and language style + - Content structure and formatting + - Target audience and engagement style + + **Step 2: Style Recommendations** + + Based on the analysis, we'll provide: + + - Personalized writing guidelines + - Content structure templates + - Tone and voice recommendations + - Audience engagement strategies + + **Step 3: Content Generation** + + Finally, we'll use these insights to: + + - Generate content that matches your style + - Maintain consistency across all content + - Optimize for your target audience + - Ensure brand voice alignment + """) + + # API Configuration Form + st.markdown("### API Configuration") + with st.form("ai_config_form"): + # API Keys + st.text_input("OpenAI API Key", type="password", key="openai_key") + st.text_input("Google API Key", type="password", key="google_key") + st.text_input("SerpAPI Key", type="password", key="serpapi_key") + + # Model Selection + st.selectbox("Select Model", ["gpt-3.5-turbo", "gpt-4"], key="model") + + # Temperature + st.slider("Temperature", 0.0, 2.0, 0.7, 0.1, key="temperature") + + # Max Tokens + st.number_input("Max Tokens", 100, 4000, 2000, 100, key="max_tokens") + + # Submit button + submitted = st.form_submit_button("Save Configuration") + + if submitted: + # Create config directory if it doesn't exist + config_dir = Path("config") + config_dir.mkdir(exist_ok=True) + + # Save configuration + config = { + "openai_key": st.session_state.openai_key, + "google_key": st.session_state.google_key, + "serpapi_key": st.session_state.serpapi_key, + "model": st.session_state.model, + "temperature": st.session_state.temperature, + "max_tokens": st.session_state.max_tokens + } + + config_file = config_dir / "test_config.json" + with open(config_file, "w") as f: + json.dump(config, f, indent=4) + + st.success("Configuration saved successfully!") + + # Navigation buttons with correct arguments + if render_navigation_buttons(4, 5, changes_made=True): + st.session_state.current_step = 5 + st.rerun() + + return {"current_step": 4, "changes_made": True} \ No newline at end of file diff --git a/lib/utils/api_key_manager/components/website_setup.py b/lib/utils/api_key_manager/components/website_setup.py new file mode 100644 index 00000000..6ae840ca --- /dev/null +++ b/lib/utils/api_key_manager/components/website_setup.py @@ -0,0 +1,266 @@ +"""Website setup component for the API key manager.""" + +import streamlit as st +from loguru import logger +from ...website_analyzer import analyze_website +from ...website_analyzer.seo_analyzer import analyze_seo +import asyncio +import sys +from typing import Dict, Any +from ..manager import APIKeyManager +from .base import render_navigation_buttons + +# Configure logger to output to both file and stdout +logger.remove() # Remove default handler +logger.add( + "logs/website_setup.log", + rotation="500 MB", + retention="10 days", + level="DEBUG", + format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {message}" +) +logger.add( + sys.stdout, + level="INFO", + format="No website URL? No problem! You can provide written samples of your content instead.
+Share your best articles, blog posts, or any content that represents your writing style.
+{time:YYYY-MM-DD HH:mm:ss} |{level: <8} |{message} " +) + +def render_website_setup(api_key_manager: APIKeyManager) -> Dict[str, Any]: + """Render the website setup step. + + Args: + api_key_manager (APIKeyManager): The API key manager instance + + Returns: + Dict[str, Any]: Current state + """ + logger.info("[render_website_setup] Rendering website setup component") + + st.markdown("### Step 2: Website Setup") + + # Create two columns for input and results + col1, col2 = st.columns([1, 1]) + + with col1: + st.markdown("#### Enter Website URL") + url = st.text_input("Website URL", placeholder="https://example.com") + logger.debug(f"[render_website_setup] URL input value: {url}") + + analyze_type = st.radio( + "Analysis Type", + ["Basic Analysis", "Full Analysis with SEO"], + help="Choose between basic website analysis or comprehensive SEO analysis" + ) + + if st.button("Analyze Website"): + if url: + with st.spinner("Analyzing website..."): + try: + logger.info(f"[render_website_setup] Starting website analysis for URL: {url}") + + # Call the analyze_website function + results = analyze_website(url) + + # If full analysis is selected, add SEO analysis + if analyze_type == "Full Analysis with SEO": + seo_results = analyze_seo(url) + if seo_results.success: + results['data']['seo_analysis'] = { + 'overall_score': seo_results.overall_score, + 'meta_tags': { + 'title': seo_results.meta_tags.title, + 'description': seo_results.meta_tags.description, + 'keywords': seo_results.meta_tags.keywords, + 'has_robots': seo_results.meta_tags.has_robots, + 'has_sitemap': seo_results.meta_tags.has_sitemap + }, + 'content': { + 'word_count': seo_results.content.word_count, + 'readability_score': seo_results.content.readability_score, + 'content_quality_score': seo_results.content.content_quality_score, + 'headings_structure': seo_results.content.headings_structure, + 'keyword_density': seo_results.content.keyword_density + }, + 'recommendations': [ + { + 'priority': rec.priority, + 'category': rec.category, + 'issue': rec.issue, + 'recommendation': rec.recommendation, + 'impact': rec.impact + } + for rec in seo_results.recommendations + ] + } + + logger.debug(f"[render_website_setup] Analysis results received: {results.get('success', False)}") + + # Store results in session state + st.session_state.website_analysis = results + logger.info("[render_website_setup] Results stored in session state") + + if not results.get('success', False): + error_msg = results.get('error', 'Analysis failed') + logger.error(f"[render_website_setup] Analysis failed: {error_msg}") + st.error(error_msg) + else: + logger.info("[render_website_setup] Analysis completed successfully") + st.success("β Website analysis completed successfully!") + except Exception as e: + error_msg = f"Analysis failed: {str(e)}" + logger.error(f"[render_website_setup] {error_msg}") + st.error(error_msg) + else: + logger.warning("[render_website_setup] No URL provided") + st.warning("Please enter a valid URL") + + with col2: + st.markdown("#### Analysis Results") + + # Check if we have analysis results + if 'website_analysis' in st.session_state: + results = st.session_state.website_analysis + + if results.get('success', False): + data = results.get('data', {}) + analysis = data.get('analysis', {}) + + # Create tabs for different sections + if analyze_type == "Full Analysis with SEO": + tab1, tab2, tab3, tab4, tab5 = st.tabs([ + "Basic Metrics", + "Content Analysis", + "SEO Analysis", + "Technical SEO", + "Strategy" + ]) + else: + tab1, tab2, tab3, tab4 = st.tabs([ + "Basic Metrics", + "Content Analysis", + "Technical Info", + "Strategy" + ]) + + with tab1: + st.markdown("##### Basic Metrics") + basic_info = analysis.get('basic_info', {}) + st.write(f"Status Code: {basic_info.get('status_code')}") + st.write(f"Content Type: {basic_info.get('content_type')}") + st.write(f"Title: {basic_info.get('title')}") + st.write(f"Meta Description: {basic_info.get('meta_description')}") + + # SSL Info + ssl_info = analysis.get('ssl_info', {}) + if ssl_info.get('has_ssl'): + st.success("SSL Certificate is valid") + st.write(f"Expiry: {ssl_info.get('expiry')}") + else: + st.error("No valid SSL certificate found") + + with tab2: + st.markdown("##### Content Analysis") + content_info = analysis.get('content_info', {}) + + # Content Overview + st.markdown("###### π Content Overview") + col1, col2, col3, col4 = st.columns(4) + with col1: + st.metric("Word Count", content_info.get('word_count', 0)) + with col2: + st.metric("Headings", content_info.get('heading_count', 0)) + with col3: + st.metric("Images", content_info.get('image_count', 0)) + with col4: + st.metric("Links", content_info.get('link_count', 0)) + + if analyze_type == "Full Analysis with SEO": + with tab3: + st.markdown("##### SEO Analysis") + seo_data = data.get('seo_analysis', {}) + + # Display SEO Score + seo_score = seo_data.get('overall_score', 0) + st.markdown(f"### SEO Score: {seo_score}/100") + st.progress(seo_score / 100) + + # Meta Tags Analysis + st.markdown("#### Meta Tags Analysis") + meta_analysis = seo_data.get('meta_tags', {}) + for key, value in meta_analysis.items(): + if isinstance(value, bool): + st.write(f"{'β ' if value else 'β'} {key.replace('_', ' ').title()}") + elif isinstance(value, dict): + st.write(f"**{key.replace('_', ' ').title()}:**") + st.write(f"Status: {value.get('status', 'N/A')}") + st.write(f"Value: {value.get('value', 'N/A')}") + if value.get('recommendation'): + st.write(f"Recommendation: {value['recommendation']}") + else: + st.write(f"**{key.replace('_', ' ').title()}:** {value}") + + # Content Analysis + st.markdown("#### AI Content Analysis") + content_analysis = seo_data.get('content', {}) + st.write(f"**Word Count:** {content_analysis.get('word_count', 0)}") + st.write(f"**Readability Score:** {content_analysis.get('readability_score', 0)}/100") + st.write(f"**Content Quality Score:** {content_analysis.get('content_quality_score', 0)}/100") + + # Recommendations + st.markdown("#### SEO Recommendations") + recommendations = seo_data.get('recommendations', []) + for rec in recommendations: + st.write(f"**{rec.get('priority', '').upper()} Priority - {rec.get('category', '')}**") + st.write(f"Issue: {rec.get('issue', '')}") + st.write(f"Recommendation: {rec.get('recommendation', '')}") + st.write(f"Impact: {rec.get('impact', '')}") + st.write("---") + + with tab4: + st.markdown("##### Technical SEO") + technical_seo = seo_data.get('technical_analysis', {}) + + # Mobile Friendliness + st.markdown("#### Mobile Friendliness") + mobile_friendly = technical_seo.get('mobile_friendly', False) + st.write(f"{'β ' if mobile_friendly else 'β'} Mobile Friendly") + + # Page Speed + st.markdown("#### Page Speed") + speed_metrics = technical_seo.get('speed_metrics', {}) + for metric, value in speed_metrics.items(): + st.write(f"**{metric.replace('_', ' ').title()}:** {value}") + + # Technical Issues + st.markdown("#### Technical Issues") + issues = technical_seo.get('issues', []) + for issue in issues: + st.write(f"β’ {issue}") + + with tab4 if analyze_type == "Basic Analysis" else tab5: + st.markdown("##### Strategy Recommendations") + strategy_info = analysis.get('strategy', {}) + + if strategy_info: + for category, recommendations in strategy_info.items(): + st.markdown(f"###### {category.replace('_', ' ').title()}") + for rec in recommendations: + st.write(f"β’ {rec}") + else: + st.info("No strategy recommendations available") + else: + error_msg = results.get('error', 'Analysis failed') + logger.error(f"[render_website_setup] Displaying error: {error_msg}") + st.error(error_msg) + else: + logger.debug("[render_website_setup] No analysis results in session state") + st.info("Enter a URL and click 'Analyze Website' to see results") + + # Navigation buttons + if render_navigation_buttons(2, 5, True): + # Move to next step (AI Research Setup) + st.session_state.current_step = 3 + st.session_state.next_step = "ai_research_setup" + st.rerun() + + return {"current_step": 2, "changes_made": True} \ No newline at end of file diff --git a/lib/utils/api_key_manager/key_rotation.py b/lib/utils/api_key_manager/key_rotation.py new file mode 100644 index 00000000..e0b9979f --- /dev/null +++ b/lib/utils/api_key_manager/key_rotation.py @@ -0,0 +1,121 @@ +"""API Key Rotation Manager.""" + +from datetime import datetime +from typing import Dict, Optional, List +import streamlit as st +from .health_monitor import APIKeyHealthMonitor +from .wizard_state import get_api_keys, set_api_key + +class KeyRotationManager: + """Manages automatic rotation of API keys based on health metrics.""" + + def __init__(self): + """Initialize the key rotation manager.""" + self.health_monitor = APIKeyHealthMonitor() + if 'active_keys' not in st.session_state: + st.session_state.active_keys = {} + + def get_active_key(self, key_type: str) -> str: + """Get the currently active key for a given type.""" + return st.session_state.active_keys.get(key_type) + + def set_active_key(self, key_type: str, key_name: str) -> None: + """Set the active key for a given type.""" + st.session_state.active_keys[key_type] = key_name + + def rotate_if_needed(self, key_type: str) -> Optional[str]: + """Check and rotate key if needed based on health metrics.""" + current_key = self.get_active_key(key_type) + + # If no current key or current key needs rotation + if not current_key or self.health_monitor.should_rotate_key(current_key): + new_key = self.health_monitor.get_best_available_key(key_type) + + if new_key and new_key != current_key: + # Set cooldown on the old key if it exists + if current_key: + self.health_monitor.set_cooldown(current_key, duration_minutes=30) + + # Update the active key + self.set_active_key(key_type, new_key) + return new_key + + return current_key + + def get_rotation_status(self) -> Dict[str, Dict]: + """Get rotation status for all key types.""" + status = {} + api_keys = get_api_keys() + + for key_name in api_keys: + key_type = key_name.split('_')[0] # e.g., OPENAI from OPENAI_API_KEY + + active_key = self.get_active_key(key_type) + health = self.health_monitor.get_key_health(key_name) + + if key_type not in status: + status[key_type] = { + 'active_key': active_key, + 'available_keys': [], + 'cooldown_keys': [] + } + + if health and health['in_cooldown']: + status[key_type]['cooldown_keys'].append(key_name) + else: + status[key_type]['available_keys'].append(key_name) + + return status + + def display_rotation_dashboard(self) -> None: + """Display the key rotation dashboard.""" + st.subheader("π API Key Rotation Status") + + rotation_status = self.get_rotation_status() + if not rotation_status: + st.info("No API keys configured for rotation.") + return + + for key_type, status in rotation_status.items(): + with st.expander(f"{key_type} Rotation Status"): + # Active Key + st.write("**Active Key:**") + if status['active_key']: + st.success(status['active_key']) + else: + st.warning("No active key") + + # Available Keys + st.write("**Available Keys:**") + if status['available_keys']: + for key in status['available_keys']: + st.write(f"- {key}") + else: + st.warning("No available keys") + + # Cooldown Keys + if status['cooldown_keys']: + st.write("**Keys in Cooldown:**") + for key in status['cooldown_keys']: + health = self.health_monitor.get_key_health(key) + if health and health['cooldown_until']: + time_left = (health['cooldown_until'] - datetime.now()) + minutes_left = int(time_left.total_seconds() / 60) + st.info(f"- {key} (Cooldown: {minutes_left} minutes remaining)") + + def initialize_rotation(self) -> None: + """Initialize key rotation for all API key types.""" + api_keys = get_api_keys() + key_types = set() + + # Get unique key types + for key_name in api_keys: + key_type = key_name.split('_')[0] + key_types.add(key_type) + + # Initialize rotation for each key type + for key_type in key_types: + if not self.get_active_key(key_type): + best_key = self.health_monitor.get_best_available_key(key_type) + if best_key: + self.set_active_key(key_type, best_key) \ No newline at end of file diff --git a/lib/utils/api_key_manager/manager.py b/lib/utils/api_key_manager/manager.py new file mode 100644 index 00000000..213c4d27 --- /dev/null +++ b/lib/utils/api_key_manager/manager.py @@ -0,0 +1,149 @@ +"""API key manager class.""" + +from typing import Dict, Any, Optional +from loguru import logger +import streamlit as st +import os +import json +import sys +from datetime import datetime +from dotenv import load_dotenv + +# Configure logger to output to both file and stdout +logger.remove() # Remove default handler +logger.add("logs/api_key_manager.log", + format="{time:YYYY-MM-DD HH:mm:ss} |{level: <8} |{name} :{function} :{line} -{message} ", + level="DEBUG") +logger.add(sys.stdout, + format="{time:YYYY-MM-DD HH:mm:ss} |{level: <8} |{name} :{function} :{line} -{message} ", + level="INFO") + +class APIKeyManager: + """Manager for handling API keys.""" + + def __init__(self): + """Initialize the API key manager.""" + logger.info("[APIKeyManager.__init__] Initializing API key manager") + self.api_keys = {} + self.load_api_keys() + self.api_key_groups = { + "Create": { + "GEMINI_API_KEY": { + "url": "https://makersuite.google.com/app/apikey", + "description": "Google's Gemini AI for content generation", + "setup_steps": [ + "Visit Google AI Studio", + "Create a Google Cloud account", + "Enable Gemini API", + "Generate API key" + ] + }, + "OPENAI_API_KEY": { + "url": "https://platform.openai.com/api-keys", + "description": "OpenAI's GPT models for content creation", + "setup_steps": [ + "Go to OpenAI platform", + "Create an account", + "Navigate to API keys", + "Create new API key" + ] + }, + "MISTRAL_API_KEY": { + "url": "https://console.mistral.ai/api-keys/", + "description": "Mistral AI for efficient content generation", + "setup_steps": [ + "Visit Mistral AI website", + "Sign up for an account", + "Access API section", + "Generate API key" + ] + } + }, + "Research": { + "TAVILY_API_KEY": { + "url": "https://tavily.com/#api", + "description": "Powers intelligent web research features", + "setup_steps": [ + "Go to Tavily's website", + "Create an account", + "Access your API dashboard", + "Generate a new API key" + ] + }, + "SERPER_API_KEY": { + "url": "https://serper.dev/signup", + "description": "Enables Google search functionality", + "setup_steps": [ + "Visit Serper.dev", + "Sign up for an account", + "Go to API section", + "Create your API key" + ] + } + }, + "Deep Search": { + "METAPHOR_API_KEY": { + "url": "https://dashboard.exa.ai/login", + "description": "Enables advanced web search capabilities", + "setup_steps": [ + "Visit the Exa AI dashboard", + "Sign up for a free account", + "Navigate to API Keys section", + "Create a new API key" + ] + }, + "FIRECRAWL_API_KEY": { + "url": "https://www.firecrawl.dev/account", + "description": "Enables web content extraction", + "setup_steps": [ + "Visit Firecrawl website", + "Sign up for an account", + "Access API dashboard", + "Create your API key" + ] + } + }, + "Integrations": { + "STABILITY_API_KEY": { + "url": "https://platform.stability.ai/", + "description": "Enables AI image generation", + "setup_steps": [ + "Access Stability AI platform", + "Create an account", + "Navigate to API settings", + "Generate your API key" + ] + } + } + } + + def load_api_keys(self): + """Load API keys from environment variables.""" + logger.info("[APIKeyManager.load_api_keys] Loading API keys from environment") + try: + # Load from environment variables + self.api_keys = { + "openai": os.getenv("OPENAI_API_KEY", ""), + "google": os.getenv("GOOGLE_API_KEY", ""), + "tavily": os.getenv("TAVILY_API_KEY", ""), + "metaphor": os.getenv("METAPHOR_API_KEY", ""), + "mistral": os.getenv("MISTRAL_API_KEY", "") + } + logger.info("[APIKeyManager.load_api_keys] Successfully loaded API keys") + except Exception as e: + logger.error(f"[APIKeyManager.load_api_keys] Error loading API keys: {str(e)}") + + def save_api_key(self, provider: str, key: str): + """Save an API key.""" + logger.info(f"[APIKeyManager.save_api_key] Saving API key for provider: {provider}") + try: + self.api_keys[provider] = key + # Save to environment variable + os.environ[f"{provider.upper()}_API_KEY"] = key + logger.info(f"[APIKeyManager.save_api_key] Successfully saved API key for {provider}") + except Exception as e: + logger.error(f"[APIKeyManager.save_api_key] Error saving API key: {str(e)}") + + def get_api_key(self, provider: str) -> Optional[str]: + """Get an API key.""" + return self.api_keys.get(provider) \ No newline at end of file diff --git a/lib/utils/api_key_manager/state.py b/lib/utils/api_key_manager/state.py new file mode 100644 index 00000000..6915529d --- /dev/null +++ b/lib/utils/api_key_manager/state.py @@ -0,0 +1,37 @@ +"""State management for the API key manager.""" + +import streamlit as st +from datetime import datetime + +def initialize_wizard_state(): + """Initialize or get the wizard state from session.""" + if 'wizard_state' not in st.session_state: + st.session_state.wizard_state = { + 'current_step': 0, + 'total_steps': 0, + 'completed_steps': set(), + 'api_keys_status': {}, + 'setup_progress': 0 + } + +def update_progress(api_keys_config): + """Update the overall setup progress.""" + total_keys = sum(len(keys) for keys in api_keys_config.values()) + configured_keys = sum(1 for status in st.session_state.wizard_state['api_keys_status'].values() + if status.get('configured', False)) + st.session_state.wizard_state['setup_progress'] = (configured_keys / total_keys) * 100 + +def update_key_status(key): + """Update the status of an API key in the wizard state.""" + st.session_state.wizard_state['api_keys_status'][key] = { + 'configured': True, + 'timestamp': datetime.now().isoformat() + } + +def get_key_status(key): + """Get the current status of an API key.""" + return st.session_state.wizard_state['api_keys_status'].get(key, {}) + +def get_progress(): + """Get the current setup progress.""" + return st.session_state.wizard_state['setup_progress'] \ No newline at end of file diff --git a/lib/utils/api_key_manager/styles.py b/lib/utils/api_key_manager/styles.py new file mode 100644 index 00000000..58ad020f --- /dev/null +++ b/lib/utils/api_key_manager/styles.py @@ -0,0 +1,482 @@ +API_KEY_MANAGER_STYLES = """ + +""" \ No newline at end of file diff --git a/lib/utils/api_key_manager/validation.py b/lib/utils/api_key_manager/validation.py new file mode 100644 index 00000000..a8dd812b --- /dev/null +++ b/lib/utils/api_key_manager/validation.py @@ -0,0 +1,95 @@ +"""API key validation module.""" + +from typing import Dict, Any, List, Tuple +from loguru import logger +import os +from dotenv import load_dotenv +from .manager import APIKeyManager + +def check_all_api_keys(api_key_manager: APIKeyManager) -> bool: + """Check if minimum required API keys are present. + + Args: + api_key_manager (APIKeyManager): The API key manager instance + + Returns: + bool: True if minimum required keys are present (at least one AI provider and one research provider) + """ + try: + # Load environment variables + logger.info("Starting API key validation process...") + + # Get the current working directory and .env file path + current_dir = os.getcwd() + env_path = os.path.join(current_dir, '.env') + logger.info(f"Looking for .env file at: {env_path}") + + # Check if .env file exists + if not os.path.exists(env_path): + logger.error(f".env file not found at {env_path}") + return False + + # Load environment variables + load_dotenv(env_path) + logger.debug("Environment variables loaded") + + # Log all environment variables (without their values) + logger.debug("Available environment variables:") + for key in os.environ.keys(): + if any(provider in key for provider in ['API_KEY', 'SERPAPI', 'TAVILY', 'METAPHOR', 'FIRECRAWL']): + logger.debug(f"Found environment variable: {key}") + + # Step 1: Check for at least one AI provider + logger.info("Checking AI provider API keys...") + ai_providers = [ + 'OPENAI_API_KEY', + 'GEMINI_API_KEY', + 'ANTHROPIC_API_KEY', + 'MISTRAL_API_KEY' + ] + + # Log which AI providers are found + for provider in ai_providers: + value = os.getenv(provider) + if value: + logger.info(f"Found {provider} (length: {len(value)})") + else: + logger.debug(f"Missing {provider}") + + has_ai_provider = any(os.getenv(key) for key in ai_providers) + if not has_ai_provider: + logger.warning("No AI provider API key found") + return False + else: + logger.success("β At least one AI provider key found") + + # Step 2: Check for at least one research provider + logger.info("Checking research provider API keys...") + research_providers = [ + 'SERPAPI_KEY', + 'TAVILY_API_KEY', + 'METAPHOR_API_KEY', + 'FIRECRAWL_API_KEY' + ] + + # Log which research providers are found + for provider in research_providers: + value = os.getenv(provider) + if value: + logger.info(f"Found {provider} (length: {len(value)})") + else: + logger.debug(f"Missing {provider}") + + has_research_provider = any(os.getenv(key) for key in research_providers) + if not has_research_provider: + logger.warning("No research provider API key found") + return False + else: + logger.success("β At least one research provider key found") + + logger.success("All required API keys validated successfully!") + return True + + except Exception as e: + logger.error(f"Error checking API keys: {str(e)}", exc_info=True) + return False \ No newline at end of file diff --git a/lib/utils/api_key_manager/wizard_state.py b/lib/utils/api_key_manager/wizard_state.py new file mode 100644 index 00000000..cb8ff282 --- /dev/null +++ b/lib/utils/api_key_manager/wizard_state.py @@ -0,0 +1,92 @@ +"""Wizard state management for the API key manager.""" + +import streamlit as st +from loguru import logger + +def initialize_wizard_state(): + """Initialize or get the wizard state from session.""" + if 'wizard_state' not in st.session_state: + st.session_state.wizard_state = { + 'current_step': 0, + 'total_steps': 0, + 'completed_steps': set(), + 'api_keys_status': {}, + 'setup_progress': 0 + } + logger.info("Initialized wizard state") + +def get_current_step(): + """Get the current step from the wizard state.""" + return st.session_state.wizard_state.get('current_step', 0) + +def next_step(): + """Move to the next step in the wizard.""" + current_step = get_current_step() + st.session_state.wizard_state['current_step'] = current_step + 1 + st.session_state.wizard_state['completed_steps'].add(current_step) + logger.info(f"Moving to next step: {current_step + 1}") + +def previous_step(): + """Move to the previous step in the wizard.""" + current_step = get_current_step() + if current_step > 0: + st.session_state.wizard_state['current_step'] = current_step - 1 + st.session_state.wizard_state['completed_steps'].discard(current_step - 1) + logger.info(f"Moving to previous step: {current_step - 1}") + +def update_progress(): + """Update the overall setup progress.""" + total_steps = st.session_state.wizard_state.get('total_steps', 0) + completed_steps = len(st.session_state.wizard_state.get('completed_steps', set())) + if total_steps > 0: + progress = (completed_steps / total_steps) * 100 + st.session_state.wizard_state['setup_progress'] = progress + logger.info(f"Updated progress: {progress:.1f}%") + +def is_step_completed(step): + """Check if a specific step is completed.""" + return step in st.session_state.wizard_state.get('completed_steps', set()) + +def get_step_status(step): + """Get the status of a specific step.""" + current_step = get_current_step() + if step < current_step: + return "completed" + elif step == current_step: + return "current" + else: + return "pending" + +def can_proceed_to_next_step(): + """Check if the user can proceed to the next step.""" + current_step = get_current_step() + + if current_step == 1: + # Get selected providers + selected_providers = get_selected_providers() + + # If no providers are selected, cannot proceed + if not selected_providers: + return False + + # Check if at least one selected provider has a valid API key + for provider in selected_providers: + validation_status = get_validation_status(provider) + if validation_status and validation_status.get('is_valid', False): + return True + + return False + + elif current_step == 2: + # Website URL is now optional + return True + + elif current_step == 3: + # AI Research setup - both Tavily and Metaphor are optional + return True + + elif current_step == 4: + # Final setup - always allow proceeding + return True + + return False diff --git a/lib/utils/content_generators.py b/lib/utils/content_generators.py index c26ccf6b..1af34e82 100644 --- a/lib/utils/content_generators.py +++ b/lib/utils/content_generators.py @@ -5,7 +5,7 @@ from lib.utils.alwrity_utils import ( ) from lib.ai_writers.ai_story_writer.story_writer import story_input_section from lib.ai_writers.ai_product_description_writer import write_ai_prod_desc -from lib.content_planning_calender.content_planning_agents_alwrity_crew import ai_agents_content_planner +#from lib.content_planning_calender.content_planning_agents_alwrity_crew import ai_agents_content_planner from lib.utils.seo_tools import ai_seo_tools @@ -62,6 +62,7 @@ def content_planning_tools(): ) if st.button("**Ideate Content Calender**"): if plan_keywords: - ai_agents_content_planner(plan_keywords) + #ai_agents_content_planner(plan_keywords) + st.header("COming Soon.") else: st.error("Come on, really, Enter some keywords to plan on..") diff --git a/lib/utils/read_main_config_params.py b/lib/utils/read_main_config_params.py index c283652c..dde24e2f 100644 --- a/lib/utils/read_main_config_params.py +++ b/lib/utils/read_main_config_params.py @@ -3,6 +3,8 @@ import sys import json from pathlib import Path from loguru import logger +import yaml + logger.remove() logger.add(sys.stdout, colorize=True, @@ -30,7 +32,6 @@ def read_return_config_section(config_section): with open(config_path, 'r', encoding="utf-8") as file: config = json.load(file) - if config_section == 'system_prompt': prompt_file_path = os.path.join(os.getcwd(), 'lib', 'workspace', 'alwrity_prompts', 'alwrity_system_instruction.prompts') with open(prompt_file_path, 'r') as file: @@ -81,3 +82,30 @@ def read_return_config_section(config_section): except Exception as err: logger.error(f"An unexpected error occurred: {err}") raise + +def get_personalization_settings(): + """Get personalization settings from ALWRITY_CONFIG.""" + try: + config_path = Path(os.environ["ALWRITY_CONFIG"]) + config = yaml.safe_load(config_path.read_text()) + return config.get('personalization', {}) + except Exception as e: + logger.error(f"Error reading personalization settings: {str(e)}") + return {} + +def save_personalization_settings(settings): + """Save personalization settings to ALWRITY_CONFIG.""" + try: + config_path = Path(os.environ["ALWRITY_CONFIG"]) + config = yaml.safe_load(config_path.read_text()) + + # Update personalization section + config['personalization'] = settings + + # Save back to file + config_path.write_text(yaml.dump(config, default_flow_style=False)) + logger.info("Personalization settings saved successfully") + + except Exception as e: + logger.error(f"Error saving personalization settings: {str(e)}") + raise diff --git a/lib/utils/ui_setup.py b/lib/utils/ui_setup.py index 799dde53..fb54c9fd 100644 --- a/lib/utils/ui_setup.py +++ b/lib/utils/ui_setup.py @@ -1,21 +1,74 @@ import os import streamlit as st -from .file_processor import load_image -from .content_generators import content_planning_tools, ai_writers -from .alwrity_utils import ai_agents_team, ai_social_writer -from .seo_tools import ai_seo_tools +from lib.utils.file_processor import load_image +from lib.utils.content_generators import content_planning_tools, ai_writers +from lib.utils.alwrity_utils import ai_social_writer +from lib.utils.seo_tools import ai_seo_tools def setup_ui(): - """Sets up the Streamlit UI with custom CSS and logo.""" - try: - css_file_path = os.path.join('lib', 'workspace', 'alwrity_ui_styling.css') - with open(css_file_path) as f: - custom_css = f.read() - st.set_page_config(page_title="Alwrity", layout="wide") - st.markdown(f'', unsafe_allow_html=True) - except Exception as err: - st.error(f"Failed in setting up Alwrity Streamlit UI: {err}") + """Set up the UI with custom styling.""" + # Add custom CSS + st.markdown(""" + + """, unsafe_allow_html=True) image_base64 = load_image("lib/workspace/alwrity_logo.png") st.markdown(f""" @@ -37,8 +90,9 @@ def setup_tabs(): ai_writers() with tab3: - ai_agents_team() - + #ai_agents_team() + st.subheader("Agents Teams") + with tab4: ai_seo_tools() diff --git a/lib/utils/website_analyzer/README.md b/lib/utils/website_analyzer/README.md new file mode 100644 index 00000000..5ad4568b --- /dev/null +++ b/lib/utils/website_analyzer/README.md @@ -0,0 +1,181 @@ +# Website Analyzer Module + +A comprehensive website analysis toolkit that provides detailed insights into website performance, SEO metrics, and content quality. This module combines traditional web analysis techniques with AI-powered content evaluation to deliver actionable recommendations. + +## Features + +### 1. Comprehensive Website Analysis +- Basic website information extraction +- SSL/TLS certificate validation +- DNS record analysis +- WHOIS information retrieval +- Content analysis and structure evaluation +- Performance metrics assessment + +### 2. Advanced SEO Analysis +- Meta tag optimization analysis +- Content quality evaluation +- Keyword density analysis +- Readability scoring +- Heading structure analysis +- AI-powered content recommendations + +### 3. Technical Infrastructure +- Asynchronous web crawling +- Multi-threaded analysis +- Robust error handling +- Comprehensive logging +- Type-safe data models + +## Module Structure + +### 1. `analyzer.py` +The main analysis engine that provides comprehensive website analysis. + +#### Key Components: +- `WebsiteAnalyzer` class + - URL validation + - Basic website information extraction + - SSL/TLS certificate checking + - DNS record analysis + - WHOIS information retrieval + - Content analysis + - Performance metrics assessment + +#### Features: +- Concurrent analysis using ThreadPoolExecutor +- Robust error handling and logging +- User-agent simulation for reliable scraping +- Timeout handling for requests +- Comprehensive result formatting + +### 2. `seo_analyzer.py` +Specialized SEO analysis module with AI integration. + +#### Key Components: +- `extract_content()`: Fetches and parses webpage content +- `analyze_meta_tags()`: Evaluates meta tags and SEO elements +- `analyze_content_with_ai()`: AI-powered content analysis +- `analyze_seo()`: Main SEO analysis function + +#### Features: +- Meta tag optimization analysis +- Content quality scoring +- Keyword density analysis +- Readability evaluation +- AI-powered recommendations +- Weighted scoring system + +### 3. `models.py` +Data models for structured analysis results. + +#### Key Components: +- `SEORecommendation`: Individual SEO recommendations +- `MetaTagAnalysis`: Meta tag analysis results +- `ContentAnalysis`: Content analysis metrics +- `SEOAnalysisResult`: Complete analysis results + +#### Features: +- Type-safe data structures +- Clear data organization +- Easy serialization/deserialization +- Comprehensive documentation + +## Usage Examples + +### Basic Website Analysis +```python +from website_analyzer import analyze_website + +# Analyze a website +results = analyze_website("https://example.com") + +# Access analysis results +if results["success"]: + data = results["data"] + print(f"Domain: {data['domain']}") + print(f"SSL Info: {data['analysis']['ssl_info']}") + print(f"Content Info: {data['analysis']['content_info']}") +``` + +### SEO Analysis +```python +from website_analyzer.seo_analyzer import analyze_seo + +# Perform SEO analysis +seo_results = analyze_seo("https://example.com", "your-openai-api-key") + +# Access SEO results +if seo_results.success: + print(f"Overall Score: {seo_results.overall_score}") + print(f"Meta Tags: {seo_results.meta_tags}") + print(f"Content Analysis: {seo_results.content}") + print(f"Recommendations: {seo_results.recommendations}") +``` + +## Dependencies + +- `requests`: HTTP requests +- `beautifulsoup4`: HTML parsing +- `python-whois`: WHOIS information +- `dnspython`: DNS record analysis +- `openai`: AI-powered analysis +- `loguru`: Logging +- `typing`: Type hints +- `dataclasses`: Data models + +## Error Handling + +The module implements comprehensive error handling: +- URL validation +- Request timeouts +- Connection errors +- Parsing errors +- API errors +- DNS resolution errors +- SSL/TLS errors + +All errors are logged and returned in a structured format for easy handling. + +## Logging + +The module uses `loguru` for logging with the following features: +- File rotation (500 MB) +- 10-day retention +- Debug level logging +- Structured log format +- Both file and stdout output + +## Best Practices + +1. **API Key Management** + - Store API keys securely + - Use environment variables + - Implement rate limiting + +2. **Error Handling** + - Always check success status + - Handle errors gracefully + - Log errors appropriately + +3. **Performance** + - Use concurrent analysis + - Implement timeouts + - Cache results when possible + +4. **Rate Limiting** + - Respect website robots.txt + - Implement delays between requests + - Use appropriate user agents + +## Contributing + +1. Fork the repository +2. Create a feature branch +3. Commit your changes +4. Push to the branch +5. Create a Pull Request + +## License + +This module is part of the ALwrity project and is licensed under the MIT License. \ No newline at end of file diff --git a/lib/utils/website_analyzer/__init__.py b/lib/utils/website_analyzer/__init__.py new file mode 100644 index 00000000..7db5cc2e --- /dev/null +++ b/lib/utils/website_analyzer/__init__.py @@ -0,0 +1,7 @@ +"""Website analyzer module for AI-powered website analysis.""" + +from .analyzer import analyze_website +from .seo_analyzer import analyze_seo +from .models import SEOAnalysisResult + +__all__ = ['analyze_seo', 'SEOAnalysisResult', 'analyze_website'] \ No newline at end of file diff --git a/lib/utils/website_analyzer/analyzer.py b/lib/utils/website_analyzer/analyzer.py new file mode 100644 index 00000000..f5869af8 --- /dev/null +++ b/lib/utils/website_analyzer/analyzer.py @@ -0,0 +1,323 @@ +"""Website scraping and AI analysis module.""" + +import asyncio +from typing import Dict, List, Optional +from bs4 import BeautifulSoup +from urllib.parse import urljoin, urlparse +import streamlit as st +import re +from loguru import logger +from ...web_crawlers.async_web_crawler import AsyncWebCrawlerService +from ...gpt_providers.text_generation.main_text_generation import llm_text_gen +import os +import sys +import logging +import json +from datetime import datetime +import requests +import ssl +import socket +import whois +import dns.resolver +from requests.exceptions import RequestException +from concurrent.futures import ThreadPoolExecutor + +# Configure logging +logging.basicConfig( + level=logging.DEBUG, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + handlers=[ + logging.StreamHandler(), + logging.FileHandler('website_analyzer.log') + ] +) +logger = logging.getLogger(__name__) + +def analyze_website(url: str) -> Dict: + """ + Analyze a website and return comprehensive results. + + Args: + url (str): The URL to analyze + + Returns: + Dict: Analysis results including various metrics and checks + """ + logger.info(f"Starting website analysis for URL: {url}") + try: + analyzer = WebsiteAnalyzer() + results = analyzer.analyze_website(url) + + # Add success status to results + if "error" in results: + return { + "success": False, + "error": results["error"] + } + + # Add success status and wrap results + return { + "success": True, + "data": results + } + except Exception as e: + logger.error(f"Error in analyze_website: {str(e)}", exc_info=True) + return { + "success": False, + "error": str(e) + } + +class WebsiteAnalyzer: + def __init__(self): + self.session = requests.Session() + self.session.headers.update({ + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' + }) + logger.info("WebsiteAnalyzer initialized") + + def analyze_website(self, url: str) -> Dict: + """ + Perform comprehensive analysis of a website. + + Args: + url (str): The URL to analyze + + Returns: + Dict: Analysis results including various metrics and checks + """ + logger.info(f"Starting analysis for URL: {url}") + try: + # Validate URL + if not self._validate_url(url): + logger.error(f"Invalid URL format: {url}") + return {"error": "Invalid URL format"} + + # Basic URL parsing + parsed_url = urlparse(url) + domain = parsed_url.netloc + logger.debug(f"Parsed domain: {domain}") + + # Initialize results dictionary + results = { + "url": url, + "domain": domain, + "timestamp": datetime.now().isoformat(), + "analysis": {} + } + + # Perform various analyses + with ThreadPoolExecutor(max_workers=4) as executor: + # Basic website info + basic_info = executor.submit(self._get_basic_info, url).result() + results["analysis"]["basic_info"] = basic_info + + # SSL/TLS info + ssl_info = executor.submit(self._check_ssl, domain).result() + results["analysis"]["ssl_info"] = ssl_info + + # DNS info + dns_info = executor.submit(self._check_dns, domain).result() + results["analysis"]["dns_info"] = dns_info + + # WHOIS info + whois_info = executor.submit(self._get_whois_info, domain).result() + results["analysis"]["whois_info"] = whois_info + + # Content analysis + content_info = executor.submit(self._analyze_content, url).result() + results["analysis"]["content_info"] = content_info + + # Performance metrics + performance = executor.submit(self._check_performance, url).result() + results["analysis"]["performance"] = performance + + logger.info(f"Analysis completed successfully for {url}") + return results + + except Exception as e: + logger.error(f"Error during website analysis: {str(e)}", exc_info=True) + return {"error": str(e)} + + def _validate_url(self, url: str) -> bool: + """Validate URL format.""" + try: + result = urlparse(url) + return all([result.scheme, result.netloc]) + except Exception as e: + logger.error(f"URL validation error: {str(e)}") + return False + + def _get_basic_info(self, url: str) -> Dict: + """Get basic website information.""" + logger.debug(f"Getting basic info for {url}") + try: + response = self.session.get(url, timeout=10) + response.raise_for_status() + + soup = BeautifulSoup(response.text, 'html.parser') + + return { + "status_code": response.status_code, + "content_type": response.headers.get('content-type', ''), + "title": soup.title.string if soup.title else '', + "meta_description": self._get_meta_description(soup), + "headers": dict(response.headers), + "robots_txt": self._get_robots_txt(url), + "sitemap": self._get_sitemap(url) + } + except Exception as e: + logger.error(f"Error getting basic info: {str(e)}", exc_info=True) + return {"error": str(e)} + + def _check_ssl(self, domain: str) -> Dict: + """Check SSL/TLS certificate information.""" + logger.debug(f"Checking SSL for {domain}") + try: + context = ssl.create_default_context() + with socket.create_connection((domain, 443)) as sock: + with context.wrap_socket(sock, server_hostname=domain) as ssock: + cert = ssock.getpeercert() + return { + "has_ssl": True, + "issuer": dict(x[0] for x in cert['issuer']), + "expiry": datetime.strptime(cert['notAfter'], '%b %d %H:%M:%S %Y %Z').isoformat(), + "version": cert['version'], + "subject": dict(x[0] for x in cert['subject']) + } + except Exception as e: + logger.error(f"SSL check error: {str(e)}", exc_info=True) + return {"has_ssl": False, "error": str(e)} + + def _check_dns(self, domain: str) -> Dict: + """Check DNS records.""" + logger.debug(f"Checking DNS for {domain}") + try: + records = {} + for record_type in ['A', 'AAAA', 'MX', 'NS', 'TXT']: + try: + answers = dns.resolver.resolve(domain, record_type) + records[record_type] = [str(rdata) for rdata in answers] + except dns.resolver.NoAnswer: + records[record_type] = [] + except Exception as e: + logger.warning(f"Error resolving {record_type} record: {str(e)}") + records[record_type] = [] + return records + except Exception as e: + logger.error(f"DNS check error: {str(e)}", exc_info=True) + return {"error": str(e)} + + def _get_whois_info(self, domain: str) -> Dict: + """Get WHOIS information for a domain.""" + try: + w = whois.whois(domain) + + def format_date(date_value): + if isinstance(date_value, list): + return date_value[0].isoformat() if date_value else 'Unknown' + return date_value.isoformat() if date_value else 'Unknown' + + return { + 'registrar': w.registrar if hasattr(w, 'registrar') else 'Unknown', + 'creation_date': format_date(w.creation_date), + 'expiration_date': format_date(w.expiration_date), + 'updated_date': format_date(w.updated_date) if hasattr(w, 'updated_date') else 'Unknown', + 'name_servers': w.name_servers if hasattr(w, 'name_servers') else [], + 'domain_name': w.domain_name if hasattr(w, 'domain_name') else domain, + 'text': w.text if hasattr(w, 'text') else '' + } + except Exception as e: + logger.error(f"WHOIS check error: {str(e)}") + return { + 'registrar': 'Unknown', + 'creation_date': 'Unknown', + 'expiration_date': 'Unknown', + 'updated_date': 'Unknown', + 'name_servers': [], + 'domain_name': domain, + 'text': '' + } + + def _analyze_content(self, url: str) -> Dict: + """Analyze website content.""" + logger.debug(f"Analyzing content for {url}") + try: + response = self.session.get(url, timeout=10) + response.raise_for_status() + soup = BeautifulSoup(response.text, 'html.parser') + + # Get all text content + text_content = soup.get_text() + + # Count words + words = re.findall(r'\w+', text_content.lower()) + word_count = len(words) + + # Count headings + headings = soup.find_all(['h1', 'h2', 'h3', 'h4', 'h5', 'h6']) + + # Count images + images = soup.find_all('img') + + # Count links + links = soup.find_all('a') + + return { + "word_count": word_count, + "heading_count": len(headings), + "image_count": len(images), + "link_count": len(links), + "has_meta_description": bool(self._get_meta_description(soup)), + "has_robots_txt": bool(self._get_robots_txt(url)), + "has_sitemap": bool(self._get_sitemap(url)) + } + except Exception as e: + logger.error(f"Content analysis error: {str(e)}", exc_info=True) + return {"error": str(e)} + + def _check_performance(self, url: str) -> Dict: + """Check website performance metrics.""" + logger.debug(f"Checking performance for {url}") + try: + start_time = datetime.now() + response = self.session.get(url, timeout=10) + end_time = datetime.now() + + load_time = (end_time - start_time).total_seconds() + + return { + "load_time": load_time, + "status_code": response.status_code, + "content_length": len(response.content), + "headers": dict(response.headers) + } + except Exception as e: + logger.error(f"Performance check error: {str(e)}", exc_info=True) + return {"error": str(e)} + + def _get_meta_description(self, soup: BeautifulSoup) -> Optional[str]: + """Extract meta description from HTML.""" + meta_desc = soup.find('meta', attrs={'name': 'description'}) + return meta_desc.get('content') if meta_desc else None + + def _get_robots_txt(self, url: str) -> Optional[str]: + """Get robots.txt content.""" + try: + robots_url = f"{url.rstrip('/')}/robots.txt" + response = self.session.get(robots_url, timeout=5) + if response.status_code == 200: + return response.text + except Exception as e: + logger.warning(f"Error fetching robots.txt: {str(e)}") + return None + + def _get_sitemap(self, url: str) -> Optional[str]: + """Get sitemap.xml content.""" + try: + sitemap_url = f"{url.rstrip('/')}/sitemap.xml" + response = self.session.get(sitemap_url, timeout=5) + if response.status_code == 200: + return response.text + except Exception as e: + logger.warning(f"Error fetching sitemap.xml: {str(e)}") + return None \ No newline at end of file diff --git a/lib/utils/website_analyzer/models.py b/lib/utils/website_analyzer/models.py new file mode 100644 index 00000000..b5b55054 --- /dev/null +++ b/lib/utils/website_analyzer/models.py @@ -0,0 +1,45 @@ +"""Data models for website analysis results.""" + +from dataclasses import dataclass +from typing import List, Dict, Optional +from datetime import datetime + +@dataclass +class SEORecommendation: + """A single SEO recommendation.""" + priority: str # 'high', 'medium', 'low' + category: str # 'content', 'technical', 'meta', etc. + issue: str + recommendation: str + impact: str + +@dataclass +class MetaTagAnalysis: + """Analysis of meta tags.""" + title: Dict[str, str] # {'status': 'good', 'value': 'actual title', 'recommendation': 'suggestion'} + description: Dict[str, str] + keywords: Dict[str, str] + has_robots: bool + has_sitemap: bool + +@dataclass +class ContentAnalysis: + """Analysis of page content.""" + word_count: int + headings_structure: Dict[str, int] # {'h1': 1, 'h2': 3, etc} + keyword_density: Dict[str, float] + readability_score: float + content_quality_score: float + +@dataclass +class SEOAnalysisResult: + """Complete SEO analysis result.""" + url: str + analyzed_at: datetime + overall_score: float # 0-100 + meta_tags: MetaTagAnalysis + content: ContentAnalysis + recommendations: List[SEORecommendation] + errors: List[str] + warnings: List[str] + success: bool \ No newline at end of file diff --git a/lib/utils/website_analyzer/seo_analyzer.py b/lib/utils/website_analyzer/seo_analyzer.py new file mode 100644 index 00000000..dac2ae05 --- /dev/null +++ b/lib/utils/website_analyzer/seo_analyzer.py @@ -0,0 +1,233 @@ +"""SEO analyzer module with AI integration.""" + +import requests +from bs4 import BeautifulSoup +from datetime import datetime +from typing import Dict, List, Tuple, Optional +from urllib.parse import urlparse +import openai +from loguru import logger +import os +from dotenv import load_dotenv +from .models import ( + SEOAnalysisResult, + MetaTagAnalysis, + ContentAnalysis, + SEORecommendation +) + +def extract_content(url: str) -> Tuple[Optional[str], Optional[BeautifulSoup], List[str]]: + """Extract content from URL.""" + errors = [] + try: + response = requests.get(url, timeout=10) + response.raise_for_status() + soup = BeautifulSoup(response.text, 'html.parser') + return response.text, soup, errors + except requests.RequestException as e: + error_msg = f"Error fetching URL: {str(e)}" + logger.error(error_msg) + errors.append(error_msg) + return None, None, errors + +def analyze_meta_tags(soup: BeautifulSoup) -> MetaTagAnalysis: + """Analyze meta tags using BeautifulSoup.""" + # Title analysis + title = soup.title.string if soup.title else "" + title_analysis = { + 'status': 'good' if title and 30 <= len(title) <= 60 else 'needs_improvement', + 'value': title, + 'recommendation': '' if title and 30 <= len(title) <= 60 else 'Title should be between 30-60 characters' + } + + # Meta description analysis + meta_desc = soup.find('meta', attrs={'name': 'description'}) + desc = meta_desc.get('content', '') if meta_desc else "" + desc_analysis = { + 'status': 'good' if desc and 120 <= len(desc) <= 160 else 'needs_improvement', + 'value': desc, + 'recommendation': '' if desc and 120 <= len(desc) <= 160 else 'Description should be between 120-160 characters' + } + + # Keywords analysis + meta_keywords = soup.find('meta', attrs={'name': 'keywords'}) + keywords = meta_keywords.get('content', '') if meta_keywords else "" + keywords_analysis = { + 'status': 'good' if keywords else 'needs_improvement', + 'value': keywords, + 'recommendation': '' if keywords else 'Add relevant keywords meta tag' + } + + return MetaTagAnalysis( + title=title_analysis, + description=desc_analysis, + keywords=keywords_analysis, + has_robots=bool(soup.find('meta', attrs={'name': 'robots'})), + has_sitemap=bool(soup.find('link', attrs={'rel': 'sitemap'})) + ) + +def analyze_content_with_ai(content: str) -> Tuple[ContentAnalysis, List[SEORecommendation]]: + """Analyze content using AI.""" + try: + # Load environment variables + load_dotenv() + + # Get API key from environment + api_key = os.getenv('OPENAI_API_KEY') + if not api_key: + raise ValueError("OpenAI API key not found in environment variables") + + # Initialize OpenAI client + client = openai.OpenAI(api_key=api_key) + + # Prepare prompt for content analysis + prompt = f"""Analyze the following webpage content for SEO and provide a structured analysis: + Content: {content[:4000]}... # Truncate to avoid token limits + + Provide analysis in the following format: + 1. Word count + 2. Heading structure analysis + 3. Keyword density for main topics + 4. Readability score (0-100) + 5. Content quality score (0-100) + 6. List of SEO recommendations with priority (high/medium/low), category, issue, recommendation, and impact + + Format the response as JSON.""" + + # Get AI analysis + response = client.chat.completions.create( + model="gpt-4", + messages=[ + {"role": "system", "content": "You are an SEO expert analyzing website content."}, + {"role": "user", "content": prompt} + ], + response_format={"type": "json_object"} + ) + + # Parse AI response + analysis = response.choices[0].message.content + + # Create ContentAnalysis object + content_analysis = ContentAnalysis( + word_count=len(content.split()), + headings_structure=analysis.get('heading_structure', {}), + keyword_density=analysis.get('keyword_density', {}), + readability_score=analysis.get('readability_score', 0), + content_quality_score=analysis.get('content_quality_score', 0) + ) + + # Create recommendations + recommendations = [ + SEORecommendation( + priority=rec['priority'], + category=rec['category'], + issue=rec['issue'], + recommendation=rec['recommendation'], + impact=rec['impact'] + ) + for rec in analysis.get('recommendations', []) + ] + + return content_analysis, recommendations + + except Exception as e: + logger.error(f"Error in AI analysis: {str(e)}") + return ContentAnalysis( + word_count=len(content.split()), + headings_structure={}, + keyword_density={}, + readability_score=0, + content_quality_score=0 + ), [] + +def analyze_seo(url: str) -> SEOAnalysisResult: + """Main function to analyze website SEO.""" + errors = [] + warnings = [] + + # Validate URL + try: + parsed_url = urlparse(url) + if not all([parsed_url.scheme, parsed_url.netloc]): + errors.append("Invalid URL format") + raise ValueError("Invalid URL format") + except Exception as e: + errors.append(f"URL parsing error: {str(e)}") + return SEOAnalysisResult( + url=url, + analyzed_at=datetime.now(), + overall_score=0, + meta_tags=None, + content=None, + recommendations=[], + errors=errors, + warnings=warnings, + success=False + ) + + # Extract content + content, soup, extract_errors = extract_content(url) + errors.extend(extract_errors) + + if not content or not soup: + return SEOAnalysisResult( + url=url, + analyzed_at=datetime.now(), + overall_score=0, + meta_tags=None, + content=None, + recommendations=[], + errors=errors, + warnings=warnings, + success=False + ) + + try: + # Analyze meta tags + meta_analysis = analyze_meta_tags(soup) + + # Analyze content with AI + content_analysis, recommendations = analyze_content_with_ai(content) + + # Calculate overall score + meta_score = sum([ + 1 if meta_analysis.title['status'] == 'good' else 0, + 1 if meta_analysis.description['status'] == 'good' else 0, + 1 if meta_analysis.keywords['status'] == 'good' else 0, + 1 if meta_analysis.has_robots else 0, + 1 if meta_analysis.has_sitemap else 0 + ]) * 20 # Scale to 100 + + overall_score = ( + meta_score * 0.3 + # 30% weight for meta tags + content_analysis.readability_score * 0.3 + # 30% weight for readability + content_analysis.content_quality_score * 0.4 # 40% weight for content quality + ) + + return SEOAnalysisResult( + url=url, + analyzed_at=datetime.now(), + overall_score=overall_score, + meta_tags=meta_analysis, + content=content_analysis, + recommendations=recommendations, + errors=errors, + warnings=warnings, + success=True + ) + + except Exception as e: + error_msg = f"Error in SEO analysis: {str(e)}" + logger.error(error_msg) + errors.append(error_msg) + return SEOAnalysisResult( + url=url, + analyzed_at=datetime.now(), + overall_score=0, + meta_tags=None, + content=None, + recommendations=[], + errors=errors, + warnings=warnings, + success=False + ) \ No newline at end of file diff --git a/lib/web_crawlers/README.md b/lib/web_crawlers/README.md new file mode 100644 index 00000000..b062c2eb --- /dev/null +++ b/lib/web_crawlers/README.md @@ -0,0 +1,151 @@ +# Web Crawler Guide for Content Creators + +## What is a Web Crawler? + +A web crawler is a powerful tool that helps content creators gather, analyze, and understand content from websites. It's like having a digital assistant that can quickly scan websites and extract valuable information to help you create better content. + +## Key Features + +### 1. Content Extraction +- **Main Content**: Extracts the primary content from web pages +- **Meta Information**: Captures titles, descriptions, and meta tags +- **Structure Analysis**: Identifies headings and content hierarchy +- **Media Elements**: Collects links and images with their descriptions + +### 2. AI-Powered Analysis +- **Topic Identification**: Automatically identifies main topics +- **Content Quality Assessment**: Evaluates readability and engagement +- **SEO Analysis**: Provides SEO scores and recommendations +- **Content Gap Analysis**: Identifies missing information +- **Opportunity Detection**: Suggests areas for improvement + +### 3. Smart Processing +- **Fast Performance**: Uses advanced async technology for quick results +- **Error Handling**: Gracefully handles website access issues +- **Content Cleaning**: Removes unnecessary elements for clean analysis +- **Multiple Page Support**: Can analyze multiple pages efficiently + +## Use Cases for Content Creators + +### 1. Content Research +- **Competitor Analysis**: Study competitor content and strategies +- **Topic Research**: Gather information for new content ideas +- **Industry Trends**: Track industry developments and updates +- **Content Inspiration**: Find inspiration from successful content + +### 2. Content Optimization +- **SEO Improvement**: Identify SEO opportunities +- **Content Structure**: Analyze and improve content organization +- **Readability Enhancement**: Get suggestions for better readability +- **Engagement Optimization**: Improve content engagement + +### 3. Content Strategy +- **Gap Analysis**: Identify content gaps in your niche +- **Topic Planning**: Plan content topics and themes +- **Audience Understanding**: Better understand target audience needs +- **Performance Tracking**: Monitor content performance + +## How to Use the Web Crawler + +### 1. Basic Usage +1. **Enter URL**: Provide the website URL you want to analyze +2. **Start Crawling**: The crawler will automatically extract content +3. **Review Results**: Get comprehensive analysis of the content + +### 2. Advanced Features +- **Custom Analysis**: Set specific parameters for content analysis +- **Batch Processing**: Analyze multiple pages at once +- **Detailed Reports**: Get in-depth content analysis reports +- **Export Options**: Export results in various formats + +### 3. Analysis Options +- **Content Quality**: Evaluate writing style and structure +- **SEO Metrics**: Check SEO performance +- **Engagement Factors**: Analyze reader engagement potential +- **Improvement Suggestions**: Get actionable recommendations + +## Benefits for Content Creators + +### 1. Time Savings +- Quick content research +- Automated analysis +- Efficient data gathering +- Streamlined workflow + +### 2. Quality Improvement +- Better content structure +- Enhanced readability +- Improved SEO performance +- Higher engagement potential + +### 3. Strategic Advantage +- Data-driven decisions +- Competitive insights +- Content optimization +- Performance tracking + +## Best Practices + +### 1. Before Crawling +- Identify clear objectives +- Select relevant websites +- Set analysis parameters +- Prepare for data collection + +### 2. During Analysis +- Review extracted content +- Validate information +- Check for accuracy +- Note important insights + +### 3. After Analysis +- Apply findings to content +- Track improvements +- Update content strategy +- Monitor results + +## Common Applications + +### 1. Blog Content +- Topic research +- Content structure analysis +- SEO optimization +- Engagement improvement + +### 2. Article Writing +- Research gathering +- Fact verification +- Source analysis +- Content enhancement + +### 3. Website Content +- Page optimization +- Content audit +- Structure improvement +- SEO enhancement + +### 4. Social Media Content +- Trend analysis +- Content ideas +- Engagement optimization +- Performance tracking + +## Tips for Optimal Results + +1. **Be Specific**: Clearly define your analysis goals +2. **Choose Quality Sources**: Select reliable websites for analysis +3. **Review Results**: Always verify extracted information +4. **Apply Insights**: Use findings to improve your content +5. **Track Progress**: Monitor improvements over time + +## ALwrity, Need Help? + +If you encounter any issues or need assistance: +1. Check the documentation +2. Review error messages +3. Verify website accessibility +4. Contact support if needed + +--- + +*Note: This tool is designed to help content creators gather and analyze web content efficiently. Always respect website terms of service and robots.txt files when crawling websites.* \ No newline at end of file diff --git a/lib/web_crawlers/async_web_crawler.py b/lib/web_crawlers/async_web_crawler.py new file mode 100644 index 00000000..a93d4cff --- /dev/null +++ b/lib/web_crawlers/async_web_crawler.py @@ -0,0 +1,246 @@ +"""Web crawler module using requests and BeautifulSoup.""" + +from typing import Dict, List, Optional +import json +from loguru import logger +import requests +import aiohttp +import asyncio +from bs4 import BeautifulSoup +from urllib.parse import urljoin, urlparse +from pydantic import BaseModel, Field +import os +from ..gpt_providers.text_generation.main_text_generation import llm_text_gen + +class WebsiteContent(BaseModel): + """Model for website content analysis.""" + title: str = Field("", description="Title of the webpage") + description: str = Field("", description="Meta description of the webpage") + main_content: str = Field("", description="Main content of the webpage") + headings: List[str] = Field([], description="All headings on the page") + links: List[Dict[str, str]] = Field([], description="All links on the page") + images: List[Dict[str, str]] = Field([], description="All images on the page") + meta_tags: Dict[str, str] = Field({}, description="Meta tags from the page") + +class AsyncWebCrawlerService: + """Service for crawling websites.""" + + def __init__(self): + """Initialize the crawler service.""" + logger.info("[AsyncWebCrawlerService.__init__] Initializing crawler service") + self.visited_urls = set() + self.base_url = None + self.domain = None + self.session = None + self.max_pages = 10 # Limit the number of pages to crawl + self.timeout = 30 # Timeout in seconds for requests + self.headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' + } + + async def __aenter__(self): + """Create aiohttp session when entering context.""" + logger.debug("[AsyncWebCrawlerService.__aenter__] Creating aiohttp session") + self.session = aiohttp.ClientSession(headers=self.headers) + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + """Close aiohttp session when exiting context.""" + logger.debug("[AsyncWebCrawlerService.__aexit__] Closing aiohttp session") + if self.session: + await self.session.close() + + async def fetch_url(self, url: str) -> str: + """ + Fetch URL content asynchronously. + + Args: + url (str): URL to fetch + + Returns: + str: HTML content + """ + logger.debug(f"[AsyncWebCrawlerService.fetch_url] Fetching URL: {url}") + if not self.session: + logger.debug("[AsyncWebCrawlerService.fetch_url] Creating new session") + self.session = aiohttp.ClientSession(headers=self.headers) + + async with self.session.get(url) as response: + if response.status == 200: + logger.debug(f"[AsyncWebCrawlerService.fetch_url] Successfully fetched URL: {url}") + return await response.text() + else: + error_msg = f"Failed to fetch URL: Status code {response.status}" + logger.error(f"[AsyncWebCrawlerService.fetch_url] {error_msg}") + raise Exception(error_msg) + + async def crawl_website(self, url: str) -> Dict: + """ + Crawl a website and extract its content. + + Args: + url (str): The URL to crawl + + Returns: + Dict: Extracted website content and metadata + """ + try: + logger.info(f"[AsyncWebCrawlerService.crawl_website] Starting crawl for URL: {url}") + + # Fetch the page content + try: + html_content = await self.fetch_url(url) + logger.debug("[AsyncWebCrawlerService.crawl_website] Successfully fetched HTML content") + except Exception as e: + error_msg = f"Failed to fetch content from {url}: {str(e)}" + logger.error(f"[AsyncWebCrawlerService.crawl_website] {error_msg}") + return { + 'success': False, + 'error': error_msg + } + + # Parse HTML with BeautifulSoup + logger.debug("[AsyncWebCrawlerService.crawl_website] Parsing HTML content") + soup = BeautifulSoup(html_content, 'html.parser') + + # Extract main content (focusing on article-like content) + main_content_elements = soup.find_all(['article', 'main', 'div'], class_=['content', 'main-content', 'article', 'post']) + if not main_content_elements: + main_content_elements = soup.find_all(['p', 'article', 'section']) + + main_content = ' '.join([elem.get_text(strip=True) for elem in main_content_elements]) + + # If still no content, get all paragraph text + if not main_content: + main_content = ' '.join([p.get_text(strip=True) for p in soup.find_all('p')]) + + logger.debug(f"[AsyncWebCrawlerService.crawl_website] Extracted {len(main_content)} characters of main content") + + # Extract content + content = { + 'title': soup.title.string.strip() if soup.title else '', + 'description': soup.find('meta', {'name': 'description'}).get('content', '').strip() if soup.find('meta', {'name': 'description'}) else '', + 'main_content': main_content, + 'headings': [h.get_text(strip=True) for h in soup.find_all(['h1', 'h2', 'h3', 'h4', 'h5', 'h6'])], + 'links': [{'text': a.get_text(strip=True), 'href': urljoin(url, a.get('href', ''))} for a in soup.find_all('a', href=True)], + 'images': [{'alt': img.get('alt', '').strip(), 'src': urljoin(url, img.get('src', ''))} for img in soup.find_all('img', src=True)], + 'meta_tags': { + meta.get('name', meta.get('property', '')): meta.get('content', '').strip() + for meta in soup.find_all('meta') + if (meta.get('name') or meta.get('property')) and meta.get('content') + } + } + + logger.debug(f"[AsyncWebCrawlerService.crawl_website] Extracted {len(content['links'])} links and {len(content['images'])} images") + + # Close the session if it exists + if self.session: + logger.debug("[AsyncWebCrawlerService.crawl_website] Closing session") + await self.session.close() + self.session = None + + logger.info("[AsyncWebCrawlerService.crawl_website] Successfully completed website crawl") + return { + 'success': True, + 'content': content, + 'url': url + } + + except Exception as e: + error_msg = f"Error crawling {url}: {str(e)}" + logger.error(f"[AsyncWebCrawlerService.crawl_website] {error_msg}") + # Ensure session is closed even if there's an error + if self.session: + logger.debug("[AsyncWebCrawlerService.crawl_website] Closing session after error") + await self.session.close() + self.session = None + return { + 'success': False, + 'error': str(e) + } + + async def analyze_content_with_llm(self, content: Dict, api_key: str, gpt_provider: str) -> Dict: + """ + Analyze content using LLM. + + Args: + content (Dict): Content to analyze + api_key (str): API key for the LLM service + gpt_provider (str): Provider to use (openai/google) + + Returns: + Dict: Analysis results + """ + try: + logger.info(f"[AsyncWebCrawlerService.analyze_content_with_llm] Starting content analysis with {gpt_provider}") + + # Prepare the content for analysis + main_content = content.get("main_content", "") + if isinstance(main_content, dict): + main_content = main_content.get("text", "") + + logger.debug(f"[AsyncWebCrawlerService.analyze_content_with_llm] Prepared {len(main_content)} characters for analysis") + + # Construct the prompt for analysis + prompt = f"""Analyze the following website content and provide a comprehensive analysis: + +Content: +{main_content[:4000]} # Limit content length for API + +Please provide analysis in the following JSON format: +{{ + "topics": ["topic1", "topic2", ...], + "key_insights": ["insight1", "insight2", ...], + "content_quality": {{ + "readability": "score", + "engagement": "score", + "completeness": "score" + }}, + "recommendations": ["rec1", "rec2", ...], + "seo_score": "score", + "content_gaps": ["gap1", "gap2", ...], + "opportunities": ["opp1", "opp2", ...], + "priority_areas": ["area1", "area2", ...] +}} + +Ensure the response is valid JSON.""" + + # Call the LLM function + logger.debug("[AsyncWebCrawlerService.analyze_content_with_llm] Calling llm_text_gen with prompt") + response = llm_text_gen(prompt) + + if not response: + logger.error("[AsyncWebCrawlerService.analyze_content_with_llm] No response from LLM") + return {} + + # Clean up the response before parsing + logger.debug("[AsyncWebCrawlerService.analyze_content_with_llm] Cleaning response for JSON parsing") + try: + # Remove any leading/trailing whitespace + cleaned_response = response.strip() + + # If response starts with a newline or other characters before {, clean it + start_idx = cleaned_response.find('{') + end_idx = cleaned_response.rfind('}') + if start_idx != -1 and end_idx != -1: + cleaned_response = cleaned_response[start_idx:end_idx + 1] + + # Fix any line breaks within strings + cleaned_response = cleaned_response.replace('\n', ' ') + + logger.debug(f"[AsyncWebCrawlerService.analyze_content_with_llm] Attempting to parse cleaned response: {cleaned_response[:100]}...") + + # Parse the cleaned response + analysis_result = json.loads(cleaned_response) + logger.info("[AsyncWebCrawlerService.analyze_content_with_llm] Successfully parsed LLM response") + logger.debug(f"[AsyncWebCrawlerService.analyze_content_with_llm] Analysis result keys: {analysis_result.keys()}") + return analysis_result + + except json.JSONDecodeError as e: + logger.error(f"[AsyncWebCrawlerService.analyze_content_with_llm] Failed to parse LLM response as JSON: {str(e)}") + logger.debug(f"[AsyncWebCrawlerService.analyze_content_with_llm] Raw response: {response[:100]}...") + return {} + + except Exception as e: + logger.error(f"[AsyncWebCrawlerService.analyze_content_with_llm] Error analyzing content with LLM: {str(e)}") + return {} \ No newline at end of file diff --git a/lib/web_crawlers/crawl4ai_web_crawler.py b/lib/web_crawlers/crawl4ai_web_crawler.py new file mode 100644 index 00000000..670159a9 --- /dev/null +++ b/lib/web_crawlers/crawl4ai_web_crawler.py @@ -0,0 +1,94 @@ +"""Web crawler for ALwrity style analysis.""" + +import asyncio +from crawl4ai import AsyncWebCrawler +from crawl4ai.async_configs import BrowserConfig, CrawlerRunConfig, CacheMode +from loguru import logger + +async def analyze_website_style(url: str, sample_text: str = None) -> dict: + """ + Analyze website content or sample text for style analysis. + + Args: + url: Website URL to analyze + sample_text: Optional sample text to analyze instead of website + + Returns: + dict: Analysis results including content style metrics + """ + try: + if sample_text: + # Analyze sample text directly + return { + "success": True, + "content": sample_text, + "metrics": { + "word_count": len(sample_text.split()), + "sentence_count": len(sample_text.split('.')), + "avg_sentence_length": len(sample_text.split()) / max(len(sample_text.split('.')), 1) + } + } + browser_config = BrowserConfig() # Default browser configuration + run_config = CrawlerRunConfig() # Default crawl run configuration + + async with AsyncWebCrawler(config=browser_config) as crawler: + result = await crawler.arun( + url=url, + config=run_config + ) + print(result.markdown) # Print clean markdown content + + logger.debug(f"Crawl result: {result}") + if result.success: + # Process content for style analysis + content = result.markdown + sentences = [s.strip() for s in content.split('.') if s.strip()] + + return { + "success": True, + "content": content, + "metrics": { + "word_count": len(content.split()), + "sentence_count": len(sentences), + "avg_sentence_length": len(content.split()) / max(len(sentences), 1), + "internal_links": len(result.links["internal"]), + "images": len(result.media["images"]) + } + } + else: + return { + "success": False, + "error": result.error_message + } + + except Exception as e: + logger.error(f"Error in style analysis: {str(e)}") + return { + "success": False, + "error": str(e) + } + +def analyze_style(url: str = None, sample_text: str = None) -> dict: + """ + Synchronous wrapper for style analysis. + + Args: + url: Website URL to analyze + sample_text: Optional sample text to analyze + + Returns: + dict: Analysis results + """ + return asyncio.run(analyze_website_style(url, sample_text)) + + +# Deep Crawling +# One of Crawl4AI's most powerful features is its ability to perform +# configurable deep crawling that can explore websites beyond a single page. +# With fine-tuned control over crawl depth, domain boundaries, +# and content filtering, Crawl4AI gives you the tools to extract precisely the content you need. +# +# +# +# +# diff --git a/pages/ai_research_setup_page.py b/pages/ai_research_setup_page.py new file mode 100644 index 00000000..5d094cd4 --- /dev/null +++ b/pages/ai_research_setup_page.py @@ -0,0 +1,50 @@ +"""Page for AI Research Setup redirection.""" + +import streamlit as st +from loguru import logger +import sys +import os + +# Configure logger +logger.remove() # Remove default handler +logger.add( + "logs/ai_research_setup_page.log", + rotation="500 MB", + retention="10 days", + level="DEBUG", + format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {message}", + backtrace=True, + diagnose=True +) +logger.add( + sys.stdout, + level="INFO", + format="{time:YYYY-MM-DD HH:mm:ss} |{level: <8} |{message} " +) + +# Set page config +st.set_page_config( + layout="wide", + initial_sidebar_state="collapsed", + menu_items={ + 'Get Help': None, + 'Report a bug': None, + 'About': None + } +) + +def render_ai_research_setup_page(): + """Render the AI Research Setup page.""" + try: + logger.info("Starting AI Research Setup page") + + # Import and render the AI Research Setup component + from lib.utils.api_key_manager.components.ai_research_setup import render_ai_research_setup + render_ai_research_setup() + + except Exception as e: + logger.error(f"Error in render_ai_research_setup_page: {str(e)}") + st.error(f"An error occurred: {str(e)}") + +if __name__ == "__main__": + render_ai_research_setup_page() \ No newline at end of file diff --git a/pages/personalization_setup.py b/pages/personalization_setup.py new file mode 100644 index 00000000..d35e9930 --- /dev/null +++ b/pages/personalization_setup.py @@ -0,0 +1,84 @@ +import streamlit as st +import os +import json +from pathlib import Path + +st.set_page_config( + page_title="Personalization Setup", + page_icon="βοΈ", + layout="wide" +) + +st.title("Personalization Setup") + +# Initialize session state for active tab if not exists +if 'active_tab' not in st.session_state: + st.session_state.active_tab = "Writing Preferences" + +# Create tabs for different sections +tab1, tab2 = st.tabs(["Writing Preferences", "AI Configuration"]) + +with tab1: + st.write(""" + This section allows you to customize your AI writing experience. + Configure your preferences and settings here. + """) + + # Add your personalization options here + st.subheader("Writing Style Preferences") + tone = st.selectbox( + "Select your preferred writing tone", + ["Professional", "Casual", "Academic", "Creative"] + ) + + st.subheader("Content Preferences") + content_type = st.multiselect( + "Select your preferred content types", + ["Blog Posts", "Articles", "Social Media", "Technical Writing", "Creative Writing"] + ) + + if st.button("Save Preferences"): + st.success("Your preferences have been saved!") + +with tab2: + st.subheader("AI Configuration Settings") + + # Create a form for AI configuration + with st.form("ai_config_form"): + # API Keys + st.text_input("OpenAI API Key", type="password", key="openai_key") + st.text_input("Google API Key", type="password", key="google_key") + st.text_input("SerpAPI Key", type="password", key="serpapi_key") + + # Model Selection + st.selectbox("Select Model", ["gpt-3.5-turbo", "gpt-4"], key="model") + + # Temperature + st.slider("Temperature", 0.0, 2.0, 0.7, 0.1, key="temperature") + + # Max Tokens + st.number_input("Max Tokens", 100, 4000, 2000, 100, key="max_tokens") + + # Submit button + submitted = st.form_submit_button("Save Configuration") + + if submitted: + # Create config directory if it doesn't exist + config_dir = Path("config") + config_dir.mkdir(exist_ok=True) + + # Save configuration + config = { + "openai_key": st.session_state.openai_key, + "google_key": st.session_state.google_key, + "serpapi_key": st.session_state.serpapi_key, + "model": st.session_state.model, + "temperature": st.session_state.temperature, + "max_tokens": st.session_state.max_tokens + } + + config_file = config_dir / "test_config.json" + with open(config_file, "w") as f: + json.dump(config, f, indent=4) + + st.success("Configuration saved successfully!") \ No newline at end of file diff --git a/pages/style_utils.py b/pages/style_utils.py new file mode 100644 index 00000000..cf0345cc --- /dev/null +++ b/pages/style_utils.py @@ -0,0 +1,352 @@ +"""CSS styles and utilities for ALwrity pages.""" + +def get_base_styles() -> str: + """ + Get the base CSS styles for ALwrity. + + Returns: + str: CSS styles as a string + """ + return """ + + """ + +def get_glassmorphic_styles() -> str: + """ + Get the glassmorphic CSS styles for ALwrity. + + Returns: + str: CSS styles as a string + """ + return """ + + """ + +def get_glass_container(content: str) -> str: + """Wrap content in a glass container.""" + return f""" ++ {content} ++ """ + +def get_info_section(content: str) -> str: + """Wrap content in an info section.""" + return f""" ++ {content} ++ """ + +def get_example_box(content: str) -> str: + """Wrap content in an example box.""" + return f""" ++ {content} ++ """ + +def get_analysis_section(title: str, content: str) -> str: + """Create an analysis section with title and content.""" + return f""" +++ """ + +def get_style_guide_html() -> str: + """ + Get the style guide HTML content. + + Returns: + str: HTML content for the style guide section + """ + return """ + ### How ALwrity Discovers Your Style + + **AI-Powered Style Analysis** + + ALwrity AI analyzes your existing content to understand your unique writing style and preferences. This helps us generate content that matches your voice perfectly. + + **Step 1: Content Analysis** + + We'll analyze your website content or written samples to understand: + + - Writing tone and voice + - Vocabulary and language style + - Content structure and formatting + - Target audience and engagement style + + **Step 2: Style Recommendations** + + Based on the analysis, we'll provide: + + - Personalized writing guidelines + - Content structure templates + - Tone and voice recommendations + - Audience engagement strategies + + **Step 3: Content Generation** + + Finally, we'll use these insights to: + + - Generate content that matches your style + - Maintain consistency across all content + - Optimize for your target audience + - Ensure brand voice alignment + """ + +def get_test_config_styles() -> str: + """ + Get all CSS styles for test configuration settings page. + + Returns: + str: Combined CSS styles as a string + """ + return f"{get_base_styles()}{get_glassmorphic_styles()}" \ No newline at end of file diff --git a/pages/test_config_settings.py b/pages/test_config_settings.py new file mode 100644 index 00000000..1ab9a2c8 --- /dev/null +++ b/pages/test_config_settings.py @@ -0,0 +1,310 @@ +"""Test configuration settings page for ALwrity.""" + +import streamlit as st +from loguru import logger +import asyncio +from lib.web_crawlers.async_web_crawler import AsyncWebCrawlerService +from pages.style_utils import ( + get_test_config_styles, + get_glass_container, + get_info_section, + get_example_box, + get_analysis_section, + get_style_guide_html +) +import sys +from lib.personalization.style_analyzer import StyleAnalyzer + +# Set page config - must be the first Streamlit command +st.set_page_config( + layout="wide", + initial_sidebar_state="collapsed", + menu_items={ + 'Get Help': None, + 'Report a bug': None, + 'About': None + } +) + +import yaml +from pathlib import Path +import os +from loguru import logger +from lib.utils.read_main_config_params import get_personalization_settings +from lib.web_crawlers.crawl4ai_web_crawler import analyze_style + +# Configure logger +logger.remove() # Remove default handler +logger.add( + "logs/test_config_settings.log", + rotation="500 MB", + retention="10 days", + level="DEBUG", + format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {message}", + backtrace=True, + diagnose=True +) +logger.add( + sys.stdout, + level="INFO", + format="{title}
+ {content} +{time:YYYY-MM-DD HH:mm:ss} |{level: <8} |{message} " +) + +# Apply CSS styles +st.markdown(get_test_config_styles(), unsafe_allow_html=True) + +def load_website_url(): + """Load website URL from config file.""" + try: + logger.debug("Loading website URL from config file") + config_path = Path(os.environ["ALWRITY_CONFIG"]) + config = yaml.safe_load(config_path.read_text()) + url = config.get('website', {}).get('url', '') + logger.info(f"Loaded website URL: {url}") + return url + except Exception as e: + logger.error(f"Error loading website URL: {str(e)}", exc_info=True) + return '' + +def display_style_analysis(analysis_results: dict): + """Display the style analysis results in a structured format.""" + try: + # Writing Style Section + st.markdown("### π¨ Writing Style Analysis") + writing_style = analysis_results.get("writing_style", {}) + writing_style_content = f""" ++
+ """ + st.markdown(get_analysis_section("Writing Style", writing_style_content), unsafe_allow_html=True) + + # Content Characteristics Section + content_chars = analysis_results.get("content_characteristics", {}) + content_chars_content = f""" +- Tone: {writing_style.get("tone", "N/A")}
+- Voice: {writing_style.get("voice", "N/A")}
+- Complexity: {writing_style.get("complexity", "N/A")}
+- Engagement Level: {writing_style.get("engagement_level", "N/A")}
++
+ """ + st.markdown(get_analysis_section("Content Characteristics", content_chars_content), unsafe_allow_html=True) + + # Target Audience Section + target_audience = analysis_results.get("target_audience", {}) + target_audience_content = f""" +- Sentence Structure: {content_chars.get("sentence_structure", "N/A")}
+- Vocabulary Level: {content_chars.get("vocabulary_level", "N/A")}
+- Paragraph Organization: {content_chars.get("paragraph_organization", "N/A")}
+- Content Flow: {content_chars.get("content_flow", "N/A")}
++
+ """ + st.markdown(get_analysis_section("Target Audience", target_audience_content), unsafe_allow_html=True) + + # Content Type Section + content_type = analysis_results.get("content_type", {}) + content_type_content = f""" +- Demographics: {', '.join(target_audience.get("demographics", ["N/A"]))}
+- Expertise Level: {target_audience.get("expertise_level", "N/A")}
+- Industry Focus: {target_audience.get("industry_focus", "N/A")}
+- Geographic Focus: {target_audience.get("geographic_focus", "N/A")}
++
+ """ + st.markdown(get_analysis_section("Content Type", content_type_content), unsafe_allow_html=True) + + # Recommended Settings Section + recommended = analysis_results.get("recommended_settings", {}) + recommended_content = f""" +- Primary Type: {content_type.get("primary_type", "N/A")}
+- Secondary Types: {', '.join(content_type.get("secondary_types", ["N/A"]))}
+- Purpose: {content_type.get("purpose", "N/A")}
+- Call to Action: {content_type.get("call_to_action", "N/A")}
++
+ """ + st.markdown(get_analysis_section("Recommended Settings", recommended_content), unsafe_allow_html=True) + + except Exception as e: + logger.error(f"Error displaying style analysis: {str(e)}") + st.error(f"Error displaying analysis results: {str(e)}") + +def render_test_config_settings(): + """Render the test configuration settings page.""" + try: + logger.info("Starting to render test configuration settings") + + # Add back button at the top + col1, col2 = st.columns([1, 3]) + with col1: + if st.button("β Back to Personalization Setup"): + logger.info("User clicked back to personalization setup") + # Set session state for navigation + st.session_state.current_step = 4 + st.session_state.next_step = "personalization_setup" + # Navigate back to personalization setup + st.switch_page("pages/personalization_setup.py") + + # Title and description + st.title("π¨ Find Your Style with ALwrity") + st.markdown(get_glass_container( + "- Writing Tone: {recommended.get("writing_tone", "N/A")}
+- Target Audience: {recommended.get("target_audience", "N/A")}
+- Content Type: {recommended.get("content_type", "N/A")}
+- Creativity Level: {recommended.get("creativity_level", "N/A")}
+- Geographic Location: {recommended.get("geographic_location", "N/A")}
+Enter a website URL or provide content samples to analyze your writing style and get personalized recommendations.
" + ), unsafe_allow_html=True) + + # Create two columns for the layout + col1, col2 = st.columns([2, 1]) + + with col1: + # Website URL input + st.markdown("### Website URL") + url = st.text_input( + "Enter your website URL", + placeholder="https://example.com", + help="Provide your website URL to analyze your content style. Leave empty if you want to provide written samples instead." + ) + logger.debug(f"Website URL input value: {url}") + + # Alternative: Written samples + if not url: + st.markdown("### Written Samples") + st.markdown(get_info_section(""" +No website URL? No problem! You can provide written samples of your content instead.
+Share your best articles, blog posts, or any content that represents your writing style.
+ """), unsafe_allow_html=True) + samples = st.text_area( + "Paste your content samples here", + help="Paste 2-3 samples of your best content. This helps ALwrity understand your writing style." + ) + logger.debug(f"Sample text length: {len(samples) if samples else 0}") + + st.markdown('', unsafe_allow_html=True) + + # ALwrity Style button + st.markdown("", unsafe_allow_html=True) + if st.button("π¨ ALwrity Style", use_container_width=True): + if url: + with st.status("Starting style analysis...", expanded=True) as status: + try: + logger.info(f"Starting style analysis for URL: {url}") + + # Step 1: Initialize crawler + status.update(label="Step 1/4: Initializing web crawler...", state="running") + crawler_service = AsyncWebCrawlerService() + + # Step 2: Crawl website + status.update(label="Step 2/4: Crawling website content...", state="running") + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + result = loop.run_until_complete(crawler_service.crawl_website(url)) + loop.close() + + if result.get('success', False): + content = result.get('content', {}) + + # Step 3: Initialize style analyzer + status.update(label="Step 3/4: Analyzing content style...", state="running") + style_analyzer = StyleAnalyzer() + + # Step 4: Perform style analysis + status.update(label="Step 4/4: Generating style recommendations...", state="running") + style_analysis = style_analyzer.analyze_content_style(content) + + if style_analysis.get('error'): + status.update(label="Analysis failed", state="error") + st.error(f"Style analysis failed: {style_analysis['error']}") + else: + status.update(label="Analysis complete!", state="complete") + # Display style analysis results + display_style_analysis(style_analysis) + + # Display original content in tabs + tab1, tab2, tab3 = st.tabs(["Content", "Metadata", "Links"]) + + with tab1: + st.markdown("### Main Content") + st.markdown(content.get('main_content', 'No content found')) + + with tab2: + st.markdown("### Metadata") + st.markdown(f""" + **Title:** {content.get('title', 'No title found')} + + **Description:** {content.get('description', 'No description found')} + + **Meta Tags:** + {content.get('meta_tags', {})} + """) + + with tab3: + st.markdown("### Links") + for link in content.get('links', []): + st.markdown(f"- [{link.get('text', '')}]({link.get('href', '')})") + + else: + status.update(label="Crawling failed", state="error") + st.error(f"Failed to analyze website: {result.get('error', 'Unknown error')}") + + except Exception as e: + logger.error(f"Error during style analysis: {str(e)}") + st.error(f"Analysis failed: {str(e)}") + elif samples: + with st.spinner("Analyzing content samples..."): + try: + # TODO: Implement sample text analysis + st.info("Sample text analysis coming soon!") + except Exception as e: + logger.error(f"Error analyzing samples: {str(e)}") + st.error(f"Analysis failed: {str(e)}") + else: + st.warning("Please provide either a website URL or content samples") + + with col2: + st.markdown(""" + ### How ALwrity Discovers Your Style + + **AI-Powered Style Analysis** + + ALwrity AI analyzes your existing content to understand your unique writing style and preferences. This helps us generate content that matches your voice perfectly. + + **Step 1: Content Analysis** + + We'll analyze your website content or written samples to understand: + + - Writing tone and voice + - Vocabulary and language style + - Content structure and formatting + - Target audience and engagement style + + **Step 2: Style Recommendations** + + Based on the analysis, we'll provide: + + - Personalized writing guidelines + - Content structure templates + - Tone and voice recommendations + - Audience engagement strategies + + **Step 3: Content Generation** + + Finally, we'll use these insights to: + + - Generate content that matches your style + - Maintain consistency across all content + - Optimize for your target audience + - Ensure brand voice alignment + """) + + except Exception as e: + logger.error(f"Error in render_test_config_settings: {str(e)}") + st.error(f"An error occurred: {str(e)}") + +if __name__ == "__main__": + logger.info("Starting test config settings page") + render_test_config_settings() + logger.info("Test config settings page rendered successfully") \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 11b8c695..4760f08b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,47 +1,48 @@ -requests -typer[all] -rich -python-dotenv -loguru -openai -crewai[tools] -crewai_tools -python-docx -PyPDF2 -google.generativeai -anthropic -tenacity -tavily-python -tabulate -metaphor_python -exa_py -GoogleNews -langchain-google-genai -clint -scikit-learn -matplotlib -plotly -textstat -requests_html -pytrends -pytube -pytubefix -readability -wordcloud -prompt_toolkit -ipython -html2image -lxml_html_clean -streamlit -yfinance -pandas_ta -firecrawl-py -gTTS -validators -streamlit-mic-recorder -tinify -cloudscraper -xmlschema -moviepy -googlesearch-python -streamlit-aggrid +requests>=2.31.0 +typer>=0.9.0 +rich>=13.7.0 +python-dotenv>=1.0.0 +beautifulsoup4==4.12.2 +aiohttp>=3.11.11 +openai>=1.3.7 +PyPDF2>=3.0.1 +google-generativeai<0.9.0,>=0.8.0 +anthropic>=0.18.1 +tenacity>=8.2.3 +tabulate>=0.9.0 +metaphor-python==0.1.16 +exa_py>=1.9.1 +GoogleNews>=1.6.15 +langchain-google-genai>=2.0.10 +clint>=0.5.1 +numpy>=1.22.4,<2.0.0 +pandas>=2.0.3 +scikit-learn>=1.3.2 +matplotlib>=3.8.2 +plotly>=5.18.0 +textstat>=0.7.3 +requests_html>=0.10.0 +pytrends>=4.9.0 +pytube>=15.0.0 +pytubefix>=8.12.2 +readability>=0.3.2 +wordcloud>=1.9.3 +prompt_toolkit>=3.0.43 +html2image>=2.0.5 +lxml[html_clean]>=5.3.0 +lxml_html_clean>=0.4.1 +streamlit>=1.29.0 +yfinance>=0.2.36 +pandas_ta>=0.3.14b0 +firecrawl-py>=1.14.1 +gTTS>=2.5.1 +streamlit-mic-recorder>=0.0.8 +streamlit-aggrid>=1.1.2 +crawl4ai>=0.5.0 +playwright>=1.51.0 +loguru==0.7.2 +tavily-python>=0.2.8 +tinify>=1.6.0 +validators>=0.20.0 +python-whois==0.9.5 +dnspython \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 00000000..7ab576a6 --- /dev/null +++ b/setup.py @@ -0,0 +1,157 @@ +import sys +import os +import platform +import subprocess +import shutil +import datetime +import socket +import traceback +import pkg_resources +from setuptools import setup, find_packages + +def log_error(error_type, details): + """ + Logs installation errors to a file with timestamp and system information. + """ + log_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'install_errors.log') + timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + system_info = { + "OS": platform.system(), + "OS Version": platform.version(), + "Architecture": platform.machine(), + "Python Version": f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}", + "Hostname": socket.gethostname() + } + + log_entry = f"[{timestamp}] ERROR: {error_type}\n" + log_entry += f"Details: {details}\n" + log_entry += "System Information:\n" + for key, value in system_info.items(): + log_entry += f" {key}: {value}\n" + log_entry += "-" * 80 + "\n" + + with open(log_file, 'a') as f: + f.write(log_entry) + + print(f"Error logged to {log_file}") + +def check_system_dependencies(): + """Check for required system dependencies.""" + print("Checking system dependencies...") + all_checks_passed = True + + # Check Python version + print("Checking Python version...") + if sys.version_info < (3, 11) or sys.version_info >= (3, 12): + error_msg = "ALwrity requires Python 3.11.x" + print(f"Error: {error_msg}") + log_error("Python Version Check", error_msg) + all_checks_passed = False + else: + print(f"β Python {sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro} found") + + # Check Visual C++ Build Tools on Windows + if platform.system() == "Windows": + print("Checking for Visual C++ Build Tools...") + if not shutil.which("cl"): + error_msg = "Visual C++ Build Tools not found" + print("β Visual C++ Build Tools not found") + print("\nTo install Visual C++ Build Tools, run in an administrative PowerShell:") + print("winget install Microsoft.VisualStudio.2022.BuildTools --silent --override \"--wait --quiet --add Microsoft.VisualStudio.Workload.VCTools --includeRecommended\"") + log_error("Visual C++ Build Tools Check", error_msg) + all_checks_passed = False + else: + print("β Visual C++ Build Tools found") + + # Check Rust compiler + print("Checking for Rust compiler...") + if not shutil.which("rustc"): + error_msg = "Rust compiler not found" + print("β Rust compiler not found") + if platform.system() == "Windows": + print("\nTo install Rust on Windows, run:") + print("Invoke-WebRequest -Uri https://static.rust-lang.org/rustup/dist/x86_64-pc-windows-msvc/rustup-init.exe -OutFile rustup-init.exe") + print("./rustup-init.exe -y") + else: + print("\nTo install Rust on Linux/macOS, run:") + print("curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y") + print("source $HOME/.cargo/env") + log_error("Rust Compiler Check", error_msg) + all_checks_passed = False + else: + print("β Rust compiler found") + + return all_checks_passed + +def get_requirements(): + """Read requirements from requirements.txt.""" + with open('requirements.txt') as f: + requirements = [line.strip() for line in f if line.strip() and not line.startswith('#')] + return requirements + +def install_requirements(requirements): + """Install each requirement, showing progress.""" + print("Installing required packages...") + for requirement in requirements: + try: + print(f"Installing {requirement}...") + subprocess.check_call([sys.executable, "-m", "pip", "install", requirement]) + except subprocess.CalledProcessError as e: + error_msg = f"Error installing {requirement}: {e}" + print(error_msg) + log_error("Package Installation", error_msg) + sys.exit(1) + +def main(): + """Main installation function.""" + print("ALwrity Installation\n") + + # Check system dependencies + if not check_system_dependencies(): + print("\nPlease install the missing dependencies and try again.") + print("Check the install_errors.log file for detailed error information.") + sys.exit(1) + + # Create virtual environment if it doesn't exist + if not os.path.exists("venv"): + print("\nCreating virtual environment...") + try: + subprocess.check_call([sys.executable, "-m", "venv", "venv"]) + except subprocess.CalledProcessError as e: + error_msg = f"Failed to create virtual environment: {e}" + print(error_msg) + log_error("Virtual Environment Creation", error_msg) + sys.exit(1) + + # Install requirements + requirements = get_requirements() + install_requirements(requirements) + + # Run setup + setup( + name="alwrity", + version="1.0.0", + description="AI-powered content writing assistant", + author="Your Name", + packages=find_packages(), + python_requires=">=3.11, <3.12", + install_requires=requirements, + entry_points={ + 'console_scripts': [ + 'alwrity=alwrity:main', + ], + }, + ) + + print("\nInstallation complete! To start ALwrity:") + print("1. Activate the virtual environment:") + if platform.system() == "Windows": + print(" .\\venv\\Scripts\\activate") + else: + print(" source venv/bin/activate") + print("2. Run the application:") + print(" streamlit run alwrity.py") + +if __name__ == '__main__': + main() \ No newline at end of file From d6c0bc11ae9fd26ecbbb5b739fcf4bda884bc727 Mon Sep 17 00:00:00 2001 From: ajaysiDate: Wed, 2 Apr 2025 22:41:25 +0530 Subject: [PATCH 02/45] Google Search Grounded results, Content Calendar Ideator, Competitor Analysis, and Keyword Researcher --- .gitignore | 4 + alwrity.py | 147 +++-- .../.gpt_online_researcher.py.swp | Bin 16384 -> 0 bytes .../gemini_grounding_search_streamlit.py | 155 +++++ lib/utils/alwrity_sidebar.py | 244 -------- .../components/personalization_setup.py | 19 +- lib/utils/settings_page.py | 438 ++++++++++++++ {pages => lib/utils}/style_utils.py | 160 ++++-- {pages => lib/utils}/test_config_settings.py | 4 +- lib/utils/ui_setup.py | 90 ++- lib/workspace/AskAlwrity-min.ico | Bin 0 -> 80525 bytes lib/workspace/alwrity_ui_styling.css | 541 ++++++++++++++++-- lib/workspace/structured_data_seo.mp4 | Bin 543527 -> 0 bytes pages/ai_research_setup_page.py | 50 -- pages/personalization_setup.py | 84 --- requirements.txt | 2 +- 16 files changed, 1408 insertions(+), 530 deletions(-) delete mode 100644 lib/ai_web_researcher/.gpt_online_researcher.py.swp create mode 100644 lib/ai_web_researcher/gemini_grounding_search_streamlit.py delete mode 100644 lib/utils/alwrity_sidebar.py create mode 100644 lib/utils/settings_page.py rename {pages => lib/utils}/style_utils.py (64%) rename {pages => lib/utils}/test_config_settings.py (99%) create mode 100644 lib/workspace/AskAlwrity-min.ico delete mode 100644 lib/workspace/structured_data_seo.mp4 delete mode 100644 pages/ai_research_setup_page.py delete mode 100644 pages/personalization_setup.py diff --git a/.gitignore b/.gitignore index c20efe30..b6b32d43 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,10 @@ __pycache__ *.pywpz *.pywpzp +lib/workspace/alwrity_web_research/* +lib/workspace/alwrity_web_research_cache/* +web_research_report* + .swp .swo .swn diff --git a/alwrity.py b/alwrity.py index b6ffb3fd..e55f793b 100644 --- a/alwrity.py +++ b/alwrity.py @@ -1,11 +1,16 @@ import streamlit as st +import os +import json +import base64 +import logging +from datetime import datetime # Set page config - must be the first Streamlit command st.set_page_config( page_title="AI Writer - Content Generation Platform", page_icon="βοΈ", layout="wide", - initial_sidebar_state="collapsed", # Start with collapsed sidebar + initial_sidebar_state="expanded", # Changed from collapsed to expanded menu_items={ 'Get Help': None, 'Report a bug': None, @@ -13,27 +18,32 @@ st.set_page_config( } ) -# Add CSS to hide sidebar during setup -st.markdown(""" +# Load and apply custom CSS +with open('lib/workspace/alwrity_ui_styling.css', 'r') as f: + css = f.read() + +st.markdown(f""" """, unsafe_allow_html=True) -import os -import json -import base64 -import logging -from datetime import datetime - # Configure logging logging.basicConfig( level=logging.DEBUG, @@ -45,18 +55,13 @@ logging.basicConfig( ) logger = logging.getLogger(__name__) -from lib.utils.config_manager import save_config from lib.utils.ui_setup import setup_ui -from lib.utils.alwrity_sidebar import sidebar_configuration from lib.utils.api_key_manager.api_key_manager import APIKeyManager, render from lib.utils.api_key_manager.validation import check_all_api_keys from dotenv import load_dotenv -from lib.utils.content_generators import ai_writers, content_planning_tools, blog_from_keyword, story_input_section, essay_writer, ai_news_writer, ai_finance_ta_writer, write_ai_prod_desc, do_web_research, competitor_analysis -from lib.utils.seo_tools import ai_seo_tools -from lib.utils.ui_setup import setup_ui, setup_tabs -from lib.utils.alwrity_utils import ai_agents_team, ai_social_writer -from lib.utils.file_processor import load_image, read_prompts, write_prompts -from lib.utils.voice_processing import record_voice +from lib.utils.content_generators import blog_from_keyword, story_input_section, essay_writer, ai_news_writer, ai_finance_ta_writer, write_ai_prod_desc, do_web_research, competitor_analysis +from lib.utils.ui_setup import setup_ui, setup_alwrity_ui + def process_folder_for_rag(folder_path): """Placeholder for the process_folder_for_rag function.""" @@ -94,36 +99,110 @@ def main(): # Check API keys and show setup if needed if not check_all_api_keys(api_key_manager): logger.info("API keys not verified") + # Add CSS to hide sidebar during setup + st.markdown(""" + + """, unsafe_allow_html=True) render(api_key_manager) return else: logger.info("All API keys verified") - # Remove the CSS that hides the sidebar + # Remove the CSS that hides the sidebar and ensure it's expanded st.markdown(""" + + """, unsafe_allow_html=True) + + # Set session state to ensure sidebar stays expanded + if 'sidebar_expanded' not in st.session_state: + st.session_state.sidebar_expanded = True + + # Force sidebar state + st.sidebar.markdown(""" + """, unsafe_allow_html=True) - - setup_environment_paths() - sidebar_configuration() - setup_tabs() + + setup_alwrity_ui() def setup_environment_paths(): diff --git a/lib/ai_web_researcher/.gpt_online_researcher.py.swp b/lib/ai_web_researcher/.gpt_online_researcher.py.swp deleted file mode 100644 index 1dd76695784a03bcec06f880faafe02f360a85f3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16384 zcmeHOU5F%C6~4QxNp`cF4f+z0z_o|iJxfpT#(>~Bv!Tt-Y(}%2ac10AXV<2t>UMW+ zs;g?NZq01(?rehV5B?yckVFaMgAf9uC`dpIJ}4r2$b%pW5)m=`pdkL^gBbnJ{i*Jn z>7Ln5d=RN(zwYU(Tj!qpopaAU_f+lLu`>&&_>Ic`~Jb#+Lw=9WplsS!<-AL zPp5Y~ZnoVuYIwA{Fm*QZrAUqj{@PK;Z*7RRRw7d2ByJaHJF#p ^i=xihI@1{k0 zy=0(dU}pvnu?OCLznbb3m3Q*DzwN_2H!rW243rF%43rF%43rF%43rF%43rG~Uojx# zz3g#}Xs;R78S}ko%lAI>d(nKK-E!UhEWb(yN(M>>N(M>>N(M>>N(M>>N(M>>N(M>> zN(TN18E`ztYS8Ox+VJD~zuo`;=rChH1ilS?3-}mt<1WU23tR{80seT1u|EKx1?~mj z0X%t-v4??q;5T PXRY~aB~Dvrz4VvNRlY2wJY N<2(;`HB@=vp;UlN!rujnX2AL ~99&!szw29S1*q|FOww_#X96#{@qeN#2G(VeoZQQFF;RqAc*py;7$q zxD&$Lx+3H!7gwgvkZMK`rljCQyAh;?B0sm2*Pe``P9XU5+|pt`s$2A2>e_g3#~QcJ zJH~qlW~O%bEF5OoVQWJ-R=2sC-^P8$r3MowKSj=`Mlpu?fd-=-%@Ck@9OGr@LNkkV zN6DS7>3Q6-thfCD?^2Ojgby<%`S~me_=PkIF{#azUTik&xEwfAc)D+afFRw5_6g;P zkB)LgJydNlCv<{->cges7sm32rv@K#&77Vj9>>wJGmiEbKjic}Sn&<9TXdB;@k5C@ zel9#0E{SztY^Yh9j{|+kF2(~Q;p`I9qqHHybw7#11|m$Yx;)ohdg$TSa`VhwYkuL8 zx$5L*ZXI&42^EMEC-)9J4Ke?d{=gMFG7+Jd8aj-3!ILnN4O?;FG_l%QNj91dg!P1U z;%=0Tl9pf7VNop&<0YA3u05ilmANuDybQF-txYfS6=dXB#`^&y5_30KzsMIqs3SOE zb5bmL!y!;4bvo<2bsmZ=aRTLx%cAFm(sylqvd*~OgSK%&o~ZgIO>hhbh97!uo2|?Q zVqFBsuc)uhnWOsW)tOErM0orP`OoI`6?$ 81llkdH^rCf9n`9GMGQzgs$r8c!Ev%hHJ^DLKGVb@{ zD3SJxzSJ<5Fdk-?6GRNtdf XFo3Lkk&D*v8 z&}*fB&kr1!H$QFM(n(-hBqm1sR?o~e8IchnAtP!$ggxnFL9@d(znS?Bx^=_YkzKr+ zYTGU1r)@*=)czf%$nxIQ0W0F)g5+Hh#3|3fEIvaIU(&3)JdWZFg!r r))(8;JTmjfLyC;rcR)eK+NB5-bTAYy%xk@gql0>LqtY-`+Ga{K%1MzJiQJMp3}y zBwF`9tO&)nHWJY^P%2I#_kfRk+{UB^MuBeLPa0xvxJ;>(8-)^!t C;+C&SsZFZhcE>t27U#85E3@NhC;@!HeUqJE4n#J3$9iR(5;Hm~1wr z=o!j`l`NBPP)7pyV)%}9xbntz*fh$ZW|SqaKu)Obw2~+E3m~uqR*()350-)sRvM_2 zH^pFGCwA7Lru1=vQh~5bsuqH#8PTGMVk~scAdPr4eGGZu5EFW`X>}k5%X{LZ!@&TC zN2ud6vQ=_i%5~8;ZIC}l?Kaui4^zL>mGlRMMM(#KV52icnk`4sLse2ZuyO945TWS6 z_>{*RC)F+aT-bq2@+B>kFDim+Bx^EvPVL|1oYrxn63EgMrcFu;C?)G=BDzk9bD)$w z(-OlSH;K}e_pn{^iN_e(6fF?2q)#WEP{6D~-x~BRGIyx8-DyK-4dKl61$y9jUgpYz z$+&1^Qu`v55U2+-fh MwfXl!vFax{~*bn>*d;h-y&j1c^0${+uQ3rS(_yO=e;35zMZQwrO71RJ;23`cd z2|NRQ8aNG{01g9GD>wvDt>9O{CxPogA6Nu_j~c-5fLDP&a1mGr_5rV=7VsJ1 Y_|5 zi(7e7rh1-HH^@lZc1hr~v)nYDQk%ExPBKk-Ym69kb(?@ygl;Spt5uU}RL`s2L7&Bn zqc-hEYke}Uo*&bP6*0aOwfDb;F1B!C>LOY`ZK#MbIs!k%I-l@VqC`fkw8?dl9*xWq zP@1qL>-yJ{sy(LIc`14LFb3r{$eh$d^HWNXx=t%YEYX_WI+# {f>v>EDVx$=9Gy`satDq=majwQxM?=fYiOdWdn2v1spPADgqasLt$v*s zxXX)))}to>yEAZHukht99t7W{Eq-yf7N_IzJgf~(z?_azQ_#ffhT6;026%)IST7&3 z{;bxFFDd%;1R5&=R@4*U6tPFU$~1pOMP8U(Rdv-K24fp%?Se>NTf mLq5)Z!9t~mFY(;Mgs}%ns$SSj>)-)ciT@bF?@acG|OJ`cQ5#iQUtcxQV zWlCCRusOo6r)bo?N@T68MYT{CCC!KJTp&Uf+X@CZb+?gE*>rge6-uh!V(HNZ@LMcH z>;7wFi)`C!l7kwg(TzM+J*(QOLZcTeyTgT!dh3}#4zo|M@^ULxTJ4fGP;U1^^zOxb zY-;fgDX+?CMcx=|m^3jSqluO+)u2GQ?OBgKgNpiYelbGc2>)Q@^K@)$KHP;N*lM=T uj!cQbN70PS5_jOSt*%lXJ+155ROQwJb(w?M>!Ox>>xRrJ*y$O-H2V*xZw<)+ diff --git a/lib/ai_web_researcher/gemini_grounding_search_streamlit.py b/lib/ai_web_researcher/gemini_grounding_search_streamlit.py new file mode 100644 index 00000000..b3535c84 --- /dev/null +++ b/lib/ai_web_researcher/gemini_grounding_search_streamlit.py @@ -0,0 +1,155 @@ +import os +import streamlit as st +from google import genai +from google.genai.types import Tool, GenerateContentConfig, GoogleSearch + +# Set page config +st.set_page_config( + page_title="Gemini Grounding Search", + page_icon="π", + layout="wide" +) + +# Custom CSS for styling +st.markdown(""" + +""", unsafe_allow_html=True) + +# Title +st.title("Gemini Grounding Search") + +# Initialize Gemini client +if 'GEMINI_API_KEY' not in os.environ: + api_key = st.text_input("Enter your Gemini API Key:", type="password") + if api_key: + os.environ['GEMINI_API_KEY'] = api_key + +# Search input +search_query = st.text_input("Enter your search query:", "When is the next total solar eclipse in the United States?") + +if st.button("Search"): + if 'GEMINI_API_KEY' not in os.environ: + st.error("Please enter your Gemini API Key first!") + else: + try: + client = genai.Client(api_key=os.environ['GEMINI_API_KEY']) + model_id = "gemini-2.0-flash" + + google_search_tool = Tool( + google_search = GoogleSearch() + ) + + with st.spinner("Searching..."): + response = client.models.generate_content( + model=model_id, + contents=search_query, + config=GenerateContentConfig( + tools=[google_search_tool], + response_modalities=["TEXT"], + ) + ) + + # Display search results header + st.header("Search Results") + + # Display the response text + if response.candidates[0].content.parts: + st.markdown(' ' + + response.candidates[0].content.parts[0].text.replace('\n', '', + unsafe_allow_html=True) + + # Display the grounding metadata + if hasattr(response.candidates[0], 'grounding_metadata') and \ + hasattr(response.candidates[0].grounding_metadata, 'search_entry_point') and \ + hasattr(response.candidates[0].grounding_metadata.search_entry_point, 'rendered_content'): + + st.header("Related Searches") + rendered_content = response.candidates[0].grounding_metadata.search_entry_point.rendered_content + st.markdown(rendered_content, unsafe_allow_html=True) + + except Exception as e: + st.error(f"An error occurred: {str(e)}") \ No newline at end of file diff --git a/lib/utils/alwrity_sidebar.py b/lib/utils/alwrity_sidebar.py deleted file mode 100644 index 8665bead..00000000 --- a/lib/utils/alwrity_sidebar.py +++ /dev/null @@ -1,244 +0,0 @@ -import streamlit as st -import logging - -from .config_manager import save_config - -# Configure logging -logging.basicConfig( - level=logging.DEBUG, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', - handlers=[ - logging.StreamHandler(), # Output to console - #logging.FileHandler('alwrity.log') # Output to file - ] -) -logger = logging.getLogger(__name__) - -# Sidebar configuration -def sidebar_configuration(): - """Configure the sidebar with all necessary options.""" - try: - # Configure sidebar styling - st.sidebar.markdown(""" - - """, unsafe_allow_html=True) - - logger.info("Initializing sidebar configuration") - st.sidebar.title("π οΈ Personalization & Settings ποΈ") - - with st.sidebar.expander("**π· Content Personalization**"): - logger.debug("Setting up content personalization options") - blog_length = st.text_input("**Content Length (words)**", value="2000", - help="Approximate word count for blogs. Note: Actual length may vary based on GPT provider and max token count.") - - blog_tone_options = ["Casual", "Professional", "How-to", "Beginner", "Research", "Programming", "Social Media", "Customize"] - blog_tone = st.selectbox("**Content Tone**", - options=blog_tone_options, - help="Select the desired tone for the blog content.") - logger.debug(f"Selected blog tone: {blog_tone}") - - if blog_tone == "Customize": - custom_tone = st.text_input("Enter the tone of your content", help="Specify the tone of your content.") - if custom_tone: - blog_tone = custom_tone - logger.debug(f"Custom tone set to: {custom_tone}") - else: - logger.warning("Custom tone not specified") - st.warning("Please specify the tone of your content.") - - blog_demographic_options = ["Professional", "Gen-Z", "Tech-savvy", "Student", "Digital Marketing", "Customize"] - - blog_demographic = st.selectbox("**Target Audience**", - options=blog_demographic_options, - help="Select the primary audience for the blog content.") - if blog_demographic == "Customize": - custom_demographic = st.text_input("Enter your target audience", - help="Specify your target audience.", - placeholder="Eg. Domain expert, Content creator, Financial expert etc..") - if custom_demographic: - blog_demographic = custom_demographic - else: - st.warning("Please specify your target audience.") - - blog_type = st.selectbox("**Content Type**", - options=["Informational", "Commercial", "Company", "News", "Finance", "Competitor", "Programming", "Scholar"], - help="Select the category that best describes the blog content.") - - blog_language = st.selectbox("**Content Language**", - options=["English", "Spanish", "German", "Chinese", "Arabic", "Nepali", "Hindi", "Hindustani", "Customize"], - help="Select the language in which the blog will be written.") - if blog_language == "Customize": - custom_lang = st.text_input("Enter the language of your choice", help="Specify the content language.") - if custom_lang: - blog_language = custom_lang - else: - st.warning("Please specify the language of your content.") - - blog_output_format = st.selectbox("**Content Output Format**", - options=["markdown", "HTML", "plaintext"], - help="Select the format for the blog output.") - - with st.sidebar.expander("**π©» Images Personalization**"): - image_generation_model = st.selectbox("**Image Generation Model**", - options=["stable-diffusion", "dalle2", "dalle3"], - help="Select the model to generate images for the blog.") - number_of_blog_images = st.number_input("**Number of Blog Images**", value=1, help="Specify the number of images to include in the blog.") - - with st.sidebar.expander("**π€ LLM Personalization**"): - gpt_provider = st.selectbox("**GPT Provider**", - options=["google", "openai", "minstral"], - help="Select the provider for the GPT model.") - model = st.text_input("**Model**", value="gemini-1.5-flash-latest", help="Specify the model version to use from the selected provider.") - temperature = st.slider( - "Temperature", - min_value=0.1, - max_value=1.0, - value=0.7, - step=0.1, - format="%.1f", - help="""Temperature controls the 'creativity' or randomness of the text generated by GPT. - Greater determinism with higher values indicating more randomness.""" - ) - - top_p = st.slider( - "Top-p", - min_value=0.0, - max_value=1.0, - value=0.9, - step=0.1, - format="%.1f", - help="Top-p sampling controls the level of diversity in the generated text." - ) - - # Selectbox for max tokens - max_tokens_options = [500, 1000, 2000, 4000, 16000, 32000, 64000] - max_tokens = st.selectbox( - "Max Tokens", - options=max_tokens_options, - index=max_tokens_options.index(4000), - help="Max tokens determine the maximum length of the output sequence generated by a model." - ) - n = st.number_input("N", - value=1, - min_value=1, - max_value=10, - help="Defines the number of words or characters grouped together in a sequence when analyzing text.") - frequency_penalty = st.slider( - "Frequency Penalty", - min_value=0.0, - max_value=2.0, - value=1.0, - step=0.1, - format="%.1f", - help="Influences word selection during text generation, promoting diversity with higher values." - ) - - presence_penalty = st.slider( - "Presence Penalty", - min_value=0.0, - max_value=2.0, - value=1.0, - step=0.1, - format="%.1f", - help="Encourages the use of diverse words by discouraging repetition." - ) - - with st.sidebar.expander("**π΅οΈ Search Engine Personalization**"): - geographic_location = st.selectbox("**Geographic Location**", - options=["us", "in", "fr", "cn"], - help="Select the geographic location for tailoring search results.") - search_language = st.selectbox("**Search Language**", - options=["en", "zn-cn", "de", "hi"], - help="Select the language for the search results.") - number_of_results = st.number_input("**Number of Results**", - value=10, - max_value=20, - min_value=1, - help="Specify the number of search results to retrieve.") - time_range = st.selectbox("**Time Range**", - options=["anytime", "past day", "past week", "past month", "past year"], - help="Select the time range for filtering search results.") - include_domains = st.text_input("**Include Domains**", value="", - help="List specific domains to include in search results. Leave blank to include all domains.") - similar_url = st.text_input("**Similar URL**", value="", help="Provide a URL to find similar results. Leave blank if not needed.") - - # Storing collected inputs in a dictionary - config = { - "Blog Content Characteristics": { - "Blog Length": blog_length, - "Blog Tone": blog_tone, - "Blog Demographic": blog_demographic, - "Blog Type": blog_type, - "Blog Language": blog_language, - "Blog Output Format": blog_output_format - }, - "Blog Images Details": { - "Image Generation Model": image_generation_model, - "Number of Blog Images": number_of_blog_images - }, - "LLM Options": { - "GPT Provider": gpt_provider, - "Model": model, - "Temperature": temperature, - "Top-p": top_p, - "Max Tokens": max_tokens, - "N": n, - "Frequency Penalty": frequency_penalty, - "Presence Penalty": presence_penalty - }, - "Search Engine Parameters": { - "Geographic Location": geographic_location, - "Search Language": search_language, - "Number of Results": number_of_results, - "Time Range": time_range, - "Include Domains": include_domains, - "Similar URL": similar_url - } - } - - # Writing the configuration to a file whenever a change is made - save_config(config) - except Exception as e: - logger.error(f"Error configuring sidebar: {str(e)}") - st.error(f"Error configuring sidebar: {str(e)}") \ No newline at end of file diff --git a/lib/utils/api_key_manager/components/personalization_setup.py b/lib/utils/api_key_manager/components/personalization_setup.py index f4f773c4..cd61fb15 100644 --- a/lib/utils/api_key_manager/components/personalization_setup.py +++ b/lib/utils/api_key_manager/components/personalization_setup.py @@ -8,11 +8,13 @@ from typing import Dict, Any from ..manager import APIKeyManager from ....web_crawlers.async_web_crawler import AsyncWebCrawlerService from ....personalization.style_analyzer import StyleAnalyzer -from pages.style_utils import ( - get_analysis_section, +from lib.utils.style_utils import ( + get_test_config_styles, get_glass_container, get_info_section, - get_example_box + get_example_box, + get_analysis_section, + get_style_guide_html ) from .base import render_navigation_buttons from .alwrity_integrations import render_alwrity_integrations @@ -618,7 +620,7 @@ def render_personalization_setup(api_key_manager: APIKeyManager) -> Dict[str, An st.warning("Please provide either a website URL or content samples") with col2: - st.markdown(""" + st.markdown(get_glass_container(""" ### How ALwrity Discovers Your Style **AI-Powered Style Analysis** @@ -651,10 +653,15 @@ def render_personalization_setup(api_key_manager: APIKeyManager) -> Dict[str, An - Maintain consistency across all content - Optimize for your target audience - Ensure brand voice alignment - """) + """)) # API Configuration Form - st.markdown("### API Configuration") + st.markdown(get_glass_container(""" + ### API Configuration + + Configure your API settings for optimal content generation. + """)) + with st.form("ai_config_form"): # API Keys st.text_input("OpenAI API Key", type="password", key="openai_key") diff --git a/lib/utils/settings_page.py b/lib/utils/settings_page.py new file mode 100644 index 00000000..74f2b9a8 --- /dev/null +++ b/lib/utils/settings_page.py @@ -0,0 +1,438 @@ +import streamlit as st +from loguru import logger +import asyncio +from lib.web_crawlers.async_web_crawler import AsyncWebCrawlerService +from lib.personalization.style_analyzer import StyleAnalyzer +import sys + +# Configure logger +logger.remove() # Remove default handler +logger.add( + "logs/settings_page.log", + rotation="500 MB", + retention="10 days", + level="DEBUG", + format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {message}", + backtrace=True, + diagnose=True +) +logger.add( + sys.stdout, + level="INFO", + format="
') + + '{time:YYYY-MM-DD HH:mm:ss} |{level: <8} |{message} " +) + +def display_style_analysis(analysis_results: dict): + """Display the style analysis results in a structured format.""" + try: + # Writing Style Section + st.markdown("### π¨ Writing Style Analysis") + writing_style = analysis_results.get("writing_style", {}) + writing_style_content = f""" ++
+ """ + st.markdown(writing_style_content, unsafe_allow_html=True) + + # Content Characteristics Section + content_chars = analysis_results.get("content_characteristics", {}) + content_chars_content = f""" +- Tone: {writing_style.get("tone", "N/A")}
+- Voice: {writing_style.get("voice", "N/A")}
+- Complexity: {writing_style.get("complexity", "N/A")}
+- Engagement Level: {writing_style.get("engagement_level", "N/A")}
++
+ """ + st.markdown(content_chars_content, unsafe_allow_html=True) + + # Target Audience Section + target_audience = analysis_results.get("target_audience", {}) + target_audience_content = f""" +- Sentence Structure: {content_chars.get("sentence_structure", "N/A")}
+- Vocabulary Level: {content_chars.get("vocabulary_level", "N/A")}
+- Paragraph Organization: {content_chars.get("paragraph_organization", "N/A")}
+- Content Flow: {content_chars.get("content_flow", "N/A")}
++
+ """ + st.markdown(target_audience_content, unsafe_allow_html=True) + + # Content Type Section + content_type = analysis_results.get("content_type", {}) + content_type_content = f""" +- Demographics: {', '.join(target_audience.get("demographics", ["N/A"]))}
+- Expertise Level: {target_audience.get("expertise_level", "N/A")}
+- Industry Focus: {target_audience.get("industry_focus", "N/A")}
+- Geographic Focus: {target_audience.get("geographic_focus", "N/A")}
++
+ """ + st.markdown(content_type_content, unsafe_allow_html=True) + + # Recommended Settings Section + recommended = analysis_results.get("recommended_settings", {}) + recommended_content = f""" +- Primary Type: {content_type.get("primary_type", "N/A")}
+- Secondary Types: {', '.join(content_type.get("secondary_types", ["N/A"]))}
+- Purpose: {content_type.get("purpose", "N/A")}
+- Call to Action: {content_type.get("call_to_action", "N/A")}
++
+ """ + st.markdown(recommended_content, unsafe_allow_html=True) + + except Exception as e: + logger.error(f"Error displaying style analysis: {str(e)}") + st.error(f"Error displaying analysis results: {str(e)}") + +def render_settings_page(): + """Renders the settings page with all configuration options in tabs""" + st.title("π οΈ Settings & Configuration") + + # Create tabs for different settings categories + tabs = st.tabs([ + "π· Content", + "π©» Images", + "π€ LLM", + "π΅οΈ Search", + "π¨ AI Personalization" + ]) + + # Content Settings Tab + with tabs[0]: + st.header("Content Personalization") + blog_length = st.text_input( + "**Content Length (words)**", + value="2000", + key="settings_blog_length", + help="Approximate word count for blogs. Note: Actual length may vary based on GPT provider and max token count." + ) + + blog_tone_options = ["Casual", "Professional", "How-to", "Beginner", "Research", "Programming", "Social Media", "Customize"] + blog_tone = st.selectbox( + "**Content Tone**", + options=blog_tone_options, + key="settings_blog_tone", + help="Select the desired tone for the blog content." + ) + + if blog_tone == "Customize": + custom_tone = st.text_input( + "Enter the tone of your content", + key="settings_custom_tone", + help="Specify the tone of your content." + ) + if custom_tone: + blog_tone = custom_tone + else: + st.warning("Please specify the tone of your content.") + + blog_demographic_options = ["Professional", "Gen-Z", "Tech-savvy", "Student", "Digital Marketing", "Customize"] + blog_demographic = st.selectbox( + "**Target Audience**", + options=blog_demographic_options, + key="settings_blog_demographic", + help="Select the primary audience for the blog content." + ) + + blog_type = st.selectbox( + "**Content Type**", + options=["Informational", "Commercial", "Company", "News", "Finance", "Competitor", "Programming", "Scholar"], + key="settings_blog_type", + help="Select the category that best describes the blog content." + ) + + blog_language = st.selectbox( + "**Content Language**", + options=["English", "Spanish", "German", "Chinese", "Arabic", "Nepali", "Hindi", "Hindustani", "Customize"], + key="settings_blog_language", + help="Select the language in which the blog will be written." + ) + + blog_output_format = st.selectbox( + "**Content Output Format**", + options=["markdown", "HTML", "plaintext"], + key="settings_blog_output_format", + help="Select the format for the blog output." + ) + + # Images Settings Tab + with tabs[1]: + st.header("Images Personalization") + image_generation_model = st.selectbox( + "**Image Generation Model**", + options=["stable-diffusion", "dalle2", "dalle3"], + key="settings_image_model", + help="Select the model to generate images for the blog." + ) + + number_of_blog_images = st.number_input( + "**Number of Blog Images**", + value=1, + min_value=1, + max_value=10, + key="settings_number_of_images", + help="Specify the number of images to include in the blog." + ) + + # LLM Settings Tab + with tabs[2]: + st.header("LLM Personalization") + gpt_provider = st.selectbox( + "**GPT Provider**", + options=["google", "openai", "minstral"], + key="settings_gpt_provider", + help="Select the provider for the GPT model." + ) + + model = st.text_input( + "**Model**", + value="gemini-1.5-flash-latest", + key="settings_model", + help="Specify the model version to use from the selected provider." + ) + + col1, col2 = st.columns(2) + with col1: + temperature = st.slider( + "Temperature", + min_value=0.1, + max_value=1.0, + value=0.7, + step=0.1, + key="settings_temperature", + help="Controls the creativity level of the generated text." + ) + + max_tokens = st.selectbox( + "Max Tokens", + options=[500, 1000, 2000, 4000, 16000, 32000, 64000], + index=3, + key="settings_max_tokens", + help="Maximum length of the output sequence." + ) + + with col2: + top_p = st.slider( + "Top-p", + min_value=0.0, + max_value=1.0, + value=0.9, + step=0.1, + key="settings_top_p", + help="Controls diversity in text generation." + ) + + frequency_penalty = st.slider( + "Frequency Penalty", + min_value=0.0, + max_value=2.0, + value=1.0, + step=0.1, + key="settings_frequency_penalty", + help="Reduces word repetition in output." + ) + + # Search Settings Tab + with tabs[3]: + st.header("Search Engine Personalization") + geographic_location = st.selectbox( + "**Geographic Location**", + options=["us", "in", "fr", "cn"], + key="settings_geographic_location", + help="Select the geographic location for tailoring search results." + ) + + search_language = st.selectbox( + "**Search Language**", + options=["en", "zn-cn", "de", "hi"], + key="settings_search_language", + help="Select the language for the search results." + ) + + number_of_results = st.number_input( + "**Number of Results**", + value=10, + min_value=1, + max_value=20, + key="settings_number_of_results", + help="Specify the number of search results to retrieve." + ) + + time_range = st.selectbox( + "**Time Range**", + options=["anytime", "past day", "past week", "past month", "past year"], + key="settings_time_range", + help="Select the time range for filtering search results." + ) + + include_domains = st.text_input( + "**Include Domains**", + value="", + key="settings_include_domains", + help="List specific domains to include in search results (comma-separated)." + ) + + similar_url = st.text_input( + "**Similar URL**", + value="", + key="settings_similar_url", + help="Provide a URL to find similar results." + ) + + # AI Personalization Tab + with tabs[4]: + st.header("π¨ AI Style Analysis") + st.markdown(""" +- Writing Tone: {recommended.get("writing_tone", "N/A")}
+- Target Audience: {recommended.get("target_audience", "N/A")}
+- Content Type: {recommended.get("content_type", "N/A")}
+- Creativity Level: {recommended.get("creativity_level", "N/A")}
+- Geographic Location: {recommended.get("geographic_location", "N/A")}
+++ """, unsafe_allow_html=True) + + # Create two columns for the layout + col1, col2 = st.columns([2, 1]) + + with col1: + # Website URL input + st.markdown("### Website URL") + url = st.text_input( + "Enter your website URL", + placeholder="https://example.com", + key="settings_website_url", + help="Provide your website URL to analyze your content style. Leave empty if you want to provide written samples instead." + ) + + # Alternative: Written samples + if not url: + st.markdown("### Written Samples") + st.markdown(""" +Enter a website URL or provide content samples to analyze your writing style and get personalized recommendations.
+++ """, unsafe_allow_html=True) + samples = st.text_area( + "Paste your content samples here", + key="settings_content_samples", + help="Paste 2-3 samples of your best content. This helps ALwrity understand your writing style." + ) + + # ALwrity Style button + st.markdown("", unsafe_allow_html=True) + if st.button("π¨ Analyze Style", use_container_width=True, key="settings_analyze_style"): + if url: + with st.status("Starting style analysis...", expanded=True) as status: + try: + # Step 1: Initialize crawler + status.update(label="Step 1/4: Initializing web crawler...", state="running") + crawler_service = AsyncWebCrawlerService() + + # Step 2: Crawl website + status.update(label="Step 2/4: Crawling website content...", state="running") + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + result = loop.run_until_complete(crawler_service.crawl_website(url)) + loop.close() + + if result.get('success', False): + content = result.get('content', {}) + + # Step 3: Initialize style analyzer + status.update(label="Step 3/4: Analyzing content style...", state="running") + style_analyzer = StyleAnalyzer() + + # Step 4: Perform style analysis + status.update(label="Step 4/4: Generating style recommendations...", state="running") + style_analysis = style_analyzer.analyze_content_style(content) + + if style_analysis.get('error'): + status.update(label="Analysis failed", state="error") + st.error(f"Style analysis failed: {style_analysis['error']}") + else: + status.update(label="Analysis complete!", state="complete") + # Display style analysis results + display_style_analysis(style_analysis) + + # Display original content in tabs + tab1, tab2, tab3 = st.tabs(["Content", "Metadata", "Links"]) + + with tab1: + st.markdown("### Main Content") + st.markdown(content.get('main_content', 'No content found')) + + with tab2: + st.markdown("### Metadata") + st.markdown(f""" + **Title:** {content.get('title', 'No title found')} + + **Description:** {content.get('description', 'No description found')} + + **Meta Tags:** + {content.get('meta_tags', {})} + """) + + with tab3: + st.markdown("### Links") + for link in content.get('links', []): + st.markdown(f"- [{link.get('text', '')}]({link.get('href', '')})") + else: + status.update(label="Crawling failed", state="error") + st.error("Failed to crawl the website. Please check the URL and try again.") + except Exception as e: + status.update(label="Analysis failed", state="error") + st.error(f"An error occurred during analysis: {str(e)}") + elif samples: + with st.status("Starting style analysis...", expanded=True) as status: + try: + # Initialize style analyzer + status.update(label="Analyzing content style...", state="running") + style_analyzer = StyleAnalyzer() + + # Perform style analysis + style_analysis = style_analyzer.analyze_content_style({"main_content": samples}) + + if style_analysis.get('error'): + status.update(label="Analysis failed", state="error") + st.error(f"Style analysis failed: {style_analysis['error']}") + else: + status.update(label="Analysis complete!", state="complete") + # Display style analysis results + display_style_analysis(style_analysis) + except Exception as e: + status.update(label="Analysis failed", state="error") + st.error(f"An error occurred during analysis: {str(e)}") + else: + st.warning("Please provide either a website URL or content samples to analyze.") + + # Save Settings Button + if st.button("πΎ Save Settings", type="primary", use_container_width=True, key="settings_save_button"): + # Save all settings to session state + st.session_state.update({ + 'blog_length': blog_length, + 'blog_tone': blog_tone, + 'blog_demographic': blog_demographic, + 'blog_type': blog_type, + 'blog_language': blog_language, + 'blog_output_format': blog_output_format, + 'image_generation_model': image_generation_model, + 'number_of_blog_images': number_of_blog_images, + 'gpt_provider': gpt_provider, + 'model': model, + 'temperature': temperature, + 'top_p': top_p, + 'max_tokens': max_tokens, + 'frequency_penalty': frequency_penalty, + 'geographic_location': geographic_location, + 'search_language': search_language, + 'number_of_results': number_of_results, + 'time_range': time_range, + 'include_domains': include_domains, + 'similar_url': similar_url + }) + st.success("β Settings saved successfully!") \ No newline at end of file diff --git a/pages/style_utils.py b/lib/utils/style_utils.py similarity index 64% rename from pages/style_utils.py rename to lib/utils/style_utils.py index cf0345cc..049f5038 100644 --- a/pages/style_utils.py +++ b/lib/utils/style_utils.py @@ -267,79 +267,147 @@ def get_glassmorphic_styles() -> str: """ +def get_test_config_styles(): + """Returns CSS styles for the test configuration page.""" + return """ + + """ + def get_glass_container(content: str) -> str: - """Wrap content in a glass container.""" + """Returns HTML for a glass-morphism container.""" return f""" -No website URL? No problem! You can provide written samples of your content instead.
+Share your best articles, blog posts, or any content that represents your writing style.
++{content}""" def get_info_section(content: str) -> str: - """Wrap content in an info section.""" + """Returns HTML for an info section.""" return f""" -+{content}""" def get_example_box(content: str) -> str: - """Wrap content in an example box.""" + """Returns HTML for an example box.""" return f""" -+{content}""" def get_analysis_section(title: str, content: str) -> str: - """Create an analysis section with title and content.""" + """Returns HTML for an analysis section.""" return f""" -+""" def get_style_guide_html() -> str: - """ - Get the style guide HTML content. - - Returns: - str: HTML content for the style guide section - """ + """Returns HTML for the style guide section.""" return """ - ### How ALwrity Discovers Your Style - - **AI-Powered Style Analysis** - - ALwrity AI analyzes your existing content to understand your unique writing style and preferences. This helps us generate content that matches your voice perfectly. - - **Step 1: Content Analysis** - - We'll analyze your website content or written samples to understand: - - - Writing tone and voice - - Vocabulary and language style - - Content structure and formatting - - Target audience and engagement style - - **Step 2: Style Recommendations** - - Based on the analysis, we'll provide: - - - Personalized writing guidelines - - Content structure templates - - Tone and voice recommendations - - Audience engagement strategies - - **Step 3: Content Generation** - - Finally, we'll use these insights to: - - - Generate content that matches your style - - Maintain consistency across all content - - Optimize for your target audience - - Ensure brand voice alignment +{title}
{content}+""" def get_test_config_styles() -> str: diff --git a/pages/test_config_settings.py b/lib/utils/test_config_settings.py similarity index 99% rename from pages/test_config_settings.py rename to lib/utils/test_config_settings.py index 1ab9a2c8..316c0de9 100644 --- a/pages/test_config_settings.py +++ b/lib/utils/test_config_settings.py @@ -148,8 +148,8 @@ def render_test_config_settings(): # Set session state for navigation st.session_state.current_step = 4 st.session_state.next_step = "personalization_setup" - # Navigate back to personalization setup - st.switch_page("pages/personalization_setup.py") + # Navigate back to the main page where personalization setup is rendered + st.switch_page("alwrity.py") # Title and description st.title("π¨ Find Your Style with ALwrity") diff --git a/lib/utils/ui_setup.py b/lib/utils/ui_setup.py index fb54c9fd..e2b6d623 100644 --- a/lib/utils/ui_setup.py +++ b/lib/utils/ui_setup.py @@ -4,6 +4,7 @@ from lib.utils.file_processor import load_image from lib.utils.content_generators import content_planning_tools, ai_writers from lib.utils.alwrity_utils import ai_social_writer from lib.utils.seo_tools import ai_seo_tools +from lib.utils.settings_page import render_settings_page def setup_ui(): @@ -67,40 +68,73 @@ def setup_ui(): border-radius: 8px; color: white; } + + /* Sidebar navigation styling */ + .sidebar-nav { + padding: 1rem 0; + } + + .nav-button { + width: 100%; + text-align: left; + padding: 0.5rem 1rem; + background: transparent; + border: none; + color: #2c3e50; + font-weight: 500; + cursor: pointer; + transition: all 0.3s ease; + margin: 0.2rem 0; + border-radius: 4px; + } + + .nav-button:hover { + background: rgba(0,0,0,0.05); + padding-left: 1.5rem; + } + + .nav-button.active { + background: #1565C0; + color: white; + } """, unsafe_allow_html=True) - image_base64 = load_image("lib/workspace/alwrity_logo.png") - st.markdown(f""" -Style Guide
+This section will contain your style guide and brand guidelines.
+-- """, unsafe_allow_html=True) +def setup_alwrity_ui(): + """Sets up the main navigation in the sidebar.""" + # Initialize session state for active tab if not exists + if 'active_tab' not in st.session_state: + st.session_state.active_tab = "Content Planning" -def setup_tabs(): - """Sets up the main tabs in the Streamlit app.""" - tab1, tab2, tab3, tab4, tab5, tab6 = st.tabs( - ["π Content Planning", " ππ€AI Writers", "π€π€Agents Teams", "π οΈπAI SEO tools", "π±AI Social Tools", " π¬Ask Alwrity"]) - with tab1: - content_planning_tools() + # Define the navigation items with their icons and functions + nav_items = { + "Content Planning": ("π ", content_planning_tools), + "AI Writers": ("π", ai_writers), + "Agents Teams": ("π€", lambda: st.subheader("Agents Teams - Coming Soon!")), + "AI SEO Tools": ("π", ai_seo_tools), + "AI Social Tools": ("π±", ai_social_writer), + "Ask Alwrity": ("π¬", lambda: ( + st.subheader("Chat with your Data, Chat with any Data.. COMING SOON !"), + st.markdown("Create a collection by uploading files (PDF, MD, CSV, etc), or crawl a data source (Websites, more sources coming soon."), + st.markdown("One can ask/chat, summarize and do semantic search over the uploaded data") + )), + "ALwrity Settings": ("βοΈ", render_settings_page) + } - with tab2: - ai_writers() + # Create sidebar navigation + st.sidebar.markdown("### ALwrity Options") + st.sidebar.markdown('', unsafe_allow_html=True) - with tab6: - st.subheader("Chat with your Data, Chat with any Data.. COMING SOON !") - st.markdown("Create a collection by uploading files (PDF, MD, CSV, etc), or crawl a data source (Websites, more sources coming soon.") - st.markdown("One can ask/chat, summarize and do semantic search over the uploaded data") - # alwrity_chat_docqa() + # Display content based on active tab + st.title(f"{nav_items[st.session_state.active_tab][0]} {st.session_state.active_tab}") + nav_items[st.session_state.active_tab][1]() \ No newline at end of file diff --git a/lib/workspace/AskAlwrity-min.ico b/lib/workspace/AskAlwrity-min.ico new file mode 100644 index 0000000000000000000000000000000000000000..abaf82cefdd4a1dd973604ac6b0eab04c68cf00f GIT binary patch literal 80525 zcmV*xKt8_!00962000000096X0Cze802TlM0EtjeM-2)Z3IG5A4M|8uQUCw}00001 z00;&E003NasAd2FfB;EEK~#9!- Welcome to Alwrity! -
jtugc$~R$q<6OBqV_lAVSg|>v-Drx*tD#@2dBY+WW|r?%NUI_r8_S&Fy>6 zmeN(L)_T^n#O;sl(4OZJvmGqULTH6Xp#hXe2LW1ZwAKI!1OkQ9il_)7s^42eBEZ}~ zOVL^*gs9!C6`)aCpaCr!ua9Yo29(e!0aD1u=K-2p{i5G g>l+@O_>777SV`?cH9A6ocz^$~yvS|G8IS`vr=jYfnI7NW5~qOqJ`|Ic!L-FONi z5W+HVQUL;umUcKV%$$_kLZAtBV5+OM8NjGNRO9 7GW1D0SE%2!|n`ctuW(b z>SyMO*1BO{2r+$B4qQyTqI&{U6Kw+mrM<@foz{KA64M5C_I|GQYvX)~-mL*4%|r|| zpcI)@99wG*O5^)Jf$vubSSyWrb?t>wC#g^%Odm|s|BV%t)2@kxMNMGna1m GAlo5ayk(e<550o+@Q0v;TXkD#~suL#F z<*Y-&qD_0Mql5+>P6%W}|3vkgy)_ziBuJ(WIM%c)qw$D(Eqb4+kk)kzTWh2iXaq_? zAQVCerfwpf4cec*M)MQ^Gw)M%_la=+X|oUn=pd@EMn@Zu1|3~j_kMv{KvJ5%R7y3h z9Uac6aM%&LHeaJ>nsKU|pR+8u>HnUOK&Y-AWyVd0l`xxatqu}Vt-sb9g2oL&Ax&Lp z&iruhG+wW@#*&ul%cwK7FhWNNY^lK#W Mj)F?pht}dDK%1Sruwxng|gBEM{q!5Fjif{U>^NG-!xumlsG8 z?pW3S45e_je QNQ^aTXy za2!QlKSc{t*EBRyS6?}YrZZn{pmu$@xTy*7>g<@NItwxF0}d=;XZub{$;>Go2^oRi zsGihc5-zZL4El_U0CROqv`}jmG`r)0HVzRz6dH~mFjHkeScx_T8EqHyctEr?^el%P z5!Rum#7pz9y4`!42&!HgDVgSaqoPNu;hc48sPFVno4HWy$3kck?wrxJ0xcv0Wn>&0 zCCs5mMhj3l;i3q_{+eE&L*4Q9v|V|6TSFB*{1Mfj6`>HAt=;wGtLD@I%&p;a3xCfz z@@ZkTrSQvWzD3s^s``JB-k7rub=94ve-2dU+L#;H5kL?G*g_g bHYqJqsnP2wTRA$#zrusbEG-5>u6_=&hT0ygq&L>$f$PCwC~N-P>5OQjuPg4 zt+iFx87e$0YlNOdb&IMyeFu)_Pqj~{Pv~e@)Zz9M?wHlv>_!zl8n3ECrU`T?ScPQz zf{}HaXYK4?KVL;OaHd-iBJie<@AUaM_gQ_8Bhvxqt@-=F{XM R;U5Y zdGkS~`+>}T|G!k8n$(})Zc%@ZkM23pP8aHC(L@lk`q{JE0}XA77FW%9RcpgEm32lR zOrOAWiL7U&*0VhC=|2D2?dQK`(SUs*m9E~pKktXmas4x^I?Y}FR_}Snimo%-HT2ya z?U=sfR{w6at;~AsA&%wTB|6{Cq7y|n9zu(QKJWsse}Xw49DIj6;JmEv@KKrRHEOs{ z2M6r889V$80akCsoLlp3XyYv3&-e3ZswiO;0{dBc`u|^*r#nD(E>Uryoj)qmj|GzX z*5RoL^aIy>ZVm9Myd`p1{q5WG_bK!BAUrGY#)vLkV#yaH!9l<}Q9Z1E2v*d8Rg@>K>hml{m05wDBp2Q2oe 7P6UE&hM)X!AEH|1W9*hhE>A zf !5%YV%-$MExXmLi!r)T^9GmSvC#czrX!Ba-XYsBXrdO28|>VYl|I$Pb5CK?;1 z6k0@QYh6W})xT#tF5^IT`dbTv1NFsInSZlwR(~(y)_|T>YW{#CB7eTB!r fb8>ZCN R{ND9!Zx$jvlh;gWp<1qd^KM}^!pEoAr_OtF;P;rKXLvUv8{B*~ z0RwKcLjb(NT+O2+pQ_Z2@c;klN7-j$q8!TPoo}h_{534UlorWkhFBuT$jA`)Z@P!a zAKJ{Gz58%%o7HPpbHs7SapZ<$a4egNi7_hL2zxzk*O4+ZDn%0Y(1rg$YWdsPPi95U z0X4M5vtNrtw*}Zj8~9@c6P)i(deE4FX;#kZ*Us1AI2~y?A3yQDp8G6Kiuq#UXK$++ zzD6~F`ZY#Nb6uCVjy4|N@-W}}(wF$vkA6hp&mO3A)KSOrPw##o=Uj9#r9z&3E<3m8 z6KMc-b(rgz@c)7kc-D9RD7bB|`7Ep|reB~As?q+fdQ*c5_}jR7t5z!7RUlw~M}PxE z9(DDMG1E&Bxm_X*@F4s_-pRI;|>5WxcDVp_3=;8-P6O^ z$S9HaS8w@IVHlp&wK1Jd0Q@5y0iyRj9q`%Ht|zJ*$v7J@N{5ku(RI^}c8A*DgS4g= zJXJL!%Id%Ce+OvXn1$H*;1fDfL4#_pL+fIFg{u{vjvS NRdpryzW((@t0fwz-wOn zM&9(#|H8s0%Td0j>{U=oVObWg>msD)v4=PFwa Y0pNV9W4A vGbxhsIJe$(3-9^o zw=p(0z;}Q6Q_j5LBF4r?v4lj1D7 g#sz%7`^nd@Y`|CggKvcW#!0?!V z1OcEM!rPy^0GKNnZcYtAt55(u^oUlfF{ l?p_u)?X#!xn%|J7= z@w0a1Q(X9m>ZhLVdMPDZ3Uav| HfQfQGdLY6P{e zKcsf**#~NRIHxqk1O#X-%VKzNfKsVMGMS`UEHXUU&*a1eqhq5K@&%4P_E^q5_Z)Kh zGG4(4X~sh_yD>^=V (&Ie0D(JVx_q%hM77GokXL4vKtm6wdr1 zzlR_IHtRr11kxCOm|X*?`rA1)0iX}!0I8mRRM-VGNzrC)oVl!aedCTVp2m(JxkOFn zo~i(-uT%6~^B7tCTNn<-C@=$I7>f#pJbu}uTrN?nRB&yZ=1hu~mKH3>rC2JPHim6g z;qEV#KuOi`MuDcIt({^v%eTMrC9c2zT88@v4_f!5jyaic{ov=IDpV@vYI|l10N=10 z3Cp;>wQoGka;C9MNNFY{24^6E;AzT+83LdV7owRR&(LuHQ+of*ji&a^92;hY-Br^y zg7A7dhb9>s5z&U5Q-;h`H2~1GqxT%`sb3F5NCFg&E3u`PW|tLD ^cEQwUVxhun1YF9zKe4V6R*3n3&HAZ4TtfWpG8oB*vAfeJ{(5{Os= zA(}xNq@h+CPe~jhX>D#s2uY>lQ7o4Ulp)B}b&DUZv86> f7;TSh#Q ~sxc)>h&?K&v-v!{2=Tr1C(OH`o4Q%%HO`3!ll!r!*kGL)JjUDO w@R;+)G|WXJ G5Jbqk;ya>i@Ege+r#gF_iG-wcO!dt?PPpxoS;&{ zl7dWY8%tNL )IlD&hwtn z`#$_%EMK*fk <5CdJpk@@2mErO)%}FMO2?F1iRO;ZiDuf=yZ!a(Nzja8vj! z3@dJ^=)C08D>&|$W0}lOA*Ced#t>3eNmC =7fk>f@*k|keXXc!m zeSE5hP|Gp`UPGV)R1j$$GfQ-j;O`s6RI5$xUrMP4*5dh=kj?uUZBKQ(-Spt&+BaNO zNUf?8Mpb#lqMbDee}|Qyc4n_PI?Q<4JTTkyjY4}WR{VxM1k?U4rA1d)2RpWI;p(eC z!mYpm-JH*9@9O1a*L;>sUi?DFMurH=6?8Z{5rc97BwUwFI>nuL-NAL=_!4*Cemlxn zbhdSn%%l;vMQ$p~$lxF)uT*<}M;HI{uOHyDSHBhy#l++oQb-&LU-;kurZ6?ZqJ>Kd z5IlI_J=}4}Z2&Z7+W733zsUucJeQ%tK~RcHxx}x3`AeRA(eqf))5FB%1fJ(15HvNl zqXUnReBj;u>gPWV`=Yk %g^)u(%vnmK0qAq7M@`>=sR3FDtSJ%iUFozA`VBWIn>#fS}JsSk4 zY8W+kBgd79jhi3UzB0SXp$h;o QZG1K{lQ zF5uG3uVCfsb;Obhrt(ufw)p{m^1bW1Z{tP)-u1q#c+dMk$jJBzrDCZzX$8cb1m*k` zpZL!YanE1x;Pf-j<)Rn7h}G*3CzWXe+aeZ^Q7)Gl>K|sPe}KF1xQ$=^>__a~y%T^9 zM;^xqulhKv*BwSV@Ng`f*5+m$$DxofQ7Tsus=|%G{4pQ-;QP@S2=FKW`wP70jc;Xi zY?xebf>bIq%kZ$_PRnK`O<)8#nP^AN>%IJ@R1KC$+_Y K?~-1wc-0kL@`4vImCsQwl?jw0Ak3q1KmlNO zcgwQEX{C@t($?C-(7+(S`Q=ae{Vl&^+m@|3PMlS%R&(aL=kbD b@A9Aist>-NOYR{h=*f9OYbHxQb6J Ad+&Q_YR*t77O40N$F_;PG4}4=!&koiMSl3*YiIOR zB9Y)d@A&{Pd&R5y!S&a1%S|^hG%$#bz>T?BmJ=>uQNtT(2z*7URK)Yjyz0tV@h|`S z4sy91xk8bO&?c`%gFi5QLOS%O;tKSB;5@WD3Wz`SB0F&!;~5amGiD;qvse&f@gb z&&IMX9)J7^cJJ-upWgXSjyvvna@pL>h4D8M0Fj+R&2c1&7I1{Dojw=mGKkiqXXlQ? zh@M+t<|hD*7a$Y>4{1M2m8JQAQUJ`F*Wdu3NhwJr5(Hj_<2J0|> G+uy!pzPvVGeY&N}a6KK=PGvaq+8iENgVS0?Z(;0Gk) zadP=QzUQOSBvWxLDY^BgKk&uReVYCIc5}t6-^5E_ei`v}1}z|yOp?u(*|p 9uXi$Nn-VT(<@W>Fax$;$f;DaAzV04shr9_~FF^E++mzrDH1fVHyx%no3^26^@ z$mcof#FHqMiu~@T8vuyKT)uYgw>alH7gMTev U IpZcWz*f!&imf|uk7Bviy!^!*G%P$6tjhD;*6>_Kg><2LyUY!Ybz6DqrBnOm-EmA zo5(b`@=x!0AJ2K-^Xcm8KnO`8pQltTR3rP+&1uGm2Ke%)Kh4HF?;=o|y#spySiANJ z&VB9+S+;y7%_-w4C{+qP`pEtK tJ8SFSO~BcTz}WIBq%hdtk?TrT5zo_V$qNIQn(*npzql^Go#;iDhB zidVe)3U=+?L&Z1G)`3EWhZU_f1Ob&wg>t#V(9jTjcJ1N!x7^IV8~@x;wXc5d>s E z0P5Wj|40JhsTlm~OCphEY-EUc{L|a`_I2MUkx7%!X0ZWZG)iNkEu-#=$o-{hX=%kT zm$>?4ALfVOsj1T)on0(jwT{=m`E4A3!U^agAeD@>apT>5<+Gn ;$0N~Qgui)xy{+EG~ag_G4QApt$ z*CHC*F-cB5&zqI3WWICunV?dCI_qP$z`1$>FdJjg(-8oTM! emoWp{i1(c?8{Nl$y
YDbJV&ckqF9OnenL! z#wNzd9fBZA#!|GlwiEaP&CShR|K0D>+}Y0l;UP1h0#JeRjY}!RgtP(DvW*c?NSfL@ z`17qd@bUlnFrWVH7uay@Nn~?5Y)c@8MqnG^G|wQR`FN6^t^jzdYodN($Un1MNh zeGbL|RBv L-Y?1Q1YTP^`YbT(#Z^t zuvoTqDWClGr&+Rg4cUB=pyV+zKF0QK+j#tuN7=n|7sX ml0l|6T&% zDIEcxPJ?Hy*E1ZCAGw+mi3IVu!)<^3Bhqp?_SoZ)QdG}~(y la3`>xqKBObcbb&S;of4$meo+fsbw3WYQVh+S*7alUUlq_x*4V zYXlO@wx}qdp^+hmMg{=rY;U7y!BVsrnD(L=pO|29WC+hMAF%H-sWi)%EoaZ(JxokZ zn)cJUY+TzVok|mn#c&*_3e1tFTKGYL?|YQJGKFFR$99OhF70hyyyflxjI>=gKe(BP z9=w;nzCJ3I3Q|gJDF}SyO-Q9PT>hHZ@X9N%#I|kr?B0bPw{aYYYH*+BHpIIdZ7-T5 z0a`)D^9huh!%}N-1k#cwIjT0vLyh0-;6mWpB0iblnSLg&;9&Kgd7(AqdLd*j80c@U z0UT-r=6BvVQE _bj@|sVwbm` z09QKLwn=~)r~rR@Y*ct85tc;SHo5E+yZ7!yDaB!Hk09yB@O&SHL<)-_@aP-d%i!4X z?0pEdx3!xyez{cLHKTEE&NR`{) K=FbgeE z6e~WSr%*aT+5*RMuw |9i>iatsX*8F!%!$Gc7d)C&TI z#&QgpEuBo#oXMb7z~oexp|LS4zQA$QNMWp!LWt^EA1H9_sR)4ikf+Yus5eLS?K33+ zY*U%JUeM@-`1?aE=PDZ8mL$^&-t{m4!nzGd^7=Qtne5mEfgd#Jh&oWTwX{NHltNe8 zV}R=4wzTMIZ|B1wcsDoQd=tkUe*$lN=X+Uq*x|%uF2!PrJMX-MuYcjQ?A-dqL2H;v zH_^Lr5td~U1PUPx79sFM%|uvOwv85oatL>wbwWmrJxZZHpXR1!mM>Y!)}33(
T%PHiu z6iY>;i3&Zd!AvVnDj7%VGO4)B;)UHL-53Cix;j{`mh#AB+Zh_m5>L1YD>O=?vBRj> zda%+nS)M6M^=JAyVW5W504hkFX&OKTU7Vhn;2%u`FxYQRS64g#^WWF-@B@$Yy`TJy z(a{mKS3al-%nwzpNcHza0npmi#I7CNdEU9Fa^)NUKU{V7HJ}5s`8-N#q_oL2HB$~e zzVW%w^7Su#np|#b#`A5 pp`h;!lR3M2fm* zWMqiZi4puj6VxTVOeWK`wRX|f-o@zHFuV3{J7}M`W|~;hyO@L wfS(4%=`PJ9qCw ziNJu6go~6`!|vabX<}+@g!jDrpV_|kNo?Dqv!ja*habU(7d@BDFS`tBNgjK0C%XrR zNu*MtXUD`g$T>8C`9%257p{JwL?H)U3)7O99&!SZX&OL16xf|%1JHUFm9w72^O*}6 zekPcCgfPyMcrr$5Vv5sGJD%_V@VA_B(n*X@juOt)x%)QU1sXfp?9K~1+Ijyw-$5>$ z=Ns2w&(PQi -C(|@HHDNmrfp6lu zbudjN$wp|ku*`%Mq32OaL)R6h01Z%~#wM|(1wt7=zt#kSkEax#U!hRQF)=w#C1eVY z$C51SUBvk0D5ImJbM{GROB*YfEJbTgu2^6ym!()PQ}QYVT48I6ZMmcpX_E02(w3n% zV+59v^|K`uW_28gO!L?4a3OnMh5mtllvK1dw^1sVShaEuzy9@aC 7geX8k-`SN*Vn&?0$Qi@%Lb5 zzKl $nYEj05q| UFr`c=~DW{x`cXbQ!A*u>c_z_oj8 z(!chZh3z;D4h=CpI>y!4e1`s!F-oNZQfsslB$G*8*WoX>-pUWJyN)Lxdy-T<&a&R+ zpnO1JNefF_1X>Yz{=6lpQW~T%JbktVt;rYiWb>1h$|baKRBS12;_*1?bc#eGfh8Sm zJ4VceBhi{^rl+%qk;w@L2Ky+L3vAo5h4%J#T-T)%A?ND4sH3Hog*`p=4G%FqHbSvH z(|8zjW9ZO$nJeUQq{MaO*tUx$ZKPu&EG;RG8&(K}tP78w79JdFJFer<+1|;Xfqe`P z^^u4txc|Ys`RG+2 uj;XWO0>`oF?rP)1ANaR97eBz3ty?(r%+vY7 z_kX}EE`J5T`@`+{6$i_45n9bSv;S@K2>gADp7w9Mwy)3O8;xRqstgd3tK|??k?GH! zZ-o!|9*t$$_~i;e|Je^X@ubs9q*IKIjZ|AS17cb4)e=!qkg*2`fiZ5`w#C+6J2~sT z=g`#N#>C`=Ir|D|ZEj|;zn`yu@eBOv4}ZXwg63oj+cGL`1f5n&g+w0p{W}w(rb9}0 zS-s5o$dwKl8y{nIbcAB1h$U=d@i_5V4A+ezg@x8oES6A8V>{!te0uVNxZJnf( zDfad4C11=jIyzbnys0nAM1oW@#n$axDNKXaHl>?sZ)qi!OybxUT0yyD> F4SVY@cciqe(U0>>IQoNxn5C6g@fSwvs|UUTjTzWMEMaO!EN z^19c*mQD9POs-VGb_Iz
k+T+rKAy{8c{x2j3po3X)42Jz zJ7{j}4xKA{maye{0&V_5HqQC3$7ufR5Snd2m6!b^Nl4pq7# 2KG^|RB$YZ#S53v z-qJzLO`xO-S$T{RlfZZj0t`aW*8 Vht-+t|6IDVcRy2Z2&r2D1_GJrgH4qx{a+{ zpQKbO5{ui_o-onpwLXdJ5d;CLM3U~#1q=-L8KDfk Z7zZce43`2l(!HuH~^uALjfEE@1199dviKF;y(# z#$(fhmF6!5{@!b$CP<&U5VEBfRj{Cr%(4e6kx`;r3+wMF1&tawIAna`mQ`DF7NG ^=<|R2N1y8b?Z3yg6FdS$Rk*>VhzPy0k2X z8=LOFn~%N!y_5?@THD)AfJ;!9ji-)QO=uv{U>T!XWU$kOe1?K*bX~+tptWLjY=pj{ zel(i)whk6{_u$wrzNg6N^Py%zh>8n?Oo&Vgnm}oWM~0c08bwHGZR#eSY9i*itX{Q> z$G4Qo=W{d0EW-DL@rW#2vYZ8-T}Taq=OeVn(gLKzvUO|eTCfB!&`eHd2?CFaiE*-1 z6Zk;|OWI^I8J4eH&bqa0*|cdRJ9lj-7I#8iNexzA6UnCdUO+mTp()kOWPTD$2t428 z-1E-is{go(S6==~9(&|bKJkf }vpxb}NLrmd}$Ty~1(D^~J?7rmIT ze&tJi;WPikr7wONKmO&fnb^4-sbfY^hWLQL71Qxk)gT&z@ajNNIs*J0q;8RoR)y~t zn;v*rdm1DX9ffb!H&Rc-0Zb 1T>RcfNnWhZ)ZoHe Dk%U| qI-$kWZAel^oHt^sBa|Mc!&P5@CAUxmKx6$mXKIKxG$;k<(iY8ib zQFkvL9UXX;AhZjB3Idd`@G2ENuVOF`I{ZG2=(KH#<=A*CV03H*rF~o{PFrgS3m5hv zgw6dA-iy}WtTnbQi?yp()6v`tq03JzRqfA~u5Q}877~l4%^6;59(m*uwr$x$%#Go? z7GMmMwzOHaXfYc$9LB(4KMy^$+1NHhy0rRvQ{x*ne+SxU*S?)BTe_5VrkRHyd1&7A z;;N5-l2^a>&Gh#Tp@qR$B;s+py1Mw d2C z<0Uv0Ay8+npke1fNPdhNUI3Iq0&aTXVeKK2+Nf{S1%Qrdxg%9=1}m^^_`W_Y@~KG5 zLlXdy$u#lsLz{W!i=Rg Q41F-Q`K(SOpYC$TMrn$9+ zR3?KHbCFUK_=@p~arW)o$K=E$ZY)kB5odgSfLz|VCtWATx^+jA%jej! =xe27@8@U%i zsa!-_lEV&LkCc)p9(z1oWQ_~I-mavSLRf;S$w|syiGO|PyZP+rKF7q=*g@w+N7n*w z{^K1eWmB${2~3VNgjU#&Lw9#CA9&~ghadmoIzIimFLKre7n9578wT!RLg1Mc01bSJ z^U*%S1oV;KUR{p|5CAh_&vgP| a(|FT!{m2Z5LKr4#HA~9(b;Pd;NZ{};C{XAoX17w<;v1|uFPzV)-xNMD-AqRlc z1YVh3F^`)_A*4Wg9tqba5Td4f&2nODgqNDM-P?CPfn^Jtn?l50u}Gy-!N-qI|6$*1 zdOH@-*4~b<6jBImq@e+~Y*wsV!@AY$=w8@MJe2}zQ47~r2nk-G80hO~^8*j^#G{W9 z1O #0M~Iyr_v;nX;QH` zws1%{wb9zuNpnjJP3b0FC&r@>Jwku~E*wW9v<=b(FXi(EI@&vkxh@kE6Ct0#%*7uq z3WW*a>hIsrD=xo+_RbDI`KhZ9IwwB*pVx5ttKY!L$Qa-gh `p z?xww|nN&JOsZt?dDl$1WNim ;H`0Qst%eSxnHb46D_h&4LX!kz#zMhH~smqNZX}SF}9q!7XMQYVw<*Prf;AJN+mX{ps2Pu z5(FV&PaOFE#?Su21ONxg;dY>RN-62=Xy=F5U&n{u`>)fVjw53%=<1}St<_+|^I1m6 z$C=7ynaz5dh$rY-u#l!?6TTN<@Nc@B8tY)4Y=!V?+qNisWw!3vLhph_bhLHi`(6!# zS)G(2wm$^Y1ineMp*7?RIo2PxfpgA14=jtm{sH>;_c1m)MzK)9wj@o-BrUD2q|#|( zi8xLyK|GP6silS9-d BW*3Dhx={_0eU@cwCgy1elx#Qd&srce5y2rJ3ly@);NyAP52$FIsF|aCI8M zjNbyr#>ctr@>lSNH@=ateeKJ9{Tp9z7(2_hdBqj4 xVyO(c-0)%9UBi^J#NcPMb1H`;80x5NOe(^(8{fmO;8$ z)qXnI+^l`pa0GZr2Xz8KPYYA5C8?ah0BA@wI* Bo~sO6vO3-O)`~TNhdgg210MCDsg@aU`~55h#x>+qMvQT~@DL9|j%y zD5U9>$i^DArEbAkKsHxk?GZ n>+9NGo|t*+qOSZtt}{$$s}4Siq%L*L2pkl z-JJ`Jry~-6x;C~#g_?{|2HY2J#8u;~h=jU9R#qLLq4#6ozI`BI@xnzRq Feu}2>zpPgjGaVPMG*S(H(EX6m!_63eV?Q|9`UV`U`XX|NG@n8bsPz-oxeW)1% z;6Tri({y5^LDXyj)z6=*0NC`v!#co0i@ FPNXoT9hHiQZHJIjEvkzjt?`5kHw+`#47sNOOH_rd3 SQ zZ{J>Gj!SoUcY_ zJqhmyMXOGcQZIL!t&K?uwyX_`MgPG9wsPx z{Lx4F+7~|0&h3wrNVRd=sb_HF$tSXW^=j6yTZ?1Guw#-!$s|A((w=kC9O(0>A^_^2 zsSdJ0W~`?u8+T|Nt{PJhQ2+!e6ANGjfCM3`(i@Bm+uYH}XMWI3K+&{1gf$)605k{B z 8(;k#zq|3*4D|I;srYnvb@H4GpG(i;Uhcbh6ZhY ?@G6@J>i_r*a zL9UQzcyt&fw6(U-*4%+WqLks$E0&8S;&C2%^g)VV?W`ZQXkk}33wstLESt&6afSwl z= 8})NhIR5HMdsV zB!ocwfiWdmqI! NpS#QB$8#8kP+= 6IxJOK2%@T(_FJ$7m^XPI~SGm1mPRsOFBJ7Ct=3rg^a3fGV h@iGC46xS662hR;!e*?jTy5C`KU-aAsh5kbE(REiAe_yJ%@?2QBfH zPcoihY+{7n`*%0=MLL;b@vX%VKb_pZ>u ~#G?d=UoN$P^Dgb@T%8!ar*KIKZ8 zTs}{sP@q&U;RgX)2cZBkL?+2Zisq&k(up*V v*|X zq*yF-#yRKnoJ%gDxwC^H(1rs6fh7d4W06k9X>QJ72|=M)#Pf}SkitX(A_UU13{glZ zoCFeK&CY>&pa{RemjH;+rVa0zuK*Aht}AK+KuB3N$Uu!2e7ce1nbrVAofqJM9Wcw^ zB21hqWL+ee5GI?BZIfwA)0$56&VPD4-}&}etM{!uY(3{+cp)3s9md1=J;($1Kg`tB zB!Q|>$mJOt8pKy-F-PI52eML1A&AFgq?2iyQcc8Su_}H-3Q4(KrlqxoBaS$dhaT8W zu8_sDT~0sqTyDSPPe=ro6qJfNCbC(sxbih@-Lj1*AAOumOPXA6oNe2-vTX5kQpuF@ zAVg 13MaOIK8}`jL>)m>dWq^j=I%O)@b#PQH+vw=p;FCg|>5Kyy>t7!ke5kPr|A zijK|&6bmJCg~@8{f@z sTyY*hwlCr&KDT z5hx`=m<>Zpv(w8kV9ESg4I03K1VD6;@6T-;nyWn3)angckLzt&)VK`7MRDMDB*I-* zOPrX%K1jgavt9%>-1YQo95f(<5Ynh>FiweaDfu!$TAJ6s>Qa9F^Pd5*ZvA2W%RAr0 zspp=9>pFb;V;|?XTW-O1B0Rew*`6em$*_0tE()arLd`rW3m_JYlS(B?CDX -@p#0a#<>S zchTFk5X*^UNsDAM$yAsppu1}U(z4mHYdeK}7RPp2yl5F{@O{79zd}j^KVWcVh(IfH z*-5;><4{YfQe^l3opcsD=xFaWy;=v6&gJqfUABTJ9)GABqKyzJEG!$Eno{^d!1hNs zv-RNziN)jCjtfXU&tuuj6}<6{Z(!BhRpfFeI#ekg9yLwBMH!s6Sye MkA0d{Ap}c@kf9#Xo@XO!O-r9n+1X>3n^fe$E zOR{F=I`;10$5d_tfxxzHY}+Otj}ec>a6%^A= o{+tXZ?3ZBK4RD<2Va*t>fd63v3n1$bV7 z($bJ=XoD(LLQ^OfC|1gNA^bKLixYEVNWgY1db*dezke5{a*?6oL6-Ke#8y_7R=X+H zN^5HeyY_6OkT+E2y$co_f##Vcs5+ OpuZDQN@t$ga5|7OXuWfY28Bu=O~DYUdff(VajDqQ}K9Se8V zxfgG9b2Inca}VR=;~akY;bikUb0pL=v-i$6m!D3&!3<)NX&Sni=d*P9EW7bL%^|Jc zf4~w5a2<>M)EJ-ppHBkt)_;8$?|T0S7#ki%l?A{5?G4=Yn;U3vZpE^~V?>CsumzM$ zAwQnf*tUZN;*KH1v~5E!8F(g>u4RevNEzx0fu>L{k#26{!i&zQXF(4)-+VKB_U=VU zhhvU9mb1=3m(kHtwr}5#ZP{oQkk99cxiMN=T6z4jCyB>nOl7AS8y{uyf?iBYttii) zIltyh1qMfkD3oTh|Hj-nEzPZ@Q!O~IO-Dxu14Dge^Eq<)EUC8kDmlsG#fupl9wZko z{*Jaz(&^N!JO)7!VB0o{WCAND>FHS#+AoCR`KyETYE?@kuC(D2$fPoKw0Dr5nqqKx z5HBb*HaSW%5vMuRf;LhV+kwX(e}wbSdoKAxiJjY@ARcpS1S)mK3V~2sYfy&FwJQ;4 zXn2^feEBPU{F+aNbvIxiAuTi4MaX!iO~xT9ZG`QROeV=sP0hL `WHx>BJjk)pG;gPzU>bhLKT zoNgi&Phkt2AW(RokLUSRJRjSQani}B^72bBXDXNHy6eBop8fmDG`Dj4ndflIX{UpN z#~yx^?9>#tW1;}PN|{Vkno_Aqsa(c!Tt-GrR-jZegYOx48(JYP 4Tm31@4{uFt1to OB7v+puM$~M;>{EEnBuBEgR1_D8wj5 znV{xDLPIhc$FUSQ{^pldDrKbA7%d;oG2ipCZJQHMJb@qm;QNN4#j>hdr4Cl+s{5?% zsrI^l#sm!1ya;yVGv}5Ze`_wlIqOkp`JAmrLMt50=I*-;An1J`{SQjHGE@|arZhkQ z{!bVi9HyhW2_+0+hmhvE{R8_MnHa0e{hro#mi8{At)r8s<`z;-&16z3T*pBxO+KGv zDnH4@WR`NNLOh R5KUn^Lb=Q?pZyZ3V{j~{e%4t2|{KYVt~o)I2EtLg6 1z>zk-AFyfTCRVO~DZXC;ZIehP5w>km$Wr1s zHZ7SZ0>8pPz3q)m<%?W=@$< 0<49h-P8iL@Pb=t@+9;`g`QLLr|c zm53V;Mo>m$ys&%r?7_BeV?*#g96RE_i|i|c$*J+shc6Lfv|AMqUKb@AnJ$Z^JOcv* zbai%<$uu!BHO5pvM_XGb{oD7VwICLclgk$w9j(!ayH0|xjxJJ(c(`sQN*J(DEN0W% z(#+Vn;TOyo^W=+phKB{IWI8;4WQ>-igSr_~bNTwpBPJYr7c64O?(K|?kJ8=Qfv|04 z$TFOaCm0#o&jXu_tXQ#${J{OZ_{Eo)*osn- z{gYXiuUbVa(?p;YUNO&|fBF-jy5?%OZheBAZ~rSw34|9^BZ;E*5OGKv!?VSj6rE#O z ~gx7lvvW?P#!z5n;ae48(GJ=Z)q=lst7`$zkY zj_A>3 P6=xeOZcFCk-t+xS5;{iOa6gVkq27{y ziR)udaP*P%Xjh79*_JCy 0s4pg#FB0-cd zAz^NJTyD=~1&cSqKRgUmr5WK(4+{kg`;Sih79v#<7JD&YVRms5TAhPKV7%vj!^7+E z{GY`Toab%~Xob18`QfK0-A^!;5-H3UW4H~2X09FUc(bXth$nNWTp?`99PmQ(W7t3_ zB{n_*2teB?v~J7l!-eQF$?yn{l_ZGtf0iYl-l-^ {e~}lcpT)ZV`wIJze|?Qf{IuB!==a8F6xT3$dzRxK5%- zUGf@6x5#}3jdtX(dQRf1Upl#(#k?{+1wJ7!Pee;OvNcI#wz O{$Ix~d`ew(c zf0Y@L9X)hwvRXst4ToUUPubH2xt`9F%aVBV#bhw=fN4o(7lVS$mE-*TGrYlXolT z33J`s$fZp-j*bVxFIR`a>eU$cUe*t^pPs6^zRJ^ba&c+WBxl)zuuwj5ag|~dKGSDZ z-<@VLao)gd9}DO-_P1HblvRHCb%4H5!6Y5BhSlbyyysi;?8zzGGp<(Azh4oVm@Wtt zRi!cjBDCT+JoncqT9 Mm}MyU!krH-FR@w~4$UtG!%tWmgD z;!8E@*|TbjK6MJ _yHn^XUKIQAItSVXOE4IyM21dJ3l)!cXMmVF4ERl zx$tB7^~j(pgw{lE#ZR5<&4&;2(<#c#%!T;nYWQnbL#zL|Net$xCxN6918O|Y4ZlcE zm%UM=#d7|q$Y1FYLPO^`|Kd9@URgYX49wivgjiaCK7KcHquH71Y{utYrLkn!Rb5 zd%WR$S&j=+jPUwnVnOcyfi K=Q^%gQ&K8{Uk#x zLx+vXWYo0?&v(j=gC#)hZU(%wM)8meJ>Wr{v*z>zB%7GA@$uU}IBNm|_2w*TZUpNs z)=cmJP{vyvNxvOas0hAU2yGojGuD1NnIGD}^~p9c-~&RJd^`IKQ5D(vgv1i)VP7+{ zcy56saoKaV)0wwSU4$!@G&9Xw 2EqLCRDU)fPbYXkjld#s`I@5S?kzpGYt zY^CJb-cCUJwgTdPeY%QNK@xAagV2g%fb%Wb>U`i>*PEOt*b?NTyV!`o?CG<+1J`~q z3_ORrgE(h|Gmxfx6JaKBZ~F1Np?fuDfMl`L4jS*qWIjo!JPmPErDfmPf!ptuQ%weC zfdy={#{qZlWHH1pv!sD^=Dk$&_`@A$gSNBxmoXK~xHaZD0~I;(Lci1p $o~S#||uf-$>Zk z;gPoP#KUyGmO-;@Y}&iEku5HJj*sm5WJj6Jj8D6=wfkY7NL8uh@TR8x>`+8ZBSk-c zq=>%$!_6ez8N5eoKZ_DcUDA~ZL&E-5QB{?t1d0Q(6;#pLDDA+J;?zliHO<`Yl7)S} zz?IhQ@q3z&EOF%Mh>A%13pL2G$e5KbSHB?%! K@esL-1IDZ}hW)CcvuwiN2#pxk`bmvc88Relneb zHr+jMpRM>P(Jrv)FUur3@~rDh4ek-%86lM%c(Jv&uP7_SF<9FN4L_~kFT3x}1dKQQ z{=t9Rwx>7WiNBr%Br`&Z0`G~;T9Dt~KO0s4>wy#y?C6&^a8I#8=9lYl;lCKf@;*+I zecm& >^JVe!A0o2 zyu6|xHJ|BvKTufrUQ04KHkV~7jxc5hkB$Dw8@OXdc@y(mrNplD)lLzad}T0r?8=-< za5`u|%9Atblys#`aM^=+T9eh{3ci|KTN>Ow=ynKnV?;8*o6mP6V5Q4#Tjx_vWy3!B z41t3I>hA~B)b1tAbI;9f_U-RuuR5e39v%{~u-0(jcoVO;AZP~BoU%U)U{``7cSn!u z0RGqfIXdlptvxT%%6ppsXoUw0D@EvOJJj&?^(blLkbsP|&&>#vnR#%Rz0`3R$}0EA zSpj40*K&-e=2n+SqrjagS<=0~?koWE#+gMTd`$SwYNE*gbYUpP2*zth{pOqZC8sTf zgA(%R$LC4J*;M(vdshFRj;9i_u4o;rFZBsGH|!mq9kwdgazpdp`6Df|q=#TQG6@AF zsV}qsQP|)%gAJcBbZBw0Du?N{cr2>4QSzqY^ki~PlQ}{X=%4!SA>b<4ckksq_>p0- z(qc~VE1`(A=yf%&?nJ{Rns!O|Am;G(k?;9xXNm+p^j4GC7y0Y7_Fl~LcU-X|!!KrT zAwXbX_iKs%DGXw-+F LJ6ricetJ_i*+3}0C(k|*v& z+BV#Am3mJeiUeQIms%*iR^m@M`uRp<0+tFgpTnE!pVk{EfVlwcz6QB-;l_Yn_dxmX zJ1~5;p|?03@YmzgyRL8}Q3MT%9Z|Xkto3$Uc-iTPpb&-Nz`&1QNSD6wnSlm}CILR` ze_!BxLmdm<*0KV$b-exkD*| A6yF14@- z{8Tra>Dd6%lyeKe 0Ly1z(^k zAn9vB6fh+DB}jZ|mVuSM{STaMgl^jxNU(QTQ9{pKjOcZ-#@0aANI_V#h;dbjNwrv- zw^UA8F%-0j@oMO7A9?dn5D|{2LFnwZZ#3|pGpoWjc8$hbVm-VZdAUjLZ$=3W*5)RX z!iLq(k-YEhBxojA)71i%t}0UIXPT~6+d dx0-TyTrS*h3LXU0B`y;2sSGEzPz>Tf}xCQPqh}+0%gJBE4;+Y z{^TKoCcYv%tTs7>9^NqibI<@8d&*cTz-(B*fJwUCq5!11ao=69IJN_D dPvR4RC-PvX5-O3oBXCR(YqLK5IlW2>`o$ZKAoE9RMF-$c)I~lM+#66jWd4 zj!7l|X4Ix=MyAaI@_80t_->WtO8&u$)F=~lboj1xdwDf0w@XAtIfN^kyi1hws ZY9cKBN-j}as2*9nbd>%WGFHd*BQzg*gDt^)HZ_6Qx^kx!w zcW2Bh2a$feHr{;yw_JZy=LG?%YVW)(UB_m_v?It_@lJn&(ZVi2RaY^7U&2_EdJ4C< zv+~%Qq#M#p3YX9xaT91QLS3$MRQ$=?d+(@2&nyW-W#8k5(pwm`3T2fTeO%(3Y(6V5 zs5%@820`=1D~seDtZzLr7?-L0FXe2#tBBl%*SP|!j?45j-Yq~TM@UsiDSK@USvWy$ zF{UV#{SfkK{ T-9b-|-O%Zt5SZvAf7CF j<`(gL=3i zvh;5Fj^hrNhS&+gqNp{7H$wR=P54FILM&$ZUd-Hiox4mkJbm$mqIEv`@8jdbdgpiG zANdWBwM3T*>^x%(&|Q8~X_i%1u`P}{J?|m_HA!3GhZr!luLZ1=*}{g`&Bm2qTqIcl zaMJ>l2uc}gkH#l7+{BCCov6OjF29b*;uCW9`wS$>-ia1`V6GZYZ}aAdI0qa?=LGcq z{Zil~(tFvn3Jn2IY>-VQCX6&4i=ggx2>+NEajhG?+J5Q|r$V;)NwsY?n!uZ4N=2W{ z)N`MG (hXbp(131BtBejiH~9b?w}fb*@cN6% zZUkI!foYqCZPenp<3rl{c+QT5opMa8GYW1EG->9pl8TOB`fA3`i>weFty834rG^kJ z7omf0xK5F$l4NTK!bm8BB57EKPYmhVc}GvB1~bKwR~{gLZ%^jli018mb@+O*I<&2c zD<34`a^dW3E@ZMpYvRotr4;Y*6`3NJz&noM$eBN*4bvN^;4)m;gVp%gubu$?H5gX^ zP1D!g7THx1w(xq)m2`<{x@;Aqf1FA=N(C^WfX!qrLh$O$X*sDlIt{X|RQ3|yNP}AN zh;zWbJIl8|#bdp$SkCF5pfbhuo7uycldawYzn2m$ic+IVO8r_BO4D#ftm8nEa7fE@0o2pevo z#(bK4tgF!E3OH~5Sn$>JeIf*bWHAWUm2G?On(OYO0zbUW;-&311jnQp{<@!5 8Rq&^6*B?yCb%q#<$d;XBJ>kF$F=11c44j{A zjXdqco2O94s>E_+<&m3=mxNPZ4j(Pb6ti>0Y&})~CN^zvag0|m0FX^7N@ZVJUSXsT zy}+lPpL(tN@$o8Q^_kh@pXv6e3>#2x;usw-J0Al8N2+Zvo+e2NL$%E3Mv>Kg>=p+Z z4ULnJO|0Fp)R0MAFR%E$W4F2Pz}@5>1eW65(=d;dx!_L>UHI5&Bee25`hBFDt5G8I zOW6H)k?|+=N1^L_)Fu>4HrK H&o}9yUkeO?%oo7N}<6ICvc3wg~^@dS5 zlR;0m<%G8T%&=hlL4xQJ|B=xz_h}y%tX0>pXxVm6)55v}SyF#|)@u|@G)a~AMrT?G zU|Ds0ya^5fuzr$>DQq|yS76pVUv`yoC$jxsY0V+H$&rayrjR>eh$>@-Lx5jq=m*U% zz|{fyoRawn!sW5Zy2EN-BPmxiWP+LN{dd>*y)#iNliJ1a%(J=m0VJ))UML9mnP&9P zYOxEbwC)9p_JXy+_=@vYT%9c!yDbJEVbZzY9O1F;*bI;*8aUD6m 0B0bKFT8py1%isB;$7qh>vNtz}e(E zHOK==`$tbVdKsM5NZuc{Rl<9nZK39$5;Bb zQ!)X882Y5#su=o)s#_w;5S8ff1>g1Hrcs#=`EZHVg^Rx;W?DB{G*^mYsFvz>f6c>y z=r6CLk6W@z3f^uJ-ISqB2l_0~G1Mptxv^3pS4w^?;QI&&bhzfW$+LG?!UZLP*&onQ z*@XBeN3Yqa20raKXuwbKu)k1jS7@@QzerJvU-e6aP=(ZV;hAhdqcR}J# g2*jTuio8Z-T$7o+Ae52A7ZB8+ z+|YX8MT(A~i9DiOl;N9U 0$MxsG+{?2|t3w#FBvZE5`D6051>@t~_WTcnRNnVztmYQp(Mfaim}6W$IzYDu z*I%gLGhj)1d(KCNXM>S-UFUCS>vi*Td-gT%zW9u;{`cjy2yqS&?j(@KHLZsjm8ko{ zKZ`vHgO;)_7^Y^o$phOj9w7{GPM1?`r?w;MyPe*roC(MX*=_T!aX6yY+xy4U?D;O5 zIBo1N#1Q+gM hLHhQICT567JfFm8l#SZt$m_5k1pPXBo}>$-97_?w(;Q+r`HJ z^XR3?vk7eV8}KOC6X&HP$0Tv^cd wu7)q1rnbvu_LKy(73{Y3-(_;X-i6KoPQ)Du+K^Lg+L!Of@Q#bSTORX_z zsekU9qE2>=7k$SE62jAf{HwrX0$7J!KnxJ`hOf*ms>UFHKVEcmc#3y(^zh}9)`~{A z+l}VLB3F}An!do=!J%|XRZDXHd{3x%$gr_YxgyX0>7D$nJix); NS>hnPGnewZt(Se}oZSH2T8NQosQmY@nIozgS#w zIma(4p%S{@_CH_mg`25E_u6Z+n2%Lf`OJW8V{a=7N9i%N*e*Gs!UZcB =f%|onRO-z^P$8wFuMTF<}ie`C3>Nsh?x~@b+uX8TkRp9p}k8nH_7oG&i-r5}> zbvcK1+wWp=Vg8i) Gw0<-iOx`Uz<~lffZ8lYY--qex4=l@^j>J{WLj43WE7s1E&Yg@N<#QjYUB4683f z!RwX1jh$0*jj(v(0&-lexnR_3(nQlv)#UZ?DydI-;XC~GdFrW XW@HxdF?qCdBAg?hEdpMYeKc8&4CIJYd=f5HmXu&4W#Se$hItIgSI=})2L z@dB&a`KKOPMR&%Eyl=2sn$b1u`TI+vA4zWQN 3u$?Dx$!&SR)H7XW=(OUJ9+&hva_^>*{^PJaFB1+ z0hJk=|3KrWD{-l#=U`5eJ4-3~$JRUM`7ZuMyvSPVtTtr+t*d!Reshei;~2zerAQ2j zA397DTH~e;;^qZsE{_74hVU3Sredj5a5SY$Us@a7m)RzKv%!|(>V@^qR`+Yt@Do-e zDS{`)vjTfQRo(!1_Zfv4GK?RS8Q9x_y#Y>?&H8U>nnGFs^)Xw!VLVVa1d57@Htf!p z;)uVH3wVn$0oQj%YN(22^}e2{o^Q0`Ew^9 4fDF{Sv{HTPIr7Sss?ac>%mS>+*dF5jql{0qQiS+_#X_nVw-0K z-+xw6Bpg!VSz{sg9PRM&)cITxL98^~%bT%F{-G %bpEw#Rd*>%-70%w7V-oRIe(8Q-KF1qbXWlryp0=E;fZYq?B};I? zK$1tDQD{nR%D9CbuO)g-%+r0{`*;Dbu-RJZ9I5fQ_Y>moo=`LJtXSr^dJbXpx+PLd zwss^Vow>!|<6#;5%w*57_S1n_I)Mi|qf6RMG&gC(jQhGBKl56`NzREfv!*+%s>Yrk ztg{rw(ScHt^qBLjtARTP zPiuFo9>e9K2Bv_A#)6{M-BGH9!N-10@8fE0Y+Y!}0=J&9YVQ@EzbvOa!OD-f4<9q@ zvygl((i$N7Z5_g{Pap7x1kF5Tuj|>c=));FT@VW^(v=fw`3G7xi3ge?^_^i*^x7gN~N$*<(bpIc!2~AI3|;pkfI$^ zr|ydcx~iylFu^W2Jd}R+|9-cUlG5zlrV~pR4HG{`0v0r6ur@hNyF|R<_ajx%0`Oy@ zN&Y>+M8H%YryY}#e&G=0R?Vp$YRE@r5!&N3e(BfH>!lkjb$dE{hZ5}Z!Ae618IB(L z*i487e7!3my6kx^ZhU^#sw?8)mq8C|Ar$WrK-98mi88 zKHQn#O7 jh6)xa)py0Mz$4EtpCp?_z zcuD~IUIPP4RmWu#=OeHl6Q*l*SIx~^I4^p-^4IwlHzW_Y9gsr=@^2RU`t7HpqGO$# zZ(jj3HHy|Ol@TYs#&|0EU}ClT9zi``OL!DI*i2j%ZA>#;@Z%eAxLL3!h`qvS`q5ut zZ9R*);Mbi$@kn0PFun97_m={4cyx6(5q8L+RGye`cm#GgZ23lr@;!zYN1H~PKtQLb zvpWri5zlpTT)L^%lFmmYyepCn+CrWETgUpbt>WBoO!K#5>KrfSn`*RxwC)$U%pV@S zysi)OvNwkr`BUv9b){-7i8Y$v^_nMRJjb^oWf%nZ_25YozcS}eJCXYPWd`<~F8ulW z_z6X)z<(E39n8_;a62_b5))O0DEfL|;NvjyaP4TdXXAXvwhij#p|!xSz&IpIrO2N4 z{Igb$wMc>kAB(OPln!sYu(}y{HoVg4h1ag01nkwI4J+NCvuoxX?_A%@)zKJ}znHr> z(E7nG9&)DSqer)G@lzwBA2bg@ZPlSczywhCrwGMbNWU@oVdP7~8n6@N!i=}hv0t`} zPX8;vhQlNz|3uc+Kn`nwNz2hujsp3^KzOXUaz@sek^B2mQ(i9%)|lZNr;#rNy1LnV zawOTe#4<(Pl-TASpXjJ443>`l=hs_2(Zh72z$weRZ`{vd1bq$WT(?s_2mdEmIUA2- zQcQ`txjE?~O;dn&9W+Cgh{;;j{vm_6r|;nC=(rz-R&p1HEw?VQ{l&KbOJ ZlDxhNtYi0Gewq9<3_~oK%(AcMF E3ZN*Azg{H! w>=DAMfm4 *`!v&6N<{z71}E(JVACg*9+C-O a$Y3h2%;4{SJF}|@d#p>>~kAF`yeBU+@ z-=Dfw#eC3>=0NY{(HV0x7Y~Y%?N#!rvqi>?_2}*@KeA+2*^s3bIa =*f=j>%S0kTVd>kgf@oIpYCpJ#qIDuqL-9nfyw|X71N`pz zw8xnUs)(Bwp?g$?vcBA(dY)fx$rk!CO(5cRocPY%N!WOXD-4aaFn=mQrOCR2;o{{@ zAyiFf+`kT0$i(f@cBW^BG67poq!i+)6NaP#lIS%5i-Xh0L;(Q-+4hm>8v>s3D`g@n zZGHgk)!Qgs`)MdyVdjMENS>G5ANmpVaEzbqBV~pUM7tzbIcIR%_XT>)ezmi^!!Dv4 z=XNV}oUX%-t`O5&%WGL%*)NXwtU&wC<%XhxJ!6!`e2^~_Qt$4~^ fKhR-ngaHCB3$qOG#2FD=w%7c>K rhB!RkGliwTB^w zI#VLG(Q^ot7qf{#>6HXvCy7`mnqo> $TsrjSDbg6*Oa9ibEtV(<% zkgnjfzi78;Oo4^{>F2@SKW{^yf5geg_A6i5SE4gP>k$+)J?#XDpYrJ5y4qcR*R~uu z@~|&22&r)W(6}hbsP*2itYpulk!_bBwFeJzIV(M$jP__IQ@^g!Y4dzQc-`01hJhC! zH^*42>U3$YByhdDfLR@)smO@4#pxg)s3DQqj`!m5wf);JHilgYrcuWGiyj-CQY%9! zWH&Mj3fOvvG}}Nw_AhqCn nJ2xiPuDhJVN zeJ{hq1rSf-W;1r@z%hbU*j7M+^S+d}cJD`sT$oS&rEbY!9N%E_>yFX8C-MlcoMf$S zYqtvD@j`h!Z7y9h?Vs)nzJhEVb_9;zlDPa4K1#7 5YloCV@lGi9SpBlTA5nE0A1+Xce#xn?+Z>Z6vy6$3DhCg8L=fd+EcZG}6a< z6LQBhOXz-sC<296#t2`8u6}bQ_&ylF(J9Z{T3AM13A$zr{#;80G?b5(8VKG