From 9fa6dd338b4efd386db0ed87738a5e16e8192cd3 Mon Sep 17 00:00:00 2001 From: rcholic Date: Wed, 31 Dec 2025 17:12:39 -0800 Subject: [PATCH 1/6] share utility for loading textensions --- screenshot.png | Bin 103021 -> 103162 bytes sentience/browser.py | 25 +++---------------------- 2 files changed, 3 insertions(+), 22 deletions(-) diff --git a/screenshot.png b/screenshot.png index e96bc424ecfa7b19d08d8fbef9f649e92d9e3707..847eb7308de071eb295db2abb31ac306f62acbdd 100644 GIT binary patch delta 55925 zcmZ_0WmHvN+XgBjAl=;|UD7QnA>Exyw@9-*Zx(CKxbLf`ySWHgc?jkB)zG}u(euL+?`)N{&t2)%U2}67?zb!G?vpT~LZOY| zMZk(Kt?*SPYwxzz!sLl{eEz$WD?j5FpVX;`SKnTfXJzG3W|>j5UM9S02Qd-R_X5Qv zrX=BOjWmDB$Xh{v{!PDV0#T>q^}zz=%;@Il!_=9vx3OWd!|@{wHMLN!a)UDLN>kQN z_A%A|c)M}47agOei`P*_ zO*AyBe2JOmjV1g9x?#w=S~Yk5#F40iFpp;9krGU{;NHY}z{IPxE)Hi)R=yteI@yfN z$;m+q=&^Dr-A|K|IO&E+FN?((lRAm*mpjAH_ZMC~U5bu~wVfypFR-Lbaz~<5bu!|$ zpN8j*apXj)A}zgA=UZm^q9noVbz+69Dm>;`JXtVqTgWBx@LMI2!sV!&5E%X1uJOun zZKkWNL($1hXh`{;OcvV$MjT}lo+{3_`05wyaDI`pz`G1$)Pow{SZ}q$R^4{LCm~?b zp`^ohI;ynjd**p$5e}~Ra8W;czj!#(gqI(md3t(&SM)ttgwdeGeSIVI?A|IZJ>A#W zcPLYAe`Q5ITOyc(ujG)O_C++Qjj}U+kc&L+fA@7cc#-sJbauAsGxpYb_s^e7r*o|j z_e+HXzQeX6n@S98#XlPR@AHATu@Qd#`t?bh zkTC4AJ=Vhyg`cB5{1#d=e)v!S-LmYr=f*HU4u@6?mR473EBdkGaWU_;9zGSDD?v(X z!uzII`MKt~j80h+5>+d+k9_9C56YpV2R!|IS3#KZELo491|>lKxCwicsQ>$dI1>F( z#l^+xl`|5TrJbA(s%Rey1eCAQ!HCvxOZkdnMv=_GfCPCnnDW zYr^dC{QTY*{YLxnW-GLOSoXicjqeO^eU!mz9#T?L5)cp&7$}(`?CG-pL%`#}1WCGB zuky9x)yYs^D9@p3a^ zJMDL;tI-k@nKYRU8{W9>{085*z|uEr^_#1*9yO?U*_~_Djv#PmO_ob|I+!VDEMF@8 z=5%|+On(L;6&6;4?qQrTC1kPb?_fP9%T3`B(L3AY=@hc27NMzzhK6KjZHI*x-*iDY zVh$63Tw}dzn`&@ZHPfWlnMnos|4@ty+l)E2Ez}_8A<_-RhQ9b`l>D9&IN(dTB zF2y4+oN{9pTy`VUJmjGo{aOdGu?n?H^tz*nCd!Q2>HPWe6iiGOm%Ac2etg8i$A3zv zaD?}ArtTF4!$L@O`=tW=-z>#u%Dnq@YCV!0hdbSdwD48EfLzpvA3unNn~Q5yjEqO} znP#EJdlEigUf$niXi_pZWFL@;bSuoHMQChS&URjb zdDqs|B)8zR`0qq!1gG*jih3MSDm^B_!^^{muq$=oiA9!J^=mAKGUf&bP;RlNdE@vr zt{KnJQ4Fs*rKD|oojg1&ihsp^|H9CW>eSiUGb!bakBWvfmhf*}kuJ$(Um7!0Qa)PH z($Xpnn5lIP^(r=QZOI+$evFJOo5GeJD~Bly-CbJ7QA>+rnUR2jdJ z-33#|VIH@*HChlSNlF0u=pn6u-7ZO@{Z;L;AOBG-m!%RZ?q~5K$?=s<+C2F*5pP~t zdwY9lgS)#sHM=mh-Q8VHI8YD5+i`!Nd{NCQpRKXiujrqcsC_k%96PAer1PBg!(?HX z%lY1%sPC1@?ovl+dRm%Fjm`YCXW<>8hIZ!WT(-Yi#x*U6GJ>JLn89Cb0chGKdcn;r zv<~x4L}C>vtN3{$7QL9b3L8JuJ32Z{pRODn;_!$Iw+TDU@+b1LILL`fNKn7Eci#PN zbNz{KBJ_UWWC|8#hG+WbW(%?6?d~^{(Von`2~Xu1ULl_*IJ&%(biUJ@AK{i)Z>L2H z@2Rlt_iL^bWHJdMu2rj@E0Wt=+?-E=fgKf8e!Rs^UdP+|)Or>cW382JXnXZ##v5~~ zEeP2EW;g0*n>DB2@mtGz0Pw^;!;lH@?TkURPl2~D%9;o*bcI2tr! zZU%$8g9`JW*FV#~HatlrZ=u>{Gj2gGK@mZ9tO2*w%Zi(olS9+V!Q#wE7Z!CR8N3J0 zHM&oIdqpkAZq(FPqZ1taE9Zs6bbyXDX`CO}yqemIA2?uwpY*+dC2P=Y?&#=PVKd+4 zNkv7qDdqoGngTxh7byMJI0X1uHwmvqWOz@4$sbhsbXdqe1RBxOGOO2YD z%0R;%pRTq|VbO&xn4X#<5}K}o>`8D>`x59$cx-|a<337;qG!**HpAKeF8)p=Rg2_> z?}RJhvg*GrLyftSr;s9}9Gc6QO@&qJJ}-&DXP41x9)mqZe$DdXt$5rx7dydF%WQ>Z zA05dj-y^h;eocfHkIii!AD<+m$Q_gt6jR|>f4(lQFezuK&rsRQn-bjfK$~F+l^s_i zB|AKQe2&w>>Mt^>oR*>@l_|W2^kz+#K0Qs=dir0dS``zgMp}e{LulJ5Hfr{IPs~HP zokciKo%`~m+R8IjQ!8Gi%9E|pNXGo_@uCzF@3%T-)bHZrQ~^$)&1()1CKvH)dmr># zviJIYpDKhZJG| z>hwKQe(^Pmsl2g!n3KyJ86w_Ua9kF>lp_Y^CNOUs+;+4&19~yg(L$*5bwehLh7FR0qB9vxCz}ep-jTyppt`+p z54}oCbd?%lWHG2K2R$HuWDj3#O*1SCyU3vaXtKimm(ZhNmhMnVw+nHZKR zl_tJjck;D|)IT(e97|VWh>F)bHZ?l0l>`UN?I?9&E}!dC1-jccpnP0t(Rb$JT0wLd zVbAa++NAKZSTHv??}C4872McQ%Tz{K-^a!@ zq$D$zg24!2AYpLDj!h9qrC9c7^U`yAI6}AtSM{scPcgK{Wx{NSHXGO(UVPeC=Vz`$5 z`6H(v=3)*u?{9@DqR2faZ+9&TyO5wV9oEpiB9zJ%w5ODCEA!q*?gI z`Z@xSnVPH8a;Tf=g*)UgJfMB!KNabyppcN&R?fy+`+YI}S*t^-urYZ;NdBDH7gpT; z7BUR4swpheVurIzW)ePI_02uS7_n{pAXU;uiV%B+#6E+3Gp)k_)Vsx;FAAbY3*!vL zZT}F>4Wex9qiBJ?a$X4u2{Qx<8u3pOP7?O*$oMfK+kIEi=?+!XP?`Xj^$4w<_sxYf zS={~Yxp7@5=*gj*dgUeo(_9^3Fb|K8TwGmo#6W!r-*56fmaoY6y1-lokWH`7i7HB+ zbDRHZFDvO$nt-dkd@y6^P%57?Dt0sl@q#M+*C*C!ho~v1?ZGHdpE`TQ@^EwG_Iq}Q zVV$&I^E?t6V>zv^h9thXfiFE+%V-rd-uM=4*1PW1x{4+esoUY@~u@>=aPF~*L zD3t_4bCs4c+1XU^cz*43ngjkZF&HgkzE?r}&EDq{3wrwc7^M7|oa5ctkwj3&8lp25 zf$7rgzWB!j`}57-hs(ja;Xgid83T}ZdwmX88x|F4$6fAo4Q}IOPooVJ@yzZnH!@@U zV-rD}3TPL0-2_H4@> zDQcE^!`M*te7ifT78J@dBcBr=3a7K6)a-2W)Sq2gVwyH6Q0G*ux-*-6$^fh-zow1C z2C^iTnlTK(0b%IQ$0|d=7I*)FE<`mUpRpujNMXU#7R&aQgRupe z10pP}c3$a5s;a6g*7%B@_8gx2*&h|1=!4f-m%B5y02_VhOPl;G9jTPYf4Dc-s1idb zpQuw~Hw7?5wk*~jU}@(IkmPGHb0pEW04{1Chj3f|ObbV)Ob5IDxi?MGus0$GdC)9g zVA?BEQqsl#Xks2@YD}zyX0tA2E{Kb3GJ}NIt__B|L&dgm)E&mceCKybC=vKPKY#u>mCM4o@PXCC0O@#pd!H~;KKxSs z#<#4|j~}DN%~n~fCng!NLscmj*d6+S} zME@E@|Fusc!3@x-P+<}5O-D^}^X+$`-nH$SL0gBlpQ*1GH<4vzLf(F5Q6ls8Gns&6 zNi0d+oeZEyh_G~5)V}j(<)HlY=Wv6rpMT8p)52&6beP7x=P0_+y7Gi)#@wH4%n_>~ zYVz!)V#6t_!oCPy`n@7*y1Zq`-dx@G@3Z&kYBTyF!P5EAkX1sfiXhG?{QIe=t70Lh zI;w|`nvKSPY5z?PVdn{9Yw^X^SWejD^71mE{#^tgYB7t3V>xJpCndjO8pi4za8w{~ zp8!NW>;`D{Z+dZ^Pckwx96_g^@>&;}!;Xce`NSL&RJ}_WJ7GBpXXw?81Yfi+9hMrt1eObNU!g#UN~KD+P1)IdM1XM_jAV_iw0#uhy=wsP8ZA(M(f54$ zvz=)Z;IziQ`x;CZG6WyI`Ci~i_l3n=KR&YJAW?J@xAjOE^jNkUmr<>4 zkQ^}o?H@XyS;H`=aI~ZcRb%*EvY(uyn4~TM^z@iH)S&S#nrPDrPMl>>_Y%q}3Ph+z zx^NI%{ndCb>?p{nTERZMroVGFmCrUGw^rh$up76G=Uatl<~Y$ZZ-QjRhen*4!mnok zDG?)U^iLK)08tAn+xTkK+)!Q31FsJN^;5-9v~)aRzi51xdcv}(lmGG~btrfN5f2Xq z@Wnx}hHsv_MpFR57H|t>uL`w1`S0PrfLH*EtdPRSdaHJKu@VCs%1Uk|=dttS<%!cX zZqi!GMMoxH{C-)U@~#2m)<=eNSw5E#*6<;D4S0${P6Y-PrsN zoIbzz*&nBRF~6Izj`=D6+`uH$ue2Hb&X4~R3S?f3L(}vbAajgGfDW=#Kk%`2KR1a5MQ0xLSswmvc2N8=Fu_q2Y z#e@)sD#efx2GzDx{83|FoovvJR1}<`{J&<2885pjgu)G^oX=SE><$W6jAkGwn1J08F=tu+=R;s-RcBOI0SW~|?~bP~P$$eAPb;G(ke zfs$D%-=$z=AXGXD+P;Yi?Ndt&i&el!0ni|N5JMOtlW^5?~c< zjz_8GueaU{u4(aa7!H>oZKE+eMzrmuEL7Q65e0}$|M9pxfsi23xt@xR0AG=3!cOnk#+X}|S;Z?qsrD*mbK`VVNT#(t()x3U~{S1;>% z0I2^!R`mwU3e@FB0QI{f34r!>w>3&HwK?{6)E@@U?5Wyksc^vQOukP}@`Gjvbip$1 zvIhc`yAYxzqENN1f{U`-i03#_U5CV?+ikjc6q2l!ukf31;3Kn}KQx&|eob7>!U1Wn zpm{_3F_}g<&JS9}zxnKi-oc{EY2RH|}98Tw3obc%A=pFKV z^Vw!^0;M}p{HP2LE5Z6VGBP?iUUNlmb-%m0bZ~G0U6z0f&_0`RDP~R{p001%*`H;; z^Er$c;uV{3ZD5m?*xDCr;9m$TSzBfke`xgo`Gz>VdGmoMl~rl(ZFd?iTNDoO<<_H z0NU35wi=E@zbJ_gbn8THJAEJy0u7J)4)pUlz30!{z+X%>fC#CiPd&K`b{2r2^*)#1 znwv#Xhd%M({hU8q8(=qVz{SN~-QGq{;Cj`csCHz2aj?YUHaIv~adWr=^>l<{5U?2# zTE5_kuB>zg$``27G1%(rtyoxC(CsOK#v3cEF8~NiprWF77Dkf^NpStsbw|{eollo{ z-ZMKk%pbBCu-pz;Bym}Mo2op$`vn(MH1_6UnNNPFuRop|(i>^(pVki%5_4$H>uItWfoTbB%kL~Y)IY>%?%XJ0*b>KfW z`v~VkHNiwYJzC3UZ&m<^DpvGzTZEFMrMi^}Ayg8WY+_!5j`K~iv9TxwY(GBnpx9gj zc=h(}TOAZhHDN_$0kq2qMcOslxqQXc-(OS#c7RHNEF8@HJ1#6UlLD7r0;r})mft`< zVmEF%IXKw-m4jHUl>1PtHu)GYu?Pu0PBv9fu);EjE8<6*-V<>FLGQxv?9WF%{*z;X zyMS*29j(fdkdTaP0v=cIx*;PLoaj+E@7elhFKH`syKH{`=umRs)E8Y!S|(K=B3x+R zs_^+fKC)JMLSnRc_TD}8Of-J^ap%FoVN!*Y$9~T5Q@;7Squ-sKNEFppfV6v^|90&9 zjgMnFK3zHJcKBxiAzqyN|3)bALKT)@A(;i}vOyN_qoQ6WG2!``z_+9RNabS>+5zs1 zEi=}VNnbo2*a$K*kj!|YWe&Q_(EPg1PIO1?@;oV(bdDB_3CW z1h__WKaHQbzRSJpk~D#xg9s1mJrdRGi-Vky=U|n406&0PyEKSzbAA135ero^JcLy^G54Q4Y?ECTvaBMF&)E-V3ARDpSR4ez%Sn;>Qlw zpIG3ri4&`Crc3rlV+q{X_zJP2q2k4pkQXn{JpZQf3X2Gfkw{j-j?uYJg6{z?-E$aF_=sd_=Q9&%4K8ObvXBx*p)7RJY`p9jFIJ(E0<53_X7D_coh znv#*04zDkU2Wk+KRHy_X8X9~AYM0=W3^oEP<1pw?o`ipmSV2oZPR4buvQu_SX1o zY;4%+fZ@+{uWTlNX*G0RbzJ9uhW~*C+;A~?V)6$ywdXy_`EnNugBk<%=8#Tpp-u%g zHMYRddx_-y!#2jT zwxFf(SBA)4tO5~whS+5}v{B1BlT^s;1s}AW)E6sf`eA?OkahaEo4Zl-#s41J&l{W8 zA9!G_jPFGcxda8TKw|;oMxk0B@|*nD*84b`7g_mPV%fpuVp>*s#>NX-S(I1{Gc)?a z?o5pIXwc7CDmYqQCnt{1LSb&8Eqek&$mcLS38WIxP$)!w!V=5C?${d1ONgfcyftWNXK9Hl6qSS*)z9Pp z?)H}_m4wP7=r`=T-)ObMF~!8he4v~vup!fcuM3DmeO+Cau(Kd$^eC$!%kWNsRDTLP zJ01CZeY`i&v8U8RAYWPi{`_`>)$cqTH_|{@Jai|W51$k0n3%3|*p>W(x(9bhb@13S zmKOIyYaM+&#SalPvjCtGOc|o~d;LcCaKBgqW6vuhbAp`_!7rsO&F}{5EEFYsYnpAa zW+%J4@R7jL(A~J=fOyRD_n)^{mj7!zMPc`tn_~Aju0SEvu^P#J!c9N+6u!iRwD!%` zNV#ct*!J%dA*aPQP#3-bOjdyA6IAE**<$b$59k44bx7V=kLJH8jWQ& zsvge6G`EMQs{$VF0OKnXpH&pdeZb-bz92Q@haaHmq;gwhQv+cf(M;c*I!`|79nE_? z6K2p`kX{3N!s$coTpkJD27ZygeDX_Rm-Z(z@(Y=5J|lH|V;Tk6`Yk$;LxaOaywB`n z$VHq=gVS38Bm+*Z*%EYry#VG5LMImvt273r0oM*F=D>G}a)=%tR#8`1w+l-EwLqWo z(CSys!RQOo&~woSk9E4)MQDATlbc(I2>5-PWk+lYad z_^q~hn#h+P&<5Al><82LzaIjm&%zb|B?z@1R*|K2-SF zPc1F)I({%58+LHOU`sNp-0~f0lomfMY|XA9`=2#oVPAL@91__!D9N9k9U^n{a=x@q zA>F{Bmogz8R=v?5-y`_@@U5(3HuzR@0(g~8#n&Q|IP^+Q(7kAsu5M)eG@kMo`F>0F zu6KoM(%uSRj0M1Rsit@8yp&;`k4Q`lFam}*&K1%D>+68z4oztLsr_`-$oal9L@eUP zEiX#GtxaKPH+T?-?H?Yl`kRzksrh4l#m8@@;v_3MHNV%k^%pS2A4mFq#2gSUNs{`< z1?ALkzMMbm2?yM>$@jWOr@~Cd>_JA)d$dvk_Jx>OoCQ(?DlcFpCf{Khas2`;$yJpF=O!afAjhigA|sGUexc#~|Ml0aAh`GscYH3NZ(+ELI)c-DlDRGx@!v_1|A5;WbJ3B>{5Qf(N|Bn$~6F)>U zq-dJ{Y9_Q_+o*>BccJ*lSI8#O&{HuqdV2bB&FI)%|BKqcHYP3J^5Ub)Y)L=Z!Un!} zuM#PwOT#7gtA`(yb8$lAPJpIZ6SAgI!512oeX?@_S}EW^Z+Z?74m{5H4Bx!rP6oOF z`z?80LqkYVknHLN79P>gX@Jemi|T zWK5sexiZfhgRWTn#-ZhOcV}|QMaOEjINY*Gkk##HW0vlFH~ru9$=G%;@4@9hnvk=gTi}h=j>|JSSazG2SO^}@-gBBLVSXfX|KtYqN z0vfgBY#k@iCx~&tavTbPLmbIhj9dc-rH-Cn#KD78zx5wAA+n$j}j6bGY1qP}Vr zsFJgs6RAOGG11asWPZDy%jGDelIZtMcWdzYT!#ovur)h-uQSv6dnq1OZ>vbTr;*>L z|5_<`n^XqW_!SNzl0NObe`vx}D^XB4E=L4T(1}W0U*}+l=XbXvEl2MIX zC@}f?$c*pU9{KXdd_)T8Q>M85k?zt0rf7MzGm?^$(qxLTgb+2g*=lc`Qxjm{(PNlq zCMJ~Vp1?T)pqX7QR~8r;6v7^qj4zC1h@B=f)-1>i!`Z3ZM6ooA{^M@7`~@dfYr#gx)2)(thjOlqsdb17=S9XtV@<)I zxl4CrWKz?#$p&pI}BFACnD+Mb3yE?tr;cc*h69EM@@f#=p4)&kIyN4wKo z+~#}_ukv@^-ovENHr&Z5V+89Kvd*sc&om3|(fG{w%V@JTi9*}bF9{}2e_rRgmc2CA zfKJbndppWS5zA@FE~_fJTHkIBexJio^^{RAzWT-IvDP|4+m*Ag>MIESVblF>dUNuL z%!|Loc+cgos#I=~(jb!ml?DnbDpI4z`m3)l=!*9YL7CU?qOsb#x~UlgccfIF$ho^> z5}ZNTLjj>35;WDpMR`r0gC*{cw_0}goX%P$A~OacyTMY{pDs4I=32JO*z;d+{!IG@ z-%8fNrvpAZB0>)W$`SBM4qbt&WOXI!TH`QBEJm;RDUs7$rnmw4gzG;(QaNXeL2SIC zM?rA2CHl1wa+0hh7s~nM{JfDHI|hYlkP#~Y?(x_Tm3M9HUFX0tjqLzpQHK%m8o6_o z!?cb5$I$tugig*txmsjZkgv}G2^`3yUdO1$P8ZnIowJ4+85zCUypB`f^5WvmniyQD z8N0;N<^fdw&JFTuAy>iDQ3s%gQDZH613MOj{296mbvVo_QHy#mcZwu6bxxL&5Z^+? zP%-oqk&H2;kT5jpuDX4N2mA#beYiz$IbB z_d~tax}}>H<&3OwxWOHAXc@&X{bXk)+Zho&@q<;-#9y+4X0%RbRqTbTTUcgC!ZRoSR@{Eh$Gfi ze?M|)h+{Y{xhG2Q_u3fkEK)O5(~-~irai(Dd-2gC3QaERQRx1Oc(zOW%h$hr%Qlon ztx$Vvp^|g~_!2@{R62vYx{ztNovX%|=rxn1nMQB}K(pY;XVa8)i{-`j&UN{6+&n`L zvJyDT>^VcZ-mN1aNvopK{pW~r2@#I9cvPa)-g+OI{`#Y!uwf{!2Hc%Yq0995EMmWR zGz7U?9F1Hj$k?2qozS^ggAVlQI6eGIYkf8lih?wKTxetKKjRVXQ78~+60J8QgPPWvbo_4KD6Of~hb}&-o)M4Re8le6)!M38=;G)y3E)Q+lXz|IoXa8>kBLC z09xvr*th*(pgi0D9)ioPIbVEcBP7ku(1H6F{}t2gB<${j7%GfC&vsiLu9#cM@>SlK zj})!YRN26I?SV_2<{`voSvs~7LN?R1u7#IZR2)+M{$sANj&FMxPAia5I!WKKG8&!Y zX3qw3W&X_pJuE{qli@mN^NQgwsx&?TbZA~Q0P`p=F@wossXvLi3s-j${BhLz>se`O z=>?ln(}TKph}KN34!84EP>`4?cHz7T`CE{1TmDZ72xA6v-0(*3rgI&$Lpt*5y1r8w)8Gt~AZ7AkE7Q3 zLq31p>E80r`t-?AY#;u@^;;kB&mKDsLLd&{Id6m-p_HkreU9@_VQduxp(!#lGSm0G zR>RMuh#tz=6I3*VZx(V`*)L+qA+R@xJAibk3(yM4e(KfOF>3i<9IVYT0T}vKvk1Rf zgNuucDh8}Zuia@LPxj?06PO=5K8Of@qXIC z4zPAW1lu~q>{8uY>U4F%;F9v->-q3pcYdpFjU zNbG5zR3Y4A-o^ZZgEM%)@T2z?iyWFgPX$8FnLySlg?*v+`Bu0OsS1EK115i(UqAm=DokTdNPGl?tg}bAatKk=kcv#lBRnw zU7Q?l#0~>feG`dbUs*;=eMVnGm16ex;Gd=gJ%~DQ>D>_*>p&dCqQ8A>_HUn*9eL{9 z%?-Mt9;zWeH>jC|kSD`4?j?{A{at2^fq`GT1;PqlZ7WzbGAw#kn_!P)MgUC*@Q8%O zhwv&k!Mhs3b)5o%^T(balEGz(?gx;ED>!6u5&qnxGC4UpQ`GkzG%eK&%)sikazK#5 zd=+b#Wn5UldKLU*vA~0h!({|yh&Ek0!zX|l3w$t9kpEzzA;DY?Lxp+JW_s5_(Fftw z2RCE54;+aI_9#`&>85~g0#>H$UFkIrfmzg&@klDNcd+AGLcrS7$h`%ZGdG)qjc$ zntArk4rGcUq<$pnFzrSR7#;`hHo(fb6`IZSb*HxzvOejvUQv=%zT>KmZgz3`I$R#+ zN-ig27CdipQEld zOucsA-q(wZCMYN&W)q(?hY&<-$~y+>DecZSUKHDc;tK&``VZRKya z?9KMrl-l-hPZ%fJlJaZ>WoKt+Cp$F^P1!#A_Kl6z=`X0o`*B0XO~gj$ZK`+@lGM9u z`kln&I22{i&2c&s-!6dLTD+ZDq+zbDuBzZkKvMnApOte_TR8XEDVOlm$L@gvOJ3_1 z1grD?=+@x1Dc5U`qJ}vGLW^x!=rMUtnHi5i(!4;IbL^}f|3z-?omLDy>*^r9dUbNvNLphxvxsUUScP8`Y(rY2j`@x9q-eZK1BYh-1XQ#uU}9{ z=Tn`?txBOmvJqT*-U7m8yZZWsq2RuJi^P@&Wb9vzb|s zwThXmif~Hf|EuG2>77Kdin3!>JfQ@uK42@Q=C zAj0RCK^t0RkrD1#wkY(pOF;a2xJEqb8-KZP-D5w#0A1gP9bYNp`R|%NR1J-Lr+@x* zJk|dl>OuW`vd8BC>txSW3&I0Y{)M#vSNoG=`tS0fYQ1;Jon)(CM{jm zm)_0u{hQlciLN0G(I<*3Bz~EaV-oLRJHRP{T=?TH^MhNV{XBSoiRaX6?MP_h9_PQ6 zNmZq!G=8ky-g;hN9TyQ$#?0F(ix{%-M-4L|v*%i_nCA%ek% zldH7?pmDta2&oyuOMDp2d<#=Fl3%+gRBSNlRgbj^j1houDwrGts+N|3@#NDR&zkqK z{;-%1O^MX2!R`)^*39~q9zfMFGy^0=zB}}KEsNXhJhfDY&jfO1Q@#^ZQGLe(APAo| z&bcq-HF2fattj(i^W>EX%)!+yT|4UpNWOaC>)|1zVX#@O|@1#dCNVEl%@3@hK>#nJeFp02| zn2kSS%m>V81Td<7(k@FaQrs{rd=;Dt2#MNUa0VR_DQ=6&Rj{GHL4`0VV%1WVH6$8( zP;kHh=!C1uN1JN0yt?E04rotoLsjyQugc)s;ca1&3J%DW1-_iltC6*~49EZ=aiq#h zI~O<W`-RiYm|x zS&Y9|4bI@pm~{WBQ70IaUhJHLs*ecdLt|NO9f#6g$m3BPe$5&j68N8AZ?tt_!ISpB&6zpu0S*vEWoT&faPaf{ zY`oUya?Saj9{K45q1++tH&U-V(8|@?CtH?&Rx3EpW?Zg2y`)2w_*ekRS@M{69){;A zt@b7n2Zy)Czasl4lwWAlkccf$uenLq*zq8p}V^T`ODdhG?68Q)5zdE8s@|aHhI${W8vd( zZ##sEC-R3(xE#FRb9F%(JqF`Btspc?CB!$E%3t*M$;OZJI{Ii~(cFo<@JgF;m9yN= z)cL>xy9JMl-Vf$Zf1!dOlbv5&F5-`&oNJ1X#6X{jhzKpiR}QA{oIccY(PwDlg%UlR z%9Xbd4=#xD$?`)X77AV3tr9A@5nm5!lr{(WU&?hhsyqNJ>NJFglYJho#d@iMi(F z-Ylql^~zx{{htNJv7uWI@oIlr_}Q!=q0^riCj@V`ZI{`BQeeKLiEi zJHC%_%WU|W9+5)kTRV)SNY4W2-_v6>Yr?78t3TFQNIi#n8Bb22)EZuOl%$lEGj&U;u_5W0K2T0{nQj7H|s{pB>{RIAmLrG1Uu8Z%*J$f>0dAVWE;83RD zFh04fJ4bRsjM!YmU3W;5wxgDuE~0l$!x@E93)nJ^ybF<+iw+=J;EnDSa&_xEyaa_I z1sia4aL2Oltf7=GH*Qfp9mp&k5_9N){4)PJjcG6F9!mE%l%L9MEh(rB8{B- z*1IURLUo77{Y#*fAbq^qkkgqBJr|K6^72M#L(AYf{BvVXpf@pt1nHBe+yNdeT%F3( zQyVujsc_HcT`0&(1~k8!iOg)MXo>LLsuFJfeypOPC`o6!Xzu6pwMAIDt=RM3D(@5j z4qw4PYHyf*H1BP~gF>Y<_)l;--R+vVzVwLFzU$>2V!vX?A8Z+GIa~*rhVo}WR94@5 z86^eO^Z!g?MNkuRd(P0@iFt>N3sjBd1Tq9(!xxNvyazQ#1u~iJ^$Ts6pwZM8(Gm>U z;+pEE!X)`WJ*==h3mv%YFoqFnQA92$TjrS`uqZ*8+atYIMX&lmh;5(Lb*5mzWUg2- z8$d<1ffgEtTjONo%`9*LfKWZ{T*jsR6_%|W z^3kfYIyj9X{|7W)tZ%+6b7F${nnIuN4A0RCRsD9d-)?bDqi!}#bj&#}7whS0wp!oe z>FejpSvJ4jdatlyghr}q`l}%yGs4{w@{;xPPh4oag3t8npHx3+(`WKEk-Ox$9I4mN zXD@8^@>b9&0r{M^&wRBUGT%7v6;g&q9P7+JTxB=MU`~zFWJ!*)id+dP8>-a0mO1*u zbK*QfMfC;|eh?Qg!Z!}>N(byTJ5Kz^@pT{H#p;sf7YshO)~`COjevE_szCNS@PB({ zT=t`}P>Bmh>}T?Hx@-ipQeG>H77^d=m1LgWX3d86GBCvc z9IRb--OEkQSpMES{D?wQQj&%>N5@oJJ_4p{d*ut&3<$>gKmMY7s2-G>N}^qu0f#bo zmCR;L=e_^^RhBDdc1{lTnI|N2!|oON>}k*^v=a;wz>q7B`bLfmjAR7^>~+elMQLax zfe1#qjq9r0g?T@0xRO8Krg!o2#kp8=hi?+s>zTY74h;dH6MoX^Tb0Ds)Raux+pucd zS9z$P<6EiVY%9p2ArA9t;q9u5<)84fU1&Wrp%OCfJ+{O4ebjB+R zaPWz1S&)kEPf?_;vw~F3f+Lw(c9!$Fs0{R}H{~%$eZ{SFFw6i3_@cx)^bn`c8xSz@ z9L~h+Q7)S*ge2oY_DpGhb1F;M@1GlC^)A6Aj>J1t zUsEYHuH2_^xM2T?`snMX79w~xWAJ*%rOf3ES zMteid|?d^B8`)mwm=g&-R$TT1Jxw!U5JNWOmJI~u%)W(?uR4d{5Fn5&*ipC6}&iX z%ryCDV!x|QlxWy({EIOe|EZX0dlrA>{0eT(6W2TFK!#q{LMr#!H}AvvkDVdC^&jiw zqNP43VVr*65%2u<)~`!)A0K~V9g^VVi2d7A!+06|<<#02*iZh*)ncs_g=I=dSo++p zc7W35b;53MZkvx!L9iO$eD&EzZyITgSp zFCBKte=RdbrY;`V9^0cQkWUy~RFS9=t}fS~6_{n8vaH^np;x(jRXyNXL*`J14}-vn z>f@K6`951u9m zr)qif55VT1w$txTs*xHa?~C5Kr2E@)i7%5Rv&5jgn`LR19By!^7DRLCf5ZYIQlS-X zKT&pd!{yEW3ILc(ME#lg{qAIam3JU-C>;cm^gyZ1Wz8SviI&BK!rW-_7UWaV6d$q<_lwzQb_D01khQbGS4Rf<0J$1ike~;F(#k>r=|7*E}?FrTXoR)9~ zQ)+2L2bZNB72`$$;<-*nvR`9oEcH{a1i>?Hz_K3jR&e?Si|QG*yxVaI z#ELv-8;`+Ok++so8U)-0C)L3+wFZ5$Y#_%-(Nx@T1jM!#QpKA}2CHNqJ{ z-qMGwG0I6T3Zi7)d?37OT7CZd^1~t6k5im;%^=nH5 z;vME1F0Zc6wnksvfO8IqIWLfnA1?rY|3VfV5g6l*iHefCNn}(nHEuOBzuMc|1ICqn z3L7xNcCXI&_cu2|8lr2q9*=##37jvjwi)kinXa)fQj~yhpbM;PykE$;3=b#95fMXy zFB92B2d}KGyvhnrt$(GlwP(IIlYWHgyiyoe>!A@9?G;NT}G!q0L9VaAW?Bf_(?l84fL zewErfPce=Y?wt#8%U*Jny9ToizlLIj(Zw#W^7wE)B9d!C8bJ-m#Uk04hH!?W z;j-gc@gND9&&6r-mcLtR>AHvx&z-b|o6sW3@f&hCdl^7(EVQBP&4OE^;}zMk+kWsU zJ2+><`)z?L?5&(Y?8SFOsPn0-+Mv2Mf*PZoi5Z#<%$+#@gF)U!dgyD-U?-swvyd z6oZD(HlA&7m+b5Tj!UE6>)$Q)E)X|0O#DvpC;L$`Vc1>uO^??+y_eK-0ZA@fx(Z#K@bl#%#nHTb9p2J-3dc4(0wUXqMle>f`bLIi00<#l=q(S zh@s~@RfvM{$Y>59JP4ohGmysXsEI}iR!!UyyScJ|bSD~sYuqQ}sVMYN!b2t0UAi+= z7Wg`ic5E?VjX0%McGJI{_bgAz{Pdzl`zngp>}K~cP&xUB<{NuEBUh4bwr6ToCP9X? zr%)c0WfUaD_qO*J9!hCpDXRu|vtEYx|3lkbhIN@nZNFmBUDDE>DpDdTDS}9M3KG&C zw}41VhoB&-bV(}R2&f<_NP_~>-Ry-r&wloMykGYmbIg305&r)>u4`TEJkQ_4aW<&w zxuQ!>##(DFCnEn?14|v1JK4jd<1Sx>QQvYm9I(R#4wHosj|an)qr5#f-!2OEYTsAl zqB3y=E$C}pKeN=C+wudG-EXJ?UaP6XyEi4LZfF+gFYsbwMBo!iNZbf}xn~sX+a-(G z{%a~m3tmnq!}C5I*@DvZ8RZAVC=P6Ok>1kke$@vv(oX=~O6a0;jW0j75{E{qb8Xp-Q{ju!yHu z@+sUF^&N+@oX&n%=l{B3p*6Xl(WmUN^(rH{7blvd*k_(%64fu0wROrLvCzlFi_Dw6 zd97e}cRaM%(4kB*%vP@9Z=*JCAxesBR%~mjk=1}#pHEicqGG&DHR8O>guAqiQPus+ z0NluDZT)zi7e4y;oZf*^T7*Hodw z_ayDn#ul`%@#_%K)%aO+s56>^hcXSK{U%BCmew(#4Bu56<_s?Zt<3fFikl zyH+}cu(zE}HA_4ptr2|f9-xlMvy`Nz`;`{r=L5xu;>M$%b3gi-_P0yRowV(7*XOE6lSZV1gb04>p9|sFUJa46ui^5&sx}-qYFnj6J`$P3>CF38JB-MliRl1e7a~R0=v0jH!FU!Gw19mDM1l( z;4v%cH%dZAmcU&Xey0L`m#6$=d<7e(l1s!*AHqI&-$C)>s6cc2WJ@33|0Ak)S@e=# zt}2sOE<_+Pa5Y}^2YhMWgV5FZI0?_v_THYD>#`QOPD?pSZES5pCpcODUN!A8F)?uo z6d5VT)sU!#tKTf6q2AEg+Im@)bV*a;0^T+D9xA>#ip!{|yQ|}rHLmvd ziK1SWSyU6)N+4`}0TFtzQ1|L}%1wg_Z*anb$QcqSu=ZcCz#2SN?^)0?nu2V*5LD8rk>hRDkIGPLI_;FzG`w<0Fkm|yG)#JVaeDJ2f!xHRy?@cNcMU0?oQ zmcP0HxgCKg$PdVn5Y&tLzSvs0!>YRM1~INqvfE?8z6e`~sDyf<&JKu9_!;M6^4(hk z0((02DXnzwC6N&ufh7c@c|9gVa$@y%2}%ezXu#ry2s;|l(1;K+p2h*AjM2FG43 zi}ItM#6Aeb=2(N#+U7ctV_4V0@jHa%ssDyT=A07-)e7VS3knNUQc}3MxR^Fm8JrS` zi5?C8sGq)Hz(?n(Z^!Y@qsFVBLE8L2r|tCN!zu@Zda{J`_x%|@+jN<@)XWSev%Q+y z0DOAjMTDkuXcRoPI8P=+S)cQ7d~+D|qgtW}0zHUQ4MW%_yrwh36Rqx~z1r zKrb6RyW)>m7i5A5;j_@QP2cLkK)=tr1zsL-`&oj+haTUG>LYawWj*(@xqR-(bn8%V zX5t*kVM4$(B4Bu%9<(89(u$rHE9_#$+0q>!hbPH!!~R8KL4p0UfkEh^Q+gCw@dw{2 zQF1E74FoNWjWr0a02rxLMckf%rNY%U^cSykitr%#B!kaD)sCbVA>FkD>m<{r;pK$! zr_m%P;XhZ%r)Eh6jie6>zCSb&+@T+`KOx{D|A)z9bf(A#eTNvLR0@QLl@O1Bd=1!` zVDCMF+r9>%De%9@3+0j^j86`?LqlxSjF{$ctD0<;KW?a%P-}fQKuly==X0dt!lTjv zk^rw7#P{&HsE0dxpvc5c$6dt2(_Vn%-Rtu3@bE-4ERTR&qr+iYpdmG(oZz_* z&o{#V@S#4QI_7Or5-0ThPmb~j$z`3jPtHh>B!w2ZQU0nV=yYOn&v5+`>LbulA>P{T z28&H#@mQ8Tk^DhBLv>1Ts!1rPJ&4>ml(%x2W|o#h*C5LWR#wVMduvlGqwz*k8B~9N z|C>chK=uMc!rh|Uy1LS+8`(36ISb@_2naxRgscRMSaN~PPslxbgC($#lzUD%;y!K? z1bQHqIGB-s8R$PuUr*;}XJMNm3t_|tAGE4}NazdSg(ap2+Z|c_bK9qcN-a<7QQj4H z1AVWr{f6_O>ki>j369*JeNbz$Uu0of`zG%6m`(bRQyHNGmw9T3p`_Okt2f^5(`U15RA z!`+=zA4fsVT2la|^?RgU@@a8Wja~2DM3~wBK`0mEDd^y$V8#wLPV5 zCjReX)qriMB>oMXmkDF@**Nz5v(IV9E9NJFt6?3dQ)xP@??dPpdg|00eMq7SXTD+z zh?gl+vEdfx^?w5`2AN-Ha^en?Y|M?Gi5Tn0)7N=2*0AMA><0A?*aURch$m;H&eyk1 zLuP`@yq4o1D-w6*dtvE>$EaH>`kW``;Q9Lt3@LEG))(;W#-ukJ;S2YxXh6h}Qv|Igfm#>?5K}1BN zzsm#YP+gJ+5W*sTZE6f*k}qGrfPv)h{SsL-42o)qa`Rgah^xYJ6QEEe-z0hf_0}AB zo<7YX~M!|V%Ll#w^y5OIZEsYwes zpCxaYro&MLju>} >nhNW#T@AxJd_pyEh&!ln%ADCPRwH;Sl6PIy^)mpe~8R4=ls zaY{8z&u_o5F@AgGHIY1l=jx^Zn)})g@OD-^^@Dq^0E0xGoP~L52}h{FKI3%TX5y_G{jpB z&isbFzy6t~Ri`1zOIxeeD6Y46LO^Euo@w}4o{w;@Mo|f@JMX>}hh2f{D_oa!zq0@I zi{J!Q?=u5(4lKI&YaV0$9xO=J4Hb#Ep6k)%MOG5&WEr*v4gZ|Z7(-0blsw73Z4W}x zVbxVhA64}uzRAbuN6qdFk+!B7lIJBozzjS-K2E!VQ;+n&&Ca#}=XvYx9;?PaAGnj! z($bK2oBFIEcEM~1ww0(}lhHgazoN?W@@JJnK{m!4D}gyG%PKoov6}fPn&gOqa{{OK zO&U&ueRB=kmGOnOeatbS7H!C%>^7LrGRr`J4y0JB#(u%uH^ovZQ ziTsN{LRys5T)=#hZN()kRI8i6($4%GAW#n#G5wb|e2T%D-tlU)er4~B+{p^M;rvTJ z{dSqTkdE`Md$v*ygFWu)Bq*)&sGHisTTxN5qzpakbBh2=HDN4!KHXPzgqmpK-bU0)~9J#}si1||~&Ta+3<;;!(j-r~Q( zuEHpaTrP>rp$(EzaQSsaXF2?6xW((*xjZnO;PvurofU+gqWI!Vx_qS~zvfa3MaaTN?E%#c$2@35a7wE#9(i~yju*SsGo<%wIfn8%>(oAjO_tn(oDU|MAr)0x!h1k)6gL&2zEEL%;HM%Q-Hh)S9o@YF{wT2Nm78>14+u8G7uZs-xOd zQ4XPf>JCP~q;5DdXtmZu?tAB9iEAOb zxqN2qY6~E@jk$2)raVuP+%Opg!T*~@Ir*KU%@T)rpIQ>U)S&0mQ(9?+4-}#fL68>D zv=R~bK6D<@e_zl73bM$yy-vfA`Qyc_O%l_=*pI^t5!x$s1WbVjTraMSl;y5)&A&L| z{|{zIr$?u)Os#(V%zO74W!x_aXilvraHrlKlOW-WCYU(}g=YrC!g3K5C5@bfOFAEs z;kw>(UA&5xbeuox)7O^qv#B>hf~;7IvEUd;dQGWJiF}kOJ7zd5@?QhTPHz;_+1rbb zekwQbR0}?I(XrLexu|ooB5YXTt~=nw97)?1pF>0GV;+TOoa)XDJ5NxzjBkewio(tq3S|Z59fK}hPW&4rqa!`tA9m7 zVP+Fmww<7H67C~@i^kBUMks{}bdBRFsP3sbiDc4O=?k61`k zPf4_{3&I*ogKvf><5NlHy3lMn_O{w7u)v(%%;8~@5vRIUU?sA(6#LgFd>VG{t~l3) zdT)a))GS+DZ`sjq=X9Ntj3P0SECh`Zn%~jj{Epu$c6pZX_}A84BCxT@U(y4~pg^in z486~s-&G9+PcBk8eyuzzkr_moJwgSn@Kj{(%H-3$n$h(@AG(F9O-4WyvewRp$r@)+ zQNi1izK~UH)jzP^&FPHqwO{YmEj5cWQL_@=5Y?;_PkiYJr)G}>CnZ;|m39}++L%FC zCrJ{IL4wi$@&n>ju|Qg}&4AvI9fX>ND>-D}Y7c0RSERhI4XS^H-n1vuyiamaFwV;p z!)0(iHY+)ZQ%Xj+lrEyw_N56+QlZ&^vJ%dxPV6xFvxbN-< zY}Zi)DMYHVnQaJ`9#M+@mCuf^9?;!TB-jjC-@_91`exg@&pu-jJwu)fA!b+}XigNz zMqahW3WnL7_X?16?A{Bz)b{f(Osc)yXQ0QO`0Ei@MauM$49ZZ-@oCGMDF)oxyQdhw28c;j@bh-?0^i z*x7}Lh)_5LT`0Pt2E|*fKp?FFn%Dz<4phmOnJ=&OH-7l;>g6)dZ$JLnPyA3&TgUO0 z@cCgS>WmpIU#c?;ryYYWz13T*YE#42JJL)*kOxeKu91e;K zDSLwP@J0kl{=B&1g+ufK`o`HdfoJ9Imz-6;1h|}Z=@Kl!cDARXDKFMar2|G160vk0 z5M;0(gqpw1x7)dw^#H5QI_g$2@?ji=PlB1K&#SPUZSFhmAA^hp?j=?ECeq|s|-ukp0$sl5DB-|W^u6zKd64fXjfBBFILUac+xEqU2(QTMG^BGOYC z+As449n`u5vV$6eYD~NfO1MjVYgb-wPlm8KHn`mpL$G)E&hAIoJ06XcE+**f4%}tF zx7gfvL;O?s4PKvhj92`N1zW@NS^jZJ*f{FgUZlcGRIpGy>8uQV`{^2W7`UmTQdWK` zdo#`>dMdx}*IN!(2&#Q-zeL==2)^S#mI8_KJ$02YpE#et4C_Hd<8WQs6Ozut4qtie zoR`93Cc)D<$q>2r{io;2x7Qdgk<<7_f|~iUSWAcN6S5;UiKM@OR#frDf4DGI<-Yas zbMYFIm)Y~}X^|ICvajD~*$)I#oQs}9&E3#5D#yV|yS z5Ia-jddVEG?X=^80>{Ws1dA&pJ07ld5!W@JfB^qywJ*gp)h4cgX^~N9s4{Ru9j-Qk zR$J@EM@tb{fmrrcV+x&0=~ zog~&RZ-7+DDf{_Tz0&p3X~ZWoy6VDNNsso!(DY7Tmth{a4=yMA9dDQhz!Myv{6txd z^Nr5|MAd>^#r!KoES=jx+czb-e#>H02x=zF{T~PY$0A$b#xzq!f1IQw-)P|+uRO(d zbpRGhrGBXCu-0?IR)?nN=ko1p?PJu4A|o@i#47R8M6^WMD3KoH;JF(eZ^S;X?fjav zl0)fu?=Yw|_v6S%9M-ItJs*yH2Y@QWC- zi9&zvO^M0-RY&bzn>wUpo?0@3+WoqVgy=GQ{RY@kXq|z$b2qi^-C&gi=c7mMCf|z3 zb@hO<(`!geAj*r)!T!HMXQTm^T(h@ukox<64D{92)q+Ao5Uw`*=SEhgO1i#FZ1(NT zEcG2<^yd*VERNquX^7Cr|JsbV6ECyBX&g=An||S5rE+h|s@`iBO`rYIg9wrhJq{aX zk1p<S^hR zVx02YE`_&D1Ckj`8EF5ZA9Ln>< z%fymR9GfC0^=n#f?;aNg>p8BnFVbeP#>wo6UG7d>jiGxFGA=^Qvr(ft25J>;C#HXo@ww}MilPUkFW_%V!rgDKu@})W%3J>ysgQSs3%gS0C7#q=& zmft-m$T0qcr8)m!VQI0kv90W~a;oCTx_>P7F_;eaPrvuliJ_>EQ^HgT`e(;Q>uf&f z--}V`e-9*h3K{*El;kM1FL}sOu>br6X;eAHLC41SKHScYJ9gh+PtVTI-X=OY@H&>b zjQZ@c?))stg5yr%wp0K4t^+^v_!M|#JX$5R01yKnE*R7yOnk1uLkI~(CvsxqEQB6B z`T)W#t*iv);eDK3Pl|{D{5N`2kYTYfFrs~h0gNi?M~ky^Jow}GZaPj#Jnr95#+ZwL zwkuk04kQm~Gs5(ODNwx_%z&^7p@9bb>GKxe_Py4T5qB5{0PW})!brbM2->5rI1F;@ z$$C+>kJ}t%*UjX#v_^nFWdS{WXi7f=T$vj*gFu40%f@dzswO?wfQWUe9EKbpi!yWX zJ@J4x3p_Q<)~+Ih>k!9i65aEtKH||ChKJ{OYQle?4I?^@t``7u4;YR2nWCYmH{Tg; z3Jq`s0$Gm93k#M9x|H@$-%8&#!(N5tJFT86Y97sl@SGi0OtUkI&A+OvC;|6ETdG{)gK;11=_&j5pw!j>+~yPRqqNa zD)7PHf%-jC2^mv(Qh;XLUNo&^x&=c8el#G4t*yv?kU~PK=#Qa^Oy4^^eD#qdkb8fd<8vDs>QLryIhy2Wyry9GnfspcFoP{Id=Nsfqs;+9P?b|e{} zWYz_tP+|}o0vh3VA08daN=rk22wfJQKdNVG$q#CMeSKq8bDNWce!VEWV1XFeM$+EC z?G2gp5z@e?*OB!pzbxlG;17E3uN#As${5TUvJdDbS=>_6jk|Qm%#B|}tB;Hi*uS3_ z*)HQLXl@O+=W12jv;>#W2X$x^=>dork>r{G zlHvuAIXWpeCZ>N5M6L(2-KB`?rjHr9Cuc&@`eoj(UZz*Dik&(>nEQLL`p+Ttz^9$ToPSKD^xJ`uG-cboenoo684 zi+#}knhx5kdPf>C5IBoB?E$D6zX%MFtKS^Q5-O?)zGXHzt=8^B#~};oqU(4oy)C)E z{oeoIYi+NkMfNNrcCe>O?uAxPztAKm;4T?g&Y|# z!jg+2il?8XRXRNT*XbF+ED2w|v{*GA(JkU#Jkj?a0>97*`5ePF z|AXz<$sG=6G32+{d;hobizO3i{sGNE1*Bec{&V&K{s{^QDN;NVSrU`Y?a#(BMTpxu zo2T#1h(Mp6o??!>zW<#Phi?MAI_4zAo{7Yay_)l%op7*VnUb4u*2MsCxQV|I=*i7= z2Xlh>J$W?mZ?!USmUVRo)V?70_VIzJZXg=+8`Kzb@8uD`@62rzu-&k+rTz@8}MneGsNNWCKVz>y2YF z1yH(K@0n@c6W0epuwpzU<|RujJ7Bx(`@-n1zLZnx^kNLIOY29W5xi~>8`TWb76zui z=g(19K2);xl-fLcqE}Vg{0NzU$6E(6$6cTX?sO}|(UX~x`cDxd*@--D_3l=LE;UTm zV0z^8yeYp@Z5*76*rvvGd|zKkkG2OpN5b>Gou|W-OU0$2IGBZK1HFu zbFg$0;@TF7{{W&D{*x8ZQ$e7{?!)EKGT^w6R%+>n+hLv$MAEw|#}5rD$l*3+dvgsTkkc+Gj2;$QOz<{#=^CtU`S+GUidv$ZMDF6+!- z{4&2pGjxDq5D=#(Z?8`ueK)=F5A48c(luKA*%bocx^OzRy4SQGo8J#52&hBgmEr0|$M;Xh zgyk3VZNfDzG$u@Q&hGmgEe(L`+V^mS%x6jtSH(T`0SP2^fGyC}7-tImd8h zF;6z={oSNsr6?3C+@3iIIm;QMrFMI9!fUaKK$sTAVM8e_dh@?G;rv zs)Z0NErMFkT5=7+>m{AgM@mbx7s`S&q{n8q(EBw-!^Ry&yQQ05)~W66?AH%d8}8#$ zsi(v7q+;n8F!VlhKADY+_(!ukP&Q)xsTCS>yXJlH6sRRY#u!A8k}4rZAOSi>FF62X zVirI!j%QaN{pi9m&@)y=6Ac6+%^VdJX2v+iEb3*IRv1__1kY0R`=y+z z?`0MY*OBY4N4vHQbug}~NnK_RZULPS)iC(`qDx^75*Kg&B#qhY0XK7ny5N89OplHxc#i~aO4H3om6BY?o)1xFSIz2#y7a+wEt#LNc;m=k)s;Zi z)z;?HtHzJmK0DnL>(sP7C1g_I)UCuOe?9@tlsK3sVi(r}t4O|jKI&lJN7-byWZYzt zWP(g0rAMO5Ims2F!wNUwY*1V7?}+Ru=NY7iAHRcK9ZzbW&c! z>=I!e$gM6UJu~Vk=bjQ5yZ`3M@K2Tc}>&v zjd{R4pm*=aDt4j_;=}r zv@3@-KF`T*qE4`)1FvO)Q1B`bh9s$6**hb`+)5< zf8EjfCNa5g3GGcE&j3=cd;j1|=<=ABB#rttsD(Nt>}}CAZQ*))Hl^z`ch#cleN~jS zzNCzG4h{~rO4EM~c@d|8#BbJk+#u|}bqiIqcyz9NuiFzBxSgvLqV{{M_8T{BbuRG~ zTbAmj`#Vh3%5=XBa$Pc0Ksl$U!v~l*83=kwuGhP9Pwp{!!2w zZUtJCr4|oL;0G$F6Ho4^>5Wk z9vvgvc1?M)%BdLu8nt%{qqyFxms+~e`^o3GFFGnJXIA$%sDOY2YYt4<}0R&B(~QpK@_FOLK1(N6d*?k~3fE zxUsL#{>arjgpm-AtuQOfmsGRZAn*`|F8;X-31saQEkE?W1>Fs+FAL1q;Q#)ipJFrD zT(KYPs?Bw?U?17mFZVZo*d6V@Q_rWco3MHNE@-x+K${M~+yNP#+g`NW5cBR$OAfm# z@QSxdk{;tHoMd5KVVo~19N54l75DEK6)~#4bp1`r_(L+d3Kgdw05R&^)_E6g!D1lH=154!J)UQwRmmhQDMdrkFNO5Q!$DkK<10pmtlZmI8wc;RQ zc?E~=*Uch@*!JE|xzeSHy3Vkx465&LcD^3Y(c=63HSc+#JlI|UroZ!3iMU|#1vw5D z=}kxbUZz{~Kxl5PYoYsPK5BG06(op10 zn}uvg(%{JQvl>olbr20Bu;CQweR$u{&_FctO^JY+ z6t?!H>*k?JMnH}SXCj=vsYpnmn^Oc<%xtCCu5g5h;-)K+FB66hKn9`oq0lKfUs_ke zOU%X9fIS)f;zesjRB&;zT?CboBXqQ&`h~xd5#acr_mclKcNiVgicOLpfBN)E`OPb` za!ccnwy)#00P1zA*0oSQ2zT&VMwwG9Adims@x%M|svesPJ7nCApfb#I3BvVTFI8t4$T5EJ zlCRzo;g|O?Y&Pcs-s_tO*HP&n8Jn!-P_B%jN%nX7E?>gN4&b_^a$PRl@;w~ z+2h#wA6QW=k(043(Rl>+?`2-_+KmfO4dn8~XPL>*S`XVWD%E=J-Omgrp%O(r%b}BJ zasXy6DEJ?e6s6fs)`OvoUp0CdO4HSW+}Kw&SST;>3;Z{*4)PL$d!T>=Y~G723+Hor z0NnJzg)56k?GV63Frew${}39FgT!0tDMfW!)^+3prGg3uZ_A{=FI{TIpwu2iPxFV_ zVrsD*UVF%MMa5-L2BC$5Vor1(*rOX`$#y8sui^%a%$p27=LGMb$Ke59Q- zdB>;6-(QtRFfH*T27RTgEg~k?w)*WI|7~PKM(IqijsoWXJzXkz@G$%N{*i34jycoU zAkL9`QS7Cw9x9^r3#s&XkEQR-#+^np+_ExlBT_{yd2btc=I_$VLHfQif?7%Gp0iNn zV7|5cE*aWuv4(4Ntls*zlr$owZvHt>jES-0WPz{X#? zn!?u9#OCAkaCd3vt&KyM)xdg?WI{c0#Fb}nmkg%JG%p2T?fCCtT)bMmYr;iL%UFuk zk|j=1^}nfG+-) zuy1#z%rQcvd3%`)pG0XY4^FQ6OVj6*+I$Abjn_=vARrVQ zS8S38!(vn#!1oUu-U`HzfIyYI^x$tf6*CZ0b|E4-`1wV@0XFS#hDuK{rW6NrO~0l2 z<+iI2=8@;K1>sOO8|l9hu_!YSi?`aZ^_X04D%5S6y45W(|)HH+kL# zFlM2OUUW46r^z3E^0A!2T}=x%>XOH7!vCK0;KE#hpPyQNE#J-bCEv3HSvuLl%eC1S z&b!OcrGj4NQa=blWnNundn2a2vDPs8^FoU+i?RlKpWs`=g2ZUfi$n&NBWoXBG?)~9 zv+D@_zrNzvc!W^wZiNpODy5&?Eb=GEBL4xzwc{9zq~YRBt3gcs=@GaxPRm!G6jGst zUQJ=v(yCy;SE-!N_4zZ-@-p`6@z(3Ez|myUqi#R5o*Xjk!jB9&8r3slq}mg8)?u;S zL8im+*}2s^&r5?<+S0D3Ci`LT-yE7YYFKA8ZNVLpNJFkkvY>WF#%Pv;`O~e5;?yL$ z1|5C`4==F&OKgW_+h>tSe?-5wo|^QLv}#;*}SU z^Qr|pHMfXQU!(KrjmY^T!fK??*8R*`9&(ArG-p+OfOE_856Le=!t67_exLJ%zLzg{ zN6o$w{e0cxw35_h?R!~Hb#`iEAgLYghrQK`FDbgeEiI_1&DI04CcnRy3i74IRmqJB z@!M1>sE_@nT5>&f>G}KKyNkK28|~qC4yy0ecoJHI3cdDj1$0s0T1G@{kCz4onsQy= zN1f(A98!9f7{_h-CcG zRt;55lt(QXUfFoMe{u~xhOMo26Gp{>J)EAN{@nh3o>4ghVreOMxG3~1Tb@P;( zk38nZ?M9b_O*?-8{ny&qopHf@oL52FGreiv_*>0aUNb3jLdopkm&eEK9DU5lC7pSj zJW*V|xsAt%df}lZ28tuW{oGe7YV#{&IhZ8K@!I5>>eRxMk@^$eq+eg{_WA8(-WRNl z^xbr^y6Ye$f^e`}#|vO)EAFi<>opWrcwOOFq2b_m3q+r&>-St%&m-%O;n%(OnW@se z+YOI0?1w@Z`Iq@d(~g^|8uAWLALXE~k5t)~y}HryZCo_a$|sD-&(9Ai(l3$o0D!^y z41oZxr>ZV4E_d&F#J_y02`T_^URWGTTIaY%Mv;FZ(Y~ zOIKN22%E`8Epb}rIj>SVFUH_xRqH&f^=Kfzq9GxdDopZ*EjGQttHw>B4P&&DtH9BG zHiW2Q+hTuT8rjGHRq#5=C!04jV_Tu=hhEF2rK!HmwfjkWwofLJQ&;DXh{wLU*S+28 z+|vDY%VFG92AqbA4!7X24B0jOZb;k2w6vAgo4jEsnEClr15zPSphr>)6hQ#soFtre z4Fmx76sCL#i~t~xO2pL$#7xiKWwyVwEjCkW-^3b0KJrn<(!e519X4R+>_!O{1efxW z`wzU(waEg|)qEsLD(#`zm~q-)7ucCuEz(ZQ8})IJ{2bwXa6P*Jj7dCmqT0B|F14aP z+u!&YGgG&sLqfHlR>7csIV8Z10=zFWH{8qYPL>t-=PmCaiIZ9 zly8yeji10qB@(ME{)j83jxRAvfoDz)z-Dv5W6c#*p~=YoO`qZZY%FK zUcb;e6c*P1EaG!^M#aXn`dK&!vR=^XGkq@RvDy0l`vI)m z|JGFbR74QZPvfmhy0jOemVkB{#mA4Mm5;aPnV_~pReEKHas_6>Urj;XsqxslD)Cx) z$BiB*!Xbh4`8%C3|_PK8FsC_O!Vy2Z7$$}?w` z^QNuf6soh6T~$>TA?pEoHOLfH0_vC5xT1=5n&skfp)Si}Q;Bg46brizXD4_WnzrhY zC5HZCcsP;shrcsq=3d6qMnB)>HM2;%E|#gSm-e(Tp5s^ZP5f0$A7NJ&dB7!_@-x!Gt@RdMj; z33^IoStf;2t*U>4A*Q)qGskdIKzxOD^~Zs^mEM~r?emO}CD&J1*}He)NWwu+ zp8*X}c!my-)*93&eJ{gAcZ6ZF3A|7+RxVGocy(#%qw9)J1-83~$9cQ|Z&!&iBvxg5 zqvP7Una@&UuU_(G3cF$pJ2BHEz6_n?zrH@ajQY-di@B}dXNW;2F~4p^L+MlFS2e|z z@fuc-u7y@frANQIUouh~k*mMfBvDihz$B?pYH15)3)LxX{EDPT_6{wsAG!<#6HE^9 z6nG5|%bw_L&K+>Gz-+0zz6gGLdT5ocyciAt2?oO+OdPjzNb?enBjz#i23aa*#=*J< zhRuMi@D5)7yfIWXG{w>fps9c~K?IX$nE`nHlm^Q5Qme~?jsHD%$s-lAa-D8sEzO8; zQ)VFwN*@XPe%RF&@zn$#JM38kg?qZkKR-86){EHc1c!ACux%}Ct1W&z<x-Rkb8dp+SIV_*B5GyjwobD+nXyX0QW@wY$@#{YQf!7f zL3e>&BBm``W|%i*c|K>nV7 z#Ro?wlfDd<0}CS~=S&%t!Mua8mdL|9Wz*9bCx*d6E-=^b_uL#YDXB)Wf#_fbbh0}Rx@0At(EIA|^I_}1>e8L1= z!=Ek!w{EQh3mgQ7xqUDJ{MY!HX}#`A-u2M-$%NM)ec!gg3njtHk2~WU_ImT?6_>}y zGx@m{yE@$38h;|E7}0}$CB_oN#j+zKWQng7WMoj=+e3#xlCovEPZk^po1C5gELl?% zonTi#7I8&x2C|Ae(0iYF2HMm)b!ZfL9$Z(o&5rS<{n?u?ft=YI+uHJ)D+K3FyuN+# zdahT@M{}6fmT&ffWn1U}w){PO^+yN-?Ky1E_ilD&jcacO97W9JcwI}) zO>D-U(oY4`TqSo+`zJf zt%i_^(~|&hz2KaO9hc`DTnp)gI5c%aU*a`*lH}SBd&Zp0BB>0r5$Do7i0F{W#z1)7 z0AZl=VN-_(rZ}~Qx`0#)`uVyS%$#`zfiZNW^iaTuW4BbotLu0SQP{9yicednZ0rl|%)RY8_@!T)cuQr%+rCXID`4`FQugRm@FI(UNY0I0+E(|IVcmew7wjsdvIZ zy7OHOKfPgr40L!`+C0XTj}j<0&Q)#ai|4)9r(e^oQ+0ZOp^^P~&#)U>sBWv+zrWv* zQ|FMZ<~Fd=F3s40-onZlQl$>4*6oYllCBd=v558oa#wzX%;lOm_NiIu|l7c-L| zT+c&glfGjzRBZRrLfIP=r<+6LS#zg)x50F?3^#XWyUxcuCNvDhzTp(@(D6Ljm!=jb zzT<3J(mD3+Z9<_gB2Y!Z`C=AlCQ=Qe)t>Vxvfq4k%74@ym?BCef18P1e_E%4Z6Y0k_nZIZL^9Q$7Kf*hBe zbKob8nM{-8e}ZMW2g9%6bqkCRGbb3S^FrJ#jVLK!L)C)L_hKG=AHi%D>&ihw1-BFv zvi5nF!M?$UZ6E-YQz7bYIK5I)Q^Nv|sltExaF(gy{!udk-NGWkO+&Dv0 zDfh}9gl2wWU+9q;--EJt^`sk=1|Ljfh030*?<^%z2^!UTI03%96p0*-CoXoZ_?&vC znBeW5frf*r>XkskeNF9cZ~*$_Z0HjCM!$Km{5@fTc2(oQX2JU@F-OK)^X@fN zxkyTjJ0{N#QbeN7yS!s9P7aflQlIp}k+t13*imMJlm|Eb!_p}K>%*N(H%WhXzhukP z*l}~}pBj>p9SaFuw}1Teo(ogtH-qv{LNt0`Qr4+#WTmhT`WBx*@AxQxgUwvcuo3H_ z8|>uZp^BD*@$O>~PKRB+)&EY4TS|N#BuvhOu8Tdo(YSh(Que zaAr8z9Z`$5L}5Fa-;E{ zMC0_`Z$@@&3nFgx6Ah7HV(#*(3f~RgZ)oLzcj0+(_=~tFKZ4zNIoa(D7@W}(6XhBx ziw*8JeR~la3fd_dyIPeP#sS)7^%MQr0&NmclAz_83|3J!qRvPAjp)NC_t{##HzfHw z63JeK*P}+qv8Rg3O0gq{#7GKV07}o2^J2a9)0=g!To;8}HN;k!bSW^fU(~zt5Y`E; zzYQa~@hxYU=n4!#1ByzhLnp|gH$xuyscgfe_|fOcRZb3__2*=9a|PW?|2W7I9b?{99j{@`#h_(8790uCi=XwWSI+xMzot7^T)hDdL7 zU}#^-2S=YW9HI^UxsB4%ed^G#<0k{fcJGn1$fbxs8Ux+4)_BJCqEP|6j5)Tju7tsY zk@@NOBpmJ6i(MpDyaM|2H6J`^*l!d4+sDP1+-AZ+pTZ``wza8$;j#LS30CMe=4+c< zVFtB=&OKypad$;M6Lagj5PpXTuhsYAEDFu{e~$-@HuuZgzy`>8w5S)V{)f%Gw(pdz zqwdrh%LUI(6EHCoeM$aq-$fiIcFN0tYK#hT+9s!;&vLFvCJ8*T+59R-+WAzvb8j6+ z{{9gzd^volo=!_=*&AnezqF+hi>P(u+58mCx-e0~%K_#vT62W}kO&kKZgQ(McJ8BH z!*soA8TFxEJvy3@jxYW2FE(qgswzwxg$#wL9McF6?teEF=vp5Xi~m&*^=VyEV0q&D zdq9C-El(xf_}}&TGZpIU)vF;iYFb5((V2L=zF4-D>%WMOAtg4X(&UWk; zEUD0>)zZ@XZ$@w`F13-QuOR5XDh=@boq@vuOs4;hy)Ax<%S$Jg+}J4Y{gUlrJN*0K z<8w^U`V`|}x)G(ls+8~S3G(;r94m~_v&?l6Qd9eYy@HdNRpo#Ff8l*>_(@R!>d#V6 zt%vZjRtXGP`Kuq%HQ-{xhoA+g;CC3Q10i9E6kQ`BVfuH)e5r}yD~Q9;oayD|+qR=O zp(Fq&!hZ&2aCIOk%yU7p-1@@aXqPYl_tgy1{(QBW#Yqs9{!ML;GDN!qpM#2sC>FXf zJ1{{Q+#W#T{#O9cX##e%^Q#f~Nnlpx_j;ci(Cq&6a*&)qIKG5Q@&EG+TJ5GU$jZnZ z?j!qhVlQAqEOf>_Jv`(aUS0FaV(jzSnK6}w0Gwc%ZTV&Ow@tvVmJQrZRzEU7cWM=3x2LgEod+P%+zpZUAMvtXfkBKqhltC zhDFw3K-EOv7gPXzhu~*~zzz<}nF9T@obw9HZ4H@_XB6^tHMO@dk@i&t_Vf8{C4$ZP zeXJ>C@Lx~~;$xj-c3@T%7)5E>V2=P&OntgkP^da`vZFa!?^7BDgdR9c%6Bc=SJ`}r zJ`U+%);Z__{tPfH@=>9Mwh2Qs+ay6Ef=ChFMeq&QTYRPAOcq1#Aiy~5{buQFELD0n z>qRo0dSvc>^nwQ&d;>59c@A7ba+s7*{M)w%6G-~Q&&Ag!%1D=kBVQ{r?)VKcXH30%5WOnl<#FzXTfkKy zNQk6HL@JRp-$fQea&mHb(@!|hEyF*1$mNAT-HH&teOoV71f~XdRDwL#x(R-8fNyZ0 zTBFZnk)ff4-CDtF5~-j2gcep;X|nr=VP0Zf;3NmwxfqOrNqQEM z*s?HCNq8lLf{q3;_Te@O4&Z=#=lZ+KJ;9SB6|rdakL)Jx&*FSw?lZ`F2;)hkaq|ld zEUYjO;uwWbv??x!^t_M0m=^k4EwvN;kZU{ZXN?u(Oc<@7Wj{} zY9r~3k~TIrKE-nk!&tkl>h;|fT^su0I#8G)Q6kqs>{Apqvl$jv_GWrC_&3QQSgEOT zJY^!Ry`;W+g|aWiN+RQ67b|P8z@WHzj?MR`BqyIfHy*=W(3f#LQU*-*SHQ)q@*1X= z@!mXsk}dgOf&JA(msr?gP16B?vl)N&=lS~w5EO8CVny4Sm}FFv4BdZy3&-UoQtVVb z896;bi*CyU4-&L-QO}aVM#Ob^YzGKYOuTFZ=C^i%DrZ_gz>y-`_4X9&UoJFl0VBWdt!na38KNhQt#Lz3 zF?9Y=M7G@eBpogzg?juLd*r18`4yP}MMwD;W-*6B0V9Bis-+HMS*(H(?JQEctiiz|xu_8c31P%|x5 z&!F|-d14MQgkB@{P)7D@f{F-aF&*Z_b~iz13o@P9QjS=2+jn+@aj(PfCBE5Vu40G> z>K+SZsf+8Kz$T4M9IwP8yfUUdJTUA_`gka0i=DTnP86nBY53F$FZwHlBbw#LLL4$< zBM2xLWc8N_!0 zZFE0uCDb72&@C-4=A?w43DkYGUya4;)Q6fcOpq|Rk7K2OR5F5DajOnbrN_PZQ93j` z?1L}KztKr$t-VLEhU9&peDniZcx|@j?d^nW0;RZBjbih-pbuE*^qzSjs9nwWX8(X7 zA(9eiu`w(970n5e5qO#`wm@kacfIiHOd18Z{sLXU&?f_BKZ6&G0K44pkfz40u&N`^ z>a3(xGx05hehMbm=PQ-;VVhraD@Db`Hoi>s_oJt*ukn7Ah58iYUOim++825wL^KeZNGjL8hLR5 z)>TS}=d9L-AVbu_1*$ud9H-(;#Pp&eEV8-F5aokrc#BKc>@!IM6g!oseNl~eAPoBq zSt302S?svPU!_j6%U5<>Su&__ndf0%2*Qg!RtvPt+)L~iG1o;IXl1N^07CB8t95TD z_FjsE7scv-vPQga8^QuSW2Y~j(6RFS`)8KUi$@Gu-QX)(*6-gMnT(2XeaP`H)}Nq> zIdQo5Z|-@U=u(jY;AbOslxJi$A}S^-xmb2xTZL0ytB_SaIeua}Nm&tF zXQt)OCvAssnIq)`&@6`?!^@?qxyaEw3K!2U;|D=osEQ?)^oknBl13a?7>*}dPw1yL zehSNpN1zO4D&+PO3A0X!?-8)=0Y%C-FZ@DG9~BrJQqH}S*9aRaKAw>1cAi`WQ5{!y zU}q8Yr-z=gS=e$ax_h^^f0v4WI2Z;UV30ns>zts}a}p=o^jMft78%EbvAc|}r=W$( zZ>`EoXClDq5VmaM`W3-z+YM(Q_wDGWFKl)I7>52~Uastxj1&_bmv-O4F`% zD7`kG<9rm|$VTb5B(GF3Q(u8y6HB7=-fao0eIf=(apXhCEtMKiHTy9{w(8ld4rQ%=sSPe|X??Bak_Nn|jBU#i z?w(iKI;F8$K~&99+C%>di)#_%8cVl2x(Du(6sC{;^=NfUdS6o3ce8mr^P~nf8ALVy z3MnkLpe^{KTg_gafi^TeateozlE1UXEd54JI<266$hL#8z38=>2VU2)vLioy!x%=I zyHgaBM2sE87Ol+fMRGfIyOWfA4*Ig+XZ3S&5{m2{Z8FwJG)kOJe!0HFB}fOO2{~+if3B>Igc?YsJFNu5an`) zw_KDU(Y^kIDk!c&XTq08si)0bPRI8%@{0A9b|M~T%zJ#nOWsj?%ow_bTk>ynRbS9N z%%3_AVH0{pBtjg1b(1v~`DVzsbY$AG(3h(gnbVa?fT(KezkL&zES*HjrHev*WG?!xo|7 zo6b%Wy9+M$+kFdMvHhJbaxp}&L!H?;UAwg(wo1sC%Q?&MwEhFgYRcYvle4AB#>-cClG7QE6X{G^F+rq!0W+p) z(bc@k-^=vcPRO7(e8ST$EI&pa&h7c-wDsJiRLgj2(z z!s4_OEqM=5dp#%?1hW|LnJaJ_pNmq?PWJm|4ZeJ3-U*s5z}Ne!=S0blg@;vUEdmR!#-4x_~2ZMH`aWo~`m`9_hYI%KP0 z`@-a_*ZSA$RYuldByKwi;tOti^6%`eWc-zP6lPZ5nhU;n!;!mPS^>d3^Y3S`i)bT{ zMTjtCFMiiNz`H+q(T$~f=6pSid!x42ay0`fLiqz1Q^t&Q;O;P)9yo|@MBQNJ>mFSl@+jo}3--~UlJJ}h=h$zW)$DD@FPvEx@0{h>sjWinbY*CAw{*o zhY39{oUn+!M62pJVi0m!u5{sj+<7szcSV4p_(r+#JqXdne%&3utf%MFAhR#c)Ko|E z8SnNJDciEN+#tUVkkjwXlFYgo^?>bfOK#6MS*{XZlDOEqm?dBL*)9d*bMo17?Ijy} z;{yOck-uc$R9MRh4CNrOONdXzpv~3ngr2%K8yVCfQImkZkZh<0w_hKYu)Gz65WbrA zIAu~-OV#_9lB?=e{DR{z2tP5_eHFDSNwb##=RIffcc~$Eg^5?~)w@YmZAr1nJ2?!I zn<0`uBtJ-vjd?UpK4&pZ8$I)Vs3|ga)+R-cz?M%Pa)?Z&+~tdjW!!c!4OS!RaR;W8qC)OE5 z(dVJ4HUAeZ(Qk>5`r00wVBaeK(^`O$ppUiyjgNO+J2P+|5iEyb%S68KTPTT(ra-ua zU$uE%K@^8Wi*h@0pb5||yUVzqU7=e+)$R9p<-q8GD9LXn9qUnCV$a>K);TPm6Eqqg zuo$>IyV^Gv+Bd$;zOwi`tno?|0Rf5WHPdp2S9n;|2CQQgy2X%XM4eI4{ICee=Z`?H zCTX7k@_n$GyUf)~l>kSgB9so*mo@NVn z#v8lKI$x#`d!)OWf2O1oEGan`3R>HR%E!xR){eHib-qtQ$|pQH=y=%t@pI@EOW~C} ziRd`DcB||h*RQd4oG9az(#WUN4Zdscd4#`wgA>K-@({p+x}tBDvD~SS$Rwx#{BZNf zeTMaDq5xB)A(D^dH2tD8bREf*GbwJ5<+TCf(GS9+WO!0{b-^W6k+?$Tu3E4eLGRzR ztCAODe#m@die)rXUaxv!^yV&cBI)lNoC<7l#^b9UTr(C!%hx+EUg>zh7>e9jFypRl zQ@bmO*(uGUd{{={VpYewV1c+RwB;N1ff$V)N9&zSliY#~P_X7QSMGRi%fE^fNw$_O zm-ZX*y`18x>GF-GFnfYU)1tW%x5myO@^ZrO7o1H|?+a*Q(z~epP=1}>lKNwvs|6QE zLp82A=rr=>MM?=4!Svt0564DMElaJ;A%rBvgDhV zymWkWLZ`zXPLh=tNsqkfv#Zpvic0P2OdGw`%!EMMIUSEf&~*9Ne0;mRmaO|TB@r(T z)vTRcaRhj#;-BdDPP1d9qc2sAh#LLXSql6neYIpbG%}hIU;d9C;wJO>yW+CU&lm|^ zOhOQpr5^V`5IhS<-S5J2Z;1T%^VuBccluKy$95CJH$<^nHSRPrBD`lfIai|2j6CJ8 zl6X;`NT;(^8g-VF=jVE36yEFWk>MUFOZ}wCBnr3D%xA47Rzoezg>T&_6 z!|06crS-(|cOIffNUZQ{LXjuQg18iA%~w2k?oLh9-6iDlPRF4RS-T;Z^dzotp(P|W zCh?-Sw5qN!iup>BSKYSq+tAHRyuGvrogZR)vp)0t{kEQYF&Q_L{Pbl5&_UJlE02#-t0k2PnSZ{we&m~s!VM^MAJ z&SDCxt9l((OsF@+rr|FMPa-;7F@D?|p>rSCp<8(p$HX6zcvT(t?yX_1sDbDtm-(|` zzu%m+545z*#e@*Y@xdUFMiS_@YO!b28d{N_ynJ-%*3`aixc6L^c>y1Ney{Rr zRAwzG;={+;yDAt%<1aFX6du`C-MnyBy+dOr!F~P3JvKo^kep$p?$bu_MK;DP7P4CF zz4FDF$W7`$tok4C%YP$aSGrhhXMN=&mP_m3M5Q-Q7s83l!xJgRgxt*BPv!Zf4)kNBr+8ZjyO%t_TRZW7)~5n;tCnh=Jo|U0H_)&$(;FMb!2=*&H2rU(~%J zvR2+F&vH@}q#0K2&LH>3+-_OB*A}i&QA8_KN`#_4eBy!C#N0`cPCNFiQ{{=p=tS^1 zCa*mQhhWuE1C-!7Wfw#{8z?rORk@7KP-|iD(?~}nPZ?;tZ(izn%~DdW`0)uTK~ZbT z;E!jNL`OSsv#jnGzV8*CV#ia$yTC)cnDb@lIZ2ao(UqLVZzXZ>o2iZPtzVOE$Ik3^ zn_uMd+7m!ePK~1>Ui2qzm!fK;wx!Jrcl^f$2>!7!SoC$5w+y2mCKLm%!VIPRcvypC zg(Z?vB)lRp{_4-GP+Pi+3-9ztup!Vk$2lA0;Em+?YIa40WkymyLq=`lG?MLFprdf^3###3`y=NTt`pqF4bpw zwOKm0Wq<>vok_dMYHGgH4G0yfIbtVSKb zEKFIdLIR%ByF7dt!~F@sI!)%M)Flc3!3uWxRgQU#gLy73{QwYg%?IQ%n)h^g2UIO3n5jecpPZ*8LKH+t~2n{Mat)0qh> z?F&?v!_S`%7?T5>O+Cf`x6cZYEs+ik;UVUyeMA>9dERNAMEG6%cw`I5~ zseGIRKAy^nWnBN)lG@Y&7KoEdnvN;Yv8KWIFiQj@e5xVSBy6?)xh94NnT-$-(qxR3Z~r?Fn(Bf0k9JlZT}c;ERyiD zK@yi8YyG}ef$(AS%}(4c;gh6qr%~MOc|=z_q>Rft!;L+av#?6zRAm)` zpfK<1WJc2wB3=K!2FebLb%FC6qIPv-c#Vci0oVo<&Q|o6wu+aTKeouJ-haAVG1aAbD)adh zvCA69xsYct^K1SH$IUfEEhqBzftd8vyP7`H1PNrTcbz}H5_aQ`z;GDK!mKIX;BayN zR!1B8z@_e_rwLoEqR4(KyT;wjnw9lTh{WFBo!N%I7};pAdNwjgQZ%O{f9};smm+NW zUhc|wjOaiodMaMqZ&aiguGtj@F2;q;Ns{MGt;z4byVt`V|M%I-yD3AQK5@-sc8a z=?rec;7jC{d1Q9BOyL_=khyC1x#xbdy`AM}ov-IwMV&ehE+-dH`ohkr(?y)8gd1=9 zlr=D83lj3^?>>%G9#{V5?xAqZc;IVl>ZbN`{uq}Z6lMmX%dQnyI*G^SyO@NCDTlj= zY_wzk71Lt(m$KS6J@FrzE)WU$3+e?N40FaQCj3Gce-U?@eW$6f36+%ftYI-SRozdq zq5T>Xn&ti{^hrn~t;!?dR{eEa8YDVUl<2s|sq`X8i3~wZ5jU+@0?(ouqhvj6!#Q8- zv}Z^sONnxp@XEk$l>f$=?GU~FfS+D5!spf>q)LkRC9J#*}bFBwI>_R99zNwv? z!T3%v!x+;{y!&j(iDCfW{qKMTCU}PFW9l*u(E6uVU7fkU6De6k&xPUNs_p-XUJv`NHDw_#91GZn$J4p-%M{7DP5rM0><~6y?oppQ?oc z7UO8AQPIWa&wc}qDa1jDNL|iAst^Iv4vOM!@58v-wX7%{am4T}nl|)XFh&@@4vCg( z_PtWRF|_ibGVI4)XMnhHlWx89wngK@Tmf}k;9`8buG^H1Lp!eFB`kVpI3p$5o*VPT2Sjxq6Xd40 zPWY0xYZRG|iS5;A<>FJwNo2t+W$iyn&ya`>UtR_SUEi_DGULqQ2rO{}PyQvvrwva` zo_!GhwnZ3wK#nko+=}gZ7y%V_D5@I?wk;?W_4CIrkrwCnM2RG1mn}Pqq~@)0k(~xw ze%d+z+DP`7W8?UIiR}AL=_@46(s&{l>vJw~X+6X94ax4*j?-igp+37h5+zlh*!mnx z0Z%r8j*yXuQY8kDEixoTvlzLZn-xk~L&B?2@k9XkycRh=6>m9pes}N6yC+qZtB<=S zpQe27`|+di$A^~86eGE%CC4ay4st=lD5U6N^%)LxwIHe>0%w^Mi^~) zO>T`HP2+%i;UQ>^0Shon3CJVuT<7=-}TTHp4iT#CQ zVk0SCUR%$nO!S;54qr5PdG=VkI1gEGz5g7hR?em4q$qYmk?%aM+D+GDmNj}Sh^F+d zzInbQKkBEmFqLY_PF=xcebjoT?X4!KCL;!tK9t5lxtB4qn8Q%S;_ppw-czqUJ*V2Z zU@>!db)*^y09w&+>a#z2pPl&aau@`Ci>1GP@kP00k+ ztcrS=npHvx?p4I@y0LCXe^H?@SH__OfqSrEhCtAeSpP_&#YWUomRC-3(Zb2)!>|S) zE;r>~3?N`r!dS!gglVtRGyknnt@1p~s zCjI_NI<=Owe7DANVU@EO`)GklJQM2(d&TJ_(Ll&t${!5`Hj!p{U$xkcEu>y)f}3S zIufY+H8aXK*UDv#*^Mt5oM2M-y1y@O>YDYSOrMO|b>8LePM7tgE?IuK)X}^=a)R{O z4l~`(9xr8`^v+pItG}SVdgpH0k0u}iq4z`NV}Yt^GuqnP?UIH@mvv^6>7-FM z?pt6zt$it~pYLcL#^ZO0(${yj z54^tXBZUiicGY|g0}^CcXbn-uFL}*SdVO)#g@LE$qZK`QL}a`gc{<`R)zA8#CpX4a z&MMKpzgS-*Ntr~n?D%%#YmB$z@gu7JF&U%z)FXASy=T#r0^^2zYWj<50{0XPH65L* zD|@wb8=Jj6)wB4xlKA)U7g1C^M>JMN6qKtIbMuYTLYvzG)F}FsL*}{KomQV)>Bgg- zU3*mY(IhQ&;L3-c_#{~=HldBu+eXt5w5F8egZF2%$+u1n%bZ$E10p1y^f2(7O6v`o z(2`{BQJ zr?B+ODex*K?rna3@3V}93cKx;9))>yT`&2;&594cb#Eb#H+KA-8YR=j=*CpNnSk8o5{IG%$mX`kdHDsN6T& zE?VxidVa^7CFm>0Bw`4ekL2G4Y@rV*htaN+iEApoJVg3m-)7qSVe4$?msd-qg2+^M z3yoIU>xIb3NLl3bXOI4G8-x0%r>6lhRK!gyAu0oTeR5HA1^4ZUc1K&1L2>qBYbdr^ z)70DalJ6D?BA_bJj5+nRLPIy?8+Q=~HU95Qf*k*Qb?%c(=2*37FVp9KV`1Q-JslxDW8~Qsmz42N@CUJ0a*;!eU&aSSK zrFLSe8u$Z7JPy{)1ux*K1^oGHcf7a$Ix%qq#*zJVRHGt$%wO-2v)xQMWf=bhuJ-Le zpCLtm2DlF+PLMC&K)+-YfpOV)7N~0p!QA@RKIjwexnlP{glcQPrNUxcbjs z#0bQ~q(;B6K!(?f{)v$({m(^X{Jns66a9NDJ4p!oD=;KOF)pKj{mQMA3~NN>lZYa-B@_4$uB@z*mx(^Ywm;Qq;ALqCPDPwA>5JIQ@g8FRtCY(>x) zRAFL@MTq|Fzb@#%PvHN{Jwg8Kru^R?X8Hb@SA~Jjmu>pPRwH z?0X-`qGr|A)&Kqud|3V~+w*)aCnNu7cL8kxPyiXA+}HzS33wp?+8D41O$l6^_9s9h z4@lEa6G)lEKL;j&1DmoC(*NsiPyTDI)-k3O7U;p4!y7mH;P}!G3yc;EiXPDES34gp z4ztN;fwFV!w@oZTH2Fg^!P=PWN=YfcI+!Ojd)F*JWj8 z?3sbT8-F%)TO!0DX(s@v`*902OASgM|3Fh>CBv`u)nvii*Y^be8bPl(x&Qg^q?Y7u zDl^{)z;oO%L^&o%Vxi;>4U}T3paHfP1A%Rz0(-ZNfQt9S;Yym+*f_h z6x7ZZRMWKJ$9#=SP+2BY3IJ$SatQ5EhhHp*Un&v^1>51dxgg zoZ(jZZO^qAvirliO6YAC&wyT+#row_;8@iTz+XK)F!-XwsK1XE-T^T-6n%m!jzxlo zUf&p6J$c;u`Ok`$Y$@Ftz1i_?9hiJN?b>qxW97m%baIuV$pC^p`0YLEi3r`cZOKEw z`Je*_N}xbTb*r>RH6F|bRQfd}yld*|1*)0%8(r8qdY(eFwR8TZX3+MNhxHRgu@c7n z<>vx^-XSKinp=T+t=zgr=w&;9y6v;!0oo%#ndA2I@k|Dt&^wRdwx}wJ{M) zPBaGvpbei*1cGt-Zxo1-aPo7>alKDTEid*+jOlqZuny7dBeiY@pP3yzU z+CqZ?_(s>G%S@6Ie0&-V8n8uu=7W4D@B5^@L5@Zwy2LacsU8Q-8q=CLMUWv9^-&{zNLV1F|ZSW^mu0IwJlj_ zfWRJB($r(sF7Ch8ceanBGX$a+&CibvK)rQ5`>0&DPONYFqsqd@kmjI(o~gkZh-)l^ zjOqOXQ()A=Ss9>aKpzkI`*N&nc5nQ;W-W`@9^R(K>f_tw>QPiw^fyZmQ$>RHcV;Av zj!G+=wM%L%XT=Q|GI4FnPYM40nH_)bxy0|t<)X3;FX*)|>1qBmzE79#q}Lao{Dsvo{omL!cW!ArJFmohU+6=|G>rFJZ77qOMlGYZ%zgvNR(S zHAV_|kMYgYHripyan(#EaR;m+qiP})AI^;OXSoo)5};)(jt_7Vfi~&Q$1e5Dl>NF2 z3lKb|^?_`=AaGC7PvXgw6toPq4;B|k%@U| zAzm};`GF0{_O@&tOQ$6|;F0k1%`F3zM z1b_CVUyh#{EnMM?VH>G)x9Uw(PZxb~J8BCZE~s3BaIiwD)4vZL6-I^qmm7uO|A`to z)D901K^+!Ws-3m9HQN5o#wLO{MSbCWfYk0XR-~n>8q>I51}3_b${ZT_@~H4kzXF6c+XLB!RHp&|qQDA4jcQT0I13oRi&3m)Ls>x&9WvU(#QN7Q+zZ1)vH&jss7*EaMVYG5QalAN3j`}?1Dqy(8sewRdbPw=CJA}%^As!3RLu(=`PsCAkg@GH7uSS6h; z0$nsdH z=)2U)BIP5cY?+ytN(zA*B|<~vfVrIbzqfbFY@R0c#tiiLqnM(XQ%agksGYt;3tf^7 zGy*`=SJp*N#_2nm0o%8ize7>Mdwrx69r?~IFgLfInysi2&XUS@T#HWwKe;5tCO!oAM6Ul$9rM{Fn)0V@MyEr%F^MeY4W&Zl>2 zCpvJ686UD$+y(&SG1!FE=reJUWE!ky6mdyEG_7gClp4P-WziY} z3*2EV<$Mn8Me0j4L`ddz@`Qd9=1bQQO{1I+7Nhy*9`*7Z%y>MX3-`+Xk`U}^KmWaX zmXhK8tPBDLjI5K!k_ZbMPi9wX@P7%>@F_dzvds0!Aze1U3#8ym*NYOetf=PiRdNjbVIw~RIRJ4Kci%z z==JOJ!|q@}e>NX)LnjO@@;pC>I0#K!b_c|v>i~NEyr4@hyaxQF)bWq%XV0EdaG!a; zJplVKu2gLTBPq_(GBeG=?LgWBqC${znG941AFUVm^Tk2)VcVg*I1XnN0^L!oQ5mUY zkl~Gk`2?aF>^2dH5qTqb0AN6Aq2+Mp^TzVre~1NX35QJnYuWHtE=Gt1#~zI}LHJz2 zj~a<*=IO=`7MzEm$?)fHG)3ashG8{Oq-RvSx8SaU| z<|w2YHtz_RP^rPaqL<_50_gJu*rx{bi>py7TK&j4)>xP)yQEoV%#ny1=GxAMNeXQ? z=!QjDlq*k*|EYgIZ*r$fMoy!)>Nm*^yQy+vml;ruP%n=Hf7C#y)>!4OI$F+CXee?C z4a2Z{=p{)*0B2GuGVA%;%xN4c*>5A@E0TLQ#%OI83(tw`6!c^VSx>cZ{@ z_i|XrM5E%O`u~uV`-8NB`?Pay@wCU0P_I?xg#@>V%QQLiNV!Du`9}Z7cwZBk!IEO* zxJR{z?NaB5gDTZMFIMhA72pKSvz(K%!Xp%wuUWfo^8j}>h{d|QyBEL>MLp4wJhcAk zlg&|)P)5{wzljSw?W|;Tiok+n4y5Qm*C=uPGKqUpX29y4fKZ&3(?ufVDwI*2Y-|P4 zDziC(lnXd>03xU$o4IX%r$a0NK zp|st!#2KCCLeC(y;nhZItRn`M;b&VS^MSbruSN=f z^gYsX_Q8V(`VW2>mHL+2hRb$q?R@15BbNLt{k7}+ck|N?gT$rBhK=6kMpZ4RTdw+t zHDb;`&;731z|LiEN^&tiDDNFwn$Kj|{ZYamTpg)(Yt!JIXDf6k>L; z+Pzuu$8zg8j5Dc%I}qZOKZaF>^#wUUKuYEK`?kSC42-L*ih6oOyJ1aDO$4L=-IwE! zt`COk1h3~Okv|uC_~qB}{P`T$Esv#&l8?NnK&f}VT~Yl0_%gkyrh)-QtlGXdzez$L zgx|)Ug<~1<@p)uho&w=N{NU;1ym&PCQ=Wc&~ycn7t3Y8~|oG~!NjC+(a~4GDv&{m-AXjcA>H$BWwcbf7(~@2Xc}UY>os zA*3E)N$7+e$;rr;T*MqXM_2L)l^Z`4*#5kVz^Y;ZB*l9Kuyybk8=_VJ2|MGo@mgUq zYTopS3w-~QMX7|+`g~Htzo$TU2vR0+6_vD=%XLd40sqNTd5U}+ORG6rCs{-_NsckVC^ygAtk+!8x)DbIV9kT#_-cKqo0Wwg}! zkyhK9-t#+1usEdpLLco58{NKzH9%tWkk|SqI;_5wnby2kPwr8AMTwDIweSt9>8J zEVbo*d|b=JK0+7E=2)@SxoW&SfDP+j;2e3E(rolodbf4rh00>YV3O0W)dz$0{qZ%} z(hnb&{eI?nj7A&nAcxf3GU)CGg>WF$mzvh6pN~@1&u^V)b`murr-gRUmsiivxxJX4 z)$=%i6``B>QLPy`r3;GWsPGTq4l{%Pr0nl56K?|~WyE6%{Z*P1dhJo1yN;#$n}%j) zZ-{6!!LftC8?)E@l?sR=Mr(B`ek+9E+ZQSP(=vPSv_clZ2$SNiq>lSxlYC-%zDE8v z`0Vw)bc7BnyB{2^d^j^h?uG(A$f|R!wB^6R)}tvW^0z0 zxPs3`&FLcV+hc6=o)12nnR}SPjdC<7MU2aTpP&9fg44uy7uH`4T|FipdK@vIpZeYg+=UQk?--|P`b~} z%q-(zn$J_sHMUw@h`dP^^V)#YPyb)YI2eT=CMO!9Ts$^&BZMAK#N2loH$DZQat78@ z%k_IywQ%3~05zo|}nP&YaulHl^gI2+A8v#wBna}!hsXav%6IX|Wf%t9Aw8v94I77-GRrvt6l z3GuMp$DMg^3k${_+FZ?8IXgROVY=gA^Ym=X6`_;xtqLj!ttCA`yRZUx78NpRK$XFK zmhZ!_Hi%7r$Ur7LF6f0-P87Qfm4HohKY+U+CZnHIjRQ# zdrv-$nnM6s1B<5vv2{#L3@BUr8Mdd^6*yF6Ok_R@59hSfg;7foxu!Nxj8oW^2bPN1 z7i5XpCk^aTzVThW)a(AUhAoFO>dP0yUiS>d8w^f=3`9Kphb4;TXFfCT5ypZwmubr( zXm^y+#H#Jd{_@eMgQ$5o{H25I;Qa4J0q$d>%Rh%p#|O$IsCZ^7vOUZWf3$_qL^jG1 z1^PH@R8a!`9`jgHN3-?e+z1elXM>dS#&MNJYAC+sZw7dpZr@h7*_D61))i=j%vr_F zt}F>4Nn$Z<-lMO1Lh_sjlWx6xGaqGByLL+bqN+|%)w zCZ`!;Eb0Bto}#;|ckM045y#GBaxUBMuR6`a-mf>Kgnz;iK!C?6Y^;Thi!IAu63iV{ z1Ra9^q|KZCx?P-jYuQ@J%IiNFH+OiUw0S_MN=xFBWIH5K#;gg6iTZ1DPCpwqdG#vF zMl>fsa=G>ayf{%T`Y-_*FEMZ#{;f}7M)FUVK7JX=>&lYFbujl1mTq|KILqPe_ZpH!M=Da#=^CK2jIv=xyK=?D;1RP2KxF`VMfy3yEj{w2;r^1 zbZEE?jjS_4`Ujw@Gzm-1>k z+97(Jw#8@0Db5_ znHVGYQ9Fm2IgVCnqhDk`CNeS^lp4WpLh$O0PBA)VaHyp@ks{4(Sx5n4RASL~S1es= zBS71>LjW#~+kwx3#K1#ZIz+5O6a}RqbWsxIZlQvHGw2E#*-tRHU^xNC^jsVI21O)v z9Dq*I$#LP%ndNanK0gV19iYJR7NkPZMF~IRB~UOq%uh0-y=ft>udj~)vI(wb0t^K# zJb*SKzbktBP?1hD&=*|mzmA4U9YV!p^bYh*)c}bph6;_hHvo~<89y;AwDY6Pz<@a& z6ho~Ssv_)asWo7YAjXfQ5m~y&;rTJTLHK&T)(=81~}i0T}`7Zi%?nDyLZv^n)c^Uj}JgYRj&lm zErz`h*#s~eZsxLHzkZokun5rpv$M0*F+eoxF3_T%)$)ID{c9{NJXtgUrY&`I-Jz&n z`oUS9M!o@(xJvDkQ5YDQkI_@zAp`hh^;SY>+ynq(dA|>Uw?7%~(R?;a@Q!yI@ zi6k)Uu`rSmYHa9d|4-@aLn#O*p+PILM~@!;bNx1}=Wqwl&sBuDZo#`QqMryhMj%xL VN`^b~A8v@Ug1Y>L3R#P={|BjAndSfh delta 56079 zcmb5WbyQYc8wUs|A(GM^g3=)^0@5uA(j9_ycN~!}=}@|*yHi@aLAtxUna#cTn^q92PE+*QOCI_K#0g||WO=0J?x1e&8b|LM@z2UN^GnBi zM+GP;M>s>_vzRF!c>Fv`I>z&k8pwSxgpjU2OZo_w*u&OIxWh>P(y`tgO8dcP%

I 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 diff --git a/sentience/browser.py b/sentience/browser.py index 5670bdb..eec1888 100644 --- a/sentience/browser.py +++ b/sentience/browser.py @@ -11,6 +11,7 @@ from playwright.sync_api import BrowserContext, Page, Playwright, sync_playwright +from sentience._extension_loader import find_extension_path from sentience.models import ProxyConfig, StorageState # Import stealth for bot evasion (optional - graceful fallback if not available) @@ -156,28 +157,8 @@ def _parse_proxy(self, proxy_string: str) -> ProxyConfig | None: def start(self) -> None: """Launch browser with extension loaded""" - # Get extension source path (relative to project root/package) - # Handle both development (src/) and installed package cases - - # 1. Try relative to this file (installed package structure) - # sentience/browser.py -> sentience/extension/ - package_ext_path = Path(__file__).parent / "extension" - - # 2. Try development root (if running from source repo) - # sentience/browser.py -> ../sentience-chrome - dev_ext_path = Path(__file__).parent.parent.parent / "sentience-chrome" - - if package_ext_path.exists() and (package_ext_path / "manifest.json").exists(): - extension_source = package_ext_path - elif dev_ext_path.exists() and (dev_ext_path / "manifest.json").exists(): - extension_source = dev_ext_path - else: - raise FileNotFoundError( - f"Extension not found. Checked:\n" - f"1. {package_ext_path}\n" - f"2. {dev_ext_path}\n" - "Make sure the extension is built and 'sentience/extension' directory exists." - ) + # Get extension source path using shared utility + extension_source = find_extension_path() # Create temporary extension bundle # We copy it to a temp dir to avoid file locking issues and ensure clean state From b8b6b533cfbdb7ba9bed4334f84af3d8a3af8a97 Mon Sep 17 00:00:00 2001 From: rcholic Date: Wed, 31 Dec 2025 17:12:50 -0800 Subject: [PATCH 2/6] async api --- sentience/_extension_loader.py | 41 ++ sentience/async_api.py | 1149 ++++++++++++++++++++++++++++++++ tests/test_async_api.py | 271 ++++++++ 3 files changed, 1461 insertions(+) create mode 100644 sentience/_extension_loader.py create mode 100644 sentience/async_api.py create mode 100644 tests/test_async_api.py diff --git a/sentience/_extension_loader.py b/sentience/_extension_loader.py new file mode 100644 index 0000000..58bd873 --- /dev/null +++ b/sentience/_extension_loader.py @@ -0,0 +1,41 @@ +""" +Shared extension loading logic for sync and async implementations +""" + +from pathlib import Path + + +def find_extension_path() -> Path: + """ + Find Sentience extension directory (shared logic for sync and async). + + Checks multiple locations: + 1. sentience/extension/ (installed package) + 2. ../sentience-chrome (development/monorepo) + + Returns: + Path to extension directory + + Raises: + FileNotFoundError: If extension not found in any location + """ + # 1. Try relative to this file (installed package structure) + # sentience/_extension_loader.py -> sentience/extension/ + package_ext_path = Path(__file__).parent / "extension" + + # 2. Try development root (if running from source repo) + # sentience/_extension_loader.py -> ../sentience-chrome + dev_ext_path = Path(__file__).parent.parent.parent / "sentience-chrome" + + if package_ext_path.exists() and (package_ext_path / "manifest.json").exists(): + return package_ext_path + elif dev_ext_path.exists() and (dev_ext_path / "manifest.json").exists(): + return dev_ext_path + else: + raise FileNotFoundError( + f"Extension not found. Checked:\n" + f"1. {package_ext_path}\n" + f"2. {dev_ext_path}\n" + "Make sure the extension is built and 'sentience/extension' directory exists." + ) + diff --git a/sentience/async_api.py b/sentience/async_api.py new file mode 100644 index 0000000..8dad11b --- /dev/null +++ b/sentience/async_api.py @@ -0,0 +1,1149 @@ +""" +Async API for Sentience SDK - Use this in asyncio contexts + +This module provides async versions of all Sentience SDK functions. +Use AsyncSentienceBrowser when working with async/await code. +""" + +import asyncio +import base64 +import os +import shutil +import tempfile +import time +from pathlib import Path +from typing import Any, Optional +from urllib.parse import urlparse + +from playwright.async_api import BrowserContext, Page, Playwright, async_playwright + +from sentience._extension_loader import find_extension_path +from sentience.models import ( + ActionResult, + BBox, + Element, + ProxyConfig, + Snapshot, + SnapshotOptions, + StorageState, + WaitResult, +) + +# Import stealth for bot evasion (optional - graceful fallback if not available) +try: + from playwright_stealth import stealth_async + + STEALTH_AVAILABLE = True +except ImportError: + STEALTH_AVAILABLE = False + + +class AsyncSentienceBrowser: + """Async version of SentienceBrowser for use in asyncio contexts.""" + + def __init__( + self, + api_key: str | None = None, + api_url: str | None = None, + headless: bool | None = None, + proxy: str | None = None, + user_data_dir: str | Path | None = None, + storage_state: str | Path | StorageState | dict | None = None, + record_video_dir: str | Path | None = None, + record_video_size: dict[str, int] | None = None, + viewport: dict[str, int] | None = None, + ): + """ + Initialize Async Sentience browser + + Args: + api_key: Optional API key for server-side processing (Pro/Enterprise tiers) + If None, uses free tier (local extension only) + api_url: Server URL for API calls (defaults to https://api.sentienceapi.com if api_key provided) + headless: Whether to run in headless mode. If None, defaults to True in CI, False otherwise + proxy: Optional proxy server URL (e.g., 'http://user:pass@proxy.example.com:8080') + user_data_dir: Optional path to user data directory for persistent sessions + storage_state: Optional storage state to inject (cookies + localStorage) + record_video_dir: Optional directory path to save video recordings + record_video_size: Optional video resolution as dict with 'width' and 'height' keys + viewport: Optional viewport size as dict with 'width' and 'height' keys. + Defaults to {"width": 1280, "height": 800} + """ + self.api_key = api_key + # Only set api_url if api_key is provided, otherwise None (free tier) + if self.api_key and not api_url: + self.api_url = "https://api.sentienceapi.com" + else: + self.api_url = api_url + + # Determine headless mode + if headless is None: + # Default to False for local dev, True for CI + self.headless = os.environ.get("CI", "").lower() == "true" + else: + self.headless = headless + + # Support proxy from argument or environment variable + self.proxy = proxy or os.environ.get("SENTIENCE_PROXY") + + # Auth injection support + self.user_data_dir = user_data_dir + self.storage_state = storage_state + + # Video recording support + self.record_video_dir = record_video_dir + self.record_video_size = record_video_size or {"width": 1280, "height": 800} + + # Viewport configuration + self.viewport = viewport or {"width": 1280, "height": 800} + + self.playwright: Playwright | None = None + self.context: BrowserContext | None = None + self.page: Page | None = None + self._extension_path: str | None = None + + def _parse_proxy(self, proxy_string: str) -> ProxyConfig | None: + """ + Parse proxy connection string into ProxyConfig. + + Args: + proxy_string: Proxy URL (e.g., 'http://user:pass@proxy.example.com:8080') + + Returns: + ProxyConfig object or None if invalid + """ + if not proxy_string: + return None + + try: + parsed = urlparse(proxy_string) + + # Validate scheme + if parsed.scheme not in ("http", "https", "socks5"): + print(f"āš ļø [Sentience] Unsupported proxy scheme: {parsed.scheme}") + print(" Supported: http, https, socks5") + return None + + # Validate host and port + if not parsed.hostname or not parsed.port: + print("āš ļø [Sentience] Proxy URL must include hostname and port") + print(" Expected format: http://username:password@host:port") + return None + + # Build server URL + server = f"{parsed.scheme}://{parsed.hostname}:{parsed.port}" + + # Create ProxyConfig with optional credentials + return ProxyConfig( + server=server, + username=parsed.username if parsed.username else None, + password=parsed.password if parsed.password else None, + ) + + except Exception as e: + print(f"āš ļø [Sentience] Invalid proxy configuration: {e}") + print(" Expected format: http://username:password@host:port") + return None + + async def start(self) -> None: + """Launch browser with extension loaded (async)""" + # Get extension source path using shared utility + extension_source = find_extension_path() + + # Create temporary extension bundle + self._extension_path = tempfile.mkdtemp(prefix="sentience-ext-") + shutil.copytree(extension_source, self._extension_path, dirs_exist_ok=True) + + self.playwright = await async_playwright().start() + + # Build launch arguments + args = [ + f"--disable-extensions-except={self._extension_path}", + f"--load-extension={self._extension_path}", + "--disable-blink-features=AutomationControlled", + "--no-sandbox", + "--disable-infobars", + "--disable-features=WebRtcHideLocalIpsWithMdns", + "--force-webrtc-ip-handling-policy=disable_non_proxied_udp", + ] + + if self.headless: + args.append("--headless=new") + + # Parse proxy configuration if provided + proxy_config = self._parse_proxy(self.proxy) if self.proxy else None + + # Handle User Data Directory + if self.user_data_dir: + user_data_dir = str(self.user_data_dir) + Path(user_data_dir).mkdir(parents=True, exist_ok=True) + else: + user_data_dir = "" + + # Build launch_persistent_context parameters + launch_params = { + "user_data_dir": user_data_dir, + "headless": False, + "args": args, + "viewport": self.viewport, + "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36", + } + + # Add proxy if configured + if proxy_config: + launch_params["proxy"] = proxy_config.to_playwright_dict() + launch_params["ignore_https_errors"] = True + print(f"🌐 [Sentience] Using proxy: {proxy_config.server}") + + # Add video recording if configured + if self.record_video_dir: + video_dir = Path(self.record_video_dir) + video_dir.mkdir(parents=True, exist_ok=True) + launch_params["record_video_dir"] = str(video_dir) + launch_params["record_video_size"] = self.record_video_size + print(f"šŸŽ„ [Sentience] Recording video to: {video_dir}") + print( + f" Resolution: {self.record_video_size['width']}x{self.record_video_size['height']}" + ) + + # Launch persistent context + self.context = await self.playwright.chromium.launch_persistent_context(**launch_params) + + self.page = self.context.pages[0] if self.context.pages else await self.context.new_page() + + # Inject storage state if provided + if self.storage_state: + await self._inject_storage_state(self.storage_state) + + # Apply stealth if available + if STEALTH_AVAILABLE: + await stealth_async(self.page) + + # Wait a moment for extension to initialize + await asyncio.sleep(0.5) + + async def goto(self, url: str) -> None: + """Navigate to a URL and ensure extension is ready (async)""" + if not self.page: + raise RuntimeError("Browser not started. Call await start() first.") + + await self.page.goto(url, wait_until="domcontentloaded") + + # Wait for extension to be ready + if not await self._wait_for_extension(): + try: + diag = await self.page.evaluate( + """() => ({ + sentience_defined: typeof window.sentience !== 'undefined', + registry_defined: typeof window.sentience_registry !== 'undefined', + snapshot_defined: window.sentience && typeof window.sentience.snapshot === 'function', + extension_id: document.documentElement.dataset.sentienceExtensionId || 'not set', + url: window.location.href + })""" + ) + except Exception as e: + diag = f"Failed to get diagnostics: {str(e)}" + + raise RuntimeError( + "Extension failed to load after navigation. Make sure:\n" + "1. Extension is built (cd sentience-chrome && ./build.sh)\n" + "2. All files are present (manifest.json, content.js, injected_api.js, pkg/)\n" + "3. Check browser console for errors (run with headless=False to see console)\n" + f"4. Extension path: {self._extension_path}\n" + f"5. Diagnostic info: {diag}" + ) + + async def _inject_storage_state( + self, storage_state: str | Path | StorageState | dict + ) -> None: + """Inject storage state (cookies + localStorage) into browser context (async)""" + import json + + # Load storage state + if isinstance(storage_state, (str, Path)): + with open(storage_state, encoding="utf-8") as f: + state_dict = json.load(f) + state = StorageState.from_dict(state_dict) + elif isinstance(storage_state, StorageState): + state = storage_state + elif isinstance(storage_state, dict): + state = StorageState.from_dict(storage_state) + else: + raise ValueError( + f"Invalid storage_state type: {type(storage_state)}. " + "Expected str, Path, StorageState, or dict." + ) + + # Inject cookies + if state.cookies: + playwright_cookies = [] + for cookie in state.cookies: + cookie_dict = cookie.model_dump() + playwright_cookie = { + "name": cookie_dict["name"], + "value": cookie_dict["value"], + "domain": cookie_dict["domain"], + "path": cookie_dict["path"], + } + if cookie_dict.get("expires"): + playwright_cookie["expires"] = cookie_dict["expires"] + if cookie_dict.get("httpOnly"): + playwright_cookie["httpOnly"] = cookie_dict["httpOnly"] + if cookie_dict.get("secure"): + playwright_cookie["secure"] = cookie_dict["secure"] + if cookie_dict.get("sameSite"): + playwright_cookie["sameSite"] = cookie_dict["sameSite"] + playwright_cookies.append(playwright_cookie) + + await self.context.add_cookies(playwright_cookies) + print(f"āœ… [Sentience] Injected {len(state.cookies)} cookie(s)") + + # Inject LocalStorage + if state.origins: + for origin_data in state.origins: + origin = origin_data.origin + if not origin: + continue + + try: + await self.page.goto(origin, wait_until="domcontentloaded", timeout=10000) + + if origin_data.localStorage: + localStorage_dict = { + item.name: item.value for item in origin_data.localStorage + } + await self.page.evaluate( + """(localStorage_data) => { + for (const [key, value] of Object.entries(localStorage_data)) { + localStorage.setItem(key, value); + } + }""", + localStorage_dict, + ) + print( + f"āœ… [Sentience] Injected {len(origin_data.localStorage)} localStorage item(s) for {origin}" + ) + except Exception as e: + print(f"āš ļø [Sentience] Failed to inject localStorage for {origin}: {e}") + + async def _wait_for_extension(self, timeout_sec: float = 5.0) -> bool: + """Poll for window.sentience to be available (async)""" + start_time = time.time() + last_error = None + + while time.time() - start_time < timeout_sec: + try: + result = await self.page.evaluate( + """() => { + if (typeof window.sentience === 'undefined') { + return { ready: false, reason: 'window.sentience undefined' }; + } + if (window.sentience._wasmModule === null) { + return { ready: false, reason: 'WASM module not fully loaded' }; + } + return { ready: true }; + } + """ + ) + + if isinstance(result, dict): + if result.get("ready"): + return True + last_error = result.get("reason", "Unknown error") + except Exception as e: + last_error = f"Evaluation error: {str(e)}" + + await asyncio.sleep(0.3) + + if last_error: + import warnings + + warnings.warn(f"Extension wait timeout. Last status: {last_error}") + + return False + + async def close(self, output_path: str | Path | None = None) -> str | None: + """ + Close browser and cleanup (async) + + Args: + output_path: Optional path to rename the video file to + + Returns: + Path to video file if recording was enabled, None otherwise + """ + temp_video_path = None + + if self.record_video_dir: + try: + if self.page and self.page.video: + temp_video_path = await self.page.video.path() + elif self.context: + for page in self.context.pages: + if page.video: + temp_video_path = await page.video.path() + break + except Exception: + pass + + if self.context: + await self.context.close() + self.context = None + + if self.playwright: + await self.playwright.stop() + self.playwright = None + + if self._extension_path and os.path.exists(self._extension_path): + shutil.rmtree(self._extension_path) + + # Clear page reference after closing context + self.page = None + + final_path = temp_video_path + if temp_video_path and output_path and os.path.exists(temp_video_path): + try: + output_path = str(output_path) + Path(output_path).parent.mkdir(parents=True, exist_ok=True) + shutil.move(temp_video_path, output_path) + final_path = output_path + except Exception as e: + import warnings + + warnings.warn(f"Failed to rename video file: {e}") + final_path = temp_video_path + + return final_path + + async def __aenter__(self): + """Async context manager entry""" + await self.start() + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + """Async context manager exit""" + await self.close() + + @classmethod + async def from_existing( + cls, + context: BrowserContext, + api_key: str | None = None, + api_url: str | None = None, + ) -> "AsyncSentienceBrowser": + """ + Create AsyncSentienceBrowser from an existing Playwright BrowserContext. + + Args: + context: Existing Playwright BrowserContext + api_key: Optional API key for server-side processing + api_url: Optional API URL + + Returns: + AsyncSentienceBrowser instance configured to use the existing context + """ + instance = cls(api_key=api_key, api_url=api_url) + instance.context = context + pages = context.pages + instance.page = pages[0] if pages else await context.new_page() + + # Apply stealth if available + if STEALTH_AVAILABLE: + await stealth_async(instance.page) + + # Wait for extension to be ready + await asyncio.sleep(0.5) + + return instance + + @classmethod + async def from_page( + cls, + page: Page, + api_key: str | None = None, + api_url: str | None = None, + ) -> "AsyncSentienceBrowser": + """ + Create AsyncSentienceBrowser from an existing Playwright Page. + + Args: + page: Existing Playwright Page + api_key: Optional API key for server-side processing + api_url: Optional API URL + + Returns: + AsyncSentienceBrowser instance configured to use the existing page + """ + instance = cls(api_key=api_key, api_url=api_url) + instance.page = page + instance.context = page.context + + # Apply stealth if available + if STEALTH_AVAILABLE: + await stealth_async(instance.page) + + # Wait for extension to be ready + await asyncio.sleep(0.5) + + return instance + + +# ========== Async Snapshot Functions ========== + + +async def snapshot( + browser: AsyncSentienceBrowser, + options: SnapshotOptions | None = None, +) -> Snapshot: + """ + Take a snapshot of the current page (async) + + Args: + browser: AsyncSentienceBrowser instance + options: Snapshot options (screenshot, limit, filter, etc.) + If None, uses default options. + + Returns: + Snapshot object + + Example: + # Basic snapshot with defaults + snap = await snapshot(browser) + + # With options + snap = await snapshot(browser, SnapshotOptions( + screenshot=True, + limit=100, + show_overlay=True + )) + """ + # Use default options if none provided + if options is None: + options = SnapshotOptions() + + # Determine if we should use server-side API + should_use_api = ( + options.use_api if options.use_api is not None else (browser.api_key is not None) + ) + + if should_use_api and browser.api_key: + # Use server-side API (Pro/Enterprise tier) + return await _snapshot_via_api(browser, options) + else: + # Use local extension (Free tier) + return await _snapshot_via_extension(browser, options) + + +async def _snapshot_via_extension( + browser: AsyncSentienceBrowser, + options: SnapshotOptions, +) -> Snapshot: + """Take snapshot using local extension (Free tier) - async""" + if not browser.page: + raise RuntimeError("Browser not started. Call await browser.start() first.") + + # Wait for extension injection to complete + try: + await browser.page.wait_for_function( + "typeof window.sentience !== 'undefined'", + timeout=5000, + ) + except Exception as e: + try: + diag = await browser.page.evaluate( + """() => ({ + sentience_defined: typeof window.sentience !== 'undefined', + extension_id: document.documentElement.dataset.sentienceExtensionId || 'not set', + url: window.location.href + })""" + ) + except Exception: + diag = {"error": "Could not gather diagnostics"} + + raise RuntimeError( + f"Sentience extension failed to inject window.sentience API. " + f"Is the extension loaded? Diagnostics: {diag}" + ) from e + + # Build options dict for extension API + ext_options: dict[str, Any] = {} + if options.screenshot is not False: + ext_options["screenshot"] = options.screenshot + if options.limit != 50: + ext_options["limit"] = options.limit + if options.filter is not None: + ext_options["filter"] = ( + options.filter.model_dump() if hasattr(options.filter, "model_dump") else options.filter + ) + + # Call extension API + result = await browser.page.evaluate( + """ + (options) => { + return window.sentience.snapshot(options); + } + """, + ext_options, + ) + + # Save trace if requested + if options.save_trace: + from sentience.snapshot import _save_trace_to_file + + _save_trace_to_file(result.get("raw_elements", []), options.trace_path) + + # Show visual overlay if requested + if options.show_overlay: + raw_elements = result.get("raw_elements", []) + if raw_elements: + await browser.page.evaluate( + """ + (elements) => { + if (window.sentience && window.sentience.showOverlay) { + window.sentience.showOverlay(elements, null); + } + } + """, + raw_elements, + ) + + # Validate and parse with Pydantic + snapshot_obj = Snapshot(**result) + return snapshot_obj + + +async def _snapshot_via_api( + browser: AsyncSentienceBrowser, + options: SnapshotOptions, +) -> Snapshot: + """Take snapshot using server-side API (Pro/Enterprise tier) - async""" + if not browser.page: + raise RuntimeError("Browser not started. Call await browser.start() first.") + + if not browser.api_key: + raise ValueError("API key required for server-side processing") + + if not browser.api_url: + raise ValueError("API URL required for server-side processing") + + # Wait for extension injection + try: + await browser.page.wait_for_function("typeof window.sentience !== 'undefined'", timeout=5000) + except Exception as e: + raise RuntimeError( + "Sentience extension failed to inject. Cannot collect raw data for API processing." + ) from e + + # Step 1: Get raw data from local extension + raw_options: dict[str, any] = {} + if options.screenshot is not False: + raw_options["screenshot"] = options.screenshot + + raw_result = await browser.page.evaluate( + """ + (options) => { + return window.sentience.snapshot(options); + } + """, + raw_options, + ) + + # Save trace if requested + if options.save_trace: + from sentience.snapshot import _save_trace_to_file + + _save_trace_to_file(raw_result.get("raw_elements", []), options.trace_path) + + # Step 2: Send to server for smart ranking/filtering + import json + + from sentience.snapshot import MAX_PAYLOAD_BYTES + + payload = { + "raw_elements": raw_result.get("raw_elements", []), + "url": raw_result.get("url", ""), + "viewport": raw_result.get("viewport"), + "goal": options.goal, + "options": { + "limit": options.limit, + "filter": options.filter.model_dump() if options.filter else None, + }, + } + + # Check payload size + payload_json = json.dumps(payload) + payload_size = len(payload_json.encode("utf-8")) + if payload_size > MAX_PAYLOAD_BYTES: + raise ValueError( + f"Payload size ({payload_size / 1024 / 1024:.2f}MB) exceeds server limit " + f"({MAX_PAYLOAD_BYTES / 1024 / 1024:.0f}MB). " + f"Try reducing the number of elements on the page or filtering elements." + ) + + headers = { + "Authorization": f"Bearer {browser.api_key}", + "Content-Type": "application/json", + } + + try: + import aiohttp + + async with aiohttp.ClientSession() as session: + async with session.post( + f"{browser.api_url}/v1/snapshot", + data=payload_json, + headers=headers, + timeout=aiohttp.ClientTimeout(total=30), + ) as response: + response.raise_for_status() + api_result = await response.json() + + # Merge API result with local data + snapshot_data = { + "status": api_result.get("status", "success"), + "timestamp": api_result.get("timestamp"), + "url": api_result.get("url", raw_result.get("url", "")), + "viewport": api_result.get("viewport", raw_result.get("viewport")), + "elements": api_result.get("elements", []), + "screenshot": raw_result.get("screenshot"), + "screenshot_format": raw_result.get("screenshot_format"), + "error": api_result.get("error"), + } + + # Show visual overlay if requested + if options.show_overlay: + elements = api_result.get("elements", []) + if elements: + await browser.page.evaluate( + """ + (elements) => { + if (window.sentience && window.sentience.showOverlay) { + window.sentience.showOverlay(elements, null); + } + } + """, + elements, + ) + + return Snapshot(**snapshot_data) + except ImportError: + # Fallback to requests if aiohttp not available (shouldn't happen in async context) + raise RuntimeError( + "aiohttp is required for async API calls. Install it with: pip install aiohttp" + ) + except Exception as e: + raise RuntimeError(f"API request failed: {e}") + + +# ========== Async Action Functions ========== + + +async def click( + browser: AsyncSentienceBrowser, + element_id: int, + use_mouse: bool = True, + take_snapshot: bool = False, +) -> ActionResult: + """ + Click an element by ID using hybrid approach (async) + + Args: + browser: AsyncSentienceBrowser instance + element_id: Element ID from snapshot + use_mouse: If True, use Playwright's mouse.click() at element center + take_snapshot: Whether to take snapshot after action + + Returns: + ActionResult + """ + if not browser.page: + raise RuntimeError("Browser not started. Call await browser.start() first.") + + start_time = time.time() + url_before = browser.page.url + + if use_mouse: + try: + snap = await snapshot(browser) + element = None + for el in snap.elements: + if el.id == element_id: + element = el + break + + if element: + center_x = element.bbox.x + element.bbox.width / 2 + center_y = element.bbox.y + element.bbox.height / 2 + try: + await browser.page.mouse.click(center_x, center_y) + success = True + except Exception: + success = True + else: + try: + success = await browser.page.evaluate( + """ + (id) => { + return window.sentience.click(id); + } + """, + element_id, + ) + except Exception: + success = True + except Exception: + try: + success = await browser.page.evaluate( + """ + (id) => { + return window.sentience.click(id); + } + """, + element_id, + ) + except Exception: + success = True + else: + success = await browser.page.evaluate( + """ + (id) => { + return window.sentience.click(id); + } + """, + element_id, + ) + + # Wait a bit for navigation/DOM updates + try: + await browser.page.wait_for_timeout(500) + except Exception: + pass + + duration_ms = int((time.time() - start_time) * 1000) + + # Check if URL changed + try: + url_after = browser.page.url + url_changed = url_before != url_after + except Exception: + url_after = url_before + url_changed = True + + # Determine outcome + outcome: str | None = None + if url_changed: + outcome = "navigated" + elif success: + outcome = "dom_updated" + else: + outcome = "error" + + # Optional snapshot after + snapshot_after: Snapshot | None = None + if take_snapshot: + try: + snapshot_after = await snapshot(browser) + except Exception: + pass + + return ActionResult( + success=success, + duration_ms=duration_ms, + outcome=outcome, + url_changed=url_changed, + snapshot_after=snapshot_after, + error=( + None + if success + else { + "code": "click_failed", + "reason": "Element not found or not clickable", + } + ), + ) + + +async def type_text( + browser: AsyncSentienceBrowser, element_id: int, text: str, take_snapshot: bool = False +) -> ActionResult: + """ + Type text into an element (async) + + Args: + browser: AsyncSentienceBrowser instance + element_id: Element ID from snapshot + text: Text to type + take_snapshot: Whether to take snapshot after action + + Returns: + ActionResult + """ + if not browser.page: + raise RuntimeError("Browser not started. Call await browser.start() first.") + + start_time = time.time() + url_before = browser.page.url + + # Focus element first + focused = await browser.page.evaluate( + """ + (id) => { + const el = window.sentience_registry[id]; + if (el) { + el.focus(); + return true; + } + return false; + } + """, + element_id, + ) + + if not focused: + return ActionResult( + success=False, + duration_ms=int((time.time() - start_time) * 1000), + outcome="error", + error={"code": "focus_failed", "reason": "Element not found"}, + ) + + # Type using Playwright keyboard + await browser.page.keyboard.type(text) + + duration_ms = int((time.time() - start_time) * 1000) + url_after = browser.page.url + url_changed = url_before != url_after + + outcome = "navigated" if url_changed else "dom_updated" + + snapshot_after: Snapshot | None = None + if take_snapshot: + snapshot_after = await snapshot(browser) + + return ActionResult( + success=True, + duration_ms=duration_ms, + outcome=outcome, + url_changed=url_changed, + snapshot_after=snapshot_after, + ) + + +async def press(browser: AsyncSentienceBrowser, key: str, take_snapshot: bool = False) -> ActionResult: + """ + Press a keyboard key (async) + + Args: + browser: AsyncSentienceBrowser instance + key: Key to press (e.g., "Enter", "Escape", "Tab") + take_snapshot: Whether to take snapshot after action + + Returns: + ActionResult + """ + if not browser.page: + raise RuntimeError("Browser not started. Call await browser.start() first.") + + start_time = time.time() + url_before = browser.page.url + + # Press key using Playwright + await browser.page.keyboard.press(key) + + # Wait a bit for navigation/DOM updates + await browser.page.wait_for_timeout(500) + + duration_ms = int((time.time() - start_time) * 1000) + url_after = browser.page.url + url_changed = url_before != url_after + + outcome = "navigated" if url_changed else "dom_updated" + + snapshot_after: Snapshot | None = None + if take_snapshot: + snapshot_after = await snapshot(browser) + + return ActionResult( + success=True, + duration_ms=duration_ms, + outcome=outcome, + url_changed=url_changed, + snapshot_after=snapshot_after, + ) + + +async def _highlight_rect( + browser: AsyncSentienceBrowser, rect: dict[str, float], duration_sec: float = 2.0 +) -> None: + """Highlight a rectangle with a red border overlay (async)""" + if not browser.page: + return + + highlight_id = f"sentience_highlight_{int(time.time() * 1000)}" + + args = { + "rect": { + "x": rect["x"], + "y": rect["y"], + "w": rect["w"], + "h": rect["h"], + }, + "highlightId": highlight_id, + "durationSec": duration_sec, + } + + await browser.page.evaluate( + """ + (args) => { + const { rect, highlightId, durationSec } = args; + const overlay = document.createElement('div'); + overlay.id = highlightId; + overlay.style.position = 'fixed'; + overlay.style.left = `${rect.x}px`; + overlay.style.top = `${rect.y}px`; + overlay.style.width = `${rect.w}px`; + overlay.style.height = `${rect.h}px`; + overlay.style.border = '3px solid red'; + overlay.style.borderRadius = '2px'; + overlay.style.boxSizing = 'border-box'; + overlay.style.pointerEvents = 'none'; + overlay.style.zIndex = '999999'; + overlay.style.backgroundColor = 'rgba(255, 0, 0, 0.1)'; + overlay.style.transition = 'opacity 0.3s ease-out'; + + document.body.appendChild(overlay); + + setTimeout(() => { + overlay.style.opacity = '0'; + setTimeout(() => { + if (overlay.parentNode) { + overlay.parentNode.removeChild(overlay); + } + }, 300); + }, durationSec * 1000); + } + """, + args, + ) + + +async def click_rect( + browser: AsyncSentienceBrowser, + rect: dict[str, float] | BBox, + highlight: bool = True, + highlight_duration: float = 2.0, + take_snapshot: bool = False, +) -> ActionResult: + """ + Click at the center of a rectangle (async) + + Args: + browser: AsyncSentienceBrowser instance + rect: Dictionary with x, y, width (w), height (h) keys, or BBox object + highlight: Whether to show a red border highlight when clicking + highlight_duration: How long to show the highlight in seconds + take_snapshot: Whether to take snapshot after action + + Returns: + ActionResult + """ + if not browser.page: + raise RuntimeError("Browser not started. Call await browser.start() first.") + + # Handle BBox object or dict + if isinstance(rect, BBox): + x = rect.x + y = rect.y + w = rect.width + h = rect.height + else: + x = rect.get("x", 0) + y = rect.get("y", 0) + w = rect.get("w") or rect.get("width", 0) + h = rect.get("h") or rect.get("height", 0) + + if w <= 0 or h <= 0: + return ActionResult( + success=False, + duration_ms=0, + outcome="error", + error={ + "code": "invalid_rect", + "reason": "Rectangle width and height must be positive", + }, + ) + + start_time = time.time() + url_before = browser.page.url + + # Calculate center of rectangle + center_x = x + w / 2 + center_y = y + h / 2 + + # Show highlight before clicking + if highlight: + await _highlight_rect(browser, {"x": x, "y": y, "w": w, "h": h}, highlight_duration) + await browser.page.wait_for_timeout(50) + + # Use Playwright's native mouse click + try: + await browser.page.mouse.click(center_x, center_y) + success = True + except Exception as e: + success = False + error_msg = str(e) + + # Wait a bit for navigation/DOM updates + await browser.page.wait_for_timeout(500) + + duration_ms = int((time.time() - start_time) * 1000) + url_after = browser.page.url + url_changed = url_before != url_after + + # Determine outcome + outcome: str | None = None + if url_changed: + outcome = "navigated" + elif success: + outcome = "dom_updated" + else: + outcome = "error" + + # Optional snapshot after + snapshot_after: Snapshot | None = None + if take_snapshot: + snapshot_after = await snapshot(browser) + + return ActionResult( + success=success, + duration_ms=duration_ms, + outcome=outcome, + url_changed=url_changed, + snapshot_after=snapshot_after, + error=( + None + if success + else { + "code": "click_failed", + "reason": error_msg if not success else "Click failed", + } + ), + ) + + +# ========== Re-export Query Functions (Pure Functions - No Async Needed) ========== + +# Query functions (find, query) are pure functions that work with Snapshot objects +# They don't need async versions, but we re-export them for convenience +from sentience.query import find, query + +__all__ = [ + "AsyncSentienceBrowser", + "snapshot", + "click", + "type_text", + "press", + "click_rect", + "find", + "query", +] diff --git a/tests/test_async_api.py b/tests/test_async_api.py new file mode 100644 index 0000000..ff7b6d1 --- /dev/null +++ b/tests/test_async_api.py @@ -0,0 +1,271 @@ +""" +Tests for async API functionality +""" + +import pytest +from playwright.async_api import async_playwright + +from sentience.async_api import ( + AsyncSentienceBrowser, + click, + click_rect, + find, + press, + query, + snapshot, + type_text, +) +from sentience.models import BBox, SnapshotOptions + + +@pytest.mark.asyncio +@pytest.mark.requires_extension +async def test_async_browser_basic(): + """Test basic async browser initialization""" + async with AsyncSentienceBrowser() as browser: + await browser.goto("https://example.com") + assert browser.page is not None + assert "example.com" in browser.page.url + + +@pytest.mark.asyncio +@pytest.mark.requires_extension +async def test_async_viewport_default(): + """Test that default viewport is 1280x800""" + async with AsyncSentienceBrowser() as browser: + await browser.goto("https://example.com") + await browser.page.wait_for_load_state("networkidle") + + viewport_size = await browser.page.evaluate( + "() => ({ width: window.innerWidth, height: window.innerHeight })" + ) + + assert viewport_size["width"] == 1280 + assert viewport_size["height"] == 800 + + +@pytest.mark.asyncio +@pytest.mark.requires_extension +async def test_async_viewport_custom(): + """Test custom viewport size""" + custom_viewport = {"width": 1920, "height": 1080} + async with AsyncSentienceBrowser(viewport=custom_viewport) as browser: + await browser.goto("https://example.com") + await browser.page.wait_for_load_state("networkidle") + + viewport_size = await browser.page.evaluate( + "() => ({ width: window.innerWidth, height: window.innerHeight })" + ) + + assert viewport_size["width"] == 1920 + assert viewport_size["height"] == 1080 + + +@pytest.mark.asyncio +@pytest.mark.requires_extension +async def test_async_snapshot(): + """Test async snapshot function""" + async with AsyncSentienceBrowser() as browser: + await browser.goto("https://example.com") + await browser.page.wait_for_load_state("networkidle") + + snap = await snapshot(browser) + assert isinstance(snap, type(snap)) # Check it's a Snapshot object + assert snap.status == "success" + assert len(snap.elements) > 0 + assert snap.url is not None + + +@pytest.mark.asyncio +@pytest.mark.requires_extension +async def test_async_snapshot_with_options(): + """Test async snapshot with options""" + async with AsyncSentienceBrowser() as browser: + await browser.goto("https://example.com") + await browser.page.wait_for_load_state("networkidle") + + options = SnapshotOptions(limit=10, screenshot=False) + snap = await snapshot(browser, options) + assert snap.status == "success" + assert len(snap.elements) <= 10 + + +@pytest.mark.asyncio +@pytest.mark.requires_extension +async def test_async_click(): + """Test async click action""" + async with AsyncSentienceBrowser() as browser: + await browser.goto("https://example.com") + await browser.page.wait_for_load_state("networkidle") + + snap = await snapshot(browser) + link = find(snap, "role=link") + + if link: + result = await click(browser, link.id) + assert result.success is True + assert result.duration_ms > 0 + assert result.outcome in ["navigated", "dom_updated"] + + +@pytest.mark.asyncio +@pytest.mark.requires_extension +async def test_async_type_text(): + """Test async type_text action""" + async with AsyncSentienceBrowser() as browser: + await browser.goto("https://example.com") + await browser.page.wait_for_load_state("networkidle") + + snap = await snapshot(browser) + textbox = find(snap, "role=textbox") + + if textbox: + result = await type_text(browser, textbox.id, "hello") + assert result.success is True + assert result.duration_ms > 0 + + +@pytest.mark.asyncio +@pytest.mark.requires_extension +async def test_async_press(): + """Test async press action""" + async with AsyncSentienceBrowser() as browser: + await browser.goto("https://example.com") + await browser.page.wait_for_load_state("networkidle") + + result = await press(browser, "Enter") + assert result.success is True + assert result.duration_ms > 0 + + +@pytest.mark.asyncio +@pytest.mark.requires_extension +async def test_async_click_rect(): + """Test async click_rect action""" + async with AsyncSentienceBrowser() as browser: + await browser.goto("https://example.com") + await browser.page.wait_for_load_state("networkidle") + + # Click at specific coordinates + result = await click_rect(browser, {"x": 100, "y": 200, "w": 50, "h": 30}, highlight=False) + assert result.success is True + assert result.duration_ms > 0 + + +@pytest.mark.asyncio +@pytest.mark.requires_extension +async def test_async_click_rect_with_bbox(): + """Test async click_rect with BBox object""" + async with AsyncSentienceBrowser() as browser: + await browser.goto("https://example.com") + await browser.page.wait_for_load_state("networkidle") + + snap = await snapshot(browser) + if snap.elements: + element = snap.elements[0] + bbox = BBox( + x=element.bbox.x, + y=element.bbox.y, + width=element.bbox.width, + height=element.bbox.height, + ) + result = await click_rect(browser, bbox, highlight=False) + assert result.success is True + + +@pytest.mark.asyncio +@pytest.mark.requires_extension +async def test_async_find(): + """Test async find function (re-exported from query)""" + async with AsyncSentienceBrowser() as browser: + await browser.goto("https://example.com") + await browser.page.wait_for_load_state("networkidle") + + snap = await snapshot(browser) + link = find(snap, "role=link") + # May or may not find a link, but should not raise an error + assert link is None or hasattr(link, "id") + + +@pytest.mark.asyncio +@pytest.mark.requires_extension +async def test_async_query(): + """Test async query function (re-exported from query)""" + async with AsyncSentienceBrowser() as browser: + await browser.goto("https://example.com") + await browser.page.wait_for_load_state("networkidle") + + snap = await snapshot(browser) + links = query(snap, "role=link") + assert isinstance(links, list) + # All results should be Element objects + for link in links: + assert hasattr(link, "id") + assert hasattr(link, "role") + + +@pytest.mark.asyncio +@pytest.mark.requires_extension +async def test_async_from_existing_context(): + """Test creating AsyncSentienceBrowser from existing context""" + async with async_playwright() as p: + context = await p.chromium.launch_persistent_context("", headless=True) + try: + browser = await AsyncSentienceBrowser.from_existing(context) + assert browser.context is context + assert browser.page is not None + + await browser.page.goto("https://example.com") + assert "example.com" in browser.page.url + + await browser.close() + finally: + await context.close() + + +@pytest.mark.asyncio +@pytest.mark.requires_extension +async def test_async_from_page(): + """Test creating AsyncSentienceBrowser from existing page""" + async with async_playwright() as p: + context = await p.chromium.launch_persistent_context("", headless=True) + try: + page = await context.new_page() + browser = await AsyncSentienceBrowser.from_page(page) + assert browser.page is page + assert browser.context is context + + await browser.page.goto("https://example.com") + assert "example.com" in browser.page.url + + await browser.close() + finally: + await context.close() + + +@pytest.mark.asyncio +@pytest.mark.requires_extension +async def test_async_context_manager(): + """Test async context manager usage""" + async with AsyncSentienceBrowser() as browser: + await browser.goto("https://example.com") + assert browser.page is not None + + # Browser should be closed after context manager exits + assert browser.page is None or browser.context is None + + +@pytest.mark.asyncio +@pytest.mark.requires_extension +async def test_async_snapshot_with_goal(): + """Test async snapshot with goal for ML reranking""" + async with AsyncSentienceBrowser() as browser: + await browser.goto("https://example.com") + await browser.page.wait_for_load_state("networkidle") + + options = SnapshotOptions(goal="Click the main link", limit=10) + snap = await snapshot(browser, options) + assert snap.status == "success" + # Elements may have ML reranking metadata if API key is provided + # (This test works with or without API key) + From 523882f8adc09cceeb91b6ca4dce72e01a22cfbf Mon Sep 17 00:00:00 2001 From: rcholic Date: Wed, 31 Dec 2025 17:13:44 -0800 Subject: [PATCH 3/6] async api --- sentience/_extension_loader.py | 1 - sentience/async_api.py | 12 +++++++----- tests/test_async_api.py | 1 - tests/test_browser.py | 3 +-- 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/sentience/_extension_loader.py b/sentience/_extension_loader.py index 58bd873..d969ec3 100644 --- a/sentience/_extension_loader.py +++ b/sentience/_extension_loader.py @@ -38,4 +38,3 @@ def find_extension_path() -> Path: f"2. {dev_ext_path}\n" "Make sure the extension is built and 'sentience/extension' directory exists." ) - diff --git a/sentience/async_api.py b/sentience/async_api.py index 8dad11b..d85fe13 100644 --- a/sentience/async_api.py +++ b/sentience/async_api.py @@ -253,9 +253,7 @@ async def goto(self, url: str) -> None: f"5. Diagnostic info: {diag}" ) - async def _inject_storage_state( - self, storage_state: str | Path | StorageState | dict - ) -> None: + async def _inject_storage_state(self, storage_state: str | Path | StorageState | dict) -> None: """Inject storage state (cookies + localStorage) into browser context (async)""" import json @@ -628,7 +626,9 @@ async def _snapshot_via_api( # Wait for extension injection try: - await browser.page.wait_for_function("typeof window.sentience !== 'undefined'", timeout=5000) + await browser.page.wait_for_function( + "typeof window.sentience !== 'undefined'", timeout=5000 + ) except Exception as e: raise RuntimeError( "Sentience extension failed to inject. Cannot collect raw data for API processing." @@ -929,7 +929,9 @@ async def type_text( ) -async def press(browser: AsyncSentienceBrowser, key: str, take_snapshot: bool = False) -> ActionResult: +async def press( + browser: AsyncSentienceBrowser, key: str, take_snapshot: bool = False +) -> ActionResult: """ Press a keyboard key (async) diff --git a/tests/test_async_api.py b/tests/test_async_api.py index ff7b6d1..0165972 100644 --- a/tests/test_async_api.py +++ b/tests/test_async_api.py @@ -268,4 +268,3 @@ async def test_async_snapshot_with_goal(): assert snap.status == "success" # Elements may have ML reranking metadata if API key is provided # (This test works with or without API key) - diff --git a/tests/test_browser.py b/tests/test_browser.py index 1c85283..da4afe3 100644 --- a/tests/test_browser.py +++ b/tests/test_browser.py @@ -3,9 +3,9 @@ """ import pytest +from playwright.sync_api import sync_playwright from sentience import SentienceBrowser -from playwright.sync_api import sync_playwright @pytest.mark.requires_extension @@ -168,4 +168,3 @@ def test_from_page_with_api_key(): finally: context.close() browser_instance.close() - From 7f4be31da7fc30f8a482891e20b40dc844532a23 Mon Sep 17 00:00:00 2001 From: rcholic Date: Wed, 31 Dec 2025 17:16:54 -0800 Subject: [PATCH 4/6] replace dict with Viewport type --- sentience/async_api.py | 21 +++++++++++++++------ sentience/browser.py | 25 +++++++++++++++---------- 2 files changed, 30 insertions(+), 16 deletions(-) diff --git a/sentience/async_api.py b/sentience/async_api.py index d85fe13..1b3aec5 100644 --- a/sentience/async_api.py +++ b/sentience/async_api.py @@ -26,6 +26,7 @@ Snapshot, SnapshotOptions, StorageState, + Viewport, WaitResult, ) @@ -51,7 +52,7 @@ def __init__( storage_state: str | Path | StorageState | dict | None = None, record_video_dir: str | Path | None = None, record_video_size: dict[str, int] | None = None, - viewport: dict[str, int] | None = None, + viewport: Viewport | dict[str, int] | None = None, ): """ Initialize Async Sentience browser @@ -66,8 +67,11 @@ def __init__( storage_state: Optional storage state to inject (cookies + localStorage) record_video_dir: Optional directory path to save video recordings record_video_size: Optional video resolution as dict with 'width' and 'height' keys - viewport: Optional viewport size as dict with 'width' and 'height' keys. - Defaults to {"width": 1280, "height": 800} + viewport: Optional viewport size as Viewport object or dict with 'width' and 'height' keys. + Examples: Viewport(width=1280, height=800) (default) + Viewport(width=1920, height=1080) (Full HD) + {"width": 1280, "height": 800} (dict also supported) + If None, defaults to Viewport(width=1280, height=800). """ self.api_key = api_key # Only set api_url if api_key is provided, otherwise None (free tier) @@ -94,8 +98,13 @@ def __init__( self.record_video_dir = record_video_dir self.record_video_size = record_video_size or {"width": 1280, "height": 800} - # Viewport configuration - self.viewport = viewport or {"width": 1280, "height": 800} + # Viewport configuration - convert dict to Viewport if needed + if viewport is None: + self.viewport = Viewport(width=1280, height=800) + elif isinstance(viewport, dict): + self.viewport = Viewport(width=viewport["width"], height=viewport["height"]) + else: + self.viewport = viewport self.playwright: Playwright | None = None self.context: BrowserContext | None = None @@ -185,7 +194,7 @@ async def start(self) -> None: "user_data_dir": user_data_dir, "headless": False, "args": args, - "viewport": self.viewport, + "viewport": {"width": self.viewport.width, "height": self.viewport.height}, "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36", } diff --git a/sentience/browser.py b/sentience/browser.py index eec1888..b7617b9 100644 --- a/sentience/browser.py +++ b/sentience/browser.py @@ -12,7 +12,7 @@ from playwright.sync_api import BrowserContext, Page, Playwright, sync_playwright from sentience._extension_loader import find_extension_path -from sentience.models import ProxyConfig, StorageState +from sentience.models import ProxyConfig, StorageState, Viewport # Import stealth for bot evasion (optional - graceful fallback if not available) try: @@ -36,7 +36,7 @@ def __init__( storage_state: str | Path | StorageState | dict | None = None, record_video_dir: str | Path | None = None, record_video_size: dict[str, int] | None = None, - viewport: dict[str, int] | None = None, + viewport: Viewport | dict[str, int] | None = None, ): """ Initialize Sentience browser @@ -69,11 +69,11 @@ def __init__( Examples: {"width": 1280, "height": 800} (default) {"width": 1920, "height": 1080} (1080p) If None, defaults to 1280x800. - viewport: Optional viewport size as dict with 'width' and 'height' keys. - Examples: {"width": 1280, "height": 800} (default) - {"width": 1920, "height": 1080} (Full HD) - {"width": 375, "height": 667} (iPhone) - If None, defaults to 1280x800. + viewport: Optional viewport size as Viewport object or dict with 'width' and 'height' keys. + Examples: Viewport(width=1280, height=800) (default) + Viewport(width=1920, height=1080) (Full HD) + {"width": 1280, "height": 800} (dict also supported) + If None, defaults to Viewport(width=1280, height=800). """ self.api_key = api_key # Only set api_url if api_key is provided, otherwise None (free tier) @@ -101,8 +101,13 @@ def __init__( self.record_video_dir = record_video_dir self.record_video_size = record_video_size or {"width": 1280, "height": 800} - # Viewport configuration - self.viewport = viewport or {"width": 1280, "height": 800} + # Viewport configuration - convert dict to Viewport if needed + if viewport is None: + self.viewport = Viewport(width=1280, height=800) + elif isinstance(viewport, dict): + self.viewport = Viewport(width=viewport["width"], height=viewport["height"]) + else: + self.viewport = viewport self.playwright: Playwright | None = None self.context: BrowserContext | None = None @@ -201,7 +206,7 @@ def start(self) -> None: "user_data_dir": user_data_dir, "headless": False, # IMPORTANT: See note above "args": args, - "viewport": self.viewport, + "viewport": {"width": self.viewport.width, "height": self.viewport.height}, # Remove "HeadlessChrome" from User Agent automatically "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36", } From f16e27421771d8b7569a314abb0326c33fbfd97c Mon Sep 17 00:00:00 2001 From: rcholic Date: Wed, 31 Dec 2025 17:30:52 -0800 Subject: [PATCH 5/6] updated doc; add _async suffix --- README.md | 22 +++++ examples/async_api_demo.py | 185 +++++++++++++++++++++++++++++++++++++ sentience/async_api.py | 46 ++++----- tests/test_async_api.py | 36 ++++---- 4 files changed, 248 insertions(+), 41 deletions(-) create mode 100644 examples/async_api_demo.py diff --git a/README.md b/README.md index e52c62b..2631594 100644 --- a/README.md +++ b/README.md @@ -487,6 +487,28 @@ for match in result.results: --- +## šŸ”„ Async API + +For asyncio contexts (FastAPI, async frameworks): + +```python +from sentience.async_api import AsyncSentienceBrowser, snapshot_async, click_async, find + +async def main(): + async with AsyncSentienceBrowser() as browser: + await browser.goto("https://example.com") + snap = await snapshot_async(browser) + button = find(snap, "role=button") + if button: + await click_async(browser, button.id) + +asyncio.run(main()) +``` + +**See example:** `examples/async_api_demo.py` + +--- + ## šŸ“‹ Reference

diff --git a/examples/async_api_demo.py b/examples/async_api_demo.py new file mode 100644 index 0000000..1899d91 --- /dev/null +++ b/examples/async_api_demo.py @@ -0,0 +1,185 @@ +""" +Example: Using Async API for asyncio contexts + +This example demonstrates how to use the Sentience SDK's async API +when working with asyncio, FastAPI, or other async frameworks. + +To run this example: + python -m examples.async_api_demo + +Or if sentience is installed: + python examples/async_api_demo.py +""" + +import asyncio +import os + +# Import async API functions +from sentience.async_api import AsyncSentienceBrowser, click_async, find, press_async, snapshot_async, type_text_async +from sentience.models import SnapshotOptions, Viewport + + +async def basic_async_example(): + """Basic async browser usage with context manager""" + api_key = os.environ.get("SENTIENCE_API_KEY") + + # Use async context manager + async with AsyncSentienceBrowser(api_key=api_key, headless=False) as browser: + # Navigate to a page + await browser.goto("https://example.com") + + # Take a snapshot (async) + snap = await snapshot_async(browser) + print(f"āœ… Found {len(snap.elements)} elements on the page") + + # Find an element + link = find(snap, "role=link") + if link: + print(f"Found link: {link.text} (id: {link.id})") + + # Click it (async) + result = await click_async(browser, link.id) + print(f"Click result: success={result.success}, outcome={result.outcome}") + + +async def custom_viewport_example(): + """Example using custom viewport with Viewport class""" + # Use Viewport class for type safety + custom_viewport = Viewport(width=1920, height=1080) + + async with AsyncSentienceBrowser(viewport=custom_viewport, headless=False) as browser: + await browser.goto("https://example.com") + + # Verify viewport size + viewport_size = await browser.page.evaluate( + "() => ({ width: window.innerWidth, height: window.innerHeight })" + ) + print(f"āœ… Viewport: {viewport_size['width']}x{viewport_size['height']}") + + +async def snapshot_with_options_example(): + """Example using SnapshotOptions with async API""" + async with AsyncSentienceBrowser(headless=False) as browser: + await browser.goto("https://example.com") + + # Take snapshot with options + options = SnapshotOptions( + limit=10, + screenshot=False, + show_overlay=False, + ) + snap = await snapshot_async(browser, options) + print(f"āœ… Snapshot with limit=10: {len(snap.elements)} elements") + + +async def actions_example(): + """Example of all async actions""" + async with AsyncSentienceBrowser(headless=False) as browser: + await browser.goto("https://example.com") + + # Take snapshot + snap = await snapshot_async(browser) + + # Find a textbox if available + textbox = find(snap, "role=textbox") + if textbox: + # Type text (async) + result = await type_text_async(browser, textbox.id, "Hello, World!") + print(f"āœ… Typed text: success={result.success}") + + # Press a key (async) + result = await press_async(browser, "Enter") + print(f"āœ… Pressed Enter: success={result.success}") + + +async def from_existing_context_example(): + """Example using from_existing() with existing Playwright context""" + from playwright.async_api import async_playwright + + async with async_playwright() as p: + # Create your own Playwright context + context = await p.chromium.launch_persistent_context("", headless=True) + + try: + # Create SentienceBrowser from existing context + browser = await AsyncSentienceBrowser.from_existing(context) + await browser.goto("https://example.com") + + # Use Sentience SDK functions + snap = await snapshot_async(browser) + print(f"āœ… Using existing context: {len(snap.elements)} elements") + finally: + await context.close() + + +async def from_existing_page_example(): + """Example using from_page() with existing Playwright page""" + from playwright.async_api import async_playwright + + async with async_playwright() as p: + browser_instance = await p.chromium.launch(headless=True) + context = await browser_instance.new_context() + page = await context.new_page() + + try: + # Create SentienceBrowser from existing page + sentience_browser = await AsyncSentienceBrowser.from_page(page) + await sentience_browser.goto("https://example.com") + + # Use Sentience SDK functions + snap = await snapshot_async(sentience_browser) + print(f"āœ… Using existing page: {len(snap.elements)} elements") + finally: + await context.close() + await browser_instance.close() + + +async def multiple_browsers_example(): + """Example running multiple browsers concurrently""" + async def process_site(url: str): + async with AsyncSentienceBrowser(headless=True) as browser: + await browser.goto(url) + snap = await snapshot_async(browser) + return {"url": url, "elements": len(snap.elements)} + + # Process multiple sites concurrently + urls = [ + "https://example.com", + "https://httpbin.org/html", + ] + + results = await asyncio.gather(*[process_site(url) for url in urls]) + for result in results: + print(f"āœ… {result['url']}: {result['elements']} elements") + + +async def main(): + """Run all examples""" + print("=== Basic Async Example ===") + await basic_async_example() + + print("\n=== Custom Viewport Example ===") + await custom_viewport_example() + + print("\n=== Snapshot with Options Example ===") + await snapshot_with_options_example() + + print("\n=== Actions Example ===") + await actions_example() + + print("\n=== From Existing Context Example ===") + await from_existing_context_example() + + print("\n=== From Existing Page Example ===") + await from_existing_page_example() + + print("\n=== Multiple Browsers Concurrent Example ===") + await multiple_browsers_example() + + print("\nāœ… All async examples completed!") + + +if __name__ == "__main__": + # Run the async main function + asyncio.run(main()) + diff --git a/sentience/async_api.py b/sentience/async_api.py index 1b3aec5..12beefb 100644 --- a/sentience/async_api.py +++ b/sentience/async_api.py @@ -498,7 +498,7 @@ async def from_page( # ========== Async Snapshot Functions ========== -async def snapshot( +async def snapshot_async( browser: AsyncSentienceBrowser, options: SnapshotOptions | None = None, ) -> Snapshot: @@ -515,10 +515,10 @@ async def snapshot( Example: # Basic snapshot with defaults - snap = await snapshot(browser) + snap = await snapshot_async(browser) # With options - snap = await snapshot(browser, SnapshotOptions( + snap = await snapshot_async(browser, SnapshotOptions( screenshot=True, limit=100, show_overlay=True @@ -535,13 +535,13 @@ async def snapshot( if should_use_api and browser.api_key: # Use server-side API (Pro/Enterprise tier) - return await _snapshot_via_api(browser, options) + return await _snapshot_via_api_async(browser, options) else: # Use local extension (Free tier) - return await _snapshot_via_extension(browser, options) + return await _snapshot_via_extension_async(browser, options) -async def _snapshot_via_extension( +async def _snapshot_via_extension_async( browser: AsyncSentienceBrowser, options: SnapshotOptions, ) -> Snapshot: @@ -619,7 +619,7 @@ async def _snapshot_via_extension( return snapshot_obj -async def _snapshot_via_api( +async def _snapshot_via_api_async( browser: AsyncSentienceBrowser, options: SnapshotOptions, ) -> Snapshot: @@ -747,7 +747,7 @@ async def _snapshot_via_api( # ========== Async Action Functions ========== -async def click( +async def click_async( browser: AsyncSentienceBrowser, element_id: int, use_mouse: bool = True, @@ -773,7 +773,7 @@ async def click( if use_mouse: try: - snap = await snapshot(browser) + snap = await snapshot_async(browser) element = None for el in snap.elements: if el.id == element_id: @@ -851,7 +851,7 @@ async def click( snapshot_after: Snapshot | None = None if take_snapshot: try: - snapshot_after = await snapshot(browser) + snapshot_after = await snapshot_async(browser) except Exception: pass @@ -872,7 +872,7 @@ async def click( ) -async def type_text( +async def type_text_async( browser: AsyncSentienceBrowser, element_id: int, text: str, take_snapshot: bool = False ) -> ActionResult: """ @@ -927,7 +927,7 @@ async def type_text( snapshot_after: Snapshot | None = None if take_snapshot: - snapshot_after = await snapshot(browser) + snapshot_after = await snapshot_async(browser) return ActionResult( success=True, @@ -938,7 +938,7 @@ async def type_text( ) -async def press( +async def press_async( browser: AsyncSentienceBrowser, key: str, take_snapshot: bool = False ) -> ActionResult: """ @@ -972,7 +972,7 @@ async def press( snapshot_after: Snapshot | None = None if take_snapshot: - snapshot_after = await snapshot(browser) + snapshot_after = await snapshot_async(browser) return ActionResult( success=True, @@ -983,7 +983,7 @@ async def press( ) -async def _highlight_rect( +async def _highlight_rect_async( browser: AsyncSentienceBrowser, rect: dict[str, float], duration_sec: float = 2.0 ) -> None: """Highlight a rectangle with a red border overlay (async)""" @@ -1038,7 +1038,7 @@ async def _highlight_rect( ) -async def click_rect( +async def click_rect_async( browser: AsyncSentienceBrowser, rect: dict[str, float] | BBox, highlight: bool = True, @@ -1093,7 +1093,7 @@ async def click_rect( # Show highlight before clicking if highlight: - await _highlight_rect(browser, {"x": x, "y": y, "w": w, "h": h}, highlight_duration) + await _highlight_rect_async(browser, {"x": x, "y": y, "w": w, "h": h}, highlight_duration) await browser.page.wait_for_timeout(50) # Use Playwright's native mouse click @@ -1123,7 +1123,7 @@ async def click_rect( # Optional snapshot after snapshot_after: Snapshot | None = None if take_snapshot: - snapshot_after = await snapshot(browser) + snapshot_after = await snapshot_async(browser) return ActionResult( success=success, @@ -1150,11 +1150,11 @@ async def click_rect( __all__ = [ "AsyncSentienceBrowser", - "snapshot", - "click", - "type_text", - "press", - "click_rect", + "snapshot_async", + "click_async", + "type_text_async", + "press_async", + "click_rect_async", "find", "query", ] diff --git a/tests/test_async_api.py b/tests/test_async_api.py index 0165972..11d7f97 100644 --- a/tests/test_async_api.py +++ b/tests/test_async_api.py @@ -7,13 +7,13 @@ from sentience.async_api import ( AsyncSentienceBrowser, - click, - click_rect, + click_async, + click_rect_async, find, - press, + press_async, query, - snapshot, - type_text, + snapshot_async, + type_text_async, ) from sentience.models import BBox, SnapshotOptions @@ -69,7 +69,7 @@ async def test_async_snapshot(): await browser.goto("https://example.com") await browser.page.wait_for_load_state("networkidle") - snap = await snapshot(browser) + snap = await snapshot_async(browser) assert isinstance(snap, type(snap)) # Check it's a Snapshot object assert snap.status == "success" assert len(snap.elements) > 0 @@ -85,7 +85,7 @@ async def test_async_snapshot_with_options(): await browser.page.wait_for_load_state("networkidle") options = SnapshotOptions(limit=10, screenshot=False) - snap = await snapshot(browser, options) + snap = await snapshot_async(browser, options) assert snap.status == "success" assert len(snap.elements) <= 10 @@ -98,11 +98,11 @@ async def test_async_click(): await browser.goto("https://example.com") await browser.page.wait_for_load_state("networkidle") - snap = await snapshot(browser) + snap = await snapshot_async(browser) link = find(snap, "role=link") if link: - result = await click(browser, link.id) + result = await click_async(browser, link.id) assert result.success is True assert result.duration_ms > 0 assert result.outcome in ["navigated", "dom_updated"] @@ -116,11 +116,11 @@ async def test_async_type_text(): await browser.goto("https://example.com") await browser.page.wait_for_load_state("networkidle") - snap = await snapshot(browser) + snap = await snapshot_async(browser) textbox = find(snap, "role=textbox") if textbox: - result = await type_text(browser, textbox.id, "hello") + result = await type_text_async(browser, textbox.id, "hello") assert result.success is True assert result.duration_ms > 0 @@ -133,7 +133,7 @@ async def test_async_press(): await browser.goto("https://example.com") await browser.page.wait_for_load_state("networkidle") - result = await press(browser, "Enter") + result = await press_async(browser, "Enter") assert result.success is True assert result.duration_ms > 0 @@ -147,7 +147,7 @@ async def test_async_click_rect(): await browser.page.wait_for_load_state("networkidle") # Click at specific coordinates - result = await click_rect(browser, {"x": 100, "y": 200, "w": 50, "h": 30}, highlight=False) + result = await click_rect_async(browser, {"x": 100, "y": 200, "w": 50, "h": 30}, highlight=False) assert result.success is True assert result.duration_ms > 0 @@ -160,7 +160,7 @@ async def test_async_click_rect_with_bbox(): await browser.goto("https://example.com") await browser.page.wait_for_load_state("networkidle") - snap = await snapshot(browser) + snap = await snapshot_async(browser) if snap.elements: element = snap.elements[0] bbox = BBox( @@ -169,7 +169,7 @@ async def test_async_click_rect_with_bbox(): width=element.bbox.width, height=element.bbox.height, ) - result = await click_rect(browser, bbox, highlight=False) + result = await click_rect_async(browser, bbox, highlight=False) assert result.success is True @@ -181,7 +181,7 @@ async def test_async_find(): await browser.goto("https://example.com") await browser.page.wait_for_load_state("networkidle") - snap = await snapshot(browser) + snap = await snapshot_async(browser) link = find(snap, "role=link") # May or may not find a link, but should not raise an error assert link is None or hasattr(link, "id") @@ -195,7 +195,7 @@ async def test_async_query(): await browser.goto("https://example.com") await browser.page.wait_for_load_state("networkidle") - snap = await snapshot(browser) + snap = await snapshot_async(browser) links = query(snap, "role=link") assert isinstance(links, list) # All results should be Element objects @@ -264,7 +264,7 @@ async def test_async_snapshot_with_goal(): await browser.page.wait_for_load_state("networkidle") options = SnapshotOptions(goal="Click the main link", limit=10) - snap = await snapshot(browser, options) + snap = await snapshot_async(browser, options) assert snap.status == "success" # Elements may have ML reranking metadata if API key is provided # (This test works with or without API key) From 92d22b8879fb780ccc78302c12369b8bcda3ec82 Mon Sep 17 00:00:00 2001 From: rcholic Date: Wed, 31 Dec 2025 17:31:07 -0800 Subject: [PATCH 6/6] updated doc; add _async suffix --- examples/async_api_demo.py | 11 +++++++++-- tests/test_async_api.py | 4 +++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/examples/async_api_demo.py b/examples/async_api_demo.py index 1899d91..81b478a 100644 --- a/examples/async_api_demo.py +++ b/examples/async_api_demo.py @@ -15,7 +15,14 @@ import os # Import async API functions -from sentience.async_api import AsyncSentienceBrowser, click_async, find, press_async, snapshot_async, type_text_async +from sentience.async_api import ( + AsyncSentienceBrowser, + click_async, + find, + press_async, + snapshot_async, + type_text_async, +) from sentience.models import SnapshotOptions, Viewport @@ -136,6 +143,7 @@ async def from_existing_page_example(): async def multiple_browsers_example(): """Example running multiple browsers concurrently""" + async def process_site(url: str): async with AsyncSentienceBrowser(headless=True) as browser: await browser.goto(url) @@ -182,4 +190,3 @@ async def main(): if __name__ == "__main__": # Run the async main function asyncio.run(main()) - diff --git a/tests/test_async_api.py b/tests/test_async_api.py index 11d7f97..b2a9d0f 100644 --- a/tests/test_async_api.py +++ b/tests/test_async_api.py @@ -147,7 +147,9 @@ async def test_async_click_rect(): await browser.page.wait_for_load_state("networkidle") # Click at specific coordinates - result = await click_rect_async(browser, {"x": 100, "y": 200, "w": 50, "h": 30}, highlight=False) + result = await click_rect_async( + browser, {"x": 100, "y": 200, "w": 50, "h": 30}, highlight=False + ) assert result.success is True assert result.duration_ms > 0