From f3ba4feae32f9079348a863f2848701e29070009 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Fri, 7 Feb 2025 20:43:57 +0000 Subject: [PATCH 1/5] Update llms.txt --- docs/llms.txt | 59 ++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 51 insertions(+), 8 deletions(-) diff --git a/docs/llms.txt b/docs/llms.txt index 2e0af5b7..fa23ea5c 100644 --- a/docs/llms.txt +++ b/docs/llms.txt @@ -514,10 +514,6 @@ which adheres to a common pattern or exporting your project to share. Templates are versioned, and each previous version provides a method to convert it's content to the current version. -> TODO: Templates are currently identified as `proj_templates` since they conflict -with the templates used by `generation`. Move existing templates to be part of -the generation package. - ### `TemplateConfig.from_user_input(identifier: str)` `` Returns a `TemplateConfig` object for either a URL, file path, or builtin template name. @@ -716,7 +712,7 @@ title: 'System Analyzer' description: 'Inspect a project directory and improve it' --- -[View Template](https://github.com/AgentOps-AI/AgentStack/blob/main/agentstack/templates/proj_templates/system_analyzer.json) +[View Template](https://github.com/AgentOps-AI/AgentStack/blob/main/agentstack/templates/system_analyzer.json) ```bash agentstack init --template=system_analyzer @@ -737,7 +733,7 @@ title: 'Researcher' description: 'Research and report result from a query' --- -[View Template](https://github.com/AgentOps-AI/AgentStack/blob/main/agentstack/templates/proj_templates/research.json) +[View Template](https://github.com/AgentOps-AI/AgentStack/blob/main/agentstack/templates/research.json) ```bash agentstack init --template=research @@ -828,7 +824,54 @@ title: 'Content Creator' description: 'Research a topic and create content on it' --- -[View Template](https://github.com/AgentOps-AI/AgentStack/blob/main/agentstack/templates/proj_templates/content_creator.json) +[View Template](https://github.com/AgentOps-AI/AgentStack/blob/main/agentstack/templates/content_creator.json) + +## frameworks/list.mdx + +--- +title: Frameworks +description: 'Supported frameworks in AgentStack' +icon: 'ship' +--- + +These are documentation links to the frameworks supported directly by AgentStack. + +To start a project with one of these frameworks, use +```bash +agentstack init --framework +``` + +## Framework Docs + + + An intuitive agentic framework (recommended) + + + A complex but capable framework with a _steep_ learning curve + + + A simple framework with a cult following + + + An expansive framework with many ancillary features + + ## tools/package-structure.mdx @@ -1043,7 +1086,7 @@ You can pass the `--wizard` flag to `agentstack init` to use an interactive proj You can also pass a `--template=` argument to `agentstack init` which will pre-populate your project with functionality from a built-in template, or one found on the internet. A `template_name` can be one of three identifiers: -- A built-in AgentStack template (see the `templates/proj_templates` directory in the AgentStack repo for bundled templates). +- A built-in AgentStack template (see the `templates` directory in the AgentStack repo for bundled templates). - A template file from the internet; pass the full https URL of the template. - A local template file; pass an absolute or relative path. From 7c1bf897742cfb58f4942a2547be70a0a1bb767a Mon Sep 17 00:00:00 2001 From: Travis Dent Date: Thu, 13 Feb 2025 13:37:47 -0800 Subject: [PATCH 2/5] Use Anthropic for vision tool. --- agentstack/_tools/vision/__init__.py | 126 +++++++++++++++++---------- agentstack/_tools/vision/config.json | 7 +- tests/fixtures/test_image.jpg | Bin 0 -> 36368 bytes tests/tools/test_tool_vision.py | 51 +++++++++++ 4 files changed, 137 insertions(+), 47 deletions(-) create mode 100644 tests/fixtures/test_image.jpg create mode 100644 tests/tools/test_tool_vision.py diff --git a/agentstack/_tools/vision/__init__.py b/agentstack/_tools/vision/__init__.py index e491153a..677500e9 100644 --- a/agentstack/_tools/vision/__init__.py +++ b/agentstack/_tools/vision/__init__.py @@ -1,70 +1,106 @@ -"""Vision tool for analyzing images using OpenAI's Vision API.""" - +from typing import IO, Optional +import os +from pathlib import Path import base64 -from typing import Optional +import tempfile import requests -from openai import OpenAI +import anthropic __all__ = ["analyze_image"] +PROMPT = os.getenv('VISION_PROMPT', "What's in this image?") +MODEL = os.getenv('VISION_MODEL', "claude-3-5-sonnet-20241022") +MAX_TOKENS = os.getenv('VISION_MAX_TOKENS', 1024) -def analyze_image(image_path_url: str) -> str: - """ - Analyze an image using OpenAI's Vision API. +MEDIA_TYPES = { + "jpg": "image/jpeg", + "jpeg": "image/jpeg", + "png": "image/png", + "gif": "image/gif", + "webp": "image/webp", +} - Args: - image_path_url: Local path or URL to the image +# image sizes that will not be resized +# TODO is there any value in resizing pre-upload? +# 1:1 1092x1092 px +# 3:4 951x1268 px +# 2:3 896x1344 px +# 9:16 819x1456 px +# 1:2 784x1568 px - Returns: - str: Description of the image contents - """ - client = OpenAI() - if not image_path_url: - return "Image Path or URL is required." +def _get_media_type(image_filename: str) -> Optional[str]: + """Get the media type from an image filename.""" + for ext, media_type in MEDIA_TYPES.items(): + if image_filename.endswith(ext): + return media_type + return None + - if "http" in image_path_url: - return _analyze_web_image(client, image_path_url) - return _analyze_local_image(client, image_path_url) +def _encode_image(image_handle: IO) -> str: + """Encode a file handle to base64.""" + return base64.b64encode(image_handle.read()).decode("utf-8") -def _analyze_web_image(client: OpenAI, image_path_url: str) -> str: - response = client.chat.completions.create( - model="gpt-4-vision-preview", +def _make_anthropic_request(image_handle: IO, media_type: str) -> dict: + """Make a request to the Anthropic API using an image.""" + client = anthropic.Anthropic() + data = _encode_image(image_handle) + return client.messages.create( + model=MODEL, + max_tokens=MAX_TOKENS, messages=[ { "role": "user", "content": [ - {"type": "text", "text": "What's in this image?"}, - {"type": "image_url", "image_url": {"url": image_path_url}}, + { + "type": "image", + "source": { + "type": "base64", + "media_type": media_type, + "data": data, + }, + }, + { + "type": "text", + "text": PROMPT, + }, ], } ], - max_tokens=300, ) - return response.choices[0].message.content # type: ignore[return-value] -def _analyze_local_image(client: OpenAI, image_path: str) -> str: - base64_image = _encode_image(image_path) - headers = {"Content-Type": "application/json", "Authorization": f"Bearer {client.api_key}"} - payload = { - "model": "gpt-4-vision-preview", - "messages": [ - { - "role": "user", - "content": [ - {"type": "text", "text": "What's in this image?"}, - {"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{base64_image}"}}, - ], - } - ], - "max_tokens": 300, - } - response = requests.post("https://api.openai.com/v1/chat/completions", headers=headers, json=payload) - return response.json()["choices"][0]["message"]["content"] +def _analyze_web_image(image_url: str) -> str: + """Analyze an image from a URL.""" + with tempfile.NamedTemporaryFile() as temp_file: + temp_file.write(requests.get(image_url).content) + temp_file.flush() + temp_file.seek(0) + response = _make_anthropic_request(temp_file, _get_media_type(image_url)) + return response.content[0].text -def _encode_image(image_path: str) -> str: +def _analyze_local_image(image_path: str) -> str: + """Analyze an image from a local file.""" with open(image_path, "rb") as image_file: - return base64.b64encode(image_file.read()).decode("utf-8") + response = _make_anthropic_request(image_file, _get_media_type(image_path)) + return response.content[0].text + + +def analyze_image(image_path_or_url: str) -> str: + """ + Analyze an image using OpenAI's Vision API. + + Args: + image_path_or_url: Local path or URL to the image. + + Returns: + str: Description of the image contents + """ + if not image_path_or_url: + return "Image Path or URL is required." + + if "http" in image_path_or_url: + return _analyze_web_image(image_path_or_url) + return _analyze_local_image(image_path_or_url) diff --git a/agentstack/_tools/vision/config.json b/agentstack/_tools/vision/config.json index 37963f0d..0852aa20 100644 --- a/agentstack/_tools/vision/config.json +++ b/agentstack/_tools/vision/config.json @@ -2,10 +2,13 @@ "name": "vision", "category": "image-analysis", "env": { - "OPENAI_API_KEY": null + "ANTHROPIC_API_KEY": null, + "VISION_PROMPT": null, + "VISION_MODEL": null, + "VISION_MAX_TOKENS": null }, "dependencies": [ - "openai>=1.0.0", + "anthropic>=0.45.2", "requests>=2.31.0" ], "tools": ["analyze_image"] diff --git a/tests/fixtures/test_image.jpg b/tests/fixtures/test_image.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a30e369db62fc148f08558d24139e9cd02a2a25a GIT binary patch literal 36368 zcmbTdcT`hP6z@w15kyMpMF}Daf&vLeKe(*_IzgVe{=udGn|JRnHn)L zGBPr_oE{AS78&#zSeTfZnVDElFD$2p^$Z*9X*tWm!FGoGEH^jzSuU<~=LLAr@$mC- zaq$WB@dE^fgoMuViin8_iV6q{3I10J<9}bWvU0MqaSHNq@d*BZF8^LK@NzH-G4nAq zN;5F=GBWcr{_AIuVqjooJ#FoO4gTMSk%^gwm5rU_4CmR?7uwD42iNq+uGSR;@QBE$xcG#`q=%0P8JStxIk|cH zq_Xmg%Bt!ba&2=CqXIFRM>;8ekq2ZA?Q`0p1%|GcOD4MI|;qU2AsNKz?cE zSPlU_VrkQ>Ge8x@7l2#PB&VPZ14y?RH2($m|RW(&iG0& za<~>#bkV72@{M@Ehzppq);`4!d#1tJ3DEnvl}tM<{i^%|qo=WJIVktCY=-wN4_@^b zE#5i}pV0vXt5>ZpR0P6-E2%MgW1@GwEb*#aq~7YmgnNE3V2=So#TiR~K=tugVY7!5 z?r!BIfrUU_SzVa7f|FO$Wr4xeOt7ZFn_#0paG0LlJkd0Z(C-32Mgsov(|R6 zlDz9xcyUwg37gxyBp?n0=`7KkTN;6wxNGd8=r$Z4pn z?!D#h71K}hY;7Q3hgV&ukbev7#?5|54@e;0AA1$zkq9P)+sA_0d2_P9GOq!QCBuuV5ZhBKj@Ulh)~& zAxti$L&323VOZN$$o8caA+4Nan6-=F*g> z7B5Pk?b9^QA(a+)xzI{b!jOpo=m&C5VFh-KYWevH3}v-4tlTrHI26C7mGlCxOS=B# z)>X(*8Z#lsT0-^76e)^s7?Wa_JO!02&A3kNo|U%FejE(z#3t)zyp)>%i=RO6tH5~6-^FCQ(pe0N901o=q7v4 zvvMa%WH8t!Q_G|HDc}`=UuUD4XTz}mvW%A84$eJ~We}8{`uMA8oqXYYl2<`WS9;E_ zOJ^ zXf9DS6$4d*T*U-8ma5c(xb_(igoH5N7|cg9sQ!h_IHC>DlY*Yhxf;E@KXqy@k2g_G zZ_3HFi#J-+(y!B+Q{-{wJ8*{-Vs4z{n#r4hGvqyg3H2qsD6^mL5P)A*z1Hn__Lgjx z6MM~uTq~t0OW<-LDiC(vrs85QBxsJa*?7-B*DP_{LD|KlSuqt;nJqEQAW-)`;WDId zl!szkMtdQekvCvMP8b2m*_@BtvvPv5L;^+}&X-EqFB@}ix%Gal^z|0}PK>CZ3mtB} zefZvo>(Ed?90&ONDgU{14-!2Jn{x9m>G%EKm_Oh_QO&#&$CGRHW+T5ULo8J+e5TQ1 z>Df>r=U3n0!O5YTX7EArR6FE8ztHrZ^^;hUR}CDS8P_**Uw-=i<;Kb451Osem;SDm z>VrnP1GUu4=XR%yHGV`2$Kl0|EW`4>XVGK}mzi zlRU-cyWak02YY=6<>RilmXPdmeAgXc?8*(Rd*8g?WWS$H89LxYIkr+U7^!~C8{7Ku z*rIsbCVeGD)0WN48VZl4KNh~cZ3F4mJG&bwV}8Rm6YW6w=0#7=Ny#O%ujV^3lA}4zi;nhd!9-xz>S=EpaZ7t_`t{d-M`G?-jjNlRH9vleNh!x6h} zqkwgrA8>F?Er))ddz`6)B>-L6;QBpVEN+Q+6hWqrYJxj9TAUTX+f4SpuCL||#z~85=qJ{H!j0M9X>|{tB9AaDZDX1S<*uc)DPzrFXDkmG zxT;g?9?q27F|Jl^J{YBs;YTipg_0!c%0YROEDm#{A+-E z(|4*@dNzNKqkR0Qdwm@qM=y#0QK#= z5;<#cT#0miZH_!pNS8A*`BsI|o7=N#6{9_GN;TBDWP}gDsnl3nZjUl8l9Vn6a`%xO zR?=W1cUeQLOW#a$aUpy@lWf>Kt7|T|A>4zS0MV#7d7nV8X$zGXumU5zM(nVlql)(j zMwV>|CY@y!7?to?@@R8-7sKnOs^LQ_x}dsQr{Y>0dOw$-FD0u=*D$7{D*{_>O!NE( z6s<0#pQlZ!P_OHg3`3wB&kJP$zIR4q^N|qiS^d-${I3!uksViq0+{XjMg=Pvk^dh`u8%v zuCN2f2JKq6jw4+2a>JQ;nIi)&+)OIS=4?fBud2Di$pTD9_?QrmGa7rg3h9d6#`uy( zSRs>=eLJ&%jMpwG7fKE~gL#|oGQpw=_6}tt@l=}hSiKV36Z-B}>65Tp)*65%yN4{! zv|vHYeE5L}tCCT4`=XmF8szdq)|cBlkBJ8~TDUo5&7HMERw}wH!%`(-fWgt2F|u^o zSftiIznckcn24pR%<0Zv(ZeAq>$ZG zP#N-yUp>|cYzP-nCjb~;g}7plxVBlDqNd7v!-VsIFpgj@*N;K0XtBM_B{2gz~+krFzws}m# z^wcPqG})(!u>$RkpbYUq%Np779rFsM=ihLnnI@f|t#3J9YISK6(Po5FG5E8{ssuH) zQY-`4lzg?7?dfo;w`C>e>dDMa*mOYUD33L!IeTxYh`%z%?gR)+Tm=UAFH}}oWrNRX z?~e8$Lj@KO2(+2dGnM&|RgJuJXTFR7njM2xYj%BtY1IyaC@(&r;|~6wnZ5;RqxkV* zMH=p{OKGxo*YqDA@X>%-^qO_YU>kcK>y8Fo!NZ0PermSS+^>0~PjS<2cGG@OuOHQu z&#Jgpx3S*uqsa4%NH8t?JMu3QDkH{BH_PL4QKa#7u zeFxMi(c$JA;yIfb>F4*FIuB(`fj*uM+|jnNpV8k<9_B9D1bENBqPHg%KdK&U@S&KL zD@>7MLZGtjSm$+jlGccqHdbTL61CQ*!lkj9cW3E(Yc}rNAdMu0J@MmE3t`ZY=zC+SPO0OpY}*c^5Ku zzTODby0l3!Xqqo8Fk}c%e?kCjRNiRK=h93#^+JkM7~|srU$_jHr*nw|@**8Eh&=oU`o%cx3BO;=G0`-Cw#tCQh|@ z=*U2;>@h73Po)H2CJBEwu@FtvLK?4>HkIu)*eexf%Nrij(CnK7_SuQk13F{dK&IQ! zC;*}yiu!#Rh;|bZpAfuKTkUG?2tFQwx#NNT6#p}g)x=C~az&hZL%FH?E_}UB zYpQ0{qoS^i{^0k!1iMcCR~6IK;`AIzdU9r`tWC$SCE6+kSZVYczs`vt3wI+rJeC3e zYD3!v7TOG^heOX^Pme6|4oY1zVRcGfwl^|FXS$?cgWmRJ4A5AuBpIaCuW783mmOIA zSZ~=o{$t?ThLhAXGn9}4N*nqMj4K^xWaNW%w0Af@s2CXr zPnXMHZ!ree>Fv+k)H^&rb9R4`8YQq`1tgO2I9|b5X$OsnY}qvFZr`HFOIZnl?d8|+ zP=7;ru0=%6XKo@-P@^9@uXF6SL&nw24=q1!-+9}8AKb;3w-I0@KXt3`ATm4XUEhJ9 zBl)+Xuc(nF99Wd)DW-2IuQHgeIY+_Y#4{mB{j-E=zGI@JA)9 zBtpKAOq|%-h1rIo63m%1Oa(O&_ZDuC=oZ>sQ9rR?%hk=?WXkuwi2k+g-FD!g^agZT zeu4@E3^?gcjK17{CD5BIrve*a|ID)f)p_WPANEqg?KI|C=Zn=jI1TOHh36dPUm|Ym z%#;vUdPORJQ8K(I%%y!Ab(0J42-naYUPD3LB!EKZ&#`vO2 zoPjMvAad!#;f`?GX`dHj>`>WHt^ruq(jUjB&efh&7$k*}6$aO=J*rwa2DR}Io5Iw8 zY{MoGERnGm9w%P6eUSNqHtv8Z{b#Ai=vCz&B0^e}n|lBlQljTF-DJH!-4Si$Sa42D z)SP6#MUv}O>MzDH4#?sx1su^5Fm9+N-f@-4C!x=iHLWHpk}4;oYbfhQh3BgZu@U^> z0`>{1Q+tQ_=fM_vnda8c2YIbmF>(Mdc56cc21Mh^W(I+2{h$WlB7S+Mu5U-lR$(1E zh>PVn3Q|E1F)ZfbWn4Rbr7%w&)`cZ%g?Uf#wI=nu8)9`*uD_h|f#}|-+rX7XT zMFr0mV$0|$mO=f-Z=)!#nG5yHaU03Bwvsmw@8O%oB?>RO!98dyo@5SMWkR^as(=2KSg_Ao3EZbMd@ zDTvbtuZb}|oMYR}F-JvP;?toDp4PDl=*w-C0{sQ7g)7qMTKz_sN%}~a$$)Ku@df&E zjPD?*S*LT`zBy-rP0@j2ic(Jz!6L8kL9h58uD0ARBP?>i=SOQL=#s8s0trPCi=$)^ zYiBZTjDL=H62S=#bj_<(I#A?A-fi4hYH$kiw`xAiz?GNR7>_VfJ0L@9+?8 zmdHU8KJ1yEXV?Lo{IDjOrdhGx0zVbO&}+^3qX}Pm1hCkh>AOO4*NS%?ZxKpH zl^ABVP2zKsm~ZpiH!nracHRNRi7Kw&j5y?uSQ>5jiT&+3+j2*u^B=<_i?Oghr17e; zgm3C%fIwe=@nG7vF<@_c?$EuB32(AFKUKQ%fipImJ|4f%J6XF1(ZF zDpa&MS0MfTN(a>@r=nY?2z;r^JjsTl#rKc zWX$u0PWfLKNQ61nW z{7W>^YQsOO{NB<`aaL{DfG3{8Z`;~MWo|QJ8}>5Fdb&+eRAEls#pdz24gfLsn+}~0 zP|_Lko!%I@qQOrzZx2F3(w|?JL1Ptz=O+3YAK+!fKY@Fvi6i7dkt<`*I<@N*f0_>m zc#+WD1L*9IcDO<;$Jkw5MMfl~H7kYrpnQQKM`L?h{aVrvQGL$PuEX&Vr)urN5#zY} z$kv+)Y(uoU=MN*`GAmuBCuNi~dZyE~cgoCCm5!=K+;4`;g9IPlHm7lgDUDQM10op+ ziK`$>1tP-g9|IH(@2BMojclxv7Z)KJ4Ahy%6zyxuhMMP;L3dl(h6RpkFXFF4{XYeS zfWbXhMzTH^)_Ofg4Lo*C)fxVx3KYd>G6sajFI3J&#UfPQ{}ctyjR?$B#D{XAl}hsC z;FRNWA*PnwFGso!o!K#UYP_gfF0;%XeB0VyN?n|2ZnL(Q*q;xp{}{xI&~(+rUrPs$ zmzs-J*ePKz1U8j-#*N4H-MZw?L%RORuiq9M{;6TMeBGbtA49EFg15<<|5P&sdOO~o zzU?l)+5Nsg)h3#_bWaWmFh(}7D(3h zLBUhamGy70KbzQFJev;cVtXW}h4fym=6{nZvjeJ%u~_itcaj?mKj5h}^Z)zQqwF&& zhe?fVrp`@rlTxK>6Z_f5cU~_U^(!$Fg1@-0+>4XeUCG~Md>G*C^bHDNiTlMslD1+A+D$>n#HGRn(x~vH#TBguZCTwd37VK&P(Z4FiarIgH$n{ z^_#9$#+&&Z6z@^(#%=Ukos55NV7x+l7yX57A51kky;tyw84B14Un?WXF_cW|ZtHN3 zS{r$loa+y^v{A2Iw6J!be8}229afAXCXK>3tKO^Oj;qgs0=!<8eQ;GGnY%q9sBZO& z_dR{7O#3(}Y$fE&X|%(RHKP4~k>J?Da140D5@S;r`Em#{@i>SF_J$VAjwAfcg_OVn zQvvT5O+3k2rt!f`n>mw1Wo}Oy=Qz8o<|s7=fD9r3p@Nm;Qw~FSg#5zV*tTuO;CObi z&;1De@1%|2a$R`2gqDSpcn@>hiI@f4(0>-nCvJ9Sn7B>{wXy2Q;Gm{&j0B=)4El41 zp-kt`uP*(4YVQ->JHUC>2*}hptZUAT+R%*~@a0aPBu4tnG<%f%9eZ*@^>Q@ZC;}JkGz0IH+?BVm zZZ}v6X(}!zB6Jc3*(g^Iss3Fx_^D!Ynen|fa z=51nXb0ez0nov}N_9;4=3Rs#hpg;%VHvN;({6AYWt!q}Jq|ugt40bo#>X^B+_C4Pu z9MWjYIwDQTI6_MG;a_Z=;eb1`yJG=d-XI3R?}Xil)xL{*U>6gGBJ_mT%u>wD<-iRW zl}s$vt6f&}JJXIH0?lPd0J9v zty@BTmR2cJMmW>ya_TO#jxUOATUc+Kv~DyyjyXn65(M0*zY)yYunRGodt{|kd_d*k zXb-2#F4>eywaGNC6 zE}C=bv>SsLwj)myQgx{&QPU=$k(UBfQrZi@SLPsaDKmM?cpoNBe%ho7tqx@?C+f{L z{p=n@6{mFm>1tjSs=i{vRvRx~k1^2xxL}EY;&!RCF6`TzC<1l3!o$~(mZ!aIu}aq& z1{i%{>R>C|a9$TE5jB*8YIh^V^fd&!zE_lXquOOm8DBq;w^%_Ro@mtPrHZ!JqadM$ zk74Vde*a@Q-sz8wp%qJhXbND$rm zh71MtB-qMK`!&##nNg$HdU8a|x+kTX9&62vd)fP6{w7JUPnO^}xmLaII zPEDQyiM%@R>C<@C1Dnk>Y#;_CtgN*e!wo%U$?h{97wck$%aTdTUY%XW7d5ug7qYYd z>@;L44MBp>4N`k*CXP32cg$5t6t4AX3^sVe3M1q|Dk)&t5~MPS;s%$dgt z4{>b}a^h-1E2pC1MZZ>L#ZK#jftm*pfvml8Y@!H6ic!>$t2!{g`3VBn)OgihSi>I&NXzYNu$@vCW&!v=PBaovhK$sI(J*-Sy3bqXzT%VlD zWg@1?GN#hSvnt75!xc(q{4^?i}7oUMkPm4CkJA*0f`&(i^ zj;dNKzTjoGx4rE7K?;RaLEM@FR4A#rDICyMW5=gXj@m4DjWywLXNU4L?;?pgtC=1+ zZL^e_%DY|YpSEXL)iLj^(!VAjkd90~Y*hxo;H?k0dmOKuz2iMLsM}d=N?gTqwtMpC z1b5oreQ#Ft&N4%Ghhep6f5xdUjv8{~ES+!VAHyi{ViN2GHi7qCvY}W6Ok@saFavp~ z)f>RBU-_Lp`{gTgzSNdj8AU)xVE0bvayw1n7T$Qp{q{o%yoPx8Vj)3Bwp_N&{LOHp z6}x&VN;1&!l~^FDe%hq`EV@Dw<%Nme3?2yi$zf>O>v$z-e42~%-PC#~xQCbq`&fk} zXaYhcUXif@4yUa9-5&h#)G3^z^0E_mylZ6@fTecXxfE!wzr;>XLDXB}%?9}8(E;Bv zVsd%Sphis=5XL+usP&WXkQkDw0o8nvLi8A?sU<7q8(Bv-tnMbMZ!bLk4fLu~3i;jP zGqW=|r6%27^D*+rV_t9up5EgS0KcIQ`&Rva?`1fnzHnWrhrMgz>DOcc36 z^*sdmLm)x5&14^)uSrw*Ub{{hYBQ9PQT$}3nRWXYZ6ODBsaBQ6%451SYa#3|IT^0R zVW=kl)dCBzGKhBpfv6)D(iO3CWrXmvr*RmDeM4S7sHSr`V)7R}wf=Ucv!-psCwEhE zMO#g{MY=>65a0p0Xbkzr>}QEd7m;Eo5u0Y}RNtFYMX)RJ$_vH001-V0$lS=OIf-#H z`l)#cwzOT)52g629@U0_@n7fUR0%x%3YV^MA2cql;zi{pLLo=h*LKbSiYl`Ba5L{u z=N-VglytaX9aV|u22?@KM8*CKmzdc_C>78|sz{SEw^0U=%;N7_#DCB05C8@P|y8jKhurx%QC2$CrHKff=jGeYbU#pmDYVQ zlY=xqoIf_`qe@Ify_rM4ZyRg6RCkm|Z*IS+fZEQyl+tIyMko)f@}N8FR!+|qBcYY9 zqpVkec=N2c~{BU{u$6%Ag7)p%S$O))7Oq6$6- z;lo%4l}}O-`uJyGmZphB+slMhvpIVASBooEzS}?VT+&^*DZ0?qk`NdppWj3)c*An; zuD8Xk_04FlS#ufleEQ;Rz8Jma)9vJl2Q1!q-}Hie6Wc`kh6A+m%QT{;|6#kXWZP8L z?XI-SXXRKZrT5{k{ec0W1^DP@xJDg97IO|^A49%3?2!&Wq}dO?4Jb8^dN<|sVk%sH zdzeD5E2M+%MHjTIv$4WJmaF)w#A``VK#N>FlZL?i{v!E_YB5PC)Ao7!_o+S8*VNqd zbU#zi4*1mvw!@zy?M*6?IhCC+ox!Wa*z)_i0@E?`kl~T)^KfMoe-7;1N`wombEiJa zq4$Dlw6$&-iLsg@AlZsuXQ-Mcw>Fqq8%mHf`Vw{xIhE(C9yQ^|xR<>xBudVCE6)fn z58QerELSyr#T_}wM2|S^t<}LBn2Lzi_7ls0JW&jJ#p9 z>1JufSyA?2rQBWv-EnW&4%a0sVqg)j%>K--p{BrshmTxYoG#F-jF8SLR~FOtPN$?O zyOfP)6}V8l*qo|_j2s;T?92@}z0d}&$pl*&ONZ7mxl7bM%@@1cCWFgY2Fc``q0)!5 z7*2pk616vMy6PlDA_ff&RR*aUQ3%F2CnA;UDLpo!`G!}EI!nABtV&Gz)yO{f$4sV7 zB*&T;U_(o}-nbG{QXIOiiKs=2^22oiKR=>tvkWiBD7QZOW1<(Yk>tV**-NZQ&<{qy z6EKgI=FIG7!=cs8u7;iq=@^PX1L|R`gO=yE)lyK`fUZn@aPeUNx0KNo+G7FB76Q6^ zDMI~Q&9~Ce)}~ESQiGYI3WFOvz8|7URQop}(oeShb|ULn21KPpw!6u<_B@79$LwD; z#EqG8Z_Os+lCe!5mg0=K^qz!n*I#)|0zwNmtJZ1XCmwc-Qi|xNseMh%GkN`+kCe=P zqGk}8QgWZB#C@yoL7vmqK(cB-$cuJI0>2{6+Jn(Oy)mAE0!R}^UHEjaU+6QItwf9I zkZp9G+mu;uK$*$aoyn=$&hVFae>Pqkv0$W{&6SNNFcT8;O?FM1Kc|r%_-ROZvr(}Z z9Yl654G`6rYifHnARj;gK7!Xu;itEXKN7rLX-{4b+RZ8zTRT5GIBRMDmX!3RQM=p7 zR2bVEVfS#s_=S-zL4HY`%k4BCMENeObCzdgrLC9#m*`p9Ej{xMC>8BqSXReFvHm+% z5Vl(VwV?ntVUX(1e0il-w=7+G#28c?cX8^DKA+t+I~C7I%d8FbIKy3achqPJ-iV)P<`qBXhI z`PqrjIFW(VFB7j&Pgh+7l3GOH|NQFwgrmp?mzEIts*bm22$sSQy$sCW-FB*$jd{ZZ z5P#hMWRsVM3R1OzW9GJ^2`K|NHP)a^MN!Dt*f!1c^R6i0&lZLNo}xZ2M)+45Ek>P3 z4FcjbAtQ;>4UzFz<&UYxvV*HNoaP!;PXTTw!ym!2Mr9XE#-D&FR30LH3tD#RqsaGa zUjY|n5dM*nKvwX=6Y<+S58dQO$XLr2PLh~jB=olLu+PNBZ%Xes>(?Y|IUM|>?ya6O zsja6~$;V|7w;=qTJI{|e8a@*MbL0h(BC#W+dZBTI%0h_$UQIrNWL&L;TFAL8`PnKR z*FPS9c{9Z~V05@PeBwbb*uhO%>8o|eSyJoaO{uGHC-+$lnliQkeXR^98O-1_w6XC^$?GFgFH`S*}Et_uB@ zt#Qb$;O_zX`Rj8ltqm&IHI@QN-tdcm!M&FVu>%G(wY^@Q02$)&I?epYDPXPnNJs0i z3dv!iMuUZaK!vn@h{(u$>t{t-GgzIGnkWIyabTsYsp)6(cJVj|KL>r;GW&BF=s!a?824RbOJ!Cf9`|zDiHp< z{XQAjwBd63FGgzChikqPsJXP}4xZv(QyEHXo71D^sm#C{tgQWVlv3p{_jm8096qAn z;zeL?V3V8a{Ley~Pammdj|z<~X-!bDZdt3|h0#R-e794-a&!G-P(6k6iPF{i$=RME z??uti(o^BK=}(rB!Gy$Pk(&!rVc}N(%Mnzxhj7)kCH0CyJ73+m;slFEgz3XyEq5lx9{BdiUF{ z;@Dm1;M&1=UW&D0v;GHX-u+SomPmN#*_V@E0WbIso@o>l__HgyFZ#P?tAa%ptcu-! z0H)G(-7iRHJI!>4dERl?jFhL|STfp6)ZSrZ;j#%0%5e9*KsOE!1(tP8zt7QXVXf_0 zkSIt_Wcg*|5S`B`r4qc05xjKDy+VyZ1jIy|&O|u@d%;4!j3{uysXW$tUZSp5l6G?D zGbTvBusPkJB$QIjvzYV5gTX~op64Dn&Wl8EYkkZzgp*;!=v^-i-SG;09hJnQw&M`gNClfUUS@>wu2Vd%v*+|3q7v9A;-dvxe zASO~qokK4#Xzx6a_9&ow<#P)Oer6V~{?nTJfE*7gwM3dY2nkvJJ$#@or-ls+b0aH9 zA$11++X*7SEAgnv(%OwaXPtFC~ z2mxy#^_IQJNzPBkpWy;MQ!YPIE9;L*DbJ5@vO@;hLz&#HjkpEQM-fVV5USRRtb+Ao zy*G5^Y0~Qx&uu9t4ou`aU#;l{3pr8Aq2NF?@B^M;p=J{+cb3Fv=P4FwZ6+H*HQwa6 zrpiuc8bkY8i14pGr(Rwc12%ly2J@k1<}o1HdNa)<{9fCWP}O_8q7CDA$2Ft;E7>t2 z8TNhcPTS@@pU>?ky(O{RZ0F7vc#OCj|g% z`h9n8Jh7>#%xrW+Ua_!*iy-;RCYaDAqP|74FW|VBt@W`@g3BzMb8g#OG}p>!gjC~r zTsh|adZEl8fA-wb@>&T&j^OL#nbz(ot3mXu{bEs1f+4maLzYi4Z{UXb@mfeBNuyXClQj7* z{7dp|KIOIHMdWa!)q&54*6lTR=oTq4B}C!(q@>NGR}#A~`>7hmhDJ|HJmA+`_G`l9 z262bDj=QR&`mVuJSK|M$x(c`Br~*L64+C(ChxelCo+?J~c9zx_s#;$J%(=_I4a68J zu(uh-U-!D?4O@6AdoG$LdiQ0zyy#S-``L8ehKB;n6YI&~byRTRR59db1AA11MI?pz zR<0?KG=9)lpYFwsjQ>!!iy3~Q;G8<|AL3A_=Jaafz39iPhJabL#;wGE415T$fs`$U zPC2Y|el?Z*O`;Kt8jy72KY&JT-?)$?{Mxwb$E$|54~=R2A>R^b4jrok%ud(z^5xV5 z+IM;fyks9D=6r06Eu!^~r?0Zm0EK~Z>*8ot46)3fg^xhMS|6l^JuZ^Dss1~Ps4JiBY{zOgg zeYX0AcwmU&pPNYFZHh(b$i?3eL5KGIvg4w6?yC%8IP@2U|1orfv?{jSbE$7DIZR7f z&FtnpeNEZ}&(S_Eit{5P3n$_YW+vBSt%tlt{IYMa_wic|Tu`=lGlmx4UmiZLS#M7J zl^q;;_El9+(o7A!T)Q5-{2e#yDPc!^q>Wb%x-P1yFZK;G^p62krrJav5h1VTKbvat z6M-ytU#RshUwj6DZRq&tG&m1iYpd9Q>OX|*Bu>fYr%}Lfi7w9|M6_1me8h#@L&*6q2Y|lB*j7Tv2S-W)ZvBCU zo&X0Ff?ODg_|xc?Tu4{2_&#=`Q-TJM5K_~hM@{)jW;tGw=Y)xnwLfl3=~Cd3&8jx( zZZ03c;qo+$MOGIXs}ndh(`Zj@uemxl2PpRzSzw1zlZ&Zh!PZ04g0dN_;)-}VX=uJ- zvC>@}1&pPDI#^st-k7ST%`(IDRAs?E%S88WYg4BE0Sy<1FW&NVHJOk{B!Vfb0o1n~ zO-fyzx7C^qs#jG_3p1@gA@1*54S3Z@PtS}Pk72ThT&c>WRHNj^HG~*y@q6tV--&+= zyft0e-~AHXnN(pt-3kX6-4n7*6Bi|k?C}^YeenH8FsT7<-;-=G5L^ta86q&G>VHMr zcTY8~K?d;;ay}FuAO-{IY`6ne2Z08g6!}PgUpr;l>7&~7RZ*zF7RDO&3oPL7%$X`> zIp;QKb(E@-Wkl{%@?x;bK(n=UNX?eW7S!1dxLNCrf2#9B>+#7vXcq z{`Q{z4WS+(Oxvr!-z>PX5vh8ii2?WNg|@{W^YdtQ3UOgbG3yz3UxT&^M)H0_{=J>F zAAra^vzFIhVNYoMSNiWXt8F=jlt&b4W111#l52l?+q2gEFV8gf*pXxeWfrfWR96P_ z4g_un%l{4aL>grPu9&J1VJ7>AYoYENWPh_xw~7aW3vIqm-{MGvli!T}TaN)0pPeYt z%}v_(?TRVLMa|qyg!CFkmyw;&@<2$sBRL5iCO!^`nt2yL*Z!$NKhTjjZl7%!`dsMo z=(Ij>+^*9RFnD;-R3@TvC`DEN>iO2Xw|U;3tZ5B~-@?7WDY1eR;+$f;?L6Oj>q}DqqS!|ojI(e}q%kjfKiNc-_etS$5u48vVE@=7?xH_1yc*(8Eo+#&6{>*FC!Hq{Ot~`I1I! zEjEK)FIu|xl{#ICrkTa~uiWyhEDpx`&;I^wD1aaM7B)S&^%>pN5I6S(-;SF@j?_mh zphXHc(y`j|y0?u)xSe=w^9xJthL0;Q;xCw#%ghwc0<)fQ%lYA1)hn?Hj}z!Kf@iBaJsY)IaeRAG?kNea(ls z@pB8&!!W|58~99rbCPiA<+eLlq1@}E&jTTWHASLaOhWu1@t#fVGj}**`aH0i_ zrx4SSSyWlcgq9e?>7ABEV&nvwXd@LOHJMjpU*T@{ z6+K*evRyj3lkeVl@OxK7=zPt7to#pT$2u+i+^HQ7_M14jejr*8zPnJpwXvCRg0!iS zdCo}AZ(6CXW4!6k^qOKz&=`z5uNl)V8 zx@!l3R|~oEs!9zuuKzG*v$EqR)Nt;=GHJu72rSlA;Wkh9?4uo;ltw}ezBP%ym!#_> z>41ysz_?vIShM-Zl(Xd+vzn8WrnIOrI`rX(Cs$1<3WNC{Adu( zUQ|nVOS8!Af7GQLd_v!oN87I>8jGT5Q%7n)cMjzRllHaPBMu|+FL`$z_glJJ-5R2Y z9t5pTjTpe^N@4DfXdZ5{PXFOl3!zM`TLHBH+j%ifdwZ|FTuJ$Vnpj?y)m) z8mbsP)$LW}aq(CGM03uo3i{hzrZz*78=kQHk2svP&uOBY=OXS7U)jIbBRbjkBqg~0 zkawlkr)rQ9x@Bo9Wvc(-x&tWYrW!0~|HL5LrVE<%tq*t-eL(0cwI7WRXgE1dR$M4_ zxQ(tYOL@m_KN1`useAa7^l7hro+qa~)&>%7NB}TknOfD28iRQ(CTS;yPWNz-G}%Ap zGL0sv2(c`EO`y& z@Hu+c^2>98|M>wRPp9{T3L=FQQTRBgNSOYKf;LLD>RjKe--c>j?7H^s{OhL1Aktrr zwL^}-;=#_B4)k8y?cNAl<5T)ApO3QD2Dxr=EENr*UH&mB4XrszW>UI``iD~cXA6uY zYD@TSDQ!CCVlObygD=ZO4J18Qydbq3{d4A9=Gvn>13UqDyBdYkkV~Ck!L;iI;N+XI z8oF13>|O4v9|P_SGKKsf;UQ>IYW*9Id{ga~TS^YzX(FpezjqzX4 zGVt23webo=T~#W!n+SdvMvlG4G~vIma!TsZW>4 zs@c1UxrzK&RnG!pCpC+eCKHeLadtcyU!Z7qlfg{!t~{D=AkZGIzI28X5n)biXZ0@m z6-`ojU3)~XPIt?>EaojnI=`QOTtUpcDqW36w2lx~W5Dtne%Xq*#0e&ZlX#nYMX*lX z*ac4jX4O_9ZmLzaLDR;bCb&wpmQK}U)qH!YiE1j$`~z>aV7pJqbU)oxSOM2kf9+?N zSce?}tji<=Pk+NXGfx2EsCx7t!>-Lky6&F!4riSZX51mi^-bjE|MHLUKN?oRk0t4R zzM_U+!1FjUW(qWqbp&c`MFYl56?!yhO{rGHHsh|>Xr&F-EiVs=M>oqC(V+u#3_j6l zw(3UyGyN~D9sdUa!9YI0Rcjrz=_GV|*0~b!l5z(%$ZOszSkS7zNv>zao+AhjB*Cr^ zTi4{4LaO!W)U3_jk~$v~_^wA{1cVdsTvn;9O(8jupQ)-Bmv1X&jZl?|Z05O~+c2F( zi=S#{Y_X|6-pLjLtk+{nyyF!u+`-f@M>{mx&3Z?|najx>YPWq*L-{ zp~&4*1M=2r!}$bEn709<>dqCQ7-jG5*8@9V(v2xL=JN>g7dEZpW=M z5xKb)v!Q7Q?X$>VTH|o$XV5~Tj7dB@VI|yw&nsSm;r%{1BT=+x6;s1{QrcUAis)?A z1?gU<8xE12{?L8|C#(K1tScNiKhhJ*#sqU=~8Wi0|d3VMQ zGf>oZDP)c>-Kvwgo~FE~QL?#*K57sLQ(sEim~L&eI;jjf&1CqS!WvGYr^pdlWPc5K z^{Y1{*#6J7R)@|kjt+5KJ`KAT+N4jMayYJ|#GVC`*^k+SGN|q=lm5?|%TQhMu)}l$ zx#uRwPJ)L+*P)cn<<6{d*fpK;4@ippNomS%WEtsNo*0A9&~4@b{oZmbVQrFoc8$kT zT#|0*t2$~_kD7HjB!=mbsXz}(slCDE*K6@tOg37(C?9x}Tsv;v(zK1wo5fS*cP>M8 z$hfKF)LB3Vv!icnT%StXCl5PDvfmO%HD*0U)DCM6T;i0!9csC5SG8wLYpXJ_=C`!H zZIOfTI0C%!CSg}0zmO&~TSR`)_8$-U?d|ZZBX#dygW>Op)~_0^$>$v6zGR0|ao(;) ztxl)}QZO~NB+~~peSfCwc6aJLrw4aVYX1Q6=DuC{f8&eIJ5Y>VPVAZdtLUEv>sI>K zm?wdW$mex-)2P{#vo)CKienQ_lZK|6=}dv7(q^5E(<+26X{6<-%^1%?RGE~8&!s*o z85@dV|;+n)5f6fY#w0&5f}0<_XL5%cL;`g>wQTBcR<`Shre6mA3W z#W_|_T1jO?CtOtVl)&bo(G89_3yjkzQ_$6wU^|M6;elQVskq9Kqi@Ykwei-U5na5I zQz=|hxYng&vJU?MtyY1_E)*fOwz_($u&~q zREje0a1>t9XlQu-;8`H`=tW#P0oT zF7oN!c{#2sl`d(_U)D8uxN@MJRy2|mA!5|le6~+|iH}<4>R~#9NcpDc=H$_w)On;~ zDcq5m8eGr#P*38xP@lVAYi{2#uS)%&4&N9>?}{vRVg4iO zJ$wYS^7GL}bAnMEh)+D$=ZS6sXFzgxisu=Nl5vfuyC_P?=&NHwOOxbGar_nKO))x! z(&v*QuS{hoITty~SC-!_vFq0WDaPvTQ_$z-EGG?iC1=gDuuikNg|eo$NUDm9UjoUK**$f8m-ZEUn{wj;r$kA z2zg^*zNWh^F2*<_3WntUT1UaxnkmtCGsv84xs0+6`y(GJvR1VG)Ah~%~rF&3w9wb^BUf>xE<=Q zbIOWrh7BZH*oa2mtJ4);G37{eTN2KX76oGJK%|?Ho|TN2r8JaJCivjL@j8zm-LEvb z07%v9e;SiFDIp)jUR!bZ&(^6P1~qP1Q%)SxF~ZXkvs*;)NwbPfj8ZA0wL-9CH6qeR zGg7MYTE}B1G!s@~LNU!p8Uop?G2rlPXrf%2C$oPpP1)&Q;qVV##0zL-9JVoD8xR47 zbw3St39Pl)+Br(DM-|Zo%Z#plRk#z1a;|EQlQ*AZ45aeN3H+-<->2hUP%IfRDkei# z_$y73K3dQ$ihFTJ4@#MkfvJ4uDs1TcIqdxRd8l8tUjZc(q$sH-1y{pnO5}ir9g{0a*Eg!=`=A?}?){~G7 zT=uCUUz;b^r(1uMB9=7AZfbdlV$HZ!QHpzzXp5j2sWy*F&AmoOR2a#}YRb4%zP4^Y zSk65w9)3+CtW}U=n$;^*Z;AkIIeDgM=BFX%lLEz(lJ$RM739EUD z2IEpA9CB%hmNSO0L--pu-xR_W_Zj@_&NJ^@UJ2HlMb(z@HVXHpdmB@gne`s8a?YLRs9SwP?B9Cj^TF`1m1>DK!0 zh_R_Ls_|PIH;J?~Z<-j{PaP}D{5PfD-)aeOFpw(|&3Aq#@GZrriyg!S`I!5l^{piSCw1%(IAr^YVqH7bbcN2m5tBJ^MKgrn(2-sl^pdc>2uH_3OK0m zrbr^+pL*uz@kNYL?h_BruzW$|3t27M5^lcq<%)JOjU8@tpB6kwX1(%M{Gs^fw7enW zQ5}pn&$Ko%UUB0)xTd-+vIDs1uQi!rt4#!(yuxwPxN2T*=c$LS?9ZW<;JAfk45ZZX z#~Z?9 zt@vhp$BadD8wbZV(doLBZ6B302CINu6)h4V^x~xLRhLr$-iVuPXRS!gC5*Oe9cIOh zhDAM3H3fh%uEKu`O&Q9fXyg7dSozv1nG}{&it($+q(>o?sjs2@d8eDb3RPddjEeA| z5L(Lx)Qcx4JQ`^pJ~LaGa-Ff_tD!rpOT3OM1O$*OqIfbzr8(eKv5eGl3bSs2uS)2r zsf@)%`F$zC5Y+r)k$K?PM=71h#yixp^)(vf6#dw(U`Zsvvc{^qjL^X%Ytdx z^c5QMQbuz{%1Dwm$f;w(a%hP`6*^n*q}a)=Qr$+Rf16xn~f~S-2)jK(rW9Fuk;xaml&9R}XPUH2d z*9NLAtDabXbvK_7{vr9+SJbpfa4H#?ZO?i}xQzb*5U3@UPXLNnD6tFUpr`L7@l~LZ z9^$3mqmpS9hseaCs%^_vmX&rMMO0}Qc5**DnH3$$7m71X4xis2oiCW3HxI68x->*{ zHfk_w(=zrVp=l6*5d7;hwJa+IPJJsIP<+k!)ys>Ku&6oDdd;}YHfnBB1cZ5S^sk+M zGh7zc?vR|}`24HrT~LU$*_@x?KgzyX@zk#-uW;qQ^n~(jlNGVFcR3r*T-81^-|JSc z&RCkN)#OWZWw`wOxu~R*wL7O341=1_R7gnO>N-|5%>2}D4UL+jCZ=Tvy)l!TM2%C; zJQ3?m+k;ERFdvalrYIe0%So*PF;}l9w>%@N9XjzP&)qp9u3|GRSN*WT8kP+ zN1^GyHL@{@B?u32MRYbkE79d>!9nv7I&oZ`r-5T-4V6CGt*tk~5hT&Kp#<|?Q8wKk zz2O@zIpK=tC05lXwL2eop36^U&vqe#a94)xSpG%_;fh;Mr9gl=sK zD;`hrZ$k~PB1BUrK1ZR=aPxRl;mO@N`~`P^8m=`cwdQLy&mBd1^xE#f9}bd!0;XCq z%1BT6L9ZRRB*XCrl5YrWHnHx8K;Q1?G|9Ygs9KNS$tR}^O^4zvfw4CCU#Cij$a2uF zH;4Q}>xYtDA4WV>68LXhOMJAi^d#2c_=#v2CLH}mGH)J7JS-L!10BUh%%<*+BUG@y zwtP$&pIU-A^U2PAjcIGTlHOc=jhyuW)o5(xmQUQHF+P>2GBV|#Lqozh4R5KcxjR_! zYrAPoZM!GyTuzszYB5Zby{W>SjMrZ(O4g{wNNjQ|nxnfugD!*idYc-4ts*jvYC2A9$p> z_NZYQsjm5ulSQ}#icwmhKT~pOW6n9NkpL8AR57k;(m7S@U69cnk|mjdUTO{Ms!c&8 ztj8IlV<(bLX>u$}B>Ztn6wKXe77%+=84PENY1o@tmm^|Wb6qEdVl(PcMh`6G70O$a zxE}S}d?#=`M{m7B8`rIGPFFRF^#1^a?V8_7wu(dZEMqwOR-RCf?Eu5ni3l}B1jsZrAa zib67&{M4AKB#-7Mnp3qjK!b`gyMsnCO0>vg#-BA?ju2L>May)nk+uNER_1aQA}Di; zUBop%a6Nq~j8{BS%0VP!aOR{5P{k^P)~55*`@*Rr2xo{M4Q9%N%qv=RN)JO>@*kX< zj#DxXzSL6gZ%QcT1Ff025zR0k=Sj(;yTT)~DbeSOj8oQzQZ8BG^);sm+O>-efjSD) zx12P8dl{)lKhD0d@hXf?oPtPtgH%6?w)`@%G~p#9 zX*}ksSh((QXCrTOlwz*wnv}O{!N*GPqO={(XW~Y$ zXJx4cxC@MOTDi~dO$Oz@a-92$xAAL1ZBF-k$VVM3%QasM>d{O@4t9@v&b6*SW46?^ z%?k2D5tko@P?N%XOCOZ-d+}T?)|cW7fAxN0`ZW+u;$-9Zst2jfbLcgx+>Zrm@70&` zsII&pVlCJ#fN|crKkYvf$JR*(Dz*N(u7tyxD>gW#a$Rdnk4v}@8-0G2vEVzKG`cN1 zsR6s1;qSFO`EqjlHR3vN ziHb;}pXJX&dRJZH-x8y?6^~w(4lk+fVet`+Y>wvZ!`8B{P<820N8w0%pU~A?UlJ;S zJNuf_wAUkNA2xXPq>ldpXqi-aed26J><4c3dd}VL*BsWJz1&W!%EOv;g5dt zsYWM?Mo9-lR|1UiC@MWDWR-Tv26Ac24P4p{4H}-b5>;Rb=dD2?^s6(*6!3dhRy5GJ z7}S)lNlcbL5;t>JRQ$D1IOJ6J?VM!QM9O5?B=zk`%6gMlq4M_RkZEKHMln~=#Lskd z(9$YM4_dz%OmwI0L)xSYJ7g=ts?vrGb6U)*7o}0VRSap8Qpxn9q`;zpI)$Qjns(7ilp>sTrSqA3Q)#X5P{6focPMNI;k2PxR&*i%$2us%YDc+^xCB)Of>Y~JJaaE<&9S3aIaPVhINc7JthHIa4MxUo*ukj1%dx8^ z<{N@4545YdCbQWmJ*qrRy{Z{z!p@{%5liOaXLVy4CIg|Sw6pET4JJ}GZYAB`nvy8~ zAy|`3IBxYHpE2i}l8%T>y?0S0%2ye#Z_l65(uwEp4MSR#o`%GX8cgI<@|$|CVYIUQ z{uF(pZ+eoA<#_zQNXV?)$pM!ltEy0ez%(b$UhM3%B6bSi(9e6{gb(d|4% zaHTea#DnW!RBb0Y?_WKB(Dut0h$Lmf^sZb+#dSP}?SJ}MA^E5RN}ANmzX_cCR5fc) zaFcF51vR;(cQ-U~tF^nlOD^5KVAo0Em^H`Mqge#OS8jRrrg&FWvP(ZK?azGIVWjGo zCi@AuBri(dCWoVF@snp7IOk}{KT6So78uJe1ya+ch+CtU3JBvheb@JS&lTBDNPQ1I z_@S@OVR`pR806O%{{RWiw(s7MeR!__0LS`mwxe{a%&NG-Cb-Q@!1fN`B!ecit+?V; z{{V!~N^|qBe+sb=inKOPBaD4(HsiqITjeW{tyPo2lQWWlw@0&1$ot=y=`IW?Jm;k|AM`E!BqL)fH-_=#b!xAETEsdmJe=qqPQP}eDgk-M7Yl$y}`EIZ~` zItx8V$xs;M(>1-M--)BiIj(oaR_PPM7x;#1=7u&~ou?x;bfcUq}24@S15_S-hNn@=?dNeCeUg68089!s5QN z_;Yg=&xI}8GmsQ!wT9;LHu2h*6j5C$&~hA9%gCq}re2jmJtH8WRQWnuiBvC=e=hubDFVlauud#9@SdlF6?nuQ-wTLEAAVuXQ@gg*6O(J zQtXe=epOC3Au~fvcW`Q6g_dMR^s4C`Wx2;+N({d*T80@iPd(~+fVj}ac)MPkQffPfA$Y01 z%H=9a3}#FL-j5|cDNvIjH0I!%f~+dWfpLmth=Y;Vot`Q=jwx8A%JVQ0xz9z-eBtrB z?hCIGNw!_Vde_jp)Sgb8Jio%Rp8nPH-;E>}T9w4Ay;$}7*Crs=8k|m~>*in{7N|-^ zUB6G?Hab-qaxyE+sI1AF)L=Bp8x2lVnrwrUSl-5R8e2W76y}nJ;){|xolT|w8lVx4 zM^2KOdQc%Tno-3x98%_i5&|k~b=@GU%5c?U-LU(IS`nm9n?=xWZjnkIyZ-10t#!T` z@U`?BptzC2MaD+z=R7y!A!`x|e}koUT5pZ5mGO=nt!o)r(Cs``sT)l$)ggbG8!gXl zRjcn56XEnhyUyE{!0TL$*U75dNhy4)uN5`b%odimMoblM39Vy0n?-1Idj7ww$#Cq* z*$u|p$dkpIq8+3@R=eRQiz4-3nxTC!!}Ib=Zug>1g^nqFX{hvNKZvWg{yDfs1;#O5 z3wUQnlcO&+R&NLC%nl0sR(jklSBJ;3Ja}Na_o~{5i*09whC$E0I!^_o$W}7A6_KoX zPHTLs$fNM0OumLby?U2&D(;zl%5N$_!1S+G z_)j<5V{|7xa4UkX?tAzgMdM?v@fFVbV%l@HYe&N}Z?z*m>z~#c*5IQbx^Y^57j2QA zdYV+*x$1M==9^3Vmh7jdITS*`E^5lCkwyh}Mn*E#9v$NiE$uZ~+hc8JB}o;^ z>Q=?uAXmQln?j3GVxCZD{uT8#|vDW-9Zn_C8y;PF=P zBKe6GP)5PC`c=n6$((G-BU-T-%_Ix*=M^&hR`D=m0g9G0ie?QRD{I(Ga_yUKkq`Sq zzM1#|sYiF=wS`-h>t8M;ti9{qe`V{ZJJg;Ct)Z#W1KeA!6pC^w%Uwv>Br1baEk+MD zAp5mjgRt^F>N!u#jGJ_s-&2$;^jkp|ORS7a1rC*OS zHYxMjgC7*`i$-2)jhywVW{9fi(xK}~vDkW&DKm!6HIHgxJXG9vEkipFb5nh>w(ZSl ztKd`B$?Z_Ue5_4G#L_by^`__EuHXz!HF8fPoN!GxprmNn^vzrIL9FXGJ8Hu@4UAMs zq*!OqtyGzVNJT3-?NypWZV0UIQE6BAbj%F&ph|ET6fw7Vlh!aUMX}0VEjOkr-tq@e}kM? z%(v1BFQ(5;#=eF4(ReLnjv}~-8h3G0KO=c+wWoACPuHxUtT4Sp2NX}~C_P&WS z{{YJ(Ph(ct-lv{6p{zi;0!}G^;V0GNz&n@<^e}X5o(jeIQ!X^E4p{KMSoOtaCR9!u z+s1l`JDMh6ttID-<&|+H1&^t&rf&=BayCK8^c9z>ct*^r^4zUNn;FRK8nRy~D!WJ( zk2XBAPg7kEx1h&nxenOwYn{GOqX6c$r+XZAOsk*_oKX?N;L~T$<4nmFGBGU1M&2v2 z_-m^*l-on&p60lPVmPaI6Et#XaUV+No~Kq8uPZ%{YvrFVNF}*AHKC$jg=}uGG0^p( z7$dR#O?4V>s|N(X*A<*@neI}r7LQQ0lr%%;>spqti1KTJ(mX+H3WCz->S_9?k8Q1E zA|}fb$2F{FsjHH)yRU0-Y1ck`O|xxzM~yX@b$xP4)=kHzN$Xi2Gx0^vrKmpQcJ>*p zTRVc!oMSc6svcG%W|Zg{k&3G%W3Z{jg!MGmK9$){DoFBbPg1+NUIjik%G7e9Et(?x z*s51d#j@0GnpRxXBRHuP_pQ>{sv*oJi^WGUvp znD`&7*uIMrL$z6#kPTdAW2TnJqKtt`wB6i&X;jxtXLD_b4z%p?iftn?9DCN$C9&l8 zrZy=Unr?d45pp_4r8GB6MBHr@?WUfpeJHRC8lOzjB4AX|r8$t|sD{c|AahCHr z6!#&FrCNRmr8lJrk*Dua-6_m7O~OGSdsbnqNCmIMHU0B z^4rX-5uv~}VmN>~#X3a`&29l~gRNL^=T+^`6)QFcP0}1wD_=&YA5Lo`Osf%Ex3egZ zZgcHfGas8dp}Un~y?LZ;Vw4(dRCN%U6smdaO`?ae>??Jo$fo9uib;qw^H2_Hxyh$S zK4DVXOJhpKaTQF%=QybCc!X4hlGU4KAsI3;RaHOI4>f8>#X%S*LP)784q=LS8+ub& zfHd$_uUbZhOR&31#TMawzLZ^&w$%pS`tA~NGeBc)H5~( zM4p8$&o%v_XEEq}E_23f=WEN1Ghp$}d(Z6|a-&aLP-XfKGfHxUX+&PvB1$tq*CNn z5){^_W}4Ih%;JsS)HNjou;6n_pp#6^IZkL9(ReFOi%`@SIU5z}8h?eX0$_z6_2imO z&CR^~?U<{3S4C^C>XEYVosyb0$ZvdIMN!7Yp~}s&{%eh$Fydsg}Y#<5btL zE`CP@Eo4mP!6v)SUq#ZQjok)G=yTGz%}OYvW>hs)=g`8cF{k_7RCyIN>xRuikysi-!?y}<*n@Z;J zsU&HgX5@{jBLnrQ7+&1T%Q4S<(=@wAcTz=m8eW{DFvI~;&dPNg>T{O5P07nK&IKe< zv{URg*UuD4hXb{8*W6zmccMF))lHTciW>r(8OqSvHS17*>E+c;NE;h!H6JxU>qrJ0 zt#7HFhp(+TXB5C_-_25%g0R^O4z=ii2mDAh`{?({v;qJG*Pk&B#Yv{S+uVs4pgpQC zYX*__kB0SFwHrm2DFXsKR z{CZY0Dke47?DLUTq!p`tqE2d@upDqIOVFyLjFC)Zin7rVIjYle-72r4BXVfZtv7M> zqb87qZ2Hq!kxF=`G5-KLq=~Sq)jVzURaH4`W~PrJg-neiv^A~I?!XlM{O2aC!@R}{ z(Ef02)_qDOxL^e_4tmvlrkH;TrpF|pWT~b!S(6#*MotGcS*4J-D1Rz(mQBBMpkiDM zt_Q6msR>X`EBTc5l$GStxkJ!=)gzDHnuI^KER%e=$7)vs7{|?0=jE#DCp7)U^r-|$ z>~Z*14oPZzImJRwTQtoO<~&qwj`ZgHw8p4qrZnew6v@XN)Pt=y;Nfd2Y+5`w_LBK; zBR%n6A9Q|ZB-gus(-A3XWY1dgYp?Z>TosMF6y|V7DzuDEO8)?QqmsB;jEJT0N&Gag z_e~=7sU(U)P01pj{{YsR-h(zO($Xz01cf6~(lsZsOL3G@MM*(QwHtlvdj9}QanXUX z;b~)CK|Ii=KKZXgv5!oWcWfc~M_Tgh2%1>{LPs^wTUlyCR!CX5Z5``EqI!0#96w{X z`#JepmjlwVwQU+bQ^ZLWg+nZP8>-Hg4b`@VXqSYvn}O?C9x2wYEc{RBMx^arU{;+D zh?~-QD|CM$kLW8l^T2m4lN_Z#3Wm?ecPM^j$JABm{C4GoJAM_3Mm^7iH5+*Md91_Q zv*ywDSQinATc3LEqVdJEs1gB+vk!^%d6y5hPtt;qIF{d7iF392^Z!LJ*z0OEPT~%)#+E)Iy|3n#=n5B zU&JQWTXcI4dx1n2q*PAFBYwR|7^-W>TGG@AX5b3VkYlxM&Q77nYIpF{Gliwk%bG&D zdrOyWn{&af8^{aB8*Vu3SaCka1a_{1&SjS0uQ;gX)VRBlX^awJ3GZEApD5Y^J-Dtq z;ryG7XOe59(k0($t>2|54GxG(%+c{;nBi6ckWRSvZm)AiOH`cOSoif;c zyMvBuO!`)Rq@*&fXei2qUX~r`d6+6s{vhV%;p{C@z$b{5NdGWEiyn& z0wGpAs>E&Re_F4w(yATQg4Q;CKd4P(s7G*z?w)a9LHscBy_NmEHu5_O0^pK(ubUxa ztHH%+_-|Id({GkrX-cnk^{ry8X$c=_Lnh#*TX12JMOacw^ImcAf5o5Lx_Ut)?vWI8 zo|WiO%e$$rs&YCLO%icR*yt!-xuLq(K@wt6C_N}nw8-j64k^mQn~cx}GG(~-Iq5>u zQJhmU9hywiWcvP8nKYQ2AHfw}ht1Na1mdETIn4wyl-gFL6cWOk-+G^lZYkVX8)xS~PW2p87Rbga{#Y%BG_EzF8$Ra7 zYFJfR9MLR5E=57+vNq93W9LOV6`y?$6p>ek=gvCQE}}ayeJBGRl+trnB##vfEdWOA z-lA-Dsn1HUB||R2xCX31+3((<#aXpLT9BEg3BrsTl}>6j04+c2YD7VJc@#yBqLT9l zof_c<4`HBUH&RW39~6(xM>AmvsYMl>10^|t0C7wNnQCdi?Iv0x2b@yP{;#DZe|x1m zC7=oLp_`(AJHOq?XoJQAwS|ty(NQqhmFLG&)QF z00csjlM@{J16bF76Pz{_x3LDAo;AD+^P#JDzB@f7izBB>24P=ldWD*?NgVzruw+d| z=EJn399N{r;(adaH8RKZFsirL8Z?W!6?y6nCdx+w*0+(UW-qk#Cac~gyMAUZ#dVsU z)tR?3Mi=W?7fM}lpi+a>&UZ1N=Id2tPW7WKPOXi#Qs`p0+~Sqfxk>6Kaw+oXH4_Rz zJ!Imvba}Ag6UKU0SmLF%a*_fMdIpXA*aNt5IIZn91XgOGV~WYOxQ1IXf-)#=Ov2#{ zU}BTi7abX=b$H3bw-~HFN^dlhX_uE`NBIwGs?WNyQFl5eQ{5)9LeeN4n%Db0s;iW2 zde&B$;U-?(*P&>UTit2DI9%6tJ0pftY~t<|ueCzXMQZqp*h4#Q=uc|Jkd>`etVEGv zBsD6WtyGP{spE1wR*{ory$KB{T4v&VRDfW1t3=!vhQ(U8Oc2#xB^!aO(2j@Nw_6Se ztA5TRZ|!+Ld`FH(E8XpQz^@_wo%G6sTzS0?aOqf+3ikW zy-vV9REoqs>8Gto%ain`Fgfo*lVVXu54{y!!nxKh+*`OD)8@K~ml>#*$}`gime@*w%r1jDVMOkSjE(|sj4iS zP7N+$2zq`s6DN9`0+`h;mWaKo)t*D-)ogs+Rf}-Lr7P$Lf}S!d%_cak`5s`{0+-E| zezhswEKPFXH$5suW99l*b;Qz{271$kl8FB6Q(`VOgEb8De5R{3(*FQ-)Kg0!T$s&f zQ8G8rny|dDCbOZO@B5WAT(T$JYA>N^ySE(XqPiq66IOSmPuw1sz%n4EMK&004` z$oa}nwFH%WSAn3qc=3&=6`uJ$c&|25B1Cd^rl{tXf#!|}HJykY-*%Z&eecSf#Q|}q zkmi@YBYo;-aubnA=e;X)QOTXWQyLn~J!z~*tv8w}08@eOO`&?wBGtR>kTrGQ8SxZW z(g8N%oB%~}Y7XqzZQ$J!+UnUQkc`ttkvr)x((!F1LH9wd9WPO{wec+f0BPH|krvIEcw2AHTHoEVv~)Ymd#wuWVV*D2pCn!ynRYjmv>ft9dE~wq z)@_&NnUB(KL%Q}nJumvSFi`AW8HXK@jg_#%F`DA0K`zojjq|KWYx8O*p*x#r6y{$ z5%dWo`||Vanwr|q-W6q$!yJ0nRps`k>_66WeJZ;{sLI(9$&=rjo?&iFt@#lgA?NE@ zR}2ZOnw6FGP5bfNjMh!o zsHJnzhZ_- zRXdYe@V7Ospo1;sN;;OVsOpM|^gqE{BWdB7%P`#B@m=gE9qX0&b8Ob$8MO?4@&jE> zzbZ3bH1;(tp{Y4F8q<}&Zk4J6PDMC#(vy!$Rr}Rx6DrcoW zQx=?P0n{+>NuDWkI#5W?M>M&lYC%#@FzvnRjrXcdpIT$}pv-P)qKXA$tVi^y?ws{B z#bd@QIT*76*j3!ryJ*=;nOqL_PBz-K?=(@*V!2CGNr?K@Sll$Z6&=G6ywY0WHBT`1 zE&l+GMR3P=6!6Eltu}6Y(ztUJ?`Cb7{&k+mEtTCfXCt*m zBcDnJIez_UTA&?kHYVqq(zib)7AobS7p5vX6+>iGS@3wM3mh`{xwVNk-uJ!)29d(tOShpi)h>RzUr^Z?wN zZ#!wtQMPCrRvN?7n@?X;NWcL=F|;qzlFUks=N`R+oI!$>zlRHEpK%@ zk27xLRQ5NCVjmmYx86{GqMaX(tW}6*PsM1D!j@;FB{pveK-_>E@Te@e4~=wJT)9Br zfq1DO#kw=cyJ8P=D=EAg5&i@J0DRMb;R;q=p|<^K8a1QwCXF!b7{|3&ztwbO^A9ca zR9nEZEPpJcryPo*b>M5F{{ZWMT9%Cq(rbD$3?ww0<>0QpD;ky{swvn85&2t*Qo{+KHdRCCR z%~CoTgy40k*^Nzc7*^oaBmrG9YR4S+8zPc3Oz1hO81Yf(HG7d^H=2Ac$fgf*Pf>$X z;)inDN%G?~gyx}9-k!fo%aG8PHwt>y+i6HEhBcnNb*nb=C?h7W8VJ~eKv%6tbVk~W z(ZL)VmTqf88Jf%%Z^o}e+oR&CT_^7|(-mgW{J18D2#v`T1ClUnR@Jw|oYqC*+c3ec z4Ju8^iqe}VMWx7a0j<9bzRTE-dN8bvb6Q>~QAHGU z5DU#NDJfSpTufe+i%U%;`q6R+Q>r>&r57e!3TZMuDKkYP2X6MJG3TW%A?raRXWEJ| zXrNa&7UK(?Vw>lJ-D$X8qn^~&L}@CGOJ8Zv6fDYmnv2O_T6)B_GDQW%<7Zl1N6$4{ z=@)J@PPIE%0MwN&i{586$)wvR-_k`eBElk^R}bCeAG(bwI+nj`83H5 zFtsLgT67a13w+$xrMr#2Ya%1_R)x9wc%W7-7SA;d?gI6umSQTs!Ef>^SVWRC8S_>; z)jM3^RLt1#P-}8^P(?#@HuR=>tUJ^hQyA0Q zqiO+-6pY+-r4+^=Em9`p?)Ih5X{$vh1d2}XwYTAzE@iqy9J@w(X0X)Y=DI%y-y}Cm z3UobaMa1-|R!=GcC^=aB_pUzvYm~m2z*rL9MRpfAzG}lJF}59}J?ojZyGXAmS)vc~ z)!68Xo-ehC@rfAbtV6A9DaPjf>rpi=J7~?R&(friQPX<0x9BQtS(pC+2?n#%=lbTC z`)gE}{cF18r!}nFmXdy7vxWEDM3;J0ajUY&wT~F3U^3(J2B8l|c0QcbX7OI9$PqaD zir8E26yTkUQ!UnsBxIC~`_g+29F@kmtI3kA_&(K6T}xM=f?JU9gvE*?==Z`QJ} zHM=yyNQdEBmwgOzZLLl?W+2vXr(u48?gZBBc#R_FIDT%`m#J$Ha7i)k%`|5%j(YMY zOoklenxw=I)vt6*WR>BCuyfk9 zxD{?j9MkR2I(MX)62^;vyGiUa8`6C0y`$t4ilce{y02*e0C}rQTahDDA(k1g!{Lse zFE!|c4it{n$=a^!^^bu1S(fAN2l#ofN{S*X=eB7vg|m%%ZBPKMC|8cPR>eZcS0Veg z)Q+ge#DRKqQh|zMnttz>inPZo4-|{{s0NirTByQAlu<=VAxgh`l9f&kQ7l?%>rSM> z%>*$idaWVilysn%uws&(nr#FHCMjs3LSl+qC?`UHHHSN?A)6BNd(<;A3&kXCH)j>3 zv___qPo9RHW^Y=h8wz_d3t8?xNu-x^Q{!-}$*k`%`J5V^pE1&7xqWT`TB~(1-Ni%a ze7LIfOPupdaiK087N|-0q-V`DbrwUo{oM7Zr>1HZIqOeTil8Tna(JyPb;63zu{nNg zNn0FH8V}kBH3h&Pl|Jv5tKB!DsbGb*OsP1jjAOW{?AQpp)3@aCbM>sv1u>>NRXc?S zUMlM`+tR7Yo%JMwrAUd|gz3+vJqX>JqLCbmMMB?ttju_*INeEr6ZSY z`cV@T&{ov?X=yerY;)~h7lXbYYTCWRs#~(AGq{eGMMDiHad}`l2k@&_!_9?=>MPZM z;a3}LS(f7k`yAI5t!NWzGCXpQGl7bh(WJ1>DCXE>tugv!Rhx*yw?4IAPB^65%P8w& z7Ch5mm8USt%{91MX_>tQT3Tw;sgj4|JXb^EJ6NQd3qC$-=hr28t-lMc&)O3U#_9`4 zrt1kD5WY>oZcs+F%E2Hb=i&Yl(ajLq>DkiY4(5s-yj1 z=}~h_BGld$c3+-SJ@-{-@YFE3%{K3;sy90BuOeUqzTj0?yw+!20^eLzh}T;!Di#Yf z4EqYDt4*dVvMMJXDyrFP6GysP8$U{PdMTDaGEzzU&_u3dT@-S~7X!6sTrXPR)GX53 zii4WOyS{_hgH>@6RpH0lp}k7 zbX@@TW4EO`Gg0Rh=-gCE6>`i^SEGE!1!hLv)w@^9{qB??E$7yqZU)MRHrzV*sUdE> z)$UEmuL0}}6ZBW-&0Nw*3!hr2cz$fv?JY8x#cf79lJ+(9&{{yF__(j3J`3q`+i3C4 zBJJF~d)JP9KG0^=FXCm4@WTfc^pA!uO{SL&1Pr3!*QZSc5w!@yikS(hV$KF?W%^e^ z(210sQjF%By(!E0jTf+`q9u&fvD8a|MM)h}W-OA5D4;<_6jcd@I-5?Q$&GH*<)ms+ z(tr`Z^(77WsaMv6Jw&3MDW;^B6j7Qe6^Lag9cj?F9Maq-I#hT#;;!wQmLre`*SDfRJX-gW8L%|e;N4U~t(osyJ zMW?MXY3tIakGHKU#&P8?tI-%0-Ac zN>XvsnH;OLgGS(c)^{v>Nc&Gcs-n4RGEG>K!BbUY`~Luq0ikhNdQ+5&hd#8bed%3= z$IL}QPc-QbGjJx0jWOAzV@XYFMFBaYiYNf4^`}#5GNMt}K3f;T{u5s@d{!=eOD<1P zYw9gRFFF#0@I`p1#s2^Xl(;u`>R*CQV^TLqR1XknN13>am97d$w>6=tS-e>eg-&`J z%NFuWbgDXLyxNf{pGz+^*kQ-LZO3;Fv|%zgdQ}M!TBph|YVU=`?nMAsIj-DB z(cxd{YcFYY(x--uR>fdgsVr;GYhQW<->x!9m8-D1ugmv95;|RwFqJ2+KC*_o%ps8`OvCDl3gz)kocr^sZ`4>o(_eW9d-Mb!Okj-<3!m18SCx-|FK$`cnD&e8V!^r{F7? zf_pDBnGfY!mb2Mx19|+aY*pEA`r}89IJaElvZdEF_;^Sc@~uBFNtJ$SSMjQ|+30H9 zyx7h?YFIN0`(Ku4+iz^Z9x?@2Y4tZ7RyJDKywr4SOob}P*i{RSe$>bNp^@~dB%yT$ z(I@Q(n#a71s}a_=ZnZOW*yghDhW6cxY3fY%Ms$nbtFpH4YV3)QqN_{@HM~wHh_?IC z1}W4i6+{jEDREE0tCrTP>M}(ufttdb-nH&D{i~I#6GB(;qnTW;M{AL(H6(AgnZ+sV zPjD^4E(SkZvlFQARMy?uM>Tgz)2=6+DkC;~0b0f_lhl04zYVnUU5|i#H9n_2V8q*a zIRdnN8{k`u$)anRTWer|MSH)3{3~sx!yB|Jt_J`L?W0W7lR=&X@XA=*p)qaRqXeyW z*6EHtC|Now?ZsG(4Y>llqO5@{4vbXFIH<))r{%2+(GCKgO;!};<)}iFO%g|vYG}m|up8Qp!c&fx5{89o~lMBU2sq0OI5VazaO2Z?PFczXz z`M(;TW*cd-k(ztkks%%mlMHEm_ zQ+cORQpPogYd2f4hVl}Upk>E;(`seVUo4s(tZx(N{{W0$9fHdFW`-?^CmHRU;A6Ln zHZD5ziuC^g+5-4Kr46)F4T>^X-n^#r?`({f!&{cdw;K!(sOn2?8*SWrn)gqKw)$^| z^dM3dX&pgq`r^Dl!?OtWXyqNN(CzgZB!nSm#d$T4PYbI(6HxJT`8#17IQycoFT6)} zGO~tm#Md!3&&{-x`P5BxO5-(!l#%RHX&3LWrj(NMAI_-VT@x5m-6}Hsx#p_f0y`dQ za#-KmB5PZeRyj4x{6d*zwisUHFz(tLis*x~ zG>F+e7|ru0e>#c$9dbVI;rP~-zNvn^vdQm?noV}(bXNSTB_n8jcu=qu49irNMr6Pi{mL&{_+K1psd>{%3lWF2FMTi8lFE2Un=tL`PG^HZt8NV zDj)bxrO*1YNC>9zt=Z(s`qp*VhOYx)$;Z~UTgI}#NJ0A6Zl&TYsZQla9>S9gm0QA$ z!z|RW_-+`N=WJ9FYxj#GFT2|{9NN{?zjvQ()L3fPG*?cuwq3*yJ!>0SYjVU!picty(#nDKr3#)DD>GTN@Pi zs(0~)=M|-!MhvD1&JA3(lWNs9!y1uJ-u0PLi)OFdR7Zo&XI(N9a5~ZnIqO2NnAyz= zI-7H)8xREUI#tVSQzI!0QbBIJt5VJ&qPi*5yD^-S*h^b5N`foVz7kp3Hn9kvF`V*$ z3gKcPHEHQOjJ9%#A(54o@rv$*5;KgGXVdx}m9%Q5BODGZWmg>yd4Iy+7d6}o9MXv! z7|A?}^w^&`ch=mAab60G~h?AN0Ub5M!-Iihk5}Nl0O)wasxC}d+4IzK2!5j#zhoWl(!v_ z4_a)bd2~@zB9O!*WkqqzQAH6Hh?}J~sG_NmB_8xqQUkkCrwk~ff{hD9nzEqvG*MDe z?E7}87anC4Pz1ZMwAE@TpaIw()Tjj%REJU2?EBG07Bm|rHi{_dG&Y*lQBpCz=Fh2n-o z#d3Z#K(=S=iYTh5xzAr?&vhvO0DMR_PorQ_MQYERukkU%tvdiwMF%W`iYTB+bX+*C z?+X6_$XXxwg%nU(=ywgbR9-ru{#8=e?kKJqB#0fkITTS{5%^1qWa_pGoWObZtjoP> z(6eld81|xy#ha9FwYzUD{^uthaaflZ@k=8GQZvmIR$viCCtfKgwAn=!H)9=c%066H z_lNBxxNsRk`q4#HNo7ABSVsZ}jY9F-x!oQ`d!<$x8KR1|RCYUPEaEl{LNDF)t}j}; zB34d*S}3HqB4$XGnK`M@=klV8YjX(`B_-5;WOPwm#KJ~Q(RoeBtu;wz!QzT4Y9v%7 z(I`rm+;S+Qr4q~b*w0GQx1x&fqGv6M7w=Qlk|Jl8O&olr4 literal 0 HcmV?d00001 diff --git a/tests/tools/test_tool_vision.py b/tests/tools/test_tool_vision.py new file mode 100644 index 00000000..3f12df7a --- /dev/null +++ b/tests/tools/test_tool_vision.py @@ -0,0 +1,51 @@ +import os +from pathlib import Path +import unittest +from agentstack._tools import ToolConfig + + +TEST_IMAGE_PATH: Path = Path(__file__).parent.parent / 'fixtures/test_image.jpg' + + +class VisionToolTest(unittest.TestCase): + def setUp(self): + tool = ToolConfig.from_tool_name('vision') + for dependency in tool.dependencies: + os.system(f"pip install {dependency}") + + def test_get_media_type(self): + from agentstack._tools.vision import _get_media_type + + self.assertEqual(_get_media_type("image.jpg"), "image/jpeg") + self.assertEqual(_get_media_type("image.jpeg"), "image/jpeg") + self.assertEqual(_get_media_type("http://google.com/image.png"), "image/png") + self.assertEqual(_get_media_type("/foo/bar/image.gif"), "image/gif") + self.assertEqual(_get_media_type("image.webp"), "image/webp") + self.assertEqual(_get_media_type("document.pdf"), None) + + def test_encode_image(self): + from agentstack._tools.vision import _encode_image + + with open(TEST_IMAGE_PATH, "rb") as image_file: + encoded_image = _encode_image(image_file) + print(encoded_image[:200]) + self.assertTrue(isinstance(encoded_image, str)) + + def test_analyze_image_web_live(self): + from agentstack._tools.vision import analyze_image + + if not os.environ.get('ANTHROPIC_API_KEY'): + self.skipTest("ANTHROPIC_API_KEY not set") + + image_url = "https://upload.wikimedia.org/wikipedia/en/f/f7/RickRoll.png" + result = analyze_image(image_url) + self.assertTrue(isinstance(result, str)) + + def test_analyze_image_local_live(self): + from agentstack._tools.vision import analyze_image + + if not os.environ.get('ANTHROPIC_API_KEY'): + self.skipTest("ANTHROPIC_API_KEY not set") + + result = analyze_image(str(TEST_IMAGE_PATH)) + self.assertTrue(isinstance(result, str)) From 0250b7a7b64605af6bd9d2c0cd27096580d6bb47 Mon Sep 17 00:00:00 2001 From: Travis Dent Date: Thu, 13 Feb 2025 13:40:07 -0800 Subject: [PATCH 3/5] Use internal file with web URL. --- tests/tools/test_tool_vision.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/tools/test_tool_vision.py b/tests/tools/test_tool_vision.py index 3f12df7a..3fed9689 100644 --- a/tests/tools/test_tool_vision.py +++ b/tests/tools/test_tool_vision.py @@ -37,7 +37,7 @@ def test_analyze_image_web_live(self): if not os.environ.get('ANTHROPIC_API_KEY'): self.skipTest("ANTHROPIC_API_KEY not set") - image_url = "https://upload.wikimedia.org/wikipedia/en/f/f7/RickRoll.png" + image_url = "https://github.com/AgentOps-AI/AgentStack/blob/7c1bf897742cfb58f4942a2547be70a0a1bb767a/tests/fixtures/test_image.jpg?raw=true" result = analyze_image(image_url) self.assertTrue(isinstance(result, str)) From 80e02993427edc7c81973206c62c0da21e09c261 Mon Sep 17 00:00:00 2001 From: Travis Dent Date: Thu, 13 Feb 2025 13:48:33 -0800 Subject: [PATCH 4/5] Fix type checking. --- agentstack/_tools/vision/__init__.py | 29 ++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/agentstack/_tools/vision/__init__.py b/agentstack/_tools/vision/__init__.py index 677500e9..8d67cfd4 100644 --- a/agentstack/_tools/vision/__init__.py +++ b/agentstack/_tools/vision/__init__.py @@ -10,7 +10,7 @@ PROMPT = os.getenv('VISION_PROMPT', "What's in this image?") MODEL = os.getenv('VISION_MODEL', "claude-3-5-sonnet-20241022") -MAX_TOKENS = os.getenv('VISION_MAX_TOKENS', 1024) +MAX_TOKENS: int = int(os.getenv('VISION_MAX_TOKENS', 1024)) MEDIA_TYPES = { "jpg": "image/jpeg", @@ -19,6 +19,7 @@ "gif": "image/gif", "webp": "image/webp", } +ALLOWED_MEDIA_TYPES = list(MEDIA_TYPES.keys()) # image sizes that will not be resized # TODO is there any value in resizing pre-upload? @@ -42,7 +43,7 @@ def _encode_image(image_handle: IO) -> str: return base64.b64encode(image_handle.read()).decode("utf-8") -def _make_anthropic_request(image_handle: IO, media_type: str) -> dict: +def _make_anthropic_request(image_handle: IO, media_type: str) -> anthropic.types.Message: """Make a request to the Anthropic API using an image.""" client = anthropic.Anthropic() data = _encode_image(image_handle) @@ -53,7 +54,7 @@ def _make_anthropic_request(image_handle: IO, media_type: str) -> dict: { "role": "user", "content": [ - { + { # type: ignore "type": "image", "source": { "type": "base64", @@ -61,7 +62,7 @@ def _make_anthropic_request(image_handle: IO, media_type: str) -> dict: "data": data, }, }, - { + { # type: ignore "type": "text", "text": PROMPT, }, @@ -71,21 +72,21 @@ def _make_anthropic_request(image_handle: IO, media_type: str) -> dict: ) -def _analyze_web_image(image_url: str) -> str: +def _analyze_web_image(image_url: str, media_type: str) -> str: """Analyze an image from a URL.""" with tempfile.NamedTemporaryFile() as temp_file: temp_file.write(requests.get(image_url).content) temp_file.flush() temp_file.seek(0) - response = _make_anthropic_request(temp_file, _get_media_type(image_url)) - return response.content[0].text + response = _make_anthropic_request(temp_file, media_type) + return response.content[0].text # type: ignore -def _analyze_local_image(image_path: str) -> str: +def _analyze_local_image(image_path: str, media_type: str) -> str: """Analyze an image from a local file.""" with open(image_path, "rb") as image_file: - response = _make_anthropic_request(image_file, _get_media_type(image_path)) - return response.content[0].text + response = _make_anthropic_request(image_file, media_type) + return response.content[0].text # type: ignore def analyze_image(image_path_or_url: str) -> str: @@ -101,6 +102,10 @@ def analyze_image(image_path_or_url: str) -> str: if not image_path_or_url: return "Image Path or URL is required." + media_type = _get_media_type(image_path_or_url) + if not media_type: + return f"Unsupported image type use {ALLOWED_MEDIA_TYPES}." + if "http" in image_path_or_url: - return _analyze_web_image(image_path_or_url) - return _analyze_local_image(image_path_or_url) + return _analyze_web_image(image_path_or_url, media_type) + return _analyze_local_image(image_path_or_url, media_type) From da7c83c1888601c14927ec411b59cabe00c3057b Mon Sep 17 00:00:00 2001 From: Travis Dent Date: Thu, 13 Feb 2025 13:59:44 -0800 Subject: [PATCH 5/5] More work to do on tool tests cuz they need dependencies. --- tests/tools/test_tool_vision.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/tools/test_tool_vision.py b/tests/tools/test_tool_vision.py index 3fed9689..e04415bc 100644 --- a/tests/tools/test_tool_vision.py +++ b/tests/tools/test_tool_vision.py @@ -12,6 +12,11 @@ def setUp(self): tool = ToolConfig.from_tool_name('vision') for dependency in tool.dependencies: os.system(f"pip install {dependency}") + + try: + from agentstack._tools import vision + except ImportError as e: + self.skipTest(str(e)) def test_get_media_type(self): from agentstack._tools.vision import _get_media_type