From 352c73a39da8fa1266d28ef1192c004d63f1f700 Mon Sep 17 00:00:00 2001 From: Ward Date: Fri, 29 Sep 2023 22:44:22 +0100 Subject: [PATCH 01/12] UPDATE - Updated retrieve_utils.py to have the ability to parse text from pdf files --- autogen/retrieve_utils.py | 74 ++++++++++++++++++++++++++------------- setup.py | 1 + 2 files changed, 51 insertions(+), 24 deletions(-) diff --git a/autogen/retrieve_utils.py b/autogen/retrieve_utils.py index 5bb26461248..fc830370ce6 100644 --- a/autogen/retrieve_utils.py +++ b/autogen/retrieve_utils.py @@ -8,9 +8,11 @@ from chromadb.api import API import chromadb.utils.embedding_functions as ef import logging +import PyPDF2 + logger = logging.getLogger(__name__) -TEXT_FORMATS = ["txt", "json", "csv", "tsv", "md", "html", "htm", "rtf", "rst", "jsonl", "log", "xml", "yaml", "yml"] +TEXT_FORMATS = ["txt", "json", "csv", "tsv", "md", "html", "htm", "rtf", "rst", "jsonl", "log", "xml", "yaml", "yml", "pdf"] def num_tokens_from_text( @@ -118,16 +120,51 @@ def split_text_to_chunks( chunks.append(text_to_chunk) if len(text_to_chunk) > 10 else None # don't add chunks less than 10 characters return chunks +def extract_text_from_pdf(file: str) -> str: + """Extract text from PDF files""" + text = "" + with open(file, "rb") as f: + reader = PyPDF2.PdfReader(f) + if reader.is_encrypted: # Check if the PDF is encrypted + try: + reader.decrypt('') + except Exception as e: + logger.warning(f"Could not decrypt PDF {file}, {e}") + return text # Return empty text if PDF could not be decrypted + + for page_num in range(len(reader.pages)): + page = reader.pages[page_num] + text += page.extract_text() + + if not text.strip(): # Debugging line to check if text is empty + logger.warning(f"Could not decrypt PDF {file}") + + return text + def split_files_to_chunks( - files: list, max_tokens: int = 4000, chunk_mode: str = "multi_lines", must_break_at_empty_line: bool = True -): + files: list, max_tokens: int = 4000, chunk_mode: str = "multi_lines", must_break_at_empty_line: bool = True + ): """Split a list of files into chunks of max_tokens.""" + chunks = [] + for file in files: - with open(file, "r") as f: - text = f.read() + _, file_extension = os.path.splitext(file) + file_extension = file_extension.lower() + + if file_extension == ".pdf": + text = extract_text_from_pdf(file) + else: # For non-PDF text-based files + with open(file, "r", encoding="utf-8", errors="ignore") as f: + text = f.read() + + if not text.strip(): # Debugging line to check if text is empty after reading + logger.warning(f"No text available in file: {file}") + continue # Skip to the next file if no text is available + chunks += split_text_to_chunks(text, max_tokens, chunk_mode, must_break_at_empty_line) + return chunks @@ -149,10 +186,7 @@ def get_files_from_dir(dir_path: str, types: list = TEXT_FORMATS, recursive: boo files = [] if os.path.exists(dir_path): for type in types: - if recursive: - files += glob.glob(os.path.join(dir_path, f"**/*.{type}"), recursive=True) - else: - files += glob.glob(os.path.join(dir_path, f"*.{type}"), recursive=False) + files += glob.glob(os.path.join(dir_path, f"**/*.{type}"), recursive=recursive) else: logger.error(f"Directory {dir_path} does not exist.") raise ValueError(f"Directory {dir_path} does not exist.") @@ -200,26 +234,18 @@ def create_vector_db_from_dir( collection_name, get_or_create=get_or_create, embedding_function=embedding_function, - # https://github.com/nmslib/hnswlib#supported-distances - # https://github.com/chroma-core/chroma/blob/566bc80f6c8ee29f7d99b6322654f32183c368c4/chromadb/segment/impl/vector/local_hnsw.py#L184 - # https://github.com/nmslib/hnswlib/blob/master/ALGO_PARAMS.md metadata={"hnsw:space": "ip", "hnsw:construction_ef": 30, "hnsw:M": 32}, # ip, l2, cosine ) chunks = split_files_to_chunks(get_files_from_dir(dir_path), max_tokens, chunk_mode, must_break_at_empty_line) - print(f"Found {len(chunks)} chunks.") - # upsert in batch of 40000 - for i in range(0, len(chunks), 40000): + + # Upsert in batch of 40000 or less if the total number of chunks is less than 40000 + for i in range(0, len(chunks), min(40000, len(chunks))): + end_idx = i + min(40000, len(chunks) - i) collection.upsert( - documents=chunks[ - i : i + 40000 - ], # we handle tokenization, embedding, and indexing automatically. You can skip that and add your own embeddings as well - ids=[f"doc_{i}" for i in range(i, i + 40000)], # unique for each doc + documents=chunks[i:end_idx], + ids=[f"doc_{j}" for j in range(i, end_idx)], # unique for each doc ) - collection.upsert( - documents=chunks[i : len(chunks)], - ids=[f"doc_{i}" for i in range(i, len(chunks))], # unique for each doc - ) except ValueError as e: logger.warning(f"{e}") @@ -247,4 +273,4 @@ def query_vector_db( n_results=n_results, where_document={"$contains": search_string} if search_string else None, # optional filter ) - return results + return results \ No newline at end of file diff --git a/setup.py b/setup.py index 1e036075a36..8e477bd0ab9 100644 --- a/setup.py +++ b/setup.py @@ -18,6 +18,7 @@ "diskcache", "termcolor", "flaml", + "PyPDF2", ] From ee435ca7307e777e165e61f15c1a300392795e8d Mon Sep 17 00:00:00 2001 From: Ward Date: Sat, 30 Sep 2023 00:16:02 +0100 Subject: [PATCH 02/12] UNDO - change to recursive condition --- autogen/retrieve_utils.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/autogen/retrieve_utils.py b/autogen/retrieve_utils.py index fc830370ce6..7a99f468dbf 100644 --- a/autogen/retrieve_utils.py +++ b/autogen/retrieve_utils.py @@ -186,7 +186,10 @@ def get_files_from_dir(dir_path: str, types: list = TEXT_FORMATS, recursive: boo files = [] if os.path.exists(dir_path): for type in types: - files += glob.glob(os.path.join(dir_path, f"**/*.{type}"), recursive=recursive) + if recursive: + files += glob.glob(os.path.join(dir_path, f"**/*.{type}"), recursive=True) + else: + files += glob.glob(os.path.join(dir_path, f"*.{type}"), recursive=False) else: logger.error(f"Directory {dir_path} does not exist.") raise ValueError(f"Directory {dir_path} does not exist.") From a750f876f57c6ccb8f5eadde7162bf41dcb28efb Mon Sep 17 00:00:00 2001 From: Ward Date: Sat, 30 Sep 2023 09:29:22 +0100 Subject: [PATCH 03/12] UPDATE - updated agentchat_RetrieveChat.ipynb to clarify which file types are accepted to be in the docs path --- notebook/agentchat_RetrieveChat.ipynb | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/notebook/agentchat_RetrieveChat.ipynb b/notebook/agentchat_RetrieveChat.ipynb index 035dd01d869..1cc6edb22dd 100644 --- a/notebook/agentchat_RetrieveChat.ipynb +++ b/notebook/agentchat_RetrieveChat.ipynb @@ -148,7 +148,28 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Accepted file formats for `docs_path`:\n", + "['txt', 'json', 'csv', 'tsv', 'md', 'html', 'htm', 'rtf', 'rst', 'jsonl', 'log', 'xml', 'yaml', 'yml']\n" + ] + } + ], + "source": [ + "# Accepted file formats for that can be stored in \n", + "# a vector database instance\n", + "print(\"Accepted file formats for `docs_path`:\")\n", + "print(TEXT_FORMATS)" + ] + }, + { + "cell_type": "code", + "execution_count": 14, "metadata": {}, "outputs": [], "source": [ From 37bbf43fc1a3c9c707d9e38e95ed21920dfa96ec Mon Sep 17 00:00:00 2001 From: Ward Date: Sat, 30 Sep 2023 09:32:03 +0100 Subject: [PATCH 04/12] ADD - missing import --- notebook/agentchat_RetrieveChat.ipynb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/notebook/agentchat_RetrieveChat.ipynb b/notebook/agentchat_RetrieveChat.ipynb index 1cc6edb22dd..d9767ea9860 100644 --- a/notebook/agentchat_RetrieveChat.ipynb +++ b/notebook/agentchat_RetrieveChat.ipynb @@ -163,6 +163,8 @@ "source": [ "# Accepted file formats for that can be stored in \n", "# a vector database instance\n", + "from autogen.retrieve_utils import TEXT_FORMATS\n", + "\n", "print(\"Accepted file formats for `docs_path`:\")\n", "print(TEXT_FORMATS)" ] From 3c9e750c8465b4a2d66c0774ec8f199ff641da4f Mon Sep 17 00:00:00 2001 From: Ward Date: Sat, 30 Sep 2023 09:40:40 +0100 Subject: [PATCH 05/12] UPDATE - setup.py to have PyPDF2 in retrievechat --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 8e477bd0ab9..dfe52cc3c01 100644 --- a/setup.py +++ b/setup.py @@ -18,7 +18,6 @@ "diskcache", "termcolor", "flaml", - "PyPDF2", ] @@ -56,6 +55,7 @@ "chromadb", "tiktoken", "sentence_transformers", + "PyPDF2" ], }, classifiers=[ From 6d1ece6c27289250bf48c3fb640d03c7c92b9842 Mon Sep 17 00:00:00 2001 From: Ward Date: Sat, 30 Sep 2023 09:42:49 +0100 Subject: [PATCH 06/12] RE-ADD - urls --- autogen/retrieve_utils.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/autogen/retrieve_utils.py b/autogen/retrieve_utils.py index 7a99f468dbf..95b557ec00b 100644 --- a/autogen/retrieve_utils.py +++ b/autogen/retrieve_utils.py @@ -237,6 +237,9 @@ def create_vector_db_from_dir( collection_name, get_or_create=get_or_create, embedding_function=embedding_function, + # https://github.com/nmslib/hnswlib#supported-distances + # https://github.com/chroma-core/chroma/blob/566bc80f6c8ee29f7d99b6322654f32183c368c4/chromadb/segment/impl/vector/local_hnsw.py#L184 + # https://github.com/nmslib/hnswlib/blob/master/ALGO_PARAMS.md metadata={"hnsw:space": "ip", "hnsw:construction_ef": 30, "hnsw:M": 32}, # ip, l2, cosine ) From 48e28ea33d14e7e16702ecd971a8e09cd3d12efa Mon Sep 17 00:00:00 2001 From: Ward Date: Sat, 30 Sep 2023 11:20:35 +0100 Subject: [PATCH 07/12] ADD - tests for retrieve utils, and removed deprecated PyPdf2 --- autogen/retrieve_utils.py | 51 ++++++++++++------- setup.py | 7 +-- test/test_files/example.pdf | Bin 0 -> 45917 bytes test/test_files/example.txt | 4 ++ test/test_retrieve_utils.py | 96 ++++++++++++++++++++++++++++++++++++ 5 files changed, 135 insertions(+), 23 deletions(-) create mode 100644 test/test_files/example.pdf create mode 100644 test/test_files/example.txt create mode 100644 test/test_retrieve_utils.py diff --git a/autogen/retrieve_utils.py b/autogen/retrieve_utils.py index 95b557ec00b..68b95af0d56 100644 --- a/autogen/retrieve_utils.py +++ b/autogen/retrieve_utils.py @@ -8,11 +8,27 @@ from chromadb.api import API import chromadb.utils.embedding_functions as ef import logging -import PyPDF2 +import pypdf logger = logging.getLogger(__name__) -TEXT_FORMATS = ["txt", "json", "csv", "tsv", "md", "html", "htm", "rtf", "rst", "jsonl", "log", "xml", "yaml", "yml", "pdf"] +TEXT_FORMATS = [ + "txt", + "json", + "csv", + "tsv", + "md", + "html", + "htm", + "rtf", + "rst", + "jsonl", + "log", + "xml", + "yaml", + "yml", + "pdf", +] def num_tokens_from_text( @@ -120,51 +136,52 @@ def split_text_to_chunks( chunks.append(text_to_chunk) if len(text_to_chunk) > 10 else None # don't add chunks less than 10 characters return chunks + def extract_text_from_pdf(file: str) -> str: """Extract text from PDF files""" text = "" - with open(file, "rb") as f: - reader = PyPDF2.PdfReader(f) + with open(file, "rb") as f: + reader = pypdf.PdfReader(f) if reader.is_encrypted: # Check if the PDF is encrypted try: - reader.decrypt('') + reader.decrypt("") except Exception as e: logger.warning(f"Could not decrypt PDF {file}, {e}") return text # Return empty text if PDF could not be decrypted - + for page_num in range(len(reader.pages)): page = reader.pages[page_num] text += page.extract_text() - + if not text.strip(): # Debugging line to check if text is empty logger.warning(f"Could not decrypt PDF {file}") - + return text def split_files_to_chunks( - files: list, max_tokens: int = 4000, chunk_mode: str = "multi_lines", must_break_at_empty_line: bool = True - ): + files: list, max_tokens: int = 4000, chunk_mode: str = "multi_lines", must_break_at_empty_line: bool = True +): """Split a list of files into chunks of max_tokens.""" chunks = [] - + for file in files: _, file_extension = os.path.splitext(file) file_extension = file_extension.lower() - + if file_extension == ".pdf": text = extract_text_from_pdf(file) else: # For non-PDF text-based files with open(file, "r", encoding="utf-8", errors="ignore") as f: text = f.read() - + if not text.strip(): # Debugging line to check if text is empty after reading logger.warning(f"No text available in file: {file}") continue # Skip to the next file if no text is available - + chunks += split_text_to_chunks(text, max_tokens, chunk_mode, must_break_at_empty_line) - + return chunks @@ -244,7 +261,7 @@ def create_vector_db_from_dir( ) chunks = split_files_to_chunks(get_files_from_dir(dir_path), max_tokens, chunk_mode, must_break_at_empty_line) - + # Upsert in batch of 40000 or less if the total number of chunks is less than 40000 for i in range(0, len(chunks), min(40000, len(chunks))): end_idx = i + min(40000, len(chunks) - i) @@ -279,4 +296,4 @@ def query_vector_db( n_results=n_results, where_document={"$contains": search_string} if search_string else None, # optional filter ) - return results \ No newline at end of file + return results diff --git a/setup.py b/setup.py index dfe52cc3c01..bb642af4da3 100644 --- a/setup.py +++ b/setup.py @@ -51,12 +51,7 @@ ], "blendsearch": ["flaml[blendsearch]"], "mathchat": ["sympy", "pydantic==1.10.9", "wolframalpha"], - "retrievechat": [ - "chromadb", - "tiktoken", - "sentence_transformers", - "PyPDF2" - ], + "retrievechat": ["chromadb", "tiktoken", "sentence_transformers", "pypdf"], }, classifiers=[ "Programming Language :: Python :: 3", diff --git a/test/test_files/example.pdf b/test/test_files/example.pdf new file mode 100644 index 0000000000000000000000000000000000000000..1327f9ef6d17faed10f17446c941c0ee80df1a97 GIT binary patch literal 45917 zcmdS9WmH^Sx-J|bNFca-;S}x!cX#*T?ohZV!GpUK+}%Am1b26Lcey0Hcc1QaPT%j( z{qr#vW34sk>+-y_Y808guqZ7f9XmWMS?${&JS-!C0bpxj0T0W~O|R@`XG|}o57M`? zHKmu=H#K$yFuiRn(o5^xm{JZ zPxPru#K>8r#Hv%ywMh3v!^*au4&y0Y#>yO3s@(#9! zO2!~9`nQmMdZ zkb|+lH9V|qYOU&k%_0L*E8H00St44+kV?)wKVOnx0wFu{-Ta6mh#Wb*(|Z@M{2W`6 zS!~VbO1ef-UF?edi^qny6%?Sy^?OfZcKY36As} zdkyNZ$|lT>Y&uObaa0+6C)cinYuHn%e*tU0COY~AXB+(4>;2#}bV^V?3=1rI`iR1k zPo7XOi5ya;pgJaG$IoK(Xy!@()V#QN%9t^O34-VVD3`&X_=ZkUdv7NP~a%K!+3(ZB96s_4JF%H6qETzJq%ZwOJ=| z7>pWqh4`g`OPL3Cs*dpube4pxam< zck{ab>B(|^$gD_RN1|GC(g%6Jh}0EAGfj#(+=UUFTiq`ex%z>>9E4Zj{9^_+?)?bj z9%$z1!$78h#g9{J2zx+O0-ZBQvjaIZ`#S9o^Il-0-9Q^qX?_iyn)|y z+P2|OE^66ZSFX~JnbiJW44fF`${n~WMxP7VcPV0~qtRxde<&I{3+kMmunDnYE;t!G zm_U$5YPiL!d#5g0625P-i6Nh0UDt;33WmteNnU5}zvN8ATIQ+d{Ft?QuO{Jqo(MHj zp>aZ4Q(*e^xqN8-|AN2XV^#2B;-?;F1-~4IE-;hDC z06l_IGEFm*u3?Yxs55kgwb1bn}ghF#oxNt z(eO>Sz76O%ha-UPPs1zQs@RzSZbSgn9|ZW<&izgC#{w+?`)^_$z<4nwa(2jwD{%upq%@Jg5{TtW;f5G=} zuHRlbnA?GD9RPozn_dmT^v9EbHT*aE-^R%~7#Tafg@W?$P`r&1MPpO*w{W{r3cO*y z@$b3fWM^k(Z2cRm{|&o-tN2^cKXdS3Cj3EhS$%6``u`UHH-IyB1TZtvOT0}pb3*|e zQ!8Tt1HHiS41j)P-9NGc0?;zOy$I>si5r`nn*FsU;B2a9ZuEwUtW1n=1I_iX7e;m# z=0Ez>{-R`g3zGUjxPHq}dW)6n8_ORW!9QS6%gVs~N5R`@VPW{K_6_iV2k{MtjhO&% zGvLo8`LCq@nKS?9?>|Qoy`T)etgVB!z7@TpK7jFGqy1+h{eN-^3H&#%|1oI)6vf{) zqx?3(|3v&xctx51t<**Tw^CO#w-K;$H2-Jm|F>#k{4=xZMa>-?K|*Hw4!^S_t^Z%o zf1><$Aox@1|K{HRnC$;9`QM7*UoHFpYZ3hGz!q=|ELr>0}CaPHN7n0(_i@nu)ej%e>_?+GqV2{_cy+}riLkwTXZs@ zd0{>UK0=!-a!~{^yOEMbffkcE84_?_JQ3^oc0@JV^Upv0c{Z2TOunN;z`N)qqL}{jZbi z&*b^#UjfbLwi zP;(a88|txN?Xoduvt~%CJR7Dx@$fLpc3Uo6CxX$ng0ExLVtW|#-`p5;;;Vp|tfrI? znXa(+PaiyKTmpj|XZt*D)4CsFxZ6m%5`oX)Hw2WPo~bw;gtaoSLmDD>0kZvC&IM<< z9)4KUw57Y48(1b$IGW@U`IcyRy?sof?|z}OntRPgBta3E?<$cK75&Z3FBUmBn1bL# z&Ub36i0hZ!4TTfqRHbC;fs#1Cpc7dBn$8>-#W;LW8CHhcM#gjtt+YS5O;Q=1Z9_P1YGus#v? zmL0eJT5Zn_fU*4F^Zc(zlK-4%W_H%!*!t&O|NHX_6FUdfzbF343)(eSW#NY7l#4k@ zTEb*PoaqDefDwuwIGCWsyMaj2U=S24ATm-gQa;Be8^#RnmuBS`9f7ouls`j4TB&bT z1a#EYDvyc|zmTqat(eNfzzm+e`W)^$4aY1wuI_T|a-Z&>eo%YYi*3l}xu>oop?{vE zQ*Pz%doG2+Y_db-fJIgFUie>fM+;brTZbt=$%cVD2Q!d&nEbLM$8qiZh7 ztv^G%8dPa{`^Dvfxo%sw_X=^AK9SYsIPieKcSmP1ahj{xUgUYV@NkC7*@M>5k{Rcq z$ny2R<&j}S1Id;j0&m7cQTWLqhZ$#w?(<7({Ug4(&xm?gL+?H7^ek%`-moiF`s(S} z(!n;I`$#{Y?-Nw4%W$cdcB}2Bha*9aR*k~!PEnirF(8KtMm7fgqF$>^*u zW*yX^FaTm)p}UyUd7vCu!pINhT)=sOHL;|2tbQfQtFI`0)vBD9ta}#r?pma{d99az z{x0%rGx|bMe7ee#7H+MgtLG?}s21q7%;7y*xgS$!h__h8mB9K!ymFC!eXrI|0C#^A zvkiQd)1KAqaHQw5Pd~9yeYG6rTA9iZ?^Qq~U`H^V??;^})C2Z$JEAu4$7Z;E%f$|; z@}n5ix?Q|1Y8%K)JE+dtf2bTjkY?K!c0;}#uwCn>)N&fSM5rEAatm<4xWp(h*10Pgg3mRd=`8mNn|*6Y8G8=ROGC>*YT9JoAcq9e!xpU$0^Df}!=QH7c4k z{A!LZM7y|j@78W6wGzC0l(Ws%qnOUeR+ik`n^6${vSRo=*h+Jpn3z$?vK(#bMd8Ud zxM!%LDU3aH+uW}6Iaa`<{I1ktVhUHisqH$!v0!_-dqR#ioWYojTJ@z9Cyo&(LSA#; z?&Z@j`7#rpSbBAma{cKlnv4+~rx=ABW_Yr!^qp7a{gskPpU$PM@Tzo#wjYCx*~Uxfvf2 zj^U5j{6d%f&(~uNLFg}8?LwTqDo(NPthj?Ajh)$zVHB5TEx3ez`Dc>68T9K?_c-2Q z?LoSO<=eNHJWuv%MU&^xOTRqM*SRAS7$II2LT@(DUzN}%B==t96z9rur*rD?(qd~5 z=5ZeVlpfWRxn399HSl3ux$=~cB~s2q8OK`iH{UIrlSaDl#A;r9B)ePmbX%pn%YPPQ|=OyD$Fghtab=nV0g!tU~mnw$NN; z`g|EmJ@uJl<>K^U{&)Jo73HHcJ4gfz_nJJXB+bqcVND#121yj$5FN13+f*N7>8#D@ z3aLa}Fscjtb(BpHl7=;gnF>VoMT=dn_^)G?XQ6t-=;?vZ zIk_rkys9}puvj7B$eJ{>R^uklKZ?mbsBwIA#B~1U6ErWRoU$6T_=oTg0}gQ`mwe6=qAE_iD4O&7q;@OXP9To;^ry2lBd7 zn(n)qo=)hTj9MSN?k@PlJp=$FI^vLPcEKEpJeAtTC`A}S)UXvr$?~QAvJ#bM3B@^$ zMTt0+2913QrzVZZk6I6ncw7$lAmt$E_b4F@z$CKa?b>TF)3|Jm!|D~46LGjZDh5yV zlZz%;MkyPu%tvo-IvI~FRX8uMvKks1cY<}sra|;TO(GLG1T`S%9Y^9YQs3thwaUxH_ zd_41{EV^k!7M`2DfOo1xz6P~k3E2?YoxuXD2NWKuQ3TC}$qT8^l91RVtmYf$7}tEb zgl&dxZq@DwitN(jqPEE{${+(Q@{E2L{}|H z%%R(Up2WCLtERZIvChAcpfVPpRvYyp3yvDxuoeBJL%N(xrl#7|IW$URf?Ne%zdU}U z!7)L#S%*!{+|kL>V{&K*Bct3+KtiPpa1?5C+rMCP+kHWVPSND)BnX2V`?_ z6w%Ta!}^Fya$2=~jv9~*wQ2GwrB~{74b@il_*bS{=zF{7BJ*j5aJl%IpQfa`sBtG4 zRMx`bGF#mJXT) z{m|A)M95La{}Yco_zYE>n{RVqmG1^Y2nH?((drsDM@$_(Vm5psS7TyxZ@BjqBhlc8G{9 z)5@XF3_gtoxa-&&N`78Ih^+p8eo|uL0OCyHLRGONcIBd->O$`SJxu);Tj{GM>uCuAddtrS^VUvSEE z*#cnK=Iea1X$Oy3Vt>G5;TwgaS^vBhWl=%%(*B0MI!NA-u(^j8HbX1|;L^MPUedc5 zc961ezdJA}Y7dhv#CQ|UE_k){#lA0UY{5IwLqwCJYo2Ju+u)h8&&Enwb=Wz{fO^!)qv+luIk?!ve$vSMefcm>JDn!hJPEsc;I2xZ4&PwxS3+-*@$~okUnxe)U z4Q&O>BB3q}cSXJQ+r-E@OuSHNKVcVenN4jK^fD@Knftqo8Ww10k~x(KkD_MH7T8_| zpI({qGOFoegJ9+*Why*Bn3lwY;=?6#*RSupKL}^jFBHI8jBRfw+dyKlc1?z){ zqjWWOJGPh|*$P3^Q1XC^t4c6?v{cH01+*g8wsu6U3(VtS$fLO4+|EQuDc~;V9==FaawG31tl-|}*-=}8 zY=`%z=(hD+eSZqk4(5&jgmcGumc1s{fym4^2|TU#?jYqqmapw7?cn7f-2tGOnoRA| z$hv(&hp6o^ScBz{sEQyyRTbVE%1y0jW{QgF{8kpTp$!~~%904F&Q+)G!2zmDRZ51$ zDV9VUG@|{=QK#xb0b)p1iicF^r~`UDfjbh6Y9XS)9dY|SwCY^5NP~E^YsD!E1!|En zH0Iob_$($5HxhfSSkAew3ys8DMpEqU`6RjgBG;S94zV{ETE{A zeG{6o;`eBS95j*~EIN@wG%`grDFsrIA81`UXA%m49(kZ_j6o3E*IX=8kv23p#gDND zzG%<6dlVvNXkNK{BqFvzhnzh!5jkL0?wRDcP{@fQD27-nL>kDgXe7QKPplKd0@POg zl)FbIQiKLn{FEaePb?843~UmQ0*JJr*(-KSB9r**z^0&$D+){K$8{3>Yrw{!MJZzC z21RxP{6E8{p%tNpplQQmptR_OumQux&#Tb_b3c*xwCCJOg!o9FXQT0npSPmDMsA!c zdQ$Y%D|(Xj%qrr=Zcqc;VmE9RJ&AgD6`xc>Msw~$(J~~@gV6}3?D<2U6+KCN0u?<0 zJsG*Kx*@kYu4*AJIj&kE^*OFeAzq3tL_LR!E#y7LiY>%FzZ6?YdqNdk06mwA+!Q_4 zirgeUlZxDAJ)eQ;u^S(N1PKNaXb}?U1!xiC=M`v(66cv{&ADcA1_@}0;`Z%mT)A^Z zBJv>^Kybw+ik?=*C6XRzV4Z}0J(`PzeLk9txP3EPy@Y)+TD`b^HJVoL9C43~;u2*K zyrMRJkCUP{b&mkS1cAx94g4XO zxpR~~P{0`}qH-SX&mIjeH+F8v>lX`M+qHqeED;lC3F!Xs8+U@ht>?gBkBAAkgmkBT z+ABT{Ew6TNuj>~tUEBW;H~wjkEtmF+b3@Cqo!jF2MP1jnb>Oc>goJv0I$N$5vp}TM zLARe}LTL@m(9OMHM{=h)ewKaZmx<*}F|)2HhO7xtEDlyhE+y4Omd1zDB#@>BrzO#p ztoe3)WaDh&inlVC|H@H4|u@M6ftQWh)J%)-e^ z5}p=T5oQj`Q)-ozlp#r$WbrTfQjog$F>65BJ4-?sHJBVJi~>iDCEo1gxLMX@@L-TA zBtD4%NwDa9{A~WNV9BrHO!5+A_M8QY1wjRo7Xb=9gbHBOxRM~8aU?2^L^C9nQHntl z9B~%k0zSw?u~;dE2b61~1DT%=LGB8z>B95EpR)*rmxG&-jTQE!i4l8-01l|RN_#BC znA46ZPGpi~l0*(jWY95vzR^J_Uk3~Ybb?U&vE|UjM%1&gh`F}sgR%y&MU2R*wWD~6io6IG$^oxG zo;OGC9v)I_A(BDKn1;fxn6 zZ=q);4BVrPhYHgp@yLm!leovHjw12MT%nDB&Y~#XLXCA#HA5UXjJ>5CC)x5$+#||@ z4t7SZRUFWXb@$!#&Po&J`tkuXQ;$KPs=%L;A4E9bDtwD%O|c*fa$x}5Wo-}5T9N=k zZ!gP&aK}r9>yehFYvRn{#%0isL_-`P#;;>t@W*R~Q-aHWz^W-7P-RgEmu0s60uE*b z(Pg(t1=A5VC7Pj*izr_RO0uHr@Pl~AMYG_4-dT)2=sih~xd|VBIls6P7NVroeOp>R;t(#yCz4+ZtJvAyNat(uGKt`?;eq z&?p=x>=-mcnk;jT6J(!8pbUx7BRgeX};Sw>le<*ON|Q&Y4iTbLP@lRK<`StfKG zYMQ`snl$2tQtUq}60!H5fAfp|#s|Z%_6BYdA9_n6!Vr3|->1on{4UTWq$@KbUrZ@H(0SBo@4(vL=#ap;hz=N%TKJEy$CN%sSVWDSbi0D9M+G7H>D!2u$ z1X{rbb|#E6lrpCOjX+@#;Gi@F3P}?Esg2enF%@@^i;|1P5fmL9{iRM)-e8Oke~Qd3 z1xykULJ>j|LKgA^*11TJ#FRw=Eh?^GkZf3aLU=@+m_*@yl%M25VNQ4|Rg_?NaAG-+ zd!AAzZBwrsY17O-^OgP@(i#1o_m|U*j))G#j@z?Wh&!%}HlYWtHQY7JHNcZDm@e_D zt+)@EC&Cl@9gRi&CA|Xth@79CZ-gJj*)g=wdrufo->ZzDrx2%Lr{JgRyPsC;ZtUA2 zy(bs;mQ061ZD1`3&&kt=p6ITSuGshVKX0>~$}S=Ic7J4i^`3FhyTm@U+MS$%+Etp- z7-=ndB;DWv<3_k!xLvzsTvJ#>{-rSzNDG&SsDZ&jSVQA{0;TDvbH?{>DWWB0VYNfIBeR3F<2a+3YW{dm#~Nn;*#PITWsc=n;{Yh- zl6Vq-4g~<~!}-QZS$}4xpehPIL<@{FkN{+D!Wd#by;NY_A22ORs2NJzu|A zUp`+R>^_VNLzwOkU$9%HY?qG~U(Z^<{cjClsqL!Vn{KH*Rfqht?w0$40!q)F zQ`80)$*|~2!)kWowN%u1?arOv&HOM7_ce?V3JaEXCZ}G`j&U8IBjx4WKgyAcp4#hH zLqXRyxILQKqbmp97yM$^0adoqEqlaOz1R9eEf9w3C56RPsJ!{bV1xI$wv7{YDJggS zoSSn6yFAttb(EbUuKAPc5|m;XWpzv+m(;Xa!yP0K&*qaN+kn=Lg%Ro?c(1(@#M6_n zY1Z9USOyA1DxuyBQ0bS}r}hTH$Fy(!sdu?3w5{Hgn_*2((zG`Q~rZAL>{=3FKi~>Cy2z4cYYEb!g}Bg1X$4D zA$#9t`%U^W>7g;8V1a#vmh?l(#G(X;_0#u7qL0*CN@;pYQj z?!jGwxkGU9(`Qmd!n^tr@VjKtb$s6g z2!bFC{IEI@=6uZnuod7K9Ul{Y#XDex-$C&s?gi9tDnX+8LVTZgxO2s|dw+-h=_4Na z3*^)LC)7LSJFq)~JLof)HLkUPfYKTI8QB^88r+)WT6u?Qhpz8G;HblUjbu$|4PtGy z{A7_>ql2K*Y~bit}w1hbMQ6b&A!mSkA7(#2mqLxcVKJ0YoFH$^tdYE8*m%o8c-V` z8t@z78_*lxpk)r$3d{!F=Dp3kGUzghGDtRv*>`FXYG7*MYVQ&te8A}-=pgC9=%CoZ zFu^dvhdUrU?mI;FAS%+Q5iB?6Uhy6?3|}3Oe8QhcD_=Qo`1)RU|MMdKdpjt*R4_c# zcYW9$_Wv&*|Ec>C&m+U|)bYqGymhqlmg9!EZ)JDxZSnP%H}G{$_1uGf4bjGGJWCdn zSwEOLkGYrF#;D}@!tiBh;FB}CR(JAQt)I0)LUrHVqZPRpbZwHgetmVbx-ZX2x}j~x zh3*6gq@~u6j9)YdbS8S0-)jji_~x9s#)={iwi@vY)zc0O@|R~`uI<~(7wK&4Yin@~ z5+#Z)RXzva5FT^E%$N_RIs1&Lpj zq|;B=$*ns?$5FTfl0H!M)aiZsc5MgQ9Iew+_Oq!WT*oK%)?(O)wmE9~8!O30*|%MC znc$_s`N<=1XZq#{p3E>~c+oIX;c%%|wg5N=sN(T1r!-CJ&H562!gw zOR}u-0lf;b+JVt%{yPG~8jefi+9AA=ybC0fI)&G-1@)H&PswWGeb;HCJDjv+Q|Bu& z`^9tcbADRqKv9)~lphkyIr}3oxLkua;}i82TJzS4pv0NlHe*ZFpRk7WQVpOtS~L7pW5SHh+o4a`R3N=hY5@j_5HaAZ`N|$z;!1s zVLT)JTRd0^b2Iehh-oDi4$N;p$`IAM z=$9Yx87o*}c|Z9eZF=&gjO4w(axaIodohKYYAyI!=+3;vaDJJ-lxX0$>7zL|!d$1g zaTAkrpkUtTHLfd=@uTf@;`jD{qQ4M1L^~|o+5*2VdkK&!@-m`6ZL9_upxW$0 zit%z^IE+LgBKB_W;%#vd;@zG39ycc(-*YrV-{9{}F0O}4)ty)fPrsb#ggvWt<%v#z z1{^#R9fDEN#`+bkCf}ypflJvP1#>2u>(Bk_zR!lNdhDF4ui#FbavDIsZ zz)(!T z-F-?WB-xCqCpSV(wGW@-N0{a_dKykmu#YoNYv%6uPi7+ft*l?2^RYW={6BOi(8TtU z^Oi2;i#%RAZ4yMKS?^>sFD0d)(wayaxEC!NZGYf|tDd77^g8KVDsND!@39V+NJHA3 zebf*T5RmrvmX$TmZIx=npGddlnww2u%xAw<4On^kT;4T{PDGo2ip$k^gh!zObpW#| zf+n3#LG&|oSUo;DPqgslYG?=yf_qgKh$+a8ir|1tj1YtDq7#z9S>1$~cl& z=ToXx=28iM1eK%08h zW*}(@76Aiq-epV{H|xTeE%T9z`ZoFt?qU|1PPc2O!uxSV|E2MQQTE!46E56Kik%?w zpQ7J7dnwOOt{1k;U`p*PA2v6e4AKy>%;cm-r)E5HMA=seFmn%U;6P2=oY`cZ(d=H! zu^cY+w3HfdYRi!-&x<$n$4%nj_nmgWVJQt|nloEIYF|&&#!#Pfy6zV^9!f^bH0@Uw zR`w2kPBKg;Olx-^b?YfC^b~OyE$OmhSx_`sFoxWTPTWGGIv(%jb_%S_KbRJ<~wkafpOYhAdCn72rWV^kB(M8G_#Jx*~!}NVN zeD4EP6uOmgIu?56P`6po%A$}K?+RTnKkcM`8s7Y3ZZjht4D2kBN6)EJ>(Bpg$L zTk?d_LSEts*F+jLC1u!NG#D}J9^{ob(gw##-3!cQSwZ^(J>gti1iC0FG=?3njs}>P zvIZHSp-k(~W!sn1AsOcs8nJx6KI4ELQELTO?PkQsq)^{jl7-Kr_KI`k2qVO*qdBu< zE+XT`G#}%ScyB28jBGU;L|8zWpW$q#O6W`&gh^_ZS%k}lg`VP=sxAaL8m3YM$I)@4 zj?L_@N5sEx{krn}$}yjm#KOLSY$*}{wK&D;E(AvDTTv(jwjIdkODeX_FCp{5Wu`o)LP#L}KV?nQ+EjwKMrk1%3#&pranlupbc%bif!M*4qmb zgq7{8Bj3eQK%(l3wb+4cDAv?hFSqbpF6)mO7YjEbBIqnRAt+q9wZvG=wz`c26X6JQ z$xE^e8}7KFzvi6%8R=+&j6AZG#R}zpf$r(Zq0;s2Y&|yIo`zyBrZ~sI%E~K@x5HO} zw|CJmBkU4fE~%@lmu@wVRix2kYRBNCo!NXua>FA&_VK+>~}`oY1Tn6&zZ>gcoRiGQQXdciH9mo zi{*9~%OzzKD~GEU^~_C~pu7fjsd_Gb$*6B)qW@)hoi$3E)QAYAVrI^dt{~xENnI{V z4R}e8spdVIr0Bd%DrLJ34AR(u8M8sZpM!=5Aj~0L<>HEDltkbvOzhm3jH}{CJqnRX zT8icFS?~`3paZdd%>1gbMAB4b!H1V_)7&83O%QiL3v48NN!GWP>oqp&KDS|>ksS#i z##*}xACY?{Z!;Z;Y(5z!oT2ruFkI{25aT=2O-&hee3YTvPqC4bQL>ehlA$ldj#uF> zFpaGgQ{ZTy;VIA(Tr><%7g`u>@oz<0w2?VOKsVi7y0M*hYFa7bOCb>@O6;%FjQhbG zL$+=wYavh+oxCz==xO57dJ$7Bcgic7aiqG^PG-P%qW(yQXhHbewcx^EZ@=Ou3ju}y zb%hHI>AisOd-V>e3NQdnG_iTG@{a<+!FZ-BGpTSZi9_P~sx((rvZyqrnG~lJ$!U-0nuH6P}*ns`yb4xq(u4S+*t*`{7SFC%u(c!A+-+CQ$EFLL5zmeLIHOnO`p+ zUtf&LU7s#;FZrS98-_9}=qZ^D$a$fby@Ti@Pp*pjVD8z<5Kx^*${B`fqY z02fH;b`K}gyI&3M>E0~6Q|Q3HdOx&PXPOst;;RJCu*rS7v=x%W=7Jp0)b%i5Dgt2tK4b6;enUfJfw#38bpmB*>IPYr@I{bKsevxxWG( zf*Ks#2Vau`EjyeVMfYPFSNLz~suO5;De0?URwby3cEL{N)>h-cjC%Ohn7_E2_5PS$ ztf{(BFz>Y)o5ym0IW_MMC_8%S>wa7t1fRnJ8KdC0FtsWbg7jnqw?br1fAF5mY603{ z643b&@0#s2G}E?V;p)y6Gb+reVPcFqiFG_*;)r@?^~hIdEp`bzOmnGRk*W5d7?Z#{ z$b)*bKH8mnhlNmjFQDrnf?MxSdIOKUp^ zgi3JaE8|PD7|$qD-anC|*J-ydJ zHg_;g<^dXG^A?s>sy2$(Qmfs?^LF%AIJF=|UuqV874D4gjyPZMOLMFbg}>bHxF3miG`Hxtid`@oiQq`t4#*8wJ5TEPjR;%c z=-g@M4l5RlLN1YX*ij)Kp`UVcl~ZYOXev$MLS&x}Yc2^4RWvv~07`qs;Hq+MH0hJm zVrFSy6MT)J?rh=e;L?9;mjW@M8oRDbmdeAa4H$hS!CoiyUwZcwH6)q@C(D|#1m+_l zE(T{HCSbnf?%`EHZp}%tV{&XN_EItg;4+FmU8AU??=`bVxrSRzwAkr!h3S|S-6O= z&E~5QjPyj+WEp({H;1InbZ;hj825uPMXYn-d&-8mP2-5=*`{9MV?6KR@8Ej(88O!L z+vC~m(}eu#X8WgywH`yERx#a&jc&EMo1^sd?^d2BE?T!olMFWyVaMPYN8lgrf_8l- zFh7vwK~StBDCfbwN)RhR^0CMiQ+*jDi#jnU(|@;s5(m5^3T4wk^8;Y2F4EEsk%jr1v#`|qanozXi9aKranQLn7<@^LNlq1h45zV%d*9& zQr|z2u*z3u_R%Q2oj)Ro?EH)oOm>bj#d?33d3vy3HA5J?|E?aiXNDEtG5?IeVMMg{ zOC!C{;{!wo5wkvbHq0;sb2uc@U=RQV;l=cI_TU1`?8)-6t*M%fQ>VttCNB?Fb zqWF!I-HEr=J7c{>$ixxC(L37D{yp!5j8%WsG?)yb`04Hm?C~eDWte2+JzCv+P%pe| z%2!tcn+GY0Xj{FTXOmfy0n!jfwD%Te?YxRgnisdE`yy}k!@m#mcgSAdz9*YE)=(K{W@QfcjV{UY1~ep;_hSUAKu~wJ&wjn-rd4msP+6+nkQ{i|c$5q; z@l}2uedT9y{z~T5jYU15g-Ucg3B3Z9Wo;jBErh}obJ(HK=lW6pm{)-(zu0{*x_n)^ z7{c|}gjnoMWtMNsk+9jsylvzP<_Y5B_Sfx^hLWsL%Czkdy7#xSz4~AiU!3Aw3r3=M zqx!)oplSyZPVtJW43ILmsYbu66lFw(6ovalzHA$3nigzds2S64!j04%WkqFKJA7R= zfPmuU%g?GMYUeVGR#FRoeJ6?V=~*C5IGB$|_lEjOz#y?agL3=w!;?yqqNIXm3U+sb z%q$0!&w=A(OG}FKAT2NsmQea4daq zCX9SGc^%14WiE3~Sw)*p7-g1WaKMYH9f`IPo`GscV2La%+r<&8fLfKp8bv1s8aPa{ zm@TvPSpUaO&>T;49evo=ZFo25t(WO8k>j&`lcA+gt7KlZQcbqUa`@B?#R-hpWw}Rw zGtc2H=y<=PNLjH^vX&?YZpRrEXA=5T+d?KA0>CaVQ_GgXv&J-4=5-c+8Lv(rjeB?k z$;k-@x{?~;QHP)LYxO;F?Wjg;BmaRnPa6AnS}N>nkD5vuStm5c&nqCXLc9J#n7t8CpLpqhe2?Dq`j5Q_4wCs0k^ zJ^-g_l8_sOCPEE%*i&l^b|e_YM$hA}W7qHogV0{5v80@dIjypbVzxu((B4L)Cfovk zDC+sZ3NVMEnIrI?x!IaYV$7koEkYJ$^z!cob~Ik9LKC-tFK6W2y}vFVja z*SU(Pl@3+qC(fhK%sj|KYb}L_8hv#%#^=JfPQjn?$1k=~h zgrck2=*P;(3aNrThux`uy1Ur#!j~{R+=03r_y}^?y4~e^OdTt+8js4ci!q}$2=yqz zigm&@@tRWiLt5~Y{@e}SB?eu99nxnqnE|M6QH&_R&eV`!q#XFGCTLa2>Ydhi$GK7o zPM)<3%@l)`9v2I5Kl}BIOGNoWJbo~lRIgs~0tI@9uEh&(E+h!q$M5_R5oMS(dk^rA zhx|S@Kx}953VtBr^Bn*U1%|M;>P{v!)~N2A;Fl(GR41R8PzYQEgu87(d9C~R8nUSJ zQ|>tmp@DLNypLvFy;^uGj5*X!3_Q^opjtMiuOLX)MK*?TmK5W52BRWY_3zhQZFS5O zvs-F0G7!)BT#8X3%fn1jU^IfLA3No!9hv3=vHs5?}3ayNiCXN=||j^_}5kc_D`j zx1;C$mJHdqSK&bON%A8Bl%AHA{7hHnnmlC+r1MzC98M-}EUQ?ebc;zYgAbFMSu)BX zo*-&1o}tY=caTYSrOoDgWo0`~SR0t(_i(pt4%0%1WFwTM*c{doyRRheJcldg(@GAk zt*aP^;mR(v^GhEZzl{}7{fw`Q3PRA?f9VhpkWp|L8OJ?dVb)ZuKZ&YMF;82T-%fK` zWhPVWC&MXTIaVu<1KH`FvOO8vqW4qp89cgHcg;r@n`4`g?ODf}bPD-ZqOkZT)rjlc zG(+m0R7WqX!$=wO2`+WTn0Lj6vl!u-&-F<&e|&2|gzM7X0m~AAWmMmJkxH~1glHR?7T{z}elQC^o!%LE6!yn9O{ky}49R+okX8(P zZX|`W)cr7TW=UZ``BDC|$gFSt;vdcto=b(xxd*Z{TfEv9GI*r%vx73^?~2i66-Mq3 ze`H;!(I#ua|A=be#6z2~={`MaR3mkUZx6okL#(QzRP@a#dd7a#s1c=n;atPSIpm_r z(`We>KlN$*v_h75Lx3qzec7l{uA&Iquf0n~9FxV3*s9riFf9dtK=!sz1z=9{AzGEK zw3(uLck#BaZuN5MMn}#z*eldi&!;a>mQMglf3@TJ@IcxbwX|}78md&%yc&lSF_o+0 z6guMS`s6v~L)ZGIQ*M$;ko(U=LR2wO^5g9*jsUSka zHi!D1P|d|pMB?Y5r(2hMTyDh*)ccq zM%`IN_L{r3U8lr7fHjmH}Ih*~Rorjs^9OS;k}+($BG^eiv4S<2x#~jm8&y z3os&+HG+BaK%3s0s5awU34)HRImfw2@zuVFQ#XWz`=K9&g&PmYXUqQwYe1C09>bnY zR0+v}yyz|N0Y8U89_`F}8jn7F=()nP+*7&P?T5Idh0m6Dmev>gN*SdhN+mQ;3;50_Q4@PM{4js=HARwBd)-dq~^9u2-kv3S`4mZU?b+qNm+s^$f2)D`r?ME_h= zeW(ZJP4-Wkps6^3shBksYNG&4sYaFp(yS!1y}o>yHo!y-*`5N}o*CGl{)Ye*D4q6) zqV3JIORcb~@R@vVw@Cr;L7|qJyw&z%JAP@2Wf1ZXB0ZvIu7_UXORw)$!6H{yfU}J?e2jfaB6p~8N9qOnD3b}*ciytO?MTi za$#;P-`v9u7CK6?f~|x&T?OW+)6mQL_6Jl6KMgqtmbeS8VQ1kYV_}X3%YamZtysvK zsf77-WoKlCEbmjpy^ugkrMwo5w7Hz)A*Fm;ln0sP`hz8(?FO0Mr-gPtX%zG(%L3nd zn$Pa%o_J#W@o9+OvtwhOgX1uQZ-07w3ZZzXs&hN|nT^7ll*@Bq-|X?iG4A2q^!O}y zps=enSsE-@?CDallmMzZYcJ|@c}^mNH!O>;2Hnen+kL$7@G*{siyxTf*u~4+;ralr z;SnTxj+KoTq9q~eI zGBqV?M+$>nd)`~iHEAVHg;2?cod42Ah7)b%a>H*vxODjyXc6X-Li-@cg6*4XIaZq$ z_kdo0$$l{^YJ5qLn!E+YEvl=T;H(b$g4`T-yTTTQ!V-45!)Ceu23pAP>^0X`ITTWg z0IJa0gU-HIg0|bJmz^QFLdfY1+T?Ou@RRx*$&-(sGPy#@San_}Eu#TGJFL7!B_AM( z9>Pu`KV7pXc0ZKkTjjdDI`mxu*-1|0P96jpDJwR zO1VU!oy!%PN)-|<`^bV=c`^cos}sD#dv9(B5HMV&w2@=sI=Oa^tysyLsq~H868P=9 z#zMH0{Hy2!q>NU@8xyA62`rw)aYayAbwx0=D z^mrbsQ7?|Jo}5Ap*xB+5&KSqSHOwlGMXOi^1@g;87k7DAP)%jA##gRh-K4aP?^;%z z$?pbvsar*pq*Nx68DT9a>DA)T$Y56ikcc#a~(N`4tUZm6EIIUkMds#iUpE&loGZ0m~_4m9X;K9UKeSZZ2>vTD=}*2mJapJisZIE_XH? z7DAOc4@%u;WLinbx2JmPn{vhjl$D##x^EPzV2v)^>jqA_sHBG&{%SAcy6iJo}_*@qF>hm_h_sn8qy>gbBbGQN58@$ra8BYX^YKp$^k-G<=Z z9gXu}A)h3Fj&))~*c5I?2yN2G@~}UUm#M%nFJr(<`DEtY^^agdH6wHe!0Yc`fL~?k zVemGq*6Q)nu!GVj$Rtg}IyQrVE<)OfpJYbkDg%{wd%g=hF1^M1vbAEU{OuvBXSRv3>* zqdM>*b1@437Zra&OAMoRRmFc{RVLHeTP$3>oqU^x%eQpD#1uknXLhtM^_g$ov-`xhTEv z+4U)3XG_v%E7f*v2?Zy+2M^p4lgoVbA8fkQ+tOdXeut;IaBXvIhLG7}k!n*{k1NrM z`boe(o+W>QbpYn`II6lgxYs~5!895y2fGvMY8N)z(%;az#!Usf;ByXR#R2egCQ!1z zE8*3~R|<8>Bp$zf9$^IP8mcSyjKN*!*jOOzjvs=5pkzzg{7F$P=d|y5yR)&`~kAk~T zY^hc1O=>M;RKfb4!Dg^`%#>y#=_>8uk?(EW`@;x!+DVzd!_)2h zNKZYCr6&NZd`@x?)`0yjLhVcnuQ3Q63xn9oFbJ&-gV5tJz<6tMD`7KF3HwV**lALt zCX*6=r@*q{E6iPk$~tFb#j0GnkOMpj2cB|yVekZX=1$kxVyJ#2XUm)OWK@5XGR@LN zo_uamCVi-vkBJ6aoHWTF>)&wWGD_umiC7sSu*a@{jE_j-m3Jps}z zOh}mpIibG|=oNvGSwJkaAXxK@#66f3Gp%+bs~~n05akOBPZsP6AFVoPWBQQ{f5k+@kqczP1p!8bIY)0 zkA=hpTf_wEVS@B9QF_=pbVfPxj6^w|Mjw5O(^@bJ{;VXg0#sUqyHUh)=;4jw2IvPN zMybL}VJrMSJBsHjyr=9|&?*dqRTzZPBIH$Ww;8;3yBqjBe$l5rAlPRWnkh;7?wTDWT% z@F?N-Vd$gid5n&e1JF(9*nwgfT1~?)Ud}46$KN4vBgb9MlTE^Gq>w~PUgaIyAT!qX z)}{BNIx5Uy%(Xpr>3wDYhPOzVT?}3NVt>o}-UJgH8OT?Sf41MV;OT+%5>H?L$%6Im zUps7%cU9N-)))W;lti=yT5rJ4A+2k9t;1gea4yxXfR{@x0FLmvm2eJ%$4|&acsNB! z3cf!pKq$gQMXXq3tLhik1vy9=Lj@2OHRL$S!>E$2*oVJM<)->rEdJGjv@Gba4&DSv z7YP#xaSOyzIF+y&V52^8x3J=Nf5RKQ6c!a>+!a#^y5UN|4 zSa1sy3vNNEZb${(aRsh0!dXZo6sZx;L>S?uq!G&1_(MVg@0~xZ#Rg}AI&JVN$l7Ay z98L(mWjLuPh*ccf17a2b<8Ot*=U8nK-U>sMd&jqOD-8MD*1i95@1DQEyD7c*yL-WF z^Yf0*UBe*o`y82F!-ZYFUi|lW|KR9A_k(lyg4ZH=J=A}ATRJs$c(8bQYdW?0FqGbj z`Qzj-f%M?q=t*&I)YqgCY)v89nnJ`ug&-dV3fd-qWE2gLTH)9H^yrE+qy5bAsxzZE z9vKCD-fU!a1+!ayWc16Mt9!e$RTcJUG&%Hiq%=4ZgI(|EO-VE|nhWVFY{%dJ z?2nG*8GkA;-zlOt^|nZa9;oSzm`aa+@4miArrQmG3C(|PY@&Vo5bxo{x6$M@-#MIZ z#6wy^qze!65JamLWUhsx)ni!@@-Ya`7?d$)2NDlv<-t^;9)fHK-gA`5X}R91sW{5Jer(YaKttKSYnXP{1*C zRTg}%f@xd`td~)JPerKee$%XYSGPvDMK`M>n{~}PbNjEm9Fj=U{1$JlKzhQ=Aj2`p zsiWe9F!=j{7I;%O*j)D;R;PHE#dM6$t0Oi1ijnqTvuI6;`7P0m;eA2kcwn?bJJc=T zAM{Fc^WmYycwfS-prlG#8O^L~sp)NSgtKebj%34;(SxH^h1Q4(B<>`wkjni{{qdS? z#8jOfT|1TyeC{^w8M?2TGH*W2(Rq0KPN^54|J=|uS z*$_xL!;RHme@*)x7_UvqZpj|(%h?m0`S~ohLyb#(|9Dge}_?VWn z6`q7u8##}tl>?j%Or6uHYe~SgjvuQcjR**l5>Ns)%j~7pJMs++NsT`kPabA;HsxQ+G^;cng@nu~@0(8=FXM&1X7oQN7uc z*mUHM(L;CCyaAsYB9{7uQjX57Q!2G$F{g5#vpyHe7M))A3f9qjW7}%o+82^at%Xqc zo{>dEqzmO7cz@I%iQUvcV6E7b2tOm3E)WwiAtqo&Ou&km5ZPk_uEtOq*P@Pbfr3lT zSqk+F#Xs&M1kR)|%X#rU8lneFb1c}_lC`Kg)>5GBD~owiJ99C!==l}Pigl}Kjo2+? zypcLfZaV9Fkk{E?iRr+buy+BZ-JG{nIgK)jT(Y=u#>@H!B`+i+>J!QR*fe$=#qZIs zu7>F(ym;G?Gt_`J_`#2QVraUsd9ySb8sa7j&9DxwC=Aw?oCPx{%?kk;RzzW;5=iE} zPzSvz%&4FOs8(2FVu)iW3j8W+mS09{=2&T7jLxv|30H!cwzy;|6faikLfm)|S`;$b z8Yt}U_xHeQLR7GhCQv={0$-*6g&3~pqZMLZT?gdZg<(TffYd8j8BMG#SNn9UDhb}b z541x5Q4%LY_;)Z2qlqB#b@(tm^!e++qPzIM+o^aL12fC0+a1+HlvI(8(XIj$<=Pqx zMmUBX9F+58$QJ?j;c?VFg#{<15jEJfG0L%R*~UW9h$fHG_Gt1L^{k-=(6b?KU&`Nc zo5kA*=Hs_0Nr(OFS8rclf))Q0_0J%5d`pmv2L8WdMTaI(r&-;tc7h+QF?t6!ROl+S zwRsDP0#TUIM7hR-9y)4pa8reYLt8#S$rtv|!zN65%3lzjjb8>UFa<(M78)lsP>?{3 zdO?izrbX@#O`3so#4A#x_m*_1E$Xg0chj7A|yy+Ro}qfV_>uci(h7atZie|=@Vp|=3M`2zL|($uzn zJy27d9|^-UYWIEJj2)^h6gnIl>23^}&7sEb5eatJ=7V=1y!$Q%_eB1Y!oEUJ*uI&& z3*NIsotgj{15TZ3A1qHU^ZIMx>w7?R5Rv%!UeTNpHEBl+KcV2*^3r#0=GeOm6M3Y{ ziNSWb{tK*d04tABd~+rvJYwZmW}jDfom+OO3iX%!IP?}uBA3!8)KT$h%hS_(AK#;? ztA^PVUl>jv?4j@2-b6xKlyL7>qlX>A^y&vru1%M!hYG z7NTVsb8x`|1tGvBP(=X%GJLFu$cbzS84r5q;WS#x%`U8kwJB1`-mEB#<3G$ndRqVK@R| zEbm`c@AV-c1cuq&nQ8yl>#BP7s{X6~`m0`5y}vHZW_boWXq6w0%KT_l=9i;cMLh55 z*W(FB`ZrIJ@vTSj(+?a|qE4nS9TaktI~WX39st;!(X^%$PE5?_Vd z!s9L5?jDhuJ?PE3qA;7aws&&N;zP5HPaGCs{>9X-Lv;uCGOMNY)4nTjuy+F&gS3Xs zgx6g8b;O1ye?4+^t@F^;Bv4IT1#Vb`UvqIv82=iDUjsUlwHpH62D;ur`}K5yqkREL z*y^KeeY6(^2fTExm*#OQPuKFaUq>JH(q6Q|L}9RudwCcyL+rJP0-S6w+8KmmNM2r4 zz8W6%2BKb-Gpd>pH?Rw5w;sl`UQOgbjpn|HbPl8AU0vrXFRkZrMJlMMPzKrQ-UQ7} zFwU~VVUZ>8#Z0ztVV^&HH_foj#znltzdGR1N;aOAaJ1ayu~pTW6p{@I`&&k7^15u* z1{wPyi9)HC{qrZ#j(&+;t7P}9jS3d@o`H+vvQw>QeuPdDWaKI&m(DA1a1Vf7M(9=| zm$56Kk8A}d^q`l@4Lx)>`mGDm^-b`DEy#>Os%--MkKaN(KfFFLa1nS;Boiic78H^PU6_@`B8gND zMs<3R9#th#lX@b!qTNB7#g!PLlKNm*kYPYoC8QdV*tgJD$EI6RP2hhXIZT%4NeiN5 z{j!py8m=M5k|6hhgxh$X)z|{no<@h7eTHEkXEn}-YL6dc8@~bD2L7qr2ae}=80O22 z!U%27W0W&5GV}|K!sK<@tB|B+W?fOv%p(fL0MceVq8arLD?b0%S1?@x=O?^(<(F4Ez z4%!oSC{0o2PRSItQ+P=-0L`L_Gzio%&^GSUOv0-V#6d4h3{tnL9-Ro)Q8?DageMNH zW*O6dr?j=r$r~8yJqkU$@j1D^wz}G9R&X@U{#t7A@$OoKbmP3sUUvWF~o zEyv1rn#&>P1(S+HRvpa=4>Mn4BAknAhH>|Q!hN#L(uY2bO`$bW7wChLDqY#RH67u?z93G7S`&1gLB z^>tgCtK}M_wts%>@ZnzP+ouEl^~SbfaG;j?2en$IX{l?l1_v5LBhA(tSEE~Fv>0mK zCUdpDs%uyHKDCv%`u(+j;8hZM^+BnLs;9cCLzq`ePs?ZN{pfWB{jP|CGI^BR;2GbF zV_BPPKg;FC$?x7?;YcaW6qL&sJd-5(xc0M2$SF>B_ioxiwz+R2+8`6&z*xvcH}gTX zgV)^_YPP$4daIU`>YZk@Q_r;>Y>(XB<9xTq(^gwI8V-y#)UTN11%Sx3pnKCfo9h=y>qiL{SC6mDjszYPc zDDB?I9+}GRtd*(^2Bp@hRhf-SiPv^p?6zB~Y7_>v5_%k{x=U(+R^Ca@FnmJc==>~w z09@5(`k{#4;8}Jk*uWXQaU79VqVLd2^eA zL5c5jcnwyaG#uNq{gz%QKX~gv>vTY-!xELgGcXpYh0%k$x~;x88e)D(Bsdsu4NoPu zjOM1A>g(wcCzr5b^SF&Yp%C9OR8u?J>22yn?R^YrPC6$s{}@2|+WL&@L99 z;~O4L@+)@5siZw$IEoat@6z)-3;S}dmNF$M?#g9SPSeH-W5f2=`hhmDQYqK^np(U0 zQ>Xkp?id~gNB13)k>Q$-S`#CooDToiMypDvHaXo6ty;l7dTMlKvN14vq{}e6(-!Ec zM!kD2^ELWushjGe_ThO(qmj~DFVF`fhQ@lI{IQml@3i%FW7?h5rt$dn5?SK;v$l1z zG|=24e=OOOYV@5>Hd^72YN-nzXg^N|x>EBt)^*ilH~5oXz1U4E6=F9X0ly!%clPI?4AOR#THbG~VXasFgOo(auO2OMQr>?QIH=x4WdA{X;F?hAO|gr9)p;yK#GUH6wBPtu?&P&f_y`nXfU&p+^$z^4Wk9 zNzhHHK%I4z+Yqp6&o-_2mTfsMCoZAtEj`{4ax`hrCJTAQbyKCi5cEFvH(ocz9GB@` zW)pNc-90tq4IJrxq+%}=y1AwArZy|{!-BTX07#ulfJ)A(9KUSFuiTf~{MOXX6Afy<{GtE(FtJ=KK&)X#zcC2N`XKdIC&W30rr zOv?n3Yhv;9&AcjBSY(PIY4#^zw@0ZImULGQ`oec;ivjglZfX=Pw^^w@JGjE1?OoZ@ z)zFq{$Q7hsT%!|ywHN*)(c1<=?t?46{Mlsr1DjUi+&~L9#}O;Z5)f-)7spS4FcTuM zqmv&BNOew=#if(Ab^5j!)WlI!V{1Fo8r^GmwTHv@zR6be^;KnaoUyCn-}d%Uci3I; zP}Odo?h)F>-N3D&aOP*Nq1Z08lv{hozfx=CGa`e?suMnH{eE* zdekEBMglbfsa-uaV*!x~jvB9{?Y8#*>DIE=5Je=Nyow}D477w1=(9fz)R>^x7S1m> z(+{EZSHVkBC>@XKR%+f$yv>sH%XN;&!0F;@-YdG7zk8`k7xUqUsqFYvChGG~tWNGo zMe82X)ol$mZVi~x@8n+g^`XpkvwvcFY$$zGuwmzNG%(g#?d}*0HjZ{w9YNS8>0dJ+ z1ZL^=@mt4$R>MNt%O*dE; zQjo>fzVg74t3!`lhPr_O6^px`&WEAOLfMQE3WAH z**ZCpO5<0qCyV#r#5xY5aO49D^f$mg=Hx1!#h`=!$XeRA?2Yv`Cau>h1Ap$D2D?_q zNja50P_^-w43P6mK121=Gw1mrA5=Ro(0e0Ql)CZ6 z4_bfP%CvSp>geU_R+J|`Z}@uyW3WETpge(> zP=LKKaMni!8Iyxt@PVp_uf1ZT3kHOe@^_BHxB)nFsdnICC*L3PsHH3?lPIeKU3Jad z8n;CU8u;Fu+N#_AP8A1tIH|QZ>}dn%ZO1?(yVkV5*{;&*)HaL8q~`QSozL&~+H8SH zr+-V6RiV}>;f_JgY4n-~XLXI;T8F={Py_sWocl1%RE@`vq^>vVRGzf|v_o~; zlx}*TjI`2=c>CA-&%X36JW8^7(kB1uq{9?3sZJ+NX<5_zgh3L16)o;X#SZdj|FO)+ zM8hSNMn5i9TC2Uf+h!(JDwTSdR2V%x3^CQie512oDv@#wtGC)!a;fCd5xO4vk8k1R zoJ0bbw_xw`f3>#hBq}4;=`S(p>ro-$9DhX(875UBdupgrunieohCfpnBn@CU11u3a z)>Fu!=FgN2j|N09LwdbwSZmoVEOXpZW3*|wmUw&rO|4cbcpPSjUfSL39d8hgZDBan zhK(mZAtg;#y7856DcDH zwHJm8V%mW}l?x}Nfxzcule(1c{$}6nq3-rSgwbJU&SQNU9 zBFi2X9Eb8ldWnrwmgKS*CiaBG?7O?!;t{Uhg(F<)Nw%rcR;8mQyIzvf+G?lWZBR%~ zGH++3hAO+gT1Rt?PNR~@HOjM$RcBU97@1nNvC7bo$dsT^Dw7`dz7+duywgBMsi}bY z)q<5Lr3x0EP4db`;*>tAU?cdf5$7uxi*s#cVLyG7xR8J2amhtt-TuZ|RJMetcXAJw zm%WC|p5IjV{B_FS8SL$8Z0hZ4+BnbEbvHG1_dwa}6hkYo{EB{wI|5EZ167CDws3W> zUHVbr^2=z;{!?6C1jms}|MIh?t_)jW*q>1D+|bX+&{y!>Mwx+@TWZ{{8jD=3a0EP_ z2D?IGZ}4~m4h6j`uJUA`RvXov6q?y@d%aCAmCDuR^)@?HDn~Qo?4>K0=*K0uVhnqT z7iMMVDW0-0y-%t1jeu{00%Y~;q7U{IN{qPBnbS^`Uh1u|9oIwpluYNcSY3K4ZIGI3 z-7cR=rchXGtK9WAg~C?vuBx>v=uUJ}9}5@eidwJaIF(NQ&wQ2Nu2R|kRaF6pQt1eQ zUN>&MLg%O-z?i-Y+c=fYP3fOU`^wI#A`pgF?z||x43h~T6zMhed9 zO-V~0RM~5tj#``QPdpu=X2-Kg@zC@+ZMx6JqcIE*b)LTRI-P<#Ra65xL0k$W_ovWj zvJ~tDH2_-D^aX@~BEq7#2txfON|Fk1+Y&;5%VXit2>hWI`ryXL>7R2CQ#Di@+2_Zi z7mF-f^vUYg8hV1YcvKHk18DgG`D)t}C~hCXZ~I}GZ8q8pAD{}cc-I@TM)d!__3*8? zaJ06{VRV|*Y}a&;+tWMUPJ>al+T41EoBis>!53cKxcO^pgNg%#!!3N{+b^%Iy!4%K zE=r_QR;tt^4DSLAe+(FUsUgxfjO0z7QCP!I;%7^cj(BGBN9gMwprW;IkULRI96W%_BR$d z7_f3GgZgjjkH7c={qe6V^h&^1D!I5Z1^7O2MMwXQTxlV?6T1AZFIDbb$by8yeVF?MZ)n;!ZtPgr!+ zXBi`9q&SG^oJx!yAg3qzL4rN}JD69^(ofrh+k=eC2;#2R(2^a|@d*j5tFl3FBxf48 zhwSvLht56zDL@9;2RQx(z3=Sf^ns5mv`UTzFMH;bXP&%rg?jLc1T^wBM#>(i4nzDd zi2oSJ@1+idy?B^iVE&9-EwyN_`Z4_&ShS06*rK^2IF2mZ#kMkwCXUWFrC2S@{ZhTn zXte93Hl^8XvwO`7dgC2sX)X2a+eMQ`e_af1w3eso^=O^OZOjgOPMTq45+lg`{g841 zQV!u16G{O|+_v#JJ;!`Ylmya~<5*AbLwZ7g@*L>N1JuBJQBj^i3HE_?(&wQWIICdw z$`$3d{=O|e5?W=q=qx$~Q`1>vFxGZ>X}QK`FgnyM^Xa2+yzTzq9z(Cy83`xZcKogP z4-Y@^)_VsS4ir?b0_+X|c84%_9ipBb$9h8l71opc&Vil~^pK)pyZi)7A^6o5HKn7w z3&s=mrJ8ccSkvia6*{XH{A9Mjx3`~R)OM4>s+H2d&RWxdAHM&s$F~7y40`K-*KZzp z+Z#vG(g}v;n1OrW`T(V;7O0ygw@4-_8Kt9aln1=cFpP@_s4;4a+E3j|EmCP}jk=e{ z-;;@?b}uDol0A3cv*n&ZCLhf6x6Rki%f~0w6I5haGOTauFn1*H$9MDm;Y-^M_XGh-P+O7N`D#M+&F};ucO>2QHXsEE-et-c(J{`_4^Q^ zPeWiID(!B#(9g7mJ1>tz=u}Hf2g3`QHe?WZ4L$aqj+TxP1fZq6nTyQx+&@Bpc8)@} z;AUo>d71ku#Gl6TcQfB$2DzU={BuJ5>+JWDeUpD4$0f|ym}%~Z5dSiT{3D?}%16~v z!+0c8yIxa;KI?UXdGMUEFx3X*i%Zx^R@bg4H)XJkIq-P@`X7vBY%;c;d6uJ9)pomE zFO_WH_|X{eN=9J-C#u?@WM&qgWqzgAD;OH=`MI;GPh{l=4fC>0Az>Ja zQu=2bUq`1>6Y8O5Ch_E>p82Y2b zm!4n0B%B_hgUmkrdJ?ktD%X?ff%9SWdNN#m6Ndh-^QE^=@jcizKdQ^TjAk?BzTVy4 zeTO2xy1`qzs<${lrBt#&8m*G*v{FrNPyg2HLw`Eg*}Hh+K=ahLM!l4qQ5n?|o7>j7 zW3hi|alBTmz2Nid%ql6TG-@`!YOouvI(cv6(OZtZ_l_-ktE09KxFYBP!;^mx7Z!>_sm6`0!_mDRL9WKTL>mJol-G=dKc3hwjJW+!_y?5XVs-_bZ=zPeg zVygovRGl*vX1>WM$S$?%FSTJLJFk_qY%E)tU@MGaYfPn+Ze-wwzr8`GxWA8vr6!kF z_K!4l6*|3DtDs+^VX$hqpgDWtGLDH{{%aP^ z#Ym(Q2!FAV{({p2MHnvs6QeOYby7}k&;TcdRtK%DPxP+ogXirAsc{`OqqFLq_qg($T=2tOTyG)fAT-;Y&zNH)dHFi z%OJsE2kGh2is9lvHNww@F9#2{fQNkLY1yAJEwW!yEG0+7$#8p1E99JjRu9ueS;t3i4|`kDx`gBaeu3Eo!* z-z|cCR{nAk6#dtMk0MaE2=H7HR6YUjyC%G%UM+#=HiPD4*8}Z-9a99oL@zO*f1f8{ zy!UD_nayg;9;?OrGrP;ael<9rxe+*TCg9rc#v2l<8mfB>@R|so;|O@kRnU7CJWvGh zy}t5)U${2ZWH7uzp!U@w)awQK2m$}H|JY^-aDneO{Hl?;3Vzh|(6u1Aw|QR)eC^s0 ziiKVYN5fxi8E(1I`d`}YZMS3iP9 z;}jO3nXkn$7VojtO3nyjma3KfHz6!3$-_~0$^Q_-(vox;byV_(5SCMo+;JhSpm^Ea zgs_tN^Fn?VwNLgDA*`kvWxp1}8m&|=;;E&Q)_}O@iOh!ECNUHr!Y6I!AGil^GB7OIu%fzU20LCsNFDoy3!Z-L4~+7N`YR0dyT zkdlBv^9{$ph=NLRI*1;EWZlT&CR9}d7Q$bvR3`)pAULNX=K|QF9 zIqD7}?+%D9K`Oet3i#y;^vpnd0@IMZE2MFO780T+faH3>{fqCiIs{+8hhf+3+NhU?%3Stp=GRB8Hw)n;#wm+=f#^-Zby46HDK(F2B6X%PMROR_6s{$Qxc)hz{t&hkWN zqBgXEaY1@hUTPBKfG{ZL5X-xkj^?nQXE5!A3*vJ~ss}OWiKRJ)IZZf|FGyR=E!u@S z!FHoo9>hFA`7(l5l-lVG=4(o@52Td15RVBqIgTw#0#it^o5gsDQr;}Fyzm6kpX^mB z3k4d2Z^{yoUSS^dG%xf%qOBXSb8hIl7Qp%*D?_lJXHk!dUS$M-ht!b9es>JhDQZfLD)#c5?b*MAED9WoVcHSj5`wLt7uxEa z;A^KauBClu0(;{*teXOk*Ox7X_mubZAnk?{9?h4G5K7y_n=X$kj%vi**S2oKP1a51 z{pP2WcvO(sM7M}n(FZHGnqp5R$}mWYqcb!L6XWp`4awl~R}$;nQb`X>u%5-Z#D$(} zwIItNQ@D>1dzsSlnb=QU z%chh+mBY4z$ZA%g``UeO>^c(77O2l*EluIlNh`W~^1GtV6C~XhUIgQKS1*sG8J0j#mPYglIUWlXc7`B3>zDanBj3J^zpGYvB zfpoq=VL4V&T%(BDE{IJ-`QyUfK`1+l%R~61`bTkivViBPfaxIS5h@v#IRqF@;228X z3%|P|-yU55Af}mMJBeu+h5Lk-5sU$_&fp$m3Z5Lsl$X=@s;2* zT~b;(@Al*S#ax8GLE$=tIX8u4L>falHiKn_?gj-}PGdSY*Sa5T;t0+)h$-Gv(2!BA zy##wvJ4qc=C76)O{}2hA9gnYR%34>Hu9pWxeCT6_3iiMeb#mtM&8L+NZLosH!a=~RdxOeXp1#Ntvu z$4|#|@$Av~d`L4MpUuYC_^C`hHFGQz=O<#v(yMtsnO;oH@pI|Su`GImMkb^L&z|^7HZB zTsDzG~k;p?GbGaa&TaE!= z=VBQMqeqrkllepjicGC8$Fq<#7tiAYa(pJ625BH{q4Z=jy~ZyAbNR$_CN`Jn6DdBA zcnz4rBS2#c>Pj#0vx!AqFsUdXzcUXHC+>)cc!5lRj$e+Yj`4G=AX0)iVqXfFoQ(l3 z*+dR;G#*>#S2L&@C~*-|a*4Yjdp->W9YrL?co4ucsS7F3+)^wHh{dy^>GN;&BufMd?J?t1ttQ`WD{_A4zk4I zHwI#iXO|QCJQO#33{xelBd`keCY==n3#f+R)wF{i%&*SngGe2Z!m~m2j93B4XKg7l zw^V}J8dRD{%_Ubs_lwx4Q=lnUy6<|FS%j(A* z2qd5uXcNekWKqkRPp_qt>DYWZ_hN)qpjki;hz|u;^BJ%L^KnEb%Ci(tX39AVb_sNs zWJdx?;C8Z@GKx zh?QlAQlr_8gpgO6TM)TMj5;#oNgQ=qafim6atu71V$m5uz}NY!L~;JXA>C^|?GKQvyU(=y`6$0My>bNBBKcqci&lr$_kc9)9=q)V}EO$T068+yilckl!Dj z8K2ra!$Xeg!O59}{M0BvIC+rY8J!#s@*@X!Pmk=`!%t1~(OtVIq9c$NogA9jI~<)H z|}IubQ)?I*)=jb16}SUr12yBAj0n%ADo!LRSoV1 z?58n)LsPpCPDjVaXZZ1{iQy4Q+&%)B4Q`(pAyolUL;t6}GXbkA; zNy9#kCo0WKsWgxw%_V7&P)SZlQiPBOq(b5vDqXWEB~i%CHDwk>WC-DV_dX4W>)z}C zJl}o(-}gM$lJ=T^YrXG!-@VV;2N!Fn@j6^P>+#lO?TJ=y(2ECAt%q)sqdk!Y<5|NW zTQ4U!S9}__ZmwP)kko-$dU*9Uo#feor_+;oS67u2N%B%F8+hH z{dI8h?;TvQh@W4F7ymyGFGSM&b%62f0OQvI#;*g6@2&Z-LyT_^F?v_$*FnawgN$DX z8UObVGFZLefq?K;0ZBuC6i7Ya8bDAj_?<_5`{3^ag(A!0pil>`{@MBx5_o-*&(GGE zk|OGN{A_(`X}mr;{b%dT$l&!U+@Gy4D+~20-F0JzeCD;v0qfvXI-jsn*i;JN}_Ex`30xVq3Rlnh*wz%>N8 zh65K5xNLyS4Y+)PD+IVAfNKG8EeDRZ-*S-$i*XGCu3^As2wdZU%LllEfol$MEds8U zz_lK@b^zBw;5r3dSAgpwaJ@$t0!s!eg=V5Ez@-gbCctF}T<*X%4Y;C#D;c;}09Ouh z6#!QmaMb|UnQyr$55%|z1D6(XnF5zPaQOjO1aK_?uBE`03tU@(s}i_Q0M|v}Y5}ee zbRIA*1f~pFpG;uN0;ct-4RE;w*EHao0bC1!YZ-8@2d*u^^&4RS8^mz;zb5E(6z7;Od4wNd1es?F532sdjt@1!YhLqR_!$ zpbS=RZ3%?@d3<_q|VTA{%M5D!LW%&zY(mQ&3DsRA>Fi@015g;NGQKBc%7$}Xk|8xcf zqFYhzeRot;$+zxL1q1{H1OZ7cSz>2sB&g&h86-(;a;C{iNeT!`MxvtRBp^tRl55P>Z>ZK-CgI@NfHU@Gvg(ix=Y2aY$1IN z=D4=+Deod!Chnz!FhNf*FltU0ZyWby;D*E{t*sHkj2Pg?f+v1w)>ZE zUWDS^ZB`G)Kf_&S#2g+mU<&7aJY)g*-#6ELA5M@ zn^D|2E*6LXJ!%IJ4bv*+b%R0$mh>C<3RDXQZ!j3&GwLH@ISIzdzfxA@=fuwOnk2YR zh=#nHt1BNE7%2C3A>m@7h4vXfWx)3*jlmOpX@|KXZSpZa&FKp8UU2;Sl)b=KeDl5~ zse?ZJSND#2J=4;HqEjSPE?y5R{Dx+J1ykftCpBZ(kt7Rz>^8PX6vw_?!dkNinhf{p zbH|dXk~&nN#&}2L%?tew#E1m_GHn4SGUhrgrWUr~Hu4CDn3g&o(F;LrWRYU+KD7o6oG*-OI4K^(0|)h9 zaYm;iv~#7U3Q9hzhfaP#PH-aI!ZY^r_b#K^G&^-Vb#O?|g!><0{2CV{(lOoais5?U zmN(j(D0H<`cIX00(UJ7Qk#cuON+pksq@NTTc}!sI5ru^wgX<7b_U6XGB`B z(KyAjgngRl-90^!QB=n5$2E7z>nMtJJAAB5u-c-t)H2lA`q=y0>^tl~hIEoPkp{e| zOJlTzNXcA_eo2gwO%#zp$%ZNT5ib)>r(!-?U%_(nNQuPKkG%A78JyLeH8UNc#9se; z>uAoZF-^oQxz=B5%8{Ea$jLgt1$$$*mL#U^QJpwd_>-)!jb}E(+?U+YdwX}74xA2B zX_F^(?@7`*!`{=iUcv3YqWP7l^FJb@{#HilLFVb%YgJEy1nKd(!5wEOP0e?FmVR}0 z62ME{yRy_Tj7%fmmT417Pw!B5&}Tj(qni87-cwVYTPjLtH<*3NkJNUtl+<4D=1~^c@nd{e8&Or2K!yu{{H^@);b9Z%3y0oF|y_~Q($uv>WE z8>)m#xb0WEVC=l>3LEMh3&*7;7exueKw(U#C)++K8zJJkdC_UL3ZCy|p6IS67x7u{&QtZ1o;Ga8CRL!R}0 z8Dsg_)%PVdgfOt@<|KRXmvXF+VkZuS2Aq+USG^*6>ugZHJ#DC11y{n5%*6TFy@wZ5 zBZBpSE?(%fQ2hB!J!uyr8MV_V`8HbvVAaY5&g>rDHF7$tl&@42#8eHA zABvk)aHKY5!LW`<6Y~z>ckQ;-l;{jGT+WrAe@?4*X^+{rwi@9cvmE(I-JXU}>?IdoEBvkLyxT zQzSEUm!EthkxI96+C)6Z?N>_JyIOL^qr`JVvTpBXJ~w8&JMNi>hgArbwBXVE$@yX> z-a&YKwqMFdp3jhEwVtDu{haTh@^Tg34eV8q>+GzOUbLcJR$lCx%V~uQhcl&foNtDT zu0E(FUzlxE-pfU_Ig`R=N4OO87WYhhXI8MeBXF$g9+Z3yuX?s4SC;kYHtBi;dLeTO zg``IDKTJHeOI00=XeHt5!sSW$rlPD01F;)6OJf(M%x*6XI7gF2~kMR!DK=Vuoq2qGRvWpbJmWK2S^acC}xcYYn96 zN&ldtjvxxV#T2GbZAFqI`+$T!jX)-gQ;JG5%$>}5dR6_jZf2HyAzQJjxnE+ngojZ@ zQX?_t5g}PiCbi?lhV4`RVlB_O{Y-l!*Ic_)vUz`Xh16%w36S)Z0Z9GsZ;J${v-gQ)pe0f4%!0b#1Q+g2x zy`>BX=e+{y1FuOMI`+-HzuXitCA98VROK&VQyx_mBJH(2J05g>$jWQ~@RQ|oF=1|W z82Qyp{M+l2tCV~nbFx_q%;$*cqi~{Z=lgn_j{QDV2;_#uls??9ao$M0H##h@8GP*& zET+1}8ml&4VXyQ`B8MOxb$GLxPI6K`wcJdg5nIzE|<8Rth^ zzmNCKjM!sasnu>~bkt&*8aqlIERtFp!KYdno%i>Bc{J%SKSdkBwIgWHY}630cVZ%I>T;@;dH#tZ3e znKVo?xh@eB8@Cls&%7wm$I|XKX%T|;jzNmS0Y<>f>lr%g|9Edzr7*V4yz)k@+vL>B z=$$gA76!Du$U;F8eB6<1P9tyxXBnTbz=F5S{UiI49`)=Q7+Q{{`&4z?49O}&k}0`GWAn9FXM5@dX_3R0>;0(-o7 z#&;*5{5byRG?<8pJ2*x678Ajt_3a3MCbHLrDpjZ@KX>2q@p-(>;js##z%)R{KP36Mv zm|FQu)!&kK)}&uW#9v2NHj&>79`hEm7Us6{M(A->m8z*P(cxDH8V>Y4KH=& zCH)NhfYixhso710VsJ1)(%Hkzb)nuEwVTPKuV^rOL<@C;AHOJ#q4rX7g1H;yOD5**)DCz4E5_k80n>L~BI-U`)usZOdvo2oID znmN|fHBp!6;uu;bbadL`v(ebdGR#n>8?Ak)=?)(DJ8Zm{$0Bbn;i%a!qu+VohEU7j zGgM}}KTcSilM;3ipxqQf?hk|anLL}clm5(qj3?5`ytv&|yfs6(FSp}dfm|c@o>Q+H zC->5_V!Yp+oX=wV9>iys;!H)AZ(TBYDMhYgFZDkFJlofkGjd zns6SfxQ|*rY&NVJ{y@MD3E6X|cc|rV&wW~T{GhPALRnwtlC^bPwndBOGdlNtO`eLX zX&uz{QDncFyG2)J0K51^Os~y;2PAbh(Zy?Hj?t#5n^V<|D>krvl|CGrzSTGI+EDzE z?US50iQ0lnmbb--eehDJmAF%O)bqUgj%%9LITd+YvNv-v#~WM7B>`6$-vxfY$1x17 zwfK~L4Nu>lNKZ}GeIuEVY}AGc7_M3*>R~qSKusBKz{tgTS>xnFpBtfMqjDi$6-y+v zgiY5}cm?1BY#?qw3qf9Mag3#l_C@PY5akYB*en$#wSlqTCqfs|KH`Rmvns7c@4|Ke z#v=B$NsRWJM6%bs2M-5NF0*d4W>l;TIcwFaiYzz!_g&$xm;vW?ID8||cig%^Dq=ab z(i$H?cy=k!rTkSEmoNA3!|?Fou-><0rWpnAKkvqanG@Hr<@=(N1??u3wBM|`8aGND zZFkHsp*!c0k)8A< z?!b|pHOJzt6S-XTGuidD68C2B zyTcq6uxL`k_d19qR#khw z8cO}#cweIQvI=lLU-vJ9f&!Z<&EN% z3*SYOUqrR)-1E*3j4OK{(CTC{W$2n0e{5@E*E96UTSw5p53l0#_2(SyqpQ&71wtJ) z9xMk66gDftu|k+tnB!P%at+TKM^nv!U%Y+&O_x9vmPE&eqH7Mg$7i!il*vzWGlr8Z z=>v+RyL8lUhgh1}SM2UjY}+Ks#DeK^dxGNy5#fv#+n)-svx$~<=-#1V(2dVbN^dyS zhXl`@QU*_?B-%WNI3|?E`<-Y5y{jtAtl;0KPYYBz%^UEWk7$#&K1?{}xgHobc5wK_ zHIL?-eUoX)%`Qd+GLp!b%(lxDwQp!^ksLX%afL)kI+mjfI8 zz{iXattY|tPm#Kijrac2Z?oR{dP*;}xQ@3;(2reTbARXPp0Ig$IHG9qGC^`Pa=#@( zu(3{U%@mD3Ja`vyFFxM~3sJ}7%DI!-`DiJKFg_p)Fh>Q7FrHJ>{&7PH7Y`Qkc)UCo<$aV_9gd+HtUTiB8)Hr!PTR!U}SLRKk>Ph4Nt zYIwx_J-hmGN=HQ2ENs1NUv>R7wPpE*$X~$og5&gz~9G>D;>*#d(OjIbhvTg;Ry}76? z@@5!uB^))bYp6ELk{*%^OVh2asB_?vNiSJx3Jr57eTg2nXGz{ve_ImJJ- zB9UN!JId*@NB;0q*uq*u?)l4_j!Mlmy-kP znQUB|%3jSqlza?4)w4z?k{ofFvxZVTi%Cmmg-feJzWM>E$Mq|}U2)vzJI$hu&tgeW zc%@hAa+1al=D{edM)#UfO|HDmMQq%KH3zfMR$--l zrM=uH=E-D!tDHuUSMkJQVE|z<_Ma@aj1{u%k zSeC^ljV|0CuX8Rrtv%Vj-AykPL$GE6l1Q;tdGM5VQ!S0l;3QY~BtrSR_jKSOtr+mY zJze;l@=13!w;<=cf(^Lw4EELstQpo~GrN=D;Z$P#U+9})aGy*cOe z;wu!mXrtxPh~KMNlpAcC0g_!!BpDkPbU~fU?(T-S1c~>Bq313a)OPAI7l8+HD%)uL z_U~2DsUJ#QHPFxRm)_E>BqiJB)_Hz~y8Lk>qXUnrT(w+ReK1}^P8_kYAZ>fz*vOTz z;2Ik0?MO%Coy~&VC76II0)C-^Ql?>ECWOTo`^(PjUlR~g6y%9f{GC?O?(i7zP!~NZ zU|0N?g2or&?MIOdCh-$CghEy(B*__*w<>J4)ILp<+uV2(LGxaoGeW4)@p4qrYtN=; zDz~pe@0(2}4*0M$p$%`+X6FR7kJH>#(;HfDPrQlGex@r8QG^6Vwg)<_fgHbb6lUl`+w=}(8doEQ!!Eq*6D5{V;`@3fTJNCD?L>@5ROcCdaMRfS6zE>r?xs&YgdG= zc}+7pe8A0)RkVg{UuW}u!q=8TgC|+7iDk^PCK5{?kIiAwX`{w~aLC@a>a(vr)|1+? zv3xskX7~palbf=xOjgd{4r)ICkT>U!fUmgd+Yub*lbT!UE;_* zzMpzt4vj~{D8x!Oq#`EXC9n#SP-Eyl;gpxW)$A9k!vyT>8anwvJN#UjXhoR%mUL!A zi2QR>SgSzjLEROFo%9g_DcMRDI%VJh>@SbaZs!y4X+klw=*(rc>+nWoeP72P=eX zYPrJ2r@f6Unfh53w9(v^i0M!f3oTeT~9R_~j>5rps`YW-d+ye{<%?I}VBfIMlw)3}9#el+ zp_|M1zHh%N@Yw%?G-m^|5!P3NVS7ooLO9*78WwS)-Ru8ayjQO-8f9E=BRh9k7!8T zuTnB=A3RyZm-awJn-I0Wj5Nr8**SUx_}(V^IMy}VdqE~8f8A;IE>TqCb@H~X3x^ie z>2e$L^EJ+8?JQ=MD*{z4y1|8LQMef7e1@SGP2t$%t@Be6U;O!COqpgh` zF!pGDtM8(XfjJ>Q6bS-@zDrsluoeh-*i{Dvf@y)EKmmdmb$)}=0wG}#Kte*bKyWDd zT;kIL@gWd^1OWqHaK3XdC<4&JzxzOWAwVoB0x*Z2m+%3e!0mSt2^gXH&P5a;!ocTR z80=ijdtM0)4v0T&VQ`>oDD1pK7?Sr7kr#l+i$eWw#P_|b@32uo$#1UTW5dt!!_N`I zQ0KAXkaH1sULzcW0BjNGZg3b7`-dAGe%=HG6vPYpZiE1g;E3}k@FC7gL7ZdZLjVY& z2MBN7>N;bFO$G zC(9#?wl=|-0+GQ;At6Hi-#bn~*u_@g&;pGC8K6zftOXg?D{2@(W=4Vx>IgZooUI7j z)J)RN9q4H50A66Gq*F0+s5962ZBPOc)(B| zD3lAZ;Bs)a#^}3nSvxTP9zhiCU}$ehoE{4ZVq()EuR_(#V7j;?>{ z`bP}>BjbNZ*FU4{$G?4Gr3|bJ|5$1Z{nt_*NW26YoPmY--{)YcpArI5GY4BseOF+0 zqhf}!M1wH)j_C9CKM>-3ng#>^Wc)pQc%Xzf=213tLj$=2kGd8J0)m5pN?B|@tB>~ZK8{b!t80+()r!ZJA;x=vxvg}2(lB~HDiIUrYjz(s0 z?3H&dUO$4oyZxxM1U1fVyFunXtwuUzKn4rc++?w1^Mg#Ds}LV#}jiyZ>+`il+%g95z&*$x6n{Edze`imVB_DdZQUdXR@ zCH z9qd;f!2Msa09pAj{Q*Nj{@OM;1Oopnr*H_I5BO;0PrLwA1@hN%0EZ*NzxcwD@SnyI z#$Mmd5^Yb252X7)rYYqA`P}?5`}FNGKL$M Date: Sun, 1 Oct 2023 11:16:39 +0800 Subject: [PATCH 08/12] Update agentchat_RetrieveChat.ipynb --- notebook/agentchat_RetrieveChat.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notebook/agentchat_RetrieveChat.ipynb b/notebook/agentchat_RetrieveChat.ipynb index d9767ea9860..60c2f8861d9 100644 --- a/notebook/agentchat_RetrieveChat.ipynb +++ b/notebook/agentchat_RetrieveChat.ipynb @@ -156,7 +156,7 @@ "output_type": "stream", "text": [ "Accepted file formats for `docs_path`:\n", - "['txt', 'json', 'csv', 'tsv', 'md', 'html', 'htm', 'rtf', 'rst', 'jsonl', 'log', 'xml', 'yaml', 'yml']\n" + "['txt', 'json', 'csv', 'tsv', 'md', 'html', 'htm', 'rtf', 'rst', 'jsonl', 'log', 'xml', 'yaml', 'yml', 'pdf']\n" ] } ], From 990f5d7f9d6d654158f5af21dc8400dc0728d18b Mon Sep 17 00:00:00 2001 From: Li Jiang Date: Sun, 1 Oct 2023 11:19:57 +0800 Subject: [PATCH 09/12] Update retrieve_utils.py Fix format --- autogen/retrieve_utils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/autogen/retrieve_utils.py b/autogen/retrieve_utils.py index 28aaba45cc6..ed85e2a6794 100644 --- a/autogen/retrieve_utils.py +++ b/autogen/retrieve_utils.py @@ -262,7 +262,6 @@ def create_vector_db_from_dir( chunks = split_files_to_chunks(get_files_from_dir(dir_path), max_tokens, chunk_mode, must_break_at_empty_line) print(f"Found {len(chunks)} chunks.") - # Upsert in batch of 40000 or less if the total number of chunks is less than 40000 for i in range(0, len(chunks), min(40000, len(chunks))): end_idx = i + min(40000, len(chunks) - i) From 84a962579b058eebff56f1ab3b54dee9a0f0daf4 Mon Sep 17 00:00:00 2001 From: Li Jiang Date: Sun, 1 Oct 2023 11:27:03 +0800 Subject: [PATCH 10/12] Update retrieve_utils.py Replace print with logger --- autogen/retrieve_utils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/autogen/retrieve_utils.py b/autogen/retrieve_utils.py index ed85e2a6794..f56d15d4544 100644 --- a/autogen/retrieve_utils.py +++ b/autogen/retrieve_utils.py @@ -55,10 +55,10 @@ def num_tokens_from_text( tokens_per_message = 4 # every message follows <|start|>{role/name}\n{content}<|end|>\n tokens_per_name = -1 # if there's a name, the role is omitted elif "gpt-3.5-turbo" in model or "gpt-35-turbo" in model: - print("Warning: gpt-3.5-turbo may update over time. Returning num tokens assuming gpt-3.5-turbo-0613.") + logger.warning("Warning: gpt-3.5-turbo may update over time. Returning num tokens assuming gpt-3.5-turbo-0613.") return num_tokens_from_text(text, model="gpt-3.5-turbo-0613") elif "gpt-4" in model: - print("Warning: gpt-4 may update over time. Returning num tokens assuming gpt-4-0613.") + logger.warning("Warning: gpt-4 may update over time. Returning num tokens assuming gpt-4-0613.") return num_tokens_from_text(text, model="gpt-4-0613") else: raise NotImplementedError( @@ -261,7 +261,7 @@ def create_vector_db_from_dir( ) chunks = split_files_to_chunks(get_files_from_dir(dir_path), max_tokens, chunk_mode, must_break_at_empty_line) - print(f"Found {len(chunks)} chunks.") + logger.info(f"Found {len(chunks)} chunks.") # Upsert in batch of 40000 or less if the total number of chunks is less than 40000 for i in range(0, len(chunks), min(40000, len(chunks))): end_idx = i + min(40000, len(chunks) - i) From 544b17ea188caadbc27d6564b53c9c5fc53199d0 Mon Sep 17 00:00:00 2001 From: Ward Date: Sun, 1 Oct 2023 09:59:29 +0100 Subject: [PATCH 11/12] UPDATE - added more specific exception to PDF decryption try/catch --- autogen/retrieve_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/autogen/retrieve_utils.py b/autogen/retrieve_utils.py index f56d15d4544..52874617e38 100644 --- a/autogen/retrieve_utils.py +++ b/autogen/retrieve_utils.py @@ -145,9 +145,9 @@ def extract_text_from_pdf(file: str) -> str: if reader.is_encrypted: # Check if the PDF is encrypted try: reader.decrypt("") - except Exception as e: + except pypdf.errors.FileNotDecryptedError as e: logger.warning(f"Could not decrypt PDF {file}, {e}") - return text # Return empty text if PDF could not be decrypted + return text # Return empty text if PDF could not be decrypted for page_num in range(len(reader.pages)): page = reader.pages[page_num] From 4f7456752ea093d51b774724175e32eb0e52c66f Mon Sep 17 00:00:00 2001 From: Ward Date: Sun, 1 Oct 2023 10:19:30 +0100 Subject: [PATCH 12/12] FIX - typo, return statement at wrong indentation in extract_text_from_pdf --- autogen/retrieve_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/autogen/retrieve_utils.py b/autogen/retrieve_utils.py index 52874617e38..cd8ccbde2ed 100644 --- a/autogen/retrieve_utils.py +++ b/autogen/retrieve_utils.py @@ -147,7 +147,7 @@ def extract_text_from_pdf(file: str) -> str: reader.decrypt("") except pypdf.errors.FileNotDecryptedError as e: logger.warning(f"Could not decrypt PDF {file}, {e}") - return text # Return empty text if PDF could not be decrypted + return text # Return empty text if PDF could not be decrypted for page_num in range(len(reader.pages)): page = reader.pages[page_num]