From 060a8d0aadef485de6938cbb0d553c5bcd86c953 Mon Sep 17 00:00:00 2001 From: Yuhong Sun Date: Sun, 5 May 2024 16:54:08 -0700 Subject: [PATCH] Discourse Connector (#1420) --- backend/danswer/configs/constants.py | 1 + .../danswer/connectors/discourse/__init__.py | 0 .../danswer/connectors/discourse/connector.py | 215 ++++++++++++++ backend/danswer/connectors/factory.py | 2 + web/public/Discourse.png | Bin 0 -> 42742 bytes .../app/admin/connectors/discourse/page.tsx | 274 ++++++++++++++++++ web/src/components/icons/icons.tsx | 13 + web/src/lib/sources.ts | 6 + web/src/lib/types.ts | 11 + 9 files changed, 522 insertions(+) create mode 100644 backend/danswer/connectors/discourse/__init__.py create mode 100644 backend/danswer/connectors/discourse/connector.py create mode 100644 web/public/Discourse.png create mode 100644 web/src/app/admin/connectors/discourse/page.tsx diff --git a/backend/danswer/configs/constants.py b/backend/danswer/configs/constants.py index 045c7dd913..b6a7bae596 100644 --- a/backend/danswer/configs/constants.py +++ b/backend/danswer/configs/constants.py @@ -94,6 +94,7 @@ class DocumentSource(str, Enum): ZENDESK = "zendesk" LOOPIO = "loopio" SHAREPOINT = "sharepoint" + DISCOURSE = "discourse" AXERO = "axero" diff --git a/backend/danswer/connectors/discourse/__init__.py b/backend/danswer/connectors/discourse/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/backend/danswer/connectors/discourse/connector.py b/backend/danswer/connectors/discourse/connector.py new file mode 100644 index 0000000000..471fc04ede --- /dev/null +++ b/backend/danswer/connectors/discourse/connector.py @@ -0,0 +1,215 @@ +import time +import urllib.parse +from datetime import datetime +from datetime import timezone +from typing import Any + +import requests +from pydantic import BaseModel +from requests import Response + +from danswer.configs.app_configs import INDEX_BATCH_SIZE +from danswer.configs.constants import DocumentSource +from danswer.connectors.cross_connector_utils.html_utils import parse_html_page_basic +from danswer.connectors.cross_connector_utils.miscellaneous_utils import time_str_to_utc +from danswer.connectors.cross_connector_utils.retry_wrapper import retry_builder +from danswer.connectors.interfaces import GenerateDocumentsOutput +from danswer.connectors.interfaces import PollConnector +from danswer.connectors.interfaces import SecondsSinceUnixEpoch +from danswer.connectors.models import BasicExpertInfo +from danswer.connectors.models import ConnectorMissingCredentialError +from danswer.connectors.models import Document +from danswer.connectors.models import Section +from danswer.utils.logger import setup_logger + +logger = setup_logger() + + +class DiscoursePerms(BaseModel): + api_key: str + api_username: str + + +@retry_builder() +def discourse_request( + endpoint: str, perms: DiscoursePerms, params: dict | None = None +) -> Response: + headers = {"Api-Key": perms.api_key, "Api-Username": perms.api_username} + + response = requests.get(endpoint, headers=headers, params=params) + response.raise_for_status() + + return response + + +class DiscourseConnector(PollConnector): + def __init__( + self, + base_url: str, + categories: list[str] | None = None, + batch_size: int = INDEX_BATCH_SIZE, + ) -> None: + parsed_url = urllib.parse.urlparse(base_url) + if not parsed_url.scheme: + base_url = "https://" + base_url + self.base_url = base_url + + self.categories = [c.lower() for c in categories] if categories else [] + self.category_id_map: dict[int, str] = {} + + self.batch_size = batch_size + + self.permissions: DiscoursePerms | None = None + + def _get_categories_map( + self, + ) -> None: + assert self.permissions is not None + categories_endpoint = urllib.parse.urljoin(self.base_url, "categories.json") + response = discourse_request( + endpoint=categories_endpoint, + perms=self.permissions, + params={"include_subcategories": True}, + ) + categories = response.json()["category_list"]["categories"] + + self.category_id_map = { + category["id"]: category["name"] + for category in categories + if not self.categories or category["name"].lower() in self.categories + } + + def _get_latest_topics( + self, start: datetime | None, end: datetime | None + ) -> list[int]: + assert self.permissions is not None + topic_ids = [] + + valid_categories = set(self.category_id_map.keys()) + + latest_endpoint = urllib.parse.urljoin(self.base_url, "latest.json") + response = discourse_request(endpoint=latest_endpoint, perms=self.permissions) + topics = response.json()["topic_list"]["topics"] + for topic in topics: + last_time = topic.get("last_posted_at") + if not last_time: + continue + last_time_dt = time_str_to_utc(last_time) + + if start and start > last_time_dt: + continue + if end and end < last_time_dt: + continue + + if valid_categories and topic.get("category_id") not in valid_categories: + continue + + topic_ids.append(topic["id"]) + + return topic_ids + + def _get_doc_from_topic(self, topic_id: int) -> Document: + assert self.permissions is not None + topic_endpoint = urllib.parse.urljoin(self.base_url, f"t/{topic_id}.json") + response = discourse_request( + endpoint=topic_endpoint, + perms=self.permissions, + ) + topic = response.json() + + topic_url = urllib.parse.urljoin(self.base_url, f"t/{topic['slug']}") + + sections = [] + poster = None + responders = [] + seen_names = set() + for ind, post in enumerate(topic["post_stream"]["posts"]): + if ind == 0: + poster_name = post.get("name") + if poster_name: + seen_names.add(poster_name) + poster = BasicExpertInfo(display_name=poster_name) + else: + responder_name = post.get("name") + if responder_name and responder_name not in seen_names: + seen_names.add(responder_name) + responders.append(BasicExpertInfo(display_name=responder_name)) + + sections.append( + Section(link=topic_url, text=parse_html_page_basic(post["cooked"])) + ) + + metadata: dict[str, str | list[str]] = { + "category": self.category_id_map[topic["category_id"]], + } + if topic.get("tags"): + metadata["tags"] = topic["tags"] + + doc = Document( + id="_".join([DocumentSource.DISCOURSE.value, str(topic["id"])]), + sections=sections, + source=DocumentSource.DISCOURSE, + semantic_identifier=topic["title"], + doc_updated_at=time_str_to_utc(topic["last_posted_at"]), + primary_owners=[poster] if poster else None, + secondary_owners=responders or None, + metadata=metadata, + ) + return doc + + def _yield_discourse_documents( + self, topic_ids: list[int] + ) -> GenerateDocumentsOutput: + doc_batch: list[Document] = [] + for topic_id in topic_ids: + doc_batch.append(self._get_doc_from_topic(topic_id)) + + if len(doc_batch) >= self.batch_size: + yield doc_batch + doc_batch = [] + + if doc_batch: + yield doc_batch + + def load_credentials(self, credentials: dict[str, Any]) -> dict[str, Any] | None: + self.permissions = DiscoursePerms( + api_key=credentials["discourse_api_key"], + api_username=credentials["discourse_api_username"], + ) + + return None + + def poll_source( + self, start: SecondsSinceUnixEpoch, end: SecondsSinceUnixEpoch + ) -> GenerateDocumentsOutput: + if self.permissions is None: + raise ConnectorMissingCredentialError("Discourse") + start_datetime = datetime.utcfromtimestamp(start).replace(tzinfo=timezone.utc) + end_datetime = datetime.utcfromtimestamp(end).replace(tzinfo=timezone.utc) + + self._get_categories_map() + + latest_topic_ids = self._get_latest_topics( + start=start_datetime, end=end_datetime + ) + + return self._yield_discourse_documents(latest_topic_ids) + + +if __name__ == "__main__": + import os + + connector = DiscourseConnector(base_url=os.environ["DISCOURSE_BASE_URL"]) + connector.load_credentials( + { + "discourse_api_key": os.environ["DISCOURSE_API_KEY"], + "discourse_api_username": os.environ["DISCOURSE_API_USERNAME"], + } + ) + + current = time.time() + one_year_ago = current - 24 * 60 * 60 * 360 + + latest_docs = connector.poll_source(one_year_ago, current) + + print(next(latest_docs)) diff --git a/backend/danswer/connectors/factory.py b/backend/danswer/connectors/factory.py index 5e6438088b..54cd4dbdc6 100644 --- a/backend/danswer/connectors/factory.py +++ b/backend/danswer/connectors/factory.py @@ -6,6 +6,7 @@ from danswer.connectors.axero.connector import AxeroConnector from danswer.connectors.bookstack.connector import BookstackConnector from danswer.connectors.confluence.connector import ConfluenceConnector from danswer.connectors.danswer_jira.connector import JiraConnector +from danswer.connectors.discourse.connector import DiscourseConnector from danswer.connectors.document360.connector import Document360Connector from danswer.connectors.file.connector import LocalFileConnector from danswer.connectors.github.connector import GithubConnector @@ -71,6 +72,7 @@ def identify_connector_class( DocumentSource.ZENDESK: ZendeskConnector, DocumentSource.LOOPIO: LoopioConnector, DocumentSource.SHAREPOINT: SharepointConnector, + DocumentSource.DISCOURSE: DiscourseConnector, DocumentSource.AXERO: AxeroConnector, } connector_by_source = connector_map.get(source, {}) diff --git a/web/public/Discourse.png b/web/public/Discourse.png new file mode 100644 index 0000000000000000000000000000000000000000..48e4046b8029b34a5e1d0d24821c9b81e52dc350 GIT binary patch literal 42742 zcma$%1y@vE*LUa|7^J%t1WBbkMnD?rMvzYFW&nXfqzptl9;91JYDQ9#ZV(tkK%^U~ z@8bIhK3pu8_ny1Y?mGL#>1e4yiRg&{0DwMGg+B!VtbOqJh5!$IvimSC1^k2KprEM$ z05wU(m(Os)|FhbvKGg((AWi^4yaa&1;3LEa0Qd?5z@{|-NTvY*jc4W;U1{(Io{hQ+ z9Jsmtm)lnK7F-EDfh*_-%x=y5huBVJ$zHE}?-=;L-L8@8w(YkIg4oAEViwq7cd!Tv zonFFfxswmnYoCbv*0nfY%+DV!FuY7wrhhpsMUqv*U)wt-)9~}2`f2teOZMG&+^H2_ z{fA*~0gy+jKF1O&qXjE^0Q>**$4lZFArh%pP*WphL#xb7&?O9uQoZL|f&&|s*MG-f z=Y|8(k8`A8H>f6msW?NtqgsG9ye7PG@Cf$09h2nxTH)Cp`8%c$P5+QXIDsb$zn@k3 z;?2@V_P~VSd~zdopR0F+A-bvWvfCiJ*uCnh9vI?0Qd075cqTlJGAA#IO2FewBi@i4 zd0K=k1hF|=mt&)m`}GWX0>KjzB(R1JU$NJkxy)2O+l>@GLH8^i{FtsRI^AKi6=3@Z zkthB9a9yPzD`KtOFd^TE!&O+}d7k4u0a_8V=A0YzUNZj$&;n^Q%MHpsCN;~WF+F|d zcA}~pBqb>sW5UVK9uvTx|NAK)A0Kgd?JTY`Lzg@Lf0_tUfS7JLo|6zw3cZ^<0zvyt zJGY1)o$T!FFafTqNFOgS`}ilAL7)lZ_Kqzhx42Q?(vdu z$f@ts!O=8DW~?@xWFHP6{BL>x%m!PW8ubE;v8Y~_G+BZ-VRr_x6WK>G+oLS+mid{B zW%th?25YB(Xcemt0zb0$&(tdHvY?Dy!@bTJT6BDa**<+%#$ z`!QPQ@a7LeRw;pJhB1ug#F2B|sdIXMYaW?q|43a8yyc97Tc-1aO#XcB?(t>4x9{0L z)B@6lZ2k)USv#`rSK(hS5mc<+wib z<^6ykz+mho4a7?p5o8#>kt`bDQ=xN4tZ`VGn3yQ6u6QdHec||wbkjN=0h$pSY%h8Q zYnkG^w=W{vm0Wy@u{ImCva&9W@7!*k-)dT8^H@+uHY}!LJ08+ZApZbdDj-SRCu6#D zn(3!>3r+aJ;rdWg40Z%D)p#NOUmL)noJdz!HaRB}(^$QoAsG~$WBd4@Y!mM@C-rKW z4Nll}Fro?(@9KEq_v)YEh{a8cx$pj-`Qi0XIuIb0-&iL@DDs2TMnBw>69JSduJ&nf zI^_QP(C}4retzD}1a^zv!AC0J2j9n1_Klp;E|5^>D5;b`vY&HC>E*<>wuTyxkGG#c z{>PSr*||BTw9kWMHXZbtJW8unqO4?!E4}u2&*Cf08WXGU-jcBzR$9?~?i10ffL(s?r}rF^|FK^t2fo>cIwE-I_YCwZQ4m%;aB zfdTM42_)XpBD_q%|HyCgs~1eDi{jjJ?rXz)otZz7Kx}cl7-S|3ji~d;O%-=a({KL4 z46a-8S_F8w%TgqhVKpJDSubsk z6UkzXa-fLoV*V$IzDKu$4;uY~rMDDH_&^bESr8#a9x~Kt-%(vr<-zqp5oGYH>8;s9 zgchJ1e}M5CcK9kbH}t&S@J!L)-=FNC3|8sm*9Ed|aUbqtAGLt?2d+_~gM>LVYtrPtBP$w}~YSk7bbf-i`gqYb*KX#rM}6Fa;^iXYU|%k;aN9 z^Syh=zmEQPE0u}aai0d{Uo3=3IUc$0w=3$@ee_`aAZcnOXfYf2s71sC>K@BN6O6$1 z@WeL}7zWc4ZJ*n)^H0*oKc|~e4+pg6S1JG!!FgSb7Eq z4h6fgJxt;wX}kBxJ{jglw?L26#GPOhB2Xz0Qcgal8o7*E;SB$9hLJamHeWt{aEz|3 zthA(#0;!aC)%8AnXx4wD=qX#K0AD3sVLxNnIIR0=m3{m6Q%y|`=kxTOmpv`0K#&Ow z1vM5Vf&MD~r*6D>pguR}j7eik4j{rOJuf=`X4uPo_TiG=qF5 zZ%f1bQlHxBf9iMheu6b7ApcH$@5(c!{$Aip0Z;(>h)jI<3-=8DUxVE6+?-UO7Yx)u z!p|aN9>8h~kXi%E;$2kR@Il_-!VXIvC5Ws3UG-GnlD_{qaV9 zGsSgvK&p*NL5HaA;^Zu06n9PDVBeqV)Irt zIGp*jk;}!gMO$0AEq5^`Nh*RUuduMtg!h6B;I~C^v9pu$(4>e>8uqede_PE0z{H@t z%ky#I)a=iYkMiyKxvwTz+1Xi|9uZ&t&CbrA|3}{+&%Y$(t^HM}YGZSA z(@q3@MdbeNYxLox?`QP@S2bu6@VZ(vLjL(|Rp}p3hh06jq?tx;k zS14k8l$RuK(`s<$fZmBM`=Ao^k0SJEG6;!V40RXM080}+rZPGWP7mx{lRMw3ipGZX ziW}Ba!|{()W>i2RM4OEiAHPj`qaO%$jg^-do9Sl1O1z>)75)RDJ33v^(AVG6(sEjR ztN0QwR>KR$ntvJ*mi@OhP$l7gfo{Y%oF2K_-_r1j1+%hGQQxx*jNdv~_&uTfkw&-f zxrWk5*nC>e(Z#);GIVNTn813GCo zL(n`!i|gxsYm84SfaO~*q?BRJ>CtTv2L}cD)zNpsb~)TMHC@;?wK3FhKqTGY564J$ zl$E(Ks4Kj>x2J#rvkBt-9qU`|_o{C)KEdAJKa0E7>|i;R6;U4x7};-$t2AAh4ZP*|Wi~{V~H&5O6&v47}l%xYST=XujL{`gOZ9%HFUn#Em zR}2h%sG+>>>dclt>n^Y0zV;;#Nh<`c>A)|M0%I^yW4w+T9)84&uO!{vBr})3ycmU{ z2g4w;j|@(@Y9Laxi5;c%O~sfjCcx|CZMpd%f5Zye_x>Nn9dD!ff59N0=qWVMVW z`>S^n5)#OtS6C6RQvF+D71f$)WkID5CB3)nAOQx(B||accQa_rE+CO}Yt#pUZ)h_0 zD9Re}Ktrvj2SMoECWmyLm&B|^|CGaL*o5TZ_BjYzgmcvcnEdznXqnZ@f=Qi>^X+L3 zCHNY)A0SPwvuOzYxwT4mNlHl3q+r+E0<6}&V=IV6_U&f*1(beH5K z8d+v~25rHf_nT7+t&v4oF0QWSg!flSBg&uMTAd>`PUHszquvG(!J%8$=(FCsdpyq! zAX8BZgnpij698)U@mfUQDrYLV!GY`b<+(1!0Y1R+kAm+G$*~x!iJH=0{MeX=I!1Rl zxL4+_1UMovRlwd|Ly$_Im$~v`6U<4ETm-xHWXmmDM3d?1F76?SZ(}+;PcCxuEHv2r z@C9%51%W!IT)<#^U_ZwZqm~-k*GvLT!1P^MiCD2%NpdeS9ZIG;pCTTP5GcSV1=^BW z5g4|G$VbDf*tFP!+^Cm$J-Djas&PF{vS;hVs#vs?3U}zKGoq39?j54z-2d`UqL_em z1+&lHD~m6C>UnW>ozj3cr(EVpdO(P;I7+#mT3<;}#ia`l8yjpnVFR9gBs#&yBH1jt zVS(kcp!glPs98JhIrEkN)Z%b)-HDUo#Z{92m0K@yEr->b^q!R`M5ijsI$|;nA1!SL zi_LxPGL9`Mecd3AW!>nwg;hy``%3M8R$-EK1NXi14l$@V-g!~$J z?{KF^vSlacf-VzkdXzLn4S!?=$<-vLcFKiD`pHmXnqzmiX42?ub}2&zr?sz6ncHp2 z10|TQ!=ACfn6Z#g?FO|&3IFVaOHDWw`OO)onFPswj9vEGnhK}C-kKl(RlK_nVjaHX z1Z2Q%%Nz$Axeo3mc=(=hUPniRpVHu;1zf_-R{S8}jiLD*u_vN@#!%}*KH~!1_q_2d z#%bv>ha?DxcRhN;w5wbsDJ|Zu7!HfLu61L=m;lLQw`X%|_x~^h6Xkd##L)m_LDAR0 zYEa$@tn1S6S$_d(Hu@x&fnJwDJr$2rZ4*$Gi!NYwVAv1_ph66z9KTBw zOgdRt@EMaSyc@HX?)^G${(Wp zGwEwn$IHxmP7i#4v=hB}r=hr(3+>j9J|G48St)#-FYNb74)&g3n&`TXBjxPw12k;c zB;M3itIjQy0rk6MRfY&f56yIVkU1!^qyHSFhq~}Zi=#$0BfR@lJ?0pcoPaUN{$1R& zwKY4@RR|zt7}fe8b>yMQo|n2{k_VfVpk6JUQ1f!!b*E_BSn+T7cr~_Nz3WB;-n7iYsAgBdG(fM}77Bh- zFWMNQnY;K+EjgoN6lK@&wZyckg{0Xu7U<9uz2>3kPK_$oaXR=k2wx`hRAx|Wb-@mi zz9tz%zRqDQ5nlvXjDkpc}%+8iY?i_ibMJzk|UNt1~y*1K5D>a(0 zh^Nq_cRcO@Ua5Uqeo+$Hbr6~NI=smXDT(8HU?QI(o+T|=3%m=WJ z_%uoeF&=bIX87!aHZb+9UnA#0HfWY1E4NzyFONYo;svWbr)?CWKzKqR`fJjMk$NbW6M&K!C+VkftF25g>uyk4OV3b2Vf{7z+S9 zfWUf|P=5|Fjc|kH+ifm3#54eI#6HyWhMnJejNc-%Mo@QiA1+h|CjoLsqV6=%`1xeV z7>Hg+OF_}POF52G>mgQ%-q;NLrr};A-Rih35oRFus}sp?V#xXg8w!b~Y^)RwAtcg= zC9|?ZpoEb8%nQn+ySa}LSr9?)>Zb@T$j8s9bU+YtzkhyZrX_b@5BF!OYfNDNPdit$ z!JUPXPyx`@?$Uz&-+}K9EGvqJUe9lEJl~6m98?-7jz*hf3x3B@hATaW5W3KkYv7(F z%PoZZQdf^5vVpC4D0=+S6`~;h&NLrw)GqcOQ)iD%zQjPx{odWL4R1uiMjKc<$DF;n ztr@YK@acQ&=vVL7<9A;42gvKah$;nmaAFx^dIIkBD4%*A+h>l?!q(IH&TIw#*kr^n9SAvMC>By-WBJ$LN zYP6mvu5aBPK^_r(iBHNi=UV9<^bUXv+#2fgp9;zISYeIWgxSRh^ozQpK{Gq>v0w|A zklHe_{aujTA>TfW)VD(OI|kH}DeeR7&S(+o|4KYrq;(alM`Luk1xr6EN?`}@&p4sz zv;T6*lv;B^3c3!r`G9a8(XjjUQCk%7v4C$IhmyfTJJ#H^%kh=n#9MkXa2KhFR>=u$WI#I#osgN?3vFL(Du3O?|X805VGF;sEbD3#rsej=K#+z zQtciIMlIph~ zI0ni?M=(qtJ!^EP`}YZ``t+dobDD(Z)6_vi`Tt=>xsKN3HgLf(HGB=slzeMuO@S)L ziae%|kU+dNp$H1Yhtw*`uOT98uISPGUvd9h44=sohD!n@D%N94xQ!|*2MB-n!-c0P5ro!J- zu_9thN*+mnO7+T%)QeB&AQt%{0bmjReuwH?scoU=3uvbP4`-Uk`W;t#?dMOD4Gw}I zfA&57ab}xrSzb}{OA~g*VHo-9-0d$BUqE~z$^x47v`mdv(1=5ajDY6T(+J~EdFPyn zarXpy&hVNGlaaH`+L2yuD|SHDNxfos^G{F=7))~N?+py_ne@E{cub%?G2Nzn9u2u4 zy%))*BA2pPqm90}IGe)k@rXWl6cX~ac}L$BtN1>w zvXR`Bpcu(2l9y{QB*_AJzM(E3C(?e^Lu+-tx=Uly76}JShQ=Cdi4K{O#robAu+yIN z{9f_!nloC(Eqa16Cir*8Q#x3B5qVlKh2FR3@@Li|NfBH83^6i?7j)Qrnw>p-KhZIl zn(=RlCBL-Ltg_{sFo@FoTKOM#22&7l_{+afsvk)&MridfX#Bl1SP2_MtW5|# z`#!lY;74gm)PMTT)**`MH2buw?(w5^3#_~1c=Fdlzu@n!_pKiWV!NPW;l?6V26A|w z(k+l*b9j7bk%fX&y&YS%nyf#|$t~haqCPgk5SR2%a0I_2Hn5-uY^vJ#i;qS+LjV+= zsSeb-01#Dwa$WP^r_wKAopw&C{^I2+b%*SW?C)yPe+@5{&6YG9P*r2CZrB6@fZ~gR zwrnJ6Cp{4Jh4RPJ?}Q&7Gzr=-f}rR9$)e+%x1CwA5V&Tc78B*KCOM=7CbqEN2?#+$ zeu!2N7R|}HU=s+nQvAA`ffMsZ69+G;{k5+-;)a^R|NaiQcG$T!x+1GBj@THfbhzR} zA7?Q26R@Vr$T_fkk9R%w)bVuX)P{mU1bgH?=#+js#oVXiA%U;Y2$td$aAVtV?m=V# zj1S*^?7b%7^}*^@SIkNJrm>CSHz$!;{-hx=(3lgCEZU6ewOh)6m?OHeiQjD}pTmQ> zkAo6yZ*lnREl2?GOs?G+mm2|(q#;TQU-P1@W;ImztfOL;5J8qhQ=)qW%M;_8F%xi$vLo3#4A z_wE|v8kj*#?&lEwwo54qS1CZ48E2-p5{qf~H5454+|b)M=cxTTVK^|=sd_bYwloZ!8j zP1^Xw%cS$wVY$e*`bHMNT`glNKg|S8%dvzVVkAq!*y5woPLLhsEc@BY`qwG0z}cPG z4Ms;NC#1EMvPAppG`z(FX%U*hCb%I(uM4_}_gBUk+6RVgaKu@QT9=~PwlK-vbbcl* zrYORP7?Eb_`KqjnJM>?MffX}-OuemGIa7=E?lhrGulscHDRx7!pH*XT#B+aOZDEV; z=9+rsOs+GgdyR9q{Y}T*%-EsmBwB^7y}tRJ3o8&iBg1}$s^ROgsP!4ft^k)n1d?a( zPGjO&+JM)SPfa^OY%R=(Fl_?v{Hkeq{CoZF+iL%vh=~c}GLDF?6aJ0W>Xj-fDOOc? zavZXnw*znU_n&n3z%o8a|E~pFPS*xBm|8X{j$g%+bivosjp5 zi+NSMP5gB63q9>NpWv*Sb)_+mNKxwJ7f%&cN!jL75cO>ghT1IUJC-P~1;@6Ah$GVI zIDY8LW0No%VOeYEt!2)4s{=+(vG;0uQdIw=U6yhE*Af&z5JnjK1*OLkAd<|87(!r} zdTwOHoSvS;eeF04&4lX4#;@k1LwQ+Ty|;kf+rR0hj8-P1*BV_W9H(8eVKS%f$gL z1SW*_XHUtaz(Bo<`%%sRF|BR>5==6|<6fx}mOc)mV^ZERy} zP<5wzIeHV)Nsqnf5tG10xB4ykY4vM^tk5hbl6^~(y?;H(XJ?1{rs;*vrh9U!*6mXc zneU0er#jQVzf8^%QbIe%cbYfjtbMl=q#ne6S%$d!y3j^5v)-VYFzT_*vyC=Lwf-!C z&~R8|VF`H)Tz6K#Ad8X?+$OH?aRbRYq+4!NsTb{Kfnjj`yuTZO8I7E}U~ea6K@_1% z3^Ofm{_{g)LA*a-Fv$Yqef%)3pW2&^he_iH-SeEeS3f9*STDxyH){4rTiaD4t_SeG za_D)V^8q&w(@oI|*{vs*<-bIj+UCW;#;7XT%aRgVb=g{YR|cjdxMpl?OGA<+Sl?4! ziV5bV)-ho})o>=6u)Sy4Q98UtOxboEE%n$ly*UbN`wn@rDz7fi=+ma1SnKDnd%sTQ z#P>8pGu)IcYgHJWos5h=2#7sDFh_d0xRj|(&SK@Jhh})acugHLX^0FGM!Dd^O+_M! z(Icn7+Z9q(o>iJeqh==rKRj$2)36GmH&ns;*qihCWZ%J>h4%h}f1D6&^NhT808i_U z;mld>D441ZwQ9RuTB&#}hk}A3*xx@(%H8MmbNSvK6BR3tEA#hKKbrN+&zR+&`zqmj zuVbE7O4&Pig+7srlKLe#?6KRks>F~Iy2ZV*uP?-Whe8S~ZL3ca(xl{cS+ty|vr3nG zr+f}P1^5QrwW-joC_+x3B353%i}L=A;YdALC357GcVvS(kD0p!cKIttj7+{pJ^2F@ z-xpWC-<7VjWLKx>y}=T~_>n7{`z-8el2)*K2JSfhRR#?9**j_r`|@D5M2WJB(k<&uXmYhvdXUIl5Hd9x1Yz$ebtusX~nJqgWYiO zlc1GUV_VvGvNpj=L*$3TUb!A@x|;OPQfl&QLe!;K25`-g{lA3U~y|F%c^KSp@S zYAG>eqqH3)OULRl6sf4a>AUIH95BDy7#hsWU9dfWl8nbcPhK9dA(^$A- z<=hRV4RRFsICV)PYS@M@m1eD^J^%P^0&VENv+=tLE*%3&RahALA*n!LjyIOgR_ zQD#qoImRB$370)Hc-qqolPZlA8hNpI=bS) z9IOgCBov6RJD(B+Y)C|#3M;-iyN2TwILSr|NHIjzmo@j3oM(d`|F_&?+}5LvqLVk7 z1OC{Bo^=LsL-S%lM%2M-&u-(e$M)v$b6KTU?0S*L5R&1XXJUmMCIz@Thr>_GXMy~F z5iH+pcO&m743RWBw~oZmE1xFzoJmUPd|m2RgHhS@?9)ZxZJTx`A+NoQLj+u^^|!ZvY-VrgQQwM zlqU;e|AmJ?0M$UEEag1Zj=c5an$U-2j+du-J8ESmOJ@EC?5MDU5wH1i4j+UrF&37(oyK@QS9ygI&dKvbNYrT<>klIcZ zQ``&ccjoXUQ}%dpG>BP1R%)u^5Nz?9u$zYC!zwKclz6bg;1ChbOt=?uF8{nRChgiq zy}*u*eabv;xO-i(n`fpu&R(A90n9j@@rQwWUaYe9?tIQ+C=MTkgOS6t!78+^R1@aC z`PfAiRvanmy_?l<4(koX?k=xxs3VQ4ZAbIc09g<2@`)X*)nZ&EFK*=FWn-zvK!wW2 z`*m+Lk1oWRiSWPhLx<;$euEn)jqfLx3&LlHaI02Rwa3aGLp4-9b~$6t?l>p)YLC;C zp-zLnhS{Q65J@vc4l~RIdkVQBg)!uZMht4@M0p=_;xXKRnhoyiM%d-dM2-)HznpKB zTA!0Ov54}&&ST-`54h)l<_7w-ryxj67c=imS%6`h=C{s*6S1XjFzZ=##KDTNewWIU z486i$%n`n851qL)FK0QV(@Y4hE*hM;kAx z-uf?)8IO_u!cKw7?)}&EHdv3BT|#DmVeX@9=2~IV>Z|Aq2Vl*!{4XcqyA^$8Q{;?} zb^G;K!f=f-p>_g+b2;L!tWJ@?+e(Cfq!sy6{I8b&Xb1%f%DkB}VkIe}ZTB{#9s6Q& z*JbIl8C7XbCrPSNo4opbV8TG;l#CSWC)7oA$ev+Z8G+&33O}-Oqd&_mgn0(OBpbwM z?-vrOS^V=XkN4G{oq*D+<_D#TuIaeA%%i`&0!o-J5kcyD)aE>)%)_7~2!Ywk=kL7A zE;a#vVk)cOnkkP)!N%t)*ur|={fY|hYJZ>%Tz)R1DaHX+PO-ipJERzwKwxyis8?Yz!(b6q)mtC7Lq1$_87~1Z8Ylh z7FU#qg;zh`TwRp5Jitwmtjo2??S({$s`&FlN(U;+#N0SnE!yE|t8nKv_C(tDOL z*&4x=)<^Zk()x2Zx%7F!^aUAep>DTNjua#Qnxi3ve`lB0?y4_O!Lo{;k^V0=Fcp3# zDcRM#vR{oA7FVKRlph`t%O|eg;=dDTC_f`a9}$$)VWiA~G!NwdS`|*E?@J}cpTc0( zy9C4flN9Y-90mHOH0jHq{*da2k796p2RpOORE(v)Y?3OgN)p|`!}lCJq(JU$Y3V88 zB_YKqvnXM@M@q8`FM#G=(n9Nc=C+?#n!efyu8L@Wspo)fMLA~1Vw|{u6^iO0f98D$ znj+k?oIbeUZ!oswBACYaF?y;ms z{yqRa^n)7{{>vvx5o@Dg>KBNFf8F{Fw+sr!#{yGNQrjmC<%2j!(`V1ZMYHL9H&-@2 z|4a&bas_%Bswi*k`0@&+X~=$qSpf#eb~h{t_35#VU9i})GJ|>lkq)4^FGngUEnRlE zdWROicxXuLHn3F9?4R_+y~zA*PkPV zS&R!g>?ybAH^cG%b;o$_3#k;3m7Gunw0Clu3~Zi(rK%#l{}!#b%#16By{rQf-}4VX55nG^$jvP4vHwV3+d z*L?<6${TMEo89iFEOsV4X$`#ha8dSC=So)VSan!SNU|N+_dOf4#|!j&Eg>>dGZ6J{ z)th8LP)D}tS_ZKE7WNp>sff5Kg2F8Vw*6qm=4H>|ZBzXJjJ|_f0-S;9QjJE{;`n+m2%Wk-Zp6ge#D9=#EV@Fy}dPC zXF)%E$OFA%zN8yZ=tU_hnb9sg7T*khdUMFF(U8Ec3TaC=LZo=Cv+Pj#%3#-w;a8-hoag zGuqXXt{i_g0Y(kZa1=}~>Q8aWt1CtnqaVlLQ989J|T1taG%X{Pkdc1-(sjp#OA0q^7ikloYyc zjT}-4*6S~0JEJ*aku)M+uz4_L;qUWN*}Bqy77cp(ky9&T1FL?mC-AySO1{pf_uYIP z*k^&ZBAc9ij2ErHMILbV3eL)s4f#Ck)8l?J0M0ik$DA>C+0^V3qTbqsxRc`KG*bTb zTW|=dQTB{5TIj&ZBOZdU@)^lTN(a729;_cp9PX`$hHhvU?XAbC9l@2vgKzF*nJNM0 z-Qq!Og9=8AwDFG%YkjoJH$Q-bAy@8A({IQE8+uoYh&{0`plw6cQ(1dd&Ulf`=YP3J z1oL;>iSqjSv0keYjJj$`Mwa&_xgm|Pnn%3 z?XM24?ikAL!VdzR#AyB;HcnW<@2&A%;XAA5Mpid zUi*IH3#juOIu&;V?0TJO$s3a%(IQ*VMF_I}CD%JBbVo#9vZqzM{`9xrmAlbF;5+y4 zncLyeY%!s~j=bQtl%r`p&^O{~M`qRgg9AM*FOF!I-^h4@L&h3ef0Sema4|tNxB&z@ z?WUdgz5VA4d0%Z@R6fS6uFMd|6+f44_7O=UnM9SazdG<&yk(`2Y-c_ z`#dGyNz;os?GG!rL@Ujg^hsFuA8WO@k6xzB|M9PJIWr?7K|M^W)VsCg}MfvkI(dfP$nJTP8)l>%5tAz zH;9Sl;Pmy*Ji7eDW483XQKNv6_IrOT~$HZ4X(SzTN|!5Y~62 z85qrO4&|3Ujh9vzL_6|I@WhkZ9_mapv75c!UaSzGbLq>P(XNZC*LBwVt-L^Mb4|@i zN-qJ8uVW?H;MbZv@rR^sQC}bFsXZbyQ%oI08_h%ar&l@P3oQZ_; ze^jP$9wqqU?UeUkaW&xsN5GELmJfWD{sE6|fLX|hZum|Sap`cN4Hb2Ludy)Coj@+e zrupY1{2L@FoUO(>j)?(%JXXSBsJALgkkw2-IO2qzmzLfPGvQ&=Ir^DQ!JwP{p@?azI(Lrqj@8a zYCjPvM)KyTnu17eUKx8+xnK`J+Ip4_0M`9g7wu#w<<9!WQN}9%n#K0-2REOSLwzJN_JYf<}=RleJULVs^~*_uhGadlOB( ze!<@*;N~Yb4IutJBq1@%xD84c)EqvG9j|dUar zR4ivV1GY$$WQAibJ2xLb>4K)Gapk77C}n&0fl7>4e)VtJGkzyq6%KZ7Q*+BWG2YB; z*2f+DK&|?Nn%#t)C`0MQ7$PM&V;!n~PtW{wdyM8zrxq!Oo(41#UH}Wq|G*t}D|mlYJo}`?u9yA4Phz$1F1gYN6t62JA;F7BCU}87I+fjzw~1$&YUXSd!q-1H)x3zP zcwH57xD>k13+5_CyTn#_@2bXkHnToOY)CEOcuzd^d&qv!YRrlZ{Y@zSa9XY+ho$k6 zqljdU$4*8{Nm9Y(M=3~!iRyQ*dolBJ$r=kr2gKZ~S!T)@XK_aeK((8(G(o!fbd)+b zB-+ntSC}&Y=72M;x7?bGgMIPr?2WDNGp!q)qRK7NuAUpj$jV^nb-&yXtfvc331t|> zd!Ojnf=?;pLs{fSGUlxKYjD@4Lij`ev*;}M*42FsYu+TnNt8=vdW{Sz{2?rqK7d)k zzW5Z%@Mt>XbsO{VPsfO7?6p_A)Nc+-8aNGo=Ril8_|y`Pj!`$GZpg$!%-Nr55O`jq zwZ1SC@cI~8bbhdUiHh;vvWBm!vzULrBZB^&(P!#pteOr{g8c;VnH7uv-W39kf*1ac?T-Wx7A2Yi0UA3q@KlQnB<-m)04p ze*f^@lXdU=Q%iXT#a5^Oen{7J`}Khu0Z(Q~#IH!}pEAe7A3n1kOin@Ax6I+I!FKk* zRO4F1BctzwAgNFU-n)o4k4u67W)p3NxaHVPS9yk_1I(PXWDh?Ah<@Jn!RRThcYO60 zZ(|dp56J_k%z(ESj1=To^S%}Rx>N+KJ{j{fvR#~Ea>phxf@h~9R?0!>r_^ulbNGY@ z-QC>W4m55P@G6P~;_1)V2M^73{$TzP_d*Ir?=5bXmBE^?omGW@BtC{8NgG4}3(Qt; z4!5`Va#$xZ=o|jt?8Z>K8Dkk@wYs5^hR{L>#n$Z4mhmMKCI^JrC>JJa8p{t5bdwb6 zMY1F4E5Lk^$+j09iG6w6D4^2S=Vq0-X9gV6n7>h)5eMv$yt1MhVX>8DGONYVl}N_0lXtatl7X)wdr-@!Ng zH@|V!Yj(d60biAVIT;!@iix~mrR-@v`Pr?hSz%h8z5TnuBNwn-$29M)g`kSPRQNzW z!|^`$^5X%Aa?ghIsRSKh?FDEnEY+)(n_k15gb3nAbmVBcW6IPQ*{(S?3-qU>4{tw< zpTGs?0QQQVG-V2LC%(w_95m@;xGdxm`czmAJYIR@$V3R;uXht%)w z6)JIF$=zeCVzYGd^a#jl9z}aE6}TaqFgjcX&EzR!mV<-07fZ+lOmROdxYIurRkV}J z{?LJQQFVs%bbo@EXF%UYUA=!FY({Pq(=SXkeZN)TfIVn_ zLO$6A9f$O)seKsZy}9-)L`vOdoNf_&Z)k|XEv9-?b*N)S+nR(==o0G4eR?Hc*HJs*y zU!U$s7<=M=65aBChK%);MI;G3V(!l6%}n2mH{md{fD{zQWyK*KWBA>Tk*OO$28qb< z!?8Dxh_79}+#j4+obO6=XV}|Vf>&dHR}$}o1K07m;;Gx~EFT;rmQZ=_bHG=A;~(_W z7nC?!n5{?b`6kh{kcE*i0m_X`eTmNoSGBDU)@{oM;Rhtl(%Q{qIv<+(yW$J2IzJsI z0f-My4(<==+{O~EemHkAkcS<2Z>fSeZ#*@}*x^ub5QGp5V&h5xdPZXrtIvRMdcRki z@Zt`bHzKddry1M$KqS3SdLehD|+Sd6c{M|U# zkO>#?EOa^!i9Fc&GHLgyecQ4Pm*;Rx{rYEVarc7QHBBxFb(&Y)WR;gDl19Db4Rnnq z{QjoOe?)h z4+42|_d}lQ#maze7qKN8?=Z>8sVMR5Tw-O$NG)usUxF+fS*)`#Q|r^yxZQZdmezcj z94_X$P)}>+b%$7EjLHMNA27Mk)(+nE!W%BCZM)3FLVYC7RO9dl3TB&qGt%*I@KCE& z&*P9pzeWQ!i%Dbbujw4FnGwE7u@}*y9%p3F%Qj7(r&B{I?bHU!6+?`1E%6A9HqN_n z8x*IV1Smhwru2Mc_h`m)CZ$mU?`S-o7WC|nJbUdC&YA%a{5uqTDOJ{r2S&|6$EYYF zVW#CvSk?RXm(X7Yuw7J%a6}+NckO37u9ggB?ZtzcKqT@NrZn2E?n|g(zyrh&wj2J5 zAl^3)Rs-x~KD-`aqj`B6wg}!cj_kp8G1xiSfk?W>p z>gdJVQ9Y8Y4Q7>!d^NsHdT|=HZV~T8fp70fPlswCZil(|uvt1hba>{RE_{v(B$fWj z{Ta|21USt_eSfbA=3IQ;R^+NYD5d4)YcphU@mrL${cDjZJ|W|Q~ZQ31og9urU; zKP@kNE=q`Tjdi=WA%;17?c5L9YaP4ILe|;Xd4iB0Q4;Y=O3LMwJe=X)(;Y+Oy)Q7! ze~cX^)AdZv%*u$)fk!*Pum(zmWorzH*H9*hOG*iYOwmS%46v~7^DS}k;)`v%VrE_G!Tqnfp+G4wc9-r#GYbo;#UTu zlrk%P9PEEk9?o%#toYIY`j|m>gUvpD^F{k>qG9c{&&$?)Vo4V}Wj0HafoIA&1WKN- zI{gZTXb8IU^ahkZWa4uy^d&06^%bkq8es+zGtf`QYoP!qh{a54&{4-h6t5@~o8XFF zUnjcs9J*gkw;Z}m!hjUtG~%FX+keH37Dv{BB;S4aY=fXUQ zVeSn~CS{oBqE5&3AuT2NDvU;$SKf|QyzGK@Fp(p@eH$rf``%y$^>%+crr1`>O&Zwr3PQU%VvkkkHM{L&+**z)6iU#ZcSMc=;>Of zF!qteF{!(~-8@LJ&rY&0`=Ye-)96bYNpDKo%fCj7kqUma?Ci1Mdf%l-xZa8QaiwzA zeDxuma(2}L(kAROXTSF2P3&1iLqm|`Uh6ZK`%!hDMmN$>o)|65h~wU`=53iwG046M zepg3$v(w<;u+krLjK>ZSeKl#NFPy2fEYqKL@%L5z@n(o?EPn)v&HAf^`{E6C8tIqp zVr-SAd-Td@A_5K$Z0uge$Y&TkXx-vTqg)IA_XVHv9QEvA5l#kEB(NdUzo&>qXH%Q6C-=-naX>h1aBj zf(VV|f1t6~PJ|^^T#h1s3JRRpfo9qsS?_ob@KzQVMR3~)_=7G=CIXA3Y7OhKXt-7k zZaN}qyt~;1$0emV5H|}V)GYVRs5`Rp({7i5E7Zn#w7v{K>)6S5Q0kX#LWt?O2}uBl zu!Y%Wui=BpR2DQ=8Alls-p9x>)Y;??d7YvRc}@9m`&T6iDo0C9NZ~fridE?wO&6xJ z#XppgVwn=9Y=mI)jGFBC9j}7*`afwzgfBjWojQ@|4UEuyZE(^ShkAFg#p(7f4en4I zmSBFI`O450L=SH%e~W3E?#Nv;0$pTrUz^hz0<80Lkj>B>w5V>j3=BgHE`oCHSHE0T zUY<2^7-VRi;jOe}H{-{-Cav5|3z=VgIJZ>E0@21Nxt-ag1a>P)JoG8<+l^4%5^hDE z@}3#fd~ENmyszz_SK3N1Jj3gd=G3%2{51FCig`64T$HsX#ycmG{`&qIpuaeTA~oA72Dh-&k8btvJ@`a$|4~f{Z;`{la>1K{CR^mt2Kh)AI-_i<$m% zgd%+}+e38m-Iwsbe*;4hrPAWjK_8Rq5S6WSi-+4blp}x2O;1)tX_Hg_*1tuMDvBF% zXNtY?qQvF3ZLJaF`>pif$kWt=FBl`jMOGI>Jv-4rRVJH613Xm=_#MZ)7f z5#YQpq7}1F-9xaaE64ri(5IgIl8N&0`%_6y@oyPH;eAL-#TI=FKG+Ly!kb;@#l@ZH z_u^6}x@5B_B8gxK_N>vk>XLu{`s3NL(5P&u<5AC+6M>)~6dgYv?N?c5P*HrVOu_rP zg@U`Pd360%lOsd=KTWkgwN3k%?vr?SNIH{$k*p3xIKE|uta|sKdYD$}GY50Pm__j8 zKOiS|T>rhb@!_|yMT|>1AC0B*;&{IO!cgim!y-c`3Xe0_f$rx)5Z)0uuC<1dIdUo`m3mD3B`_=)e6;_I$d_^3>Ij6pG1nDxp+I`q71G z95bn$+go8MUf)^KK&lB4LMx|Yikgx+pJMQcXktcScweW|kNzXa)>liItN(q^ z19e=HZI(ogG5HQQ-C0nVl$6vB5oDR&TR}!(careM5j4JXKHhMN`yv0|l0T;P?(#yi z`S8-y+s$tqX24SArA9q5FFHfrD{PeV;Q`Ol0$s z1@slK*8jN8MEWw>2*KKPrN?I&^(G|mEOe%6ETy5P`m?90d-QS3|86blA5bFgr%Fa) zG;}8sE>l;O)njB_RXilo;#(BPMox1yVhn8+A?WZxt!3^8sp7hCL((X$@2-WD6&4Jt z`KzSx@UOb&HSd)-O239V;XZ^$@d^yXVT&bKPn+a3;y@Vc6cFNyzJ1bL$f^rW(D&s;#Y zS6D-ooIWA`Re#58(!W7#P0MB)AMtKd7OQmXu{xQWzC^@6^w`d^`Pi`0{N*=THQ3 zrr0Xd3&u=byJ?oMmjpD=OwfO=rqeq@yHn1x4He)kD)SUchS9PDH}S{b^mN(2*o&|q<2l;k+e*nF@9&207Mz)~wf?5H8|6H>k_Xqek{Gq-)g?!W`qdbYy-qK`x9RZ&PNv!8=&6S(Pn=tYq4F#r8{W<%0Fket z2kf&rj;L77Qbm;ow+R!_=_!f5W3Z#u4eCs2L5JsiIjL~JpTBrMA^l%?K0F*D#ijT4|6oZ!RYV5Y9kQ ziK_u5#8b7WTcI5&4R(V&$x{bm%AU{2`vjuW-NL*LQt_8Y=&otIzH!KW>)!t&b^(gw zW)Z@}cTiFXFshU+7s*i0mPuE{n)O)#uT%wiw}p;)xb+dobUB%k=@oIVd0$Gn>`FGJ zu0TK~5)=YJF1Whn1~A-@ma2a96;56Tg^zd`r4SFZ{YfW0LOPYN=80zXXI(4cYX2WS zV4;_BH6E_C_w|^NHC`3!u1l>DcZBbWGG{HJ1d4_INea8?VZ;F*9c27sVDhdG^J;rq z-CK&Nf08mRCVwYt>+l52_Hzv%k-B-ctMPTH&41C${u1p=ejVzRO8U&%(*JVR-92PHK|Z&($oo4duLf|Wpt-vyD~@QEs~H4d|R;)K@R4V{X=@fLfymSykE)C|1BxK}Iy^+%8L0|a96L?NX z=w)_B@;{$P%DvDT={YIYcXD!)Qlee1{MEobS1H9>@Q&R&BPz*M^(SCFzHcjvAlO4j z+tNl4f3-V2co)Sf9v!`DAYW`@$D0RfoAl%u+``Qltm4(wN<9igIK&3+?GxB=6sm|( zeLr8x+s|sVnpCEm6Isip5Q&fDZIxGyUBu!lVf6Vz#Z#IM!PrXR|Co>eS6(3WR~otZ z-Qs-3-?Q@4D`~tTy36X30=bo-bT7IRCSDXA#I*pkwg*&5!l)wgDnAXq?h2Z)xTVtx?B$iW*ZfgYPs!B~2#&ts>dO4-J&%;Lp6;QU_f@eq*XV!)0~OtE zbCfc27G;|{QKN10e9n*Gddi`xPhrO;`!t7xh^5F!AI=r&BU0z>&)|2&o`uw?+ z9-Wr(XhpDYT)51v-_ln~j|OV!({||vVg6KN`lg=Sbjc>u@C&(K2p;o^(T@`tll%LR z!zI9EWv;{13*P0`I@>;I>G@`-pXb5zYsx6KGYd^Qq_P1Tjf<>u`wDLVWUJGO6&nPk zs9>1l8K&b@XmKNhKKa@+rWP3_q|H=S}O4xDk8UiWiTVLsppmdMJ-uQ1`85N!vX z@Dkuc2yLvuAY=3bVpyjH-{2;0&O7HWI`N`8Dl;iUeAN-eAA&!7u$r0tXj?L9mlKRV zt==bg;~ixQ6wz^74U-@ycl*T1e#9gP;7*POE)Kq*D961{UG)+$lR9wVb)(Ce@Cj(^ z8w;8q%PXmh?YCNtf*bvsPyaXfe(q+~tzzL=XoUJL34i8Hi^}{&x)Kt8sC&z+XIlzV z-|O|rzgHsuC{4l!=hfgROJuR6NfhMK-1@X|(mgiYR$EA+`iDWyZTlBksWo3-cSehm zy%@Na!*Q|>V)PT|B>r!g4`U-gin8#R;NhBsJFE~s(~UjVuJ6!$uc(wyOP5Pp8OcAA zk02V?%87#cO^O_WKbupk$vpGv>Nf%ArydN;?1!(EW!V6t82<1XPh~Qaa^BUAygn+o zQ0#V|dqn0|0gt@YfF_4uACZR}*)wL?I?7wn-Z0w-&|4-})w|HhhxeB9i8+jrJj36* zG3fHv3&h(kXMctgaLS0#O`E5qB|j%m0bnd$)7`gG6b9ji^eNM?eqrQ-pbH&aH)X^( z3HUNzbjW`?GNtM>8SGSqV5L4Cd*{WEnwoz_6HAE_++8=)4sxVm&N8AABP1ZpqN26t zU_N4hZ)3WYN36DC8nI*Ay)mgiyn+Nr#2WZ$Fg^(P=L>h)b#Hzyu(4V50(aWwHWgoS zr(v4s+qD(=&6_*4RIBOkt$joeG`{2PFacEjBH5TnIms^Bl*lq~MCvJcbkXktUp9Pe z#z579cSOv>vRquEQSMHwJ2fRqXYk+5nyQ5pedfT7YmPgGHFr&47d6VU+PjfGIHN-d zWjxtG4P{smIX>Q&;Suv%Fmw+w_!4}_D`H$uuj&VH;l$;h;*Zxz8w2XydpiK0N}5-} zXbuLL28HBZTKWu5xS^D1m-9&jfjNiSxmq~q`1(^9RW`Q*aI{)vEUCcbgo%kBY`*B(P>lE76QyUMzTvXM&`|c?xgjWW#tzWq6$3PvG*;b{+m&%@u*|Sb3=tcKb;5 z`-NxyvNDzO3`Q+@1`}dhmix?~QD9E%i$k6WxPuxNDM}D48P!!+PAe)|Ae0g6gzx=b z6Z0eXqD89ip_#Crjs&u%E+j{-D5=_;PoVWNNKg)9ju4Kr^= zrJCg4-KwI^Jrm66`EcDlIUgGMBcehTXcvvoVi@Aj>M)(kTokQUl#SirvUMe7NqWcE zCbReC6$$bAw{yG&^QoO>SYVK}SH2ginIa_Bjbv5CrRya_;aZE|&i!FztpZNLCtTT! zxvTf@TsBp9&DLv?X7$#mn^5C5YFdr%9tLor3n^rr?Ri97PA|#VUpEo=@e2byvLibF6zT1y*;o% z*f;NYy1fA#rMzlw3VX*_%3=Ue1;9!#iPxLlFDA$+A_~9#J)JYT@ zpnFs=BaXUO{OS<%m+rKd2BfVSsUQe(k09dck|TkAQU(gq*&O=4ZNj25q}lHY&*Qz` z7lo#?UmLNKKoCQ2uwopCllK536%IKJ)B6Dh7e0ng9uGEXGts9qMm-VNfg`PDpd{o1 zMQ|(3>D*yCb|!vv6FJK6AF8}f8*a%SmPqzG|DN(588E(g|=m=}GCncZj zmNrfY6IuOaJ%&y9Bf1{?_oD)ulPW~T8Vd4g<;rn(s1wt-$+|j)hYjN?hs`|6j)T6M z-$h&&BHqUXhbZ#WJv;bG$?sL%9Bdrdh7@3V3V&huKSWe29-EUaO*feVmr^VE5@1G# zUXnmqqLv+E~telp?8hn#D9#J&tvSnxv}I1%6l=GtfWy56Rh z{!4?a&X2Ywse+XVH?QVzOy(YEG9{=B9{?T8W8HXtYfL+X$zre$z=@(nctpZ0tP2=o zU#LJZTJPu!C+gFCRO^%s!!H^*#!x;krlv5j1}IZR0GEu^vp)EA4Rm2D-NFueZE@jm z0Bhy4`~A2iGe zEq(_1@%fbN^Mu8sLagm#J-nzYgB12eSu#(BP|x{?gj;&etUWM+s%~)hgYsQ=IV3D2 zPX%3*q7QWYEqs)XGR#%{mk}wizbX z9~rQ$qF(z}ckDA}Ycqi67tSo;cDV9!ytj_4%sjSxm$#I^L`V+{euf*8{7x-tnz;*4 zoVb<%U$yjbhw~s;7dG|nJafBlI+5Z+ zVWR4x^l)27ZN8bzkT+vKgUE!8PPa<_bdO0fZdlLC*oNc@gjcwxFrcu%GX+SN%lcE&b>ujmLqD)>a^ z$Mb#WE56xr?dsll#j4@mpB8me*Kik6-dbJY0ED-Qu+t{Jrg49&(IWvQ(Sb)@20R+2 zWZV_?>adqRVY54>j1yu*{0C$(m#Pr|g*u6YB99PcAN6#iAI)DSa$}mtD;9OO_fy8b$ zn0>!F^%5l9ew3@uiqOR`xV|T^z(m;dC5NmCwEVv z(JOw+b(d4RJMzXJ1QK*pX){eNAZ-&Kx~r+F=v#GJxNybCe;yJ0EREFTu^hf`N+mH^ zZ%j0CR1z6pUQzdzy8WZ?+n6TWr$-F5PR@a%^6JRFsM70W8C-bJ4uUJhZQSB}p-(jz z5Khyda^w5AzYQX#+@7Byz2PSSG#lr1J8yGrn`dD>MUalXPTp#x0%PV^W@ZEqM5{?* zyXCSFkp zk~{=d@bE6Lw6rsNGog{YdG-n^aUK!z)yE(d`i9tywh&%tr4mHwrPx zCl_LTTz!B`a=%@A=n?a#S7O7z8W4`d;p5{BK_@Sw&yghl!92>^PHHLGz_oq?xsCaD=v7Q{=8Py6sPsT_Ok zEN$N3UL8N*?raq7vWn?a^A&wg7UYroj&M@Kb#X{JTsc37>76yemb(r+y0CUrVg9Id z7RC$qz9dhMDhFd}Wq)fUkiyxYBLdnL&B48;URn&5!DF*jIHKd*GPQxJOfUy9YR9m(v1s80zB~U6J&q! znq-XE8NFOcSq%{>v2FUh`EU@%>)G8vfKVA zbr9Mv2q?=lD)~f^;}T{3CfnLL`WOVf?HSwo0eoIEum8eHb(@RhjB%#qt*?X?KMsC; ztVvBY^|`CymZx+Lft6 z-B}zd?Z1tNZh2NysD0_hK0GiqvuAmOMgl7?_mo-R6^ZJ>^k0LU>z4Mznt*=NyHPSs ztKxXA@r}95iD5ba{67d+0GD}q0ZuDne|S_Q;oNp4l8Nr*=Ydh@<;1dBuTCs&qJNjd z$t;L5H%=9#ECOgsN7uT6(I=&ecM1M{H&7T1pzJ+dw~i>{X#c^`bYw5^UD5Gw_j%%q zPCSkWb{xa*FTQPH>MempxV{FAZn47%1?dgBpahHCq8Wp{QN~nxBbB4(qX8u$)C)o%dZ=PFd1v^9mMuS z74FDZ=~Gi^QCY8A`30qZ9*FMteYaWP`}C2*MUhMaA`M2gi(3k&OF0jxfQCR1_V)MF$6#WQB29P9of+54 z?fd`$Vm(F=9%gF#RcGi`{C~ZeTMzRhg?)On9zp77X8g~b-&W;(|LD7$;k26plWRFQ z*a9r(e#xCqX!RHSa|H!Qc{WJVBs1cT*BrObYoJycPpZwWQ#V`S?@6QI)>>;^82oc| zV8f5syB$4O9BE{(d0f(8ADln@&`?Z^5GGPsv9WJ_7)q&^>u4$4--xH6bItlO(d0!Z z~n_N|H{nzN2~e0OJGQiQmU0N-OiK?_tt(d6&l+AhdpFHiMAj)5^Z09lH zkavtosA465e%aH9klf$APr>n>R(WXa{BaN|L_EdB?(=>+eFk)r@&_UN% z4q2r)@r;ZvgM9AQq6MAChYMaQ5uqNvseRWU#fMqJHfcO9KCWKGL~0$@d$7Q;P?Xi1Dw1dc$Ub#x^3}TAKM4R8LK_|~0_)E~ zSZrcj8;Vpb{C2k1xuh&vc{}99SVEoYa$)q0W#te*didq(LaZHG=2dhR(XT^FYh?29!c_+exI4cWWk-8`l1t08Ks7>ZJ1}#@g={75gkv}*yaAj zqseMpEuPUsTnlwOaw-{Nz2Yl^(+0cR7@0W~R`WaSYB2DC-Q8(gQzY+x^S9yoxN1pF z;@@n=83`WKc$u|)!VtpH0Y+>`J!B$_7`qEn~Bemb{<69=i*?L2Vl#CEpCXH zdimT5bzA1WT-5`jeP+<#IlWAp-rz7UeMenI1j|<%vWzT|{C|AWll$&OuWlJd*^^M#9GgX-|bchP0X-L5GCCU zSId$D;~MEY%iiW++7Z7?~4^BrVZC3!YTe%v?E>G#&-Hy zS?a8Y527C|BJ%WS%q+_9-wuZXL-@;O;`l6okW6VmQ>dfZ?VH$W`bHCIq>}W8L@ny_ zzcZ=w8mBVV@Az?>G4(_-9gV4cEJWZ$nuw7u11sbAF5PVgI?VYm#x95Gl`D4uEb`g# zVTrqO`O4T#(G&`#3x9^dCzN)%MB2#6&hCf?4Al*T2n^|K4`&uSW~pZRoM_RcnT&yz z8uxZ3uM<~&o`n3BmeYw$Sq%D9TCy+5kuPIAdg1f3ORfL*wZ++nV7;n%`YVizMP0#l z7UQf^*gFcq41U`EB}28Wp8tjK`QQ6b8Ka)x_p}Sx|{^i5Z@k4}MKYcJL-5c@ zQ`n=fZm=WgcipZ(7&`2AOd~Iwuy{OBR&MNQD=I%o|IqimGPO4^vXxwi{;7+cqq$NM z6YeZvjw@?vv(`NI=Ohy@B+(N{?y2WyD(vkSyM7v}2uka0lVy=0wZES#k%oj#GyB0b zz_6+p>t!MJdH^svxL0PKleKQ_vL!6c2@|et7n=FH0z~r}%OBS(S~mutVR?sJlmPU8 zd}*%%&b}dx0`V_$pRETM1Mf!0;4v` zAG&Zh4wwDN9mRfpq0Q7O@In3LJu3A^qM%jTWUEC^)0{4s?ne89=)>DQd|rrSpi^n@ zK=%GFbT&ZeZ8hs`ASzg9RJZU2A)KtI^0oFv7P+0vMaO(aLickz)-Q!&sPJ|(f!{Fg z8V{CqUJU9bXX>a5257w1uC>PQ0=OfmM8n6fH`M(H*c?ura_bS{pJ`3B+}GDH=uEU? zS~cQL<)S-4Pifv_V$ae>i!_~Txt#%!<>=4-r%9u_ko+l!Y$h2_|E#wehsrR4mqYfl zJdl-fu^+$azzA^|`j;$qgTLe~0F7^yx~0bSIC>mhqHWrg@}WDoa&ft+mnqHzrbUzy zUgU4PJQZ}rP}{I>IHfI-B>49WE7dJu7fNe;HhqXSu)Uh>`0X-vHh!sC&dK2T=AOtw;AiBzxsO9~x2_sT;IC z2yQOR0?F)ws$aG6-@S+JZ5!X&N54~O{MB%Y5j+AEE!X-q9?#h*H*R^_xRCmX=asVjX+;{dNk7?jq}R$`{rLqL zP8-@aG+aRo*vL-{fxjvl*eG#U29l8QJX>8G-bfj5n!Osm|dI^F0}Qk;kRtkzxZ$J>=4 zG;?p%MI14qUkB&@$Y>=F;VI|i!gG^84IL!2ffrn2-`w5*kT zcHf$xy`op2B>mx@a=ITkeFVy-8dmf&mmA%pXfs7dA%9*5EkqB+j<}ELAMYn z>U|z;y{#t*OMjb|a3C4Ste3J?9vB3BWf~MZ3XQ$1#=ZyC`VOci7_=}|6JmCrpHo5# znyJAzc@;!~H@akWQEy7i1_`f*{G|Y2-H)NJP;1aLC77m;{HG7xmv=l`lc@+Qgd~|` z>nNkbA%KNb^W&{i%iMSDc9aWsoQ4sR=G`Od^OWr`T&3$oyJ9wd>n;Fx7tvljP)*ge zzNnGfH+vm_Tn_;Z)_X=os}EH`a#!hFL<@+xa%Pf|IZwG*xggMt+$viCI}nHHoRMP- z>o6s(JUv|l6MQ$VEaIEi70c^Qo!1!G2eR9AO;lMtaOG%j?S~c7B;sEJIo&j&a!eED zr5^C@6o0;}Jwq`d`xB!OviW)!W+7YZ(W9%M#-)#|K?N*`QC=GMR$T&tG4rX8e+Bt< zU;GSF;~o>GSVSR?KU!LENdvOs5dl?kss=W-%H;%!lDK zl|T3{ig^isO6m`@5~YVT%XOx_(Yahbm=zY@Xi;Hw@ps5~YSnuiMdLc7JlEeu`()x4 zQ%dAYHWJlwiAn#kV{!Wr+UO#LWPc0fIFrlSn_z2D_>O)nW|?kwv=tWj=L z))ljbK<4NqCnpC&5#IDO#k};PH0i@|eSBNCHy{ccRRaGjci8di&TH`sd9vG|-C=)| z)F`Ov_XY93dFC6qMn4${X~@&f)Xk`?3HdK>tpF?oq+({Xiiq%n{&6xp&s*JF2fHk` zGQIDlT)JyJR=-*fpenl#uKackmo25s%Z=cGKR8lG*oC7x`9`g#kUwgX-5z}NruE`5 zdnvs-1tf3Cd_9r>?yMDWf|trw+wi{iG8_rQxKJAb{LhiL$ydq~i3Hh0eKxGo74^?# zo#=tKSY`7rKx`_gS?8mxYYzL#i zIOX9%>o|Y6S1B9hfTiyere(GHxruBqGtu-y{#wiF8S49SvQpiNLeoAFH1#x7LS3DK z-YnY?PAsSu{qnJ}G=kQCU!Eux)JBLkT)pu&-J3WZJ}N>Pszbn&(=!{+YQ=F_3(#_| zk16z1|Es_?hoNrWz;0QCT=WEYn|Gbveye{`KTZQX-Yya8}De%=U|nN02=p@K_9cMIrU- zMPdq9W!}WS2Y^3{>hWNIi9bfGslEQUm%44HvGzB#jULOU1ykY=d`A0j=BR^WespA; z<>UnH=pt42Bby8QwLJ$K>%H4U zRL1DX=LSEhg*FZA%M(A7!<;VXt{cv)-SSmyrU3HD2qGbVbDzrdAO-3cB)9V;hld2! zZ6wPKHjbKew};`NjHd0ak&zLf7!RRx(IQPO$ialpq%esJy0+9r%Mn1YLi)Uv625bF z{+-Ew>&_1@>;LV&Kiu+zj-~NJC$rZ%m8(Q7etTWIvnYt?=W3y+oabs#mO#xmVhtiE-y0cMiKDss_}7Ae(@3R6 zo(+GYs7@BaCmPT8uwId#Bc;JskzJyf6#UL)F z^~-Gm8AUAzANPZo6GAQSCoPBp^}9g{#%%)}Wy~VW+6=&-blZSZ76@eJjdiCC?magB zTohX7aC?UN_hHI!Kl%RJUh=AY22ze$Aq#*P%mi)AR6rIf*XN^k*4FtKCjjP9P$%vU z?%Tk{u}1iDo3FAc=PTrSck#ta z;@cIOtVZHzvG)O#6F-r~=PI2F^ICHrV;PEWs#c$t7~({)M;$)Cp*u+f=0U(us#a6#4jMGjM6gO z9`>;>whbD2{|YrZKpdF__(2zRlbp`r$7&XHZTp{1*0X^aHj8vOb&_^~>M1 zSl^df00>}m(Ua(FxQLw8`rxNDc-g2}Otp>2nHFnXJ*o=1j>o?-#f#j(_j;98o_96O zmvjX#8z6#p)iy>{Q-EBg79==t7Y*7G2Mh_~x6hQK)_z1^tbv?PrcVqVhITWd%AStL zUC++rk@9BqL4Va9qz#$+Z4_XFf`*P}2PAW-d^mL`u}m`+Xy+;l72^89Zu%;sPtA5d zrEMY0v(M?HT+6Xjo4>{p;v|`y7{Y?&ow9h3n$}&scFR0S`+!$ffxXlA=wF4p6|zWR9qfh}9a1^7Ife7Ge{M>~q-oFP#|SXZ!Za(zr<$jGJO>don*(ztwA zMnoQ6Y?CC2w*pCwG8en{RvS+Oqq!(tL~pG5%LjtQL%{%@%q<^UO}kH&w6uT$D^Pjc}II? z1y2_}?8@OUE)3)B;}-v#e%PLl1VN!d&Fp{^`dc5rlnD77b4+cIdP4k$QF~K@ti=tB)tNozRF^uGxfc?dDoch&{q%rGd)QJDGA`{2kXYXZ_t9U?S2k&&SsDM8|x9yrw!oLMr?f z797ORfG?!cGkxi4$Rk#9#e!w_+~?mS3a$C6w#4-BhX&(`01u2PE}UWw2&cy={$e%H+5#E+Tk~~Af8k-CQI_AE2t!v>ew9Ht z72=pJe-69Z*{>_e?>ln z@M8(gQr}33Eq?tWyt{$_JuhA{Je0OjoE140QsCp3z zRQp9TMf@M;&#`=~8hdBfeEuqW;8$NML8g|Ui1arnUzE5>=F85_`MkEe-3E5CXboA$ z<8=M_VwXP_a@!0fy;mm*X>#x|UrWoltpxa7-kzHs_liP&YG-a}C!pCQ2vIW~Y^N&- zZ6reeS9*TXp&Xz6x(}?Ffvfq>%EOA(Un=bK&s<@-yR^E>&4h(_s~fasxdsKK#@YMCm+PKY zJ3PteK*(9y#jopG8~9bmAhYdp8u8yfa`4W~gG*m! zG}nS2tvD$>>Fnqkq=CE%lA7qyEFX$T_bU3pjcjFi=ij*wD@QcyIZyPcOrb|XYDnVO- z{}=@pXR((-(#h%e{5U8R$C~U-CC!99&tSn;%OJ&(m<(G`XqNX=5Ske|e-7ChKz!H> zV*-^d9u*s0e!6wXe?GChPxrHvfj4*cEGYzzcooBhi}@HOGUQt}MxzU^7B$AvczLNvcjQz^ z1vxZ2167pT?><1i>?cZ*sf_KjUVYXA78pVtsqe+BLBz_{ zW#_?;hf`-z%ZTd!{$D7C3)f4;10rzvcD1L)_KZSA-OS)S7V)S}7uy!!T9gnr zI!jPUF7d1wf0X$CZs%~L=kaQr*zC|jgPB`#s74vmr01yk2R`mVAn0+{4hv_Go#RF< z2i?oEGPwZ{cIQfYuTIwn2M~w27c)!gi`FHcrEZ5N)LVSOwN1}0bA~{kBfOD*rM|P* zGd-5`?e;Dvg4;XKi>+m>`I%~GmzIkY9Q3X`^f!ehlYcJv7yCNJPer!VOMSlI_QLC0 zu-AuwjS@pU1G_{QCHz89+0vWjwDliHuxFOZ`ow8#w{P>$vOQ8g(aqW|!Ct7j#+KzO z>Rt%_Pa^+WEsQ|o%s3K&&=#!gfe^Wcu+SUoV5BEtyQ8q$K#IyGg-mX_2d@l85%d}n z0f<-~t803hQ{TvXv(C@fl)ck^%-G9$Qe9kJ1+<22dc%nRx}Wp z)%w~LlJwGi9YZ1m<8p(KU~vK{-WHs#Ra;kxC&I^LJ73J>$0T-Gs=V415sL0p7?RZo zsrMf$^f{{7_+rvr8SRbq5OJ~3h@ImNUh+gd`WWDc05M0Gu{T}PN#`@;82ObJd0r{c zA0Bk@MtSw{zPWz8jcT#RkCQ(Z%R$w0xwA1)qfqhSCdP3^^&tKWIP82%ajgZuYxY`$&yE z4}_?5!N^Vh$GQ(wH%{Bdlt-+i(EJR6z~3MnOMP*^7VC8bfgrZ$o?xV_h@$OTZRz{` z!s%6i?{tyo?I&-kKWI)6i%r#L)bA^jD&o%+h8kDNPE?34otiWC%wPMkahvL90axXw zw+xcnI+i0frnh&fBm$c;1Eyxxcy_>SCnci_3Uh|E_9GnBi-q%p zj_)>&dk5rX3$kGqz>yZ88{_*|@EpU&2BEnORA74H%A;iH8lK*_z5_cg`6_ zcz2AVfXXW^E$?vLq~F)<$4%_a|DSzu3=j#jb#orn8TJJ0WB$WrkW;CwwUFtdXD>}^!8nc>rBBcoY?6j)?O z{F*4_g_#8TH%QU388hi@h{sAZRr}JS z74%G3I?VToq#xITd;o?hGN}LhM*mBW%Rp?k$fA!G~OlS;A!chXz$x(ATQ}gsl(WxB8Z-3Qtf9consEw<%0oHMR zg$urv>=)WVR1PlJ6g$`)opTAh<>vWL& zrFORQ27GMsTfC?-1Xm2Ndj3GUdkj^4I~s$tH+jun@M*a9W^zvg(zv&0vO?@Dcq#w61dW1v zgBRcxHIZS-X4D^90vV<{PgohR0<)fEy1c}EkNp2?8W^lSO+1frG#8 z%5KiNPS(LFw2t18CzK*Z5o?qX#2YvLL)?|v)lwW+oK&2TRyV05F*qZnz^a)83{}kQ zl&V)BDG5+xw7!dYd79%19Pamkt%CK~J~?`a{bF14=L7NM8N(K+nIW$`neUgXsdRMo$b+K~Dn5N&$bUR+fm9}wqFMa_SEN)nn- zn~{>hSd@uG?SvI8X+M))Z3nilk}@GY7q2$`;bw@{I=2**=6b$*k94EscDE@@8Ymot z-;70S^yJ<6=ciaSg8wconh*zO1Ig~7SUWGuJ;>-in0_N!@@0<%B|T&BJtfIQDtfqL z!Ur~A$4L z5QXV)apu<=VeLioPy9h}DCdqHbyId-j#4z@qZT@_5fi%4#pTv)An)VxZ$+I@|CgAG zFEFt4L=-rX2L=i<70E2x0!l-32eXu>fZ?R>=O<2l>=v@(Ls}H-Le|G-65~qEizvquW+`vp=qIkZ|7}u zT2e-#;7>g0{9j?;8PwDkwH-wS@hVCYQ7K9XK|0bz0f7LaCp2jW1VZm66hTEg5(r4> zoj~YC>4*Z-rPqMcd!(08-URObX1*WqybLo;Hf!(otY~(TZP98H#ZPVOYBEPAc zv$<*{{WbkbKrQ83!2EAYQ8^k|xA>Vq+=G#I(P+`*cpILQ3*U%V$m}yysbZZ#P(D7q zh4^-N_-=Uauj2+xKeb^*b%PA?Dy^DwQQS{g4_^YzTQ&PMd^L6@Tb^4bE+P1y?PT+*wSax5{ z?{b6dRfMgFdwQ5r`&ca}aJ`t#D~Gr`YS@hpE&Vo~N|6-dV4AiHZed-Oo>ev%(SW+( zQ9a12{b}sIfyPJwBuxR++@2*Vu^t=*QAHi@G=~`&R#hsYcsDl=4d?8!s?QrOG7N?& z$plw6_IJzw_<@_PjDPJOljddr_5oGLb2_?P>2+I{bokZaCMAXE*FV{6+8=y|B&OO( z{at+%c00~2;Zw8-9k6GJB3attnC_*owPb~fjl)^egAK!gfj16n=EoC11E`|Bo@_Q6 zhv3#YlgE{*{aFhB_9hmA-P%l38&Y?7W707lW)&OqR_24tg>i-xS%LL>Eu(zME(N4H zD+l+qdCcYg-FpM8Y?ks7m3SH##y;~;i`Rl$yeLp=`YsU|c*N-HX@63GMNQ3_L81C2 z=$)N)g%2zoc1CR(g(Ve3>)R2Ch|s z!cPSXorlp{SWIB8cDf5%0c6O)%ju4ut5;#GR}pN04Eg1Uy2!ue3u^J9(;P_uwHB8o zUAEw}{AD8~V7hSmN2b9S+>`nFtK=k>&B}Xn399UhG_jst<#hJhA$E2#!gz@=6a2k) zH^?hSB>y16`}jzFKP4GnClB&y)7Q<@4o2QPY;N~4XEwgTR@CNz6w0`Y8p{Y7jX>lh zytH`#$mJ)@a(|rsNtxRwvQE*(*gjV9Y38&;%{w$^<+yLU;N+JdpK(hF2q%zKU{1ElzP@Wzc>nibI5fU+eA@@Ew#zw*{hHF~9}=_2B@| z$nn~{m73kg_D;&%S9?AqwTz+PIe>vwQKVbP%EzueIEIaf$juQbEv6Ok+Lyfhs;>&&J-+G zDCIB%L(jeqb-IF^R(>7i?OABA^yDCUyC3_Bh$z@#MFfXH-0$hoqmI?=-A7{rQQ_Hs zo3c1t-M+DB`xXXjo_gKhT0&-!^siWas2BU^ud>_R?Q5}JYWn3t8a*1T(eR2rGJhTq zSQ0;fs^a7m_T!x=M75LlbGg8t$&DI-d^PZIkZFi-s7IDN4Y zu6A;>de?z;5&2_Wgn~y0fd;llI~#PmswuE8BH13wYMN!e`2s##0^1n&@0wfs_LN^l zJ0xmZ%eB+XljX2dP8@B6ZWES6&O64!9QSSRnWr!H$8nEU2Y^XS}`wDc@oLiYP{y}>Zi;cU| zn~!t0x_OX5eP&QEL;v`MO!0v!g1>|xbNDq?je&z)S?Uh@hljo0C*i^ax8j1@QpEC1 zg?)EUMRD)QpX1TMQ$q&hFx0yU6xwz`eoYyP-5z=p58fl!l}`X6#E4t zc}Ux%QM2sl{VNCWX`;CNy1{OnzWecS2C=x+VOxW!@QBAhJMwKF6uOKkAwei|zCqOi z`}r<|`8p(CS;!1zY_Bx+<5S&cz!($tyK5p1(WoKLv~^x*)*C9^@V`#h~odu*0W=9R|MBUxu7;BNlO< zu6%Lzwiw6>_3N}B(A${4HBejIuxP&A%a)>X-huH4V>pkjSHqM?zU!SwHU735f28*J zM8CW!SofywT!b~($_|aP-P1FJGA1Vk)*Yw4o_)9FOtQ5_vgQ14YwO)sD}J7~l@&h| zV>PdExg{BRSt|1 z)Y{(isRL=m7~YK*dzLqP{kpe(QOV-fx!B&iG>L5M!o$ro2wo0*&wrv#xEgPVKz|>@ z=DZY~V6)R@Ue*z`e^Ew)U_h<~zjoIU5aPCJ1>fYL88@ZBzKzZJjcBBXeoFuc@fCx4E8S4scA5=;hVolH@}_WeOI8`K)~!=U)yvuGYuWAfj;%}< zcQ(!%HR_`zANqf@E-(+z3aXxV7+k9afXYn@y!Lt>Jq9ekCEkU!_1bGqq@M_0$~Dx} zscu@LnBMozAeI6a;xbrPAb0;s3G8Go^~sSA%K7e+<#%G2FfQ|}pxt1%;*@dZsR7RY zCs%5=k7Gbiq6v#l%}PO9yyY9+t~UJq{Ktj8OiFIisV5FT8XI#zx1K(gETVu<6&)u& z%Y!YB)sY7XNtC8)wE0dC=HnYVw0Wt??<9&Sh_U*deUuka=a`d!8=v`I*~Vh9UmtQD zF~VmMK#3V*nPnk%Dr|}UR)mib9Z32SuV-82X)?T^HUd`WJ zn|hZe&2_9CE4hue*W9kx>WWwNf1mxn($k|BN6+1OoG@7=V1TRYEW`}y?D~Ly6luiJGR&r<{~WSX%*(x{^1c#0ozun+nec>Za&V$`bn0TKJ|bVET6Y%Uz8W&53j-*skxio;b&8aK56ho4cRET1NU0=%(El#8`j+s14t% zl2XLSZRn9ZJ9-0C91yQRnV9@}`14Kl*B6JsAAu?x)J}5j+`DF}Ks-Kvv{>Ho_C%hg z#?nyZyYr9A1}x+%8LJfw)v+oGWX84vP>I|(>Yv<#2(Xlwx&IJu1W~gPwPayA}lGaOtD`{-xiujF zBBYIK?9TR}PoH;nqG*qy)0weJR7#@vCOCt=Z9pPd?uB(S#SjC^wpu|vS590}%hv2P z)!xk2o&9KA>~Tjo)&l$fdnjCu%WImB`Fqm%D1@}@^X(W< zE_h+zw2?=Z=*jmsw#9k%>~@-5H~e!0>{tckz5A!#<%+Xb$rJgW*2(ND6R{575rM5j z6F2{K(Y*Qcl}3T){<>r(9hN8jXYyUJZCxq}mV&Cn4d&cCA>!2CsAUJH3o!M1P(ipT z%qX5VBy(0ZbJBa8JfkCOD8Dw!vK%U3>ZljerZSGD(p{Wzug6?Ym*xZem<~eTRKue%8V~ehKX_yT{}I3mD493h9!?=@cyYr@!4tiU<=X1D#C0mmGw%0P(9u2 z(u5>DtED3(BMEzixc^DA9eznI3JreKrv1!4H*{h@-*HG%#CMtLr?*q{i;6Lg?lz{r ze6oh(g;~mN;G>l}kS>$j!7>LqK@jA6jV|S?oeq)6E^tQ;wCL-!j9}KyLd2Wlg321j z7~bN65`7m~S0)-vAUv^F!}afog|ygHkK4-t56T}IR^A(*P$`mGX%>})!%P7Md^){D zf8yHJJ_&yHe+qUtnFyyO>!A@lROBm*b=pUG#_6*=z{mZERPdQiye%wpxyJ#UH*7Q^ zOg-qTD4L1e-C2<@%0VyR=vaP@oQZEw?mBBV-dV(PuS8hTcHU4jd;U2%t0QQeup>ta zSMOgXEiAZpMa6}`2syO1a{`n0$wj65DzX2TrgmHW(>fey6U89e`kv4TX8Z=htE@*F ztBv=woxR`GWtE=Z`Eaib4NUMB|97|Nr4kC28lmL~S;ZjX)-Q)Be_Xgm+3*WQ^Nwq>bYaOrs1UCo_jxWWYY0 zFPZiH*tGT_Oq?T>C-^MQ*|PXGw0mP$zKeG=!^>!uA3CVM&8KkKP0qvs3@6?9O{pQu3NXn zUNLv<_xOb8}DCN-` z5Y%8fHXTXZx;K<1z3l4!_A%!spu~sL!}wKpVLaM%3dwC(t4d%FJo-zXf7kgF20Zu+ zk3p$D%gLdR%IlNirXD80$}^M&-8qVB@PE%qoL=^{hK znM;7&hgPdPhZu%@+wt^{qr&_mEM{fAn_^tznq?#bJmGDd&2L#VBpkFjb zERMJXmu_O}N}si~mW4)S&Bx|=$pW_bH?~YoyLKCJ+tIWHEtY)6ZAAtS-y|)x(Xqj4 z{c2Env3ifRpoXRbqghz5{bIs^)Q*ATzG3`a4mbxAEs2bm6k+#pR^8B1+n76;EN+`u zo1Y_W`l`LBT}q0kno@KHU=z4hA_EHrL5IvM4|L6-Va=Z9wso3ja@)4mQDU6oVqCT` zxCL1P>}8GtLOUrAjhxpBbygk_94brd2v@D3eYoc%-uk@w_cL~kKXn*Iz&IB};00N#4%;rd5nmIwVG zg&h5GzegCMZSC;mNKo@vqT0WnuYED+F}@d=E5ppi8hX@1h!)U9VHUm-y;zS=(_y(m z8~|jP{9%Ie^-MatTIH$U*^OEkCJc5?XBtC~3l%>G<%GQUG9^Y~sxS1hK-AN4>qiK% zOz)Xq2pIwP6zS=aDtipe_4B1T{}S^LFlCf}{d(g**WXxA!cv2vfc+_i5-SCvqaaLq zz9X#~kV`a6^E+;~u}y0#z7_uY3ILGjFT+Jv>A|jk@G?V>{(Sj7=GFo+o zu?#>ApZVx>nC`!1{ITjHv>oFK|vj;H)%BQ6NxzZ0L;2oc>9XkCeKbtzs+Z<>8s za&Emo=E-ay4Hp-wN}ad@Z3R zr11~;aW^u}-A*DorwM9YN{om9tfOpUnqBA=^zXd6v1ib(Mx`w6CKxfcg1L8=Zp0fR#HZlw@4y=x;f05_hbBy@c+>zo!;b2SAf%l+2#_` z=xGfpJL~*WH=_0XHtBf<9Q(1fqA1w~PZ|CnViM!K4QKKW4l?@xkz=z-6&;Uz$mvxS9g%*ZI=K>&=MH+TqDU@E@1 zpBo{-qKBt`@`3gDZWQ>c*A3|E)hw4p4C|!X*i}%=mKhiBt&Z8 zTE*{g{#tn=W_kRCDHg_h`P|GEY`%j;?a<4tFT#Ib*!`ri*dgg1S5Nsnz%>IP63*Pe z7VyjI6v~bC4t)$DR!gv8W}EpsN6hk0@3ntdhqvl&C&)|ek_fCaocmMSw&=EuNf;bq zCH--M$<@NV+r*(KtHJM+Q}lVQ%O|Xt5Bq!zftvtXsVwX(&kqsL-*?tx4t;)qPCTwK zc|>_GoeTqPb}Z)9a_RG#iWk{AsPV{lRBv(&5bOL&8h5c;d~{0L@bNx@W+UJw_|!Rt z!H?lhceg*h$DPIgi@hj6eD7nc&qL&vKk&iD#F4D<1~Z1mFv9(p^#8_Ob2YpVB(m&( z(2&Y;^8&+aTIoV(q9FQ&EJ2v$oIaf~E_xRgtFCx$^q?kC`4+{w8lxZMuGw_UK$?FS z`1<e0^*Kd{Xc;HJg%X_wt_gfgwN36J&_a54GCu6>z=l+(yyG}p0Rl-A(1 z=KoguFyC@;gGmmT^{Z7$smigQ9QH+;DdHKX8B1296rP{41OIfJj z{9X8C^6r83YpH6v10?1o=l`V=?DynMj$H%NyL4Ua78rT2o~o4VTUv9jp(aGaCso-p z4mNnd-eT4Rcz*#zow4^#TQC`shPN&8F{)AF>(Roda=Vg((vTNXmg`L+Q&WDAE)&Br zHPdIAXT9+e^1Zb$E$fB8&}-Vg&YJIK_nc)d)6Cqy|1E{Kam!yN2G-OWgD)(W15zvG z>m^v*tXBwBz0R(SMt4|gY4ru61X}09W3SULgvSbL6}?ea)0)~pIqp2YStdme;Fn>W zLfxm^Vk%+Bhz}>QuFyH!vi*~69q9RO`^R`0G>{%~Ai_e+fR?9fgg7N7@c&bNsrS08 zn;VGXna@{cD#D@l|NLocKJ+gevYWrbw6#VsW^;Y1=W1@|Y9Rr2wjlfg2=WUG@$wUX zMRf#(Bm~7J1ci9``6c-I1+~~Q|K9{V2XpH;UjO?9Im(n { + const { popup, setPopup } = usePopup(); + + const { mutate } = useSWRConfig(); + const { + data: connectorIndexingStatuses, + isLoading: isConnectorIndexingStatusesLoading, + error: isConnectorIndexingStatusesError, + } = useSWR[]>( + "/api/manage/admin/connector/indexing-status", + fetcher + ); + + const { + data: credentialsData, + isLoading: isCredentialsLoading, + error: isCredentialsError, + refreshCredentials, + } = usePublicCredentials(); + + if ( + (!connectorIndexingStatuses && isConnectorIndexingStatusesLoading) || + (!credentialsData && isCredentialsLoading) + ) { + return ; + } + + if (isConnectorIndexingStatusesError || !connectorIndexingStatuses) { + return
Failed to load connectors
; + } + + if (isCredentialsError || !credentialsData) { + return
Failed to load credentials
; + } + + const discourseConnectorIndexingStatuses: ConnectorIndexingStatus< + DiscourseConfig, + DiscourseCredentialJson + >[] = connectorIndexingStatuses.filter( + (connectorIndexingStatus) => + connectorIndexingStatus.connector.source === "discourse" + ); + const discourseCredential: Credential | undefined = + credentialsData.find( + (credential) => credential.credential_json?.discourse_api_username + ); + + return ( + <> + {popup} + + This connector allows you to sync all your Discourse Topics into + Danswer. More details on how to setup the Discourse connector can be + found in{" "} + + this guide. + + + + + Step 1: Provide your API Access info + + + {discourseCredential ? ( + <> +
+

Existing API Key:

+

+ {discourseCredential.credential_json?.discourse_api_key} +

+ +
+ + ) : ( + <> + + + formBody={ + <> + + + + } + validationSchema={Yup.object().shape({ + discourse_api_username: Yup.string().required( + "Please enter the Username associated with the API key" + ), + discourse_api_key: Yup.string().required( + "Please enter the API key" + ), + })} + initialValues={{ + discourse_api_username: "", + discourse_api_key: "", + }} + onSubmit={(isSuccess) => { + if (isSuccess) { + refreshCredentials(); + } + }} + /> + + + )} + + + Step 2: Which Categories do you want to make searchable? + + + {discourseConnectorIndexingStatuses.length > 0 && ( + <> + + We pull Topics with new Posts every 10 minutes. + +
+ + connectorIndexingStatuses={discourseConnectorIndexingStatuses} + liveCredential={discourseCredential} + getCredential={(credential) => + credential.credential_json.discourse_api_username + } + specialColumns={[ + { + header: "Categories", + key: "categories", + getValue: (ccPairStatus) => + ccPairStatus.connector.connector_specific_config + .categories && + ccPairStatus.connector.connector_specific_config.categories + .length > 0 + ? ccPairStatus.connector.connector_specific_config.categories.join( + ", " + ) + : "", + }, + ]} + includeName={true} + onUpdate={() => + mutate("/api/manage/admin/connector/indexing-status") + } + onCredentialLink={async (connectorId) => { + if (discourseCredential) { + await linkCredential(connectorId, discourseCredential.id); + mutate("/api/manage/admin/connector/indexing-status"); + } + }} + /> +
+ + + )} + + {discourseCredential ? ( + <> + +

Create a new Discourse Connector

+ + nameBuilder={(values) => + values.categories + ? `${values.base_url}-${values.categories.join("_")}` + : `${values.base_url}-All` + } + source="discourse" + inputType="poll" + formBody={ + <> + + + } + formBodyBuilder={TextArrayFieldBuilder({ + name: "categories", + label: "Categories:", + subtext: + "Specify 0 or more Categories to index. If no Categories are specified, Topics from " + + "all categories will be indexed.", + })} + validationSchema={Yup.object().shape({ + base_url: Yup.string().required( + "Please the base URL of your Discourse site." + ), + categories: Yup.array().of( + Yup.string().required("Category names must be strings") + ), + })} + initialValues={{ + categories: [], + base_url: "", + }} + refreshFreq={10 * 60} // 10 minutes + credentialId={discourseCredential.id} + /> +
+ + ) : ( + + Please provide your API Key Info in Step 1 first! Once done with that, + you can then start indexing all your Discourse Topics. + + )} + + ); +}; + +export default function Page() { + return ( +
+
+ +
+ + } title="Discourse" /> + +
+
+ ); +} diff --git a/web/src/components/icons/icons.tsx b/web/src/components/icons/icons.tsx index 33605911fb..01aa233549 100644 --- a/web/src/components/icons/icons.tsx +++ b/web/src/components/icons/icons.tsx @@ -52,6 +52,7 @@ import document360Icon from "../../../public/Document360.png"; import googleSitesIcon from "../../../public/GoogleSites.png"; import zendeskIcon from "../../../public/Zendesk.svg"; import sharepointIcon from "../../../public/Sharepoint.png"; +import discourseIcon from "../../../public/Discourse.png"; import { FaRobot } from "react-icons/fa"; interface IconProps { @@ -601,6 +602,18 @@ export const ZendeskIcon = ({ ); +export const DiscourseIcon = ({ + size = 16, + className = defaultTailwindCSS, +}: IconProps) => ( +
+ Logo +
+); + export const AxeroIcon = ({ size = 16, className = defaultTailwindCSS, diff --git a/web/src/lib/sources.ts b/web/src/lib/sources.ts index c561dd6d8d..c9206f2b82 100644 --- a/web/src/lib/sources.ts +++ b/web/src/lib/sources.ts @@ -2,6 +2,7 @@ import { AxeroIcon, BookstackIcon, ConfluenceIcon, + DiscourseIcon, Document360Icon, FileIcon, GithubIcon, @@ -155,6 +156,11 @@ const SOURCE_METADATA_MAP: SourceMap = { displayName: "Sharepoint", category: SourceCategory.AppConnection, }, + discourse: { + icon: DiscourseIcon, + displayName: "Discourse", + category: SourceCategory.AppConnection, + }, axero: { icon: AxeroIcon, displayName: "Axero", diff --git a/web/src/lib/types.ts b/web/src/lib/types.ts index 66b1f7f0ef..7ea1b123c8 100644 --- a/web/src/lib/types.ts +++ b/web/src/lib/types.ts @@ -39,6 +39,7 @@ export type ValidSources = | "loopio" | "sharepoint" | "zendesk" + | "discourse" | "axero"; export type ValidInputTypes = "load_state" | "poll" | "event"; @@ -118,6 +119,11 @@ export interface SharepointConfig { sites?: string[]; } +export interface DiscourseConfig { + base_url: string; + categories?: string[]; +} + export interface AxeroConfig { spaces?: string[]; } @@ -337,6 +343,11 @@ export interface SharepointCredentialJson { aad_directory_id: string; } +export interface DiscourseCredentialJson { + discourse_api_key: string; + discourse_api_username: string; +} + export interface AxeroCredentialJson { base_url: string; axero_api_token: string;