From 7bfa850dd623e15ad69fe409f295873f0844446c Mon Sep 17 00:00:00 2001 From: rdurnik Date: Tue, 27 Jan 2026 10:00:48 +0100 Subject: [PATCH 001/173] Script for query --- tools/aoptk/aoptk_query.py | 59 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 tools/aoptk/aoptk_query.py diff --git a/tools/aoptk/aoptk_query.py b/tools/aoptk/aoptk_query.py new file mode 100644 index 00000000..1eb339e1 --- /dev/null +++ b/tools/aoptk/aoptk_query.py @@ -0,0 +1,59 @@ +from Bio import Entrez +from aoptk.literature.databases.pubmed import PubMed +from aoptk.literature.databases.europepmc import EuropePMC +import argparse + + +def generate_database_with_ids( + query: str, database: str, email: str +) -> EuropePMC | PubMed | None: + """Generate an object with IDs from the specified literature database. + + Args: + query (str): Search term for PubMed or Europe PMC. + literature_database (str): Database to search: PubMed or Europe PMC. + email (str): Email address to follow PubMed - NCBI guidelines. + """ + if database == "pubmed": + Entrez.email = email + pubmed = PubMed(query) + ids = pubmed.get_id() + pubmed.id_list = ids + return pubmed + if database == "europepmc": + europepmc = EuropePMC(query) + ids = europepmc.get_id() + europepmc.id_list = ids + return europepmc + return None + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Fetch PubMed or Europe PMC IDs using aoptk" + ) + parser.add_argument( + "--query", required=True, help="Search term as used in PubMed/Europe PMC" + ) + parser.add_argument( + "--literature_database", + required=True, + choices=["pubmed", "europepmc"], + help="Database to query", + ) + parser.add_argument( + "--email", required=True, help="Email to comply with NCBI guidelines" + ) + return parser.parse_args() + + +if __name__ == "__main__": + args = parse_args() + db_obj = generate_database_with_ids( + args.query, args.literature_database, args.email + ) + if db_obj is None: + raise SystemExit( + "Unsupported literature_database; choose 'pubmed' or 'europepmc'." + ) + print(db_obj.id_list) From ca87cb49923442b256415374b9276fb8a91667bc Mon Sep 17 00:00:00 2001 From: rdurnik Date: Tue, 27 Jan 2026 10:35:07 +0100 Subject: [PATCH 002/173] Script for abstracts download --- tools/aoptk/aoptk_download_abstracts.py | 27 +++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 tools/aoptk/aoptk_download_abstracts.py diff --git a/tools/aoptk/aoptk_download_abstracts.py b/tools/aoptk/aoptk_download_abstracts.py new file mode 100644 index 00000000..1c1c1c2f --- /dev/null +++ b/tools/aoptk/aoptk_download_abstracts.py @@ -0,0 +1,27 @@ +from aoptk.literature.databases.pubmed import PubMed +from aoptk.literature.databases.europepmc import EuropePMC +from aoptk.literature.abstracts.abstract import Abstract +import argparse + + +def download_abstracts(database_with_ids: PubMed | EuropePMC) -> list[Abstract]: + """Genereate a list of abstracts from the specified literature database. + + Args: + database_with_ids (PubMed | EuropePMC): Database object with IDs. + """ + return database_with_ids.get_abstracts() + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Download abstracts from PubMed or Europe PMC using aoptk" + ) + parser.add_argument( + "--database_with_ids", required=True, help="Database object with IDs" + ) + + +if __name__ == "__main__": + args = parse_args() + abstracts = download_abstracts(args.database_with_ids) From 1bed026b98c1cbf6aeb2bf3554ed2fdacaacd2ce Mon Sep 17 00:00:00 2001 From: rdurnik Date: Tue, 27 Jan 2026 10:35:13 +0100 Subject: [PATCH 003/173] Query clean up --- tools/aoptk/aoptk_query.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/tools/aoptk/aoptk_query.py b/tools/aoptk/aoptk_query.py index 1eb339e1..3abfb53e 100644 --- a/tools/aoptk/aoptk_query.py +++ b/tools/aoptk/aoptk_query.py @@ -11,7 +11,7 @@ def generate_database_with_ids( Args: query (str): Search term for PubMed or Europe PMC. - literature_database (str): Database to search: PubMed or Europe PMC. + database (str): Database to search: PubMed or Europe PMC. email (str): Email address to follow PubMed - NCBI guidelines. """ if database == "pubmed": @@ -36,7 +36,7 @@ def parse_args() -> argparse.Namespace: "--query", required=True, help="Search term as used in PubMed/Europe PMC" ) parser.add_argument( - "--literature_database", + "--database", required=True, choices=["pubmed", "europepmc"], help="Database to query", @@ -49,11 +49,6 @@ def parse_args() -> argparse.Namespace: if __name__ == "__main__": args = parse_args() - db_obj = generate_database_with_ids( - args.query, args.literature_database, args.email + database_with_ids = generate_database_with_ids( + args.query, args.database, args.email ) - if db_obj is None: - raise SystemExit( - "Unsupported literature_database; choose 'pubmed' or 'europepmc'." - ) - print(db_obj.id_list) From 50f8ced4a8d9b97041acca712a2cbdefa9667050 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Tue, 27 Jan 2026 11:13:46 +0100 Subject: [PATCH 004/173] Script for chemical identification --- tools/aoptk/aoptk_chemical_identification.py | 23 ++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 tools/aoptk/aoptk_chemical_identification.py diff --git a/tools/aoptk/aoptk_chemical_identification.py b/tools/aoptk/aoptk_chemical_identification.py new file mode 100644 index 00000000..e983e75e --- /dev/null +++ b/tools/aoptk/aoptk_chemical_identification.py @@ -0,0 +1,23 @@ +from aoptk.chemical import Chemical +from aoptk.spacy_processor import Spacy +import argparse + + +def find_chemicals(text: str) -> list[Chemical]: + """Genereate a list of chemicals from text. + + Args: + text (str): Text to identify chemicals in. + """ + return Spacy().find_chemical(text) + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Identify chemicals in text") + parser.add_argument("--text", required=True, help="Text to identify chemicals in") + return parser.parse_args() + + +if __name__ == "__main__": + args = parse_args() + chemicals = find_chemicals(args.text) \ No newline at end of file From 36ed78e3bd86c008ea64e0e6c110909336968dcf Mon Sep 17 00:00:00 2001 From: rdurnik Date: Tue, 27 Jan 2026 11:57:47 +0100 Subject: [PATCH 005/173] Script for matching of chemical names to database of relevant chemicals --- tools/aoptk/aoptk_chemical_matching.py | 39 ++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 tools/aoptk/aoptk_chemical_matching.py diff --git a/tools/aoptk/aoptk_chemical_matching.py b/tools/aoptk/aoptk_chemical_matching.py new file mode 100644 index 00000000..b98bb525 --- /dev/null +++ b/tools/aoptk/aoptk_chemical_matching.py @@ -0,0 +1,39 @@ +from aoptk.chemical import Chemical +import argparse + + +def match_chemicals_with_loose_equality( + list_of_relevant_chemicals: list[Chemical], + normalized_chemicals: list[Chemical], +) -> list[str]: + """Match normalized chemicals with relevant chemicals using loose equality. + + Args: + list_of_relevant_chemicals (list[Chemical]): List of relevant chemicals. + normalized_chemicals (list[Chemical]): List of normalized chemicals. + """ + relevant_chemicals_names = [] + for chemical in normalized_chemicals: + for relevant_chemical in list_of_relevant_chemicals: + if chemical.similar(relevant_chemical): + relevant_chemicals_names.append(chemical.name) + break + return relevant_chemicals_names + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Match chemicals using loose equality") + parser.add_argument( + "--list_of_relevant_chemicals", required=True, help="List of relevant chemicals" + ) + parser.add_argument( + "--normalized_chemicals", required=True, help="List of normalized chemicals" + ) + return parser.parse_args() + + +if __name__ == "__main__": + args = parse_args() + relevant_chemicals_names = match_chemicals_with_loose_equality( + args.list_of_relevant_chemicals, args.normalized_chemicals + ) From e5fcdc91338cc977ed1e1b891134ed1d0e3f6f41 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Tue, 27 Jan 2026 11:58:00 +0100 Subject: [PATCH 006/173] Lint --- tools/aoptk/aoptk_chemical_identification.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/aoptk/aoptk_chemical_identification.py b/tools/aoptk/aoptk_chemical_identification.py index e983e75e..b271b332 100644 --- a/tools/aoptk/aoptk_chemical_identification.py +++ b/tools/aoptk/aoptk_chemical_identification.py @@ -20,4 +20,4 @@ def parse_args() -> argparse.Namespace: if __name__ == "__main__": args = parse_args() - chemicals = find_chemicals(args.text) \ No newline at end of file + chemicals = find_chemicals(args.text) From 95044b4ae7c88927751271a6c33484861cc95805 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Tue, 27 Jan 2026 12:29:58 +0100 Subject: [PATCH 007/173] Script to normalize chemical --- tools/aoptk/aoptk_normalize_chemical.py | 26 +++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 tools/aoptk/aoptk_normalize_chemical.py diff --git a/tools/aoptk/aoptk_normalize_chemical.py b/tools/aoptk/aoptk_normalize_chemical.py new file mode 100644 index 00000000..735b64dc --- /dev/null +++ b/tools/aoptk/aoptk_normalize_chemical.py @@ -0,0 +1,26 @@ +from aoptk.chemical import Chemical +from aoptk.normalization.mesh_terms import MeshTerms +import argparse +import pandas as pd + + +def normalize_chemical(mesh_terms: pd.DataFrame, chemical: Chemical) -> Chemical: + """Normalize a chemical using MeSH terms. + + Args: + mesh_terms (pd.DataFrame): MeSH terms dataframe. + chemical (Chemical): Chemical to normalize. + """ + return MeshTerms(mesh_terms).normalize_chemical(chemical) + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Identify chemicals in text") + parser.add_argument("--mesh_terms", required=True, help="MeSH terms dataframe") + parser.add_argument("--chemical", required=True, help="Chemical to normalize") + return parser.parse_args() + + +if __name__ == "__main__": + args = parse_args() + normalized_chemical = normalize_chemical(args.mesh_terms, args.chemical) From 5759105c35b88ec8e7ae174254520d8bf083e60f Mon Sep 17 00:00:00 2001 From: rdurnik Date: Tue, 27 Jan 2026 12:36:11 +0100 Subject: [PATCH 008/173] Updated matching to take list of chemical names (Excel file) --- tools/aoptk/aoptk_chemical_matching.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/tools/aoptk/aoptk_chemical_matching.py b/tools/aoptk/aoptk_chemical_matching.py index b98bb525..96b516fc 100644 --- a/tools/aoptk/aoptk_chemical_matching.py +++ b/tools/aoptk/aoptk_chemical_matching.py @@ -1,5 +1,6 @@ from aoptk.chemical import Chemical import argparse +import pandas as pd def match_chemicals_with_loose_equality( @@ -21,6 +22,22 @@ def match_chemicals_with_loose_equality( return relevant_chemicals_names +def generate_relevant_chemicals(chemical_database: str) -> list[Chemical]: + """Generate a list of relevant chemicals from Excel file. + + Args: + chemical_database (str): Path to the user-defined chemical database in Excel. + """ + relevant_chemicals_database = pd.read_excel(chemical_database) + return [ + Chemical(name) + for name in relevant_chemicals_database["chemical_name"] + .astype(str) + .str.lower() + .unique() + ] + + def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description="Match chemicals using loose equality") parser.add_argument( @@ -34,6 +51,9 @@ def parse_args() -> argparse.Namespace: if __name__ == "__main__": args = parse_args() + list_of_relevant_chemicals = generate_relevant_chemicals( + args.list_of_relevant_chemicals + ) relevant_chemicals_names = match_chemicals_with_loose_equality( - args.list_of_relevant_chemicals, args.normalized_chemicals + list_of_relevant_chemicals, args.normalized_chemicals ) From 409c8c9efeaa41ba26bf1583b1aa322f9683de23 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Tue, 27 Jan 2026 12:55:47 +0100 Subject: [PATCH 009/173] Script to generate MeSH terms dataframe --- tools/aoptk/aoptk_generate_mesh_terms_df.py | 23 +++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 tools/aoptk/aoptk_generate_mesh_terms_df.py diff --git a/tools/aoptk/aoptk_generate_mesh_terms_df.py b/tools/aoptk/aoptk_generate_mesh_terms_df.py new file mode 100644 index 00000000..cb043fc8 --- /dev/null +++ b/tools/aoptk/aoptk_generate_mesh_terms_df.py @@ -0,0 +1,23 @@ +from aoptk.normalization.provide_mesh_term_dataframe_from_xml import ProvideMeshTermDataframeFromXML +import argparse +import pandas as pd + + +def provide_normalization_dataframe(xml_path: str) -> pd.DataFrame: + """Generate a MeSH terms dataframe from an XML file. + + Args: + xml_path (str): Path to the MeSH XML file. + """ + return ProvideMeshTermDataframeFromXML(xml_path).provide_normalization_dataframe() + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Generate MeSH terms dataframe from XML") + parser.add_argument("--xml_path", required=True, help="Path to the MeSH XML file") + return parser.parse_args() + + +if __name__ == "__main__": + args = parse_args() + mesh_terms_df = provide_normalization_dataframe(args.xml_path) From f3258e22419893ccf5c01da385ce62352fcd0b0b Mon Sep 17 00:00:00 2001 From: rdurnik Date: Tue, 27 Jan 2026 13:53:42 +0100 Subject: [PATCH 010/173] Script for find relationships --- tools/aoptk/aoptk_find_relationships.py | 30 +++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 tools/aoptk/aoptk_find_relationships.py diff --git a/tools/aoptk/aoptk_find_relationships.py b/tools/aoptk/aoptk_find_relationships.py new file mode 100644 index 00000000..06aa90d4 --- /dev/null +++ b/tools/aoptk/aoptk_find_relationships.py @@ -0,0 +1,30 @@ +from aoptk.chemical import Chemical +from aoptk.effect import Effect +from aoptk.relationships.relationship import Relationship +from aoptk.relationships.zero_shot_classification_single import ZeroShotClassificationSingle +import argparse +import pandas as pd + + +def find_relationships(text: str, chemicals: list[Chemical], effects: list[Effect]) -> list[Relationship]: + """Find relationships between chemicals and effects. + + Args: + text (str): Input text to analyze. + chemicals (list[Chemical]): List of chemicals to consider. + effects (list[Effect]): List of effects to consider. + """ + return ZeroShotClassificationSingle().find_relationships(text=text, chemicals=chemicals, effects=effects) + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Find relationships between chemicals and effects") + parser.add_argument("--text", required=True, help="Input text to analyze") + parser.add_argument("--chemicals", required=True, help="List of chemicals to consider") + parser.add_argument("--effects", required=True, help="List of effects to consider") + return parser.parse_args() + + +if __name__ == "__main__": + args = parse_args() + relationships = find_relationships(args.text, args.chemicals, args.effects) From dffc88967c5a855d3bf9d5f10f65c5648fd15965 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Tue, 27 Jan 2026 17:18:04 +0100 Subject: [PATCH 011/173] IDs are saved as txt file --- tools/aoptk/aoptk_query.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tools/aoptk/aoptk_query.py b/tools/aoptk/aoptk_query.py index 3abfb53e..a8e440f8 100644 --- a/tools/aoptk/aoptk_query.py +++ b/tools/aoptk/aoptk_query.py @@ -46,9 +46,20 @@ def parse_args() -> argparse.Namespace: ) return parser.parse_args() +def save_file(database_with_ids, filename: str): + """Save the IDs to a text file. + + Args: + database_with_ids (EuropePMC | PubMed): Object containing the IDs. + filename (str): Name of the output file. + """ + with open(filename, "w") as f: + for id_ in database_with_ids.id_list: + f.write(f"{id_}\n") if __name__ == "__main__": args = parse_args() database_with_ids = generate_database_with_ids( args.query, args.database, args.email ) + save_file(database_with_ids, "ids.txt") \ No newline at end of file From 4aa629b3f9d8ac63862fb490e1b4fa39eac0ad93 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Tue, 27 Jan 2026 17:23:59 +0100 Subject: [PATCH 012/173] Clean up --- tools/aoptk/aoptk_query.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tools/aoptk/aoptk_query.py b/tools/aoptk/aoptk_query.py index a8e440f8..b20140d1 100644 --- a/tools/aoptk/aoptk_query.py +++ b/tools/aoptk/aoptk_query.py @@ -27,7 +27,6 @@ def generate_database_with_ids( return europepmc return None - def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser( description="Fetch PubMed or Europe PMC IDs using aoptk" @@ -46,7 +45,7 @@ def parse_args() -> argparse.Namespace: ) return parser.parse_args() -def save_file(database_with_ids, filename: str): +def save_file(database_with_ids: EuropePMC | PubMed, filename: str) -> None: """Save the IDs to a text file. Args: From 9204380ed7e726370e6ee95a1d4818d1cc8abe89 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Wed, 28 Jan 2026 07:55:16 +0100 Subject: [PATCH 013/173] Added output dir --- tools/aoptk/aoptk_query.py | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/tools/aoptk/aoptk_query.py b/tools/aoptk/aoptk_query.py index b20140d1..98c23c2a 100644 --- a/tools/aoptk/aoptk_query.py +++ b/tools/aoptk/aoptk_query.py @@ -27,6 +27,19 @@ def generate_database_with_ids( return europepmc return None + +def save_file(database_with_ids: EuropePMC | PubMed, filename: str) -> None: + """Save the IDs to a text file. + + Args: + database_with_ids (EuropePMC | PubMed): Object containing the IDs. + filename (str): Name of the output file. + """ + with open(filename, "w") as f: + for id_ in database_with_ids.id_list: + f.write(f"{id_}\n") + + def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser( description="Fetch PubMed or Europe PMC IDs using aoptk" @@ -43,22 +56,15 @@ def parse_args() -> argparse.Namespace: parser.add_argument( "--email", required=True, help="Email to comply with NCBI guidelines" ) + parser.add_argument( + "--outdir", required=True, help="Output directory for saving files" + ) return parser.parse_args() -def save_file(database_with_ids: EuropePMC | PubMed, filename: str) -> None: - """Save the IDs to a text file. - - Args: - database_with_ids (EuropePMC | PubMed): Object containing the IDs. - filename (str): Name of the output file. - """ - with open(filename, "w") as f: - for id_ in database_with_ids.id_list: - f.write(f"{id_}\n") if __name__ == "__main__": args = parse_args() database_with_ids = generate_database_with_ids( args.query, args.database, args.email ) - save_file(database_with_ids, "ids.txt") \ No newline at end of file + save_file(database_with_ids, f"{args.outdir}/ids.txt") From 12849a8dff91a9b8fa8175967439018419fd9ecf Mon Sep 17 00:00:00 2001 From: rdurnik Date: Wed, 28 Jan 2026 10:04:38 +0100 Subject: [PATCH 014/173] Abstracts are saved as tsv file --- tools/aoptk/aoptk_download_abstracts.py | 56 ++++++++++++++++++++++--- 1 file changed, 50 insertions(+), 6 deletions(-) diff --git a/tools/aoptk/aoptk_download_abstracts.py b/tools/aoptk/aoptk_download_abstracts.py index 1c1c1c2f..64925862 100644 --- a/tools/aoptk/aoptk_download_abstracts.py +++ b/tools/aoptk/aoptk_download_abstracts.py @@ -1,16 +1,44 @@ from aoptk.literature.databases.pubmed import PubMed from aoptk.literature.databases.europepmc import EuropePMC -from aoptk.literature.abstracts.abstract import Abstract +from aoptk.literature.abstract import Abstract +from Bio import Entrez + import argparse -def download_abstracts(database_with_ids: PubMed | EuropePMC) -> list[Abstract]: +def download_abstracts( + database_with_ids_path: str, database: str, email: str +) -> list[Abstract]: """Genereate a list of abstracts from the specified literature database. Args: - database_with_ids (PubMed | EuropePMC): Database object with IDs. + database_with_ids_path (str): Path to the file containing database IDs. + """ + with open(database_with_ids_path, "r") as f: + ids = [line.strip() for line in f.readlines()] + if database == "pubmed": + Entrez.email = email + pubmed = PubMed.__new__(PubMed) + pubmed.id_list = ids + return pubmed.get_abstracts() + if database == "europepmc": + europepmc = EuropePMC.__new__(EuropePMC) + europepmc.id_list = ids + return europepmc.get_abstracts() + return None + + +def save_file(abstracts: list[Abstract], filename: str) -> None: + """Save abstracts to a TSV file. + + Args: + abstracts (list[Abstract]): List of abstracts to save. + filename (str): Name of the output file. """ - return database_with_ids.get_abstracts() + with open(filename, "w") as f: + f.write("id\ttext\n") + for abstract in abstracts: + f.write(f"{abstract.publication_id}\t{abstract.text}\n") def parse_args() -> argparse.Namespace: @@ -18,10 +46,26 @@ def parse_args() -> argparse.Namespace: description="Download abstracts from PubMed or Europe PMC using aoptk" ) parser.add_argument( - "--database_with_ids", required=True, help="Database object with IDs" + "--database_with_ids", + required=True, + help="Path to the file containing database IDs", + ) + parser.add_argument( + "--database", + required=True, + choices=["pubmed", "europepmc"], + help="Database to query", + ) + parser.add_argument( + "--email", required=True, help="Email to comply with NCBI guidelines" + ) + parser.add_argument( + "--outdir", required=True, help="Output directory for saving files" ) + return parser.parse_args() if __name__ == "__main__": args = parse_args() - abstracts = download_abstracts(args.database_with_ids) + abstracts = download_abstracts(args.database_with_ids, args.database, args.email) + save_file(abstracts, f"{args.outdir}/abstracts.tsv") From 980378036b57bbb53c5bc8ef45dd02d2711d0b9a Mon Sep 17 00:00:00 2001 From: rdurnik Date: Wed, 28 Jan 2026 10:08:34 +0100 Subject: [PATCH 015/173] Script for PDF download --- tools/aoptk/aoptk_download_pdfs.py | 42 ++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 tools/aoptk/aoptk_download_pdfs.py diff --git a/tools/aoptk/aoptk_download_pdfs.py b/tools/aoptk/aoptk_download_pdfs.py new file mode 100644 index 00000000..bfd2642f --- /dev/null +++ b/tools/aoptk/aoptk_download_pdfs.py @@ -0,0 +1,42 @@ +from aoptk.literature.databases.europepmc import EuropePMC +from aoptk.literature.abstract import Abstract + +import argparse + + +def download_pdfs( + database_with_ids_path: str, + output_dir: str, +) -> list[Abstract]: + """Genereate a list of abstracts from the specified literature database. + + Args: + database_with_ids_path (str): Path to the file containing database IDs. + """ + with open(database_with_ids_path, "r") as f: + ids = [line.strip() for line in f.readlines()] + europepmc = EuropePMC.__new__(EuropePMC) + europepmc.__init__("") + europepmc.storage = output_dir + europepmc.id_list = ids + return europepmc.pdfs() + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Download abstracts from PubMed or Europe PMC using aoptk" + ) + parser.add_argument( + "--database_with_ids", + required=True, + help="Path to the file containing database IDs", + ) + parser.add_argument( + "--outdir", required=True, help="Output directory for saving files" + ) + return parser.parse_args() + + +if __name__ == "__main__": + args = parse_args() + abstracts = download_pdfs(args.database_with_ids, args.outdir) From 0f55a3966a5b07fb684c2910eddc5abd0f840a12 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Wed, 28 Jan 2026 13:27:50 +0100 Subject: [PATCH 016/173] MeSH terms df saved as tsv --- tools/aoptk/aoptk_generate_mesh_terms_df.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/tools/aoptk/aoptk_generate_mesh_terms_df.py b/tools/aoptk/aoptk_generate_mesh_terms_df.py index cb043fc8..6ac151fa 100644 --- a/tools/aoptk/aoptk_generate_mesh_terms_df.py +++ b/tools/aoptk/aoptk_generate_mesh_terms_df.py @@ -1,6 +1,9 @@ -from aoptk.normalization.provide_mesh_term_dataframe_from_xml import ProvideMeshTermDataframeFromXML +from aoptk.normalization.provide_mesh_term_dataframe_from_xml import ( + ProvideMeshTermDataframeFromXML, +) import argparse import pandas as pd +import os def provide_normalization_dataframe(xml_path: str) -> pd.DataFrame: @@ -13,11 +16,16 @@ def provide_normalization_dataframe(xml_path: str) -> pd.DataFrame: def parse_args() -> argparse.Namespace: - parser = argparse.ArgumentParser(description="Generate MeSH terms dataframe from XML") + parser = argparse.ArgumentParser( + description="Generate MeSH terms dataframe from XML" + ) parser.add_argument("--xml_path", required=True, help="Path to the MeSH XML file") + parser.add_argument("--outdir", required=True, help="Output directory for saving results") return parser.parse_args() if __name__ == "__main__": args = parse_args() mesh_terms_df = provide_normalization_dataframe(args.xml_path) + output_file = os.path.join(args.outdir, "mesh_terms.tsv") + mesh_terms_df.to_csv(output_file, sep="\t", index=False) From 2c5ff4a1defbc4d70bfd85325e21fa4a053d455f Mon Sep 17 00:00:00 2001 From: rdurnik Date: Wed, 28 Jan 2026 13:28:08 +0100 Subject: [PATCH 017/173] Lint --- tools/aoptk/aoptk_generate_mesh_terms_df.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tools/aoptk/aoptk_generate_mesh_terms_df.py b/tools/aoptk/aoptk_generate_mesh_terms_df.py index 6ac151fa..9edb83d4 100644 --- a/tools/aoptk/aoptk_generate_mesh_terms_df.py +++ b/tools/aoptk/aoptk_generate_mesh_terms_df.py @@ -20,7 +20,9 @@ def parse_args() -> argparse.Namespace: description="Generate MeSH terms dataframe from XML" ) parser.add_argument("--xml_path", required=True, help="Path to the MeSH XML file") - parser.add_argument("--outdir", required=True, help="Output directory for saving results") + parser.add_argument( + "--outdir", required=True, help="Output directory for saving results" + ) return parser.parse_args() From e691d6fc884a287322b525253d86c959db928ec6 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Wed, 28 Jan 2026 13:28:41 +0100 Subject: [PATCH 018/173] Chemical identification saved as file --- tools/aoptk/aoptk_chemical_identification.py | 35 +++++++++++++++++--- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/tools/aoptk/aoptk_chemical_identification.py b/tools/aoptk/aoptk_chemical_identification.py index b271b332..93042346 100644 --- a/tools/aoptk/aoptk_chemical_identification.py +++ b/tools/aoptk/aoptk_chemical_identification.py @@ -1,10 +1,12 @@ from aoptk.chemical import Chemical from aoptk.spacy_processor import Spacy import argparse +import csv +import os def find_chemicals(text: str) -> list[Chemical]: - """Genereate a list of chemicals from text. + """Generate a list of chemicals from text. Args: text (str): Text to identify chemicals in. @@ -12,12 +14,37 @@ def find_chemicals(text: str) -> list[Chemical]: return Spacy().find_chemical(text) +def save_file(input_file: str, output_file: str) -> None: + """Process a TSV file with text column, find chemicals, and save results. + + Args: + input_file (str): Path to input TSV file with 'text' column. + output_file (str): Path to output TSV file. + """ + with open(input_file, "r") as f_in, open(output_file, "w") as f_out: + f_out.write("id\tchemicals\n") + for row in csv.DictReader(f_in, delimiter="\t"): + chemicals = find_chemicals(row["text"]) + chemicals_str = ( + "|".join(set([chem.name for chem in chemicals])) if chemicals else "" + ) + f_out.write(f"{row['id']}\t{chemicals_str}\n") + + def parse_args() -> argparse.Namespace: - parser = argparse.ArgumentParser(description="Identify chemicals in text") - parser.add_argument("--text", required=True, help="Text to identify chemicals in") + parser = argparse.ArgumentParser( + description="Identify chemicals in a TSV file with text column" + ) + parser.add_argument( + "--input_file", required=True, help="Input TSV file with text column" + ) + parser.add_argument( + "--outdir", required=True, help="Output directory for saving results" + ) return parser.parse_args() if __name__ == "__main__": args = parse_args() - chemicals = find_chemicals(args.text) + output_file = os.path.join(args.outdir, "chemicals.tsv") + save_file(input_file=args.input_file, output_file=output_file) From 95c1524948245d65070c2191880eb339209bb093 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Wed, 28 Jan 2026 15:26:07 +0100 Subject: [PATCH 019/173] Normalize chemical takes saved output and outputs a file --- tools/aoptk/aoptk_normalize_chemical.py | 34 ++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/tools/aoptk/aoptk_normalize_chemical.py b/tools/aoptk/aoptk_normalize_chemical.py index 735b64dc..d3411b78 100644 --- a/tools/aoptk/aoptk_normalize_chemical.py +++ b/tools/aoptk/aoptk_normalize_chemical.py @@ -2,6 +2,7 @@ from aoptk.normalization.mesh_terms import MeshTerms import argparse import pandas as pd +import os def normalize_chemical(mesh_terms: pd.DataFrame, chemical: Chemical) -> Chemical: @@ -14,13 +15,40 @@ def normalize_chemical(mesh_terms: pd.DataFrame, chemical: Chemical) -> Chemical return MeshTerms(mesh_terms).normalize_chemical(chemical) +def save_file(input_file: str, mesh_terms_df: pd.DataFrame, output_file: str) -> None: + """Process a TSV file with chemicals, normalize them, and save results. + + Args: + input_file (str): Path to input TSV file with chemicals. + mesh_terms_df (pd.DataFrame): MeSH terms dataframe for normalization. + output_file (str): Path to output TSV file. + """ + with open(input_file, "r") as f_in, open(output_file, "w") as f_out: + f_out.write("name\theading\tsynonyms\n") + for row in pd.read_csv(f_in, sep="\t").itertuples(): + chemicals = row.chemicals.split("|") + for chem in chemicals: + chemical = Chemical(chem.strip()) + normalized_chemical = normalize_chemical(mesh_terms_df, chemical) + f_out.write( + f"{normalized_chemical.name}\t{normalized_chemical.heading}\t{normalized_chemical.synonyms}\n" + ) + + def parse_args() -> argparse.Namespace: - parser = argparse.ArgumentParser(description="Identify chemicals in text") + parser = argparse.ArgumentParser(description="Normalize chemicals using MeSH terms") parser.add_argument("--mesh_terms", required=True, help="MeSH terms dataframe") - parser.add_argument("--chemical", required=True, help="Chemical to normalize") + parser.add_argument( + "--input_file", required=True, help="Input TSV file with chemicals" + ) + parser.add_argument( + "--outdir", required=True, help="Output directory for saving results" + ) return parser.parse_args() if __name__ == "__main__": args = parse_args() - normalized_chemical = normalize_chemical(args.mesh_terms, args.chemical) + mesh_terms_df = pd.read_csv(args.mesh_terms, sep="\t") + output_file = os.path.join(args.outdir, "normalized_chemicals.tsv") + save_file(args.input_file, mesh_terms_df, output_file) From 2e915250bbeeea02b6609e4b8271756a3d4800e2 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Wed, 28 Jan 2026 17:20:19 +0100 Subject: [PATCH 020/173] Chemical matching takes file and returns a file --- tools/aoptk/aoptk_chemical_matching.py | 41 +++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/tools/aoptk/aoptk_chemical_matching.py b/tools/aoptk/aoptk_chemical_matching.py index 96b516fc..a540bcd8 100644 --- a/tools/aoptk/aoptk_chemical_matching.py +++ b/tools/aoptk/aoptk_chemical_matching.py @@ -3,18 +3,31 @@ import pandas as pd +def extract_chemicals_to_match(input_file: str) -> list[Chemical]: + chemicals = [] + with open(input_file, "r") as f_in: + for row in pd.read_csv(f_in, sep="\t").itertuples(): + chemical = Chemical(row.name) + chemical.heading = row.heading + chemical._synonyms = ( + set(row.synonyms.split(";")) if pd.notna(row.synonyms) else set() + ) + chemicals.append(chemical) + return chemicals + + def match_chemicals_with_loose_equality( list_of_relevant_chemicals: list[Chemical], - normalized_chemicals: list[Chemical], + chemicals: list[Chemical], ) -> list[str]: """Match normalized chemicals with relevant chemicals using loose equality. Args: list_of_relevant_chemicals (list[Chemical]): List of relevant chemicals. - normalized_chemicals (list[Chemical]): List of normalized chemicals. + chemicals (list[Chemical]): List of chemicals. """ relevant_chemicals_names = [] - for chemical in normalized_chemicals: + for chemical in chemicals: for relevant_chemical in list_of_relevant_chemicals: if chemical.similar(relevant_chemical): relevant_chemicals_names.append(chemical.name) @@ -38,6 +51,19 @@ def generate_relevant_chemicals(chemical_database: str) -> list[Chemical]: ] +def save_file(relevant_chemicals_names: list[str], output_file: str) -> None: + """Process a TSV file with chemicals, match them, and save results. + + Args: + input_file (str): Path to input TSV file with chemicals. + output_file (str): Path to output TSV file. + """ + with open(output_file, "w") as f_out: + f_out.write("matched_chemicals\n") + for chemical_name in relevant_chemicals_names: + f_out.write(f"{chemical_name}\n") + + def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description="Match chemicals using loose equality") parser.add_argument( @@ -46,14 +72,21 @@ def parse_args() -> argparse.Namespace: parser.add_argument( "--normalized_chemicals", required=True, help="List of normalized chemicals" ) + parser.add_argument( + "--outdir", required=True, help="Output directory for saving files" + ) return parser.parse_args() if __name__ == "__main__": args = parse_args() + chemicals_to_match = extract_chemicals_to_match(args.normalized_chemicals) list_of_relevant_chemicals = generate_relevant_chemicals( args.list_of_relevant_chemicals ) relevant_chemicals_names = match_chemicals_with_loose_equality( - list_of_relevant_chemicals, args.normalized_chemicals + list_of_relevant_chemicals, chemicals_to_match + ) + save_file( + relevant_chemicals_names, output_file=f"{args.outdir}/matched_chemicals.tsv" ) From 36239be3fde73652e2fe639ed538a5f3de599e7a Mon Sep 17 00:00:00 2001 From: rdurnik Date: Wed, 28 Jan 2026 17:32:07 +0100 Subject: [PATCH 021/173] Find chemicals keeps text --- tools/aoptk/aoptk_chemical_identification.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/aoptk/aoptk_chemical_identification.py b/tools/aoptk/aoptk_chemical_identification.py index 93042346..f97bc3d1 100644 --- a/tools/aoptk/aoptk_chemical_identification.py +++ b/tools/aoptk/aoptk_chemical_identification.py @@ -22,13 +22,13 @@ def save_file(input_file: str, output_file: str) -> None: output_file (str): Path to output TSV file. """ with open(input_file, "r") as f_in, open(output_file, "w") as f_out: - f_out.write("id\tchemicals\n") + f_out.write("id\ttext\tchemicals\n") for row in csv.DictReader(f_in, delimiter="\t"): chemicals = find_chemicals(row["text"]) chemicals_str = ( "|".join(set([chem.name for chem in chemicals])) if chemicals else "" ) - f_out.write(f"{row['id']}\t{chemicals_str}\n") + f_out.write(f"{row['id']}\t{row['text']}\t{chemicals_str}\n") def parse_args() -> argparse.Namespace: From 0c93372d21ff6b5237d23e4c3b8f48e69c4ac7af Mon Sep 17 00:00:00 2001 From: rdurnik Date: Thu, 29 Jan 2026 09:52:43 +0100 Subject: [PATCH 022/173] Script to parse PDFs --- tools/aoptk/aoptk_parse_pdfs.py | 70 +++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 tools/aoptk/aoptk_parse_pdfs.py diff --git a/tools/aoptk/aoptk_parse_pdfs.py b/tools/aoptk/aoptk_parse_pdfs.py new file mode 100644 index 00000000..400bfb6e --- /dev/null +++ b/tools/aoptk/aoptk_parse_pdfs.py @@ -0,0 +1,70 @@ +from aoptk.literature.publication import Publication +from aoptk.literature.pdf import PDF +from aoptk.literature.pymupdf_parser import PymupdfParser + + +import argparse +import os +import csv + + +def parse_pdfs( + folder_with_pdfs: list[PDF], figures_output_dir: str +) -> list[Publication]: + """Genereate a list of abstracts from the specified literature database. + + Args: + folder_with_pdfs (list[PDF]): List of PDF objects representing the folder with PDFs. + figures_output_dir (str): Directory to save extracted figures. + """ + return PymupdfParser(folder_with_pdfs, figures_output_dir).get_publications() + + +def save_file(publications: list[Publication], output_file: str) -> None: + """Process a list of publications and save results. + + Args: + publications (list[Publication]): List of publications to save. + output_file (str): Path to output file. + """ + with open(output_file, "w", newline="") as f_out: + writer = csv.writer(f_out, delimiter="\t") + writer.writerow(["publication_id", "full_text", "figure_descriptions"]) + for publication in publications: + full_text = publication.full_text.replace("\t", " ").replace("\n", " ") + figure_descriptions = "|".join(publication.figure_descriptions) + figure_descriptions = figure_descriptions.replace("\t", " ").replace( + "\n", " " + ) + writer.writerow([publication.id, full_text, figure_descriptions]) + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Download abstracts from PubMed or Europe PMC using aoptk" + ) + parser.add_argument( + "--folder_with_pdfs", + required=True, + help="Path to the folder containing PDFs", + ) + parser.add_argument( + "--figures_output_dir", + required=True, + help="Output directory for saving extracted figures", + ) + parser.add_argument( + "--outdir", required=True, help="Output directory for saving files" + ) + return parser.parse_args() + + +if __name__ == "__main__": + args = parse_args() + pdf_files = [ + PDF(os.path.join(args.folder_with_pdfs, f)) + for f in os.listdir(args.folder_with_pdfs) + if f.lower().endswith(".pdf") + ] + publications = parse_pdfs(pdf_files, args.figures_output_dir) + save_file(publications, f"{args.outdir}/parsed_publications.tsv") From fd189fee9bb0fcf436417ab1b6e4d3adae1ab3ec Mon Sep 17 00:00:00 2001 From: rdurnik Date: Thu, 29 Jan 2026 09:56:20 +0100 Subject: [PATCH 023/173] Renamed file --- ...optk_normalize_chemical.py => aoptk_chemical_normalization.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tools/aoptk/{aoptk_normalize_chemical.py => aoptk_chemical_normalization.py} (100%) diff --git a/tools/aoptk/aoptk_normalize_chemical.py b/tools/aoptk/aoptk_chemical_normalization.py similarity index 100% rename from tools/aoptk/aoptk_normalize_chemical.py rename to tools/aoptk/aoptk_chemical_normalization.py From 56c06041ea9c3a797961401b0bcc932de97f8d20 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Thu, 29 Jan 2026 10:17:00 +0100 Subject: [PATCH 024/173] Abstract download fixed for Europe PMC --- tools/aoptk/aoptk_download_abstracts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/aoptk/aoptk_download_abstracts.py b/tools/aoptk/aoptk_download_abstracts.py index 64925862..825614d1 100644 --- a/tools/aoptk/aoptk_download_abstracts.py +++ b/tools/aoptk/aoptk_download_abstracts.py @@ -22,7 +22,7 @@ def download_abstracts( pubmed.id_list = ids return pubmed.get_abstracts() if database == "europepmc": - europepmc = EuropePMC.__new__(EuropePMC) + europepmc = EuropePMC("") europepmc.id_list = ids return europepmc.get_abstracts() return None From 19d6d32c2a02c04076abaae7c8ddd6c6b088d0e8 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Thu, 29 Jan 2026 10:20:35 +0100 Subject: [PATCH 025/173] Renamed column names --- tools/aoptk/aoptk_parse_pdfs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/aoptk/aoptk_parse_pdfs.py b/tools/aoptk/aoptk_parse_pdfs.py index 400bfb6e..5aded8b1 100644 --- a/tools/aoptk/aoptk_parse_pdfs.py +++ b/tools/aoptk/aoptk_parse_pdfs.py @@ -29,7 +29,7 @@ def save_file(publications: list[Publication], output_file: str) -> None: """ with open(output_file, "w", newline="") as f_out: writer = csv.writer(f_out, delimiter="\t") - writer.writerow(["publication_id", "full_text", "figure_descriptions"]) + writer.writerow(["id", "text", "figure_descriptions"]) for publication in publications: full_text = publication.full_text.replace("\t", " ").replace("\n", " ") figure_descriptions = "|".join(publication.figure_descriptions) From ace2fe846a6692d3e5a49f9c24fef2a8394d4b25 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Thu, 29 Jan 2026 10:45:54 +0100 Subject: [PATCH 026/173] Script to find relationships --- tools/aoptk/aoptk_find_relationships.py | 52 +++++++++++++++++++++---- 1 file changed, 44 insertions(+), 8 deletions(-) diff --git a/tools/aoptk/aoptk_find_relationships.py b/tools/aoptk/aoptk_find_relationships.py index 06aa90d4..8077cea5 100644 --- a/tools/aoptk/aoptk_find_relationships.py +++ b/tools/aoptk/aoptk_find_relationships.py @@ -1,12 +1,17 @@ from aoptk.chemical import Chemical from aoptk.effect import Effect from aoptk.relationships.relationship import Relationship -from aoptk.relationships.zero_shot_classification_single import ZeroShotClassificationSingle -import argparse +from aoptk.relationships.zero_shot_classification_single import ( + ZeroShotClassificationSingle, +) import pandas as pd +import argparse +import os -def find_relationships(text: str, chemicals: list[Chemical], effects: list[Effect]) -> list[Relationship]: +def find_relationships( + text: str, chemicals: list[Chemical], effects: list[Effect] +) -> list[Relationship]: """Find relationships between chemicals and effects. Args: @@ -14,17 +19,48 @@ def find_relationships(text: str, chemicals: list[Chemical], effects: list[Effec chemicals (list[Chemical]): List of chemicals to consider. effects (list[Effect]): List of effects to consider. """ - return ZeroShotClassificationSingle().find_relationships(text=text, chemicals=chemicals, effects=effects) + return ZeroShotClassificationSingle().find_relationships( + text=text, chemicals=chemicals, effects=effects + ) + + +def save_file(input_file: str, output_file: str, effects: list[Effect]) -> None: + """Process a TSV file with chemicals and effects, find relationships, and save results. + + Args: + input_file (str): Path to input TSV file with chemicals and effects. + output_file (str): Path to output TSV file or directory. + """ + if os.path.isdir(output_file): + output_file = os.path.join(output_file, "relationships.tsv") + + with open(input_file, "r") as text_in, open(output_file, "w") as f_out: + f_out.write("id\tchemical\teffect\trelationship\n") + for row in pd.read_csv(text_in, sep="\t").itertuples(): + chemicals = [Chemical(chem) for chem in row.chemicals.split("|")] + relationships = find_relationships(row.text, chemicals, effects) + for relationship in relationships: + f_out.write( + f"{row.id}\t{relationship.chemical.name}\t{relationship.effect.name}\t{relationship}\n" + ) def parse_args() -> argparse.Namespace: - parser = argparse.ArgumentParser(description="Find relationships between chemicals and effects") - parser.add_argument("--text", required=True, help="Input text to analyze") - parser.add_argument("--chemicals", required=True, help="List of chemicals to consider") + parser = argparse.ArgumentParser( + description="Find relationships between chemicals and effects" + ) + parser.add_argument( + "--input_file", required=True, help="Input TSV file with chemicals and effects" + ) + parser.add_argument( + "--outdir", required=True, help="Output directory for saving files" + ) parser.add_argument("--effects", required=True, help="List of effects to consider") return parser.parse_args() if __name__ == "__main__": args = parse_args() - relationships = find_relationships(args.text, args.chemicals, args.effects) + effects = [Effect(eff) for eff in args.effects.split(",")] + output_file = os.path.join(args.outdir, "relationships.tsv") + save_file(args.input_file, output_file, effects) From 7e331f230dc87678422ae0ef433f9778b0ac8ea3 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Tue, 10 Feb 2026 17:22:24 +0100 Subject: [PATCH 027/173] Initial version chemical identification wrappers --- tools/aoptk/aoptk_chemical_identification.xml | 58 +++++++++++++++++++ tools/aoptk/test-data/text.tsv | 2 + 2 files changed, 60 insertions(+) create mode 100644 tools/aoptk/aoptk_chemical_identification.xml create mode 100644 tools/aoptk/test-data/text.tsv diff --git a/tools/aoptk/aoptk_chemical_identification.xml b/tools/aoptk/aoptk_chemical_identification.xml new file mode 100644 index 00000000..4811f252 --- /dev/null +++ b/tools/aoptk/aoptk_chemical_identification.xml @@ -0,0 +1,58 @@ + + Detect chemicals in scientific literature and filter based on a screening list. + + macros.xml + + + + + + + + + + + from aoptk.chemical import Chemical + from aoptk.spacy_processor import Spacy + import csv + import os + + with open($input_file, "r") as f_in, open($output_file, "w") as f_out: + f_out.write("id\ttext\tchemicals\n") + for row in csv.DictReader(f_in, delimiter="\t"): + chemicals = Spacy().find_chemical(row["text"]) + chemicals_str = ( + "|".join(set([chem.name for chem in chemicals])) if chemicals else "" + ) + f_out.write(f"{row['id']}\t{row['text']}\t{chemicals_str}\n") + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tools/aoptk/test-data/text.tsv b/tools/aoptk/test-data/text.tsv new file mode 100644 index 00000000..e05920dc --- /dev/null +++ b/tools/aoptk/test-data/text.tsv @@ -0,0 +1,2 @@ +id text +12355261 Paracetamol is a chemical. Thioacetamide is a chemical. \ No newline at end of file From 0bfc9c7a9c5383daa64ad96e748d0ac4da01f373 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Wed, 25 Feb 2026 12:05:43 +0100 Subject: [PATCH 028/173] Made find chemicals into xml file --- tools/aoptk/aoptk_chemical_identification.py | 50 ------------- tools/aoptk/aoptk_chemical_identification.xml | 58 --------------- tools/aoptk/aoptk_find_chemicals.xml | 70 +++++++++++++++++++ 3 files changed, 70 insertions(+), 108 deletions(-) delete mode 100644 tools/aoptk/aoptk_chemical_identification.py delete mode 100644 tools/aoptk/aoptk_chemical_identification.xml create mode 100644 tools/aoptk/aoptk_find_chemicals.xml diff --git a/tools/aoptk/aoptk_chemical_identification.py b/tools/aoptk/aoptk_chemical_identification.py deleted file mode 100644 index f97bc3d1..00000000 --- a/tools/aoptk/aoptk_chemical_identification.py +++ /dev/null @@ -1,50 +0,0 @@ -from aoptk.chemical import Chemical -from aoptk.spacy_processor import Spacy -import argparse -import csv -import os - - -def find_chemicals(text: str) -> list[Chemical]: - """Generate a list of chemicals from text. - - Args: - text (str): Text to identify chemicals in. - """ - return Spacy().find_chemical(text) - - -def save_file(input_file: str, output_file: str) -> None: - """Process a TSV file with text column, find chemicals, and save results. - - Args: - input_file (str): Path to input TSV file with 'text' column. - output_file (str): Path to output TSV file. - """ - with open(input_file, "r") as f_in, open(output_file, "w") as f_out: - f_out.write("id\ttext\tchemicals\n") - for row in csv.DictReader(f_in, delimiter="\t"): - chemicals = find_chemicals(row["text"]) - chemicals_str = ( - "|".join(set([chem.name for chem in chemicals])) if chemicals else "" - ) - f_out.write(f"{row['id']}\t{row['text']}\t{chemicals_str}\n") - - -def parse_args() -> argparse.Namespace: - parser = argparse.ArgumentParser( - description="Identify chemicals in a TSV file with text column" - ) - parser.add_argument( - "--input_file", required=True, help="Input TSV file with text column" - ) - parser.add_argument( - "--outdir", required=True, help="Output directory for saving results" - ) - return parser.parse_args() - - -if __name__ == "__main__": - args = parse_args() - output_file = os.path.join(args.outdir, "chemicals.tsv") - save_file(input_file=args.input_file, output_file=output_file) diff --git a/tools/aoptk/aoptk_chemical_identification.xml b/tools/aoptk/aoptk_chemical_identification.xml deleted file mode 100644 index 4811f252..00000000 --- a/tools/aoptk/aoptk_chemical_identification.xml +++ /dev/null @@ -1,58 +0,0 @@ - - Detect chemicals in scientific literature and filter based on a screening list. - - macros.xml - - - - - - - - - - - from aoptk.chemical import Chemical - from aoptk.spacy_processor import Spacy - import csv - import os - - with open($input_file, "r") as f_in, open($output_file, "w") as f_out: - f_out.write("id\ttext\tchemicals\n") - for row in csv.DictReader(f_in, delimiter="\t"): - chemicals = Spacy().find_chemical(row["text"]) - chemicals_str = ( - "|".join(set([chem.name for chem in chemicals])) if chemicals else "" - ) - f_out.write(f"{row['id']}\t{row['text']}\t{chemicals_str}\n") - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/tools/aoptk/aoptk_find_chemicals.xml b/tools/aoptk/aoptk_find_chemicals.xml new file mode 100644 index 00000000..21b0fb82 --- /dev/null +++ b/tools/aoptk/aoptk_find_chemicals.xml @@ -0,0 +1,70 @@ + + Detect chemicals in text. + + macros.xml + + + + + + + + + + +from aoptk.spacy_text_processor import SpacyText +from aoptk.literature.pdf import PDF +from aoptk.literature.pymupdf_parser import PymupdfParser +import os + +publication_id = os.path.splitext("$input_file.element_identifier")[0] +with open("chemicals.txt", "w") as f_out: + f_out.write("id\tchemicals\n") + + if "${input_file.ext}" == "pdf": + text = PymupdfParser([PDF("$input_file")]).get_publications()[0].full_text + chemicals = SpacyText().find_chemical(text) + chemicals_str = ( + "|".join(set([chem.name for chem in chemicals])) if chemicals else "" + ) + f_out.write(f"{publication_id}\t{chemicals_str}\n") + elif "${input_file.ext}" == "txt": + with open("$input_file", "r") as f_in: + text = f_in.read() + chemicals = SpacyText().find_chemical(text) + chemicals_str = ( + "|".join(set([chem.name for chem in chemicals])) if chemicals else "" + ) + f_out.write(f"{publication_id}\t{chemicals_str}\n") + else: + raise ValueError(f"Unsupported input file format: {input_ext}") + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From b6f33b5ef3024af13a36f20375bb3201dc1c3606 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Wed, 25 Feb 2026 12:05:59 +0100 Subject: [PATCH 029/173] Updated version --- tools/aoptk/macros.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/aoptk/macros.xml b/tools/aoptk/macros.xml index 5c0881e5..0f0eb3a4 100644 --- a/tools/aoptk/macros.xml +++ b/tools/aoptk/macros.xml @@ -1,5 +1,5 @@ - 0.1.6 + 0.2.0 0 From 75acb8ffdc0a405a49d18729949d9a2b4bae9c66 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Wed, 25 Feb 2026 12:06:24 +0100 Subject: [PATCH 030/173] Replaced tsv test file with txt --- tools/aoptk/test-data/text.tsv | 2 -- tools/aoptk/test-data/text.txt | 1 + 2 files changed, 1 insertion(+), 2 deletions(-) delete mode 100644 tools/aoptk/test-data/text.tsv create mode 100644 tools/aoptk/test-data/text.txt diff --git a/tools/aoptk/test-data/text.tsv b/tools/aoptk/test-data/text.tsv deleted file mode 100644 index e05920dc..00000000 --- a/tools/aoptk/test-data/text.tsv +++ /dev/null @@ -1,2 +0,0 @@ -id text -12355261 Paracetamol is a chemical. Thioacetamide is a chemical. \ No newline at end of file diff --git a/tools/aoptk/test-data/text.txt b/tools/aoptk/test-data/text.txt new file mode 100644 index 00000000..a0e33bad --- /dev/null +++ b/tools/aoptk/test-data/text.txt @@ -0,0 +1 @@ +Paracetamol is a chemical. Thioacetamide is a chemical. \ No newline at end of file From b401fd742cb6cdadec5e8c85d8339fad89da4c4e Mon Sep 17 00:00:00 2001 From: rdurnik Date: Wed, 25 Feb 2026 13:01:30 +0100 Subject: [PATCH 031/173] Changed extension --- tools/aoptk/aoptk_find_chemicals.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/aoptk/aoptk_find_chemicals.xml b/tools/aoptk/aoptk_find_chemicals.xml index 21b0fb82..aeb5091e 100644 --- a/tools/aoptk/aoptk_find_chemicals.xml +++ b/tools/aoptk/aoptk_find_chemicals.xml @@ -21,7 +21,7 @@ from aoptk.literature.pymupdf_parser import PymupdfParser import os publication_id = os.path.splitext("$input_file.element_identifier")[0] -with open("chemicals.txt", "w") as f_out: +with open("chemicals.tsv", "w") as f_out: f_out.write("id\tchemicals\n") if "${input_file.ext}" == "pdf": @@ -54,7 +54,7 @@ with open("chemicals.txt", "w") as f_out: - + From c893ba77d85c9fbf33a7eac0ffa77b9a383fc9d5 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Wed, 25 Feb 2026 15:24:06 +0100 Subject: [PATCH 032/173] Changed download abstracts into xml --- tools/aoptk/aoptk_download_abstracts.py | 71 ---------------------- tools/aoptk/aoptk_download_abstracts.xml | 77 ++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 71 deletions(-) delete mode 100644 tools/aoptk/aoptk_download_abstracts.py create mode 100644 tools/aoptk/aoptk_download_abstracts.xml diff --git a/tools/aoptk/aoptk_download_abstracts.py b/tools/aoptk/aoptk_download_abstracts.py deleted file mode 100644 index 825614d1..00000000 --- a/tools/aoptk/aoptk_download_abstracts.py +++ /dev/null @@ -1,71 +0,0 @@ -from aoptk.literature.databases.pubmed import PubMed -from aoptk.literature.databases.europepmc import EuropePMC -from aoptk.literature.abstract import Abstract -from Bio import Entrez - -import argparse - - -def download_abstracts( - database_with_ids_path: str, database: str, email: str -) -> list[Abstract]: - """Genereate a list of abstracts from the specified literature database. - - Args: - database_with_ids_path (str): Path to the file containing database IDs. - """ - with open(database_with_ids_path, "r") as f: - ids = [line.strip() for line in f.readlines()] - if database == "pubmed": - Entrez.email = email - pubmed = PubMed.__new__(PubMed) - pubmed.id_list = ids - return pubmed.get_abstracts() - if database == "europepmc": - europepmc = EuropePMC("") - europepmc.id_list = ids - return europepmc.get_abstracts() - return None - - -def save_file(abstracts: list[Abstract], filename: str) -> None: - """Save abstracts to a TSV file. - - Args: - abstracts (list[Abstract]): List of abstracts to save. - filename (str): Name of the output file. - """ - with open(filename, "w") as f: - f.write("id\ttext\n") - for abstract in abstracts: - f.write(f"{abstract.publication_id}\t{abstract.text}\n") - - -def parse_args() -> argparse.Namespace: - parser = argparse.ArgumentParser( - description="Download abstracts from PubMed or Europe PMC using aoptk" - ) - parser.add_argument( - "--database_with_ids", - required=True, - help="Path to the file containing database IDs", - ) - parser.add_argument( - "--database", - required=True, - choices=["pubmed", "europepmc"], - help="Database to query", - ) - parser.add_argument( - "--email", required=True, help="Email to comply with NCBI guidelines" - ) - parser.add_argument( - "--outdir", required=True, help="Output directory for saving files" - ) - return parser.parse_args() - - -if __name__ == "__main__": - args = parse_args() - abstracts = download_abstracts(args.database_with_ids, args.database, args.email) - save_file(abstracts, f"{args.outdir}/abstracts.tsv") diff --git a/tools/aoptk/aoptk_download_abstracts.xml b/tools/aoptk/aoptk_download_abstracts.xml new file mode 100644 index 00000000..5d07c726 --- /dev/null +++ b/tools/aoptk/aoptk_download_abstracts.xml @@ -0,0 +1,77 @@ + + Download abstracts for a list of publication IDs. + + macros.xml + + + + + + + + + + +from aoptk.literature.databases.pubmed import PubMed +from aoptk.literature.databases.europepmc import EuropePMC +from aoptk.literature.abstract import Abstract +from Bio import Entrez +import os + +with open("$input_file", "r") as f: + ids = [line.strip() for line in f.readlines()] +email = os.environ.get("EMAIL") + + +if "${literature_database}" == "pubmed": + Entrez.email = email + pubmed = PubMed.__new__(PubMed) + pubmed.id_list = ids + abstracts = pubmed.get_abstracts() +elif "${literature_database}" == "europepmc": + europepmc = EuropePMC("") + europepmc.id_list = ids + abstracts = europepmc.get_abstracts() +else: + raise ValueError("Select valid database.") + +for abstract in abstracts: + with open(f"{abstract.publication_id}.txt", "w") as f: + f.write(abstract.text) + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From 1adeee5688347abbae9ef3ec064542d55d980f2e Mon Sep 17 00:00:00 2001 From: rdurnik Date: Thu, 5 Mar 2026 08:47:11 +0100 Subject: [PATCH 033/173] Fixed download abstracts name --- tools/aoptk/aoptk_download_abstracts.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/aoptk/aoptk_download_abstracts.xml b/tools/aoptk/aoptk_download_abstracts.xml index 5d07c726..8af54e16 100644 --- a/tools/aoptk/aoptk_download_abstracts.xml +++ b/tools/aoptk/aoptk_download_abstracts.xml @@ -9,12 +9,12 @@ - + from aoptk.literature.databases.pubmed import PubMed from aoptk.literature.databases.europepmc import EuropePMC from aoptk.literature.abstract import Abstract From 2ea1ee441521d3fcd7ddea719a118018ec6ae527 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Thu, 5 Mar 2026 08:48:46 +0100 Subject: [PATCH 034/173] Fixed description --- tools/aoptk/aoptk_download_abstracts.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/aoptk/aoptk_download_abstracts.xml b/tools/aoptk/aoptk_download_abstracts.xml index 8af54e16..df0cd9f9 100644 --- a/tools/aoptk/aoptk_download_abstracts.xml +++ b/tools/aoptk/aoptk_download_abstracts.xml @@ -70,7 +70,7 @@ for abstract in abstracts: Chemical Identification =================== -Tool to identify chemical entities in a given text. +Tool to download publication abstracts. ]]> From d72834de3329c83bc1e070ff40bb9ccd9755ecae Mon Sep 17 00:00:00 2001 From: rdurnik Date: Thu, 5 Mar 2026 09:44:56 +0100 Subject: [PATCH 035/173] Download abstracts fixes --- tools/aoptk/aoptk_download_abstracts.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tools/aoptk/aoptk_download_abstracts.xml b/tools/aoptk/aoptk_download_abstracts.xml index df0cd9f9..53083eb3 100644 --- a/tools/aoptk/aoptk_download_abstracts.xml +++ b/tools/aoptk/aoptk_download_abstracts.xml @@ -46,16 +46,16 @@ for abstract in abstracts: - + - + - + @@ -67,7 +67,7 @@ for abstract in abstracts: Date: Thu, 5 Mar 2026 09:45:15 +0100 Subject: [PATCH 036/173] Download PDF as XML --- tools/aoptk/aoptk_download_pdf.xml | 67 ++++++++++++++++++++++++++++++ tools/aoptk/aoptk_download_pdfs.py | 42 ------------------- 2 files changed, 67 insertions(+), 42 deletions(-) create mode 100644 tools/aoptk/aoptk_download_pdf.xml delete mode 100644 tools/aoptk/aoptk_download_pdfs.py diff --git a/tools/aoptk/aoptk_download_pdf.xml b/tools/aoptk/aoptk_download_pdf.xml new file mode 100644 index 00000000..997d8796 --- /dev/null +++ b/tools/aoptk/aoptk_download_pdf.xml @@ -0,0 +1,67 @@ + + Download PDFs for a list of publication IDs. + + macros.xml + + + + + + + + + + +from aoptk.literature.databases.pubmed import PubMed +from aoptk.literature.databases.europepmc import EuropePMC +from aoptk.literature.abstract import Abstract +from Bio import Entrez +import os + +with open("$input_file", "r") as f: + ids = [line.strip() for line in f.readlines()] + +if "${literature_database}" == "europepmc": + europepmc = EuropePMC("", storage = "./") + europepmc.id_list = ids + pdfs = europepmc.pdfs() + print(pdfs) + print([pdf.path for pdf in pdfs]) +else: + raise ValueError("Select valid database.") + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tools/aoptk/aoptk_download_pdfs.py b/tools/aoptk/aoptk_download_pdfs.py deleted file mode 100644 index bfd2642f..00000000 --- a/tools/aoptk/aoptk_download_pdfs.py +++ /dev/null @@ -1,42 +0,0 @@ -from aoptk.literature.databases.europepmc import EuropePMC -from aoptk.literature.abstract import Abstract - -import argparse - - -def download_pdfs( - database_with_ids_path: str, - output_dir: str, -) -> list[Abstract]: - """Genereate a list of abstracts from the specified literature database. - - Args: - database_with_ids_path (str): Path to the file containing database IDs. - """ - with open(database_with_ids_path, "r") as f: - ids = [line.strip() for line in f.readlines()] - europepmc = EuropePMC.__new__(EuropePMC) - europepmc.__init__("") - europepmc.storage = output_dir - europepmc.id_list = ids - return europepmc.pdfs() - - -def parse_args() -> argparse.Namespace: - parser = argparse.ArgumentParser( - description="Download abstracts from PubMed or Europe PMC using aoptk" - ) - parser.add_argument( - "--database_with_ids", - required=True, - help="Path to the file containing database IDs", - ) - parser.add_argument( - "--outdir", required=True, help="Output directory for saving files" - ) - return parser.parse_args() - - -if __name__ == "__main__": - args = parse_args() - abstracts = download_pdfs(args.database_with_ids, args.outdir) From a63a4a8f2f24d4a9ad0ecfb5f51a582a3e312f5e Mon Sep 17 00:00:00 2001 From: rdurnik Date: Thu, 5 Mar 2026 09:45:45 +0100 Subject: [PATCH 037/173] Removed print --- tools/aoptk/aoptk_download_pdf.xml | 2 -- 1 file changed, 2 deletions(-) diff --git a/tools/aoptk/aoptk_download_pdf.xml b/tools/aoptk/aoptk_download_pdf.xml index 997d8796..3ae0675e 100644 --- a/tools/aoptk/aoptk_download_pdf.xml +++ b/tools/aoptk/aoptk_download_pdf.xml @@ -28,8 +28,6 @@ if "${literature_database}" == "europepmc": europepmc = EuropePMC("", storage = "./") europepmc.id_list = ids pdfs = europepmc.pdfs() - print(pdfs) - print([pdf.path for pdf in pdfs]) else: raise ValueError("Select valid database.") From 219fa0c3373a06ab8c0b2f180114f0b78177fd01 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Thu, 5 Mar 2026 11:23:38 +0100 Subject: [PATCH 038/173] Updated text data set --- tools/aoptk/test-data/text.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/aoptk/test-data/text.txt b/tools/aoptk/test-data/text.txt index a0e33bad..5b5f97da 100644 --- a/tools/aoptk/test-data/text.txt +++ b/tools/aoptk/test-data/text.txt @@ -1 +1 @@ -Paracetamol is a chemical. Thioacetamide is a chemical. \ No newline at end of file +Thioacetamide is a chemical that causes liver fibrosis. \ No newline at end of file From 722fead057139e8029bec84a57c70e15eab7e84a Mon Sep 17 00:00:00 2001 From: rdurnik Date: Thu, 5 Mar 2026 11:23:55 +0100 Subject: [PATCH 039/173] IDs test data set --- tools/aoptk/test-data/ids.txt | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 tools/aoptk/test-data/ids.txt diff --git a/tools/aoptk/test-data/ids.txt b/tools/aoptk/test-data/ids.txt new file mode 100644 index 00000000..45504114 --- /dev/null +++ b/tools/aoptk/test-data/ids.txt @@ -0,0 +1,2 @@ +41480994 +PMC12930283 \ No newline at end of file From 6e58b46bb8c8bf3c2a8c2c4fa1135b3dae758961 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Thu, 5 Mar 2026 11:42:11 +0100 Subject: [PATCH 040/173] Find relationships XML --- tools/aoptk/aoptk_find_relationships.py | 66 ------------------- tools/aoptk/aoptk_find_relationships.xml | 84 ++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 66 deletions(-) delete mode 100644 tools/aoptk/aoptk_find_relationships.py create mode 100644 tools/aoptk/aoptk_find_relationships.xml diff --git a/tools/aoptk/aoptk_find_relationships.py b/tools/aoptk/aoptk_find_relationships.py deleted file mode 100644 index 8077cea5..00000000 --- a/tools/aoptk/aoptk_find_relationships.py +++ /dev/null @@ -1,66 +0,0 @@ -from aoptk.chemical import Chemical -from aoptk.effect import Effect -from aoptk.relationships.relationship import Relationship -from aoptk.relationships.zero_shot_classification_single import ( - ZeroShotClassificationSingle, -) -import pandas as pd -import argparse -import os - - -def find_relationships( - text: str, chemicals: list[Chemical], effects: list[Effect] -) -> list[Relationship]: - """Find relationships between chemicals and effects. - - Args: - text (str): Input text to analyze. - chemicals (list[Chemical]): List of chemicals to consider. - effects (list[Effect]): List of effects to consider. - """ - return ZeroShotClassificationSingle().find_relationships( - text=text, chemicals=chemicals, effects=effects - ) - - -def save_file(input_file: str, output_file: str, effects: list[Effect]) -> None: - """Process a TSV file with chemicals and effects, find relationships, and save results. - - Args: - input_file (str): Path to input TSV file with chemicals and effects. - output_file (str): Path to output TSV file or directory. - """ - if os.path.isdir(output_file): - output_file = os.path.join(output_file, "relationships.tsv") - - with open(input_file, "r") as text_in, open(output_file, "w") as f_out: - f_out.write("id\tchemical\teffect\trelationship\n") - for row in pd.read_csv(text_in, sep="\t").itertuples(): - chemicals = [Chemical(chem) for chem in row.chemicals.split("|")] - relationships = find_relationships(row.text, chemicals, effects) - for relationship in relationships: - f_out.write( - f"{row.id}\t{relationship.chemical.name}\t{relationship.effect.name}\t{relationship}\n" - ) - - -def parse_args() -> argparse.Namespace: - parser = argparse.ArgumentParser( - description="Find relationships between chemicals and effects" - ) - parser.add_argument( - "--input_file", required=True, help="Input TSV file with chemicals and effects" - ) - parser.add_argument( - "--outdir", required=True, help="Output directory for saving files" - ) - parser.add_argument("--effects", required=True, help="List of effects to consider") - return parser.parse_args() - - -if __name__ == "__main__": - args = parse_args() - effects = [Effect(eff) for eff in args.effects.split(",")] - output_file = os.path.join(args.outdir, "relationships.tsv") - save_file(args.input_file, output_file, effects) diff --git a/tools/aoptk/aoptk_find_relationships.xml b/tools/aoptk/aoptk_find_relationships.xml new file mode 100644 index 00000000..cf804842 --- /dev/null +++ b/tools/aoptk/aoptk_find_relationships.xml @@ -0,0 +1,84 @@ + + Detect relationships between chemicals and effects in text. + + macros.xml + + + + + + + + + + +from aoptk.spacy_text_processor import SpacyText +from aoptk.relationships.zero_shot_classification_single import ZeroShotClassificationSingle +from aoptk.literature.pdf import PDF +from aoptk.literature.pymupdf_parser import PymupdfParser +import os +import sys + +os.environ.setdefault("TORCHINDUCTOR_CACHE_DIR", os.path.join(os.getcwd(), ".torchinductor_cache")) +os.makedirs(os.environ["TORCHINDUCTOR_CACHE_DIR"], exist_ok=True) + +publication_id = os.path.splitext("$input_file.element_identifier")[0] +effects = ["${effect}"] +email = os.environ.get("EMAIL") + +with open("relationships.tsv", "w") as f_out: + f_out.write("id\tchemical\teffect\trelationship\n") + + if "${input_file.ext}" == "pdf": + text = PymupdfParser([PDF("$input_file")]).get_publications()[0].full_text + chemicals = SpacyText().find_chemical(text) + relationships = ZeroShotClassificationSingle().find_relationships( + text=text, chemicals=chemicals, effects=effects + ) + for relationship in relationships: + f_out.write(f"{publication_id}\t{relationship.chemical}\t{relationship.effect}\t{relationship.relationship_type}\n") + + elif "${input_file.ext}" == "txt": + with open("$input_file", "r") as f_in: + text = f_in.read() + chemicals = SpacyText().find_chemical(text) + relationships = ZeroShotClassificationSingle().find_relationships( + text=text, chemicals=chemicals, effects=effects + ) + for relationship in relationships: + f_out.write(f"{publication_id}\t{relationship.chemical}\t{relationship.effect}\t{relationship.relationship_type}\n") + else: + raise ValueError(f"Unsupported input file format: ${input_file.ext}") + + + + + + + [0-9a-zA-Z]+ + + + + + + + + + + + + + + + + + \ No newline at end of file From 5e09fd9e90dd1b45c5541c4cc1e94edcd4fb70cf Mon Sep 17 00:00:00 2001 From: rdurnik Date: Thu, 5 Mar 2026 11:42:21 +0100 Subject: [PATCH 041/173] Removed MeSH term generation --- tools/aoptk/aoptk_generate_mesh_terms_df.py | 33 --------------------- 1 file changed, 33 deletions(-) delete mode 100644 tools/aoptk/aoptk_generate_mesh_terms_df.py diff --git a/tools/aoptk/aoptk_generate_mesh_terms_df.py b/tools/aoptk/aoptk_generate_mesh_terms_df.py deleted file mode 100644 index 9edb83d4..00000000 --- a/tools/aoptk/aoptk_generate_mesh_terms_df.py +++ /dev/null @@ -1,33 +0,0 @@ -from aoptk.normalization.provide_mesh_term_dataframe_from_xml import ( - ProvideMeshTermDataframeFromXML, -) -import argparse -import pandas as pd -import os - - -def provide_normalization_dataframe(xml_path: str) -> pd.DataFrame: - """Generate a MeSH terms dataframe from an XML file. - - Args: - xml_path (str): Path to the MeSH XML file. - """ - return ProvideMeshTermDataframeFromXML(xml_path).provide_normalization_dataframe() - - -def parse_args() -> argparse.Namespace: - parser = argparse.ArgumentParser( - description="Generate MeSH terms dataframe from XML" - ) - parser.add_argument("--xml_path", required=True, help="Path to the MeSH XML file") - parser.add_argument( - "--outdir", required=True, help="Output directory for saving results" - ) - return parser.parse_args() - - -if __name__ == "__main__": - args = parse_args() - mesh_terms_df = provide_normalization_dataframe(args.xml_path) - output_file = os.path.join(args.outdir, "mesh_terms.tsv") - mesh_terms_df.to_csv(output_file, sep="\t", index=False) From 972f9b0dde3e56f6564f9c57ec1db77367f41bef Mon Sep 17 00:00:00 2001 From: rdurnik Date: Thu, 5 Mar 2026 15:12:21 +0100 Subject: [PATCH 042/173] PDF parsing XML --- tools/aoptk/aoptk_parse_pdf.xml | 57 +++++++++++++++++++++++++++ tools/aoptk/aoptk_parse_pdfs.py | 70 --------------------------------- 2 files changed, 57 insertions(+), 70 deletions(-) create mode 100644 tools/aoptk/aoptk_parse_pdf.xml delete mode 100644 tools/aoptk/aoptk_parse_pdfs.py diff --git a/tools/aoptk/aoptk_parse_pdf.xml b/tools/aoptk/aoptk_parse_pdf.xml new file mode 100644 index 00000000..64d55bea --- /dev/null +++ b/tools/aoptk/aoptk_parse_pdf.xml @@ -0,0 +1,57 @@ + + Detect chemicals in text. + + macros.xml + + + + + + + + + + +from aoptk.literature.pdf import PDF +from aoptk.literature.pymupdf_parser import PymupdfParser +import os + +publication_id = os.path.splitext("$input_file.element_identifier")[0] +if "${input_file.ext}" == "pdf": + + pdf = PymupdfParser([PDF("$input_file")]).get_publications() + text = pdf[0].full_text + with open(f"{publication_id}.txt", "w") as f: + f.write(text) + +else: + raise ValueError(f"Unsupported input file format: ${input_file.ext}") + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tools/aoptk/aoptk_parse_pdfs.py b/tools/aoptk/aoptk_parse_pdfs.py deleted file mode 100644 index 5aded8b1..00000000 --- a/tools/aoptk/aoptk_parse_pdfs.py +++ /dev/null @@ -1,70 +0,0 @@ -from aoptk.literature.publication import Publication -from aoptk.literature.pdf import PDF -from aoptk.literature.pymupdf_parser import PymupdfParser - - -import argparse -import os -import csv - - -def parse_pdfs( - folder_with_pdfs: list[PDF], figures_output_dir: str -) -> list[Publication]: - """Genereate a list of abstracts from the specified literature database. - - Args: - folder_with_pdfs (list[PDF]): List of PDF objects representing the folder with PDFs. - figures_output_dir (str): Directory to save extracted figures. - """ - return PymupdfParser(folder_with_pdfs, figures_output_dir).get_publications() - - -def save_file(publications: list[Publication], output_file: str) -> None: - """Process a list of publications and save results. - - Args: - publications (list[Publication]): List of publications to save. - output_file (str): Path to output file. - """ - with open(output_file, "w", newline="") as f_out: - writer = csv.writer(f_out, delimiter="\t") - writer.writerow(["id", "text", "figure_descriptions"]) - for publication in publications: - full_text = publication.full_text.replace("\t", " ").replace("\n", " ") - figure_descriptions = "|".join(publication.figure_descriptions) - figure_descriptions = figure_descriptions.replace("\t", " ").replace( - "\n", " " - ) - writer.writerow([publication.id, full_text, figure_descriptions]) - - -def parse_args() -> argparse.Namespace: - parser = argparse.ArgumentParser( - description="Download abstracts from PubMed or Europe PMC using aoptk" - ) - parser.add_argument( - "--folder_with_pdfs", - required=True, - help="Path to the folder containing PDFs", - ) - parser.add_argument( - "--figures_output_dir", - required=True, - help="Output directory for saving extracted figures", - ) - parser.add_argument( - "--outdir", required=True, help="Output directory for saving files" - ) - return parser.parse_args() - - -if __name__ == "__main__": - args = parse_args() - pdf_files = [ - PDF(os.path.join(args.folder_with_pdfs, f)) - for f in os.listdir(args.folder_with_pdfs) - if f.lower().endswith(".pdf") - ] - publications = parse_pdfs(pdf_files, args.figures_output_dir) - save_file(publications, f"{args.outdir}/parsed_publications.tsv") From 589c8ad6b0cde72899e7e145b4b8c19c2e28497d Mon Sep 17 00:00:00 2001 From: rdurnik Date: Thu, 5 Mar 2026 15:29:00 +0100 Subject: [PATCH 043/173] Query literature XML --- tools/aoptk/aoptk_query.py | 70 -------------------------- tools/aoptk/aoptk_query_literature.xml | 66 ++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 70 deletions(-) delete mode 100644 tools/aoptk/aoptk_query.py create mode 100644 tools/aoptk/aoptk_query_literature.xml diff --git a/tools/aoptk/aoptk_query.py b/tools/aoptk/aoptk_query.py deleted file mode 100644 index 98c23c2a..00000000 --- a/tools/aoptk/aoptk_query.py +++ /dev/null @@ -1,70 +0,0 @@ -from Bio import Entrez -from aoptk.literature.databases.pubmed import PubMed -from aoptk.literature.databases.europepmc import EuropePMC -import argparse - - -def generate_database_with_ids( - query: str, database: str, email: str -) -> EuropePMC | PubMed | None: - """Generate an object with IDs from the specified literature database. - - Args: - query (str): Search term for PubMed or Europe PMC. - database (str): Database to search: PubMed or Europe PMC. - email (str): Email address to follow PubMed - NCBI guidelines. - """ - if database == "pubmed": - Entrez.email = email - pubmed = PubMed(query) - ids = pubmed.get_id() - pubmed.id_list = ids - return pubmed - if database == "europepmc": - europepmc = EuropePMC(query) - ids = europepmc.get_id() - europepmc.id_list = ids - return europepmc - return None - - -def save_file(database_with_ids: EuropePMC | PubMed, filename: str) -> None: - """Save the IDs to a text file. - - Args: - database_with_ids (EuropePMC | PubMed): Object containing the IDs. - filename (str): Name of the output file. - """ - with open(filename, "w") as f: - for id_ in database_with_ids.id_list: - f.write(f"{id_}\n") - - -def parse_args() -> argparse.Namespace: - parser = argparse.ArgumentParser( - description="Fetch PubMed or Europe PMC IDs using aoptk" - ) - parser.add_argument( - "--query", required=True, help="Search term as used in PubMed/Europe PMC" - ) - parser.add_argument( - "--database", - required=True, - choices=["pubmed", "europepmc"], - help="Database to query", - ) - parser.add_argument( - "--email", required=True, help="Email to comply with NCBI guidelines" - ) - parser.add_argument( - "--outdir", required=True, help="Output directory for saving files" - ) - return parser.parse_args() - - -if __name__ == "__main__": - args = parse_args() - database_with_ids = generate_database_with_ids( - args.query, args.database, args.email - ) - save_file(database_with_ids, f"{args.outdir}/ids.txt") diff --git a/tools/aoptk/aoptk_query_literature.xml b/tools/aoptk/aoptk_query_literature.xml new file mode 100644 index 00000000..4a9ee7d7 --- /dev/null +++ b/tools/aoptk/aoptk_query_literature.xml @@ -0,0 +1,66 @@ + + Query literature for a list of publication IDs. + + macros.xml + + + + + + + + + + +from aoptk.literature.databases.pubmed import PubMed +from aoptk.literature.databases.europepmc import EuropePMC +from Bio import Entrez +import os + +email = os.environ.get("EMAIL") + + +if "${literature_database}" == "pubmed": + Entrez.email = email + ids = PubMed("${query}").get_id() +elif "${literature_database}" == "europepmc": + ids = EuropePMC("${query}").get_id() + +else: + raise ValueError("Select valid database.") + +with open(f"ids.txt", "w") as f: + for id in ids: + f.write(f"{id}\n") + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From 96c7a70a4da9fabdadcb61ff9090671e4bf5d23b Mon Sep 17 00:00:00 2001 From: rdurnik Date: Thu, 5 Mar 2026 15:51:49 +0100 Subject: [PATCH 044/173] Changed chemical identification to output one chemical per line --- tools/aoptk/aoptk_find_chemicals.xml | 32 ++++++++++++---------------- 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/tools/aoptk/aoptk_find_chemicals.xml b/tools/aoptk/aoptk_find_chemicals.xml index aeb5091e..054ed62c 100644 --- a/tools/aoptk/aoptk_find_chemicals.xml +++ b/tools/aoptk/aoptk_find_chemicals.xml @@ -21,26 +21,22 @@ from aoptk.literature.pymupdf_parser import PymupdfParser import os publication_id = os.path.splitext("$input_file.element_identifier")[0] -with open("chemicals.tsv", "w") as f_out: - f_out.write("id\tchemicals\n") - if "${input_file.ext}" == "pdf": - text = PymupdfParser([PDF("$input_file")]).get_publications()[0].full_text +if "${input_file.ext}" == "pdf": + text = PymupdfParser([PDF("$input_file")]).get_publications()[0].full_text + chemicals = SpacyText().find_chemical(text) + +elif "${input_file.ext}" == "txt": + with open("$input_file", "r") as f_in: + text = f_in.read() chemicals = SpacyText().find_chemical(text) - chemicals_str = ( - "|".join(set([chem.name for chem in chemicals])) if chemicals else "" - ) - f_out.write(f"{publication_id}\t{chemicals_str}\n") - elif "${input_file.ext}" == "txt": - with open("$input_file", "r") as f_in: - text = f_in.read() - chemicals = SpacyText().find_chemical(text) - chemicals_str = ( - "|".join(set([chem.name for chem in chemicals])) if chemicals else "" - ) - f_out.write(f"{publication_id}\t{chemicals_str}\n") - else: - raise ValueError(f"Unsupported input file format: {input_ext}") +else: + raise ValueError(f"Unsupported input file format: {input_ext}") + +with open("chemicals.tsv", "w") as f_out: + f_out.write("id\tchemical\n") + for chemical in chemicals: + f_out.write(f"{publication_id}\t{chemical.name}\n") From fb0df480d5973db2eacbccbb4e3273d5baffa906 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Thu, 5 Mar 2026 17:41:08 +0100 Subject: [PATCH 045/173] Chemical normalization --- tools/aoptk/aoptk_chemical_normalization.py | 54 ------------------- tools/aoptk/aoptk_chemical_normalization.xml | 56 ++++++++++++++++++++ tools/aoptk/test-data/chemicals.tsv | 4 ++ 3 files changed, 60 insertions(+), 54 deletions(-) delete mode 100644 tools/aoptk/aoptk_chemical_normalization.py create mode 100644 tools/aoptk/aoptk_chemical_normalization.xml create mode 100644 tools/aoptk/test-data/chemicals.tsv diff --git a/tools/aoptk/aoptk_chemical_normalization.py b/tools/aoptk/aoptk_chemical_normalization.py deleted file mode 100644 index d3411b78..00000000 --- a/tools/aoptk/aoptk_chemical_normalization.py +++ /dev/null @@ -1,54 +0,0 @@ -from aoptk.chemical import Chemical -from aoptk.normalization.mesh_terms import MeshTerms -import argparse -import pandas as pd -import os - - -def normalize_chemical(mesh_terms: pd.DataFrame, chemical: Chemical) -> Chemical: - """Normalize a chemical using MeSH terms. - - Args: - mesh_terms (pd.DataFrame): MeSH terms dataframe. - chemical (Chemical): Chemical to normalize. - """ - return MeshTerms(mesh_terms).normalize_chemical(chemical) - - -def save_file(input_file: str, mesh_terms_df: pd.DataFrame, output_file: str) -> None: - """Process a TSV file with chemicals, normalize them, and save results. - - Args: - input_file (str): Path to input TSV file with chemicals. - mesh_terms_df (pd.DataFrame): MeSH terms dataframe for normalization. - output_file (str): Path to output TSV file. - """ - with open(input_file, "r") as f_in, open(output_file, "w") as f_out: - f_out.write("name\theading\tsynonyms\n") - for row in pd.read_csv(f_in, sep="\t").itertuples(): - chemicals = row.chemicals.split("|") - for chem in chemicals: - chemical = Chemical(chem.strip()) - normalized_chemical = normalize_chemical(mesh_terms_df, chemical) - f_out.write( - f"{normalized_chemical.name}\t{normalized_chemical.heading}\t{normalized_chemical.synonyms}\n" - ) - - -def parse_args() -> argparse.Namespace: - parser = argparse.ArgumentParser(description="Normalize chemicals using MeSH terms") - parser.add_argument("--mesh_terms", required=True, help="MeSH terms dataframe") - parser.add_argument( - "--input_file", required=True, help="Input TSV file with chemicals" - ) - parser.add_argument( - "--outdir", required=True, help="Output directory for saving results" - ) - return parser.parse_args() - - -if __name__ == "__main__": - args = parse_args() - mesh_terms_df = pd.read_csv(args.mesh_terms, sep="\t") - output_file = os.path.join(args.outdir, "normalized_chemicals.tsv") - save_file(args.input_file, mesh_terms_df, output_file) diff --git a/tools/aoptk/aoptk_chemical_normalization.xml b/tools/aoptk/aoptk_chemical_normalization.xml new file mode 100644 index 00000000..89259b6f --- /dev/null +++ b/tools/aoptk/aoptk_chemical_normalization.xml @@ -0,0 +1,56 @@ + + Normalize chemical entities. + + macros.xml + + + + + + + + + + +from aoptk.normalization.pubchem_api import PubChemAPI +from aoptk.chemical import Chemical +import os +import pandas as pd + +chemicals_df = pd.read_csv("$input_file", sep="\t") +chemicals_df["chemical"] = chemicals_df["chemical"].apply( + lambda x: PubChemAPI().normalize_chemical(Chemical(x)) + ) +chemicals_df["heading"] = chemicals_df["chemical"].apply(lambda chem: chem.heading) +chemicals_df.to_csv("normalized_chemicals.tsv", sep="\t", index=False) + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tools/aoptk/test-data/chemicals.tsv b/tools/aoptk/test-data/chemicals.tsv new file mode 100644 index 00000000..a373f17c --- /dev/null +++ b/tools/aoptk/test-data/chemicals.tsv @@ -0,0 +1,4 @@ +id chemical +123 thioacetamide +456 acetaminophen +789 paracetamol \ No newline at end of file From 67cebd201bcfdbcfd4b6609e87ba96c8956085fc Mon Sep 17 00:00:00 2001 From: rdurnik Date: Thu, 5 Mar 2026 18:05:42 +0100 Subject: [PATCH 046/173] Matching chemicals --- tools/aoptk/aoptk_chemical_matching.py | 92 ------------------------- tools/aoptk/aoptk_chemical_matching.xml | 63 +++++++++++++++++ tools/aoptk/test-data/normalized.tsv | 4 ++ 3 files changed, 67 insertions(+), 92 deletions(-) delete mode 100644 tools/aoptk/aoptk_chemical_matching.py create mode 100644 tools/aoptk/aoptk_chemical_matching.xml create mode 100644 tools/aoptk/test-data/normalized.tsv diff --git a/tools/aoptk/aoptk_chemical_matching.py b/tools/aoptk/aoptk_chemical_matching.py deleted file mode 100644 index a540bcd8..00000000 --- a/tools/aoptk/aoptk_chemical_matching.py +++ /dev/null @@ -1,92 +0,0 @@ -from aoptk.chemical import Chemical -import argparse -import pandas as pd - - -def extract_chemicals_to_match(input_file: str) -> list[Chemical]: - chemicals = [] - with open(input_file, "r") as f_in: - for row in pd.read_csv(f_in, sep="\t").itertuples(): - chemical = Chemical(row.name) - chemical.heading = row.heading - chemical._synonyms = ( - set(row.synonyms.split(";")) if pd.notna(row.synonyms) else set() - ) - chemicals.append(chemical) - return chemicals - - -def match_chemicals_with_loose_equality( - list_of_relevant_chemicals: list[Chemical], - chemicals: list[Chemical], -) -> list[str]: - """Match normalized chemicals with relevant chemicals using loose equality. - - Args: - list_of_relevant_chemicals (list[Chemical]): List of relevant chemicals. - chemicals (list[Chemical]): List of chemicals. - """ - relevant_chemicals_names = [] - for chemical in chemicals: - for relevant_chemical in list_of_relevant_chemicals: - if chemical.similar(relevant_chemical): - relevant_chemicals_names.append(chemical.name) - break - return relevant_chemicals_names - - -def generate_relevant_chemicals(chemical_database: str) -> list[Chemical]: - """Generate a list of relevant chemicals from Excel file. - - Args: - chemical_database (str): Path to the user-defined chemical database in Excel. - """ - relevant_chemicals_database = pd.read_excel(chemical_database) - return [ - Chemical(name) - for name in relevant_chemicals_database["chemical_name"] - .astype(str) - .str.lower() - .unique() - ] - - -def save_file(relevant_chemicals_names: list[str], output_file: str) -> None: - """Process a TSV file with chemicals, match them, and save results. - - Args: - input_file (str): Path to input TSV file with chemicals. - output_file (str): Path to output TSV file. - """ - with open(output_file, "w") as f_out: - f_out.write("matched_chemicals\n") - for chemical_name in relevant_chemicals_names: - f_out.write(f"{chemical_name}\n") - - -def parse_args() -> argparse.Namespace: - parser = argparse.ArgumentParser(description="Match chemicals using loose equality") - parser.add_argument( - "--list_of_relevant_chemicals", required=True, help="List of relevant chemicals" - ) - parser.add_argument( - "--normalized_chemicals", required=True, help="List of normalized chemicals" - ) - parser.add_argument( - "--outdir", required=True, help="Output directory for saving files" - ) - return parser.parse_args() - - -if __name__ == "__main__": - args = parse_args() - chemicals_to_match = extract_chemicals_to_match(args.normalized_chemicals) - list_of_relevant_chemicals = generate_relevant_chemicals( - args.list_of_relevant_chemicals - ) - relevant_chemicals_names = match_chemicals_with_loose_equality( - list_of_relevant_chemicals, chemicals_to_match - ) - save_file( - relevant_chemicals_names, output_file=f"{args.outdir}/matched_chemicals.tsv" - ) diff --git a/tools/aoptk/aoptk_chemical_matching.xml b/tools/aoptk/aoptk_chemical_matching.xml new file mode 100644 index 00000000..e644f492 --- /dev/null +++ b/tools/aoptk/aoptk_chemical_matching.xml @@ -0,0 +1,63 @@ + + Match chemical entities. + + macros.xml + + + + + + + + + + +from aoptk.spacy_text_processor import SpacyText +from aoptk.literature.pdf import PDF +from aoptk.literature.pymupdf_parser import PymupdfParser +from aoptk.chemical import Chemical +import os +import pandas as pd + +chemicals_df_1 = pd.read_csv("$input_file_1", sep="\t") +chemicals_df_2 = pd.read_csv("$input_file_2", sep="\t") +merged_files = chemicals_df_1.merge( + chemicals_df_2, + left_on="heading", + right_on="heading", + how="outer", +) +merged_files.to_csv("merged_chemicals.tsv", sep="\t", index=False) + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tools/aoptk/test-data/normalized.tsv b/tools/aoptk/test-data/normalized.tsv new file mode 100644 index 00000000..774b0683 --- /dev/null +++ b/tools/aoptk/test-data/normalized.tsv @@ -0,0 +1,4 @@ +heading chemical +thioacetamide thioacetamide +acetaminophen acetaminophen +acetaminophen paracetamol \ No newline at end of file From 9d05d1007a1c1e38321a51d89641da049ab602d5 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Fri, 6 Mar 2026 10:26:03 +0100 Subject: [PATCH 047/173] Description fix --- tools/aoptk/.shed.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/aoptk/.shed.yml b/tools/aoptk/.shed.yml index a44aadf1..09ea9419 100644 --- a/tools/aoptk/.shed.yml +++ b/tools/aoptk/.shed.yml @@ -4,13 +4,13 @@ remote_repository_url: "https://github.com/rdurnik/aoptk" homepage_url: "https://github.com/rdurnik/aoptk" categories: - Machine Learning -description: "AOP-toolkit (aoptk) is a Python package designed to support the development of Adverse Outcome Pathways (AOPs) that require extensive data mining." +description: "AOP-toolkit (aoptk) is a Python package designed to support data mining and analysis of toxicological outcomes." long_description: | - "AOP-toolkit (aoptk) is a Python package developed to support the construction of Adverse Outcome Pathways (AOPs) that require extensive mining and integration of toxicological data from heterogeneous sources. It enables researchers to collect literature from databases such as PubMed and Europe PMC, extract relevant information from full-text publications, and analyze complex, unstructured data using large language models. The toolkit also provides functionality for normalizing chemical names across publications, helping ensure consistency and interoperability." + "AOP-toolkit (aoptk) is a Python package developed to support data mining and analysis of toxicological outcomes. Specifically, it supports the construction of Adverse Outcome Pathways (AOPs), which require extensive mining and integration of toxicological data from heterogeneous sources. It enables researchers to collect literature from databases such as PubMed and Europe PMC, extract relevant information from full-text publications, and analyze complex, unstructured data using large language models. The toolkit also provides functionality for normalizing chemical names across publications, helping ensure consistency and interoperability." auto_tool_repositories: name_template: "{{ tool_id }}" description_template: "{{ tool_name }} tool from the aoptk package" suite: name: suite_aoptk - description: AOP-toolkit (aoptk) is a Python package developed to support the construction of Adverse Outcome Pathways (AOPs) that require extensive mining and integration of toxicological data from heterogeneous sources. + description: AOP-toolkit (aoptk) is a Python package developed to support data mining and analysis of toxicological outcomes. Specifically, it supports the construction of Adverse Outcome Pathways (AOPs), which require extensive mining and integration of toxicological data from heterogeneous sources. It enables researchers to collect literature from databases such as PubMed and Europe PMC, extract relevant information from full-text publications, and analyze complex, unstructured data using large language models. The toolkit also provides functionality for normalizing chemical names across publications, helping ensure consistency and interoperability. type: repository_suite_definition \ No newline at end of file From 9320772b3509573981613333d8142a996cc45050 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Fri, 6 Mar 2026 11:04:25 +0100 Subject: [PATCH 048/173] Needs to be defined before the other imports --- tools/aoptk/aoptk_find_relationships.xml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tools/aoptk/aoptk_find_relationships.xml b/tools/aoptk/aoptk_find_relationships.xml index cf804842..3c696199 100644 --- a/tools/aoptk/aoptk_find_relationships.xml +++ b/tools/aoptk/aoptk_find_relationships.xml @@ -9,22 +9,22 @@ -from aoptk.spacy_text_processor import SpacyText -from aoptk.relationships.zero_shot_classification_single import ZeroShotClassificationSingle -from aoptk.literature.pdf import PDF -from aoptk.literature.pymupdf_parser import PymupdfParser import os import sys os.environ.setdefault("TORCHINDUCTOR_CACHE_DIR", os.path.join(os.getcwd(), ".torchinductor_cache")) os.makedirs(os.environ["TORCHINDUCTOR_CACHE_DIR"], exist_ok=True) +from aoptk.spacy_text_processor import SpacyText +from aoptk.relationships.zero_shot_classification_single import ZeroShotClassificationSingle +from aoptk.literature.pdf import PDF +from aoptk.literature.pymupdf_parser import PymupdfParser + publication_id = os.path.splitext("$input_file.element_identifier")[0] effects = ["${effect}"] email = os.environ.get("EMAIL") From d1c364271186acf0055a100e48c81ed4ff0bc803 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Fri, 6 Mar 2026 11:07:25 +0100 Subject: [PATCH 049/173] Fixed macros --- tools/aoptk/aoptk_chemical_identifier.xml | 4 +++- tools/aoptk/aoptk_chemical_matching.xml | 1 - tools/aoptk/aoptk_chemical_normalization.xml | 1 - tools/aoptk/aoptk_download_abstracts.xml | 7 ++----- tools/aoptk/aoptk_download_pdf.xml | 13 +++---------- tools/aoptk/aoptk_find_chemicals.xml | 1 - tools/aoptk/aoptk_parse_pdf.xml | 2 +- tools/aoptk/aoptk_publication_tracker.xml | 4 +++- tools/aoptk/aoptk_query_literature.xml | 5 +++-- tools/aoptk/macros.xml | 10 ++++++++-- 10 files changed, 23 insertions(+), 25 deletions(-) diff --git a/tools/aoptk/aoptk_chemical_identifier.xml b/tools/aoptk/aoptk_chemical_identifier.xml index 2d5b66e8..9f23cf1a 100644 --- a/tools/aoptk/aoptk_chemical_identifier.xml +++ b/tools/aoptk/aoptk_chemical_identifier.xml @@ -6,6 +6,7 @@ + - + + diff --git a/tools/aoptk/aoptk_chemical_matching.xml b/tools/aoptk/aoptk_chemical_matching.xml index e644f492..78a814cb 100644 --- a/tools/aoptk/aoptk_chemical_matching.xml +++ b/tools/aoptk/aoptk_chemical_matching.xml @@ -9,7 +9,6 @@ diff --git a/tools/aoptk/aoptk_chemical_normalization.xml b/tools/aoptk/aoptk_chemical_normalization.xml index 89259b6f..1148b765 100644 --- a/tools/aoptk/aoptk_chemical_normalization.xml +++ b/tools/aoptk/aoptk_chemical_normalization.xml @@ -9,7 +9,6 @@ diff --git a/tools/aoptk/aoptk_download_abstracts.xml b/tools/aoptk/aoptk_download_abstracts.xml index 53083eb3..eb6ed264 100644 --- a/tools/aoptk/aoptk_download_abstracts.xml +++ b/tools/aoptk/aoptk_download_abstracts.xml @@ -6,10 +6,10 @@ + @@ -46,10 +46,7 @@ for abstract in abstracts: - - - - + diff --git a/tools/aoptk/aoptk_download_pdf.xml b/tools/aoptk/aoptk_download_pdf.xml index 3ae0675e..ba94a891 100644 --- a/tools/aoptk/aoptk_download_pdf.xml +++ b/tools/aoptk/aoptk_download_pdf.xml @@ -9,7 +9,6 @@ @@ -24,20 +23,14 @@ import os with open("$input_file", "r") as f: ids = [line.strip() for line in f.readlines()] -if "${literature_database}" == "europepmc": - europepmc = EuropePMC("", storage = "./") - europepmc.id_list = ids - pdfs = europepmc.pdfs() -else: - raise ValueError("Select valid database.") +europepmc = EuropePMC("", storage = "./") +europepmc.id_list = ids +pdfs = europepmc.pdfs() - - - diff --git a/tools/aoptk/aoptk_find_chemicals.xml b/tools/aoptk/aoptk_find_chemicals.xml index 054ed62c..91c4c1ef 100644 --- a/tools/aoptk/aoptk_find_chemicals.xml +++ b/tools/aoptk/aoptk_find_chemicals.xml @@ -9,7 +9,6 @@ diff --git a/tools/aoptk/aoptk_parse_pdf.xml b/tools/aoptk/aoptk_parse_pdf.xml index 64d55bea..8f04a6f3 100644 --- a/tools/aoptk/aoptk_parse_pdf.xml +++ b/tools/aoptk/aoptk_parse_pdf.xml @@ -1,5 +1,5 @@ - Detect chemicals in text. + Parse PDF files. macros.xml diff --git a/tools/aoptk/aoptk_publication_tracker.xml b/tools/aoptk/aoptk_publication_tracker.xml index b20a13ab..10652d55 100644 --- a/tools/aoptk/aoptk_publication_tracker.xml +++ b/tools/aoptk/aoptk_publication_tracker.xml @@ -6,6 +6,7 @@ + - + + diff --git a/tools/aoptk/aoptk_query_literature.xml b/tools/aoptk/aoptk_query_literature.xml index 4a9ee7d7..eb40f995 100644 --- a/tools/aoptk/aoptk_query_literature.xml +++ b/tools/aoptk/aoptk_query_literature.xml @@ -6,10 +6,10 @@ + @@ -40,7 +40,8 @@ with open(f"ids.txt", "w") as f: - + + diff --git a/tools/aoptk/macros.xml b/tools/aoptk/macros.xml index 0f0eb3a4..bed83c40 100644 --- a/tools/aoptk/macros.xml +++ b/tools/aoptk/macros.xml @@ -4,15 +4,18 @@ aoptk + + + - + - + @@ -82,6 +85,9 @@ [0-9a-zA-Z_ ()*'"[\]\[\]\-:.\[\]α-ωΑ-Ω]+ + + + From 69614974fe7d97ef2c156745e120500feaab9965 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Fri, 6 Mar 2026 11:18:28 +0100 Subject: [PATCH 050/173] Renamed normalization to normalization PubChem --- ....xml => aoptk_chemical_normalization_pubchem.xml} | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) rename tools/aoptk/{aoptk_chemical_normalization.xml => aoptk_chemical_normalization_pubchem.xml} (76%) diff --git a/tools/aoptk/aoptk_chemical_normalization.xml b/tools/aoptk/aoptk_chemical_normalization_pubchem.xml similarity index 76% rename from tools/aoptk/aoptk_chemical_normalization.xml rename to tools/aoptk/aoptk_chemical_normalization_pubchem.xml index 1148b765..c3cceb77 100644 --- a/tools/aoptk/aoptk_chemical_normalization.xml +++ b/tools/aoptk/aoptk_chemical_normalization_pubchem.xml @@ -1,5 +1,5 @@ - - Normalize chemical entities. + + Normalize chemical entities using PubChem API. macros.xml @@ -9,11 +9,11 @@ - + from aoptk.normalization.pubchem_api import PubChemAPI from aoptk.chemical import Chemical import os @@ -45,10 +45,10 @@ chemicals_df.to_csv("normalized_chemicals.tsv", sep="\t", index=False) From 43447bd301f35193d0f876af00cf4d00b933e752 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Fri, 6 Mar 2026 11:40:46 +0100 Subject: [PATCH 051/173] Changed description --- tools/aoptk/.shed.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/aoptk/.shed.yml b/tools/aoptk/.shed.yml index 09ea9419..9824bac6 100644 --- a/tools/aoptk/.shed.yml +++ b/tools/aoptk/.shed.yml @@ -6,11 +6,11 @@ categories: - Machine Learning description: "AOP-toolkit (aoptk) is a Python package designed to support data mining and analysis of toxicological outcomes." long_description: | - "AOP-toolkit (aoptk) is a Python package developed to support data mining and analysis of toxicological outcomes. Specifically, it supports the construction of Adverse Outcome Pathways (AOPs), which require extensive mining and integration of toxicological data from heterogeneous sources. It enables researchers to collect literature from databases such as PubMed and Europe PMC, extract relevant information from full-text publications, and analyze complex, unstructured data using large language models. The toolkit also provides functionality for normalizing chemical names across publications, helping ensure consistency and interoperability." + "AOP-toolkit (aoptk) is a Python package for mining and analyzing toxicological and biomedical literature. Originally developed to support the construction of Adverse Outcome Pathways (AOPs), it provides general-purpose tools for retrieving, processing, and analyzing scientific publications." auto_tool_repositories: name_template: "{{ tool_id }}" description_template: "{{ tool_name }} tool from the aoptk package" suite: name: suite_aoptk - description: AOP-toolkit (aoptk) is a Python package developed to support data mining and analysis of toxicological outcomes. Specifically, it supports the construction of Adverse Outcome Pathways (AOPs), which require extensive mining and integration of toxicological data from heterogeneous sources. It enables researchers to collect literature from databases such as PubMed and Europe PMC, extract relevant information from full-text publications, and analyze complex, unstructured data using large language models. The toolkit also provides functionality for normalizing chemical names across publications, helping ensure consistency and interoperability. + description: AOP-toolkit (aoptk) is a Python package for mining and analyzing toxicological and biomedical literature. Originally developed to support the construction of Adverse Outcome Pathways (AOPs), it provides general-purpose tools for retrieving, processing, and analyzing scientific publications. type: repository_suite_definition \ No newline at end of file From 832eb762518b51d8ef3a994e103c28a2a5475f94 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Wed, 25 Mar 2026 17:30:19 +0100 Subject: [PATCH 052/173] removed imports --- tools/aoptk/aoptk_chemical_matching.xml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tools/aoptk/aoptk_chemical_matching.xml b/tools/aoptk/aoptk_chemical_matching.xml index 78a814cb..524efd51 100644 --- a/tools/aoptk/aoptk_chemical_matching.xml +++ b/tools/aoptk/aoptk_chemical_matching.xml @@ -14,10 +14,6 @@ -from aoptk.spacy_text_processor import SpacyText -from aoptk.literature.pdf import PDF -from aoptk.literature.pymupdf_parser import PymupdfParser -from aoptk.chemical import Chemical import os import pandas as pd From 89d1f73e41638eaa194c7daeb2ef7aae946ded87 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Wed, 25 Mar 2026 17:30:29 +0100 Subject: [PATCH 053/173] added pmc --- tools/aoptk/aoptk_download_pdf.xml | 11 ++++------- tools/aoptk/aoptk_query_literature.xml | 5 ++++- tools/aoptk/macros.xml | 7 +++++++ 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/tools/aoptk/aoptk_download_pdf.xml b/tools/aoptk/aoptk_download_pdf.xml index ba94a891..8edad669 100644 --- a/tools/aoptk/aoptk_download_pdf.xml +++ b/tools/aoptk/aoptk_download_pdf.xml @@ -14,18 +14,15 @@ -from aoptk.literature.databases.pubmed import PubMed -from aoptk.literature.databases.europepmc import EuropePMC -from aoptk.literature.abstract import Abstract -from Bio import Entrez +from aoptk.literature.databases.pmc import PMC import os with open("$input_file", "r") as f: ids = [line.strip() for line in f.readlines()] -europepmc = EuropePMC("", storage = "./") -europepmc.id_list = ids -pdfs = europepmc.pdfs() +pmc = PMC("", storage = "./") +pmc.id_list = ids +pdfs = pmc.pdfs() diff --git a/tools/aoptk/aoptk_query_literature.xml b/tools/aoptk/aoptk_query_literature.xml index eb40f995..3d500062 100644 --- a/tools/aoptk/aoptk_query_literature.xml +++ b/tools/aoptk/aoptk_query_literature.xml @@ -17,6 +17,7 @@ from aoptk.literature.databases.pubmed import PubMed from aoptk.literature.databases.europepmc import EuropePMC +from aoptk.literature.databases.pmc import PMC from Bio import Entrez import os @@ -28,6 +29,8 @@ if "${literature_database}" == "pubmed": ids = PubMed("${query}").get_id() elif "${literature_database}" == "europepmc": ids = EuropePMC("${query}").get_id() +elif "${literature_database}" == "pmc": + ids = PMC("${query}").get_id() else: raise ValueError("Select valid database.") @@ -41,7 +44,7 @@ with open(f"ids.txt", "w") as f: - + diff --git a/tools/aoptk/macros.xml b/tools/aoptk/macros.xml index bed83c40..356b538c 100644 --- a/tools/aoptk/macros.xml +++ b/tools/aoptk/macros.xml @@ -93,6 +93,13 @@ + + + + + + + From 2d2c0dfb8c32841b16f86abc056611bbf36de969 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Thu, 9 Apr 2026 16:00:48 +0200 Subject: [PATCH 054/173] updated database selection --- tools/aoptk/aoptk_chemical_identifier.xml | 8 ++++---- tools/aoptk/aoptk_download_abstracts.xml | 6 +++--- tools/aoptk/aoptk_publication_tracker.xml | 8 ++++---- tools/aoptk/aoptk_query_literature.xml | 11 ++++++----- 4 files changed, 17 insertions(+), 16 deletions(-) diff --git a/tools/aoptk/aoptk_chemical_identifier.xml b/tools/aoptk/aoptk_chemical_identifier.xml index 9f23cf1a..2f530247 100644 --- a/tools/aoptk/aoptk_chemical_identifier.xml +++ b/tools/aoptk/aoptk_chemical_identifier.xml @@ -12,7 +12,7 @@ - + @@ -33,13 +33,13 @@ - + - + diff --git a/tools/aoptk/aoptk_download_abstracts.xml b/tools/aoptk/aoptk_download_abstracts.xml index eb6ed264..aae49985 100644 --- a/tools/aoptk/aoptk_download_abstracts.xml +++ b/tools/aoptk/aoptk_download_abstracts.xml @@ -26,12 +26,12 @@ with open("$input_file", "r") as f: email = os.environ.get("EMAIL") -if "${literature_database}" == "pubmed": +if "${literature_database_pubmed_europepmc}" == "pubmed": Entrez.email = email pubmed = PubMed.__new__(PubMed) pubmed.id_list = ids abstracts = pubmed.get_abstracts() -elif "${literature_database}" == "europepmc": +elif "${literature_database_pubmed_europepmc}" == "europepmc": europepmc = EuropePMC("") europepmc.id_list = ids abstracts = europepmc.get_abstracts() @@ -46,7 +46,7 @@ for abstract in abstracts: - + diff --git a/tools/aoptk/aoptk_publication_tracker.xml b/tools/aoptk/aoptk_publication_tracker.xml index 10652d55..f0f0c9db 100644 --- a/tools/aoptk/aoptk_publication_tracker.xml +++ b/tools/aoptk/aoptk_publication_tracker.xml @@ -17,7 +17,7 @@ --master 'master.${master.ext}' --code '$code' --query '$query' - --database '$literature_database' + --database '$literature_database_pubmed_europepmc' --outdir . \${EMAIL:+--email \$EMAIL} ]]> @@ -48,7 +48,7 @@ - + @@ -56,7 +56,7 @@ - + @@ -64,7 +64,7 @@ - + diff --git a/tools/aoptk/aoptk_query_literature.xml b/tools/aoptk/aoptk_query_literature.xml index 3d500062..095cf849 100644 --- a/tools/aoptk/aoptk_query_literature.xml +++ b/tools/aoptk/aoptk_query_literature.xml @@ -24,12 +24,13 @@ import os email = os.environ.get("EMAIL") -if "${literature_database}" == "pubmed": +if "${literature_database_pubmed_europepmc_pmc}" == "pubmed": Entrez.email = email ids = PubMed("${query}").get_id() -elif "${literature_database}" == "europepmc": +elif "${literature_database_pubmed_europepmc_pmc}" == "europepmc": ids = EuropePMC("${query}").get_id() -elif "${literature_database}" == "pmc": +elif "${literature_database_pubmed_europepmc_pmc}" == "pmc": + Entrez.email = email ids = PMC("${query}").get_id() else: @@ -44,7 +45,7 @@ with open(f"ids.txt", "w") as f: - + @@ -54,7 +55,7 @@ with open(f"ids.txt", "w") as f: - + From 13a7ee970d93645d9f981acb048c82d4ed3bc0e4 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Thu, 9 Apr 2026 16:00:59 +0200 Subject: [PATCH 055/173] updated to get pdf --- tools/aoptk/aoptk_download_pdf.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/aoptk/aoptk_download_pdf.xml b/tools/aoptk/aoptk_download_pdf.xml index 8edad669..c684702f 100644 --- a/tools/aoptk/aoptk_download_pdf.xml +++ b/tools/aoptk/aoptk_download_pdf.xml @@ -22,7 +22,7 @@ with open("$input_file", "r") as f: pmc = PMC("", storage = "./") pmc.id_list = ids -pdfs = pmc.pdfs() +pdfs = pmc.get_pdfs() From 20c54367283275358147de64c1cc6aafe78b8ebe Mon Sep 17 00:00:00 2001 From: rdurnik Date: Thu, 9 Apr 2026 16:01:09 +0200 Subject: [PATCH 056/173] updated database selection --- tools/aoptk/macros.xml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tools/aoptk/macros.xml b/tools/aoptk/macros.xml index 356b538c..dfd55ff6 100644 --- a/tools/aoptk/macros.xml +++ b/tools/aoptk/macros.xml @@ -87,19 +87,25 @@ - + - + + + + + + + From 42482c791020a1abbf43634c8097f27c146762c8 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Thu, 9 Apr 2026 16:34:46 +0200 Subject: [PATCH 057/173] added openai key macro --- tools/aoptk/macros.xml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tools/aoptk/macros.xml b/tools/aoptk/macros.xml index dfd55ff6..ec1378ad 100644 --- a/tools/aoptk/macros.xml +++ b/tools/aoptk/macros.xml @@ -15,6 +15,15 @@ + + + + + + From 57d83a5778f3dbd0b8d381c6b28827a2642c1917 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Thu, 9 Apr 2026 16:35:23 +0200 Subject: [PATCH 058/173] updated to openai_api_key_credentials --- tools/aoptk/macros.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/aoptk/macros.xml b/tools/aoptk/macros.xml index ec1378ad..38111920 100644 --- a/tools/aoptk/macros.xml +++ b/tools/aoptk/macros.xml @@ -15,8 +15,8 @@ - - + From fd700d2df1a9d27c864d9c1c6b9e8d7f64b4436c Mon Sep 17 00:00:00 2001 From: rdurnik Date: Thu, 9 Apr 2026 16:42:02 +0200 Subject: [PATCH 059/173] updated name --- ...lationships.xml => aoptk_find_relationships_non_llm.xml} | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) rename tools/aoptk/{aoptk_find_relationships.xml => aoptk_find_relationships_non_llm.xml} (91%) diff --git a/tools/aoptk/aoptk_find_relationships.xml b/tools/aoptk/aoptk_find_relationships_non_llm.xml similarity index 91% rename from tools/aoptk/aoptk_find_relationships.xml rename to tools/aoptk/aoptk_find_relationships_non_llm.xml index 3c696199..1102934c 100644 --- a/tools/aoptk/aoptk_find_relationships.xml +++ b/tools/aoptk/aoptk_find_relationships_non_llm.xml @@ -1,4 +1,4 @@ - + Detect relationships between chemicals and effects in text. macros.xml @@ -9,11 +9,11 @@ - + import os import sys From 0f370984abcb84edf71d9debd660311053cbda1d Mon Sep 17 00:00:00 2001 From: rdurnik Date: Thu, 9 Apr 2026 16:42:14 +0200 Subject: [PATCH 060/173] wrapper to get full text --- tools/aoptk/aoptk_download_fulltext.xml | 76 +++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 tools/aoptk/aoptk_download_fulltext.xml diff --git a/tools/aoptk/aoptk_download_fulltext.xml b/tools/aoptk/aoptk_download_fulltext.xml new file mode 100644 index 00000000..a0a8aba4 --- /dev/null +++ b/tools/aoptk/aoptk_download_fulltext.xml @@ -0,0 +1,76 @@ + + Download full texts for a list of publication IDs. + + macros.xml + + + + + + + + + + + +from aoptk.literature.databases.pmc import PMC +from aoptk.literature.databases.europepmc import EuropePMC +from Bio import Entrez +import os + +with open("$input_file", "r") as f: + ids = [line.strip() for line in f.readlines()] +email = os.environ.get("EMAIL") + + +if "${literature_database_europepmc_pmc}" == "pmc": + Entrez.email = email + pmc = PMC("", storage = "./", figure_storage="./figures") + pmc.id_list = ids + publications = pmc.get_publications() +elif "${literature_database_europepmc_pmc}" == "europepmc": + europepmc = EuropePMC("", storage = "./", figure_storage="./figures") + europepmc.id_list = ids + publications = europepmc.get_publications() +else: + raise ValueError("Select valid database.") + +for publication in publications: + with open(f"{publication.publication_id}.txt", "w") as f: + f.write(publication.full_text) + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From 01190f077651eadda5d80a028f8287375aaaad71 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Fri, 10 Apr 2026 10:48:49 +0200 Subject: [PATCH 061/173] Name update --- ...{aoptk_find_chemicals.xml => aoptk_find_chemicals_non_llm.xml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tools/aoptk/{aoptk_find_chemicals.xml => aoptk_find_chemicals_non_llm.xml} (100%) diff --git a/tools/aoptk/aoptk_find_chemicals.xml b/tools/aoptk/aoptk_find_chemicals_non_llm.xml similarity index 100% rename from tools/aoptk/aoptk_find_chemicals.xml rename to tools/aoptk/aoptk_find_chemicals_non_llm.xml From 8322fcba47057e40cc25bc0d114e1ead3afece09 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Fri, 10 Apr 2026 11:15:30 +0200 Subject: [PATCH 062/173] added potential llm api key to pdf parsing --- tools/aoptk/aoptk_parse_pdf.xml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tools/aoptk/aoptk_parse_pdf.xml b/tools/aoptk/aoptk_parse_pdf.xml index 8f04a6f3..f224d5a5 100644 --- a/tools/aoptk/aoptk_parse_pdf.xml +++ b/tools/aoptk/aoptk_parse_pdf.xml @@ -6,6 +6,7 @@ + Date: Fri, 10 Apr 2026 11:15:39 +0200 Subject: [PATCH 063/173] updated macros for llms --- tools/aoptk/macros.xml | 82 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/tools/aoptk/macros.xml b/tools/aoptk/macros.xml index 38111920..3826a7a0 100644 --- a/tools/aoptk/macros.xml +++ b/tools/aoptk/macros.xml @@ -115,6 +115,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + [0-9a-zA-Z_ ()*'"[\]\[\]\-:.\[\]α-ωΑ-Ω]+ + + + From f8c5550787c151827ca51ed0762b69079690359a Mon Sep 17 00:00:00 2001 From: rdurnik Date: Fri, 10 Apr 2026 11:15:48 +0200 Subject: [PATCH 064/173] find relationships with llms --- tools/aoptk/aoptk_find_relationships_text.xml | 87 ++++++++++++++ .../aoptk_find_relationships_text_images.xml | 106 ++++++++++++++++++ 2 files changed, 193 insertions(+) create mode 100644 tools/aoptk/aoptk_find_relationships_text.xml create mode 100644 tools/aoptk/aoptk_find_relationships_text_images.xml diff --git a/tools/aoptk/aoptk_find_relationships_text.xml b/tools/aoptk/aoptk_find_relationships_text.xml new file mode 100644 index 00000000..f4747e16 --- /dev/null +++ b/tools/aoptk/aoptk_find_relationships_text.xml @@ -0,0 +1,87 @@ + + Detect relationships between chemicals and effects in text. + + macros.xml + + + + + + + + + + + +import os +from aoptk.text_generation_api import TextGenerationAPI +from aoptk.relationship_type import Causative +from aoptk.relationship_type import Inhibitive +from aoptk.relationship_type import RelationshipType +from aoptk.relationships.relationship import Relationship +from aoptk.effect import Effect + +publication_id = os.path.splitext("$input_file.element_identifier")[0] +effects = ["${effect}"] +openai_key = os.environ.get("OPENAI_KEY") + +text_generation_api = TextGenerationAPI(model="$llm_model", api_key=openai_key, url="$llm_url") +text_generation_api.specification_relationship_text_prompt="$prompt_specification" + +with open("relationships.tsv", "w") as f_out: + f_out.write("id\tchemical\teffect\trelationship\n") + + if "${input_file.ext}" == "pdf": + text = PymupdfParser([PDF("$input_file")]).inject_text_generation(TextGenerationAPI(model="redhatai-scout")).get_publications()[0].full_text + chemicals = text_generation_api.find_chemicals(text) + relationships = text_generation_api.find_relationships_in_text( + text=text, chemicals=chemicals, effects=effects + ) + for relationship in relationships: + f_out.write(f"{publication_id}\t{relationship.chemical}\t{relationship.effect}\t{relationship.relationship_type}\n") + + elif "${input_file.ext}" == "txt": + with open("$input_file", "r") as f_in: + text = f_in.read() + chemicals = text_generation_api.find_chemicals(text) + relationships = text_generation_api.find_relationships_in_text( + text=text, chemicals=chemicals, effects=effects + ) + for relationship in relationships: + f_out.write(f"{publication_id}\t{relationship.chemical}\t{relationship.effect}\t{relationship.relationship_type}\n") + else: + raise ValueError(f"Unsupported input file format: ${input_file.ext}") + + + + + + + + + [0-9a-zA-Z]+ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tools/aoptk/aoptk_find_relationships_text_images.xml b/tools/aoptk/aoptk_find_relationships_text_images.xml new file mode 100644 index 00000000..6a52d6d1 --- /dev/null +++ b/tools/aoptk/aoptk_find_relationships_text_images.xml @@ -0,0 +1,106 @@ + + Detect relationships between chemicals and effects in text. + + macros.xml + + + + + + + + + + + +import os +from aoptk.text_generation_api import TextGenerationAPI +from aoptk.relationship_type import Causative +from aoptk.relationship_type import Inhibitive +from aoptk.relationship_type import RelationshipType +from aoptk.relationships.relationship import Relationship +from aoptk.effect import Effect + +publication_id = os.path.splitext("$input_file.element_identifier")[0] +effects = ["${effect}"] +openai_key = os.environ.get("OPENAI_KEY") + +text_generation_api = TextGenerationAPI(model="$llm_model", api_key=openai_key, url="$llm_url") + +with open("relationships.tsv", "w") as f_out: + f_out.write("id\tchemical\teffect\trelationship\n") + + if "${input_file.ext}" == "pdf": + text = "" + publication = PymupdfParser([PDF("$input_file")]).inject_text_generation(TextGenerationAPI(model="$llm_models_images")).get_publications()[0] + text += publication.full_text + + for image in publication.images: + text_image = TextGenerationAPI(model="$llm_models_images").convert_image( + image_path=image.path, + text=publication.full_text, + ) + text += f"\n\n{image.name}:\n{text_image}" + + chemicals = text_generation_api.find_chemicals(text) + relationships = text_generation_api.find_relationships_in_text( + text=text, chemicals=chemicals, effects=effects + ) + for relationship in relationships: + f_out.write(f"{publication_id}\t{relationship.chemical}\t{relationship.effect}\t{relationship.relationship_type}\n") + + elif "${input_file.ext}" == "txt": + with open("$input_file", "r") as f_in: + text = "" + text += f_in.read() + text += "\n\n\n\nALSO ANALYZE THESE FIGURES CONVERTED TO TEXT:" + for image in publication_images_folder.iterdir(): + text_image = TextGenerationAPI(model="$llm_models_images").convert_image( + image_path=image, + text=matching_pub_data["publication.full_text"], + ) + text += f"\n\n{image.name}:\n{text_image}" + + chemicals = text_generation_api.find_chemicals(text) + relationships = text_generation_api.find_relationships_in_text( + text=text, chemicals=chemicals, effects=effects + ) + for relationship in relationships: + f_out.write(f"{publication_id}\t{relationship.chemical}\t{relationship.effect}\t{relationship.relationship_type}\n") + else: + raise ValueError(f"Unsupported input file format: ${input_file.ext}") + + + + + + + + + + [0-9a-zA-Z]+ + + + + + + + + + + + + + + + + + \ No newline at end of file From 50156b64efa81a8d335e1876206cbd5825f158a8 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Fri, 10 Apr 2026 11:22:44 +0200 Subject: [PATCH 065/173] updated pdf desc --- tools/aoptk/aoptk_parse_pdf.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/aoptk/aoptk_parse_pdf.xml b/tools/aoptk/aoptk_parse_pdf.xml index f224d5a5..08b827f0 100644 --- a/tools/aoptk/aoptk_parse_pdf.xml +++ b/tools/aoptk/aoptk_parse_pdf.xml @@ -54,7 +54,7 @@ else: Parse PDF =================== -Tool to parse PDF files. +Tool to parse PDF files. API key for LLMs is optional and only needed for PDFs that are scanned images. ]]> From 6891035f7928f8cd51b2632d687f6bf26e84f60b Mon Sep 17 00:00:00 2001 From: rdurnik Date: Mon, 20 Apr 2026 14:35:56 +0200 Subject: [PATCH 066/173] changed desc to text --- tools/aoptk/aoptk_find_chemicals_non_llm.xml | 2 +- tools/aoptk/aoptk_find_relationships_non_llm.xml | 2 +- tools/aoptk/aoptk_find_relationships_text.xml | 2 +- tools/aoptk/aoptk_find_relationships_text_images.xml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tools/aoptk/aoptk_find_chemicals_non_llm.xml b/tools/aoptk/aoptk_find_chemicals_non_llm.xml index 91c4c1ef..731415e4 100644 --- a/tools/aoptk/aoptk_find_chemicals_non_llm.xml +++ b/tools/aoptk/aoptk_find_chemicals_non_llm.xml @@ -40,7 +40,7 @@ with open("chemicals.tsv", "w") as f_out: - + diff --git a/tools/aoptk/aoptk_find_relationships_non_llm.xml b/tools/aoptk/aoptk_find_relationships_non_llm.xml index 1102934c..1f735dab 100644 --- a/tools/aoptk/aoptk_find_relationships_non_llm.xml +++ b/tools/aoptk/aoptk_find_relationships_non_llm.xml @@ -56,7 +56,7 @@ with open("relationships.tsv", "w") as f_out: - + [0-9a-zA-Z]+ diff --git a/tools/aoptk/aoptk_find_relationships_text.xml b/tools/aoptk/aoptk_find_relationships_text.xml index f4747e16..65077448 100644 --- a/tools/aoptk/aoptk_find_relationships_text.xml +++ b/tools/aoptk/aoptk_find_relationships_text.xml @@ -59,7 +59,7 @@ with open("relationships.tsv", "w") as f_out: - + [0-9a-zA-Z]+ diff --git a/tools/aoptk/aoptk_find_relationships_text_images.xml b/tools/aoptk/aoptk_find_relationships_text_images.xml index 6a52d6d1..d9af160e 100644 --- a/tools/aoptk/aoptk_find_relationships_text_images.xml +++ b/tools/aoptk/aoptk_find_relationships_text_images.xml @@ -77,7 +77,7 @@ with open("relationships.tsv", "w") as f_out: - + [0-9a-zA-Z]+ From 0deaeba9e709ef1afe7b7a5045757e2160857e6d Mon Sep 17 00:00:00 2001 From: rdurnik Date: Mon, 20 Apr 2026 14:51:41 +0200 Subject: [PATCH 067/173] llm find chemicals --- tools/aoptk/aoptk_find_chemicals.xml | 73 ++++++++++++++++++++ tools/aoptk/aoptk_find_chemicals_non_llm.xml | 6 +- 2 files changed, 76 insertions(+), 3 deletions(-) create mode 100644 tools/aoptk/aoptk_find_chemicals.xml diff --git a/tools/aoptk/aoptk_find_chemicals.xml b/tools/aoptk/aoptk_find_chemicals.xml new file mode 100644 index 00000000..b6f3bac9 --- /dev/null +++ b/tools/aoptk/aoptk_find_chemicals.xml @@ -0,0 +1,73 @@ + + Detect chemicals in text. + + macros.xml + + + + + + + + + + + +import os +from aoptk.text_generation_api import TextGenerationAPI +from aoptk.literature.pdf import PDF +from aoptk.literature.pymupdf_parser import PymupdfParser + +publication_id = os.path.splitext("$input_file.element_identifier")[0] +openai_key = os.environ.get("OPENAI_KEY") +text_generation_api = TextGenerationAPI(model="$llm_model", api_key=openai_key) + +if "${input_file.ext}" == "pdf": + text = PymupdfParser([PDF("$input_file")]).get_publications()[0].full_text + chemicals = text_generation_api.find_chemicals(text) + +elif "${input_file.ext}" == "txt": + with open("$input_file", "r") as f_in: + text = f_in.read() + chemicals = text_generation_api.find_chemicals(text) +else: + raise ValueError(f"Unsupported input file format: {input_ext}") + +with open("chemicals.tsv", "w") as f_out: + f_out.write("id\tchemical\n") + for chemical in chemicals: + f_out.write(f"{publication_id}\t{chemical.name}\n") + + + + + + + + + [0-9a-zA-Z]+ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tools/aoptk/aoptk_find_chemicals_non_llm.xml b/tools/aoptk/aoptk_find_chemicals_non_llm.xml index 731415e4..390616a3 100644 --- a/tools/aoptk/aoptk_find_chemicals_non_llm.xml +++ b/tools/aoptk/aoptk_find_chemicals_non_llm.xml @@ -1,4 +1,4 @@ - + Detect chemicals in text. macros.xml @@ -9,11 +9,11 @@ - + from aoptk.spacy_text_processor import SpacyText from aoptk.literature.pdf import PDF from aoptk.literature.pymupdf_parser import PymupdfParser From 87aefb8258bd12a275c30411009b95f110c28111 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Mon, 20 Apr 2026 16:58:50 +0200 Subject: [PATCH 068/173] new test file --- tools/aoptk/test-data/relationships.tsv | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 tools/aoptk/test-data/relationships.tsv diff --git a/tools/aoptk/test-data/relationships.tsv b/tools/aoptk/test-data/relationships.tsv new file mode 100644 index 00000000..62e0c8a8 --- /dev/null +++ b/tools/aoptk/test-data/relationships.tsv @@ -0,0 +1,2 @@ +id chemical effect relationship +text thioacetamide liver fibrosis positive \ No newline at end of file From 54b7e5ca5cc349e1fc0f936798d9602b2391688d Mon Sep 17 00:00:00 2001 From: rdurnik Date: Mon, 20 Apr 2026 16:59:04 +0200 Subject: [PATCH 069/173] normalization using llms --- .../aoptk_chemical_normalization_llm.xml | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 tools/aoptk/aoptk_chemical_normalization_llm.xml diff --git a/tools/aoptk/aoptk_chemical_normalization_llm.xml b/tools/aoptk/aoptk_chemical_normalization_llm.xml new file mode 100644 index 00000000..6fdbad0f --- /dev/null +++ b/tools/aoptk/aoptk_chemical_normalization_llm.xml @@ -0,0 +1,62 @@ + + Normalize chemical entities using LLMs. + + macros.xml + + + + + + + + + + + +import os +from aoptk.text_generation_api import TextGenerationAPI +from aoptk.chemical import Chemical +import pandas as pd + +openai_key = os.environ.get("OPENAI_KEY") +text_generation_api = TextGenerationAPI(model="$llm_model", api_key=openai_key) +chemical_list = pd.read_csv("$chemical_list", sep="\t")["chemical"].tolist() +chemicals = pd.read_csv("$chemicals", sep="\t") +chemicals["chemical"] = chemicals["chemical"].apply( + lambda x: TextGenerationAPI(model="$llm_model", api_key=openai_key).normalize_chemical(chemical=Chemical(x), chemical_list=chemical_list) + ) +chemicals["heading"] = chemicals["chemical"].apply(lambda chem: chem.heading) +chemicals.to_csv("normalized_chemicals.tsv", sep="\t", index=False) + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From 30c8ba638d6a87793efed56c6093a05303b17171 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Mon, 20 Apr 2026 16:59:37 +0200 Subject: [PATCH 070/173] clean up, tests fix --- tools/aoptk/aoptk_chemical_matching.xml | 2 +- .../aoptk_chemical_normalization_pubchem.xml | 10 +-- tools/aoptk/aoptk_download_abstracts.xml | 4 +- tools/aoptk/aoptk_download_fulltext.xml | 4 +- tools/aoptk/aoptk_download_pdf.xml | 4 +- .../aoptk_find_relationships_non_llm.xml | 36 +++++----- tools/aoptk/aoptk_find_relationships_text.xml | 37 +++++----- .../aoptk_find_relationships_text_images.xml | 67 +++++++++---------- tools/aoptk/aoptk_parse_pdf.xml | 2 +- 9 files changed, 82 insertions(+), 84 deletions(-) diff --git a/tools/aoptk/aoptk_chemical_matching.xml b/tools/aoptk/aoptk_chemical_matching.xml index 524efd51..c1b1635d 100644 --- a/tools/aoptk/aoptk_chemical_matching.xml +++ b/tools/aoptk/aoptk_chemical_matching.xml @@ -43,7 +43,7 @@ merged_files.to_csv("merged_chemicals.tsv", sep="\t", index=False) - + diff --git a/tools/aoptk/aoptk_chemical_normalization_pubchem.xml b/tools/aoptk/aoptk_chemical_normalization_pubchem.xml index c3cceb77..48b71e9b 100644 --- a/tools/aoptk/aoptk_chemical_normalization_pubchem.xml +++ b/tools/aoptk/aoptk_chemical_normalization_pubchem.xml @@ -19,12 +19,12 @@ from aoptk.chemical import Chemical import os import pandas as pd -chemicals_df = pd.read_csv("$input_file", sep="\t") -chemicals_df["chemical"] = chemicals_df["chemical"].apply( +chemicals = pd.read_csv("$input_file", sep="\t") +chemicals["chemical"] = chemicals["chemical"].apply( lambda x: PubChemAPI().normalize_chemical(Chemical(x)) ) -chemicals_df["heading"] = chemicals_df["chemical"].apply(lambda chem: chem.heading) -chemicals_df.to_csv("normalized_chemicals.tsv", sep="\t", index=False) +chemicals["heading"] = chemicals["chemical"].apply(lambda chem: chem.heading) +chemicals.to_csv("normalized_chemicals.tsv", sep="\t", index=False) @@ -40,7 +40,7 @@ chemicals_df.to_csv("normalized_chemicals.tsv", sep="\t", index=False) - + diff --git a/tools/aoptk/aoptk_download_abstracts.xml b/tools/aoptk/aoptk_download_abstracts.xml index aae49985..0294deea 100644 --- a/tools/aoptk/aoptk_download_abstracts.xml +++ b/tools/aoptk/aoptk_download_abstracts.xml @@ -58,8 +58,8 @@ for abstract in abstracts: - - + + diff --git a/tools/aoptk/aoptk_download_fulltext.xml b/tools/aoptk/aoptk_download_fulltext.xml index a0a8aba4..4b6f3b5d 100644 --- a/tools/aoptk/aoptk_download_fulltext.xml +++ b/tools/aoptk/aoptk_download_fulltext.xml @@ -60,8 +60,8 @@ for publication in publications: - - + + diff --git a/tools/aoptk/aoptk_download_pdf.xml b/tools/aoptk/aoptk_download_pdf.xml index c684702f..4df1a824 100644 --- a/tools/aoptk/aoptk_download_pdf.xml +++ b/tools/aoptk/aoptk_download_pdf.xml @@ -39,8 +39,8 @@ pdfs = pmc.get_pdfs() - - + + diff --git a/tools/aoptk/aoptk_find_relationships_non_llm.xml b/tools/aoptk/aoptk_find_relationships_non_llm.xml index 1f735dab..9bc7c117 100644 --- a/tools/aoptk/aoptk_find_relationships_non_llm.xml +++ b/tools/aoptk/aoptk_find_relationships_non_llm.xml @@ -29,29 +29,28 @@ publication_id = os.path.splitext("$input_file.element_identifier")[0] effects = ["${effect}"] email = os.environ.get("EMAIL") -with open("relationships.tsv", "w") as f_out: - f_out.write("id\tchemical\teffect\trelationship\n") +if "${input_file.ext}" == "pdf": + text = PymupdfParser([PDF("$input_file")]).get_publications()[0].full_text + chemicals = SpacyText().find_chemical(text) + relationships = ZeroShotClassificationSingle().find_relationships( + text=text, chemicals=chemicals, effects=effects +) - if "${input_file.ext}" == "pdf": - text = PymupdfParser([PDF("$input_file")]).get_publications()[0].full_text +elif "${input_file.ext}" == "txt": + with open("$input_file", "r") as f_in: + text = f_in.read() chemicals = SpacyText().find_chemical(text) relationships = ZeroShotClassificationSingle().find_relationships( text=text, chemicals=chemicals, effects=effects ) - for relationship in relationships: - f_out.write(f"{publication_id}\t{relationship.chemical}\t{relationship.effect}\t{relationship.relationship_type}\n") - - elif "${input_file.ext}" == "txt": - with open("$input_file", "r") as f_in: - text = f_in.read() - chemicals = SpacyText().find_chemical(text) - relationships = ZeroShotClassificationSingle().find_relationships( - text=text, chemicals=chemicals, effects=effects - ) - for relationship in relationships: - f_out.write(f"{publication_id}\t{relationship.chemical}\t{relationship.effect}\t{relationship.relationship_type}\n") - else: - raise ValueError(f"Unsupported input file format: ${input_file.ext}") + +else: + raise ValueError(f"Unsupported input file format: ${input_file.ext}") + +with open("relationships.tsv", "w") as f_out: + f_out.write("id\tchemical\teffect\trelationship\n") + for relationship in relationships: + f_out.write(f"{publication_id}\t{relationship.chemical}\t{relationship.effect}\t{relationship.relationship_type}\n") @@ -69,6 +68,7 @@ with open("relationships.tsv", "w") as f_out: + diff --git a/tools/aoptk/aoptk_find_relationships_text.xml b/tools/aoptk/aoptk_find_relationships_text.xml index 65077448..2545c60f 100644 --- a/tools/aoptk/aoptk_find_relationships_text.xml +++ b/tools/aoptk/aoptk_find_relationships_text.xml @@ -26,33 +26,31 @@ from aoptk.effect import Effect publication_id = os.path.splitext("$input_file.element_identifier")[0] effects = ["${effect}"] openai_key = os.environ.get("OPENAI_KEY") - -text_generation_api = TextGenerationAPI(model="$llm_model", api_key=openai_key, url="$llm_url") +text_generation_api = TextGenerationAPI(model="$llm_model", api_key=openai_key) text_generation_api.specification_relationship_text_prompt="$prompt_specification" -with open("relationships.tsv", "w") as f_out: - f_out.write("id\tchemical\teffect\trelationship\n") +if "${input_file.ext}" == "pdf": + text = PymupdfParser([PDF("$input_file")]).inject_text_generation(TextGenerationAPI(model="redhatai-scout")).get_publications()[0].full_text + chemicals = text_generation_api.find_chemicals(text) + relationships = text_generation_api.find_relationships_in_text( + text=text, chemicals=chemicals, effects=effects +) - if "${input_file.ext}" == "pdf": - text = PymupdfParser([PDF("$input_file")]).inject_text_generation(TextGenerationAPI(model="redhatai-scout")).get_publications()[0].full_text +elif "${input_file.ext}" == "txt": + with open("$input_file", "r") as f_in: + text = f_in.read() chemicals = text_generation_api.find_chemicals(text) relationships = text_generation_api.find_relationships_in_text( text=text, chemicals=chemicals, effects=effects ) - for relationship in relationships: - f_out.write(f"{publication_id}\t{relationship.chemical}\t{relationship.effect}\t{relationship.relationship_type}\n") - elif "${input_file.ext}" == "txt": - with open("$input_file", "r") as f_in: - text = f_in.read() - chemicals = text_generation_api.find_chemicals(text) - relationships = text_generation_api.find_relationships_in_text( - text=text, chemicals=chemicals, effects=effects - ) - for relationship in relationships: - f_out.write(f"{publication_id}\t{relationship.chemical}\t{relationship.effect}\t{relationship.relationship_type}\n") - else: - raise ValueError(f"Unsupported input file format: ${input_file.ext}") +else: + raise ValueError(f"Unsupported input file format: ${input_file.ext}") + +with open("relationships.tsv", "w") as f_out: + f_out.write("id\tchemical\teffect\trelationship\n") + for relationship in relationships: + f_out.write(f"{publication_id}\t{relationship.chemical}\t{relationship.effect}\t{relationship.relationship_type}\n") @@ -72,6 +70,7 @@ with open("relationships.tsv", "w") as f_out: + diff --git a/tools/aoptk/aoptk_find_relationships_text_images.xml b/tools/aoptk/aoptk_find_relationships_text_images.xml index d9af160e..7c6ca010 100644 --- a/tools/aoptk/aoptk_find_relationships_text_images.xml +++ b/tools/aoptk/aoptk_find_relationships_text_images.xml @@ -26,51 +26,49 @@ from aoptk.effect import Effect publication_id = os.path.splitext("$input_file.element_identifier")[0] effects = ["${effect}"] openai_key = os.environ.get("OPENAI_KEY") +text_generation_api = TextGenerationAPI(model="$llm_model", api_key=openai_key) +text_generation_api.specification_relationship_text_prompt="$prompt_specification" + +if "${input_file.ext}" == "pdf": + text = "" + publication = PymupdfParser([PDF("$input_file")]).inject_text_generation(TextGenerationAPI(model="$llm_models_images")).get_publications()[0] + text += publication.full_text + + for image in publication.images: + text_image = TextGenerationAPI(model="$llm_models_images").convert_image( + image_path=image.path, + text=publication.full_text, + ) + text += f"\n\n{image.name}:\n{text_image}" -text_generation_api = TextGenerationAPI(model="$llm_model", api_key=openai_key, url="$llm_url") + chemicals = text_generation_api.find_chemicals(text) + relationships = text_generation_api.find_relationships_in_text( + text=text, chemicals=chemicals, effects=effects +) -with open("relationships.tsv", "w") as f_out: - f_out.write("id\tchemical\teffect\trelationship\n") - - if "${input_file.ext}" == "pdf": +elif "${input_file.ext}" == "txt": + with open("$input_file", "r") as f_in: text = "" - publication = PymupdfParser([PDF("$input_file")]).inject_text_generation(TextGenerationAPI(model="$llm_models_images")).get_publications()[0] - text += publication.full_text - - for image in publication.images: + text += f_in.read() + text += "\n\n\n\nALSO ANALYZE THESE FIGURES CONVERTED TO TEXT:" + for image in publication_images_folder.iterdir(): text_image = TextGenerationAPI(model="$llm_models_images").convert_image( - image_path=image.path, - text=publication.full_text, + image_path=image, + text=matching_pub_data["publication.full_text"], ) - text += f"\n\n{image.name}:\n{text_image}" + text += f"\n\n{image.name}:\n{text_image}" chemicals = text_generation_api.find_chemicals(text) relationships = text_generation_api.find_relationships_in_text( text=text, chemicals=chemicals, effects=effects ) - for relationship in relationships: - f_out.write(f"{publication_id}\t{relationship.chemical}\t{relationship.effect}\t{relationship.relationship_type}\n") +else: + raise ValueError(f"Unsupported input file format: ${input_file.ext}") - elif "${input_file.ext}" == "txt": - with open("$input_file", "r") as f_in: - text = "" - text += f_in.read() - text += "\n\n\n\nALSO ANALYZE THESE FIGURES CONVERTED TO TEXT:" - for image in publication_images_folder.iterdir(): - text_image = TextGenerationAPI(model="$llm_models_images").convert_image( - image_path=image, - text=matching_pub_data["publication.full_text"], - ) - text += f"\n\n{image.name}:\n{text_image}" - - chemicals = text_generation_api.find_chemicals(text) - relationships = text_generation_api.find_relationships_in_text( - text=text, chemicals=chemicals, effects=effects - ) - for relationship in relationships: - f_out.write(f"{publication_id}\t{relationship.chemical}\t{relationship.effect}\t{relationship.relationship_type}\n") - else: - raise ValueError(f"Unsupported input file format: ${input_file.ext}") +with open("relationships.tsv", "w") as f_out: + f_out.write("id\tchemical\teffect\trelationship\n") + for relationship in relationships: + f_out.write(f"{publication_id}\t{relationship.chemical}\t{relationship.effect}\t{relationship.relationship_type}\n") @@ -91,6 +89,7 @@ with open("relationships.tsv", "w") as f_out: + diff --git a/tools/aoptk/aoptk_parse_pdf.xml b/tools/aoptk/aoptk_parse_pdf.xml index 08b827f0..bce531c8 100644 --- a/tools/aoptk/aoptk_parse_pdf.xml +++ b/tools/aoptk/aoptk_parse_pdf.xml @@ -46,7 +46,7 @@ else: - + From 8d8e4c0ddaabf0cad4716a5fe23f1ea7979d4298 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Mon, 20 Apr 2026 17:12:56 +0200 Subject: [PATCH 071/173] name change --- tools/aoptk/aoptk_find_relationships_text_images.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/aoptk/aoptk_find_relationships_text_images.xml b/tools/aoptk/aoptk_find_relationships_text_images.xml index 7c6ca010..88cb1295 100644 --- a/tools/aoptk/aoptk_find_relationships_text_images.xml +++ b/tools/aoptk/aoptk_find_relationships_text_images.xml @@ -1,4 +1,4 @@ - + Detect relationships between chemicals and effects in text. macros.xml @@ -10,11 +10,11 @@ - + import os from aoptk.text_generation_api import TextGenerationAPI from aoptk.relationship_type import Causative From 71e57a83c579b45d3eb65f3062a3810131586cd5 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Tue, 21 Apr 2026 09:04:19 +0200 Subject: [PATCH 072/173] pdf test data --- tools/aoptk/test-data/text.pdf | Bin 0 -> 81026 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tools/aoptk/test-data/text.pdf diff --git a/tools/aoptk/test-data/text.pdf b/tools/aoptk/test-data/text.pdf new file mode 100644 index 0000000000000000000000000000000000000000..4fc5b3dccb5a20a51371b3bba4f5bb55de1dad26 GIT binary patch literal 81026 zcmdSAWl&sOw>FA9!Citw+T7FJMa9M3!raB&!SuHQ-rodaVN@h@ z2eUtp{5JV-9{urzMZ(eD;ng6EytSFD9vR1PDSfhk%KhH@Z*s3;yc%-*k2pZ)u8!_5 zuV!Bzm2z}&`+Z>}`zr=1b~3g<(Z0GZ>E$M^=4R|>{>mV&#>Pta`uUxQ*EJj2>*tTh zEGjOJrfTMHdMvLwWl=Zxa$}LTe@&YBUl)nLF0#L!e9eQIwXwLPm)@%=D;XCb7c&Pt z880UrGatJ?*=uMHZm&+dl5zfJ#l_Lx>DBU|a!CKaVNqjIcQJNwb^0BQsW*#+8Vk_e z!`jpwBrX1`UESEk^_AtX+WZx;D;euQbMssA-?@>pwsSLgVUeS$)pqG0Y| z>1IX7!OHzx$kolo+}Iu!$txocVAu-<8wcQz7TVSDpCqRnINRg z>IO{2GqEHH53_rq#2oyfc^;|s(JX`=A~57p!mUubIEv84BM|Qf_@~L zX+#&EC!=*oqFdC^(A9CL9z9g^wNI^C7vdE{x9fsqOCS63ro}z*j|Al@a6(dM<}Pa^ zMQZnBt(y=NFt}^fcs?ek##*GTs3li#AQlk;`mc6R3+{G{uHL4XPL?h`t*S4gIhsQb zBa7gBVst=I!MIER0rB7I`y0={;q`a!|CIDM@>tZ|P27IB+wUD#7I9-&^FQ0-5AT{>ofwHTUx)S+nY}8b#$2j9#8I0PIl(@zdQcl@c-My--`ZK zga35lk4H+z_U0`ACI7EYV(Ci8!OkN4Iuxx<#T+c{%*j|;#C})6jf{tj^*^fNM#jYc zdLv=%Bx7!EY4sOL%)?U4+U&KjxY*fV=c3nNH*7qd9DnR-{l&@onk4Q2kov7d?KM}L zuOfeV#Q)3;CN5TvKL%bW6esI%v#&$rcj{kT*_@q>{mXiNjP2i~{@YpbKS@c5{THeKHn0DH;&05Tza9jCGX5vMQtbZ*b*cXe>RQ$g zVh*m>|APK+b^lo^GPXbPW|6XXadnfhGIsf09R=h6T>r`P>$&o$E&eUM|8_w9yXJob z!9P9w|0@XoIrPQc-K-p4=zhIGfFK+pOdzZw93aRbfDrBwF0ZR&2;0}@k$4^Zul@L& zN%57z^p)fY;rjY(0pSMm^_5-gb?pLS_PVn8vm*N+_{bnQAh;p8{#Ix0=m2~@XXt?Z z?5ynEteos@th`*T?5qr|?6j<`w6AQ6j%NP{j@NV6%-z)d|CVF_#02|)1Fy1)jhdT1 zixL^nUl1eX$@A4z+A z2#aM|7ZXR}uGT#J3%Ei^Yev%jPmj|_zC)uu0bqphAn!=upGMrLcX@S>qTwqKuwIa# z6|trpQ`w=JD6n?-Y)Pf?E*CiHgFdxVdeL^#ec;&`+3gWyK;$1BDo~VMdM+lGO}K-} zEFIw0&Rf6_sRmeAI>f0yQJ62vJ5YahTPTX81^jZ5XlW%8>N+jwIVy~#MA^|k97ZNi z@!|GVpmT4cb|+t+%e$`-ao4eubD>5ote!Z^d1vevQo+!HgQt9Despa&a?RE5c98L1 zRwl$ZQFpp$IAfgD0H>5wr!qF7uh92l&|bdCTvMY7P}6@w_lW_qQJ7hgx{}!~jVNoq z=*o5O;mbjQ?1DNMi!$ztp?C>>X*83cy%p~rWI9CE!2fuH{yEY99%uhA#o*xj_hRVB zjXQi`Md?2kl#E)*r3iwm_!z^lOZDb4-`y7?E#-~%N4g1U$IjlOl^GeahmF%(CIMre zw>B$^jDC$D1i$qzDKQ{#In`q>5HM%H0rslX7JoWpcEb`7N z<7hU%ImT2J|ELK?X^g7Kcmj2Pz>s9B4>+D_^x*D2X+Zxkzz;mS%DI-6 z4c~8t_wMe5C(1)8{jE>Wu#t_`&;N5}{yAg*_sZ~auyXPK$5STT>mxreJJ)~dnVeQw0>P>|xX5QE}KFa4T|WEXP{0pn_KqJv$HAPM3MSZ@hnIdj3=pf$kK=w~Wv^ z&ozw?m&QwczpPW6e7^sP&%ww->Mw4*)IaRWaZ+ib(C#-ilMys8724 z5qEF&{-m~{z9pUhIr%}jxW5G9M%9nh2E2WW0uG{GCY`TeWrdLUl?epCR9lPLUtg}m zpS4vcvvPBK7+ha`dQP%H6APf+%)C5ERyXvXD=LhbX9PYR-OpYgga>mNn6%L@EqD5F z1oz4FAKNiKY~h?Eii&sEhNuKEE=uZ$H-&EyK4x2G<7i*Y!Ak1#B#XbWlA ztzI0TD+143|IMo+?%1qNZTrje^<-W#qXC|O3D=YN%}YChfu-kD?6{VQG28ov+Ly4| zTgNn|fv;NG#+OlPTH{|u%6}#GOb^%%Z08T`C%wJ6WC>_z-d>JMEN**$RRh;keK@l+ zMi_AH-Dz>KO4)CW$slC&4pv^^DD`mB{pEt=4g375t$xW1tv!sVN))Xx0WkSw8K^XKe@7sqsbhA;QFcB77clt001M5ZN=*EKC|`QwY7 zCPUL9^A#=Hi<6sIo+Kw)Cl5OhGh8n>X*g^TykL&D`{i>AIMep?#?68+^HroTHb;)B zF7ON>VGG3}VB`S(v7Lg}s%3fDe2q=&&syzx2SVGa3t{6v{a!2K_o$vnI)BMO}XN*jg>Vgw3Xkmb}>^mYT)Oz%y6#nZroj)b>R=Sb=m9DqY zeexBH29eHBFBt^?-1!&rrgQnu z@mGT1Z?=DJ{(oEk5}5NoeY+C;TTSP3?@M7mRBG;8@HI6^Wk`Bm#%$$NXz|9lg%TZf z!l!Ga=LHzev4h2T+|f)%@{2UWWI`eRpBNuHFk} zO}1W2-wQ-}agHVEa^GFLXICySU$$?d`b!+gb)xkS`c$v3uXgT*l}4$tY#~K&Xl4y@ zcu{8$;+{S*(j##flK$6LrRDig&(MFz$4Cny8H;&3SmA|JkrHuLtBl zU?-&|Qjh&m%JvXGo|q;)b1D+@gdtTgfS{gu`)^~pI_%H*MJ z?ie%9xtb75OUKd^bB7<%dk+}U#LC-kpzz%1AnGUBas>m6gAWmxoii#v>-keyvr5_aBV!!_adnp*Z1A>zSv zZrg}HVgh4!!Yh7_jdJDc)`B5|?_ZJjYF11KZg8wUxrk?1h&3Q--%*DQz+`CXrd*!M zuuu?1g}twUX|0=kb~uGJ@LEvU05T8%(g5oNjU=3cd>n*TUe=B1t9 zt$=&J5SXU%P9HDj-A@+gn$~wXY0)?~?yxAIAoDeoUB6m!2#hbzSy~?6A8Tr=?Sz%O z&TEXH!Oz%W+h~OA>CMC1Wpz298knDquCQ6LVp1Ou0ov_qd8JjyL zAUs`t?AypXh#816f6hV#v@CEbd{0ixwYMUF*mVhO$e2Gd=aPZZ9ePUes3G)?@pcu& zfh?5UK1$$26po#yaBTr=HS3#{g3H9$;nYhL}{8=BCJaIc>Pf>NHX?NEksR1M>%9>7YH8(Sx%ROp+(Yx7d@Z)R8Z~F0Hln&LHE!34m5bL=#%PDi4l+Aj&vU^L zYuxackHZzOogjHJu=?RRy!ZcOvXsnbM@q1W!) z2IIF7-zx+&Z8m*f(8%5%hLT7l2f^p!j}#yI%Sowb%Dby6e%3V zF#92uPQEk2jn9PSUflPbZ)NzQEwMTnFv-I9~$xUbk#nK+i^ZxovgsQ=dj;PZf*qN}+qj$(LUk z9u?lCw`0uw29M62ddu46;uMlZhJ$OR;jD4{dR4 zwZVe^3+jUC<63CqI2Ip`9&t;Bh68c$0$YH^Zr!}PYGG}AZS9X!pSh04?oaKU&?Q&! z6oOvO`?dY#4iTa_KTM&R>SyyvMt1ScBXtVvaTbzm#6Kp$QK(1&OW=SBR_46{61lqN z^)`((8~u}$AdHf;(2$2+O{>RQLL<|a9;nZDrE1wsqzP)1gomUhB*F`au{wt^k~#Kv zrP+cWROeMlgjA;3x(0_*n)ZqDbKljE%zbo#OM`0B9EQpImE=>TLuPq;Hc5+^1fhE& zNasv-&Dp%JS33P*B%8J*T|A_&+6qVx-Gmi;Itf@-{_pfS_TO&#IW;~CGnN8uE!BPu zJG+6UY`F+xNvg~eHMfqXc1F*dF3pVNd0|F#`8{Ii>3m2(IS<56ta_I{s%!D|ERwGV zm^`yRzOhr+ob)N6WL=qVjNX^2lyq%uNMn>{5#2JcajX8RUSrdz2g!(=(Xg7)x2dsD zI$xx4Wi`U(K(gCU1RTWdl9cYdappUu2V$oQ>Ie)+5%jjghRxS$dY8F3DV(c-X==Gx zchAgBwQ7J6$=}KY+(JaPdUe9%{4|A05r@O$gq^_X(g>8%XJ|B~U9fi! zn-4g^2oO0byK9AOeKR{4-gF zW+x22v*#kAF-6%M>xus^61@{H0Otwm&iqV%9q$`j&|4T0h>q<06eaI;RHz}ydihyM z9>_`v9}jk$&5?+Uj6R0wDvF#p+E8{dSE2ss-aNTdV;Mt>dX#bZyb60ZfU&qu1%Pe= zuLM{FRGzTu22hGO`Gj38SN#rb4q{Bq(*aY17?U=!0l0G2qTqYbNPM0wIKGfq7TgTt zlw*?y!vPKoOXBl1!GwU)!jd?X2JG5`6hI#eph#X7Ao&S90HBm$l7=k|7%EJm@52TR z6{IlsQ2}V=*u=nC1^6s|ya2gEe1<-JfLsAxQl1Q093UmnCWlms4GW4$*z^KO$*{cx z8-upxV`=+T0H0;pw83JaZ81*~$$G2$$rl#n!#B$1e5kr7t`k|Egeg?Bn&XwXseCN1Db_B>HmbgGP#am_B}kCAuNHKav`GNS zO40xIix1ioIOeBn`F) z*(I8UV~YWn3J_$R8?ilQor|$OWvX?+uVh7Vr<`*ccB71QEjEv=b3QhYjB`77dO?#C z*tW1q3S3s$BnM_FY!U|#7d9z?CK)g)=p9`jET}wr(+@BfpQizK zE##F3yMxLTO(L-`mU?#TLxye9lIKa8D}~+}_%^lOIC&p${2vG`apJ4*o7!HSyk9mh ze)a4;hYUZWCEvbfJ{5WQf06K??lko5*oO=+ze#Q+VRjUFx9ZWv++?Yd6t)$0p%N{Bq!sMsmv2|9IXmC3#3$JE z3W1{ujUPebVYsoE2}TcA6XJQWz)OG`sVdgw;I}%H30a_P7-85?bS{`_u;_I{DP<{j zDS0VPDMgwhXZX-)!3;{?J}?o80}RN+3M<064;>0C!c-)ur%Y4e46~w`gvrAS?Z=8l zUXm9p1e$)han4}_Qj0x$sW?vrK$V1+#8`3eMCQ2|`raq9^CO=SgBUWX}%TjmeR>hr7mEEQ}5YuQ4 z%}^f`OAt#?N{}+6l|wC3f+<9&hF7BviVMXYOclje9^=i^rXbqU2{ZXlplC)8&J*NN zrBa32Neztvel{91ixEx%!A)U&*;c3P3rmq|i!YHt-hc;Ob4)_!Jty2^72aSx%UnTE z-hSJ5nGpFfs3x|idZ3r*7}`302DnD~`$IZ@`TCC+^anN?;>s zl?lj$wE}dboJ0lAW7$)*MA5e<*))INvl(gy@5$vcgfbUHC958Q^5jFgL92c0!-k>E zd2OgbJWPGjx~x4vuwXQ(0=SErDNz??WzU*RSVxnh0?nUp<<6Q5>4rO*`VHo733WHzvjT-hba~ORLY0H zFG+IA`vO!89SS}-nAkRI^Xe`zYSUiD`fy$*h82A7FGj2K-7C-eo7WFyFF;{GW+b_# zENcKx2`_%<8+Ggm5omS%2uH}sH~mS0QW)N0NTE`2rsn~>18O$F2~9{sB~C^wEP2Xo zIu#L{6+c=|}t2DdiaZ(7V(yPi%@M9$q*Ns6@!&cOa zg#i(P=Rjv*wR)EnZ25KsIhdvfT~f6mm4b4RK+KGS9g~0#q#VodQVK-EpfymfB~FAaKPlIa~p&pnqbJ2<;Jpo>aG{ugY{lag^8w4t$D@C&mMgIhRMbEjkBm zS445yByHJojaR4b@{L{8-;MqFu_@5gIMrx#DVOa{%~M%dN!)o+mzb{A(LP0F8!EWc z*zLLW6Q@JXf7oxBhvjmmtlS)Ri0~9)ZEAE>Sl`v}GS1>Ud(_XBgT?0Ob9!@kTE*y? z1(!{jW31!(Pf6hhtBbrO$soy=jUdTW7SI9(>Edd$>oA3{6yidwLu{X}3)nHpapNa! z$35z$A8l@s>*dcoLZ0z+uI<_T1D^SNlLME7grzn@LwAEjM~_v7wenF8e~t@!|7jkr z2NQZ%`ot2jVCN|iJ2Oe^};r!CLY2qoh!EQ*M8OO4;(>0Dzy?4#UIFMM1{+4e2NF% z1t)NTm=#n_oO{@hyA*3>QH6|hXd>vnqIpd#3;;bNag?})p6dnt|6hVWktK8 zyFlxUD+m$V7%mMzpJ96F&#q7-T6`;O{r9jqOVPA2n|gmnP9`zJj&wj*m-4G=gUStEIiYXDKOT_2J7P=MM=AC(49?rV5T`cFbmTe4jcMeN^?t0(D!7jspy*L# z$_u`?*?R8={p(3eWS*l`jW2x`P!?1h$Pt9Qj$(vUg~?08i>eKofy4-(1Sub6VT4$P z&kL)KzzFdZ`ZADa-E5u8h;bJ4iX$hk%Nwp8UfWN3dxGeONtMP0{MrQem= zMbc%l?y=6jUfEUA#nd(0rPvkOh1zx9MYGPouDkv^W!CA}<<^nbUDnyxLF<@Clum4C zcv`Am<=$gSg5a)R6~%?Jm3!KW3Z0xE{TadLFNS;kyy} z-#7g*0yzS=P}X^&dB-54{;w|ow!ON$@JQ^NZF=f@>;{Z>;Ex}>aUDf+xj?~Ov+*(|}3iHFoz9aG*?lR-D+^Iy8ORx^SaY*h_{Zv_IXTty68WIW$sSeui$MQCmkZPk-_dj9a~#ZRb~CD&+0QWhcv6E7xwMU_gqtd@{#Z{)Lpq4;Rrx;xY7v7tHll0re%)K8O_aA zyDl_$rtvq**VkMw+^6g!FMJx9IYg7U@0yo9V$nt;TBJS#dGO8kX&fC;wk2EtVpyy{ z;x8wN^+Xh$x0c&p9VfM1UeG^s@QCJ4s>kOd#NcB_UBYMX#m|T${dzKILfC1ppI?vU zkLPcTdhN#dWa-XO&B+->;lf4n1vd>|b$EzU$^`mB^g$(>&OL1IMPl_N|H9UQk5tQ( zD7CCEK}Fgpx{$?u-U!|xTA8g>d<8=BfM3z`)^Hi8RaSoOyMqajDYL_Dg4x9nD1U}W zhMSvXJC0Up!w@%5Nk6<+we)5PTT`)yPxgHZt33_19-}X|-qM$ji1#1#vq+8%Jl<-4 z|6oU}lqP*fE%e%@D&Je>FI68fZs?{cz@X?_S|@R#(JbTE#mghMf~;xbY~TUx4W@5u z`xui7OU?M1cT9WEOQ>Y6YQ+9&AFtu4MYYb$;PkP~ba4$#!xvY>-I_7~1MRu4Vdqb6 z(6x1mnb9H*<09CDB#fY2$Z$(Nf%htwfxaYu@@osu61!Ro(Q2;M512}Pi3B>r>0LB6 z!%((@zaCRzZxevr5su4AD(YqjwtncI=%4tU!`axzklWe#`)Y)^>bu(gtBl)i80>fb z8*%aHw*m~D&Q?B*?-2WWI@$${8Dw3KWqICecY2Q{WyiPfCJI46EVrM3Njg@f<6mfR zx303$UMpBa0(v_F)q=FxzB-Ci55i{6>dp}Og68rf7^>Tsti-T)h&}^CjO%RaH~8t7K5rQU`)GESU68%T7_x+x^a662=9dcX?gw_ae=fPe zUuB0`3)+wjhR}a2Fdqa|HG9|5t1F*0xbwk>Ee@;1c`*+pAf_6frM$kZkp0RVKNVY+ zhBdW?#lATzH-PT|_AH_j*oyE!iX8iuI*8$ynpMeF%wMDSvu|N552UCn)xL^vJ~hf$ zHHmMn?%pp0U9<;geqn0o$Ori@z86+)r_T2T^HDGhLAs((HyiqsxK4bi59M3PI_hhp zUQW477oxL37!4xlg#SE2j4$qL^m-$PQdQx)%e}pbQhnSupLA72$5!GCe?dIXvxG1F zv-;%12n_~xpTfj89ygfn5(QiS9ZJOE#97n!rhC}zQwwvq$-#mZ*t3x8HWY8jWa{ud zH_N^TxaZcoIb~M_J@4V(*l#8u9-cq@xx!XbL}bdmR$(qYlftKkiSLW0dPAZ9#%gGR z9qrT4qwgN(P4h!FwNt*gKk#sH(hc4&mpEc0OW0ZH*$FET$_AK6Gl__Bi_AW}L98yW zpu?DTHJr|6A5{PTLEMC#iiWU`X}Pv|m`+pN<0dBe-EwgYRlc747e@jO+lJvp97mAy z(V5buWWRY)VQW4PR(4sq+(~NercxAF`b^@`ir-jh?~GNZO(=Nb(Hoc`J1bpGT@sy| zA4xwj7s+1l7W&cx>BwBP8tSVLcUdJkNA~a|DbqJQkDbGnE=&&-bHslsack1h)n*(G z4u#di-=t#rd6hQ==ZI-SsnYN0gAlrJxi8$*AXm;Vykcmbh5wXU1;@{TwU1UA8n8=% zL}k|!*H#}h+i<-RyG7fV5Hvd#$TMxTbnD!XO2-tU?CXrmp1-ee(VfpmwV(}GMWCt8Iv$d7 zG*jOU%D4)4qi&ZLh`vshiE9v$N&nL^m$2ySU$H3OG@LVy$!#eBv1K z61HHznZj^VQ3loks&Zu44Odz%vW1U4Zu0eDYD4+e9ai+IIa|ur!Os3ybuHZ=DX!W4 zha)R^5w%6lpp~x+>gj{Ascddzg&elb6~m)WmiTtooA05~%hoN+gSCGeQSPtc7H@C0 zri(JPWh=$V+CuF}5+0%ofbRI&Zeisl2TywWwSps-P`DyT`Z43^*Ox&H`6&{o zIqI-W>Y0iR{U+LvEjXsrFEbzUi%&%3ajBzZy$QM=U#f0A-xoH)(a;+X^txpX;SuHm zY4cD>h!mW8x%JO!6vma=*bbIc$D%4|Y~|kB(JvV|qEC?Uw*W3#X+dwil& zwZ>WAawNxWS^Utz8*5zI;=|s(vx!+`(!-qx=}OTwuE1RL+#C6JC9e;P{JIOez9_lW zD_jP5>Z^7AZoHcutx~)17alRsc%#@S4{UvW#*g*0sIHma?+4($<&9mom95u_PyNbWq|$zyD{Pp*BM}Ssp7x!@W|%Q9wINH*iEV zaRFc5=M?8;Zk9)&yb#4Ag~2!_=I98w=9#XY0i`_i4`ukgiRkT;Pgqp8S4(!Jltc|l zf|0^|Z~Ck#@~iJn#*Q0|5;5Ud+x7Buu``1CpHBP}mmDA4zvHwdPX1aa@5MWXoT4VO%{&-E$+Xwp+P)&caS{{VYuD(4Nqv2(pp{{i}tdLMbo+1doCBBY5$rs^m z$a|4kYrp;eN%`wu=dE70_U#vtmhn$DEFPh{vRcO*-l+=}gSSC-c5tw)Oy}`n&TN~_ ztb8Fca)N<_HHGh~KiK)73@kw4Z)JYglCLV{xebZ?fg4+G zvODY+0gO{8s{+2Lmv{70fVq;geSp?)_(+6n!MgcqMZUF-36cn<)Q>-X9pqYdrP7JR zVxl`$`+hmP`V=@lZ3!{3a|eBbDd5d~;6Z%`Ja&71idnmC?uKr>qR0-^dYt;SC$eb2 zJskA4$$q1(0ei_)#ORFOvhb7_e+H_hJ9C`_&fe56f_>2Nb=#J1e{Z8i z5>LiCl64Sv4x<&@muf5sRoDGK??Lk52KZ^AkVhN&X0LGp>L2=|&)JNKCVD>P& zgyYo6E87oF|oH>GnYjIBrc04akI>2Fb}NUO^oEVOzK9l-fD}~2UL-_ zZa7sNyXeHfS=-fSo)cOm25{;U!OuIs6IqY}e!h+5AtX^U7)|$%AMS02VrH8oh;lC$ zM;#q*yu$TB8St7c(jqEvgLp=09d>xb)w(G(D8o73iDAt^yIo28-Oykn=om7GTtEnL zU`={ZNJ}zZs%I|!@>H2E`Ag_nlt(G?EVJ}|WmALiTK+s!TQ`9}cPl*qZDoQY^F4na z*L8f%=}JCM>04F8oLP5#?Sk*?%l^ME^k(!_lKaKPE}Ys5Aa*pV6pJ-B?4P)$HLNbp zbA=)uSfg~kN9TuZHL8c=bSrXK4_2SG^mv5Wry&>TZ9=MbjqZ!yIhb}T1H3Ajqq{J( z7jMs^ZhqPioKjg1+uTl3>NQ+H<_@$(Wqe72hq@!PVCnX}{eW?l4ux!%X_$4R*G3;!VQ#>7L;JQnM>J#qbls@yucE7 z?sr+m7c-vKbNA3u>+KK1%7&%UAlIaFIH?dF>`#`e5Lr9df^2C*(rx+^K{7xz1XNLH z{ykAhYP+Q%DA@`>P5E8566FCtkPa&W<9IM*Hnl+ZASRi-S?%EJXIGdyZk9X5&TNnN zSmKk85`ug&xf{%4mA0g`wB^uDPi+6JXvYlk6P&1@o5(5?cf-l56o=a|@YuSsyYM`+r9O?*BO+(XRME#=H&has&V zk^dw|JjoOgrH;dEq*>o^X9Y(u{!_tgfvqe0r_nhT$LCL|nBBo2WQd=&*hF@C8LpA@ zqhP;MxtpsP(nXoZ@rv8&xp1))uo=GoakaF}PDAGdor%O{{?Bv~KAg9N_@Q)>Z)yjv z)kRsLT7XjKZUx#A4(;#lugZQNr3E(@R|!tN_tF_};U+p?`c-^F%KA~jq%|>at=w$M zm{)Q7%xV9v=`mzLpMLbIVSro9^1?*24zF|1(${n}^5{WO_^shnqvJi`B31%t z&~7&Q5mfIm-8pt_EdYZr9ctW>oE>O()pJQy0*w+p18XlQy=PbHjYMxh2`yxOk-)0Z zzW9-yBcmZSSiMiy0cElHPEZ*A3jlF?7bDA=Y`3~yGY$@Y0>M(g+X?I10uj}P5Vbp3 zHH*&KG%mNhx{R0uVDp4)m5d#y17bwdZ>Q>6YLS{NY!IxgDD*t(jXV$gh09P(7OCrB zVCy~*B;fz)%hw-ie(yfGfL;cTIIQf+ldGNf2A^;JE{$j%Z}r@RXYtSP@;Yh_^?m1C zyjV$9keCt6z}c*gAD-7GKhDxRE^=059a|_iDnjh?ZD!I^#wr#Shk6ELF1fjX>i`Ig zaHO2#pOQ`Ie^RY6tuu9Feetnq;YARs&e0Wt%Sd>y&SN9tmH(OTgLo^7^5`X;*>rJ{ zS@xSkL3WnTyG;f-KFu zMh5oC&;4A9MPO{A0fQU~*tO05p51rmWjGsEM{pBb(w*KIu|Q;X3Cw0X4*$n>TB*6W zgy*gk$_+jW<tH zbDW=2-%MPHL5D)VwbKM;4%=T84Tez-C~;yguq1%BzwJGV7$=6sKnwL%1Z^F;fAqUl=io;*;fj2 zMrqlf6#@rD+HUGG{kln?5bP0!^OyMzgOz1-21Hkq@YR^pS-wecq(EGVS+Mbt8I-6* zai&ub(Z^^p&8eg%_0YWcs*dy?KN4#QE813+c5K>sjz8CmjbuBkY(UixR;rCtrKi)} zjW8qrz=QV#{z@usx(41ZtV6qw#oD!)?=z=OUG5E1d;)BX<`a2*MEceYK>B@~qXm2F zyUTC0L^>bz5q$7|xTf>s4D6LlH5!&_)aJ&v>xGGUf0$S{3u&Ee;LAu2`g{`>F=ka} zjwOqeVv`7G7ViNIp5vCtd_<^4{wVfFFw!4Qxg%ahz~%ea_uRJkQM;3nHjBjU`g{fS z5HhlaeAEn^3FV<`HF$AxIL=6^Jc;W>D!UYKbms$6;@$CESgzt**etNUTNj#k%-1NG zmw5+UaMX9AG-oc=2l>k-{FM`(EVt23w;JduzFw2%uxhQ0f(g44?DI*DQI)XLlnDf# zr$^uF6OGylHCR7=g|fXMLmu#VUVMDKoX8S;oI5;ZMgiKHyLuSejV|cZHlFw%oP66E zmf;xFYrOM(SuwEc{|@|JR-2z#uR1zDZz(9Ykq)z zXd_D4MIj)6J(xK!4AH2$%8296(XM}Dp@H%%En6Bv||fR zOAZ^@b}9Q>Bq8{5k{`GAXw0W;kBjczlkoBR%S-5vAw~*LpPQi(jJQ(zNc&vQu$zfW z0z89ZEvR|-a} zC-bTv5xzqGrXuo^+CTRPbl)}05{ps2TVe#Pdp)CmByLN$2V8d>jCmRPl{Uml?6H0yoDzE~K;Dpbp1!+^zBH%5c z66mC@@*-&cnAR`HW0-Nw_34K~ak43P1F=%5?b3D(EbhNHM|f}mcL)-TA$;?gw2SSY z(EB%{ceiT%hFfstZ)AyV+*+vWgG(MYpGx<2AlazQK%tDWdpcmE-0E7&r~rnA+!G7x zcU8caeDb6ZotSG~RrmX8jBMuvYV`N8!#XDSl3%l^J2oe|l+{c5z?(C2Dgl_=DjX>> zP7VZ(BieLimlQG8@McFr>^EI}Pg2l3JoH=pvhS{0yejC`WE}h2Adb7_x+5zBd%jbK ziMgQvoFz2jG?5%wlr_nb@}W2R)Fd9$EFL34#HiuWh!F1JDV3Hx;Ck(qMv9z_bkJDK zfDC71XRZt+ZJ-fF6)+gAPNtS!~3jyFW7YBjZyCI4 zZOpTsRd#j&F9f9)UtyrhyHw2`BfPlsC(6RjFg5CDR=K|T#CP4kvVCeSo=49^S*b5= ztz3<_jMU%#&%E^-*jEF3ce-fZ{bQ_adWe~ z+e>=qv8AdLH^{P4zRd;Ja8rm}bb&(dt*N9FcCP3#0fx-{z;8&*5CXj%X1~5D5)<0}mp9 zvwb8GAef4Jc6otEvTF6V|OUpbQM#l1|5@ZOhx2@}M%HrlZ!vm5xqW zptlB09U7VRY#w43iyaM~lZlL*HiReR!OC(#Xr!wn)KeSM6R0~T1qShWp(ZF1vdvw` zW0J;W7_zuhq#@FU1e_BdOq_a4t>6EQl-4SkXI2iU*Uhzsu z0pB^ddOHG}8T`>UDV7T>VZFLAakArxI$-;+&tEk}^Jr?3rFPH)*W@nEj(a7mQ0)SF zPho3&$CY4ByK{YG;+Jw8_dTy24sou0ay^NJZp2tUO7>FML?$|nce}o)$1~9KD32+D z1_!sdmwxVc3xv#f2(x-b2drwgp?#L;ZG|=O;O5;`)#V9k*Tc`(Gt!_pACYx_7-m%p z3CvhI%H0eY_mlOR*hVNAp%U58{Fv}i65{PYCOI&Qo z8;w=qw0DnbXA97-yQG$nweMEw8d5OYW!c87*lDJ?Xoj2pt}nMrB(Dosg#7jO#nq~z zN4AYSNkgP8n;HHe*9Vr~24vZb%-7aOwPWCk2=()!+o`A5owWCbt^mLVKOm09Q$ zL){F{yy3^DL)ga6#R7Gf=%=2hWs-SH?->=Q^Hqws*{61ZG!=GA&VJd!2xOI^P<1#( zKrRj);3F}U-4bRti#x6^m>g@&aQ%k&eGsKoL8nojWl1DPWp*7sW#mJR9wRL!9*C=? ziF>uA_RV7}n%0L7dZ`HJ?2WX&_;c}>nAd+Mll52a4}bHFnsEnqM_?ee&z?hstn1YZ z!s%Nzu`F&(VUFu_Z@Kf6pA6Ne;B3hKoS36Dl#xAD7&GiIJo_%5AwktL{xhUxhfL`g zlVN@maVlWGsuX3bF-E?afBc3jH&kSE=th#!mE5dazm@y9hz%QJ*%uXSr-2yU!B#vU zTXZE*`6@{}|4^7?lBs(J8|Q(6Ey+jvZWIj|6WzC!YJ=L9)^L!aOleLm)o@a%;?&XhX-<^^}=4vCT<3q+c2Z zyL9g%^Z&5*jX|0;L72N^+qP}nvt!$~ZM|dL+VRYeZQHhO`{uj5xVsEl4-Y|xrA$>oHm$(*IeRr)&O^oxpjYY7_6zY6w)DD0q#&tN#n&1~v}xdK00Anx{w zq)_&EDH!_p?PJ}KqYeXd0qoF`~rkG_sAly*wl>soZP28<2!_M5UQslzk_~yf+t?4J35fJepnn3uW zs5-xg=jGny8*Yh%_ynUbdxqzw0#3r$y@Rhe4$+~qdbJ-s^21Fmwn# zUuRyhR%X$0@5(NP&lEg(g{9UE&~y9ZwYA^G^W0?%;5vRtHWoX??qK{9u=vWD!kYI8 zVS8lU^VTM1KCDai5GibeTWAgd0!qhSpsGgk&8t!wae5v(=3vDeb?x5)h3MvI(?wD@ zoHVw;(mpvy%2-dE-GWwx=Ebt2Rf`y$Pl_l5$c@wu)Zxh94?B#E+VNJfnn4>|C%t=^L?@f+20)C5*- zPOb|`^J}t(pD^V3LOSyINmn>6_*8);`CcnXiu3iqOZVL=xk@bGl_6sZ<>*JKQOl{t zK9{wN7Gq&ehL=v1X~oh-<$vyHwu&qFUQkM-_}H%6s%GQi#9q1iwm;>4H^$EM><%M+U+UqO{=4J$&Lg$ z=Yol%6Chm~1zXfx)PcwA4Ch;`YvQ|d_}$(m+v8e%kGT0MZEyCUXjL9X zNQ8;XY=@n_voXgOwrV}i16-YEp{mIv??dfvC$V72n!fyL6j$SDX@O75q!!!u0hB!$ zb&(mO@1kJHZxF%A2Fn-7siixVfUlrd5C^@V(Sz$M2i02(N9t?k-_T{Jxr|CCRs~|l zH+RXbd1F4UxdkR?I=!QVXD%JE^q0IzGla*8g_P>9Oh;F{>%ZRf#z~7t5mbCQxKUvB zc%4Jpkl+OY^PABQ0LBKV=Z_!AugKje2 zggxK`NI0U*EE8DXlse#odJ*}=D{0mJh27;#_7jFX%$dfpyn48YahtEGYfN%0tdW^j zReimj`+`VU!eoUQOCZ5P-#v`rlRBJKuiQu5YU)~WN8K(XZpJ2(&20F)sA-*IHTT*Qi={H}tR<=rgu-F<{Mdqd`tIP6qd3!>o&*?&B#s@({3!c3oF0WQRTh$D3(!`>e7u zfCiz?gq<0xs)ajQmegn@ns~ZaS@)39V7Viw=Tt{~2(!M>do}7h!abgE>W*S4?cXN2 zMr2plPX&-S=DY7+Ir~^}APpu{J7DL94Jjgq~jPJ&)W{VNNP!3aGmv);lFF zOfMcBKgF0(yprqE=7G)9`WUbXiax`F^R6*plQHsl3EY~KjHQI@DQXuK*a87x|7o%M zK1Z`-rX9i3l0!A2DKtbApJKNQY>_eRDg?(=e*oM#P!byI#__tW!pu!8e_f@k2wlSr z5dW(GHH2noHdm-14zz@mxTVKl>_sf%6T4y?);{5%Ls(1mA&7gD=| z^~VL_Mg(EeL$Ky);%C}ihL2{tN3I?&O}~Q*dhGXk5&Mtc@M!`fK1`Y|pP&?<4wBjW zBlsd>r$NJm_BWd_n8q}%R7UtB4CvDCfDu&k$)&bgZ#-->w?9rp^#!jeU?iTUXSaSY^!iMcOoIs#5& zI`eer&|l(%rnL22!_?Ihc-UkV;d69otpP*t?$O8z^y{R9jX(kp3LoNlwV+XGw250w z8=SKwRC&=%tL~y&Q)*F!KGXs-$_`vil11DAg(8pHl;n_W7`S32kuq=C?onai+zqg9 zN}?Jx_BWoB?q3MrQuqr)=g^i*=l2$u^=qB_s%wd&D~hhvcXU)0gVfK(o$4GR$7C{4 zDGB>zCxWzanG&(1`C+%y%aqBZ*Un%QRA-c-yY3~D&RD1mz$A|bilf{=9?4*Jx(b5t z6E}G3Nq&eK%}@|d&MaFaf$S-$mgUWSEBwhGw1Qa>04_NERC6jp!6~K;{1`_k>34}J zgt}SFXzupT$Lg0*dK(b&fb!dAd<*0;>b6IUmhlQlJjxj~!tXJXqsR1m!!n9xIawXZ|vXS`ep48g1kv4#5?sfcr!03VOBlI$16LHjH2h^0wO>C0A=Xvw@&iLK& zxau)XISt1Q9t3xJO>|o6MWgN;M}=FOD0mG43>4vs^9yLo{y^S1W|Msu>4h_>XpFg_ z6$qs}0lk$wYQKQlolXt4p-M#+Vk)(zQkk{F$RfO+Z{`ZDFBm0SqH8L}l#cHa%$oL; zLLmV>CE7Rl!LU6j)ZqIQ^c`3WRIh7^XIsLF+p<1YE6x|upA9)1a{m5{n&M<)o)LH< zO`Q>aO-`s6tl2XmQ^cyV6j#Y;bFyp28??gNay4~jvfRNlW`}{kFto+u{)J8xLqvXx zRJ07V}hmI7sV&=`Q>{JFywvZ&WP@0RAu z72hwz+Tp~_o>{N%ZFAscsUr(B6y#4z-d~-Ul4x7-C!bw5B4nfhx+B`(0(p2tezNdImh0ap*J=>JR%2h&KB9k){JTllv%*Kr1o^ZM zoL>3b4?IsjPJL9hy>T^}d|y2lB2TT^J_)!wHa8YFP(+GDnqWR&rJR(Mm^s5^QZGsa zJ1XX$=G5j_|By+rnAFP*G|x_kz@EIYS$+~wDZuv%W9ia!u37kAu?-c*p!*D6z`FDV zEo3w&Br~4)<>z6SwTI)dZ&4v=z;hj_=d4)Y+`)b|2ikD^XO}65KKYvFZ5=&e4K#%{ zbALn<;CcjPY@G4Qo9B;|KBer`7JirR1AJhU%GdYh%YF4Y`OWYDveS*^`(5+f z@tJkEc|jZYBOAo|j_PI>t0mZflqTRx)V>HRVvv7+IYdvohff`tKUHAWOXcO`F(9)Y ztrV#IxFXM2@H6m;bW(aPRw@ts75b+An?3Iv0_bZj_fV~eyh6Xlj}ZgA8oGo8flmu` z-i?&OuC%90t>=2+nBc>lQoYM`)1LL8=r-~Px>w0(BwWqD0*Gv<6Le^pHY9|bk^7`i z$vf>d*Yy0i=}qXeW*FCpo2}>&J|lz`3u+j+vmohDdp_a!hf4Pv_P+FEHsxlY(x*>6K3FoWaG zNnI~TC+(gxhLlrW7i?DcXSpH%h9()6_3?iLG`kv9|Ihi#MU77JgSz13?%%p`zb#(Y zfuxP}!CNpdelzRSe{8}%^|qP(YXG0v+ecHc2TpMfBI%fimO|*zM_SqhvJH8D+N-d+ z>40tOg2gLwkfseL>R5xaY7PMYXNnWk0vl5gwGkk#%K#1{DEV4*g2nvnbMbB!8QZfHh{)4Kx=yJJGKp&7Bt#`*&=Vo4OMII({z6R{`X9K zqoAYG{hq~K^M}k!2ugAUSk3}E9~M$RV~Axbm`B1~VonkQzBO%>hY2__-NE!usEAS# z3lR~KQdA@YBR&=$5wX^hv6y906VU=XiAV;2NXm-82%!ExYj^4LMB)S&Al>Te_uQVE zey3MIvh@4T+|R4vn!Z+_LHyT@VrPUvCD1aO@T}VeIEv@LE7S!QA(A19Dl5oMSzF*NiMm$Y51xed&#|^(2CM?cv%W^tcx;>ovDw=6 zXGq}Yv}wHG)Jdq8*xIDrLlEn~X{p)^R)>FQtSEE^jZgQ&%tu)bYF1BafU0S1_7Ia} zy?QW=ql};~$2>rcIUABNQAUC->zkYDW>aT+XoG`$)sLvCZoq%)U~qqzI96kRZ;Z8n z>)hF*n(q;4v@Da|xTgOZUV+FnMRc#PR1t98z3kUP5rS{hMiu4cd2&=J3}zr`8iMI3lV(Gwl6ZOn~4 zFElQe)rKGRi| zU2Z7SUy0*ib(p8jyyZkI$mY|W@C%|I_fJ=!*w=SOZ;DEh65QAG1X3T%XP0S0hsw%V z>B0{T{HhnJ6)mGF@L*+8FXp(-dNOLN)tgr&eYz+wRjd>9FEmWPYejvIOc=~^Kl1PZ zQ#>-@^>J6!<`zqix<006X7O?kb2p+wfWK0qe|Caf+IZX;p9nQ)MS85`DzA4ixp-NX zENDz)!?tu2N+F|fVL)#*dfXjcG^?--Gn){eoFQII89)B$AnTXiX?LU0f>D7}u;ClU zbPX~K&B{+Nq~Z$8;>4JQ-c7HQoh?wC%7V?9j1{mOSbnKN$w}{yx}PWvuMh7_O%6Gg0=-T4G0Ud+gQ>%Q zXiqpCiiqVnzzcp`rbR~65mq*e)BVyI!lPwG2PzcG8u zyS4^I&^4v4E_7u^UkZ1kyhm`Rf-k*oz+|2vA@^tP*ee^NZ6xy&7jWuOdT^v#G71S~_t$9RvqMsVR*t z(k+ANe7#NONmtjofoT%g(a&7CjKm21V;&e9vA&Xg9^1!g<=1Lj^%0EKHhAU+iV?kW z7VFthrPjJ2haK-)@z@B6@Fx_Ipz zasU`6ovy4*t>}PD<-hP0fNR&Tt0*2lbZ*P4lk=eB)-ezt!8QgffGMFie17ZWBz4Vg zraUlVx^R^KMFzs@>W5(p>9JY2zP0!0GIqHI5Ql4K6ZNL!METXK>&Hw1szY3jhH>f^ z2rT5QMv+CVCT83`j%NYe+UGa|s_ITm(2qEk(ogGpQ`wIfJZ&u@T|vJG+n4mUgr)}J z8LESrML+{C{97IVFYUzDJ?NSVJcrs-6Ub!@;No!{@yKchCe%OLHxAW)TxQEOOC1r# z!9_&>pIbb&_>pN0{5Y+vU&)1eh^Weg>D7HuqOj{f(gRrL*tQE=N2|E-0L;u?$(44D zHgO<*d2HrUx_+PriaPZd4xT3jiX*7Q7469?J}+~II;esIXmy^AZL6nE z;9s3I3xu8YMXJa7Ygx0fPhr3^grD+Cn3r*H#L3<|b|tEXXZ@p1gS2p;7_4CG9fa@t zt9tXMp=$@$91mIpRP7B*HlP$x4Q|Krn729f8;Pr}S5INV3I8Uu!i9FsocYty<+-YB zsFe<4hxa(kWSg6_2=#g)NCn701Z7H&bgkhcU`?rtlVpmTV@FMz8WjNPkFN3RhLMZa zyCR%Da(h?FHbob%fqx+IXly}_$%dc8F>A|dnHUMa2vYk&jYXP zAOLNG9H%F2O3w;#?WWgvEAKQ@(+2GYJL-w3gRhDwAOS0hOdox7*Y3(&D~zio$V$Gl z7VKzo;n^aiKgmjrlZU5j2et-gOm3~HKP2=qQs9+?rkUc>Xb4bvrGIIv1zekBMS?XRt+NhbRQ z-W|nnXs6zre~~7a0y7mPt5)UZ*6s*p)>u~-$_1Oq=B+W&*4Pc9A9+#Gq zd^iF6cYR2(cw);=zt-(gO&oP(JU%ZBwbHdU$d}hFv8bfSr4DZgU3y}tQ_R$s)D|Yn zQ}e^BTH1W8wUy}5)uxm2%PZ+7Q^t=L*2d1x4zMz{IqRFs>tJ$JR8y5Fsj4b-=;VSN z7Fq2V)cRzDCC{yl$>QWxG*pMlGBr%8%YHSvYnrH-d~R5`cGPucOlrU-qK!8WHoc7{ zXs8UbwYsKMHT|uI8A$DGSWI3#WQ$W|tFLUslijs+Crf4hMZBznqNb8xx~GxX3HwN0 zR+ep5k)^9ZueP9^oYLEGVUnkD+fdFVih`k{sG`lutVKtgqruY&Bfi)}!*(&$n5m-> z8Y`g7_@Sn#r=c`CK~Yv#-TXTfR(0(RJZ!4El6pKP(k9tug7B~jD=nuLS!`j4grU0a zPkKnJrfL<0v%yDYTOFZ})ObXNel-^xN0XCmU)42~t&iFbq_@|3d!XN@8|awZU~?D! z%k`^IK%HlE7jw&Fc5l@+jIEDZ-sDkp7rVTl9m3;O)is>0kDL8@d(iiJ7q@>WC8gi{ z_r7vnAj~t;>-HHr&^s&;{FiIbqi*pWGFs#>pI}z@HGab1?|NOcU#isS^lV$^-vc5S zeg|Uju^7E?(uuYW-!{#HetE&4DM4`W=R4C9>WDzA9D)dv8TzDdkm1G=abkSLQH0FF z9>ys*{1S->1i$l>X%M?a@X%G`8+RJ7A+SeXcT0?GW~6;7Ckf~!oJJ(mKA#!Z@mU9V?Mlhir$pW*$1!Or#%D_2u!8vIO%os;b!8o@;I5P#O;0332_<*#&0d#npo!;DB(_`)4T; z9NWY>;T?F>@Dlz@t1CYQTn~+4!aCOF4J*efK{;9q$`X&<24s;En30aGLO6cm9(>>& z@PIq|f;;Mhb6y8#bxS&d5Rc>&kc-lalMjtZf^$CmXL-jdK{{dy$TE+F`)3UcOmW5C z`^4R|jCkN3big=n{o*_pobr=m24ViMzLDS*uO;VOIHMzzpe)=7B?#vtff?V(ZP0&O zSw~iZ9H0EN^a8RH369|jj)8Ye0!bdAhy`TPMks%A4iX#_HqpzF^&0y=tzP$^kCMxH z8Zv38B&Q0}V?-Ux;BIc>l6C4cxw>Ruhc0^r$ugV->D)l=IU%0|W&iX0_wJ~s*6@8r zT-i$kL8gO#+LzjQliEvyG4byEi0XdZd-IbJus82qjmw`x_+N#aqG7j!cvM@#z35Fw zaJI%aEZ$Fw?uX6I#>P4vX4X@r9a0y`1pPU?)w)3x^0d9&Q_sIv)E&#c?busCV^3N7 zfq*;4B`g%H+FllOIxBN+C%&~A*2+$YKm>O4>qZz;xXsN9#+4#sGM(8QG;EYeBl1{A z&X{x@GqgRL&KuA2ld^)w7hTY64&rcT-SDpp6|qiz5YnCkSiE{chD`l}&#+W_JzsZz zD<+WkT|qa5OlK|~V^o%^2n^W3*@ar~S8*L3Y30HcdNr24Cbx9ZULIrbR7P0_O|R!_EgHUP{##Y{SKy2go6Am$nwR7HBoJ z>Xwu%%k&wa(oPxYnUp%>2ZlMwW{na4M3@PL4pOM^p&+OBp5OQ`Sx(TK#7f=QssyVE za$3>S_td@?PjGj*m|xVHNTrE<&Vzz?lP(GB{B`dY@!MJtJ4f}&j0&Wr>rRTMpJYD; zNetjFgHD8}Q8Frk6jFo#EwAj1xv88KTZmGYmyB444?{mWCiSNQqF|7LLh+C)GQM?8 z2vpk8Pt0%Vwa;j^#Jj{>yj<7&_NM$;jn~R!$&e9IdSkduxQ3$;0lm3sT3>x_*{y-L zmxLs3)s%<%hQ+%T_qZYG)vuDwSD zvgqhk>|`~*m;)&P2nQ0n7*Z8*gKHjlvmS0i6l54-DgQtkq=(K<&!9Mc&-hNmGeTAP z2-)23biXM`>RjxV&rWnK>#{rB57eS1MoGT^N^i8A2Llfn#_PJFR zbjhPf8f-m$AIpElk%(KviiANw8D4>2sXP*_Lo;5{XFRjANRLPSdcx@i7Vik2WCNM` zu#d9B`KPr+*%GdWY<&EAHN18{`O836WIfZn| zJ`Z>~wq=SSykh)4dBUOsxUxSR^jTL$a)9ij5Tg>aOwDz|M&at;#YcC(kD z*>7tSuj8uZm+?vH`0Q8?nK#d7Ej@w7rN4k-pGRL>c(MQ4#QV*67NIazf-Fy}9 zkU8*gP}q{^l1EB_dn8qowvWgeWP9c%e1HeMC1m}iZ-VUbW6CGNzalb{B>w7f#e5}u zMO+1EorE6|y+iZ}{Rr)VdW6Sq5Fl`a@x>EW1SET7U2(V}J#jlkj;4-s)n$IbTrsVW z>W%s(2p>>z%7Eo9F(KzWvQ>JN-(4>zNFLDbk#FyB7&FCyjIhm!mseI*5inb`GMmgP z+j&WlVgHkZiuywWY7DiJKa@yzCt8=~oM;!1pZLk(OZQ6H`txX;joOV`9mL8n*D^XB z{BH3u>Fv@K!XrK({CiLS!eh72VkV_f8bb3pmEWN<@H0$V6BswiO)s8tMdqBgPWoxvlim z?PCtGBW??Nfmq$t$yb{oY;=s8lHNXluM`S0ob@BRq zYPkfoBRmuFMX#L0t>QZP0EQ9dh`XX+`1VXfO%Z1U(=Hyph)ppaa17b@PzJp1IChx% z0qRf@>yyj~sRUO-7iJi_!2qAVS~+i8L*PB{-0nuwF38!S6Y(elI)H0fJDgm`s^c#C4vBvh^aqXHvC&q9n3g* zESNUK+<)C7>>&;m0>}}jiQovjV%;Hcp>|Lze*bh_a9RWh*sbh<^*|XQ1W*i60X+IY zYaSD`5qctg0uToV0pkd2MAf1jfS>ljca+cZ2802_h}$9^Jq!vWEQE>!@c;_|(;i39 zJ5oL3&n-tdCVu;MkCgiBh@8&MkC5Dfb`NY|60nZoMsOvz5#~;?fdxa3C&Uxzj&f@< zFc0_y00XW=*86h?pkc%T07-xmVu=U=nfM>!N`XqTO7X0)6~Kwu17RovH$o~Aow#;* zOQ=eayjfHSWCobows)y4n!xAJ0((BkO(qPxuiahZf8&*|q%|RhyK)3D-j8w^uGmDIFgqR{mJIzojvlGQ5g>MO|wWdB0y< zTQ}Yr4)1$+n?R?RkF39?$~z6FOzi2p>tUwk*5Xfw3=gPB{f^76rW*;JZNV=NAqYL9L+0A#@ ze6N6MuZ$nt#`tl9$2W*B!7~g|WXXDb`zICMTU2SztXweC)`^ncn-Y{S(#Vj30*TS? z)%R$b0GkfDs3UpnIaz6s+vW{Lf`jpgj6Cowm4;}S*ixKbT;oq;J-Nf15Owhtqss;d z?4xadK=cXM3ADl4o%6TXDGdpB+ae;6#t-Cueod%AWvz6Y{FG1{1v1u1!h1{(RV^wT z#CS-tk#-U2BnnaqLsCjb76~$z2|{yB7F7)@Ys9z-G17KY=+0<50aJzNGcM{>0|8xZo|!kHGplT zZtgDjFXxZe7kFAz=?qeHPMg*=0puZ)19D}O7o;T;Ye|j_`v6UI-NI`kd{$$9h2gA~AZ@1;#4xT_44GX_UfhA(XhN*d@*)``JKgfI?~l z?C9v|e!_jicp`Sf=BUjmb;2_FGs(5|l{8;GPnBgX7*M}7m<&Tr`%iDL)QcHDe!436Ab20+6SgPrZHwL zrVUIROlqbjvj^jg@sn|mc*Zyf%K5nTEKWLR`g)#v!ujmglvW}qA5*!bu8FP*c!~jhl_)y@u)IOFMe$ z@icGI5xK|0y_{yjuA0@`_0mlk>xU>Szei5!cFpel;s;8i=JyAb%X>+-W!FhmC_i?8 z|EWe%dbc~&9npfSWJCyQu+ktB?m??@Dd{k!oOC;f&gdzZ><`<%{lZ3e`}m5zZAMtbBTV{+Df#)(#ZM^K#;3 zQP>61`-tG2cuWHF0SzXBw_WRtu2c5$Lxj*~k`f;36^<_JkE;k~k~M+3WBMdwwSir47AxG%|Qt6(=p{lsq4@ZYfi zCC9Yzu`<(mvhWU`QA%CnJR4N-&*jT~7GNcAPGd$3TD_FZ{pUvwuvOus&Csb%ZJIXE z1uzHuyMT^F}S$2JVnX8BU zja|P)r7pg&*oH6IB**gb`W~(<_9pV!ogPjXUC6(dFQAsxftjoyaIE-K^7rw?SY7m? z;5CgQ00rgMnWotJAV%H*JNm_&=(YGuo~`xeaua9c3UOCwtC#1+t+T9H=FAr?m`l;( z#vr9G*KF@gE_Mp%TQS*JTdfJFkv(9F!9R05bl&XgLQo?tOhGFnJaa`4>U~~xtJ40R=aU@=Gk~89mk2smigQrmT6x5reEjM+bKSK zTTd=JbOvfQ7!sEBR=o2}RrzEc-`Qf4!cDE~@NVe7Ky4rDSty@K<+Q3*w`lgMy+(K^ zU%#jjH!jss?KOOOQg@Yy&1YxbLFf$LAD=nOR&dztG=yW2K2cJdPS%>h*2pe(^qh`o z-NAH()*NKOFLvps7Vnyw(`pVE+HI=3OxxLXt?{3~Qa0w~CY;|h2tAS@PMJKl>nA21 z%Q-uZ?O#de#OEHA942}-dYp{*XXPkfe?^C{C>0Ni`I_1tm^}^gxafYmc6ze=y>nEn zY~;nf1~n4LY#SdXT}R09XxZ+eZXZ=aebVU-(nfL~t{Oob!)J>Pm$*_mQ|ql>hii8G z#a=&MXU{cR@6XoSur_HcUEA=GH97Eh^T*E1WZi9U^;<>Z`K~?NkMe$=p?(Kw^vel# z>k<$7^$_2KYp-8^Ol(h`?_HX4@(xpWZ+^&^q|_OOgkCS+m@R&E+bGstO(R&IMu8{dcDfn z)7Lyb-*Gw6Ps4aU!>86i-J@?LdFjoWA6I8hnOv?xx;kz$(5?P zH)QQ#-hw>$K<1#!-bCD!)QrpBVO^2n`z6Hk57vKB=4$pUrP?X?t9dlOoT13RM%CQm zJe^s{zII+TM9+g}-9>z}Z2Q#Q-u_RyxWh8G6#m1Kqme%uZsrchSufWK>?`-J=F)rJ z3fnz>ob;`|_iTmgsX9zKy>Hg~JazZ>>Cz=tgFkz=Fl}}4HE8+$6!bKNVfEH*u?A1z zr*iOgpS3pqtiZ2#?CV?osE6UZf$ipqb?Bkp(OW-{9bH}t#rQD<7ScspE*IH+RN?1& zryACl<5NL>WM+C}pr0PP{r8H0rf|3MG9%XlmZxeLe&8nWI`^sKy7!pP;w2i(H)yTP zDZ}fnHTz6!xV6*rYttLHhBJQJi{%5+<-6Os>=l~O#rAba;Ljr1o438@ysz%0o;BbU z{*KO)=-e>&ji;qW<}x;tn|0sK{N*y^i`Vk2CgVwZ)$%pfk)@-_$0Um9-*JN2xAZ#e z2Vvu9>afL2h~-zm^5e(1dVbEeR97f}`4PXXHUoNA{a9#fe(Qo+Lvsi+AdeU%Sm#FU>0Flo)j8Ytt1UloFf zRfs$I5f*Rn0=V!Y@fWOKz#e^H1hVNv;C1DPheZpH=y6r={jOE-@m0!RW06(fBKyXv z{KBI_Ew}iDC+V%_?{~>qyG1(j_KYGIKh97szrq_Wefw2ezZd9OyZ0?vzZ3ab{dQV1 z{I0D3Y=X$eW$|J;Lrm{du&!9BJMSb2eZnLRiB2BKi+mn%W)yn5 zBO79K2#D-Ue!ZVq^bqS;XuO7lU4rX)3^)_L%0s$pP*-#`l;m z?SjwmQYpRuv%tQl7){H%qKLgNq$dEYCjuK=5IUP;wBm@Pr8|U{a=8nOKH<)o17TQ@ z2BC`!s+&x6m~y)FsJkW&lZVXROxpJqla}WP??$5iZSb+(TW->$2}Sh<{?}TAQyK$XtO8jC?e%w`YMq@ zB}1xKWT8m;s$QN%1*-5C^METjC+aK*$<--*jqvwG$1~Nwrb}a+=Fg(nqwX%Bk2*fu z-9y!z>aMR(dza40a<}mb8lO!y^LofP13w-;gc?nF3RCU;9p|{|tvKXVaL67wWGqP~ z5`Pg9t0b8xcc)I>9QO&XB2j1L7HM?e{;4}A)G zmxffn(y20K^3g1h1fELCZykc@V%f8#t32DYl&d1Vv-qn5+6GZ9abOmL=n}m=y|_AaYMW%T8P_$sO&M;B z5@&dyU_K&~dGXW2IgvLEpFd;yn+;lH#nh)#7D%61pBXIazS+Jy8A8i*jdPvOxH{xA z#pkDd-xwZwFMc!@cwkdov%`>!$zc;0trQm05oHLaryv$=CFx#?i zQ}K$pPvB1Loyj*TQD=3WQCWqp z`#T@>kf$t|n?Eg){Gjnx$r8jTo|}kQFg<@7_A~n;eM>MdteH_O;_^($Njf{Ncu=uG^NH>vW1U$(J)g6FGkY_$fa8^$mUcP? z^%Tqyr=N?QJ3b+OW4GvXsdTAq=;4~>Qh4iNUCy9p)g;#x)(zL2tFl^MYN!Zr3t4al00<#K2 zX_%^=itp0THp?atsl2bcFaGBe;1YmR%chB4tZJ3mC}~;GRV7=tdDhfc3|BFH7SB}$ zT?Jh{sG7S-?lXJ#P5(3nEEQq+{oGxCmXmRMRaPAUnxZ2jvX@YJKmr6rgoG0Rvnwd* z3zR_njfx>;ToaUd^2+M_rd)v_Jo&-GD0y%e)|q%&|3Vu0J_+b!Z1{*{U^HxNl!xxuW}Q z>|WX5*1o5|r(^8IIEse{T&cTN zVjRS{4M)mtgkOm;u4G@xqUE+JZI&4VxHq%uLfe=((+$(Om$Ruu+wV7D4TIZfbSU_o z(Ay&iCEUW>MWP&2TgL~?-9p-hq8!xPs0Y>ElG~-CoR(Xx2Hf4U-;>jJY+afoEix>~ zOs(=m(h6r60j-*rlt-t47{Ke=Qj>aKAr}>zrCPgTe9UQ@dgc6C!=6*NYzeZoGg!7D z%_63YSnVJ3Gm%@<)8ONt>E?Y`=FSQJU&Y1gMp8_cX8}lp)DbA z%G+qZ!f8O&TNWRA*CP4@XswLi0{WoaTU{4T({NN8VOMO3Az>{sT1?0wJR`Mfcs3*X z5dhza>hA!ZbsQT1;M~9*V6(PGJ!=J78ef|JX=u;r)=-azFooc0m}!tH9JqF(c4AMT z#*3z%17#d@oDy!>aX4WhK6oG)YR`L;!a2`S?@3mL`5DoSVTnx zHQ-Nq8WmV8^r`W^yP^c_kHeY}s8o&zlZ?f_oNGxOI7XIn=6((IST9 z&lfD}IHg8qTEbLMl}@Nu74Aj2gazy~*pb4uil;a=7kONQTq)JjI6W8D?0o;T>Na7) zMWx4Jq4L->;1Cu5MfiwP_{A`JWmtLOyfmyLFhPgMvNx0*dErHQiu}-uU3o>HzkET8 zmPuHk#fuoMV8pQwmN-c&$O|A8C1hoi^13n#N_l}9fBy=KvIUQpFyfOD6%8EnaqSqpU#)@j7u5B0PJAA){dl#N2qQEO+=br-xGIw=WQi=3T~ zwB#4#hg~#q!O&GiUjQ{PqAQc0QP)vW%L{*#`2>?$WOaefc9Fbu{9SkzrI<|Q@rkeYF2%?O8HJY*LUvX3a)Ys8-? z)bu@Srb#v9wwe(bz4-JlqIz%9qnC(@N7VFVYNl;9bhAq zj7JYh;z_}S76Vyu=?qUk_F^PE@!+A`8T~#}1sTqOPi%;w# zD)APT{n0`!>Y700Wl@Clj*iNCPVG3QcF<8X0;(6U=PgS28lnA!+PX)5_hW@!7tyk} zX!U!<#}&0>zuLjynh_hlcbh32zKHNT=q|*g5L+G+^--CN<7q>;cB84vJYwC4-m_s#(a#gP^yOEfjR&<9iV=Dj#Ik5mvK%FP(Jjy{lvKB&by* zt_kQgX*6LRcbm!mO^n&_SnDuiF*TxxqpihUiNoUOtuXOsS^fDOv7z^WIJl*9s2`$AGjC!ZxD478>VHVnrIWQOI!F*T%3t(Rn&giY`jY=*C43w(q2=3CeX-@$hH9(KSFXnlT!UGNj^ zhCQ$s_Mt6000-d^9EKxs6n;j_bR15=NjL?k;S8KbJ9Qo|z%OvodsY=(L96u}T!m|J z9e#%!a1(C9ZMXw>;U3)gE~xV7cpYAs*W>m1^ZW(gfWOEa@|Soc-k3MxO?fl^GH=db z;VpPe-ip7BlqSg}wn5}%7DVukoZ zY!}~)9pVSEQ~W4)iJ!!7u}AC``^0{6KpYf@#9?tn92GwsrHwMiPUA;om+_Oa+t_35 zHTD_%jRVF(m zCcl&0<@a)j{6X%NKgwP5C%Iehk$dGnxnCZT2jwAoSRRo_<n$zT3xL7tgco!tGm_1>S^_|dRy;XeXI|xzV>@|SG$|t-R@!cw0qgT?f30Ic3-=n z-QWJu{>c8=9$*i&2ic$4gYD7w7<;Te&K_@1uqWD+?8){Nd#XLno^H>uXWFyu+4dZJ zo;}}QU@x>6*^BMZ?Ire7d#%0BUT<%(H`<%*uk6kCR{LA~JA1qRy}iT!!QN^AXz#Lr zvUl5i?Vs)A_DTD+ebzp2U$U>*zu8yqYxZ^fcl(BY)4pZjw(r>Y?E4Nl%;Ao3499dr z&T;32bJ98GoOaGQXPtA-dFO)ji*wPrHIKMi-Iai%)&UNQ^=Z16Bx#iq;?l^ay zd(QnRL|K$aMbwCz(NI)I!_i38irP^p8jY%`8`aTRG)**ZG%lJhnm(E#8XwIVO^7B& zGesYXW{y4@%@Tbqnl<`(G+Q)#G)HtybZm57bbNF|bYgT;baHe`bZT^3bb53~bY^1f z#6F4r$PdyrExNYUgHPaSN9l^uBvBosfuF=P7!B+Z4aG~aPc#ut;Q;yGL!!NC4~Nk| z_JSi~h!_IL#Yiy{PEZ_hlKk5#@^7cfznvlfc2+DC%i$dPi3>(aqZ9+W^2ANcv{*q? zo0=7(YfFVgKZj1SGqRv8$}ZA%rr)f7))>yMY1Uev!TQF!!3)#Xrsi~=X}Whcf-mvB zC12`!OYZ#|?N|E6ZC1z&wJ?Gjjh_Km;QT?8k!GnWT0^a&5VDqA%RyR~t;_g*+?>sK{)c>T(DVNP?RWq0cwW!9 z{#AeapY)|^WDD6!wwCXqXYxGiQ|L=;l9zi9J=P%dpAFD=dS3H^&usY@y=B*b;3xmq zOO8VS_|P+^_B#KsdC3$ni2iaU`t{V_vLZ$$UQF^JBJtvp+8B}C^y87+7>&G$k;r(8 zMJ8Y*atlZ8^EgtQdgJmBpUPhk`cp?cF(-|a)`@e{Iq97YPP~)RNpKRKOwJ=tX6I2S zi}RS1)p^{>=45wrI60kMPHrdZPrdL5f9{7>L|MxIOWt_y-}+aYX^L`bNAojwe`mT zo!>?q|19Gy9VWvGEEmMF+N>TtPQO3N#hS2|kcYj>UWFp84Qm5M*=y`|D8}AoU7!T( z%7#N_HkyrtCTuDD9=xYtbbxL=nRkPpya(?E{rUU6AAHO|3dS;pJ`4?HkAyx64P{wF!$Tuk&d})4IF=_gAvBv62+a*G zVWmUMLYrCT(3a2+RzI{mw3{^v?F$`XO+$x5M_6-=7w)o_q5Bvw^p?eCX|_U^mF3x* zaPDvtTOZCFF2FX13xx}@ufxT|#o3l{$#6;bjdul(Z4FllS76_UD}^huZQ&~6D(t)P z)8VJt_HfN`P4<1bUbr6H5q=^30{bD{Fx-&s3^xtG!hQ_53AbT;BSyqv`>f{HD{Q~j z(rV2PT5YWk?5LG&CA0JBX~(b&=xNuoYv^fjuzNPQU*ghkYB>!G*2-VAEY%tYr6A7mi+c8kO@gI+#Ya_t$Fwuhsb1 zA$)niJIllJ;MmE}@}t`;!AjtFR%KPu$<$yqz+fZUNc!E`7I@iAFcZy8<|Afi^HDR4 z`Iwp2eB8`tW;b(~In7*VZZpZuW9BvUnfc8EWnw?>1**SKeT`+`Ui0k5aaYNh`x5RC6N8A&_h4Eq;d|6NqUpD>!76)qJ zOTi2Ha_L!48k*oM4gDv^w9pz~ar9)Jbo4Z{4A28#8SxyY|1rWeB;uKx0FSWDEHh-L z--|tpXKiiBf@f_*c#N(NXQlJ?an_EtgKYE*vh1uQONJb*8|#K=_J{04$jv@x10abF zW`iLwp2wphAD+icAwOHk){YXrIkipx zoZ1Xj>JeUu7sAM~I4_QoV`W|$Bgg8zI!2ClcpZ!!U*Io*iSbWM2w~*&8b*%&t&bpV z4X_4-ZB4gkfVO5?b0Lkj+u8%^t$o%(h_{Yd$1$evYIldn(2MkeY@UaJ+~^@DK_1VZ zLjknsyP*(T@k3AyE%^~Bj<)ok-^n|>BbpTd;S}%ga%)L9f zn3oN~z8_|dflQd4ErV><3TrJC#~kevl(Vi_H=rfP68+$Hj2Z?*7mO0-LT`)#mcnrK z`TJlT_5BL!`!CRw{|qZ>1bj&&U=@vk)ieUu&cXhmyA%? ze$ROdw&OOBuu=3oU-7U<=&mfl%*GpawNv%T?1w!9^`KnJIt~>eDOHcm_GYiGvDQ@F z^Ezt_j<%b&H-2#~W#A8v=P^2xg8trV*7u*Wthi-5vO_?2h>;xqIu4&1H+(3buxA zVq4h`)aD@SaE4uESJ^Ez{vq!0G(4VX=Gk}>s#}~TVe50D7Wt@dh~Ceu8?%yBH({ly zZpuni-Her?`ejy@>gKE*)vvJfRJXu0&#POqid45^m8ovSs!-hyJ*ro~$)2Tpw3)%H zjb&y$)yvI{UafA2ZLc=dh8?dCO$|q>UL96cuL-+U-wkW3?}cMj-w&tpYK&phQeEC< zw4b&tH(CN0{XECN@?zN#y}cJZ6u`0xD~M%NRtU>xtT2`@vm#hFXBbCd+lpb?f)&TI zC3^zPR;-fu8xr;;manm=uxy9%0>@TW!?Hbl8q2p>bu8azHL)Dcp22b~tA*tlGY&Ym zHyxJCO>YL|b{IPbTZ*F^+i76AI*joKwlU=WDk?0oycZ5*c|RNhj%~x}CtMzKr#ug& z19XKxFaUc(s?)lJwus++R;R5xP_sBX>{QvC{BMD@#TG1V>D=Tx_3OQ>!Y ze#EP>jhVd~Tlpx}tHW8SUK4(d>bv2rRNo6fPWAn8Hm}AOXQ#S68&CZ=fqHHt_1q-t zxyjUXQ>f3TQlCwuUYt%nH-ma^CiUDbuRp?ZqfPy_3kstz_GhG6dKzmPXpF>r`}V$tWlULW?TsYQ z_M$x*L1TqV?@Alf-v4S~=`gclWQjQ07thWXrx-Zz!#TQyw}2-!PIw#3nhEbU`@0pS(B|P)>O3A>#YseMr)Jxm9^RW+S;OiS6m5YC{u-$RAK9; zb<4VK-LdXk_pJLi8cq9Cdx$;M9%c`>N7$d)BkfW4GJCnb!v4ZuX@6<2vRB({?EUsZ z`>=h~K4zb=PuXYebM^)MqJ7!^)sarvi8z*HJB|}|l;b*ioV-pxC%;p`Dd-e(3Ohxd zqE0cVxbuWl!YS#La!Na8oU%?ir@T|aspwR4o^&cZPbo)5l~QR`S{0{msGI7RN>rJY zcO^6jWsX4EqKR=w4fKd@c{}v^9WX0&csI-nqjWccq8!kr%udssZcHo^iy%#CL1-bQ z4J{8XhjgLeL%(Cxhxra<@bVpujt*NVA-9+HU@X)hJ%1_8{FlO$n6vMLnwYmAfM>|_ z*CNmVEP4LgQ9&YQ&WG&s6W$C zf2O7WjHCWcNBxFk1D`SkGe6I0YJ~jK(Ra zaVq?69n(#-Qb ztc30~)9Zn2?`dO9H=|ha;d&7*nnE5#kX{~8k7C6+{+crQ-MyiIV4W9dz5$a1>(I_) zb~?;o=Y3~qPYaz*X9Li=b#7dfUt=Voi)oB>bXi>%BKk>P13yzs*MhXVuC5Dl`X&7m zq|;4xQ%FzWQq#M0>K}ZIx1~i}+O$mtD)V{0!B_Ef*h81`eVvYNt`|}B>G;}(>-OXO zXsY#tL|(oM*KNS}mX!4dPv*%Yj!(t4v+=z!g)o8Mh!4g(oU)GdYWyOu?TYWK4?oHG z;=2|0Q#D>V)n_x&zG~z8!aP%|PkZ~p2vH=6EF#Cl&$A1B2om(>f{QTSi}%MCo$(#Q zHk{!Xz2%fGR&wljb`i-AzS~<)*#cY0JQZrDTDJt(t;6G*%m$=dJ090!#KH&gA%Uap zi|dEr`VPDc@0n_!Rrni7x_EaNrdnH&H=w+d^D3-hs`c?4dko_@!SnN?Y|TRg_c*p4 z<3z#ZcmmIoN;izHk$CUS5BrbZVV61gE~(%$n|SZb57)zZ%oMyQe|Yb` z5BnSXLSMFot;99ndtEBh4&#PF%zOHZ_nw|YOz;0Zv!1L2^PZjJy$7V)syLqcEtvOA zD(~Gs)mE7x6Dx#_Q%9BN6?oZVMsZPQQ+4%A5KvaP&L+9dLC={Vr(zo_-I{ zx^B7$q}RQ4Z^)?o=su9B`|AFXNe|Q`;88tVPsQVyj&{SlyMW^n)lWG?WBwk!eKFdX zN&AY>zHHi;qJ6ovuQ=LQI@(t}?JI%ym8d_~A4BGpJx-xL&VeF&9`3hfiWS3sP+5n{ zXQ*sIW$y=fsiIx*{wjuL`V@;d5Dc_!Dzl~=_|Jdr`@^k)jsx8@MI+7yLn*PIj7wC!OWB}%*BVjyd6SH9< zEW>`Jw z)tLF|Y8o>?{WSI@*VWO7a9zV2hq@-7D;&?5Iy64(dVPnZl;e5R5X*+>AvpSpMp!n& zbBOE4cn)zqlLlbvov+M{q`HqGp3cR2DKjDeXJ=#r^)}@jx(UVvA9!W7D2=^jV5%X! zk9lGa?8Sy?H3qSnYy&&Uu5-AJSmchC-YRHSw_0EXH_}>cZL>~cw3=WSwQJd}?cVlSj8b>l=N!Yyj1gxYr=8Q+ zndq!^b~zWL;b_)qndtM;_R)``Q=@C5d+B-MIaGx|GC0&8Rr1PcQ7Vi4LAsxka9%I5h)iS<%yIhQa(T`5UD_e)dQpkks3s51V~LHHHp*=kXl4)5vdg*b%@j(v(Qk0BJ^~8IfiI(wsG0C}Cr>qK4;kT;3EN#xA{X-}j*k@f-d7Lm7zycHnt5P65lI|0&>NJk- z1BeU=kby)75*ZjEgNO_wGAKYkA@T{4PXc5xk-Ny!2gnE_BZ!O$kkLd&6B!*KV~C6)GA2OA5*bTmY=Dd-GLFc& z02xnYJdyDMGJ(hhA`=2+B9VziCI-kPB9n+r3XsV}CKH()AXA7;Au=UErV^P-WNLs+ zCo-ML^Z=PbWCoEL0Wy=wOd>M_WEPQGL}mrZY$CIX%npz_MCK5g6CiVm%q226K;{vd zM`T`r%qKFR$ov3VKx6@t1p%^<$U-6u17s1AMMM?_$YLUki7XC~&xw3a}RO$WkIp17sPIWki+*$Z{gfi7XG06+~7LSrHcz`07Xh-8$VwtB1LR90UlRE; zKvof1MPyZgtR}LW$m#%DLu3t+H371g$XX(617sbMbwt($$a*5{iL58WA)P-P<7KOi zvehKYR`XD{nvb&8{FtpagTgdpQ94RX~w1!&Dd0>8JlV}V^f1> zY@VSRn_8Ifj)3QAcBm1}4tX;(lOYOOAPI^>Ij9D8pb@l!_Rt0Tz#te&^COhw(-P(R zv_!c+Em6KtOO*4|66O80M7cjLQT|U$GzUOSG!H;a&*0KCv_!K4v_$#7H(#!w#5F)U zy910hg!FVZu@*FgxBcUs40B-_Y=NC{5Kh5mxP|$8dX|MHu?NR7+M8wYGyE|$<1p5n zYw*{Mqc!7Gt(ow@*!vFfsH$!Mwa=uJKxUUyLI@#*(2@{BuL1%hD!n%mP(bMjVgV7o zUgbSSg^P#?3JQpd3P@9`pa_bMCL#hNVh2&0l(*KJlVJjQuXz9OqTl~HpOazDIlHXA z_FBJH_8F!;9YJ}9DKnWuEzaT=>C^1+r#azIbHksWkLdYDZsVnJ8}q|gy&C?sApB`z z_|u|@HeTa47Khtd6259__|r1}6p}leb_m|F0s_e$@$X9f+bP`ID*kS@KhGqNL!OHb z_Jio;n^E3kS>EDM-iAN|%UfddNW?=)$+w5&gvrTcQ0js#b&93V;8=1w%aZE%hC33+ zfV(fblTTT}l$A`ONaeWX<%KdL&-xNVQN&QR@LE5PNUafXj*2r7Wewt2RH1KniT!xr5Nl*1>RVznwo~8g;>YI;=<~&5 zY$pp}jjU#czL_MZK{P$L4PWJ~+(O@t5@T`v(KFU9*ew!$DoNRpgr6+0!`! zS=x`4Tevf?3`Z>!Sl@-RWK;S5S$>}t&K+E8)}bK;m>;woGs5SjU*_+wYeUbP8>DxGuc8)sT8Wze~i65-m5%l}5 zE=Zrtd9-uXAy#wFSy^JWl^8+qL_3=@R#rj;wULz(%2H*qAB93?4xCr`Y&O6bQImbp&mkN4{`%ae6b`kWa1wD&> ztzV;AKpoF5QWj~a$cms3;~W(3I*}Pc?I!3KAJ7lPJB~-t8}oi5ULTF1<_cQ*R`_WQ zY>c4Wf>xGzy}_rFbaL(^xf!l+fMqI{C5WG4N)d=l%I#VxF};)f;MHVBEZk88u!qs; z+eRnfjr%7C_s>1p;!|jQKaUa`NM3{%Gls|g6r9UIg?7#WexF+q+sF3>{N9h>XCr?C zkcBVeyq3JLV1#-sFz2H1^5bOdKdWg-AM0ARa;_DA^KaW`J0K& zzNILjQgvItSSl72qE1C>s+cP#7oy&W)EL~4_ZOlL!oP>aIF|2o)K2(!C)$}Vg{Y12 z?>f=Vwah^c7fns5Wu!Z;tD;5cr%7& zK8|HRo@G8Ec>&5}k^d+47zMQz^xm4^Tk?A=es9a~P58Yrzc-~_3jw#MdxP7HBs6$) z@MeJE=-?QZP~3BFZ|yiNpf z4NjfaqiN-|h9cWEYWT5*4%>&I*-O;&i!RNN=In{#CP&D>6-$K@gEr6sT zKY|wEQ!yl2X<;hK$vr7F>d;d|AUG^|CrZYBxPls2H=mJNAox}AEP&U-E=--Cemcbf=W0J`9daCQ9%Y7~x{UX&Rw7p9X`cMo>QvGfeyz*7Cs z$F@0m0LNC>Ym8%C>@CKz(WuBy=lX5F#-pmq^;oH48qK{&@cKW{KCO+AmTz^cN6cz5 z*}OunJ%D>D)SBZh@)mJzgca;1qyK5YA#It9f9vy$+Fs&QUF2Ii{}rNN$gzu!nh<%3 z;L_l-;PT+>!8d{{f-8fof~$jTf@_0s2G<4G2j2?59o!InC%7@V$*ba3^{RQ*y&7Ik zua;NatHZ6*NP$P1z$4XutHXlBgSQ4p1V;vM3ywlva!2q^yy|bk`-6W+4m^k&<-dae z9en7b`oh97ZNjlu!9H|I-t{WILa(In^h^3B0KHbfN%Iox;Bz@x4v|CUFgaY_Do4nX z@-{h2E|tsVa{0P^L#~i3nya2y^VFMaom#KnQg5pb>K(OFeXYJx->UD__v#1rqdKGxtDm%`13F4a>ll5d zZmgT=rur(~Okb^=>udD2`a0c0x74k4Yu!e-)$Me9-9dNJL-bHROb^$$>JfS*YVrH@ z-}L=@rk6NWb#Z6Q`1}T zz2&{_z2m*-edulVKEX9DDQ}d6PzT;FQHtbyayOJzMwN!js)}k14OB;U1GG{1st2IE zdQwe?-s%Ol0Q#y;Y6}cjN7OO6UB~Ja7^6Gtj_{c7s&9aaXszymsrv7F0!-I0==m^5 zzpmea7f_N`!AmSZ^YwbY6<*O_>0_|QR5TUgBa>rt;bT+TFml@>h9Eo|b3iSp`ZcOAS6P)CBd2dQ?58CaOtlmU>n_r)I0y)f;MsTB%m4)oP7et3FqI)LykueWCWN zFVz9{l{%LSdmR={#Lo=j$rEs;;K1>l(VIuBB`1I=Zf|r|au0bOYT`H`1MT ze|@vQMGw#e^&mZ1->t{$d-OQ{gr2IO)YJ4+db*yWpVrUlg?f>GO)oa_CczXji6+Su zHN{M_2^wjX(Z(3tIL0-esb;EsYrQwUb>6$)2i_KM2O&LPao`mIYB-mAC-^EiE@(shny@Qms8{ua;kh%PLof`>2ijAT0SFZ%FpE+FJ0QI06ul`H@ zw|YoDtY)aE)iY|QTBsJO*VJOQL@iaz)N-{|ZByIT4)uxJsXkS^)Msk9I<8Krlj>J> zN*C9ux`a;CC3U(kr89JCovF*{EL~QY)8%yqT~TN29G$B>>0Y|G?xSziH|f5*pT1My zrAO;A`Vsx8eoRl)lk`9IWc|3FqF>gpn7i}>eO#Z=C-txTls>J`=(7flFqR3JC=+dB zOst7BRZLZHmABek<8AWZ_dfQv{|Q!-Dyz>sM`GHbO&)C8;`Xcg0i&C1e>v*D|2LJ@MINP4bo%{SrbahI3-sj9Evj;j`s!lKH>s-C!sj$>x^}(ve!b9 zzp6g<;_VSW>UY^VL{-0Zo+%yCZ{rXicX8%&n2X~z1ANL#rmTvfyup+e5tP+TS>sbK zHq!zQ5AivSZ9p-!Z^(VLX&peb=hT5cr;ccm)<7q;vIpRLv_JVU4lQIY7;oyDtKcEi z!gPiwOkeElIWx<=053QbovESN% z2Zy;>u#%UDm-WaRW3On9aXe^^(Q}?6r$7u^<0m1O>@j-HPs^tfH_ntZaWsg-(A!2F z21N;nK{Dd7mp~#8dj;o!NDMS0vDa{vOXL!8&=O7p7j58EPy#W{Gf)QY-zg}k&+4;~ zjkZrf4%t4)CEEvih;x!49}!M6R7Z>>p$1_bs7V+HYNKte4t32AvjZBKU1k?FG`r2` z(8%mF`=PNpU=F}l=Aiionwjs-_i&9lWDdi%=7>28EzAjX0@|3<<}|c5XHlrz*#SEm zI@qyxEOfTx?IO^{E@~Hr?sm|Y(8JcY13m2&I|cgKX?7aiXs6qy;3hlM&Vqh+IlCO( zVpp)UVSru9t^|Yae7gz^v8&lNVHn!)bubcb_ZGOz+m2XljJM0%4`W$&=!68ts(*%4 zejN2D{nTpT)BiX9QtGw7{H*aMvNj0uuunnVNjL}h1mae-O~rL_h(a`4 z38Gnp#;^vBy=>NjqBjoz&5||93zuDGR{%HJO-6j!%54Rf+s19fyeQ%8@@a@`G-9`z zxO1OXvmph&w|OW-NxBj4*{gLI)bYL0`u0Qp-490EXPw7jlxw*$BGHX=i-@3`XC>dVw9#%C1K%ndz=Y+t-wJA7x0ACB9Hz z6h+mfEp3YL$y5l)44HuvR7RFTU6C*IQCC!#)lpZ}lC@E$>dAU2Qw`7_CCaO1bCj&> zcwd}!`5GvQ$tAy=BBx6qK8|`EhVmZbKP9g)6IAD#SLyXx4P)%)^cl! zK5kvNuDH={;5HODxsBb%qMv(}+f4L#ySQD&E$DT>BLrKRqx~zhe4Gie4$hs3oaSf9=+~!(xr;rK z!4k2T97bqU-YRc}fXY-^zP^b?pSK3Yp|@KP;`NpKN=VSx=xd;eZmnBGBKf_Lgg);e zC`R!TNY*3t2#}_@DUR^1q)7*DGE4^8Cfj6#Lop3-c^y+s2SjU>)rpm}Quvae8dU^+S-gw9SmgNQ@^Nf=5?Z?`w04tmWUuO1 zab(B!apcMweFjH{P!30CnHWeQFOM;!Kp%Dhqf3it3p`u$5VCNdFJk*-O%vo{`7`!- zRG!3LbVlLopss%oT#8U2s`*^)g%auu^#!D>)9Ngg;?>C1RdrRgxHWWb$l^7rpgZf% zh;wKivUweH_1*ezsKk8CV?I{aPocHWXKP)BA`obaibU)fyvaOJ>Ygin*V`+|TFT+z55mThNEsI7Ok)s7fIg6so)` zkGL>T<*_}nIN}8C<%Z(N73k!nC4D{lHlsHiU#KO;nK1SL5ZV79h%AK0&wmaU{oZBT zh5MW0RJb0~E(~s&$p&n|5$xvTU)l;D% z?yVWnh;3~X)(w62N#uS%))2R#hKPp&rU-I;q$!5G?RMO49^AoPzSkVXnf#q%L+}p8 z`d|~=>Gya(@AG^STRi)xCEu~6BP}Nj5Q8(3AwZW+O)Ml{N2kHaVQy-}< z;8Da4Qq@ki6VlW!MRDj;>J;wkG@S;eb-GSRot2?8P$tXjvZ%ANbqSQlCs(ZbP)CD1QsdlcmFB!8#TzJJ1b#Hb6c1FuWt2pb%#Z`(*$n{8$rwnuwK z;4b+BEL^!={7YsQ;~Zc-BO~q(#eO#NXttZ}c=spfQ@r~#vlrX@68BQP_nEhwpZxG! zZG>W1@A3?{;T?dz?uJq{BG&Ps7wg5yjiq`i+Og$&Ia|mA9B@d4A`kRS1(_%lq@iY^ z-U*;Kc?7K-S&vxMBPY=IpT;$aXIor^Ek+{GxTyKod<(_QF>?%(?L<2fg0^eBAnhDG z2b5jgt_#|3X}1K&?re7k*Y0lj01s`;Kq$^uCCwg>mZT(Gl5~5TJqR3bl$H9W}9oT2U^dq5Op`DpY~S|c)R@3!aPFt%i6H^=7><@xRqS>gRe z+zwT7Y{qYj{_`Eg@|?u-T$Cj_$dW8sk_}6;&5~S_rMDEzZE0*bH8?a#@k2sm0?`=a zFgX%YU5s2R-$hGxSfK_(WR?$kYM>eiSEvbynA;<2mX5jA`PL)Dk+OBkuX zQs2XE>PPh>jG+t|+^tS)0rwz|=m7WQ9vJ}hagU6I1&9~!hu1mE_NHE~*TFhQX74bX ze9w$9qu_marMm_`_SSoE!}bgI)W71|`^>djs9k}zt6=RKVC@>k+O-Jl(j?ZUMG>3C ziIq&|4)gCjeg8{@en^X zwn79g0J~rp*IC(ND)L_^>0E!*8D^b%2g=!J9SOPaE_WBA)zj{2xYBbx2dz(MuQN39 z26=;^>F=6Z(2t7X;L*Ag+BK?#Dv8#WFt4Yosp^oT>Z*EZT`B&V#_`WmsvY9z493r; zO&jDvrg_dhhbV83nS;9TC+7%`ZaH!yk1bZs%Z7U6!x8+_I6tzdr5Ph##iQ=e(fOgy zVI1!r9GxG{51yydhBgcdeYmQ7SMSP#%h!EoLhuJK&(iI{_zv z{X{#_V4Qd-0c1|g14%P2MEzMo zZB+-MwmOb_vIX;|lfF}rgPRaZJOhLD%X$Hf)yGj|-N&eRwi#{4z&x|SEQJ@%YO@9w zGA~~gK)g zePHzuUMJHDu0yYV7J7Z8frhdM8phGa;r{*~ z#QWdDwu-Cb=v99}Lsn6|j5+MD*cSDtjH;a(vv;NV|>#kg<`Aj97~+t@o?h+Yx6*Spt;JhY1-S-K}wb4{o%A*AbAn^j;vR+sfy53>z< z(+l;+Dp=3ahyxsr_>R5zAJ}_8#8`}QCpgEQ<6OHLt$MCK{`U>2874E$; zkG?7P{xddg=cUCy1TcOt$l23LlL29rOY1ltS1h!Sr$XZwb$RSN|E<0g zLdcgrmjuSY9^>DJD8I|0C3_ql(U*P_x}k@B1|IUh@qU2$-Y?!USoj~8gMW9A#QqCD z(cIeb+&cOd!&F<-)Zf?id)@&=J(~Ue&Zs%Ma@99Qa(b!W%!NU?4{y{*NBX^Bci8R zzNe0>lZfRhmS2+dxfvYMFU|SfOwQ+)LELmFWO4kyENk|19ET~780ux@>q5N{ZSiaR zHMGT~<#RYPlgp8rJdVs%=EzJHj?7f$$V@ek%v9&dObw3A)U<2c^-zzK1`lcN`W%_L z0;OpXYHf64ph`TIcU)+dKI0WMZjiHNkJxZV=M?<byO{l*P91ZA&{~iyIz+~1l(U1q@ z=y%q*zh-#l5X76~;I6P%st}I6?1fOBT|wUkefA^dqz~$YxaUZc1j~@cS-SwsL=;Ov zG+)izG>TzZ@g)^(gIkTF|nbi`USuM$# z)pX9Rmg3B6Y0j)>a%MG)bJXQHzgiw=LX~=nsKMw|7X6N0s_6nzRG+Aqgg>nde_B>> zRf>m{{&ZFN)4K4d&{fIcnywJwmO_8m`G1$OEcU}S0Mrj#X-zbpLkgW>OyH+EhI&Z} z_mUFsCFO!%y8FF!_j~E?_kyydw(4GCt4cmd=+yH*&MEvu{sUKU9>wE0%V}|zFmU-I z1{bkA$RWb$kXL?VXW)8Kls$^Gow1zljN@!)0>_aPInP;)^PI_?=ah_3G~*EWFSn)zyh-)r zV*p>WWqC?2_)E?uboMUvY-7M(Otd||&N_IG#?L{(Ig13JhI95-dvE^b=1jWzCN=aD z_Z+S8*Yej(*gNOs<15A%6*SexT#ov-KQiWnJmzfO;4d`huX)Tly5XOA%!TX7zG1nV zhqnJx?hQSQ=3BO)ls5)Bhpi@gaGE{1670d{vX7a^K4um6F{}Ptx}tFQ`#X-W^rm_% zH3&W3giG8}=ePGgw>MrrjO{(5CS9^sJiomk;KGw7m&)TGna2fj0aHVhH z5478%*+Xy{%cof}-)V&2zT+h-d zo&<*g!Gmtl1PBmV5C4 z`s&^~!#O>DdU~2UJ!hx;%=9QnoL&*{$dIp30BcK=0DnAMaefI+4zR2Y=y<+vWL1rA z6BP_T zUeZS#e@Z^h{RV0CSv7e<&)e_cm^v1KQmxiE@T;_~=Vr~}Pafs7bz@evGD#j7r?GAD zwY)Cewe1m7_mt0O4VvI@p3NPp;_tPAFUiwj5TO3$Jm7D-1e{E6Gk950duYHQ1=SWH z+9?F)HTUHPGS&+_v4nGRJLdQ!m0l{uyqe|^hA+LI^n9Z6+f;XFx%NW-?Y|G<|3x9X z%J%i!7Z@u%_zE|N%$RUn(|4vI#G6)6@`zt}hln)4*$!XRIEYq%kU)$)kZWb5aZ>)` zsQukR`@56&xaDT8MLfPl%KdjoDOwh6}o7mAIgsSCsA1OWs zX_KBfF|E%3u=r4P&^$0zj*_T>fZSV`elii_`+n##anv+VC=-7UW?#B>gU+HkW=1Cy6B;7w;=iSJDFN@Vsm|pTxxnaqXcID&ArBBpCd) z%C#ZCeo^ur!>a{}F@KCD4Ywa3$eUjQ52A;4&xHS+cwBqM&a}rXeV;+?+|w7}Ex0q* ziBJ2%jC==Ots0XUv!{JI8uh#}o4i{td@Sc|CR`2+fW7K#VQFF6 z7}Rss8(`FP(d)lpaU{wkV{u3op1#{x|E{;dnTO;Suf|Nad}$LJX1LA%ZIV2Z%KWAK z3%%>UnHqybze@2q@wQ0zDU*b5?n+$5O5BeFDgi3)D<>_|)qL|Pt9>c~j~;z);>u{Z z*G^|m!p*kQqI&8(L-iQH`Nh~rUe6-Fnny&hM0@ZlL42yhiaQ+jEyIdqc){l&@I}7z z1=)bgOKP4&!C=N0B@Ren=gJq}u_6TLi4O68<&?+4J^m6>3EAVdQvz1A{1Oe`kgoR{-G~pYkzp(_-y3&l=A*QHaFK z4wyn*n|g&hNO?u?jTPdGQ5Z%33SJm?{;D3%%f9B4Re1D%wEBPxUG^m~mF~wS<@zG? z`A9>pdm+^_E<8=JE-$)Bv!^Z1WH@O3_jG|J!;q&)&Dsa$vb`)vxvvpU13bO&m7m-g zZht?*N>~?lQCcqL9d~lalKrsMo;hKjN=3XZ}>#l1XE=(jVK z&QKU|9d@0lr=r%{Ca0GBn|D>oC4$3fOcH}|#866x5VR@DI2Y|gU7_(x=ADf?U@o-Y zby69Yquca_{bV`!zKP$9V{TiNJ9VWDQ_OpxzfZ zK-3`=pENN-Tzwn$GxN)~_sp|1@Fy9)I;zQrmGnBhz9;wjifIcN$>y);u=Za zlHCz+>D7s;k;;q;^)aK^O2gv=8a^pja}E&wf!}O}-^7LxN5iaxVAcUJ>kycAFw8m@ z7S-+J+UGN{DcF|R(L}U*|4PNE_mib-b`5Hoe@G#Z72i66MX#+i-{v9ZH16e$^E3qO z%4>Ut0A;t5o7Ii>^|};>q>yTU_Tw<5;g?j2kC`Z+*SK2@{;kfr&O3^OD%tuLA!udz z++FJ#(Ug}}Jjn)>W3K0{+VN=xl)p<6%P$w9->9wNXX0n#2-v+NyyCjxqJ)SXMT#8B zfVu7yp#63DmR~HQ%)H?r(fJR*BK--B_=S@EDYclybV?b-zfO|2r-gT7uk$B1WY=}- zl|<%Q+F6|VFUDvEwTyyF{;1YzT$dt_Lus#l3A23(vg-oI>jJ)O>X-MdFYa02+{Yt6 zN`BRm644)M~|ah(#%kh^^nI4~kdg(4aWQgdYMu8>tL}T#Zl3L!#H!|x)`mAOR`kxcNY2dIypUJU zwgk?n1p6zV`zy%%D|GuSnUgIQK-?THp zD-`DbL{2_FX+t)K*F+$^#*3hWfV!eI+8ik~{zQ~>*$Cf} zkFzi8+DY>Z3s{%iaKr;RB#4{75I6N%pSd1OJ`L{3eW*}RQYDivW0Yph(ORKG^RD&AsjdEb0{OZu^QBit zxOe1B?+E-X(#t5^TPECVCDIEB2$>HKnGX%&61QmlV$mpS(Fg~EL)?CZxP^qc`G>f{ z;lL2L&=AOv5J*r6NTe4g+>7$17f<0-YVzFJ+#Q{rpP)zxwMghKNzO}5021azK!}-0 zFX&6}dw3G*E&0-0BHUXd(z^#v7YH_Md@*57nX^d6UuSkvzs+Us>jj`X22$rwbdua9 zPIgm0F>I2zTyLWxD>D_jq}k0%s|N|iL7x|<&)4&wkM_=3>KTV}X|AQ`D((MNqDtzZDk@3pyjAp~ zY`|8O5VpinT#n;KS0wWPBSz`MD#_HT6SO+^JMi1?k8is{-*$%mL{xH$!OM#qq-S_% z0UZ?`6{1)2SMt=YLKC@e69Z!|C3$D^Xj7<1OP2ZwqbeRwY|3|daZSE#MWALC)+e3#DK7PvSbte5%K zLY=V>y8H;K>$ExD_|N!LO9E$pIqoN8x2BtQ>vikBb~cdO*MFLgn8-Ruckdm8$=bZz z`kXR0a`Sx8K0T;CY<_#uN!$!}s(GvpuVKTVuOwxN&AIKjT?^d5d}Mn} z^HmfogHL`5pO;I;F@Q#YC`eOS`em%lb@+Bl+^HaXYG~iIkMuFhn0j@Py*Bz{fX1kp zbzWWnBWnt2SyaOSvQb^l*n-6|=PfZxH1z<{fW=nrMPlzpn7upMsKOtGX@zlxeuWu@ z37ix2Q}eBT@cw3)+qF|6ZwOBTz932v zD`-AuCMGh*N^bKb_?nx^jMa?T49QHzjNGigF1W6}E|P6+aU%U#)+xr`ln2D)l%s1@Y)2hu9gLaYo?dSQRrQN%M+23f-=VWuWkiJJbL{0}>Rx#gClxQ!UPo{)VioWH zpa6W=5#%+UqiN`18)Cz34DYKh2m){BQ*9eop&f_Aw?_K^D0PpEYdf zNAFj9v8V(pWW~NO5pN}N5a9=Pg<>Ge)WH72=v7l0+~DADZUZ zqp$Y-WgPxUk`aW~ZM{h}1C`co*}IaB$Mh~baPwjPm2V64nm=X^qxH|1Pb!7lsXa6? z8cO|qfCFlEZQ)6_s>o@W%m|5JJ$&653AE{APGc56enKEaVn%Z69@;z%*!-o>EgT%F z(kvQW(0Ah*#^wo@7Kro0-d;m!fqXd?i1(7;Uc;-=&`0J**GAtB_zcNm_}AoS@VM|; zj;4?NL57dwbD!2s*TH79F4Y_WbOUmDbkJt8qUFZSVorNEJRc`IME8zXqNp6S_O5{o zd*k~N8o{s?WMHv1oiFbkC~uvYx~t2G5Tv!5S- zGom)~IP%^PyRb6&EZ-)9_3bZbqo32!{?c5?46AQZS0_(IwfS)7 zA>1%cbzce@V@a<}LXT%_7A4Oi`arWbze_lr5tqlbIr{iHuKDT+}U- z|6pE-=DSyqzIi%?X^vO2ZEUp)Z*%n=mL1GLD!}*ENbkf54~;!?_G=YNj0%W7a(K(c zO)OYxwsrJSw`+N&a!CT{-%I#*?$$1}Z-t;+g}wM(4~dgM!QSwCqBig+4@Pp$GS?JR zR2ysd+-V56gRX9ujAk!t?m!^qSfaBR(GPX+$E7CF5VyCs2xgE`;Y1o#Y#BgTDSVAA&z59f7YdN*^|)$|`n)Ouz4?6sLvm zDD!n9F5sy6R}LkCSqT;0gq(_5$>yWX~N5V7|q@l0XA{%8?4_KK}b0j&6mC`mvj<4|K- zc_N`0ervX_kUrA%7q`)eN_PAfY_U_3c-Z&toS2oFcrRYVx~|vu_|5pENG7KT3=rpJ zuhwA!ot>OY$JF_^NLI#41W_Z?<3C;#S0{efm&e4b`AC);yj5$z@<|<~7k*w)>NUW@ zR3n_SFWml>1b;mh-tJPq))ehc7Gr;97W>^S_KS3kv_@?ES4%sJ9`GXdpG9hegQ%b) zHr$kP$)rumKVk7Y>FDZ>4UU*Eyh%-vfBya+MjVS zD7gOajjy!P{_2gNQ)QE7iidaRNa~N78|X+|<1tqU#sBI)F|SesT7LeCwk{%Fl;)~U zwl!v49aO#jW3KJRUMWEG*IVT+zahLE4qL^l&QnxW3TWzh)wkZ7o5<7BkI+kL$VpX~ zB&b-{N`DIxNDgrQI%lg9TBItq`bJ)KQ7QeI@uZaBM1E&yS;=EKXLzKkTTo(=zLy9a%7uq~}oml}0jyvvT#TD#r04n9fl*N`X9Pati!b z>M_KJBf<&?`bvY&IrvM(rRvs1 z%tO(77e03h^+WP4%j$k5lMUg*YL3Tb3cN}urNWH|x6TSzrc3gM{11AJMqIQC#vM^( zS4yXJ1tNbUFN6o^Z}BPBy0F5M#DvYV{aTz$r3w8yoOLeY6gp|Y%3?7Na;o1);*K^r z1v}3iTjU$V8wfs|Qqf`bl^3Ni49d9O?sgJ<7rKn$s&%3d`PB|?Y9M;ZsVADVp*37B zxb5kvXs>=gT8jL&4F7x1zs(=CIT4G81DkW}B#}Mhyw>D|weUG!BP_L0+YF|1ACv(v zIl`3o@;j_iXj!(NUw{RdsxX%BiJ*o&)lQc0*h9+0p`FD2X9jn*gB$OIHsO1NIA2_8 z3y+Sz8)XW|klz?Nc;Sm;m7QkoChfGR29XWH+!Ymbs++ZqTFX*z(JJ?E&W3%3Xvm}M zm=nBNzQ#{iEq2!7YNTC?C}-B=+SF#z&Wr5e7hlq&pGk6^7H~WAMr2LXI+SEAW^cpj zF6|zacIae{6C{xn4QOP5!N&1qJ3JOR9?ZsPNL0B~nzU9lG}f}3aju(HiNBoCjDJj? zT*tMKlGX_};3`Uo%x+SJe%S-K!0E=1P_eobugT9o+968t{r^J+|-_-A&Ve zS(0@Cp*4e?9a1wciD+Zn^3WRk>k{ODM~3xBx;3cMXsi;b7Q~Du*UPPV3dL86oF;j< z1WrgfzJFSmPwv&b*$xTki-)kP^$~_O4el^BN*7iVc-S-n$fbw_a4>Xf-;Usx zZv7ERxo3lfxm?=v&vfqk2C9l5ESJA_U-}#$x|pky(rpJiCWk z{zCd87WN(oWiE;3i|@_39y|Alw%3oKa%$tdE=`)|@pkR#@0CSN^K`H8>ynQUn3Wup zy0gMx8YT+bl!O-Y24M|-o7)(Tb1s=I0~2o2V?H~edDvP{QMRXf!(flI9UOKP&BCjC zDcn|B+BagyNh%i8)ONoYir15tbO)D+wRXQ9Gz6Dp{zPy{*6;F=mIRQp^S7RFc__zG7ju1|Pv!&Q;wf%icV6~!(o^I^Bcx&toZ5EU`~8x75CI>#Jg zOk9e)YIDrhv_ztp>t?~=RDVNcU4zlCE9iUJdpwqhL)ZhK*lHv@UmAHhfFdwG-Le=L zD>sC2F0`7Jlm*=<3xjcNc-E{eQnmYBr+R*&D(qbGQ`B)u4`uhH+?x%}I|9|9Dnru% zvZim|sots85lTW(Tk-&EyrAUID->955F$-0vxJUCtGCd)*u5oDJ>ydB;XF&#_%)R$eLHY!X>pxrGJRXt0n07M zIVPXhoc{g=lg!9WMEz%D_vQZ$A;tslFA9?0?VrDlx5a zt#JP~`%CUmrF~`I!0~)1iyz}982iGlVa$0W`%cb0V+AaeD$X1_B6>wtGdIrEK7k!# z>&43ld~jom>)!1i`(7SyRK-0-`ics{153ysD0qhCln@=5c#6fO%#T~`iDBN!!D7sp zIKBb1E#R{O-iWvz%56@rZsugSO03k$aJeJ~!3*Kj|4jWOlMvv^(@uuQrAb^7w!Ap8 z7%R$+!i^FqJT7WRl{ao|_L=^eY?n-&e;j8Vr|)Rg*%TMRBo~<|shN;Mb$(Jc>Mi9)~%mV$xiG!=TSK<^3Ca@uTV1ZC}mY zx%d4RB1>^eKcmg56%#n~zDd$AMR8IWS;#CU@PF}|(k*;_A`eTfb$&-KIXp#^Av!$M z?fj|q%ZxcmspO0WZ>gxnBu?SyvBLC1^s#)6?XSEsG1Mvt?z}n51w8Db$|Z>h#)^Tv z)7pxOawRiTOuUKXGz|x2yrKM}xrOoxyv`Ka5{J_z`$~lrJ{f}F4rk2v*$Q9k$Py)n zIMeG$=O5HvYfOg;%ICh1K0t2`V23ZTk)BTKkkq3Y+n2efozCiz)uY3nX>ew;_^MY> z6`um%kV8Uq3ey?E;v3i0w?a&x#15&YX(ne33yG#Fn{Axdz|(gp2W{6nxFT-T*@XzJ zVs7y<*FN`x2cm8>t28O6d0vuct*NK94$gp8iPp(SXQ2c5-W>>qqym!I38&X@1VlY2 z;ZtT0q~vF2XmWB2_9R(6{dM1=UE{Wkdd{5fyXCw55j)%agPR?HEir%dD%x14>}J{% zZH(ksc+Pyy6y>q-{6uALxWc>EFYIgeqS(bnt%8XS-((!5SgP}jdA7B}Ob2H}3M89* z&pwflc9W;{x{C5da7XZ-CZ3(g7{=7PhZ4q!wV>)3&wREU@@v5GvK8QLix0-S`xTvI`_t>bqj|G|3pg7V;!!((GI5bsl*hz!5L|N+r)yVu+Z&qipY8n`dy`}}Mgl%5Q%f!zq z=0sc3B%mIsP(Rx+)hD^g9|+GPjs&W7Y3kdYQ&wZC6P2TIA{uq)B+!g1Em5!W@b3uh z^b06oD95W^qIwO@OwPpNU-x+$N;Q|1l)QK7Z7sPIjIje!hjvgt^R#66+t?=7fX@aO zMrULP7Y1fRnknrNx0z^`T`rpm^X}MX7>kqV;DKHmmNXG6O6R1ls$Awg&3)=cZK^qC zN2UiXc$SoH8aef}3I$5qe^yM*S=d=F*Jm%;+T^-^=garW%f_7>BRF}lGmo*a48d3y zi~0V$P>o2FkV!Lu^Uf$K;fCEOD92~!ZPCs>1AGM~rC-vt@8cVB`s_KDL$#ZukX}?S zeq^=?6541-ae{(kg$Z-(`cF#+S&9LyU6TFWMXUp&MiHztsBbZ%O~i9*mfXh{jDa>l zE1=ym2iDtM;`dQi(RBkRTN#1e+A*l169zb|8r9!b_yI#GU7z(es>#$T%3r7=GWsWX zGk^p2;X&Ee$>ARu5v}|ixwN#)B)BnI1vlF#r2J<(H8KnkvUNS$XY*`x>fb9 zsxcP`tx+0L`21`1$rccf(FOwByVmpp)qn*6*$XVh#IC=7;E_-QMY{6zdDC8Ep-2Rz zgFk%2@Wv;@j6nkV$NJ-Sh3jK+qh-C8ds!05)|C$aXwS)wl7%6NhTP==_O_>lzHUa< ze$D(cHDIL6tb4C(uX{(Iq8Zowr8kZ?QU_usvL5pMkE$P5-D=(QUGv>8U{8C5W}Fqo zj+Y&19Z0swO+V}cwYqJ@)EjEb{Jd)UNqHd$;m;{UjV&heF z&~m~HE8c?=MJO(0FJ&oD1ZD)Tq$DPUCmeClhOVsNONrjAF&?#VKCXGq!U7=~5Q#JS zwcPgk_D=WCOW%{n0k2_L1*9Gl1u2JQLA-sJkj});iu5xct5^p?=jV_6GDCV z@(grmfzqDqj*T>PjO;4Hw`6O~)tI^P?ZnjeYJF6GCSPjtd6Jo1xZ_>Ppa(oSdXNL9 z@|Bf>rb9)Ps#FXnWve`d0y*}FM7I&x;T#dl14V+;y5p`O_`Wzr5<-OdKfi(m0{l_B zV*RdG0>SUWd3?E6TrWriru3<+Sr-tk(K!*$#D;jIXTZegAKkI|v2|at1f+J2fZY<- z48ZpGh}^iNh!Xx9-~xS4?t89@>{q)1N@w1cIxovnRbMf}yE;nuZ+(vHd%M(fC|i;| z+o}Yx9TC$G@HMol>cM4US*8%j4p39FYQ2^2aOp*6xT+$X%82xk!(>)G;>@`ojjPy6X7#ojJX)kXeBMM-m!VX-d( zBIq5Anp<(7NwcV{PDu|(-t}>kS{ZE|?yQzPG6!AFRDD z?8N0C&J1DsMcggGCgr1meY!D^yZ)U@t;EjiAjN>^W64e}x;j#Y{RmULgw(o%YT-_k zY@|jbfeArPgeFAlSwU5*45-rm+gzoVK?_?`rNVOBE9sh?RzO0kQ(@XF>6+N2p>avw zrowfOTGfkDyz<%7rPi=wTs3R9|IpHnq2req z$CfgY{8T+vc~6Rbh3pqx0u# z=gX(Lx93!q!r>zK^|*eQUfZi`G4p-#ibBDr=sInA4wuJfP%8KAyNcvSm(Qv7pRm&a znJ8oOqawQ{yJouvEuULGM)_@XKIL+yW-^ZQI~Cmp+~v0ndk<;bbXZri>aoseRAp49 zS7jcJ-s~bf6|@X__j=EGkE~eRvaWMBWh{?QjIoTej3w^6UNK%-U6EcTT}$fcuuf#` zZ66-)id`jmEIy0_4NpI<*9g^N8?DYhKU#TM6=FFIbI3_LQP(|k#R`w{UxQjckKCAHy{zA@ zk?}<~4Rmitvr1$#%^#(ByaSZnNl89@mwcFsgKoq@R!J=~8JSBM-}2oF~X4iMoA0dPCt;C8;n?Zn6JB*T4Xrt#gGZzflyyR9<0 zgq7u4X=dy%Z=cPh<%fRjWm)TtVb>Mm)GxfCaQw0$_wIplP(I`4HGiOd(-_(%3L~-)NRkYrKEl?3^T58{jo~yRuNKlCy z;(O!wJj3wtHCU+ZytQ@Noe&$6pRT7^AoD$QcSFy&wMait}rslV-u(C2^K7oazl+NYum zDQ|qe`6E*{i@e>6h0I^j4EFi(T2KL9f3_t+opltmQv}D{hF&$BF-m2R9~wLtsj}y- z?!uffYlImF@KxgWE>c%_88;1Fc%z8u8yaCSRs-9x`o#?|5U#Q(UBhIJK_9*D1?WO| zHmQE4qfySRhE+C_3_5fqsBYC3>xj0aPKGjfL@MtBF0rKueB%H#*vJww=x9|LLTU4( zfU(@s+_9rwmZ(qB2c!B}yT;51kWd2SjZr3_=TF1vY}i-mN(>2i=|gX~ZWUl@KH8et zovR21LLY;k;_}~L!&BaS_9?v2*Flg12&2DmmGXCK1y{~ULosy)R{-O{sad}Ud{K9@ zVbLN&`D;eTN9-E(a;h>KiLz=G4HeaB6*CU?Z>o{ADLL84c(;45_G2^$df{E23CPdV zA$HMr*>R46b_H>Yl@2O{oeQ1H{2~Wmhax!=z0V7bGjwMN6?-z1+IxK^B z;&&09^6#3{jRXzkKEKaY8r63C%`=u|JO0Tmqi%F@m&~b>Nv#LyB6k0|&q&Y$dDLUq z`-I>e0$En{=f+d!@hE{pEpPVw(pd zOCLSUsKCmI55J=4(sq=StwW~71r_kub^XxUUcU#LZbIs2(H#OX>aOxGr)B0L^BQb% z$|1xKNq-IiOte*~0p2m)eaPtt>@0e-1A0N14iBt?Ir3n(YHK^8NW55c_NdrV(0k)M$d8bd~eeM{;8%>7$#=1;+GXx9cOlqJ!@oGLU%{ z>VdJA(6m|MLAsXGv=w`P`+z1GF2Aef&YU}3a&h2|oI7N8vFMGJI|7qgFXWGZe_@={ zqjdL9Q2m~6N5q470^AF=7YvBqPM=fATu^Y$&r-MvRW(gbxliQp+Pk>hz9p#I@yp%B zfX;YRwz$~9U(eFM`qL0)gt<7eI`L@5)$tz_qYX8V6;j5;>sYNnl7cAkDl|~$;#~}D%Gn|leSeDbar%EV zj;$l3^ZE15R7ss{YUxH_Km%xfrfxSEdIO&zVNNxD-H7X^|ZsE<271kU6$n z-Eu5NsDHKKeT%Qvm6Jk2UDU}jMaOI^v!K`*B`}g>G*P!>jWlA!xQKBq))>)FBj8`d zaZJ#t>WN@fRp(+Yu-wI2`|TL>md-2EhLGuC*_v^Aps|kpm>jO!Xo#`^I(`QWA~(#fomeoKdZI7^EjYLa_>6bx>(wS6Gv0E* zLb46lYiEw5u0ITn*I^vzUo#AJ7^%UxL}7sW>lg{u<`%lKPVw69@j>|>be<^Iho0${ z4mz(OcW{@`lYB?0Ul$~T(+luSv_2uUi2?=XpF9NtHghj0pa#A{fE~~=%Sqy`+ATeN zcb59c2A_ea0YR(Wx`}0?Mw?H zwtRfsl{QuWFW*s-`N6>l>LCt{y?)(e);2QWY;+ zn-fq6mvBCVkiBAlT_ z2GVvZ>RVL5RwpihC5WnuROZjom7&k{x8(@8YQzJhyHfNStC<$Ctg#1vWOu1ov%N)e zec2YU+I6arUH$$o_U$XYzwH^81?;PUr_dwfLtA? zPa`h8FS4<|lpstVym0;>2G50#v;C?GX0yR8+p2bjs&KJkD-T0%z#uO1QH(og@C= zf$jqlUK{%*aDBH$;0$sJrW|P&C-=V<0!FC`zs0}bfhp%Vc~GdRJH=4i zLsH(0Y~4+1|AGHC1y*P;&bj0Q3T#8t9sjtCnIAbO0MxSue#=dm^IF~?5M0tT19s*S9B+SAOn*5Qpknq{ZIlEZ791VWg%Au z%}}aQp4Qs6eH)}WtqpBE-Z=gA4@lz6tyAjbijBza?YgmVJ9gW@r<(mQ{m%qnQ)Z7l z@PZD=N}RI(y@3b29rmlQ*wITnvb#g0##~Ko5zCiNJInXq@Ih5#fV~JxjDn)6OLLW^ zj zJ6OK#P!eQ-S7}p!pcR9i9ZOa1@QIK2tx0njDCIUKeX)(Hs(LtTzyGm&zNXo&^WivQ z&FIdnv9=h};=g92s*9bhmf3!$@-XWdsD{3i@;D=Kxc+3*e&pZC?eyMKXx6udpYiv) z_~V1gzWUQ=XI?+NjIQ;d@ohU{yaLC{Ti>-UwH#L8A=ugmS1g3M{NwAzH}vyY<<~lT zy0MM#V6|1&x0eN(>+R&;&YVCT=0K~B>C0C1asyR6%sTho@--6Eo4HSs9HWMqb?Utz z=RTEbi#WireVRlS){i^d(C0@Ou`!u|0KDi(IiCqHf|8W00-3uh{$KWj z&Xcu&R@PbzAz9s8m1R0Bf*7e?X3)|6wM#b75%LG~bB@=7jcxyEUX<7c|1})GuD{cF zvYO&1ml>~b{oL?P>cao`_6;0)8?$gZn*nK$=+vxq>%+Q^S$G+%LwX>D0HoXsAdTfe2=x zAqir;^`L;~HBye^n;Q` z1n`K`iJsg+qydWDnVlPfMbWa<-V%*a7lE_CkCa_kA2WW0N!u-|soxT$_yn8iZEz7M zcj)-6f6yJ`g6T3<-4J544752WWsVY0p@5Epfh@=0sr#@Caz(4eI-_*=$aH=;s_$j) z5bVR&w$}zWRqz2sO*;}!c|CWNUkplHKDzuc1Auk!juG17*Tsj*Vz9UQ&uaLi<}|(4 z?FQct-;5s%_?g&utWwGA&Tr2n#Dm<=2D{#p!zzsK<{MLM za~2B>bO0r@JdoKzpAA7esKl*@S}1_Kb1Z zitQ}R;jv@CXG$+pAJ#4!?4cac3EkeEc> zd3|uJxe0Oi`vwX3i^yEm5trmg&6V+e0a(pt0ked$GU*~Y4{wBc46%o_GQi_-P^kcC z)Y54iHsMdflcUC-(+~>)^7ag(oppc;_s5TPqvc|X)m-O7IXXGFmwQ`#_oAQ%>r&LI z6p!$IU=*yxpLd~=@`FOP&xQ0Q)Z(d=K;C8>>HZ|iM}_B zS1dZuEk21XnYB9+`@>~bOD<#QR%V1-TVQ#b<$Us~(@nF4K%xFe{w2~!-un}nUrMY* zztL7=M6NX5z+NBGK;q`eCjG@VbO{;)ErvF_>jFRNlCZ?8^;-Zb=iFt00&B$Z-xLDB zy;Y4nU*b-RXXd9ZnCT9w4o{7vv`J6f)uoPZ`jTxDwQ-w9Ox#Irht>(@4yp#3T|ep~ zg(@7=JAzXy03@S~q^^ETxh#=FZR|is2&e9ZqY7&NLZ!lUz$fwPQ#cck827ei!A3I5 zl59sJQG$I3tfbGDc!9kMc;!)o@XC3< z$HZm3!FiLy(Ok?DgSJIvlx?$)LC14&;WR(Ce7h(p_2x%12yGb!FG1|%>pphDDJhvlgs&fj*#E-Uml#hVmbqzMQ{q@I%dBZjZ_x66ccPmm&R}&aF+50b2}_> zmdd6&9dJ=g;N_FyWwSL>s~F65P6=U+%lm{z5>1OtxAsl5@ROTl(?aiR-m2m8KGqST zSF@M89A?K%&D|D*@rUG4{{!xE_aO3Ta2OXcx- zF@ZnX+6Ut5b*WuTy{N&g^ieB3gzhNerk{kCxezZh~e)v|@X4d(m9- z()-f8W*cJ&W6EHt(pZiCqX*9-9zoS7OM+!ml75A(2d9?CO+AQSB0@hXdAnhorJuSt zqT#7Y?$5y`gJ1kZ{=yvJ5MO^?#B&6QVXE0&bnko|CmSc5n(stT&&+slUv8SGnC-Wd z_8xS6VLUuLHt9&}sbxVp=|S+$r=~)P#_vPpIm=Kd{?WtywqE4?gXyKIviwul1CJ$* zAHD9^Iy^UqsW`cIrC6*ZcD@R8bu;t5>R$`F3X1%Kz?pTP-|lFu<2Bh+*{s=k5unT= z(jn4*&Nb;ZhQkSD&LK(9hrC$n`Ke_GTHF5Kt*2(v*leN4){(NmnBkdz-sHpfx<4Psh@9i^@3|lZR@OGD9R7_nSYJ+nfJ*C$g&0i z{QAQn!br^^p(3s_pepw}OE+Der^L~~L~*BfIBEFWF(+ZW$T0_ZAwUHDo%?UglNuaf zjro`5$u6sp;<`~|&(4n(;}7b^1Bcs_@nhy-fqK!-U%S&~p$cIMy_^Wy>$w8ooxg%w zK#ASyfvX&iCS3TtRXtgTx??=q+{Ha5nXIjvx6 zYiTRL&_@#%^=n43?(xE$oefAfvBLcRQ`v|_+QiOT+R%>iuKk_4mo)5tzkd8_e0l0F z8)4P+dI-Dy|K0!c-*n&n7mFj8U>`V#%n-|&ac6J*n*)D&rkJ?@4UfMBtKj}OJp33- zsvb3+71=9l3k2&udgJUcWkS|jH&J}H#F=^S_g2FTRHJ<#p^~~2vC9?y1 zUIRJdCv~1*i>}~7YOcW9v`em+(Iu@Cr@Vmu&UKlqF57YMy;tEU zq21@JD%XVHFjwslcNu66zt!vcK)0c<~ghQ84rzr=R(LmJ`lc}!mM z!>6V@ds5UA$s=2D(DtD;d$?d?B0V=+l9u=1(!$-;M zDo^SjWh5%3vj(%S9HlXpIqCvMym_9gH^4)&*$HKqD!B?>o%YK6hC`-VzTa16k!9G* zYRdD<^D0~_Ixbc>Er*qdT))>VeVgtB4`a)O%AWG?%MR^++bKUQ`?RT+HdyvrJvP|d z$l7S1{8<87n$3?_>1ekVa<*_*xPCjiUm9IHUdmbuSV~x`T^e3mTDsTNUDjS!T~=7u zftjAnEhxxsX)uq(BzIx9ab^(%YIzK0wM{eG^0(tJ$4 znLp|*d#-s>rM6@z=_NT4pMJZ_UE?5aB)nBV;1L~3l53&8mbTQ~>Y%e_6|F_$#=gr% z$VFjZZeDItZe?xRJTN!l6s;3&$KJ6DwcSFA25|Y#LB+PzqSx8i%pXcGTmzjNO~D^s&V;o-w2`+A)){%V^7Jo9sWl?Hn~Oaz{OL zg>xnq^c4pcR$5&3ww9}N8S|P8noF8%nzPbcq?$`h)9|)jT$;I8J>si4Yw}f&Nh(q~ zq5IpVONNXc+Pi^ckI0YPXaG14rQT3DH&5%rI_@z7e{lauh8Y`Z1irBU$p112Rp=F{ zdj@=9&!6>T;s+qmtS9_@>--}W&7Jc>{%LP|I&Rn*O=;RSf1( z4^4{$e+fW9!=efODl7SkvO5S75BL8b*fw@YfB3*pU??1bhDJw|7N`Gz2L4&kGqEnB z5E@Ge+)@25hsHetEEVt`?f(K8O7+ycAcouNIBSS@Ax&Kx_MAY1HDtR`w=TX7GH#;r zK)3F@bKB+@YY0R=V)m>+B4&R`chR{o^o92u=0NQ3opXccSAwt9dL%X&nn^1C5$u_~ zUl)H0JOXdo3wXb>{Xx1xXATBG;s2TxMiE$fog1VDR_x z%03rs#^^wF>rLIDfZilzCxVCYHRR%V{&^be?E2@ZCQc!L@XJXn`@6P?t zs?NlWtx*Iz0};$dVyQLxBkEHo<^Ml=wd1Qy+oL@VKRV$mIr;7BYl*FVPlX90aifT} zcFK&jJ}K&qg!+V2B&G^ft{&jLsuA|`&YZA72bxw8S0#slg$D$Y6G`G5#(XnS2P z_7p&j@ldy3q;rbqH_QQ%8}0UJKy2nEl{pl*a=+OqAJ;Dkq*H;85?q)Rt&HbODnH!1r7~bf%!ScN}=futAf*84h6TRf; zNX>X1sBYb2WsB`<55iiQ?raMsAWhsmIkNAQ#fnu zZbuVv_pUZKhj0azV zb?gO!q|84cUBUJ^+ON;xUlo1q>45;j7l5Ds^bx^o^V|H@0fAAB2; z_C(%jry(r80@c=VrD)OpZ|jBO*+10^!v=q8Uu04|hx4*Nz9gF8X4`^Vp44M$aOOV? z#Q?HhxB8Ei{9oH0)3@KOEB##D9S=yn_R6e#@m$rN6iB}I@+`;+tj&$RhP2n)w(-gx zTN{Cb8F39IwFd^DVDYv2wIE_@Z|DX(6jd7y5L^bfY(~cFYB*<2>v>%n7~6Yu zj@JCrHYoF_KhNKm1PbKV6iT!5DmOrDW5%A;8`(B=qGu0$b?(<__rB9n$KxevHISt5 zRXDUIS@NTnp%~`_rZp5hjRcjwF_F6_n_F(vJ!g+HC-imY6{(!~1Sdx018H;yEuZTs z@E%z5MTAA&I6j!@X13jBqFu#BuDvWaM;UV6AvhU&F9|rp^Z7mDY*Bzj$~0$Fum9I| z!Ms}>gHyA5Dj=HB`oDx41lIhc5cJmiy5cFuuBFyXPqXs4BQ?dURS$dr>jc2lqGdA_ zj=RBGmaqZLYY~hqZpC}B1slE8-XZT^{O2m%d}Q+>a|iz1uH*dQH!F$-8md5ND%~oltITj zR3ueI6LG>p`rVCSOc*Q7)aDQJW|+61eeZqW_w!zo@5@ZWcenod-;x_!PTt+r^u@1s z2Qv^k@Dp86aHr@;=Jj>kPnp{*-6!fx?GAB%-0jW>wcvZm8u#BGcI2=%_2mQihil_w zuGE2VI61y0Kfc#_a(;V>ZB?OTru;8u&eKQRPuW(tIcC%y|I`^*SCQ;IvGJHT^vr}076R!VYmY`%uuID}wzB*9jbN8I z*B+%+m9ejU^_VT_sy(o8N^*IYU^bAhvG!l zE3US=-X2M4=&-wv-n#xB_uBCLy8J(_as6(zwc3bZH%+<73oKvqjnPWCs&3zYBq-kz z_S1C@M9a3@gIxnxhQ#0Gx&|HPh2duR{;sB&`jeL4!sMa)n!%xU@peUn;R!SFi=vT^ zuZzsj?!4_eUA^P(n!`=&o>n}qX^=j>Cnzvc!+XLnJbI$uarSX@b7F>P` zh6mx>pZqB1rZYoO@T4)d;t%Hr?=)ZQa>#og?LO4d{j3>WEb$DKz@D}eh38UgK^r{E zUi>Q6S~sWQUx@{lhn{&ukL~k^TC*xz`wPW&)%Rg?q^q>ZxasoG8gxFzD*qb5DyEb%z6vUP+AkO(rKH+LNNkDAYRiz zhz24Masvd3G!TY~fr26c!zCPs5fY%1QcfjNUu1LqrrkU=jOCKGBzogGJI8s4`tRHAKllFpW; zlj?;ris)QfF8B>xund%IGMX*2T)9xIr_(4I&^Z_uLc9r^B^Qb!(FhVjg^JKBg25OT0W2acd1h9d8)3FA7>l5!EIP)J z%^GMk#6{Gm(NfG%6h(0Z$mVianFj-f4d zI{Rf6luR>vD4k}YaS)W6h~}JMI7GwT!aj*S-<%IJVQPBa_>Qx!jFI23UI$Y~%0!u2 z3&;-Tv(xKjI?(7W%_w-%5~(d-#b{6@&}jxUq_i^Ti*|wX9AsL z9C-1F!13kEh-J&fc!WxXEyGoD1h|Rv+kDxV+bBR*m@TZw-W4xS|6sO~3vR`k_@bof>_2%aU%r?;ho8(KF0h&l9E8B!-)nP1z}P+Sp%UE z4uJs&PM~QPTp&;ayuzGLLn!Xmm73_-SZN$M7m;EzPPAN!{X_nf5o?-pPImBGkT z46HJOqotD4(RylZ4nk4PcMQt?y>8Q6yF8wGJO2!Yg#2>-JH_VRKl^H6)4p?aWAkSu zE`9gVQU~+{;JABYn7H8Lh!cF=3(L!vg*pDMJ3|Pebn$P0O&} z2A3d+6m Date: Tue, 21 Apr 2026 09:04:59 +0200 Subject: [PATCH 073/173] requirement not supposed to be in inputs --- tools/aoptk/aoptk_publication_tracker.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/tools/aoptk/aoptk_publication_tracker.xml b/tools/aoptk/aoptk_publication_tracker.xml index f0f0c9db..05e4a82d 100644 --- a/tools/aoptk/aoptk_publication_tracker.xml +++ b/tools/aoptk/aoptk_publication_tracker.xml @@ -24,7 +24,6 @@ - From 9cde057d464229d26c54f8b56427090ee13e40ad Mon Sep 17 00:00:00 2001 From: rdurnik Date: Tue, 21 Apr 2026 09:05:18 +0200 Subject: [PATCH 074/173] pdf input file name change --- tools/aoptk/aoptk_parse_pdf.xml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tools/aoptk/aoptk_parse_pdf.xml b/tools/aoptk/aoptk_parse_pdf.xml index bce531c8..798c1240 100644 --- a/tools/aoptk/aoptk_parse_pdf.xml +++ b/tools/aoptk/aoptk_parse_pdf.xml @@ -17,13 +17,14 @@ from aoptk.literature.pdf import PDF from aoptk.literature.pymupdf_parser import PymupdfParser +from aoptk.text_generation_api import TextGenerationAPI import os publication_id = os.path.splitext("$input_file.element_identifier")[0] if "${input_file.ext}" == "pdf": parser = PymupdfParser([PDF("$input_file")]) - if "$openai_api_key_credentials": - parser = parser.inject_text_generation(TextGenerationAPI(model="redhatai-scout")) + if os.environ.get("OPENAI_KEY"): + parser = parser.inject_text_generation(TextGenerationAPI(model="$llm_models_images", api_key=os.environ.get("OPENAI_KEY"))) pdf = parser.get_publications() text = pdf[0].full_text @@ -37,6 +38,7 @@ else: + @@ -45,8 +47,8 @@ else: - - + + From 86dfc8fe67f204333b1379fdeb12eb4c432dcd16 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Tue, 21 Apr 2026 09:05:57 +0200 Subject: [PATCH 075/173] file name rename --- tools/aoptk/aoptk_find_chemicals.xml | 2 +- tools/aoptk/aoptk_find_chemicals_non_llm.xml | 6 +++--- tools/aoptk/aoptk_find_relationships_non_llm.xml | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tools/aoptk/aoptk_find_chemicals.xml b/tools/aoptk/aoptk_find_chemicals.xml index b6f3bac9..6c45b081 100644 --- a/tools/aoptk/aoptk_find_chemicals.xml +++ b/tools/aoptk/aoptk_find_chemicals.xml @@ -10,7 +10,7 @@ diff --git a/tools/aoptk/aoptk_find_chemicals_non_llm.xml b/tools/aoptk/aoptk_find_chemicals_non_llm.xml index 390616a3..2b1619a5 100644 --- a/tools/aoptk/aoptk_find_chemicals_non_llm.xml +++ b/tools/aoptk/aoptk_find_chemicals_non_llm.xml @@ -1,4 +1,4 @@ - + Detect chemicals in text. macros.xml @@ -9,11 +9,11 @@ - + from aoptk.spacy_text_processor import SpacyText from aoptk.literature.pdf import PDF from aoptk.literature.pymupdf_parser import PymupdfParser diff --git a/tools/aoptk/aoptk_find_relationships_non_llm.xml b/tools/aoptk/aoptk_find_relationships_non_llm.xml index 9bc7c117..12f8b20d 100644 --- a/tools/aoptk/aoptk_find_relationships_non_llm.xml +++ b/tools/aoptk/aoptk_find_relationships_non_llm.xml @@ -1,4 +1,4 @@ - + Detect relationships between chemicals and effects in text. macros.xml @@ -9,11 +9,11 @@ - + import os import sys From 4b7a32a6cf0893888301bc751b728795534bf7aa Mon Sep 17 00:00:00 2001 From: rdurnik Date: Tue, 21 Apr 2026 09:22:12 +0200 Subject: [PATCH 076/173] test fixes --- tools/aoptk/aoptk_chemical_matching.xml | 2 +- tools/aoptk/aoptk_chemical_normalization_llm.xml | 2 +- tools/aoptk/aoptk_chemical_normalization_pubchem.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/aoptk/aoptk_chemical_matching.xml b/tools/aoptk/aoptk_chemical_matching.xml index c1b1635d..3f027850 100644 --- a/tools/aoptk/aoptk_chemical_matching.xml +++ b/tools/aoptk/aoptk_chemical_matching.xml @@ -43,7 +43,7 @@ merged_files.to_csv("merged_chemicals.tsv", sep="\t", index=False) - + diff --git a/tools/aoptk/aoptk_chemical_normalization_llm.xml b/tools/aoptk/aoptk_chemical_normalization_llm.xml index 6fdbad0f..9ab43265 100644 --- a/tools/aoptk/aoptk_chemical_normalization_llm.xml +++ b/tools/aoptk/aoptk_chemical_normalization_llm.xml @@ -47,7 +47,7 @@ chemicals.to_csv("normalized_chemicals.tsv", sep="\t", index=False) - + diff --git a/tools/aoptk/aoptk_chemical_normalization_pubchem.xml b/tools/aoptk/aoptk_chemical_normalization_pubchem.xml index 48b71e9b..ee1bd67a 100644 --- a/tools/aoptk/aoptk_chemical_normalization_pubchem.xml +++ b/tools/aoptk/aoptk_chemical_normalization_pubchem.xml @@ -40,7 +40,7 @@ chemicals.to_csv("normalized_chemicals.tsv", sep="\t", index=False) - + From f07680a11901bf37ef4ce895668eb1e0b4ceea7f Mon Sep 17 00:00:00 2001 From: rdurnik Date: Tue, 21 Apr 2026 09:31:49 +0200 Subject: [PATCH 077/173] added mesh terms normalization --- .../aoptk_chemical_normalization_mesh.xml | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 tools/aoptk/aoptk_chemical_normalization_mesh.xml diff --git a/tools/aoptk/aoptk_chemical_normalization_mesh.xml b/tools/aoptk/aoptk_chemical_normalization_mesh.xml new file mode 100644 index 00000000..ef204e6a --- /dev/null +++ b/tools/aoptk/aoptk_chemical_normalization_mesh.xml @@ -0,0 +1,55 @@ + + Normalize chemical entities using MeshTerms. + + macros.xml + + + + + + + + + + +from aoptk.chemical import Chemical +from aoptk.normalization.mesh_terms import MeshTerms +import os +import pandas as pd + +chemicals = pd.read_csv("$input_file", sep="\t") +chemicals["chemical"] = chemicals["chemical"].apply( + lambda x: MeshTerms().normalize_chemical(Chemical(x)) + ) +chemicals["heading"] = chemicals["chemical"].apply(lambda chem: chem.heading) +chemicals.to_csv("normalized_chemicals.tsv", sep="\t", index=False) + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From 655f4dc51ac529753d7f348d8af86f25e08aaee9 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Tue, 21 Apr 2026 12:05:28 +0200 Subject: [PATCH 078/173] macros fix --- tools/aoptk/macros.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tools/aoptk/macros.xml b/tools/aoptk/macros.xml index 3826a7a0..dfb6ee8c 100644 --- a/tools/aoptk/macros.xml +++ b/tools/aoptk/macros.xml @@ -97,26 +97,26 @@ - + - + - + - + From 2f91e1524365a522cea6b5163a184a885e316f8c Mon Sep 17 00:00:00 2001 From: rdurnik Date: Tue, 21 Apr 2026 12:08:29 +0200 Subject: [PATCH 079/173] added database selection --- tools/aoptk/aoptk_publication_tracker.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/aoptk/aoptk_publication_tracker.xml b/tools/aoptk/aoptk_publication_tracker.xml index 05e4a82d..24e4d6b8 100644 --- a/tools/aoptk/aoptk_publication_tracker.xml +++ b/tools/aoptk/aoptk_publication_tracker.xml @@ -24,6 +24,7 @@ + @@ -34,7 +35,6 @@ - From 9b34f33ecb59e24987ab430523232c3fdd8c6501 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Tue, 21 Apr 2026 13:20:10 +0200 Subject: [PATCH 080/173] reformated tsvs --- tools/aoptk/test-data/chemicals.tsv | 2 +- tools/aoptk/test-data/normalized.tsv | 2 +- tools/aoptk/test-data/relationships.tsv | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tools/aoptk/test-data/chemicals.tsv b/tools/aoptk/test-data/chemicals.tsv index a373f17c..61fc0ec8 100644 --- a/tools/aoptk/test-data/chemicals.tsv +++ b/tools/aoptk/test-data/chemicals.tsv @@ -1,4 +1,4 @@ id chemical 123 thioacetamide 456 acetaminophen -789 paracetamol \ No newline at end of file +789 paracetamol diff --git a/tools/aoptk/test-data/normalized.tsv b/tools/aoptk/test-data/normalized.tsv index 774b0683..9bbd0096 100644 --- a/tools/aoptk/test-data/normalized.tsv +++ b/tools/aoptk/test-data/normalized.tsv @@ -1,4 +1,4 @@ heading chemical thioacetamide thioacetamide acetaminophen acetaminophen -acetaminophen paracetamol \ No newline at end of file +acetaminophen paracetamol diff --git a/tools/aoptk/test-data/relationships.tsv b/tools/aoptk/test-data/relationships.tsv index 62e0c8a8..9d809fb9 100644 --- a/tools/aoptk/test-data/relationships.tsv +++ b/tools/aoptk/test-data/relationships.tsv @@ -1,2 +1,2 @@ -id chemical effect relationship -text thioacetamide liver fibrosis positive \ No newline at end of file +id chemical effect relationship +text thioacetamide liver fibrosis positive From 5dbaf14582a517954afdde92f3fc8ba1174b7018 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Tue, 21 Apr 2026 15:41:23 +0200 Subject: [PATCH 081/173] fixed the chemical list --- tools/aoptk/aoptk_chemical_normalization_llm.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/aoptk/aoptk_chemical_normalization_llm.xml b/tools/aoptk/aoptk_chemical_normalization_llm.xml index 9ab43265..20659a8a 100644 --- a/tools/aoptk/aoptk_chemical_normalization_llm.xml +++ b/tools/aoptk/aoptk_chemical_normalization_llm.xml @@ -25,7 +25,7 @@ text_generation_api = TextGenerationAPI(model="$llm_model", api_key=openai_key) chemical_list = pd.read_csv("$chemical_list", sep="\t")["chemical"].tolist() chemicals = pd.read_csv("$chemicals", sep="\t") chemicals["chemical"] = chemicals["chemical"].apply( - lambda x: TextGenerationAPI(model="$llm_model", api_key=openai_key).normalize_chemical(chemical=Chemical(x), chemical_list=chemical_list) + lambda x: TextGenerationAPI(model="$llm_model", api_key=openai_key).normalize_chemical(chemical=Chemical(x), chemical_list=[Chemical(chem) for chem in chemical_list]) ) chemicals["heading"] = chemicals["chemical"].apply(lambda chem: chem.heading) chemicals.to_csv("normalized_chemicals.tsv", sep="\t", index=False) From 49c006fd02b13c8b869c6192921761f22231697d Mon Sep 17 00:00:00 2001 From: rdurnik Date: Wed, 22 Apr 2026 13:46:22 +0200 Subject: [PATCH 082/173] mesh terms take xml --- .../aoptk_chemical_normalization_mesh.xml | 11 +- tools/aoptk/test-data/mesh_terms_database.xml | 407 ++++++++++++++++++ 2 files changed, 415 insertions(+), 3 deletions(-) create mode 100644 tools/aoptk/test-data/mesh_terms_database.xml diff --git a/tools/aoptk/aoptk_chemical_normalization_mesh.xml b/tools/aoptk/aoptk_chemical_normalization_mesh.xml index ef204e6a..2f8a9be6 100644 --- a/tools/aoptk/aoptk_chemical_normalization_mesh.xml +++ b/tools/aoptk/aoptk_chemical_normalization_mesh.xml @@ -16,12 +16,15 @@ from aoptk.chemical import Chemical from aoptk.normalization.mesh_terms import MeshTerms +from aoptk.normalization.provide_mesh_term_dataframe_from_xml import ProvideMeshTermDataframeFromXML import os import pandas as pd -chemicals = pd.read_csv("$input_file", sep="\t") +chemicals = pd.read_csv("$input_file", sep="\t")\ +mesh_terms_xml = pd.read_csv("$input_xml", sep="\t") +mesh_terms_df = ProvideMeshTermDataframeFromXML(mesh_terms_xml) chemicals["chemical"] = chemicals["chemical"].apply( - lambda x: MeshTerms().normalize_chemical(Chemical(x)) + lambda x: MeshTerms(mesh_terms=mesh_terms_df).normalize_chemical(Chemical(x)) ) chemicals["heading"] = chemicals["chemical"].apply(lambda chem: chem.heading) chemicals.to_csv("normalized_chemicals.tsv", sep="\t", index=False) @@ -31,6 +34,7 @@ chemicals.to_csv("normalized_chemicals.tsv", sep="\t", index=False) + @@ -40,6 +44,7 @@ chemicals.to_csv("normalized_chemicals.tsv", sep="\t", index=False) + @@ -49,7 +54,7 @@ Chemical Normalization MeSH Terms =================== Tool to normalize chemical entities using MeSH Terms. - +You need to provide the XML MeSH terms database: https://www.nlm.nih.gov/databases/download/mesh.html ]]> \ No newline at end of file diff --git a/tools/aoptk/test-data/mesh_terms_database.xml b/tools/aoptk/test-data/mesh_terms_database.xml new file mode 100644 index 00000000..c7f4e0e8 --- /dev/null +++ b/tools/aoptk/test-data/mesh_terms_database.xml @@ -0,0 +1,407 @@ + + + + + D000001 + + Calcimycin + + + 1974 + 11 + 19 + + + 2023 + 02 + 26 + + + 1984 + 01 + 01 + + + + + Q000008 + + administration & dosage + + + AD + + + + Q000009 + + adverse effects + + + AE + + + + Q000031 + + analogs & derivatives + + + AA + + + + Q000032 + + analysis + + + AN + + + + Q000037 + + antagonists & inhibitors + + + AI + + + + Q000096 + + biosynthesis + + + BI + + + + Q000097 + + blood + + + BL + + + + Q000134 + + cerebrospinal fluid + + + CF + + + + Q000138 + + chemical synthesis + + + CS + + + + Q000145 + + classification + + + CL + + + + Q000191 + + economics + + + EC + + + + Q000266 + + history + + + HI + + + + Q000276 + + immunology + + + IM + + + + Q000302 + + isolation & purification + + + IP + + + + Q000378 + + metabolism + + + ME + + + + Q000493 + + pharmacokinetics + + + PK + + + + Q000494 + + pharmacology + + + PD + + + + Q000506 + + poisoning + + + PO + + + + Q000528 + + radiation effects + + + RE + + + + Q000592 + + standards + + + ST + + + + Q000600 + + supply & distribution + + + SD + + + + Q000627 + + therapeutic use + + + TU + + + + Q000633 + + toxicity + + + TO + + + + Q000652 + + urine + + + UR + + + + Q000737 + + chemistry + + + CH + + + + Q000819 + + agonists + + + AG + + + 91(75); was A 23187 1975-90 (see under ANTIBIOTICS 1975-83) + + use CALCIMYCIN to search A 23187 1975-90 + + 91; was A 23187 1975-90 (see under ANTIBIOTICS 1975-83) + + + Antibiotics (1973-1974) + Carboxylic Acids (1973-1974) + + + + + D000900 + + Anti-Bacterial Agents + + + + + + D061207 + + Calcium Ionophores + + + + + + D02.355.291.933.125 + D02.540.576.625.125 + D03.633.100.221.173 + D04.345.241.654.125 + D04.345.674.625.125 + + + + M0000001 + + Calcimycin + + + 37H9VM9WZL + + An ionophorous, polyether antibiotic from Streptomyces chartreusensis. It binds and transports CALCIUM and other divalent cations across membranes and uncouples oxidative phosphorylation while inhibiting ATPase of rat liver mitochondria. The substance is used mostly as a biochemical tool to study the role of divalent cations in various biological systems. + + + 52665-69-7 (Calcimycin) + + + + M0000001 + M0353609 + + + + + T000002 + Calcimycin + + 1999 + 01 + 01 + + + FDA SRS (2014) + NLM (1975) + + + + T001124965 + 4-Benzoxazolecarboxylic acid, 5-(methylamino)-2-((3,9,11-trimethyl-8-(1-methyl-2-oxo-2-(1H-pyrrol-2-yl)ethyl)-1,7-dioxaspiro(5.5)undec-2-yl)methyl)-, (6S-(6alpha(2S*,3S*),8beta(R*),9beta,11alpha))- + + 2022 + 09 + 22 + + + NLM (2024) + + + + + + M0353609 + + A-23187 + + + 0 + + + + M0000001 + M0353609 + + + + + T000001 + A-23187 + + 1990 + 03 + 08 + + + NLM (1991) + + + + T000001 + A 23187 + + + T000003 + Antibiotic A23187 + + 1990 + 03 + 08 + + + NLM (1991) + + + + T000003 + A23187, Antibiotic + + + T000004 + A23187 + + 1974 + 11 + 11 + + + UNK (19XX) + + + + + + + + From 1660cf910ed3bdb69f18742adb573d00ebd9e0c4 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Wed, 22 Apr 2026 13:46:57 +0200 Subject: [PATCH 083/173] final output modification --- tools/aoptk/aoptk_final_modification.xml | 69 ++++++++++++++++++++++++ tools/aoptk/test-data/output.tsv | 5 ++ 2 files changed, 74 insertions(+) create mode 100644 tools/aoptk/aoptk_final_modification.xml create mode 100644 tools/aoptk/test-data/output.tsv diff --git a/tools/aoptk/aoptk_final_modification.xml b/tools/aoptk/aoptk_final_modification.xml new file mode 100644 index 00000000..8c22697e --- /dev/null +++ b/tools/aoptk/aoptk_final_modification.xml @@ -0,0 +1,69 @@ + + Modify the final output. + + macros.xml + + + + + + + + + + +import pandas as pd + +def join_unique(series): + values = series.dropna().astype(str).str.strip() + return ";".join(pd.unique(values)) + +output = pd.read_csv("$input_file", sep="\t")\ + +file_name = output["effect"].unique()[0].strip().lower().replace(" ", "_") + +output["relationship"] = output["relationship"].astype(str).str.strip().str.lower() +relationships = sorted(output["relationship"].unique()) +final_output = output[["heading"]].drop_duplicates() + +for relationship in relationships: + subset = output[output["relationship"] == relationship] + result = subset.groupby("heading", as_index=False, dropna=False).agg( + hits=("id", "size"), + ids=("id", join_unique), + ) + result.rename(columns={"hits": f"{relationship}", "ids": f"{relationship} IDs"}, inplace=True) + final_output = final_output.merge(result, on="heading", how="left") + +final_output.to_excel(f"./{file_name}.xlsx", index=False) + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tools/aoptk/test-data/output.tsv b/tools/aoptk/test-data/output.tsv new file mode 100644 index 00000000..f0d08865 --- /dev/null +++ b/tools/aoptk/test-data/output.tsv @@ -0,0 +1,5 @@ +id chemical effect relationship heading +11 acetaminophen liver fibrosis positive acetaminophen +11 thioacetamide liver fibrosis positive thioacetamide +12 paracetamol liver fibrosis negative acetaminophen +26 acetaminophen liver fibrosis negative acetaminophen From 677b1a83d2e9973bd371be40c2a7692c6868440a Mon Sep 17 00:00:00 2001 From: rdurnik Date: Wed, 22 Apr 2026 14:16:35 +0200 Subject: [PATCH 084/173] improved descs --- tools/aoptk/aoptk_chemical_normalization_mesh.xml | 2 +- ..._download_fulltext.xml => aoptk_download_publications.xml} | 0 tools/aoptk/aoptk_final_modification.xml | 4 ++-- tools/aoptk/aoptk_find_chemicals_non_llm.xml | 4 ++-- tools/aoptk/aoptk_find_relationships_text.xml | 4 ++-- tools/aoptk/aoptk_find_relationships_text_images.xml | 4 ++-- tools/aoptk/aoptk_parse_pdf.xml | 2 +- 7 files changed, 10 insertions(+), 10 deletions(-) rename tools/aoptk/{aoptk_download_fulltext.xml => aoptk_download_publications.xml} (100%) diff --git a/tools/aoptk/aoptk_chemical_normalization_mesh.xml b/tools/aoptk/aoptk_chemical_normalization_mesh.xml index 2f8a9be6..8c95a8e3 100644 --- a/tools/aoptk/aoptk_chemical_normalization_mesh.xml +++ b/tools/aoptk/aoptk_chemical_normalization_mesh.xml @@ -1,5 +1,5 @@ - Normalize chemical entities using MeshTerms. + Normalize chemical entities using MeSH Terms. macros.xml diff --git a/tools/aoptk/aoptk_download_fulltext.xml b/tools/aoptk/aoptk_download_publications.xml similarity index 100% rename from tools/aoptk/aoptk_download_fulltext.xml rename to tools/aoptk/aoptk_download_publications.xml diff --git a/tools/aoptk/aoptk_final_modification.xml b/tools/aoptk/aoptk_final_modification.xml index 8c22697e..69acc01f 100644 --- a/tools/aoptk/aoptk_final_modification.xml +++ b/tools/aoptk/aoptk_final_modification.xml @@ -1,5 +1,5 @@ - Modify the final output. + Finalize the output of relationship identification. macros.xml @@ -62,7 +62,7 @@ final_output.to_excel(f"./{file_name}.xlsx", index=False) Final Modification =================== -Tool to remove duplicates and modify the final output. +Tool to remove duplicates and finalize the output of relationship identification. ]]> diff --git a/tools/aoptk/aoptk_find_chemicals_non_llm.xml b/tools/aoptk/aoptk_find_chemicals_non_llm.xml index 2b1619a5..35ffd226 100644 --- a/tools/aoptk/aoptk_find_chemicals_non_llm.xml +++ b/tools/aoptk/aoptk_find_chemicals_non_llm.xml @@ -1,5 +1,5 @@ - Detect chemicals in text. + Detect chemicals in text using LLMs. macros.xml @@ -58,7 +58,7 @@ with open("chemicals.tsv", "w") as f_out: Chemical Identification =================== -Tool to identify chemical entities in a given text. +Tool to identify chemical entities in a given text using LLMs. ]]> diff --git a/tools/aoptk/aoptk_find_relationships_text.xml b/tools/aoptk/aoptk_find_relationships_text.xml index 2545c60f..f1dd8b98 100644 --- a/tools/aoptk/aoptk_find_relationships_text.xml +++ b/tools/aoptk/aoptk_find_relationships_text.xml @@ -1,5 +1,5 @@ - Detect relationships between chemicals and effects in text. + Detect relationships between chemicals and effects in text using LLMs. macros.xml @@ -79,7 +79,7 @@ with open("relationships.tsv", "w") as f_out: Relationship Identification in Text =================== -Tool to identify relationships between chemicals and effects in a given text. +Tool to identify relationships between chemicals and effects in a given text using LLMs. ]]> diff --git a/tools/aoptk/aoptk_find_relationships_text_images.xml b/tools/aoptk/aoptk_find_relationships_text_images.xml index 88cb1295..08db3acf 100644 --- a/tools/aoptk/aoptk_find_relationships_text_images.xml +++ b/tools/aoptk/aoptk_find_relationships_text_images.xml @@ -1,5 +1,5 @@ - Detect relationships between chemicals and effects in text. + Detect relationships between chemicals and effects in text and imagesusing LLMs. macros.xml @@ -98,7 +98,7 @@ with open("relationships.tsv", "w") as f_out: Relationship Identification in Text =================== -Tool to identify relationships between chemicals and effects in a given text. +Tool to identify relationships between chemicals and effects in a given text and images using LLMs. ]]> diff --git a/tools/aoptk/aoptk_parse_pdf.xml b/tools/aoptk/aoptk_parse_pdf.xml index 798c1240..a61f7cf3 100644 --- a/tools/aoptk/aoptk_parse_pdf.xml +++ b/tools/aoptk/aoptk_parse_pdf.xml @@ -56,7 +56,7 @@ else: Parse PDF =================== -Tool to parse PDF files. API key for LLMs is optional and only needed for PDFs that are scanned images. +Tool to parse PDF files. API key for LLMs is optional and only needed for PDFs that are scanned images (usually old PDF files). ]]> From cb7bd40913680814eca5316ebda7a822039c8885 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Wed, 22 Apr 2026 16:52:03 +0200 Subject: [PATCH 085/173] rename --- .../{aoptk_final_modification.xml => aoptk_output_modifier.xml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tools/aoptk/{aoptk_final_modification.xml => aoptk_output_modifier.xml} (100%) diff --git a/tools/aoptk/aoptk_final_modification.xml b/tools/aoptk/aoptk_output_modifier.xml similarity index 100% rename from tools/aoptk/aoptk_final_modification.xml rename to tools/aoptk/aoptk_output_modifier.xml From fb3e10935d581a8054d41963736d2f9186ca1b01 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Wed, 22 Apr 2026 16:52:51 +0200 Subject: [PATCH 086/173] removed leftover inputs --- tools/aoptk/aoptk_find_chemicals.xml | 3 --- 1 file changed, 3 deletions(-) diff --git a/tools/aoptk/aoptk_find_chemicals.xml b/tools/aoptk/aoptk_find_chemicals.xml index 6c45b081..abd849e4 100644 --- a/tools/aoptk/aoptk_find_chemicals.xml +++ b/tools/aoptk/aoptk_find_chemicals.xml @@ -44,10 +44,7 @@ with open("chemicals.tsv", "w") as f_out: - - - [0-9a-zA-Z]+ From 90e2e60e8007065dfdddf6a7cea9380b73c6f717 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Thu, 23 Apr 2026 08:49:27 +0200 Subject: [PATCH 087/173] file rename --- .../{aoptk_output_modifier.xml => aoptk_finalize_output.xml} | 0 ...aoptk_chemical_matching.xml => aoptk_match_chemical_lists.xml} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename tools/aoptk/{aoptk_output_modifier.xml => aoptk_finalize_output.xml} (100%) rename tools/aoptk/{aoptk_chemical_matching.xml => aoptk_match_chemical_lists.xml} (100%) diff --git a/tools/aoptk/aoptk_output_modifier.xml b/tools/aoptk/aoptk_finalize_output.xml similarity index 100% rename from tools/aoptk/aoptk_output_modifier.xml rename to tools/aoptk/aoptk_finalize_output.xml diff --git a/tools/aoptk/aoptk_chemical_matching.xml b/tools/aoptk/aoptk_match_chemical_lists.xml similarity index 100% rename from tools/aoptk/aoptk_chemical_matching.xml rename to tools/aoptk/aoptk_match_chemical_lists.xml From b7da8047a0403aedc9edf339e41d0a5c9bfeabe7 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Thu, 23 Apr 2026 11:19:40 +0200 Subject: [PATCH 088/173] normalization combined into one tool Co-authored-by: Copilot --- .../aoptk_chemical_normalization_llm.xml | 62 -------- .../aoptk_chemical_normalization_mesh.xml | 60 -------- .../aoptk_chemical_normalization_pubchem.xml | 55 -------- tools/aoptk/aoptk_normalize_chemical.xml | 132 ++++++++++++++++++ 4 files changed, 132 insertions(+), 177 deletions(-) delete mode 100644 tools/aoptk/aoptk_chemical_normalization_llm.xml delete mode 100644 tools/aoptk/aoptk_chemical_normalization_mesh.xml delete mode 100644 tools/aoptk/aoptk_chemical_normalization_pubchem.xml create mode 100644 tools/aoptk/aoptk_normalize_chemical.xml diff --git a/tools/aoptk/aoptk_chemical_normalization_llm.xml b/tools/aoptk/aoptk_chemical_normalization_llm.xml deleted file mode 100644 index 20659a8a..00000000 --- a/tools/aoptk/aoptk_chemical_normalization_llm.xml +++ /dev/null @@ -1,62 +0,0 @@ - - Normalize chemical entities using LLMs. - - macros.xml - - - - - - - - - - - -import os -from aoptk.text_generation_api import TextGenerationAPI -from aoptk.chemical import Chemical -import pandas as pd - -openai_key = os.environ.get("OPENAI_KEY") -text_generation_api = TextGenerationAPI(model="$llm_model", api_key=openai_key) -chemical_list = pd.read_csv("$chemical_list", sep="\t")["chemical"].tolist() -chemicals = pd.read_csv("$chemicals", sep="\t") -chemicals["chemical"] = chemicals["chemical"].apply( - lambda x: TextGenerationAPI(model="$llm_model", api_key=openai_key).normalize_chemical(chemical=Chemical(x), chemical_list=[Chemical(chem) for chem in chemical_list]) - ) -chemicals["heading"] = chemicals["chemical"].apply(lambda chem: chem.heading) -chemicals.to_csv("normalized_chemicals.tsv", sep="\t", index=False) - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/tools/aoptk/aoptk_chemical_normalization_mesh.xml b/tools/aoptk/aoptk_chemical_normalization_mesh.xml deleted file mode 100644 index 8c95a8e3..00000000 --- a/tools/aoptk/aoptk_chemical_normalization_mesh.xml +++ /dev/null @@ -1,60 +0,0 @@ - - Normalize chemical entities using MeSH Terms. - - macros.xml - - - - - - - - - - -from aoptk.chemical import Chemical -from aoptk.normalization.mesh_terms import MeshTerms -from aoptk.normalization.provide_mesh_term_dataframe_from_xml import ProvideMeshTermDataframeFromXML -import os -import pandas as pd - -chemicals = pd.read_csv("$input_file", sep="\t")\ -mesh_terms_xml = pd.read_csv("$input_xml", sep="\t") -mesh_terms_df = ProvideMeshTermDataframeFromXML(mesh_terms_xml) -chemicals["chemical"] = chemicals["chemical"].apply( - lambda x: MeshTerms(mesh_terms=mesh_terms_df).normalize_chemical(Chemical(x)) - ) -chemicals["heading"] = chemicals["chemical"].apply(lambda chem: chem.heading) -chemicals.to_csv("normalized_chemicals.tsv", sep="\t", index=False) - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/tools/aoptk/aoptk_chemical_normalization_pubchem.xml b/tools/aoptk/aoptk_chemical_normalization_pubchem.xml deleted file mode 100644 index ee1bd67a..00000000 --- a/tools/aoptk/aoptk_chemical_normalization_pubchem.xml +++ /dev/null @@ -1,55 +0,0 @@ - - Normalize chemical entities using PubChem API. - - macros.xml - - - - - - - - - - -from aoptk.normalization.pubchem_api import PubChemAPI -from aoptk.chemical import Chemical -import os -import pandas as pd - -chemicals = pd.read_csv("$input_file", sep="\t") -chemicals["chemical"] = chemicals["chemical"].apply( - lambda x: PubChemAPI().normalize_chemical(Chemical(x)) - ) -chemicals["heading"] = chemicals["chemical"].apply(lambda chem: chem.heading) -chemicals.to_csv("normalized_chemicals.tsv", sep="\t", index=False) - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/tools/aoptk/aoptk_normalize_chemical.xml b/tools/aoptk/aoptk_normalize_chemical.xml new file mode 100644 index 00000000..3463f285 --- /dev/null +++ b/tools/aoptk/aoptk_normalize_chemical.xml @@ -0,0 +1,132 @@ + + Normalize chemical entities using LLMs. + + macros.xml + + + + + + + + + + +import os +import pandas as pd +from aoptk.chemical import Chemical + +chemicals = pd.read_csv("$input_file", sep="\t") + +#if $normalization_method.type_choice == "llm": +from aoptk.text_generation_api import TextGenerationAPI + +litellm_config_file = os.environ.get("LITELLM_CONFIG_FILE") +if not litellm_config_file: + sys.exit("LITELLM_CONFIG_FILE environment variable is not set.") +with open(litellm_config_file, "r") as f: + config = yaml.safe_load(f) +litellm_api_key = source.get("LITELLM_API_KEY") + + +chemical_list = pd.read_csv("$chemical_list", sep="\t")["chemical"].tolist() +chemicals["chemical"] = chemicals["chemical"].apply( + lambda x: TextGenerationAPI(model="$llm_model", api_key=litellm_api_key).normalize_chemical(chemical=Chemical(x), chemical_list=[Chemical(chem) for chem in chemical_list]) + ) +#end if + +#else if $normalization_method.type_choice == "pubchem": +from aoptk.normalization.pubchem_api import PubChemAPI + +chemicals["chemical"] = chemicals["chemical"].apply( + lambda x: PubChemAPI().normalize_chemical(Chemical(x)) + ) +#end if + +#else if $normalization_method.type_choice == "mesh": +from aoptk.normalization.mesh_terms import MeshTerms +from aoptk.normalization.provide_mesh_term_dataframe_from_xml import ProvideMeshTermDataframeFromXML + +mesh_terms_xml = pd.read_csv("$input_xml", sep="\t") +mesh_terms_df = ProvideMeshTermDataframeFromXML(mesh_terms_xml) +chemicals["chemical"] = chemicals["chemical"].apply( + lambda x: MeshTerms(mesh_terms=mesh_terms_df).normalize_chemical(Chemical(x)) + ) +#end if + +chemicals["heading"] = chemicals["chemical"].apply(lambda chem: chem.heading) +chemicals.to_csv("normalized_chemicals.tsv", sep="\t", index=False) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From ed62ee1bb23f1ada15b7f158064663b571289721 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Thu, 23 Apr 2026 11:42:02 +0200 Subject: [PATCH 089/173] combined find chemicals tools Co-authored-by: Copilot --- tools/aoptk/aoptk_find_chemicals.xml | 68 ++++++++++++++++---- tools/aoptk/aoptk_find_chemicals_non_llm.xml | 65 ------------------- 2 files changed, 54 insertions(+), 79 deletions(-) delete mode 100644 tools/aoptk/aoptk_find_chemicals_non_llm.xml diff --git a/tools/aoptk/aoptk_find_chemicals.xml b/tools/aoptk/aoptk_find_chemicals.xml index abd849e4..13d6b61a 100644 --- a/tools/aoptk/aoptk_find_chemicals.xml +++ b/tools/aoptk/aoptk_find_chemicals.xml @@ -1,12 +1,11 @@ - Detect chemicals in text. + Find chemical entities using LLMs. macros.xml - import os -from aoptk.text_generation_api import TextGenerationAPI +import pandas as pd from aoptk.literature.pdf import PDF from aoptk.literature.pymupdf_parser import PymupdfParser publication_id = os.path.splitext("$input_file.element_identifier")[0] -openai_key = os.environ.get("OPENAI_KEY") -text_generation_api = TextGenerationAPI(model="$llm_model", api_key=openai_key) if "${input_file.ext}" == "pdf": text = PymupdfParser([PDF("$input_file")]).get_publications()[0].full_text - chemicals = text_generation_api.find_chemicals(text) - elif "${input_file.ext}" == "txt": with open("$input_file", "r") as f_in: text = f_in.read() - chemicals = text_generation_api.find_chemicals(text) else: raise ValueError(f"Unsupported input file format: {input_ext}") + +#if $identification_method.type_choice == "llm": +from aoptk.text_generation_api import TextGenerationAPI + +litellm_config_file = os.environ.get("LITELLM_CONFIG_FILE") +if not litellm_config_file: + sys.exit("LITELLM_CONFIG_FILE environment variable is not set.") +with open(litellm_config_file, "r") as f: + config = yaml.safe_load(f) +litellm_api_key = source.get("LITELLM_API_KEY") + +chemicals = TextGenerationAPI(model="$llm_model", api_key=litellm_api_key).find_chemicals(text) +#end if + + +#else if $identification_method.type_choice == "scispacy": +from aoptk.spacy_text_processor import SpacyText + +chemicals = SpacyText().find_chemical(text) +#end if + with open("chemicals.tsv", "w") as f_out: f_out.write("id\tchemical\n") for chemical in chemicals: f_out.write(f"{publication_id}\t{chemical.name}\n") + - - - + + + + + + + + + + + + + + + + + @@ -54,8 +84,18 @@ with open("chemicals.tsv", "w") as f_out: - - + + + + + + + + + + + + @@ -63,7 +103,7 @@ with open("chemicals.tsv", "w") as f_out: Chemical Identification =================== -Tool to identify chemical entities in a given text. +Tools to identify chemical entities in a given text. ]]> diff --git a/tools/aoptk/aoptk_find_chemicals_non_llm.xml b/tools/aoptk/aoptk_find_chemicals_non_llm.xml deleted file mode 100644 index 35ffd226..00000000 --- a/tools/aoptk/aoptk_find_chemicals_non_llm.xml +++ /dev/null @@ -1,65 +0,0 @@ - - Detect chemicals in text using LLMs. - - macros.xml - - - - - - - - - - -from aoptk.spacy_text_processor import SpacyText -from aoptk.literature.pdf import PDF -from aoptk.literature.pymupdf_parser import PymupdfParser -import os - -publication_id = os.path.splitext("$input_file.element_identifier")[0] - -if "${input_file.ext}" == "pdf": - text = PymupdfParser([PDF("$input_file")]).get_publications()[0].full_text - chemicals = SpacyText().find_chemical(text) - -elif "${input_file.ext}" == "txt": - with open("$input_file", "r") as f_in: - text = f_in.read() - chemicals = SpacyText().find_chemical(text) -else: - raise ValueError(f"Unsupported input file format: {input_ext}") - -with open("chemicals.tsv", "w") as f_out: - f_out.write("id\tchemical\n") - for chemical in chemicals: - f_out.write(f"{publication_id}\t{chemical.name}\n") - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file From 980494ecb8eb70efaa393c53708c8a1795772bba Mon Sep 17 00:00:00 2001 From: rdurnik Date: Thu, 23 Apr 2026 11:43:38 +0200 Subject: [PATCH 090/173] fixed desc --- tools/aoptk/aoptk_find_chemicals.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/aoptk/aoptk_find_chemicals.xml b/tools/aoptk/aoptk_find_chemicals.xml index 13d6b61a..69ea41f3 100644 --- a/tools/aoptk/aoptk_find_chemicals.xml +++ b/tools/aoptk/aoptk_find_chemicals.xml @@ -1,5 +1,5 @@ - Find chemical entities using LLMs. + Identify chemicals in text. macros.xml From 592230b3653bc762ca88ed4808977701393ba50c Mon Sep 17 00:00:00 2001 From: rdurnik Date: Thu, 23 Apr 2026 12:10:05 +0200 Subject: [PATCH 091/173] renamed --- tools/aoptk/aoptk_normalize_chemicals.xml | 132 ++++++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 tools/aoptk/aoptk_normalize_chemicals.xml diff --git a/tools/aoptk/aoptk_normalize_chemicals.xml b/tools/aoptk/aoptk_normalize_chemicals.xml new file mode 100644 index 00000000..3463f285 --- /dev/null +++ b/tools/aoptk/aoptk_normalize_chemicals.xml @@ -0,0 +1,132 @@ + + Normalize chemical entities using LLMs. + + macros.xml + + + + + + + + + + +import os +import pandas as pd +from aoptk.chemical import Chemical + +chemicals = pd.read_csv("$input_file", sep="\t") + +#if $normalization_method.type_choice == "llm": +from aoptk.text_generation_api import TextGenerationAPI + +litellm_config_file = os.environ.get("LITELLM_CONFIG_FILE") +if not litellm_config_file: + sys.exit("LITELLM_CONFIG_FILE environment variable is not set.") +with open(litellm_config_file, "r") as f: + config = yaml.safe_load(f) +litellm_api_key = source.get("LITELLM_API_KEY") + + +chemical_list = pd.read_csv("$chemical_list", sep="\t")["chemical"].tolist() +chemicals["chemical"] = chemicals["chemical"].apply( + lambda x: TextGenerationAPI(model="$llm_model", api_key=litellm_api_key).normalize_chemical(chemical=Chemical(x), chemical_list=[Chemical(chem) for chem in chemical_list]) + ) +#end if + +#else if $normalization_method.type_choice == "pubchem": +from aoptk.normalization.pubchem_api import PubChemAPI + +chemicals["chemical"] = chemicals["chemical"].apply( + lambda x: PubChemAPI().normalize_chemical(Chemical(x)) + ) +#end if + +#else if $normalization_method.type_choice == "mesh": +from aoptk.normalization.mesh_terms import MeshTerms +from aoptk.normalization.provide_mesh_term_dataframe_from_xml import ProvideMeshTermDataframeFromXML + +mesh_terms_xml = pd.read_csv("$input_xml", sep="\t") +mesh_terms_df = ProvideMeshTermDataframeFromXML(mesh_terms_xml) +chemicals["chemical"] = chemicals["chemical"].apply( + lambda x: MeshTerms(mesh_terms=mesh_terms_df).normalize_chemical(Chemical(x)) + ) +#end if + +chemicals["heading"] = chemicals["chemical"].apply(lambda chem: chem.heading) +chemicals.to_csv("normalized_chemicals.tsv", sep="\t", index=False) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From 6a6c9141e17302797cb10155b41ae80006ff8180 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Thu, 23 Apr 2026 12:10:13 +0200 Subject: [PATCH 092/173] rename --- tools/aoptk/aoptk_normalize_chemical.xml | 132 ----------------------- 1 file changed, 132 deletions(-) delete mode 100644 tools/aoptk/aoptk_normalize_chemical.xml diff --git a/tools/aoptk/aoptk_normalize_chemical.xml b/tools/aoptk/aoptk_normalize_chemical.xml deleted file mode 100644 index 3463f285..00000000 --- a/tools/aoptk/aoptk_normalize_chemical.xml +++ /dev/null @@ -1,132 +0,0 @@ - - Normalize chemical entities using LLMs. - - macros.xml - - - - - - - - - - -import os -import pandas as pd -from aoptk.chemical import Chemical - -chemicals = pd.read_csv("$input_file", sep="\t") - -#if $normalization_method.type_choice == "llm": -from aoptk.text_generation_api import TextGenerationAPI - -litellm_config_file = os.environ.get("LITELLM_CONFIG_FILE") -if not litellm_config_file: - sys.exit("LITELLM_CONFIG_FILE environment variable is not set.") -with open(litellm_config_file, "r") as f: - config = yaml.safe_load(f) -litellm_api_key = source.get("LITELLM_API_KEY") - - -chemical_list = pd.read_csv("$chemical_list", sep="\t")["chemical"].tolist() -chemicals["chemical"] = chemicals["chemical"].apply( - lambda x: TextGenerationAPI(model="$llm_model", api_key=litellm_api_key).normalize_chemical(chemical=Chemical(x), chemical_list=[Chemical(chem) for chem in chemical_list]) - ) -#end if - -#else if $normalization_method.type_choice == "pubchem": -from aoptk.normalization.pubchem_api import PubChemAPI - -chemicals["chemical"] = chemicals["chemical"].apply( - lambda x: PubChemAPI().normalize_chemical(Chemical(x)) - ) -#end if - -#else if $normalization_method.type_choice == "mesh": -from aoptk.normalization.mesh_terms import MeshTerms -from aoptk.normalization.provide_mesh_term_dataframe_from_xml import ProvideMeshTermDataframeFromXML - -mesh_terms_xml = pd.read_csv("$input_xml", sep="\t") -mesh_terms_df = ProvideMeshTermDataframeFromXML(mesh_terms_xml) -chemicals["chemical"] = chemicals["chemical"].apply( - lambda x: MeshTerms(mesh_terms=mesh_terms_df).normalize_chemical(Chemical(x)) - ) -#end if - -chemicals["heading"] = chemicals["chemical"].apply(lambda chem: chem.heading) -chemicals.to_csv("normalized_chemicals.tsv", sep="\t", index=False) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file From b94902d6a7a77bc9fc96402d5b8599677ee11d1b Mon Sep 17 00:00:00 2001 From: rdurnik Date: Thu, 23 Apr 2026 12:10:23 +0200 Subject: [PATCH 093/173] combined download Co-authored-by: Copilot --- tools/aoptk/aoptk_download_abstracts.xml | 74 --------- tools/aoptk/aoptk_download_pdf.xml | 55 ------- .../aoptk/aoptk_download_publication_data.xml | 149 ++++++++++++++++++ tools/aoptk/aoptk_download_publications.xml | 76 --------- 4 files changed, 149 insertions(+), 205 deletions(-) delete mode 100644 tools/aoptk/aoptk_download_abstracts.xml delete mode 100644 tools/aoptk/aoptk_download_pdf.xml create mode 100644 tools/aoptk/aoptk_download_publication_data.xml delete mode 100644 tools/aoptk/aoptk_download_publications.xml diff --git a/tools/aoptk/aoptk_download_abstracts.xml b/tools/aoptk/aoptk_download_abstracts.xml deleted file mode 100644 index 0294deea..00000000 --- a/tools/aoptk/aoptk_download_abstracts.xml +++ /dev/null @@ -1,74 +0,0 @@ - - Download abstracts for a list of publication IDs. - - macros.xml - - - - - - - - - - - -from aoptk.literature.databases.pubmed import PubMed -from aoptk.literature.databases.europepmc import EuropePMC -from aoptk.literature.abstract import Abstract -from Bio import Entrez -import os - -with open("$input_file", "r") as f: - ids = [line.strip() for line in f.readlines()] -email = os.environ.get("EMAIL") - - -if "${literature_database_pubmed_europepmc}" == "pubmed": - Entrez.email = email - pubmed = PubMed.__new__(PubMed) - pubmed.id_list = ids - abstracts = pubmed.get_abstracts() -elif "${literature_database_pubmed_europepmc}" == "europepmc": - europepmc = EuropePMC("") - europepmc.id_list = ids - abstracts = europepmc.get_abstracts() -else: - raise ValueError("Select valid database.") - -for abstract in abstracts: - with open(f"{abstract.publication_id}.txt", "w") as f: - f.write(abstract.text) - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/tools/aoptk/aoptk_download_pdf.xml b/tools/aoptk/aoptk_download_pdf.xml deleted file mode 100644 index 4df1a824..00000000 --- a/tools/aoptk/aoptk_download_pdf.xml +++ /dev/null @@ -1,55 +0,0 @@ - - Download PDFs for a list of publication IDs. - - macros.xml - - - - - - - - - - -from aoptk.literature.databases.pmc import PMC -import os - -with open("$input_file", "r") as f: - ids = [line.strip() for line in f.readlines()] - -pmc = PMC("", storage = "./") -pmc.id_list = ids -pdfs = pmc.get_pdfs() - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/tools/aoptk/aoptk_download_publication_data.xml b/tools/aoptk/aoptk_download_publication_data.xml new file mode 100644 index 00000000..df68e2ee --- /dev/null +++ b/tools/aoptk/aoptk_download_publication_data.xml @@ -0,0 +1,149 @@ + + Download publication data - full text publications and figures, abstracts, or PDFs. + + macros.xml + + + + + + + + + + +import os + +with open("$input_file", "r") as f: + ids = [line.strip() for line in f.readlines()] +email = os.environ.get("EMAIL") + +#if $data_type.type_choice == "publications": +from aoptk.literature.databases.pmc import PMC +from aoptk.literature.databases.europepmc import EuropePMC +from Bio import Entrez + +if "${literature_database_europepmc_pmc}" == "pmc": + Entrez.email = email + pmc = PMC("", storage = "./", figure_storage="./figures") + pmc.id_list = ids + publications = pmc.get_publications() +elif "${literature_database_europepmc_pmc}" == "europepmc": + europepmc = EuropePMC("", storage = "./", figure_storage="./figures") + europepmc.id_list = ids + publications = europepmc.get_publications() +else: + raise ValueError("Select valid database.") + +for publication in publications: + with open(f"{publication.publication_id}.txt", "w") as f: + f.write(publication.full_text) +#end if + + +#if $data_type.type_choice == "abstracts": +from aoptk.literature.databases.europepmc import EuropePMC +from Bio import Entrez + +if "${literature_database_pubmed_europepmc}" == "pubmed": + Entrez.email = email + pubmed = PubMed.__new__(PubMed) + pubmed.id_list = ids + abstracts = pubmed.get_abstracts() +elif "${literature_database_pubmed_europepmc}" == "europepmc": + europepmc = EuropePMC("") + europepmc.id_list = ids + abstracts = europepmc.get_abstracts() +else: + raise ValueError("Select valid database.") + +for abstract in abstracts: + with open(f"{abstract.publication_id}.txt", "w") as f: + f.write(abstract.text) +#end if + + +#if $data_type.type_choice == "pdfs": +from aoptk.literature.databases.pmc import PMC + +pmc = PMC("", storage = "./") +pmc.id_list = ids +pdfs = pmc.get_pdfs() +#end if + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tools/aoptk/aoptk_download_publications.xml b/tools/aoptk/aoptk_download_publications.xml deleted file mode 100644 index 4b6f3b5d..00000000 --- a/tools/aoptk/aoptk_download_publications.xml +++ /dev/null @@ -1,76 +0,0 @@ - - Download full texts for a list of publication IDs. - - macros.xml - - - - - - - - - - - -from aoptk.literature.databases.pmc import PMC -from aoptk.literature.databases.europepmc import EuropePMC -from Bio import Entrez -import os - -with open("$input_file", "r") as f: - ids = [line.strip() for line in f.readlines()] -email = os.environ.get("EMAIL") - - -if "${literature_database_europepmc_pmc}" == "pmc": - Entrez.email = email - pmc = PMC("", storage = "./", figure_storage="./figures") - pmc.id_list = ids - publications = pmc.get_publications() -elif "${literature_database_europepmc_pmc}" == "europepmc": - europepmc = EuropePMC("", storage = "./", figure_storage="./figures") - europepmc.id_list = ids - publications = europepmc.get_publications() -else: - raise ValueError("Select valid database.") - -for publication in publications: - with open(f"{publication.publication_id}.txt", "w") as f: - f.write(publication.full_text) - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file From 773149645a03a6de2c15e1f56a3a7d8076ffb266 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Thu, 23 Apr 2026 12:15:50 +0200 Subject: [PATCH 094/173] improved desc --- tools/aoptk/aoptk_download_publication_data.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/aoptk/aoptk_download_publication_data.xml b/tools/aoptk/aoptk_download_publication_data.xml index df68e2ee..61cac012 100644 --- a/tools/aoptk/aoptk_download_publication_data.xml +++ b/tools/aoptk/aoptk_download_publication_data.xml @@ -79,7 +79,7 @@ pdfs = pmc.get_pdfs() - + @@ -142,7 +142,7 @@ pdfs = pmc.get_pdfs() Download Publication Data =================== -Tools to download publication data - full text publications and figures, abstracts, or PDFs. +Tools to download publication data - full text publications (including abstracts) and figures, abstracts, or PDFs. ]]> From bbf11e8132fff56ecd0d2cb18b2c39ce55b64de6 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Thu, 23 Apr 2026 12:17:54 +0200 Subject: [PATCH 095/173] improved desc --- tools/aoptk/aoptk_download_publication_data.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/aoptk/aoptk_download_publication_data.xml b/tools/aoptk/aoptk_download_publication_data.xml index 61cac012..bbb66167 100644 --- a/tools/aoptk/aoptk_download_publication_data.xml +++ b/tools/aoptk/aoptk_download_publication_data.xml @@ -81,7 +81,7 @@ pdfs = pmc.get_pdfs() - + From bfc05e46447b171d7cf2b74aa77c8875f7aec0ce Mon Sep 17 00:00:00 2001 From: rdurnik Date: Thu, 23 Apr 2026 13:42:17 +0200 Subject: [PATCH 096/173] combined relationships Co-authored-by: Copilot --- tools/aoptk/aoptk_find_relationships.xml | 155 ++++++++++++++++++ .../aoptk_find_relationships_non_llm.xml | 84 ---------- tools/aoptk/aoptk_find_relationships_text.xml | 86 ---------- .../aoptk_find_relationships_text_images.xml | 105 ------------ 4 files changed, 155 insertions(+), 275 deletions(-) create mode 100644 tools/aoptk/aoptk_find_relationships.xml delete mode 100644 tools/aoptk/aoptk_find_relationships_non_llm.xml delete mode 100644 tools/aoptk/aoptk_find_relationships_text.xml delete mode 100644 tools/aoptk/aoptk_find_relationships_text_images.xml diff --git a/tools/aoptk/aoptk_find_relationships.xml b/tools/aoptk/aoptk_find_relationships.xml new file mode 100644 index 00000000..147e27bd --- /dev/null +++ b/tools/aoptk/aoptk_find_relationships.xml @@ -0,0 +1,155 @@ + + Identify relationships between chemicals and effects. + + macros.xml + + + + + + + + + + + +if "${input_file.ext}" == "pdf": + text = PymupdfParser([PDF("$input_file")]).inject_text_generation(TextGenerationAPI(model="redhatai-scout")).get_publications()[0].full_text + +elif "${input_file.ext}" == "txt": + with open("$input_file", "r") as f_in: + text = f_in.read() + +else: + raise ValueError(f"Unsupported input file format: ${input_file.ext}") + +chemicals = pd.read_csv("$chemicals", sep="\t")["chemical"].tolist() +publication_id = os.path.splitext("$input_file.element_identifier")[0] +effects = ["${effect}"] + +#if $identification_method.type_choice == "llm_text" or if $identification_method.type_choice == "llm_text_images": +from aoptk.text_generation_api import TextGenerationAPI + +litellm_config_file = os.environ.get("LITELLM_CONFIG_FILE") +if not litellm_config_file: + sys.exit("LITELLM_CONFIG_FILE environment variable is not set.") +with open(litellm_config_file, "r") as f: + config = yaml.safe_load(f) +litellm_api_key = source.get("LITELLM_API_KEY") +#end if + + #if $identification_method.type_choice == "llm_text" + relationships = TextGenerationAPI(model="$llm_model", api_key=litellm_api_key).find_relationships_in_text(text=text, chemicals=chemicals, effects=effects) + #end if + + + #if $identification_method.type_choice == "llm_text_images": + text_with_images = text + text_with_images += "\n\n\n\nALSO ANALYZE THESE FIGURES CONVERTED TO TEXT:" + for figure in "$input_images".iterdir(): + text_image = TextGenerationAPI(model="$llm_models_images").convert_image( + image_path=figure, + text=text, + ) + text_with_images += f"\n\n{figure.name}:\n{text_image}" + + relationships = TextGenerationAPI(model="$llm_model", api_key=litellm_api_key).find_relationships_in_text(text=text_with_images, chemicals=chemicals, effects=effects) + #end if + +#if $identification_method.type_choice == "zero_shot_classification": +import sys + +if len(text) > 3000: + raise ValueError("Input text is very long. Consider using the LLM-based method for better performance.") + +os.environ.setdefault("TORCHINDUCTOR_CACHE_DIR", os.path.join(os.getcwd(), ".torchinductor_cache")) +os.makedirs(os.environ["TORCHINDUCTOR_CACHE_DIR"], exist_ok=True) + +from aoptk.spacy_text_processor import SpacyText +from aoptk.relationships.zero_shot_classification_single import ZeroShotClassificationSingle +from aoptk.literature.pdf import PDF +from aoptk.literature.pymupdf_parser import PymupdfParser + +relationships = ZeroShotClassificationSingle().find_relationships(text=text, chemicals=chemicals, effects=effects) +#end if + +with open(f"relationships_{publication_id}.tsv", "w") as f_out: + f_out.write("id\tchemical\teffect\trelationship\n") + for relationship in relationships: + f_out.write(f"{publication_id}\t{relationship.chemical}\t{relationship.effect}\t{relationship.relationship_type}\n") + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tools/aoptk/aoptk_find_relationships_non_llm.xml b/tools/aoptk/aoptk_find_relationships_non_llm.xml deleted file mode 100644 index 12f8b20d..00000000 --- a/tools/aoptk/aoptk_find_relationships_non_llm.xml +++ /dev/null @@ -1,84 +0,0 @@ - - Detect relationships between chemicals and effects in text. - - macros.xml - - - - - - - - - - -import os -import sys - -os.environ.setdefault("TORCHINDUCTOR_CACHE_DIR", os.path.join(os.getcwd(), ".torchinductor_cache")) -os.makedirs(os.environ["TORCHINDUCTOR_CACHE_DIR"], exist_ok=True) - -from aoptk.spacy_text_processor import SpacyText -from aoptk.relationships.zero_shot_classification_single import ZeroShotClassificationSingle -from aoptk.literature.pdf import PDF -from aoptk.literature.pymupdf_parser import PymupdfParser - -publication_id = os.path.splitext("$input_file.element_identifier")[0] -effects = ["${effect}"] -email = os.environ.get("EMAIL") - -if "${input_file.ext}" == "pdf": - text = PymupdfParser([PDF("$input_file")]).get_publications()[0].full_text - chemicals = SpacyText().find_chemical(text) - relationships = ZeroShotClassificationSingle().find_relationships( - text=text, chemicals=chemicals, effects=effects -) - -elif "${input_file.ext}" == "txt": - with open("$input_file", "r") as f_in: - text = f_in.read() - chemicals = SpacyText().find_chemical(text) - relationships = ZeroShotClassificationSingle().find_relationships( - text=text, chemicals=chemicals, effects=effects - ) - -else: - raise ValueError(f"Unsupported input file format: ${input_file.ext}") - -with open("relationships.tsv", "w") as f_out: - f_out.write("id\tchemical\teffect\trelationship\n") - for relationship in relationships: - f_out.write(f"{publication_id}\t{relationship.chemical}\t{relationship.effect}\t{relationship.relationship_type}\n") - - - - - - - [0-9a-zA-Z]+ - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/tools/aoptk/aoptk_find_relationships_text.xml b/tools/aoptk/aoptk_find_relationships_text.xml deleted file mode 100644 index f1dd8b98..00000000 --- a/tools/aoptk/aoptk_find_relationships_text.xml +++ /dev/null @@ -1,86 +0,0 @@ - - Detect relationships between chemicals and effects in text using LLMs. - - macros.xml - - - - - - - - - - - -import os -from aoptk.text_generation_api import TextGenerationAPI -from aoptk.relationship_type import Causative -from aoptk.relationship_type import Inhibitive -from aoptk.relationship_type import RelationshipType -from aoptk.relationships.relationship import Relationship -from aoptk.effect import Effect - -publication_id = os.path.splitext("$input_file.element_identifier")[0] -effects = ["${effect}"] -openai_key = os.environ.get("OPENAI_KEY") -text_generation_api = TextGenerationAPI(model="$llm_model", api_key=openai_key) -text_generation_api.specification_relationship_text_prompt="$prompt_specification" - -if "${input_file.ext}" == "pdf": - text = PymupdfParser([PDF("$input_file")]).inject_text_generation(TextGenerationAPI(model="redhatai-scout")).get_publications()[0].full_text - chemicals = text_generation_api.find_chemicals(text) - relationships = text_generation_api.find_relationships_in_text( - text=text, chemicals=chemicals, effects=effects -) - -elif "${input_file.ext}" == "txt": - with open("$input_file", "r") as f_in: - text = f_in.read() - chemicals = text_generation_api.find_chemicals(text) - relationships = text_generation_api.find_relationships_in_text( - text=text, chemicals=chemicals, effects=effects - ) - -else: - raise ValueError(f"Unsupported input file format: ${input_file.ext}") - -with open("relationships.tsv", "w") as f_out: - f_out.write("id\tchemical\teffect\trelationship\n") - for relationship in relationships: - f_out.write(f"{publication_id}\t{relationship.chemical}\t{relationship.effect}\t{relationship.relationship_type}\n") - - - - - - - - - [0-9a-zA-Z]+ - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/tools/aoptk/aoptk_find_relationships_text_images.xml b/tools/aoptk/aoptk_find_relationships_text_images.xml deleted file mode 100644 index 08db3acf..00000000 --- a/tools/aoptk/aoptk_find_relationships_text_images.xml +++ /dev/null @@ -1,105 +0,0 @@ - - Detect relationships between chemicals and effects in text and imagesusing LLMs. - - macros.xml - - - - - - - - - - - -import os -from aoptk.text_generation_api import TextGenerationAPI -from aoptk.relationship_type import Causative -from aoptk.relationship_type import Inhibitive -from aoptk.relationship_type import RelationshipType -from aoptk.relationships.relationship import Relationship -from aoptk.effect import Effect - -publication_id = os.path.splitext("$input_file.element_identifier")[0] -effects = ["${effect}"] -openai_key = os.environ.get("OPENAI_KEY") -text_generation_api = TextGenerationAPI(model="$llm_model", api_key=openai_key) -text_generation_api.specification_relationship_text_prompt="$prompt_specification" - -if "${input_file.ext}" == "pdf": - text = "" - publication = PymupdfParser([PDF("$input_file")]).inject_text_generation(TextGenerationAPI(model="$llm_models_images")).get_publications()[0] - text += publication.full_text - - for image in publication.images: - text_image = TextGenerationAPI(model="$llm_models_images").convert_image( - image_path=image.path, - text=publication.full_text, - ) - text += f"\n\n{image.name}:\n{text_image}" - - chemicals = text_generation_api.find_chemicals(text) - relationships = text_generation_api.find_relationships_in_text( - text=text, chemicals=chemicals, effects=effects -) - -elif "${input_file.ext}" == "txt": - with open("$input_file", "r") as f_in: - text = "" - text += f_in.read() - text += "\n\n\n\nALSO ANALYZE THESE FIGURES CONVERTED TO TEXT:" - for image in publication_images_folder.iterdir(): - text_image = TextGenerationAPI(model="$llm_models_images").convert_image( - image_path=image, - text=matching_pub_data["publication.full_text"], - ) - text += f"\n\n{image.name}:\n{text_image}" - - chemicals = text_generation_api.find_chemicals(text) - relationships = text_generation_api.find_relationships_in_text( - text=text, chemicals=chemicals, effects=effects - ) -else: - raise ValueError(f"Unsupported input file format: ${input_file.ext}") - -with open("relationships.tsv", "w") as f_out: - f_out.write("id\tchemical\teffect\trelationship\n") - for relationship in relationships: - f_out.write(f"{publication_id}\t{relationship.chemical}\t{relationship.effect}\t{relationship.relationship_type}\n") - - - - - - - - - - [0-9a-zA-Z]+ - - - - - - - - - - - - - - - - - - \ No newline at end of file From 811ee9937fb630cd36c57c54594a17a69e11a6b2 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Thu, 23 Apr 2026 13:42:29 +0200 Subject: [PATCH 097/173] find chemical should store the id of the publication --- tools/aoptk/aoptk_find_chemicals.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tools/aoptk/aoptk_find_chemicals.xml b/tools/aoptk/aoptk_find_chemicals.xml index 69ea41f3..2352e200 100644 --- a/tools/aoptk/aoptk_find_chemicals.xml +++ b/tools/aoptk/aoptk_find_chemicals.xml @@ -50,12 +50,12 @@ from aoptk.spacy_text_processor import SpacyText chemicals = SpacyText().find_chemical(text) #end if -with open("chemicals.tsv", "w") as f_out: - f_out.write("id\tchemical\n") +with open(f"chemicals_{publication_id}.tsv", "w") as f_out: + f_out.write("chemical\n") for chemical in chemicals: - f_out.write(f"{publication_id}\t{chemical.name}\n") + f_out.write(f"{chemical.name}\n") - +s From 6ca1750eda0f2ce0ff9bd2e18bdc561623247ee9 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Thu, 23 Apr 2026 13:42:36 +0200 Subject: [PATCH 098/173] added effect as macro --- tools/aoptk/macros.xml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tools/aoptk/macros.xml b/tools/aoptk/macros.xml index dfb6ee8c..7ebb742c 100644 --- a/tools/aoptk/macros.xml +++ b/tools/aoptk/macros.xml @@ -196,7 +196,11 @@ [0-9a-zA-Z_ ()*'"[\]\[\]\-:.\[\]α-ωΑ-Ω]+ - + + + [0-9a-zA-Z]+ + + From 910497b6b31a5c9c5a07a45b81e5347ad3159732 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Thu, 23 Apr 2026 13:45:57 +0200 Subject: [PATCH 099/173] reorder of inputs --- tools/aoptk/aoptk_find_relationships.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/aoptk/aoptk_find_relationships.xml b/tools/aoptk/aoptk_find_relationships.xml index 147e27bd..b2a3a455 100644 --- a/tools/aoptk/aoptk_find_relationships.xml +++ b/tools/aoptk/aoptk_find_relationships.xml @@ -94,17 +94,17 @@ with open(f"relationships_{publication_id}.tsv", "w") as f_out: + - + - From fa06fcc7b4245c93c6e33627e74f1b078aec4f38 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Thu, 23 Apr 2026 14:30:06 +0200 Subject: [PATCH 100/173] added relationship type and macros modification Co-authored-by: Copilot --- tools/aoptk/aoptk_find_relationships.xml | 22 ++++++++++--- tools/aoptk/macros.xml | 41 ++++++++++++------------ 2 files changed, 38 insertions(+), 25 deletions(-) diff --git a/tools/aoptk/aoptk_find_relationships.xml b/tools/aoptk/aoptk_find_relationships.xml index b2a3a455..08083e62 100644 --- a/tools/aoptk/aoptk_find_relationships.xml +++ b/tools/aoptk/aoptk_find_relationships.xml @@ -14,6 +14,8 @@ +from aoptk.relationship_type import Causative +from aoptk.relationship_type import Inhibitive if "${input_file.ext}" == "pdf": text = PymupdfParser([PDF("$input_file")]).inject_text_generation(TextGenerationAPI(model="redhatai-scout")).get_publications()[0].full_text @@ -29,6 +31,13 @@ chemicals = pd.read_csv("$chemicals", sep="\t")["chemical"].tolist() publication_id = os.path.splitext("$input_file.element_identifier")[0] effects = ["${effect}"] +if "$relationship_type" == "causative": + relationship_type = Causative +elif "$relationship_type" == "inhibitive": + relationship_type = Inhibitive +else: + raise ValueError(f"Unsupported relationship type: ${relationship_type}") + #if $identification_method.type_choice == "llm_text" or if $identification_method.type_choice == "llm_text_images": from aoptk.text_generation_api import TextGenerationAPI @@ -41,7 +50,7 @@ litellm_api_key = source.get("LITELLM_API_KEY") #end if #if $identification_method.type_choice == "llm_text" - relationships = TextGenerationAPI(model="$llm_model", api_key=litellm_api_key).find_relationships_in_text(text=text, chemicals=chemicals, effects=effects) + relationships = TextGenerationAPI(model="$llm_model", api_key=litellm_api_key).find_relationships_in_text(text=text, chemicals=chemicals, effects=effects, relationship_type=relationship_type) #end if @@ -55,7 +64,7 @@ litellm_api_key = source.get("LITELLM_API_KEY") ) text_with_images += f"\n\n{figure.name}:\n{text_image}" - relationships = TextGenerationAPI(model="$llm_model", api_key=litellm_api_key).find_relationships_in_text(text=text_with_images, chemicals=chemicals, effects=effects) + relationships = TextGenerationAPI(model="$llm_model", api_key=litellm_api_key).find_relationships_in_text(text=text_with_images, chemicals=chemicals, effects=effects, relationship_type=relationship_type) #end if #if $identification_method.type_choice == "zero_shot_classification": @@ -72,7 +81,7 @@ from aoptk.relationships.zero_shot_classification_single import ZeroShotClassifi from aoptk.literature.pdf import PDF from aoptk.literature.pymupdf_parser import PymupdfParser -relationships = ZeroShotClassificationSingle().find_relationships(text=text, chemicals=chemicals, effects=effects) +relationships = ZeroShotClassificationSingle().find_relationships(text=text, chemicals=chemicals, effects=effects, relationship_type=relationship_type)) #end if with open(f"relationships_{publication_id}.tsv", "w") as f_out: @@ -93,22 +102,25 @@ with open(f"relationships_{publication_id}.tsv", "w") as f_out: - + + - + + + diff --git a/tools/aoptk/macros.xml b/tools/aoptk/macros.xml index 7ebb742c..08543bc1 100644 --- a/tools/aoptk/macros.xml +++ b/tools/aoptk/macros.xml @@ -15,15 +15,6 @@ - - - - - - @@ -40,7 +31,6 @@ - @@ -65,7 +55,6 @@ - @@ -102,6 +91,7 @@ + @@ -109,30 +99,35 @@ + + - + + + + - + + + - + - - @@ -141,7 +136,6 @@ - @@ -166,7 +160,6 @@ - @@ -196,11 +189,19 @@ [0-9a-zA-Z_ ()*'"[\]\[\]\-:.\[\]α-ωΑ-Ω]+ + - + [0-9a-zA-Z]+ - + + + + + + + + From f67f70433974d127efc2b67e1e8ce21fc5bfe489 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Thu, 23 Apr 2026 15:04:29 +0200 Subject: [PATCH 101/173] hardcoded model for scans of pdfs --- tools/aoptk/aoptk_find_chemicals.xml | 2 +- tools/aoptk/aoptk_find_relationships.xml | 2 +- tools/aoptk/aoptk_parse_pdf.xml | 14 ++++++++------ 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/tools/aoptk/aoptk_find_chemicals.xml b/tools/aoptk/aoptk_find_chemicals.xml index 2352e200..647fc801 100644 --- a/tools/aoptk/aoptk_find_chemicals.xml +++ b/tools/aoptk/aoptk_find_chemicals.xml @@ -22,7 +22,7 @@ from aoptk.literature.pymupdf_parser import PymupdfParser publication_id = os.path.splitext("$input_file.element_identifier")[0] if "${input_file.ext}" == "pdf": - text = PymupdfParser([PDF("$input_file")]).get_publications()[0].full_text + text = PymupdfParser([PDF("$input_file")]).inject_text_generation(TextGenerationAPI(model="qwen3.5")).get_publications()[0].full_text elif "${input_file.ext}" == "txt": with open("$input_file", "r") as f_in: text = f_in.read() diff --git a/tools/aoptk/aoptk_find_relationships.xml b/tools/aoptk/aoptk_find_relationships.xml index 08083e62..9556f0ad 100644 --- a/tools/aoptk/aoptk_find_relationships.xml +++ b/tools/aoptk/aoptk_find_relationships.xml @@ -18,7 +18,7 @@ from aoptk.relationship_type import Causative from aoptk.relationship_type import Inhibitive if "${input_file.ext}" == "pdf": - text = PymupdfParser([PDF("$input_file")]).inject_text_generation(TextGenerationAPI(model="redhatai-scout")).get_publications()[0].full_text + text = PymupdfParser([PDF("$input_file")]).inject_text_generation(TextGenerationAPI(model="qwen3.5")).get_publications()[0].full_text elif "${input_file.ext}" == "txt": with open("$input_file", "r") as f_in: diff --git a/tools/aoptk/aoptk_parse_pdf.xml b/tools/aoptk/aoptk_parse_pdf.xml index a61f7cf3..395e0600 100644 --- a/tools/aoptk/aoptk_parse_pdf.xml +++ b/tools/aoptk/aoptk_parse_pdf.xml @@ -20,14 +20,16 @@ from aoptk.literature.pymupdf_parser import PymupdfParser from aoptk.text_generation_api import TextGenerationAPI import os +litellm_config_file = os.environ.get("LITELLM_CONFIG_FILE") +if not litellm_config_file: + sys.exit("LITELLM_CONFIG_FILE environment variable is not set.") +with open(litellm_config_file, "r") as f: + config = yaml.safe_load(f) +litellm_api_key = source.get("LITELLM_API_KEY") + publication_id = os.path.splitext("$input_file.element_identifier")[0] if "${input_file.ext}" == "pdf": - parser = PymupdfParser([PDF("$input_file")]) - if os.environ.get("OPENAI_KEY"): - parser = parser.inject_text_generation(TextGenerationAPI(model="$llm_models_images", api_key=os.environ.get("OPENAI_KEY"))) - - pdf = parser.get_publications() - text = pdf[0].full_text + text = PymupdfParser([PDF("$input_file")]).inject_text_generation(TextGenerationAPI(model="qwen3.5")).get_publications()[0].full_text with open(f"{publication_id}.txt", "w") as f: f.write(text) From 222e33f5aaf632813128b113805dcff32a97429e Mon Sep 17 00:00:00 2001 From: rdurnik Date: Thu, 23 Apr 2026 15:28:18 +0200 Subject: [PATCH 102/173] added optional ncbi api key Co-authored-by: Copilot --- tools/aoptk/aoptk_download_publication_data.xml | 6 +++++- tools/aoptk/aoptk_query_literature.xml | 7 +++++-- tools/aoptk/macros.xml | 13 ++++++++++++- 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/tools/aoptk/aoptk_download_publication_data.xml b/tools/aoptk/aoptk_download_publication_data.xml index bbb66167..f5b39e70 100644 --- a/tools/aoptk/aoptk_download_publication_data.xml +++ b/tools/aoptk/aoptk_download_publication_data.xml @@ -19,6 +19,8 @@ import os with open("$input_file", "r") as f: ids = [line.strip() for line in f.readlines()] email = os.environ.get("EMAIL") +api_key = os.environ.get("NCBI_API_KEY") + #if $data_type.type_choice == "publications": from aoptk.literature.databases.pmc import PMC @@ -27,6 +29,7 @@ from Bio import Entrez if "${literature_database_europepmc_pmc}" == "pmc": Entrez.email = email + Entrez.api_key = api_key pmc = PMC("", storage = "./", figure_storage="./figures") pmc.id_list = ids publications = pmc.get_publications() @@ -48,7 +51,8 @@ from aoptk.literature.databases.europepmc import EuropePMC from Bio import Entrez if "${literature_database_pubmed_europepmc}" == "pubmed": - Entrez.email = email + Entrez.email = email + Entrez.api_key = api_key pubmed = PubMed.__new__(PubMed) pubmed.id_list = ids abstracts = pubmed.get_abstracts() diff --git a/tools/aoptk/aoptk_query_literature.xml b/tools/aoptk/aoptk_query_literature.xml index 095cf849..99c767d4 100644 --- a/tools/aoptk/aoptk_query_literature.xml +++ b/tools/aoptk/aoptk_query_literature.xml @@ -7,6 +7,7 @@ + + + + + + + @@ -195,7 +206,7 @@ [0-9a-zA-Z]+ - + From 8aa9ef99f0211cae8b668b2efa6f7fa6fa229b0b Mon Sep 17 00:00:00 2001 From: rdurnik Date: Thu, 23 Apr 2026 17:09:45 +0200 Subject: [PATCH 103/173] corrected collection output --- .../aoptk/aoptk_download_publication_data.xml | 38 ++++++++----------- 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/tools/aoptk/aoptk_download_publication_data.xml b/tools/aoptk/aoptk_download_publication_data.xml index f5b39e70..80f35355 100644 --- a/tools/aoptk/aoptk_download_publication_data.xml +++ b/tools/aoptk/aoptk_download_publication_data.xml @@ -82,7 +82,7 @@ pdfs = pmc.get_pdfs() - + @@ -102,24 +102,18 @@ pdfs = pmc.get_pdfs() - - - - - - - - - - - - - - - - - - + + data_type['type_choice'] == 'publications' or data_type['type_choice'] == 'abstracts' + + + + data_type['type_choice'] == 'publications' + + + + data_type['type_choice'] == 'pdfs' + + @@ -127,17 +121,17 @@ pdfs = pmc.get_pdfs() - + - + - + From 5c2460f259fca33b583a5d23e70f665d96a45ff9 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Thu, 23 Apr 2026 17:09:59 +0200 Subject: [PATCH 104/173] prompt optional Co-authored-by: Copilot --- tools/aoptk/macros.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/aoptk/macros.xml b/tools/aoptk/macros.xml index d6191c3e..47cd512d 100644 --- a/tools/aoptk/macros.xml +++ b/tools/aoptk/macros.xml @@ -92,7 +92,7 @@ - [0-9a-zA-Z_ ()*'"[\]\[\]\-:.\[\]α-ωΑ-Ω]+ + [0-9a-zA-Z_ ()*'"[\]\[\]\-:.\[\]α-ωΑ-Ω]* @@ -134,7 +134,7 @@ - + @@ -197,7 +197,7 @@ - [0-9a-zA-Z_ ()*'"[\]\[\]\-:.\[\]α-ωΑ-Ω]+ + [0-9a-zA-Z_ ()*'"[\]\[\]\-:.\[\]α-ωΑ-Ω]* From 3450f03ba73d5da4b6e27a3932beacb34011e648 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Fri, 24 Apr 2026 12:36:24 +0200 Subject: [PATCH 105/173] config fix --- .../aoptk/aoptk_download_publication_data.xml | 1 + tools/aoptk/aoptk_finalize_output.xml | 4 +- tools/aoptk/aoptk_find_chemicals.xml | 25 +++--- tools/aoptk/aoptk_find_relationships.xml | 77 +++++++++++-------- tools/aoptk/aoptk_normalize_chemicals.xml | 19 +++-- tools/aoptk/aoptk_parse_pdf.xml | 9 ++- 6 files changed, 80 insertions(+), 55 deletions(-) diff --git a/tools/aoptk/aoptk_download_publication_data.xml b/tools/aoptk/aoptk_download_publication_data.xml index 80f35355..ec7ef000 100644 --- a/tools/aoptk/aoptk_download_publication_data.xml +++ b/tools/aoptk/aoptk_download_publication_data.xml @@ -47,6 +47,7 @@ for publication in publications: #if $data_type.type_choice == "abstracts": +from aoptk.literature.databases.pubmed import PubMed from aoptk.literature.databases.europepmc import EuropePMC from Bio import Entrez diff --git a/tools/aoptk/aoptk_finalize_output.xml b/tools/aoptk/aoptk_finalize_output.xml index 69acc01f..bc78cc90 100644 --- a/tools/aoptk/aoptk_finalize_output.xml +++ b/tools/aoptk/aoptk_finalize_output.xml @@ -20,7 +20,7 @@ def join_unique(series): values = series.dropna().astype(str).str.strip() return ";".join(pd.unique(values)) -output = pd.read_csv("$input_file", sep="\t")\ +output = pd.read_csv("$input_file", sep="\t") file_name = output["effect"].unique()[0].strip().lower().replace(" ", "_") @@ -37,7 +37,7 @@ for relationship in relationships: result.rename(columns={"hits": f"{relationship}", "ids": f"{relationship} IDs"}, inplace=True) final_output = final_output.merge(result, on="heading", how="left") -final_output.to_excel(f"./{file_name}.xlsx", index=False) +final_output.to_excel("./final_output.xlsx", index=False) diff --git a/tools/aoptk/aoptk_find_chemicals.xml b/tools/aoptk/aoptk_find_chemicals.xml index 647fc801..746ee126 100644 --- a/tools/aoptk/aoptk_find_chemicals.xml +++ b/tools/aoptk/aoptk_find_chemicals.xml @@ -15,9 +15,13 @@ import os +import sys + import pandas as pd +import yaml from aoptk.literature.pdf import PDF from aoptk.literature.pymupdf_parser import PymupdfParser +from aoptk.text_generation_api import TextGenerationAPI publication_id = os.path.splitext("$input_file.element_identifier")[0] @@ -27,35 +31,34 @@ elif "${input_file.ext}" == "txt": with open("$input_file", "r") as f_in: text = f_in.read() else: - raise ValueError(f"Unsupported input file format: {input_ext}") + raise ValueError(f"Unsupported input file format: ${input_file.ext}") -#if $identification_method.type_choice == "llm": -from aoptk.text_generation_api import TextGenerationAPI +#if $identification_method.method == "llm": litellm_config_file = os.environ.get("LITELLM_CONFIG_FILE") if not litellm_config_file: sys.exit("LITELLM_CONFIG_FILE environment variable is not set.") with open(litellm_config_file, "r") as f: config = yaml.safe_load(f) -litellm_api_key = source.get("LITELLM_API_KEY") +litellm_api_key = os.environ.get("LITELLM_API_KEY") chemicals = TextGenerationAPI(model="$llm_model", api_key=litellm_api_key).find_chemicals(text) #end if -#else if $identification_method.type_choice == "scispacy": +#else if $identification_method.method == "scispacy": from aoptk.spacy_text_processor import SpacyText chemicals = SpacyText().find_chemical(text) #end if -with open(f"chemicals_{publication_id}.tsv", "w") as f_out: +with open("chemicals.tsv", "w") as f_out: f_out.write("chemical\n") for chemical in chemicals: f_out.write(f"{chemical.name}\n") -s + @@ -85,15 +88,15 @@ with open(f"chemicals_{publication_id}.tsv", "w") as f_out: - - + + - - + + diff --git a/tools/aoptk/aoptk_find_relationships.xml b/tools/aoptk/aoptk_find_relationships.xml index 9556f0ad..9ffce18f 100644 --- a/tools/aoptk/aoptk_find_relationships.xml +++ b/tools/aoptk/aoptk_find_relationships.xml @@ -14,6 +14,16 @@ +import os +import sys +from pathlib import Path + +import pandas as pd +import yaml + +from aoptk.literature.pdf import PDF +from aoptk.literature.pymupdf_parser import PymupdfParser +from aoptk.text_generation_api import TextGenerationAPI from aoptk.relationship_type import Causative from aoptk.relationship_type import Inhibitive @@ -27,7 +37,6 @@ elif "${input_file.ext}" == "txt": else: raise ValueError(f"Unsupported input file format: ${input_file.ext}") -chemicals = pd.read_csv("$chemicals", sep="\t")["chemical"].tolist() publication_id = os.path.splitext("$input_file.element_identifier")[0] effects = ["${effect}"] @@ -38,37 +47,42 @@ elif "$relationship_type" == "inhibitive": else: raise ValueError(f"Unsupported relationship type: ${relationship_type}") -#if $identification_method.type_choice == "llm_text" or if $identification_method.type_choice == "llm_text_images": -from aoptk.text_generation_api import TextGenerationAPI +#if $identification_method.method == "llm_text" or $identification_method.method == "llm_text_images" +chemicals = pd.read_csv("$chemicals", sep="\t")["chemical"].tolist() litellm_config_file = os.environ.get("LITELLM_CONFIG_FILE") if not litellm_config_file: sys.exit("LITELLM_CONFIG_FILE environment variable is not set.") with open(litellm_config_file, "r") as f: config = yaml.safe_load(f) -litellm_api_key = source.get("LITELLM_API_KEY") +litellm_api_key = os.environ.get("LITELLM_API_KEY") +if not litellm_api_key and isinstance(config, dict): + env_vars = config.get("environment_variables", {}) + litellm_api_key = env_vars.get("LITELLM_API_KEY") +if not litellm_api_key: + sys.exit("LITELLM_API_KEY is not set. Configure it in Galaxy secrets/environment.") #end if - #if $identification_method.type_choice == "llm_text" - relationships = TextGenerationAPI(model="$llm_model", api_key=litellm_api_key).find_relationships_in_text(text=text, chemicals=chemicals, effects=effects, relationship_type=relationship_type) - #end if +#if $identification_method.method == "llm_text" +relationships = TextGenerationAPI(model="$llm_models", api_key=litellm_api_key).find_relationships_in_text(text=text, chemicals=chemicals, effects=effects, relationship_type=relationship_type) +#end if - #if $identification_method.type_choice == "llm_text_images": - text_with_images = text - text_with_images += "\n\n\n\nALSO ANALYZE THESE FIGURES CONVERTED TO TEXT:" - for figure in "$input_images".iterdir(): - text_image = TextGenerationAPI(model="$llm_models_images").convert_image( - image_path=figure, - text=text, - ) - text_with_images += f"\n\n{figure.name}:\n{text_image}" +#if $identification_method.method == "llm_text_images" +text_with_images = text +text_with_images += "\n\n\n\nALSO ANALYZE THESE FIGURES CONVERTED TO TEXT:" +for figure in Path("$input_images").iterdir(): + text_image = TextGenerationAPI(model="$llm_models_images", api_key=litellm_api_key).convert_image( + image_path=figure, + text=text, + ) + text_with_images += f"\n\n{figure.name}:\n{text_image}" - relationships = TextGenerationAPI(model="$llm_model", api_key=litellm_api_key).find_relationships_in_text(text=text_with_images, chemicals=chemicals, effects=effects, relationship_type=relationship_type) - #end if +relationships = TextGenerationAPI(model="$llm_models", api_key=litellm_api_key).find_relationships_in_text(text=text_with_images, chemicals=chemicals, effects=effects, relationship_type=relationship_type) +#end if -#if $identification_method.type_choice == "zero_shot_classification": -import sys +#if $identification_method.method == "zero_shot_classification" +chemicals = pd.read_csv("$chemicals", sep="\t")["chemical"].tolist() if len(text) > 3000: raise ValueError("Input text is very long. Consider using the LLM-based method for better performance.") @@ -78,13 +92,11 @@ os.makedirs(os.environ["TORCHINDUCTOR_CACHE_DIR"], exist_ok=True) from aoptk.spacy_text_processor import SpacyText from aoptk.relationships.zero_shot_classification_single import ZeroShotClassificationSingle -from aoptk.literature.pdf import PDF -from aoptk.literature.pymupdf_parser import PymupdfParser -relationships = ZeroShotClassificationSingle().find_relationships(text=text, chemicals=chemicals, effects=effects, relationship_type=relationship_type)) +relationships = ZeroShotClassificationSingle().find_relationships(text=text, chemicals=chemicals, effects=effects, relationship_type=relationship_type) #end if -with open(f"relationships_{publication_id}.tsv", "w") as f_out: +with open("relationships.tsv", "w") as f_out: f_out.write("id\tchemical\teffect\trelationship\n") for relationship in relationships: f_out.write(f"{publication_id}\t{relationship.chemical}\t{relationship.effect}\t{relationship.relationship_type}\n") @@ -115,10 +127,12 @@ with open(f"relationships_{publication_id}.tsv", "w") as f_out: + + @@ -132,24 +146,27 @@ with open(f"relationships_{publication_id}.tsv", "w") as f_out: - - + + + - - + + + - - + + + diff --git a/tools/aoptk/aoptk_normalize_chemicals.xml b/tools/aoptk/aoptk_normalize_chemicals.xml index 3463f285..e4bdcda4 100644 --- a/tools/aoptk/aoptk_normalize_chemicals.xml +++ b/tools/aoptk/aoptk_normalize_chemicals.xml @@ -15,12 +15,15 @@ import os +import sys + import pandas as pd +import yaml from aoptk.chemical import Chemical chemicals = pd.read_csv("$input_file", sep="\t") -#if $normalization_method.type_choice == "llm": +#if $normalization_method.method == "llm": from aoptk.text_generation_api import TextGenerationAPI litellm_config_file = os.environ.get("LITELLM_CONFIG_FILE") @@ -28,7 +31,7 @@ if not litellm_config_file: sys.exit("LITELLM_CONFIG_FILE environment variable is not set.") with open(litellm_config_file, "r") as f: config = yaml.safe_load(f) -litellm_api_key = source.get("LITELLM_API_KEY") +litellm_api_key = os.environ.get("LITELLM_API_KEY") chemical_list = pd.read_csv("$chemical_list", sep="\t")["chemical"].tolist() @@ -37,7 +40,7 @@ chemicals["chemical"] = chemicals["chemical"].apply( ) #end if -#else if $normalization_method.type_choice == "pubchem": +#else if $normalization_method.method == "pubchem": from aoptk.normalization.pubchem_api import PubChemAPI chemicals["chemical"] = chemicals["chemical"].apply( @@ -45,11 +48,11 @@ chemicals["chemical"] = chemicals["chemical"].apply( ) #end if -#else if $normalization_method.type_choice == "mesh": +#else if $normalization_method.method == "mesh": from aoptk.normalization.mesh_terms import MeshTerms from aoptk.normalization.provide_mesh_term_dataframe_from_xml import ProvideMeshTermDataframeFromXML -mesh_terms_xml = pd.read_csv("$input_xml", sep="\t") +mesh_terms_xml = pd.read_xml("$input_xml") mesh_terms_df = ProvideMeshTermDataframeFromXML(mesh_terms_xml) chemicals["chemical"] = chemicals["chemical"].apply( lambda x: MeshTerms(mesh_terms=mesh_terms_df).normalize_chemical(Chemical(x)) @@ -95,7 +98,7 @@ chemicals.to_csv("normalized_chemicals.tsv", sep="\t", index=False) - + @@ -103,14 +106,14 @@ chemicals.to_csv("normalized_chemicals.tsv", sep="\t", index=False) - + - + diff --git a/tools/aoptk/aoptk_parse_pdf.xml b/tools/aoptk/aoptk_parse_pdf.xml index 395e0600..90b3bfe1 100644 --- a/tools/aoptk/aoptk_parse_pdf.xml +++ b/tools/aoptk/aoptk_parse_pdf.xml @@ -6,7 +6,6 @@ - +import os +import sys + +import yaml from aoptk.literature.pdf import PDF from aoptk.literature.pymupdf_parser import PymupdfParser from aoptk.text_generation_api import TextGenerationAPI -import os litellm_config_file = os.environ.get("LITELLM_CONFIG_FILE") if not litellm_config_file: sys.exit("LITELLM_CONFIG_FILE environment variable is not set.") with open(litellm_config_file, "r") as f: config = yaml.safe_load(f) -litellm_api_key = source.get("LITELLM_API_KEY") +litellm_api_key = os.environ.get("LITELLM_API_KEY") publication_id = os.path.splitext("$input_file.element_identifier")[0] if "${input_file.ext}" == "pdf": @@ -40,7 +42,6 @@ else: - From a46aa1e11a48b39b5b9a3a00dba12f53fa0c8fa3 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Fri, 24 Apr 2026 18:32:46 +0200 Subject: [PATCH 106/173] config Co-authored-by: Copilot --- tools/aoptk/aoptk_download_publication_data.xml | 14 ++++---------- tools/aoptk/aoptk_find_chemicals.xml | 9 ++++----- tools/aoptk/aoptk_find_relationships.xml | 5 ++--- tools/aoptk/aoptk_normalize_chemicals.xml | 17 +++++++---------- 4 files changed, 17 insertions(+), 28 deletions(-) diff --git a/tools/aoptk/aoptk_download_publication_data.xml b/tools/aoptk/aoptk_download_publication_data.xml index ec7ef000..ccfc2abb 100644 --- a/tools/aoptk/aoptk_download_publication_data.xml +++ b/tools/aoptk/aoptk_download_publication_data.xml @@ -15,6 +15,10 @@ import os +from aoptk.literature.databases.pmc import PMC +from aoptk.literature.databases.europepmc import EuropePMC +from aoptk.literature.databases.pubmed import PubMed +from Bio import Entrez with open("$input_file", "r") as f: ids = [line.strip() for line in f.readlines()] @@ -23,10 +27,6 @@ api_key = os.environ.get("NCBI_API_KEY") #if $data_type.type_choice == "publications": -from aoptk.literature.databases.pmc import PMC -from aoptk.literature.databases.europepmc import EuropePMC -from Bio import Entrez - if "${literature_database_europepmc_pmc}" == "pmc": Entrez.email = email Entrez.api_key = api_key @@ -47,10 +47,6 @@ for publication in publications: #if $data_type.type_choice == "abstracts": -from aoptk.literature.databases.pubmed import PubMed -from aoptk.literature.databases.europepmc import EuropePMC -from Bio import Entrez - if "${literature_database_pubmed_europepmc}" == "pubmed": Entrez.email = email Entrez.api_key = api_key @@ -71,8 +67,6 @@ for abstract in abstracts: #if $data_type.type_choice == "pdfs": -from aoptk.literature.databases.pmc import PMC - pmc = PMC("", storage = "./") pmc.id_list = ids pdfs = pmc.get_pdfs() diff --git a/tools/aoptk/aoptk_find_chemicals.xml b/tools/aoptk/aoptk_find_chemicals.xml index 746ee126..72413dff 100644 --- a/tools/aoptk/aoptk_find_chemicals.xml +++ b/tools/aoptk/aoptk_find_chemicals.xml @@ -22,6 +22,7 @@ import yaml from aoptk.literature.pdf import PDF from aoptk.literature.pymupdf_parser import PymupdfParser from aoptk.text_generation_api import TextGenerationAPI +from aoptk.spacy_text_processor import SpacyText publication_id = os.path.splitext("$input_file.element_identifier")[0] @@ -43,13 +44,11 @@ with open(litellm_config_file, "r") as f: config = yaml.safe_load(f) litellm_api_key = os.environ.get("LITELLM_API_KEY") -chemicals = TextGenerationAPI(model="$llm_model", api_key=litellm_api_key).find_chemicals(text) +chemicals = TextGenerationAPI(model="$llm_models", api_key=litellm_api_key).find_chemicals(text) #end if -#else if $identification_method.method == "scispacy": -from aoptk.spacy_text_processor import SpacyText - +#elif $identification_method.method == "scispacy": chemicals = SpacyText().find_chemical(text) #end if @@ -104,7 +103,7 @@ with open("chemicals.tsv", "w") as f_out: 3000: os.environ.setdefault("TORCHINDUCTOR_CACHE_DIR", os.path.join(os.getcwd(), ".torchinductor_cache")) os.makedirs(os.environ["TORCHINDUCTOR_CACHE_DIR"], exist_ok=True) -from aoptk.spacy_text_processor import SpacyText -from aoptk.relationships.zero_shot_classification_single import ZeroShotClassificationSingle - relationships = ZeroShotClassificationSingle().find_relationships(text=text, chemicals=chemicals, effects=effects, relationship_type=relationship_type) #end if diff --git a/tools/aoptk/aoptk_normalize_chemicals.xml b/tools/aoptk/aoptk_normalize_chemicals.xml index e4bdcda4..975d4887 100644 --- a/tools/aoptk/aoptk_normalize_chemicals.xml +++ b/tools/aoptk/aoptk_normalize_chemicals.xml @@ -20,12 +20,14 @@ import sys import pandas as pd import yaml from aoptk.chemical import Chemical +from aoptk.text_generation_api import TextGenerationAPI +from aoptk.normalization.pubchem_api import PubChemAPI +from aoptk.normalization.mesh_terms import MeshTerms +from aoptk.normalization.provide_mesh_term_dataframe_from_xml import ProvideMeshTermDataframeFromXML chemicals = pd.read_csv("$input_file", sep="\t") #if $normalization_method.method == "llm": -from aoptk.text_generation_api import TextGenerationAPI - litellm_config_file = os.environ.get("LITELLM_CONFIG_FILE") if not litellm_config_file: sys.exit("LITELLM_CONFIG_FILE environment variable is not set.") @@ -36,22 +38,17 @@ litellm_api_key = os.environ.get("LITELLM_API_KEY") chemical_list = pd.read_csv("$chemical_list", sep="\t")["chemical"].tolist() chemicals["chemical"] = chemicals["chemical"].apply( - lambda x: TextGenerationAPI(model="$llm_model", api_key=litellm_api_key).normalize_chemical(chemical=Chemical(x), chemical_list=[Chemical(chem) for chem in chemical_list]) + lambda x: TextGenerationAPI(model="$llm_models", api_key=litellm_api_key).normalize_chemical(chemical=Chemical(x), chemical_list=[Chemical(chem) for chem in chemical_list]) ) #end if -#else if $normalization_method.method == "pubchem": -from aoptk.normalization.pubchem_api import PubChemAPI - +#elif $normalization_method.method == "pubchem": chemicals["chemical"] = chemicals["chemical"].apply( lambda x: PubChemAPI().normalize_chemical(Chemical(x)) ) #end if -#else if $normalization_method.method == "mesh": -from aoptk.normalization.mesh_terms import MeshTerms -from aoptk.normalization.provide_mesh_term_dataframe_from_xml import ProvideMeshTermDataframeFromXML - +#elif $normalization_method.method == "mesh": mesh_terms_xml = pd.read_xml("$input_xml") mesh_terms_df = ProvideMeshTermDataframeFromXML(mesh_terms_xml) chemicals["chemical"] = chemicals["chemical"].apply( From b5950825dcbbcaba21b651ef57b518f39f1c71cb Mon Sep 17 00:00:00 2001 From: rdurnik Date: Fri, 24 Apr 2026 20:14:48 +0200 Subject: [PATCH 107/173] reorder --- tools/aoptk/aoptk_find_relationships.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/aoptk/aoptk_find_relationships.xml b/tools/aoptk/aoptk_find_relationships.xml index 79107d0b..165180bd 100644 --- a/tools/aoptk/aoptk_find_relationships.xml +++ b/tools/aoptk/aoptk_find_relationships.xml @@ -130,10 +130,10 @@ with open("relationships.tsv", "w") as f_out: - - + + From 89f6b3a34433f2d5a74407b1f13cbcff942bfb77 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Fri, 24 Apr 2026 20:14:54 +0200 Subject: [PATCH 108/173] config issues Co-authored-by: Copilot --- tools/aoptk/aoptk_find_chemicals.xml | 5 +---- tools/aoptk/aoptk_normalize_chemicals.xml | 8 ++------ 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/tools/aoptk/aoptk_find_chemicals.xml b/tools/aoptk/aoptk_find_chemicals.xml index 72413dff..c8dcbf31 100644 --- a/tools/aoptk/aoptk_find_chemicals.xml +++ b/tools/aoptk/aoptk_find_chemicals.xml @@ -45,10 +45,7 @@ with open(litellm_config_file, "r") as f: litellm_api_key = os.environ.get("LITELLM_API_KEY") chemicals = TextGenerationAPI(model="$llm_models", api_key=litellm_api_key).find_chemicals(text) -#end if - - -#elif $identification_method.method == "scispacy": +#elif $identification_method.method == "scispacy" chemicals = SpacyText().find_chemical(text) #end if diff --git a/tools/aoptk/aoptk_normalize_chemicals.xml b/tools/aoptk/aoptk_normalize_chemicals.xml index 975d4887..d11b9831 100644 --- a/tools/aoptk/aoptk_normalize_chemicals.xml +++ b/tools/aoptk/aoptk_normalize_chemicals.xml @@ -40,15 +40,11 @@ chemical_list = pd.read_csv("$chemical_list", sep="\t")["chemical"].tolist() chemicals["chemical"] = chemicals["chemical"].apply( lambda x: TextGenerationAPI(model="$llm_models", api_key=litellm_api_key).normalize_chemical(chemical=Chemical(x), chemical_list=[Chemical(chem) for chem in chemical_list]) ) -#end if - -#elif $normalization_method.method == "pubchem": +#elif $normalization_method.method == "pubchem" chemicals["chemical"] = chemicals["chemical"].apply( lambda x: PubChemAPI().normalize_chemical(Chemical(x)) ) -#end if - -#elif $normalization_method.method == "mesh": +#elif $normalization_method.method == "mesh" mesh_terms_xml = pd.read_xml("$input_xml") mesh_terms_df = ProvideMeshTermDataframeFromXML(mesh_terms_xml) chemicals["chemical"] = chemicals["chemical"].apply( From 4ffa4393e6a16eeda6bc06272257c49ccd557d71 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Mon, 27 Apr 2026 15:53:32 +0200 Subject: [PATCH 109/173] llm key put earlier for pdf parsing --- tools/aoptk/aoptk_find_chemicals.xml | 16 ++++++++-------- tools/aoptk/aoptk_find_relationships.xml | 20 ++++++++------------ tools/aoptk/aoptk_parse_pdf.xml | 2 +- 3 files changed, 17 insertions(+), 21 deletions(-) diff --git a/tools/aoptk/aoptk_find_chemicals.xml b/tools/aoptk/aoptk_find_chemicals.xml index c8dcbf31..04576f3f 100644 --- a/tools/aoptk/aoptk_find_chemicals.xml +++ b/tools/aoptk/aoptk_find_chemicals.xml @@ -26,8 +26,15 @@ from aoptk.spacy_text_processor import SpacyText publication_id = os.path.splitext("$input_file.element_identifier")[0] +litellm_config_file = os.environ.get("LITELLM_CONFIG_FILE") +if not litellm_config_file: + sys.exit("LITELLM_CONFIG_FILE environment variable is not set.") +with open(litellm_config_file, "r") as f: + config = yaml.safe_load(f) +litellm_api_key = os.environ.get("LITELLM_API_KEY") + if "${input_file.ext}" == "pdf": - text = PymupdfParser([PDF("$input_file")]).inject_text_generation(TextGenerationAPI(model="qwen3.5")).get_publications()[0].full_text + text = PymupdfParser([PDF("$input_file")], text_generation=TextGenerationAPI(model="qwen3.5")).get_publications()[0].full_text elif "${input_file.ext}" == "txt": with open("$input_file", "r") as f_in: text = f_in.read() @@ -37,13 +44,6 @@ else: #if $identification_method.method == "llm": -litellm_config_file = os.environ.get("LITELLM_CONFIG_FILE") -if not litellm_config_file: - sys.exit("LITELLM_CONFIG_FILE environment variable is not set.") -with open(litellm_config_file, "r") as f: - config = yaml.safe_load(f) -litellm_api_key = os.environ.get("LITELLM_API_KEY") - chemicals = TextGenerationAPI(model="$llm_models", api_key=litellm_api_key).find_chemicals(text) #elif $identification_method.method == "scispacy" chemicals = SpacyText().find_chemical(text) diff --git a/tools/aoptk/aoptk_find_relationships.xml b/tools/aoptk/aoptk_find_relationships.xml index 165180bd..83733c57 100644 --- a/tools/aoptk/aoptk_find_relationships.xml +++ b/tools/aoptk/aoptk_find_relationships.xml @@ -29,8 +29,15 @@ from aoptk.relationship_type import Inhibitive from aoptk.spacy_text_processor import SpacyText from aoptk.relationships.zero_shot_classification_single import ZeroShotClassificationSingle +litellm_config_file = os.environ.get("LITELLM_CONFIG_FILE") +if not litellm_config_file: + sys.exit("LITELLM_CONFIG_FILE environment variable is not set.") +with open(litellm_config_file, "r") as f: + config = yaml.safe_load(f) +litellm_api_key = os.environ.get("LITELLM_API_KEY") + if "${input_file.ext}" == "pdf": - text = PymupdfParser([PDF("$input_file")]).inject_text_generation(TextGenerationAPI(model="qwen3.5")).get_publications()[0].full_text + text = PymupdfParser([PDF("$input_file")], text_generation=TextGenerationAPI(model="qwen3.5")).get_publications()[0].full_text elif "${input_file.ext}" == "txt": with open("$input_file", "r") as f_in: @@ -52,17 +59,6 @@ else: #if $identification_method.method == "llm_text" or $identification_method.method == "llm_text_images" chemicals = pd.read_csv("$chemicals", sep="\t")["chemical"].tolist() -litellm_config_file = os.environ.get("LITELLM_CONFIG_FILE") -if not litellm_config_file: - sys.exit("LITELLM_CONFIG_FILE environment variable is not set.") -with open(litellm_config_file, "r") as f: - config = yaml.safe_load(f) -litellm_api_key = os.environ.get("LITELLM_API_KEY") -if not litellm_api_key and isinstance(config, dict): - env_vars = config.get("environment_variables", {}) - litellm_api_key = env_vars.get("LITELLM_API_KEY") -if not litellm_api_key: - sys.exit("LITELLM_API_KEY is not set. Configure it in Galaxy secrets/environment.") #end if #if $identification_method.method == "llm_text" diff --git a/tools/aoptk/aoptk_parse_pdf.xml b/tools/aoptk/aoptk_parse_pdf.xml index 90b3bfe1..87be3aa2 100644 --- a/tools/aoptk/aoptk_parse_pdf.xml +++ b/tools/aoptk/aoptk_parse_pdf.xml @@ -31,7 +31,7 @@ litellm_api_key = os.environ.get("LITELLM_API_KEY") publication_id = os.path.splitext("$input_file.element_identifier")[0] if "${input_file.ext}" == "pdf": - text = PymupdfParser([PDF("$input_file")]).inject_text_generation(TextGenerationAPI(model="qwen3.5")).get_publications()[0].full_text + text = PymupdfParser([PDF("$input_file")], text_generation=TextGenerationAPI(model="qwen3.5")).get_publications()[0].full_text with open(f"{publication_id}.txt", "w") as f: f.write(text) From 278eccfe1f79fad4a2e2f126be0298d29a6a9dcd Mon Sep 17 00:00:00 2001 From: rdurnik Date: Mon, 27 Apr 2026 15:53:44 +0200 Subject: [PATCH 110/173] hardcoded the aoptk version --- tools/aoptk/aoptk_chemical_identifier.xml | 2 +- tools/aoptk/aoptk_publication_tracker.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/aoptk/aoptk_chemical_identifier.xml b/tools/aoptk/aoptk_chemical_identifier.xml index 2f530247..f8e6c497 100644 --- a/tools/aoptk/aoptk_chemical_identifier.xml +++ b/tools/aoptk/aoptk_chemical_identifier.xml @@ -1,4 +1,4 @@ - + Detect chemicals in scientific literature and filter based on a screening list. macros.xml diff --git a/tools/aoptk/aoptk_publication_tracker.xml b/tools/aoptk/aoptk_publication_tracker.xml index 24e4d6b8..83bc1e53 100644 --- a/tools/aoptk/aoptk_publication_tracker.xml +++ b/tools/aoptk/aoptk_publication_tracker.xml @@ -1,4 +1,4 @@ - + Tool to track publications. macros.xml From 165597ab8a1d3ad5513b4f19a9457105f42b408e Mon Sep 17 00:00:00 2001 From: rdurnik Date: Mon, 4 May 2026 12:47:39 +0200 Subject: [PATCH 111/173] specified the version for publication-tracker and chemical-identifier --- tools/aoptk/aoptk_chemical_identifier.xml | 2 +- tools/aoptk/aoptk_publication_tracker.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/aoptk/aoptk_chemical_identifier.xml b/tools/aoptk/aoptk_chemical_identifier.xml index f8e6c497..3deef3f0 100644 --- a/tools/aoptk/aoptk_chemical_identifier.xml +++ b/tools/aoptk/aoptk_chemical_identifier.xml @@ -1,4 +1,4 @@ - + Detect chemicals in scientific literature and filter based on a screening list. macros.xml diff --git a/tools/aoptk/aoptk_publication_tracker.xml b/tools/aoptk/aoptk_publication_tracker.xml index 83bc1e53..668abea9 100644 --- a/tools/aoptk/aoptk_publication_tracker.xml +++ b/tools/aoptk/aoptk_publication_tracker.xml @@ -1,4 +1,4 @@ - + Tool to track publications. macros.xml From 4fbcd43efa60bcf5392ad4ea74602b1a47a06002 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Mon, 4 May 2026 12:48:02 +0200 Subject: [PATCH 112/173] query import fix Co-authored-by: Copilot --- tools/aoptk/aoptk_query_literature.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tools/aoptk/aoptk_query_literature.xml b/tools/aoptk/aoptk_query_literature.xml index 99c767d4..3c9d3ae3 100644 --- a/tools/aoptk/aoptk_query_literature.xml +++ b/tools/aoptk/aoptk_query_literature.xml @@ -16,9 +16,6 @@ -from aoptk.literature.databases.pubmed import PubMed -from aoptk.literature.databases.europepmc import EuropePMC -from aoptk.literature.databases.pmc import PMC from Bio import Entrez import os @@ -26,12 +23,15 @@ email = os.environ.get("EMAIL") api_key = os.environ.get("NCBI_API_KEY") if "${literature_database_pubmed_europepmc_pmc}" == "pubmed": + from aoptk.literature.databases.pubmed import PubMed Entrez.email = email Entrez.api_key = api_key ids = PubMed("${query}").get_id() elif "${literature_database_pubmed_europepmc_pmc}" == "europepmc": + from aoptk.literature.databases.europepmc import EuropePMC ids = EuropePMC("${query}").get_id() elif "${literature_database_pubmed_europepmc_pmc}" == "pmc": + from aoptk.literature.databases.pmc import PMC Entrez.email = email Entrez.api_key = api_key ids = PMC("${query}").get_id() @@ -52,7 +52,7 @@ with open(f"ids.txt", "w") as f: - + From 8876eeada6218ec50d97d541285dbcba3ac1c73c Mon Sep 17 00:00:00 2001 From: rdurnik Date: Mon, 4 May 2026 12:48:13 +0200 Subject: [PATCH 113/173] new aoptk version in macros --- tools/aoptk/macros.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/aoptk/macros.xml b/tools/aoptk/macros.xml index 47cd512d..f647202c 100644 --- a/tools/aoptk/macros.xml +++ b/tools/aoptk/macros.xml @@ -1,5 +1,5 @@ - 0.2.0 + 0.3.2 0 From ff278980a9429e89afc1944f6f3c1aac45ec2e5d Mon Sep 17 00:00:00 2001 From: rdurnik Date: Mon, 4 May 2026 15:01:46 +0200 Subject: [PATCH 114/173] removed macros for databases --- tools/aoptk/macros.xml | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/tools/aoptk/macros.xml b/tools/aoptk/macros.xml index f647202c..9e35dcbc 100644 --- a/tools/aoptk/macros.xml +++ b/tools/aoptk/macros.xml @@ -96,28 +96,6 @@ - - - - - - - - - - - - - - - - - - - - - - From c9560ce1c7e565cafc2098f11e6072d21b2c705f Mon Sep 17 00:00:00 2001 From: rdurnik Date: Mon, 4 May 2026 15:02:06 +0200 Subject: [PATCH 115/173] publication-tracker, chemical-identifier removed macros, version specification --- tools/aoptk/aoptk_chemical_identifier.xml | 13 ++++++++----- tools/aoptk/aoptk_publication_tracker.xml | 15 +++++++++------ 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/tools/aoptk/aoptk_chemical_identifier.xml b/tools/aoptk/aoptk_chemical_identifier.xml index 3deef3f0..9cf73ab4 100644 --- a/tools/aoptk/aoptk_chemical_identifier.xml +++ b/tools/aoptk/aoptk_chemical_identifier.xml @@ -5,14 +5,14 @@ - + aoptk - + + + + @@ -33,13 +36,13 @@ - + - + diff --git a/tools/aoptk/aoptk_publication_tracker.xml b/tools/aoptk/aoptk_publication_tracker.xml index 668abea9..8882dd8e 100644 --- a/tools/aoptk/aoptk_publication_tracker.xml +++ b/tools/aoptk/aoptk_publication_tracker.xml @@ -5,7 +5,7 @@ - + aoptk @@ -17,14 +17,17 @@ --master 'master.${master.ext}' --code '$code' --query '$query' - --database '$literature_database_pubmed_europepmc' + --database '$literature_database' --outdir . \${EMAIL:+--email \$EMAIL} ]]> - + + + + @@ -47,7 +50,7 @@ - + @@ -55,7 +58,7 @@ - + @@ -63,7 +66,7 @@ - + From 8e80399ae35bb507cbd89443c377609048cb8188 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Mon, 4 May 2026 15:05:51 +0200 Subject: [PATCH 116/173] query literature update Co-authored-by: Copilot --- tools/aoptk/aoptk_query_literature.xml | 30 +++++++++++++++----------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/tools/aoptk/aoptk_query_literature.xml b/tools/aoptk/aoptk_query_literature.xml index 3c9d3ae3..780a97a1 100644 --- a/tools/aoptk/aoptk_query_literature.xml +++ b/tools/aoptk/aoptk_query_literature.xml @@ -19,22 +19,22 @@ from Bio import Entrez import os -email = os.environ.get("EMAIL") -api_key = os.environ.get("NCBI_API_KEY") +email = os.environ.get("EMAIL") or None +api_key = os.environ.get("NCBI_API_KEY") or None -if "${literature_database_pubmed_europepmc_pmc}" == "pubmed": - from aoptk.literature.databases.pubmed import PubMed +if "${database}" == "pubmed": + from aoptk.literature.databases.pubmed import PubMed Entrez.email = email Entrez.api_key = api_key - ids = PubMed("${query}").get_id() -elif "${literature_database_pubmed_europepmc_pmc}" == "europepmc": - from aoptk.literature.databases.europepmc import EuropePMC - ids = EuropePMC("${query}").get_id() -elif "${literature_database_pubmed_europepmc_pmc}" == "pmc": + ids = PubMed("${query}").get_ids() +elif "${database}" == "europepmc": + from aoptk.literature.databases.europepmc import EuropePMC + ids = EuropePMC("${query}", storage = "./", figure_storage="./figures").get_ids() +elif "${database}" == "pmc": from aoptk.literature.databases.pmc import PMC Entrez.email = email Entrez.api_key = api_key - ids = PMC("${query}").get_id() + ids = PMC("${query}", storage = "./", figure_storage="./figures").get_ids() else: raise ValueError("Select valid database.") @@ -48,17 +48,21 @@ with open(f"ids.txt", "w") as f: - + + + + + - + - + From fe5f358dbba06692082b8b3391f0107a129db097 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Tue, 5 May 2026 09:41:35 +0200 Subject: [PATCH 117/173] initial version of find chemicals and relationships in one --- .../aoptk_find_relationships_chemicals.xml | 217 ++++++++++++++++++ 1 file changed, 217 insertions(+) create mode 100644 tools/aoptk/aoptk_find_relationships_chemicals.xml diff --git a/tools/aoptk/aoptk_find_relationships_chemicals.xml b/tools/aoptk/aoptk_find_relationships_chemicals.xml new file mode 100644 index 00000000..ce07d1da --- /dev/null +++ b/tools/aoptk/aoptk_find_relationships_chemicals.xml @@ -0,0 +1,217 @@ + + Identify relationships between chemicals and effects. + + macros.xml + + + + + + + + + + +import os +import sys +from pathlib import Path + +import pandas as pd +import yaml + +from aoptk.literature.pdf import PDF +from aoptk.literature.pymupdf_parser import PymupdfParser +from aoptk.text_generation_api import TextGenerationAPI +from aoptk.relationship_type import Causative +from aoptk.relationship_type import Inhibitive +from aoptk.spacy_text_processor import SpacyText +from aoptk.relationships.zero_shot_classification_single import ZeroShotClassificationSingle + +litellm_config_file = os.environ.get("LITELLM_CONFIG_FILE") +if not litellm_config_file: + sys.exit("LITELLM_CONFIG_FILE environment variable is not set.") +with open(litellm_config_file, "r") as f: + config = yaml.safe_load(f) +litellm_api_key = os.environ.get("LITELLM_API_KEY") + +if "${input_file.ext}" == "pdf": + text = PymupdfParser([PDF("$input_file")], text_generation=TextGenerationAPI(model="qwen3.5")).get_publications()[0].full_text +elif "${input_file.ext}" == "txt": + with open("$input_file", "r") as f_in: + text = f_in.read() +else: + raise ValueError(f"Unsupported input file format: ${input_file.ext}") + +publication_id = os.path.splitext("$input_file.element_identifier")[0] +effects = ["${effect}"] + +if "$relationship_type" == "causative": + relationship_type = Causative +elif "$relationship_type" == "inhibitive": + relationship_type = Inhibitive +else: + raise ValueError(f"Unsupported relationship type: ${relationship_type}") + +#if $analysis_purpose.method == "relationships_chemicals" +chemicals = TextGenerationAPI(model="$llm_models", api_key=litellm_api_key).find_chemicals(text) + #if $identification_method.method == "llm_text" + relationships = TextGenerationAPI(model="$llm_models", api_key=litellm_api_key).find_relationships_in_text(text=text, chemicals=chemicals, effects=effects, relationship_type=relationship_type) + #elif $identification_method.method == "llm_text_images" + text_with_images = text + text_with_images += "\n\n\n\nALSO ANALYZE THESE FIGURES CONVERTED TO TEXT:" + for figure in Path("$input_images").iterdir(): + text_image = TextGenerationAPI(model="$llm_models_images", api_key=litellm_api_key).convert_image( + image_path=figure, + text=text, + ) + text_with_images += f"\n\n{figure.name}:\n{text_image}" + relationships = TextGenerationAPI(model="$llm_models", api_key=litellm_api_key).find_relationships_in_text(text=text_with_images, chemicals=chemicals, effects=effects, relationship_type=relationship_type) + #end if +with open("relationships.tsv", "w") as f_out: +f_out.write("id\tchemical\teffect\trelationship\n") +for relationship in relationships: + f_out.write(f"{publication_id}\t{relationship.chemical}\t{relationship.effect}\t{relationship.relationship_type}\n") +with open("chemicals.tsv", "w") as f_out: + f_out.write("chemical\n") + for chemical in chemicals: + f_out.write(f"{chemical.name}\n") + +#elif $analysis_purpose.method == "relationships" +chemicals = pd.read_csv("$chemicals", sep="\t")["chemical"].tolist() + #if $identification_method.method == "llm_text" + relationships = TextGenerationAPI(model="$llm_models", api_key=litellm_api_key).find_relationships_in_text(text=text, chemicals=chemicals, effects=effects, relationship_type=relationship_type) + #elif $identification_method.method == "llm_text_images" + text_with_images = text + text_with_images += "\n\n\n\nALSO ANALYZE THESE FIGURES CONVERTED TO TEXT:" + for figure in Path("$input_images").iterdir(): + text_image = TextGenerationAPI(model="$llm_models_images", api_key=litellm_api_key).convert_image( + image_path=figure, + text=text, + ) + text_with_images += f"\n\n{figure.name}:\n{text_image}" + relationships = TextGenerationAPI(model="$llm_models", api_key=litellm_api_key).find_relationships_in_text(text=text_with_images, chemicals=chemicals, effects=effects, relationship_type=relationship_type) + #end if +with open("relationships.tsv", "w") as f_out: + f_out.write("id\tchemical\teffect\trelationship\n") + for relationship in relationships: + f_out.write(f"{publication_id}\t{relationship.chemical}\t{relationship.effect}\t{relationship.relationship_type}\n") + +#elif $analysis_purpose.method == "chemicals" +chemicals = TextGenerationAPI(model="$llm_models", api_key=litellm_api_key).find_chemicals(text) +with open("chemicals.tsv", "w") as f_out: + f_out.write("chemical\n") + for chemical in chemicals: + f_out.write(f"{chemical.name}\n") +#end if + + + + +with open("relationships.tsv", "w") as f_out: + f_out.write("id\tchemical\teffect\trelationship\n") + for relationship in relationships: + f_out.write(f"{publication_id}\t{relationship.chemical}\t{relationship.effect}\t{relationship.relationship_type}\n") + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From f7b79461e012e3da01199c391317d030a4f35dcd Mon Sep 17 00:00:00 2001 From: rdurnik Date: Tue, 5 May 2026 10:43:37 +0200 Subject: [PATCH 118/173] reorder --- tools/aoptk/aoptk_normalize_chemicals.xml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tools/aoptk/aoptk_normalize_chemicals.xml b/tools/aoptk/aoptk_normalize_chemicals.xml index d11b9831..e533a06c 100644 --- a/tools/aoptk/aoptk_normalize_chemicals.xml +++ b/tools/aoptk/aoptk_normalize_chemicals.xml @@ -61,10 +61,14 @@ chemicals.to_csv("normalized_chemicals.tsv", sep="\t", index=False) - + + + + + @@ -72,10 +76,6 @@ chemicals.to_csv("normalized_chemicals.tsv", sep="\t", index=False) - - - - From 06376071ac972b343cb69b370e8ab7faea4ea5c9 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Tue, 5 May 2026 10:44:05 +0200 Subject: [PATCH 119/173] deleted find chemicals and find relationships as individual files, contains spacy and zeroshot --- tools/aoptk/aoptk_find_chemicals.xml | 109 -------------- tools/aoptk/aoptk_find_relationships.xml | 179 ----------------------- 2 files changed, 288 deletions(-) delete mode 100644 tools/aoptk/aoptk_find_chemicals.xml delete mode 100644 tools/aoptk/aoptk_find_relationships.xml diff --git a/tools/aoptk/aoptk_find_chemicals.xml b/tools/aoptk/aoptk_find_chemicals.xml deleted file mode 100644 index 04576f3f..00000000 --- a/tools/aoptk/aoptk_find_chemicals.xml +++ /dev/null @@ -1,109 +0,0 @@ - - Identify chemicals in text. - - macros.xml - - - - - - - - - - -import os -import sys - -import pandas as pd -import yaml -from aoptk.literature.pdf import PDF -from aoptk.literature.pymupdf_parser import PymupdfParser -from aoptk.text_generation_api import TextGenerationAPI -from aoptk.spacy_text_processor import SpacyText - -publication_id = os.path.splitext("$input_file.element_identifier")[0] - -litellm_config_file = os.environ.get("LITELLM_CONFIG_FILE") -if not litellm_config_file: - sys.exit("LITELLM_CONFIG_FILE environment variable is not set.") -with open(litellm_config_file, "r") as f: - config = yaml.safe_load(f) -litellm_api_key = os.environ.get("LITELLM_API_KEY") - -if "${input_file.ext}" == "pdf": - text = PymupdfParser([PDF("$input_file")], text_generation=TextGenerationAPI(model="qwen3.5")).get_publications()[0].full_text -elif "${input_file.ext}" == "txt": - with open("$input_file", "r") as f_in: - text = f_in.read() -else: - raise ValueError(f"Unsupported input file format: ${input_file.ext}") - - -#if $identification_method.method == "llm": - -chemicals = TextGenerationAPI(model="$llm_models", api_key=litellm_api_key).find_chemicals(text) -#elif $identification_method.method == "scispacy" -chemicals = SpacyText().find_chemical(text) -#end if - -with open("chemicals.tsv", "w") as f_out: - f_out.write("chemical\n") - for chemical in chemicals: - f_out.write(f"{chemical.name}\n") - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/tools/aoptk/aoptk_find_relationships.xml b/tools/aoptk/aoptk_find_relationships.xml deleted file mode 100644 index 83733c57..00000000 --- a/tools/aoptk/aoptk_find_relationships.xml +++ /dev/null @@ -1,179 +0,0 @@ - - Identify relationships between chemicals and effects. - - macros.xml - - - - - - - - - - -import os -import sys -from pathlib import Path - -import pandas as pd -import yaml - -from aoptk.literature.pdf import PDF -from aoptk.literature.pymupdf_parser import PymupdfParser -from aoptk.text_generation_api import TextGenerationAPI -from aoptk.relationship_type import Causative -from aoptk.relationship_type import Inhibitive -from aoptk.spacy_text_processor import SpacyText -from aoptk.relationships.zero_shot_classification_single import ZeroShotClassificationSingle - -litellm_config_file = os.environ.get("LITELLM_CONFIG_FILE") -if not litellm_config_file: - sys.exit("LITELLM_CONFIG_FILE environment variable is not set.") -with open(litellm_config_file, "r") as f: - config = yaml.safe_load(f) -litellm_api_key = os.environ.get("LITELLM_API_KEY") - -if "${input_file.ext}" == "pdf": - text = PymupdfParser([PDF("$input_file")], text_generation=TextGenerationAPI(model="qwen3.5")).get_publications()[0].full_text - -elif "${input_file.ext}" == "txt": - with open("$input_file", "r") as f_in: - text = f_in.read() - -else: - raise ValueError(f"Unsupported input file format: ${input_file.ext}") - -publication_id = os.path.splitext("$input_file.element_identifier")[0] -effects = ["${effect}"] - -if "$relationship_type" == "causative": - relationship_type = Causative -elif "$relationship_type" == "inhibitive": - relationship_type = Inhibitive -else: - raise ValueError(f"Unsupported relationship type: ${relationship_type}") - -#if $identification_method.method == "llm_text" or $identification_method.method == "llm_text_images" -chemicals = pd.read_csv("$chemicals", sep="\t")["chemical"].tolist() - -#end if - -#if $identification_method.method == "llm_text" -relationships = TextGenerationAPI(model="$llm_models", api_key=litellm_api_key).find_relationships_in_text(text=text, chemicals=chemicals, effects=effects, relationship_type=relationship_type) -#end if - - -#if $identification_method.method == "llm_text_images" -text_with_images = text -text_with_images += "\n\n\n\nALSO ANALYZE THESE FIGURES CONVERTED TO TEXT:" -for figure in Path("$input_images").iterdir(): - text_image = TextGenerationAPI(model="$llm_models_images", api_key=litellm_api_key).convert_image( - image_path=figure, - text=text, - ) - text_with_images += f"\n\n{figure.name}:\n{text_image}" - -relationships = TextGenerationAPI(model="$llm_models", api_key=litellm_api_key).find_relationships_in_text(text=text_with_images, chemicals=chemicals, effects=effects, relationship_type=relationship_type) -#end if - -#if $identification_method.method == "zero_shot_classification" -chemicals = pd.read_csv("$chemicals", sep="\t")["chemical"].tolist() - -if len(text) > 3000: - raise ValueError("Input text is very long. Consider using the LLM-based method for better performance.") - -os.environ.setdefault("TORCHINDUCTOR_CACHE_DIR", os.path.join(os.getcwd(), ".torchinductor_cache")) -os.makedirs(os.environ["TORCHINDUCTOR_CACHE_DIR"], exist_ok=True) - -relationships = ZeroShotClassificationSingle().find_relationships(text=text, chemicals=chemicals, effects=effects, relationship_type=relationship_type) -#end if - -with open("relationships.tsv", "w") as f_out: - f_out.write("id\tchemical\teffect\trelationship\n") - for relationship in relationships: - f_out.write(f"{publication_id}\t{relationship.chemical}\t{relationship.effect}\t{relationship.relationship_type}\n") - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file From 4e3b5bcce4852556d1c97d003f161963138f6c00 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Tue, 5 May 2026 10:44:28 +0200 Subject: [PATCH 120/173] pdf needs api key --- tools/aoptk/aoptk_parse_pdf.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/aoptk/aoptk_parse_pdf.xml b/tools/aoptk/aoptk_parse_pdf.xml index 87be3aa2..c13d079a 100644 --- a/tools/aoptk/aoptk_parse_pdf.xml +++ b/tools/aoptk/aoptk_parse_pdf.xml @@ -31,7 +31,7 @@ litellm_api_key = os.environ.get("LITELLM_API_KEY") publication_id = os.path.splitext("$input_file.element_identifier")[0] if "${input_file.ext}" == "pdf": - text = PymupdfParser([PDF("$input_file")], text_generation=TextGenerationAPI(model="qwen3.5")).get_publications()[0].full_text + text = PymupdfParser([PDF("$input_file")], text_generation=TextGenerationAPI(model="qwen3.5", api_key=litellm_api_key)).get_publications()[0].full_text with open(f"{publication_id}.txt", "w") as f: f.write(text) From 13356c2d9389609d36991ad04897a9785e293d1f Mon Sep 17 00:00:00 2001 From: rdurnik Date: Tue, 5 May 2026 10:44:54 +0200 Subject: [PATCH 121/173] fixed combined find relationships and find chemicals Co-authored-by: Copilot --- .../aoptk_find_relationships_chemicals.xml | 284 ++++++++++-------- 1 file changed, 155 insertions(+), 129 deletions(-) diff --git a/tools/aoptk/aoptk_find_relationships_chemicals.xml b/tools/aoptk/aoptk_find_relationships_chemicals.xml index ce07d1da..a9122941 100644 --- a/tools/aoptk/aoptk_find_relationships_chemicals.xml +++ b/tools/aoptk/aoptk_find_relationships_chemicals.xml @@ -7,13 +7,13 @@ - + - - + + import os import sys from pathlib import Path @@ -23,11 +23,10 @@ import yaml from aoptk.literature.pdf import PDF from aoptk.literature.pymupdf_parser import PymupdfParser -from aoptk.text_generation_api import TextGenerationAPI +from aoptk.effect import Effect from aoptk.relationship_type import Causative from aoptk.relationship_type import Inhibitive -from aoptk.spacy_text_processor import SpacyText -from aoptk.relationships.zero_shot_classification_single import ZeroShotClassificationSingle +from aoptk.text_generation_api import TextGenerationAPI litellm_config_file = os.environ.get("LITELLM_CONFIG_FILE") if not litellm_config_file: @@ -37,7 +36,7 @@ with open(litellm_config_file, "r") as f: litellm_api_key = os.environ.get("LITELLM_API_KEY") if "${input_file.ext}" == "pdf": - text = PymupdfParser([PDF("$input_file")], text_generation=TextGenerationAPI(model="qwen3.5")).get_publications()[0].full_text + text = PymupdfParser([PDF("$input_file")], text_generation=TextGenerationAPI(model="qwen3.5", api_key=litellm_api_key)).get_publications()[0].full_text elif "${input_file.ext}" == "txt": with open("$input_file", "r") as f_in: text = f_in.read() @@ -45,172 +44,199 @@ else: raise ValueError(f"Unsupported input file format: ${input_file.ext}") publication_id = os.path.splitext("$input_file.element_identifier")[0] -effects = ["${effect}"] +def write_relationships(relationships): + with open("relationships.tsv", "w") as f_out: + f_out.write("id\tchemical\teffect\trelationship\n") + for relationship in relationships: + f_out.write(f"{publication_id}\t{relationship.chemical}\t{relationship.effect}\t{relationship.relationship_type}\n") + + +def write_chemicals(chemicals): + with open("chemicals.tsv", "w") as f_out: + f_out.write("chemical\n") + for chemical in chemicals: + chemical_name = chemical.name if hasattr(chemical, "name") else chemical + f_out.write(f"{chemical_name}\n") + + +#if $analysis_purpose.method == "relationships_chemicals" +effects = [Effect("${effect}")] if "$relationship_type" == "causative": - relationship_type = Causative + relationship_type = Causative() elif "$relationship_type" == "inhibitive": - relationship_type = Inhibitive + relationship_type = Inhibitive() else: raise ValueError(f"Unsupported relationship type: ${relationship_type}") -#if $analysis_purpose.method == "relationships_chemicals" chemicals = TextGenerationAPI(model="$llm_models", api_key=litellm_api_key).find_chemicals(text) - #if $identification_method.method == "llm_text" - relationships = TextGenerationAPI(model="$llm_models", api_key=litellm_api_key).find_relationships_in_text(text=text, chemicals=chemicals, effects=effects, relationship_type=relationship_type) - #elif $identification_method.method == "llm_text_images" - text_with_images = text - text_with_images += "\n\n\n\nALSO ANALYZE THESE FIGURES CONVERTED TO TEXT:" - for figure in Path("$input_images").iterdir(): - text_image = TextGenerationAPI(model="$llm_models_images", api_key=litellm_api_key).convert_image( - image_path=figure, - text=text, - ) +#if $identification_method.method == "llm_text" +relationships = TextGenerationAPI(model="$llm_models", api_key=litellm_api_key).find_relationships_in_text(text=text, chemicals=chemicals, effects=effects, relationship_type=relationship_type) +#elif $identification_method.method == "llm_text_images" +text_with_images = text +text_with_images += "\n\n\n\nALSO ANALYZE THESE FIGURES CONVERTED TO TEXT:" +for figure in Path("$input_images").iterdir(): + text_image = TextGenerationAPI(model="$llm_models_images", api_key=litellm_api_key).convert_image( + image_path=figure, + text=text, + ) text_with_images += f"\n\n{figure.name}:\n{text_image}" - relationships = TextGenerationAPI(model="$llm_models", api_key=litellm_api_key).find_relationships_in_text(text=text_with_images, chemicals=chemicals, effects=effects, relationship_type=relationship_type) - #end if -with open("relationships.tsv", "w") as f_out: -f_out.write("id\tchemical\teffect\trelationship\n") -for relationship in relationships: - f_out.write(f"{publication_id}\t{relationship.chemical}\t{relationship.effect}\t{relationship.relationship_type}\n") -with open("chemicals.tsv", "w") as f_out: - f_out.write("chemical\n") - for chemical in chemicals: - f_out.write(f"{chemical.name}\n") +relationships = TextGenerationAPI(model="$llm_models", api_key=litellm_api_key).find_relationships_in_text(text=text_with_images, chemicals=chemicals, effects=effects, relationship_type=relationship_type) +#end if +write_relationships(relationships) +write_chemicals(chemicals) #elif $analysis_purpose.method == "relationships" +effects = ["${effect}"] +if "$relationship_type" == "causative": + relationship_type = Causative +elif "$relationship_type" == "inhibitive": + relationship_type = Inhibitive +else: + raise ValueError(f"Unsupported relationship type: ${relationship_type}") + chemicals = pd.read_csv("$chemicals", sep="\t")["chemical"].tolist() - #if $identification_method.method == "llm_text" - relationships = TextGenerationAPI(model="$llm_models", api_key=litellm_api_key).find_relationships_in_text(text=text, chemicals=chemicals, effects=effects, relationship_type=relationship_type) - #elif $identification_method.method == "llm_text_images" - text_with_images = text - text_with_images += "\n\n\n\nALSO ANALYZE THESE FIGURES CONVERTED TO TEXT:" - for figure in Path("$input_images").iterdir(): - text_image = TextGenerationAPI(model="$llm_models_images", api_key=litellm_api_key).convert_image( - image_path=figure, - text=text, - ) +#if $identification_method.method == "llm_text" +relationships = TextGenerationAPI(model="$llm_models", api_key=litellm_api_key).find_relationships_in_text(text=text, chemicals=chemicals, effects=effects, relationship_type=relationship_type) +#elif $identification_method.method == "llm_text_images" +text_with_images = text +text_with_images += "\n\n\n\nALSO ANALYZE THESE FIGURES CONVERTED TO TEXT:" +for figure in Path("$input_images").iterdir(): + text_image = TextGenerationAPI(model="$llm_models_images", api_key=litellm_api_key).convert_image( + image_path=figure, + text=text, + ) text_with_images += f"\n\n{figure.name}:\n{text_image}" - relationships = TextGenerationAPI(model="$llm_models", api_key=litellm_api_key).find_relationships_in_text(text=text_with_images, chemicals=chemicals, effects=effects, relationship_type=relationship_type) - #end if -with open("relationships.tsv", "w") as f_out: - f_out.write("id\tchemical\teffect\trelationship\n") - for relationship in relationships: - f_out.write(f"{publication_id}\t{relationship.chemical}\t{relationship.effect}\t{relationship.relationship_type}\n") +relationships = TextGenerationAPI(model="$llm_models", api_key=litellm_api_key).find_relationships_in_text(text=text_with_images, chemicals=chemicals, effects=effects, relationship_type=relationship_type) +#end if +write_relationships(relationships) #elif $analysis_purpose.method == "chemicals" chemicals = TextGenerationAPI(model="$llm_models", api_key=litellm_api_key).find_chemicals(text) -with open("chemicals.tsv", "w") as f_out: - f_out.write("chemical\n") - for chemical in chemicals: - f_out.write(f"{chemical.name}\n") +write_chemicals(chemicals) #end if - - - -with open("relationships.tsv", "w") as f_out: - f_out.write("id\tchemical\teffect\trelationship\n") - for relationship in relationships: - f_out.write(f"{publication_id}\t{relationship.chemical}\t{relationship.effect}\t{relationship.relationship_type}\n") - - - + + - - + + - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - + + + + - - - - - - - - - - - - - - - - - - + + analysis_purpose['method'] != 'chemicals' + + + analysis_purpose['method'] != 'relationships' + - - - - - - - + + + + + + + + + + + + - - - - + + + - - + + + + + + + + - - - + + + + - - - + From 74a818b4a22e947d5c4f4bfe8410ed20b5ca0b56 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Tue, 5 May 2026 10:48:04 +0200 Subject: [PATCH 122/173] fixed download to first select database and then data type Co-authored-by: Copilot --- .../aoptk/aoptk_download_publication_data.xml | 166 +++++++++--------- 1 file changed, 84 insertions(+), 82 deletions(-) diff --git a/tools/aoptk/aoptk_download_publication_data.xml b/tools/aoptk/aoptk_download_publication_data.xml index ccfc2abb..c6ffb3cc 100644 --- a/tools/aoptk/aoptk_download_publication_data.xml +++ b/tools/aoptk/aoptk_download_publication_data.xml @@ -15,119 +15,121 @@ import os -from aoptk.literature.databases.pmc import PMC -from aoptk.literature.databases.europepmc import EuropePMC -from aoptk.literature.databases.pubmed import PubMed from Bio import Entrez with open("$input_file", "r") as f: ids = [line.strip() for line in f.readlines()] -email = os.environ.get("EMAIL") -api_key = os.environ.get("NCBI_API_KEY") - - -#if $data_type.type_choice == "publications": -if "${literature_database_europepmc_pmc}" == "pmc": - Entrez.email = email - Entrez.api_key = api_key - pmc = PMC("", storage = "./", figure_storage="./figures") - pmc.id_list = ids - publications = pmc.get_publications() -elif "${literature_database_europepmc_pmc}" == "europepmc": - europepmc = EuropePMC("", storage = "./", figure_storage="./figures") - europepmc.id_list = ids - publications = europepmc.get_publications() -else: - raise ValueError("Select valid database.") + email = os.environ.get("EMAIL") + api_key = os.environ.get("NCBI_API_KEY") -for publication in publications: - with open(f"{publication.publication_id}.txt", "w") as f: - f.write(publication.full_text) + +#if $literature.database == "pmc": +from aoptk.literature.databases.pmc import PMC +Entrez.email = email +Entrez.api_key = api_key +database = PMC("", storage = "./", figure_storage="./figures") +database.id_list = ids + +#elif $literature.database == "europepmc": +from aoptk.literature.databases.europepmc import EuropePMC +database = EuropePMC("", storage = "./", figure_storage="./figures") +database.id_list = ids + +#elif $literature.database == "pubmed": +from aoptk.literature.databases.pubmed import PubMed +Entrez.email = email +Entrez.api_key = api_key +database = PubMed.__new__(PubMed) +database.id_list = ids + +#else: +raise ValueError("Select valid database.") #end if -#if $data_type.type_choice == "abstracts": -if "${literature_database_pubmed_europepmc}" == "pubmed": - Entrez.email = email - Entrez.api_key = api_key - pubmed = PubMed.__new__(PubMed) - pubmed.id_list = ids - abstracts = pubmed.get_abstracts() -elif "${literature_database_pubmed_europepmc}" == "europepmc": - europepmc = EuropePMC("") - europepmc.id_list = ids - abstracts = europepmc.get_abstracts() -else: - raise ValueError("Select valid database.") +#if $literature.type_choice == "publications": +publications = database.get_publications() +for publication in publications: + print(publication.figures) + with open(f"{publication.id}.txt", "w") as f: + f.write(publication.full_text) +#elif $literature.type_choice == "abstracts": +abstracts = database.get_abstracts() for abstract in abstracts: - with open(f"{abstract.publication_id}.txt", "w") as f: + with open(f"{abstract.id}.txt", "w") as f: f.write(abstract.text) -#end if +#elif $literature.type_choice == "pdfs": +pdfs = database.get_pdfs() -#if $data_type.type_choice == "pdfs": -pmc = PMC("", storage = "./") -pmc.id_list = ids -pdfs = pmc.get_pdfs() +#else: +raise ValueError("Select valid data type.") #end if + - - - - - + + + + + + - - - - - - - - - - - + + + + + + + + + + + + + + + + + + - - data_type['type_choice'] == 'publications' or data_type['type_choice'] == 'abstracts' - + + literature['type_choice'] == 'publications' or literature['type_choice'] == 'abstracts' + - - data_type['type_choice'] == 'publications' - + + literature['type_choice'] == 'publications' + - - data_type['type_choice'] == 'pdfs' - + + literature['type_choice'] == 'pdfs' + - - - - - - - - - - - - - - - + + + + + + + + + + + + + From 72595722ef466b898c12345afe6144ff8fa7928e Mon Sep 17 00:00:00 2001 From: rdurnik Date: Tue, 5 May 2026 14:40:01 +0200 Subject: [PATCH 123/173] removed images for analysis Co-authored-by: Copilot --- .../aoptk_find_relationships_chemicals.xml | 122 ++++-------------- 1 file changed, 27 insertions(+), 95 deletions(-) diff --git a/tools/aoptk/aoptk_find_relationships_chemicals.xml b/tools/aoptk/aoptk_find_relationships_chemicals.xml index a9122941..a5ce4bd8 100644 --- a/tools/aoptk/aoptk_find_relationships_chemicals.xml +++ b/tools/aoptk/aoptk_find_relationships_chemicals.xml @@ -17,10 +17,8 @@ import os import sys from pathlib import Path - import pandas as pd import yaml - from aoptk.literature.pdf import PDF from aoptk.literature.pymupdf_parser import PymupdfParser from aoptk.effect import Effect @@ -35,23 +33,12 @@ with open(litellm_config_file, "r") as f: config = yaml.safe_load(f) litellm_api_key = os.environ.get("LITELLM_API_KEY") -if "${input_file.ext}" == "pdf": - text = PymupdfParser([PDF("$input_file")], text_generation=TextGenerationAPI(model="qwen3.5", api_key=litellm_api_key)).get_publications()[0].full_text -elif "${input_file.ext}" == "txt": - with open("$input_file", "r") as f_in: - text = f_in.read() -else: - raise ValueError(f"Unsupported input file format: ${input_file.ext}") - -publication_id = os.path.splitext("$input_file.element_identifier")[0] - def write_relationships(relationships): with open("relationships.tsv", "w") as f_out: f_out.write("id\tchemical\teffect\trelationship\n") for relationship in relationships: f_out.write(f"{publication_id}\t{relationship.chemical}\t{relationship.effect}\t{relationship.relationship_type}\n") - def write_chemicals(chemicals): with open("chemicals.tsv", "w") as f_out: f_out.write("chemical\n") @@ -59,6 +46,15 @@ def write_chemicals(chemicals): chemical_name = chemical.name if hasattr(chemical, "name") else chemical f_out.write(f"{chemical_name}\n") +if "${input_file.ext}" == "pdf": + text = PymupdfParser([PDF("$input_file")], text_generation=TextGenerationAPI(model="qwen3.5", api_key=litellm_api_key)).get_publications()[0].full_text +elif "${input_file.ext}" == "txt": + with open("$input_file", "r") as f_in: + text = f_in.read() +else: + raise ValueError(f"Unsupported input file format: ${input_file.ext}") + +publication_id = os.path.splitext("$input_file.element_identifier")[0] #if $analysis_purpose.method == "relationships_chemicals" effects = [Effect("${effect}")] @@ -70,19 +66,7 @@ else: raise ValueError(f"Unsupported relationship type: ${relationship_type}") chemicals = TextGenerationAPI(model="$llm_models", api_key=litellm_api_key).find_chemicals(text) -#if $identification_method.method == "llm_text" relationships = TextGenerationAPI(model="$llm_models", api_key=litellm_api_key).find_relationships_in_text(text=text, chemicals=chemicals, effects=effects, relationship_type=relationship_type) -#elif $identification_method.method == "llm_text_images" -text_with_images = text -text_with_images += "\n\n\n\nALSO ANALYZE THESE FIGURES CONVERTED TO TEXT:" -for figure in Path("$input_images").iterdir(): - text_image = TextGenerationAPI(model="$llm_models_images", api_key=litellm_api_key).convert_image( - image_path=figure, - text=text, - ) - text_with_images += f"\n\n{figure.name}:\n{text_image}" -relationships = TextGenerationAPI(model="$llm_models", api_key=litellm_api_key).find_relationships_in_text(text=text_with_images, chemicals=chemicals, effects=effects, relationship_type=relationship_type) -#end if write_relationships(relationships) write_chemicals(chemicals) @@ -96,19 +80,7 @@ else: raise ValueError(f"Unsupported relationship type: ${relationship_type}") chemicals = pd.read_csv("$chemicals", sep="\t")["chemical"].tolist() -#if $identification_method.method == "llm_text" relationships = TextGenerationAPI(model="$llm_models", api_key=litellm_api_key).find_relationships_in_text(text=text, chemicals=chemicals, effects=effects, relationship_type=relationship_type) -#elif $identification_method.method == "llm_text_images" -text_with_images = text -text_with_images += "\n\n\n\nALSO ANALYZE THESE FIGURES CONVERTED TO TEXT:" -for figure in Path("$input_images").iterdir(): - text_image = TextGenerationAPI(model="$llm_models_images", api_key=litellm_api_key).convert_image( - image_path=figure, - text=text, - ) - text_with_images += f"\n\n{figure.name}:\n{text_image}" -relationships = TextGenerationAPI(model="$llm_models", api_key=litellm_api_key).find_relationships_in_text(text=text_with_images, chemicals=chemicals, effects=effects, relationship_type=relationship_type) -#end if write_relationships(relationships) #elif $analysis_purpose.method == "chemicals" @@ -128,54 +100,20 @@ write_chemicals(chemicals) - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - @@ -198,13 +136,10 @@ write_chemicals(chemicals) - - - - - - - + + + + @@ -213,13 +148,10 @@ write_chemicals(chemicals) - - - - - - - + + + + From 49c2c797af55e9bf46aff785a3bd3fe1f5cb4252 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Tue, 5 May 2026 14:40:08 +0200 Subject: [PATCH 124/173] updated aoptk version --- tools/aoptk/macros.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/aoptk/macros.xml b/tools/aoptk/macros.xml index 9e35dcbc..efd016b5 100644 --- a/tools/aoptk/macros.xml +++ b/tools/aoptk/macros.xml @@ -1,5 +1,5 @@ - 0.3.2 + 0.4.3 0 From 96f74d478f7157b1ae81a548a4e7b7c166a5991d Mon Sep 17 00:00:00 2001 From: rdurnik Date: Tue, 5 May 2026 15:11:20 +0200 Subject: [PATCH 125/173] added yaml import --- tools/aoptk/aoptk_find_relationships_chemicals.xml | 1 + tools/aoptk/aoptk_normalize_chemicals.xml | 1 + tools/aoptk/aoptk_parse_pdf.xml | 1 + 3 files changed, 3 insertions(+) diff --git a/tools/aoptk/aoptk_find_relationships_chemicals.xml b/tools/aoptk/aoptk_find_relationships_chemicals.xml index a5ce4bd8..6cca0e25 100644 --- a/tools/aoptk/aoptk_find_relationships_chemicals.xml +++ b/tools/aoptk/aoptk_find_relationships_chemicals.xml @@ -6,6 +6,7 @@ + pyyaml + pyyaml + pyyaml Date: Tue, 5 May 2026 15:29:09 +0200 Subject: [PATCH 126/173] pmc ids need asyncio --- tools/aoptk/aoptk_query_literature.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tools/aoptk/aoptk_query_literature.xml b/tools/aoptk/aoptk_query_literature.xml index 780a97a1..347bd32b 100644 --- a/tools/aoptk/aoptk_query_literature.xml +++ b/tools/aoptk/aoptk_query_literature.xml @@ -16,6 +16,7 @@ +import asyncio from Bio import Entrez import os @@ -34,7 +35,7 @@ elif "${database}" == "pmc": from aoptk.literature.databases.pmc import PMC Entrez.email = email Entrez.api_key = api_key - ids = PMC("${query}", storage = "./", figure_storage="./figures").get_ids() + ids = asyncio.run(PMC("${query}", storage = "./", figure_storage="./figures").get_ids()) else: raise ValueError("Select valid database.") From 5ef1cca89c3a3350bd9a28466d4c78b42e6dfbfe Mon Sep 17 00:00:00 2001 From: rdurnik Date: Tue, 5 May 2026 15:29:19 +0200 Subject: [PATCH 127/173] pmc does not like empty strings Co-authored-by: Copilot --- tools/aoptk/aoptk_download_publication_data.xml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tools/aoptk/aoptk_download_publication_data.xml b/tools/aoptk/aoptk_download_publication_data.xml index c6ffb3cc..cd5ec9bf 100644 --- a/tools/aoptk/aoptk_download_publication_data.xml +++ b/tools/aoptk/aoptk_download_publication_data.xml @@ -27,7 +27,9 @@ with open("$input_file", "r") as f: from aoptk.literature.databases.pmc import PMC Entrez.email = email Entrez.api_key = api_key -database = PMC("", storage = "./", figure_storage="./figures") +database = PMC.__new__(PMC) +database.storage = "./" +database.figure_storage = "./figures" database.id_list = ids #elif $literature.database == "europepmc": From e69fc2d8ba2f2638cdfb1ef07fb394506917d1a7 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Tue, 5 May 2026 16:33:58 +0200 Subject: [PATCH 128/173] fixed europe pmc name --- tools/aoptk/aoptk_query_literature.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/aoptk/aoptk_query_literature.xml b/tools/aoptk/aoptk_query_literature.xml index 347bd32b..a326520b 100644 --- a/tools/aoptk/aoptk_query_literature.xml +++ b/tools/aoptk/aoptk_query_literature.xml @@ -50,7 +50,7 @@ with open(f"ids.txt", "w") as f: - + From 5d8d300eec16b2e304f86b0deefaaea72c207e6c Mon Sep 17 00:00:00 2001 From: rdurnik Date: Tue, 5 May 2026 16:34:14 +0200 Subject: [PATCH 129/173] fixed config and mesh terms errors Co-authored-by: Copilot --- tools/aoptk/aoptk_normalize_chemicals.xml | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/tools/aoptk/aoptk_normalize_chemicals.xml b/tools/aoptk/aoptk_normalize_chemicals.xml index f21be798..6938436b 100644 --- a/tools/aoptk/aoptk_normalize_chemicals.xml +++ b/tools/aoptk/aoptk_normalize_chemicals.xml @@ -26,8 +26,6 @@ from aoptk.normalization.pubchem_api import PubChemAPI from aoptk.normalization.mesh_terms import MeshTerms from aoptk.normalization.provide_mesh_term_dataframe_from_xml import ProvideMeshTermDataframeFromXML -chemicals = pd.read_csv("$input_file", sep="\t") - #if $normalization_method.method == "llm": litellm_config_file = os.environ.get("LITELLM_CONFIG_FILE") if not litellm_config_file: @@ -36,21 +34,27 @@ with open(litellm_config_file, "r") as f: config = yaml.safe_load(f) litellm_api_key = os.environ.get("LITELLM_API_KEY") - +chemicals = pd.read_csv("$chemicals", sep="\t") chemical_list = pd.read_csv("$chemical_list", sep="\t")["chemical"].tolist() chemicals["chemical"] = chemicals["chemical"].apply( lambda x: TextGenerationAPI(model="$llm_models", api_key=litellm_api_key).normalize_chemical(chemical=Chemical(x), chemical_list=[Chemical(chem) for chem in chemical_list]) ) + #elif $normalization_method.method == "pubchem" +chemicals = pd.read_csv("$input_file", sep="\t") chemicals["chemical"] = chemicals["chemical"].apply( lambda x: PubChemAPI().normalize_chemical(Chemical(x)) ) + #elif $normalization_method.method == "mesh" -mesh_terms_xml = pd.read_xml("$input_xml") -mesh_terms_df = ProvideMeshTermDataframeFromXML(mesh_terms_xml) +chemicals = pd.read_csv("$input_file", sep="\t") +mesh_terms_df = ProvideMeshTermDataframeFromXML(database_path="$input_xml").provide_normalization_dataframe() chemicals["chemical"] = chemicals["chemical"].apply( lambda x: MeshTerms(mesh_terms=mesh_terms_df).normalize_chemical(Chemical(x)) ) + +#else +raise ValueError("Select a valid normalization method.") #end if chemicals["heading"] = chemicals["chemical"].apply(lambda chem: chem.heading) From 4d614e3ce95880e93a3ff239558e11dbb131a88a Mon Sep 17 00:00:00 2001 From: rdurnik Date: Tue, 5 May 2026 16:34:28 +0200 Subject: [PATCH 130/173] removed figures from download --- tools/aoptk/aoptk_download_publication_data.xml | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/tools/aoptk/aoptk_download_publication_data.xml b/tools/aoptk/aoptk_download_publication_data.xml index cd5ec9bf..47b48ef9 100644 --- a/tools/aoptk/aoptk_download_publication_data.xml +++ b/tools/aoptk/aoptk_download_publication_data.xml @@ -79,7 +79,7 @@ raise ValueError("Select valid data type.") - + @@ -88,14 +88,13 @@ raise ValueError("Select valid data type.") - + - - + @@ -107,10 +106,6 @@ raise ValueError("Select valid data type.") literature['type_choice'] == 'publications' or literature['type_choice'] == 'abstracts' - - literature['type_choice'] == 'publications' - - literature['type_choice'] == 'pdfs' From 1a01f8d68b306bafa0421adca2cb071bfa9c2731 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Tue, 5 May 2026 16:34:53 +0200 Subject: [PATCH 131/173] fixed find relationships for relationships only --- tools/aoptk/aoptk_find_relationships_chemicals.xml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tools/aoptk/aoptk_find_relationships_chemicals.xml b/tools/aoptk/aoptk_find_relationships_chemicals.xml index 6cca0e25..9d6ff794 100644 --- a/tools/aoptk/aoptk_find_relationships_chemicals.xml +++ b/tools/aoptk/aoptk_find_relationships_chemicals.xml @@ -23,6 +23,7 @@ import yaml from aoptk.literature.pdf import PDF from aoptk.literature.pymupdf_parser import PymupdfParser from aoptk.effect import Effect +from aoptk.chemical import Chemical from aoptk.relationship_type import Causative from aoptk.relationship_type import Inhibitive from aoptk.text_generation_api import TextGenerationAPI @@ -72,15 +73,15 @@ write_relationships(relationships) write_chemicals(chemicals) #elif $analysis_purpose.method == "relationships" -effects = ["${effect}"] +effects = [Effect("${effect}")] if "$relationship_type" == "causative": - relationship_type = Causative + relationship_type = Causative() elif "$relationship_type" == "inhibitive": - relationship_type = Inhibitive + relationship_type = Inhibitive() else: raise ValueError(f"Unsupported relationship type: ${relationship_type}") -chemicals = pd.read_csv("$chemicals", sep="\t")["chemical"].tolist() +chemicals = [Chemical(chem) for chem in pd.read_csv("$chemicals", sep="\t")["chemical"].tolist()] relationships = TextGenerationAPI(model="$llm_models", api_key=litellm_api_key).find_relationships_in_text(text=text, chemicals=chemicals, effects=effects, relationship_type=relationship_type) write_relationships(relationships) From d72d776dc78cf9ee3ca87183adbe4ad76eb520e1 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Tue, 5 May 2026 16:53:30 +0200 Subject: [PATCH 132/173] added openyxl --- tools/aoptk/aoptk_finalize_output.xml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tools/aoptk/aoptk_finalize_output.xml b/tools/aoptk/aoptk_finalize_output.xml index bc78cc90..315991e5 100644 --- a/tools/aoptk/aoptk_finalize_output.xml +++ b/tools/aoptk/aoptk_finalize_output.xml @@ -6,6 +6,7 @@ + openpyxl - + - + From b7942082654cdd7597c3a831184d827c672cae32 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Tue, 5 May 2026 16:56:55 +0200 Subject: [PATCH 133/173] better output names download --- tools/aoptk/aoptk_download_publication_data.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/aoptk/aoptk_download_publication_data.xml b/tools/aoptk/aoptk_download_publication_data.xml index 47b48ef9..a29784b5 100644 --- a/tools/aoptk/aoptk_download_publication_data.xml +++ b/tools/aoptk/aoptk_download_publication_data.xml @@ -102,11 +102,11 @@ raise ValueError("Select valid data type.") - + literature['type_choice'] == 'publications' or literature['type_choice'] == 'abstracts' - + literature['type_choice'] == 'pdfs' From d182cd471130d3d03ae4c6539cb622ee4e7b9aa7 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Tue, 5 May 2026 17:36:00 +0200 Subject: [PATCH 134/173] lint, citation Co-authored-by: Copilot --- tools/aoptk/aoptk_chemical_identifier.xml | 1 + .../aoptk/aoptk_download_publication_data.xml | 23 ++++++++++++------- tools/aoptk/aoptk_finalize_output.xml | 4 ++++ .../aoptk_find_relationships_chemicals.xml | 4 ++++ tools/aoptk/aoptk_match_chemical_lists.xml | 4 ++++ tools/aoptk/aoptk_normalize_chemicals.xml | 12 ++++++---- tools/aoptk/aoptk_parse_pdf.xml | 4 ++++ tools/aoptk/aoptk_publication_tracker.xml | 1 + tools/aoptk/aoptk_query_literature.xml | 4 ++++ 9 files changed, 45 insertions(+), 12 deletions(-) diff --git a/tools/aoptk/aoptk_chemical_identifier.xml b/tools/aoptk/aoptk_chemical_identifier.xml index 9cf73ab4..b4a37f77 100644 --- a/tools/aoptk/aoptk_chemical_identifier.xml +++ b/tools/aoptk/aoptk_chemical_identifier.xml @@ -101,5 +101,6 @@ For the purposes of this publication, Chemical-Identifier was originally develop 10.18653/v1/w19-5034 10.1093/bioinformatics/btp163 + 10.5281/zenodo.20036704 \ No newline at end of file diff --git a/tools/aoptk/aoptk_download_publication_data.xml b/tools/aoptk/aoptk_download_publication_data.xml index a29784b5..e1a3863f 100644 --- a/tools/aoptk/aoptk_download_publication_data.xml +++ b/tools/aoptk/aoptk_download_publication_data.xml @@ -106,25 +106,28 @@ raise ValueError("Select valid data type.") literature['type_choice'] == 'publications' or literature['type_choice'] == 'abstracts' - + literature['type_choice'] == 'pdfs' - - + + + - - + + + - - + + + @@ -132,10 +135,14 @@ raise ValueError("Select valid data type.") + + 10.5281/zenodo.20036704 + + \ No newline at end of file diff --git a/tools/aoptk/aoptk_finalize_output.xml b/tools/aoptk/aoptk_finalize_output.xml index 315991e5..3a44864a 100644 --- a/tools/aoptk/aoptk_finalize_output.xml +++ b/tools/aoptk/aoptk_finalize_output.xml @@ -67,4 +67,8 @@ Tool to remove duplicates and finalize the output of relationship identification ]]> + + 10.5281/zenodo.20036704 + + \ No newline at end of file diff --git a/tools/aoptk/aoptk_find_relationships_chemicals.xml b/tools/aoptk/aoptk_find_relationships_chemicals.xml index 9d6ff794..322d40d7 100644 --- a/tools/aoptk/aoptk_find_relationships_chemicals.xml +++ b/tools/aoptk/aoptk_find_relationships_chemicals.xml @@ -174,4 +174,8 @@ This tool can infer chemicals from text first, or use a provided chemical list t ]]> + + 10.5281/zenodo.20036704 + + \ No newline at end of file diff --git a/tools/aoptk/aoptk_match_chemical_lists.xml b/tools/aoptk/aoptk_match_chemical_lists.xml index 3f027850..73fa256e 100644 --- a/tools/aoptk/aoptk_match_chemical_lists.xml +++ b/tools/aoptk/aoptk_match_chemical_lists.xml @@ -55,4 +55,8 @@ Tool to match chemical entities. ]]> + + 10.5281/zenodo.20036704 + + \ No newline at end of file diff --git a/tools/aoptk/aoptk_normalize_chemicals.xml b/tools/aoptk/aoptk_normalize_chemicals.xml index 6938436b..4a8ad3e9 100644 --- a/tools/aoptk/aoptk_normalize_chemicals.xml +++ b/tools/aoptk/aoptk_normalize_chemicals.xml @@ -99,29 +99,29 @@ chemicals.to_csv("normalized_chemicals.tsv", sep="\t", index=False) - + - + - + + + 10.5281/zenodo.20036704 + + \ No newline at end of file diff --git a/tools/aoptk/aoptk_parse_pdf.xml b/tools/aoptk/aoptk_parse_pdf.xml index 4c381a79..dd88c3a3 100644 --- a/tools/aoptk/aoptk_parse_pdf.xml +++ b/tools/aoptk/aoptk_parse_pdf.xml @@ -64,4 +64,8 @@ Tool to parse PDF files. API key for LLMs is optional and only needed for PDFs t ]]> + + 10.5281/zenodo.20036704 + + \ No newline at end of file diff --git a/tools/aoptk/aoptk_publication_tracker.xml b/tools/aoptk/aoptk_publication_tracker.xml index 8882dd8e..52dad97e 100644 --- a/tools/aoptk/aoptk_publication_tracker.xml +++ b/tools/aoptk/aoptk_publication_tracker.xml @@ -112,5 +112,6 @@ Publication-Tracker was originally developed for the purposes of this publicatio ]]> 10.1093/bioinformatics/btp163 + 10.5281/zenodo.20036704 \ No newline at end of file diff --git a/tools/aoptk/aoptk_query_literature.xml b/tools/aoptk/aoptk_query_literature.xml index a326520b..db7d29e1 100644 --- a/tools/aoptk/aoptk_query_literature.xml +++ b/tools/aoptk/aoptk_query_literature.xml @@ -76,4 +76,8 @@ Tool to download publication IDs. ]]> + + 10.5281/zenodo.20036704 + + \ No newline at end of file From 74596c0101317468fa70304bce2003dc32251152 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Wed, 6 May 2026 09:32:15 +0200 Subject: [PATCH 135/173] specified version --- tools/aoptk/aoptk_finalize_output.xml | 2 +- tools/aoptk/aoptk_find_relationships_chemicals.xml | 2 +- tools/aoptk/aoptk_normalize_chemicals.xml | 2 +- tools/aoptk/aoptk_parse_pdf.xml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tools/aoptk/aoptk_finalize_output.xml b/tools/aoptk/aoptk_finalize_output.xml index 3a44864a..3c258902 100644 --- a/tools/aoptk/aoptk_finalize_output.xml +++ b/tools/aoptk/aoptk_finalize_output.xml @@ -6,7 +6,7 @@ - openpyxl + openpyxl - pyyaml + pyyaml - pyyaml + pyyaml - pyyaml + pyyaml Date: Thu, 7 May 2026 08:02:15 +0200 Subject: [PATCH 136/173] small bugfixes in wrappers --- .../aoptk/aoptk_download_publication_data.xml | 20 +++++++++---------- .../aoptk_find_relationships_chemicals.xml | 1 - tools/aoptk/aoptk_normalize_chemicals.xml | 1 - tools/aoptk/aoptk_parse_pdf.xml | 15 +++++++------- tools/aoptk/macros.xml | 1 + 5 files changed, 19 insertions(+), 19 deletions(-) diff --git a/tools/aoptk/aoptk_download_publication_data.xml b/tools/aoptk/aoptk_download_publication_data.xml index e1a3863f..cb30b21f 100644 --- a/tools/aoptk/aoptk_download_publication_data.xml +++ b/tools/aoptk/aoptk_download_publication_data.xml @@ -6,6 +6,8 @@ + + - pyyaml - pyyaml - pyyaml aoptk + pyyaml From b8bff3a7a4a4389ec487834f104c4884657e5893 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Thu, 7 May 2026 14:55:32 +0200 Subject: [PATCH 137/173] reorder --- tools/aoptk/aoptk_find_relationships_chemicals.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/aoptk/aoptk_find_relationships_chemicals.xml b/tools/aoptk/aoptk_find_relationships_chemicals.xml index 84d8b2c2..37151317 100644 --- a/tools/aoptk/aoptk_find_relationships_chemicals.xml +++ b/tools/aoptk/aoptk_find_relationships_chemicals.xml @@ -102,16 +102,16 @@ write_chemicals(chemicals) - + - + From bbfd56b8a08d0ad823ffd96a63edfe71f57a0aab Mon Sep 17 00:00:00 2001 From: rdurnik Date: Thu, 7 May 2026 15:01:48 +0200 Subject: [PATCH 138/173] removed dot --- tools/aoptk/macros.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/aoptk/macros.xml b/tools/aoptk/macros.xml index cdad7c8d..10807ecc 100644 --- a/tools/aoptk/macros.xml +++ b/tools/aoptk/macros.xml @@ -98,7 +98,7 @@ - + From e0f11c91dd352e47cf56cf2fc5e06d9c4c77fef0 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Thu, 7 May 2026 15:03:27 +0200 Subject: [PATCH 139/173] removed model --- tools/aoptk/macros.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/aoptk/macros.xml b/tools/aoptk/macros.xml index 10807ecc..a0b4f48a 100644 --- a/tools/aoptk/macros.xml +++ b/tools/aoptk/macros.xml @@ -98,7 +98,7 @@ - + @@ -106,7 +106,7 @@ - + From b9cbfca4aaf8bda51859d21f12c0aa94df88b9da Mon Sep 17 00:00:00 2001 From: rdurnik Date: Thu, 7 May 2026 15:44:05 +0200 Subject: [PATCH 140/173] typo --- tools/aoptk/macros.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/aoptk/macros.xml b/tools/aoptk/macros.xml index a0b4f48a..7f0b66ee 100644 --- a/tools/aoptk/macros.xml +++ b/tools/aoptk/macros.xml @@ -98,7 +98,7 @@ - + From 6de90501a9ed29b6c3b912d7f7383523bc497ab4 Mon Sep 17 00:00:00 2001 From: Michaela Bodova <505814@muni.cz> Date: Tue, 12 May 2026 09:30:40 +0200 Subject: [PATCH 141/173] Fixing test paths --- tools/aoptk/aoptk_download_publication_data.xml | 6 +++--- tools/aoptk/aoptk_finalize_output.xml | 4 ++-- .../aoptk/aoptk_find_relationships_chemicals.xml | 8 ++++---- tools/aoptk/aoptk_match_chemical_lists.xml | 6 +++--- tools/aoptk/aoptk_normalize_chemicals.xml | 16 ++++++++-------- tools/aoptk/aoptk_parse_pdf.xml | 2 +- 6 files changed, 21 insertions(+), 21 deletions(-) diff --git a/tools/aoptk/aoptk_download_publication_data.xml b/tools/aoptk/aoptk_download_publication_data.xml index cb30b21f..c4ad481c 100644 --- a/tools/aoptk/aoptk_download_publication_data.xml +++ b/tools/aoptk/aoptk_download_publication_data.xml @@ -116,19 +116,19 @@ raise ValueError("Select valid data type.") - + - + - + diff --git a/tools/aoptk/aoptk_finalize_output.xml b/tools/aoptk/aoptk_finalize_output.xml index 3c258902..dd7910f7 100644 --- a/tools/aoptk/aoptk_finalize_output.xml +++ b/tools/aoptk/aoptk_finalize_output.xml @@ -54,8 +54,8 @@ final_output.to_excel("./final_output.xlsx", index=False) - - + + diff --git a/tools/aoptk/aoptk_find_relationships_chemicals.xml b/tools/aoptk/aoptk_find_relationships_chemicals.xml index 37151317..2d8229be 100644 --- a/tools/aoptk/aoptk_find_relationships_chemicals.xml +++ b/tools/aoptk/aoptk_find_relationships_chemicals.xml @@ -138,7 +138,7 @@ write_chemicals(chemicals) - + @@ -148,9 +148,9 @@ write_chemicals(chemicals) - + - + @@ -160,7 +160,7 @@ write_chemicals(chemicals) - + diff --git a/tools/aoptk/aoptk_match_chemical_lists.xml b/tools/aoptk/aoptk_match_chemical_lists.xml index 73fa256e..0ef11186 100644 --- a/tools/aoptk/aoptk_match_chemical_lists.xml +++ b/tools/aoptk/aoptk_match_chemical_lists.xml @@ -41,9 +41,9 @@ merged_files.to_csv("merged_chemicals.tsv", sep="\t", index=False) - - - + + + diff --git a/tools/aoptk/aoptk_normalize_chemicals.xml b/tools/aoptk/aoptk_normalize_chemicals.xml index 9022516b..e06a4c5f 100644 --- a/tools/aoptk/aoptk_normalize_chemicals.xml +++ b/tools/aoptk/aoptk_normalize_chemicals.xml @@ -96,25 +96,25 @@ chemicals.to_csv("normalized_chemicals.tsv", sep="\t", index=False) - - + + - + - + - + - - + + - + diff --git a/tools/aoptk/aoptk_parse_pdf.xml b/tools/aoptk/aoptk_parse_pdf.xml index 7a51cac8..e62dfb1d 100644 --- a/tools/aoptk/aoptk_parse_pdf.xml +++ b/tools/aoptk/aoptk_parse_pdf.xml @@ -52,7 +52,7 @@ else: - + From a1f0ea5417fa2b286e6d714b9fd7d0d7c56205a5 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Thu, 14 May 2026 16:43:15 +0200 Subject: [PATCH 142/173] query name change --- tools/aoptk/aoptk_chemical_identifier.xml | 8 ++++---- tools/aoptk/aoptk_publication_tracker.xml | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/tools/aoptk/aoptk_chemical_identifier.xml b/tools/aoptk/aoptk_chemical_identifier.xml index b4a37f77..dbdc82c9 100644 --- a/tools/aoptk/aoptk_chemical_identifier.xml +++ b/tools/aoptk/aoptk_chemical_identifier.xml @@ -11,7 +11,7 @@ - + @@ -35,13 +35,13 @@ - + - + diff --git a/tools/aoptk/aoptk_publication_tracker.xml b/tools/aoptk/aoptk_publication_tracker.xml index 52dad97e..b4ca8179 100644 --- a/tools/aoptk/aoptk_publication_tracker.xml +++ b/tools/aoptk/aoptk_publication_tracker.xml @@ -16,14 +16,14 @@ --read 'read.${read.ext}' --master 'master.${master.ext}' --code '$code' - --query '$query' + --query '$search_term' --database '$literature_database' --outdir . \${EMAIL:+--email \$EMAIL} ]]> - + @@ -48,7 +48,7 @@ - + @@ -56,7 +56,7 @@ - + @@ -64,7 +64,7 @@ - + From c857b47ef4b9800071c7a840e6ac6df339bf90ca Mon Sep 17 00:00:00 2001 From: rdurnik Date: Thu, 14 May 2026 16:43:43 +0200 Subject: [PATCH 143/173] changes to match the latest aoptk version --- .../aoptk/aoptk_download_publication_data.xml | 37 +++--- tools/aoptk/aoptk_parse_pdf.xml | 2 +- tools/aoptk/aoptk_query_literature.xml | 113 +++++++++++++----- tools/aoptk/macros.xml | 26 +++- 4 files changed, 128 insertions(+), 50 deletions(-) diff --git a/tools/aoptk/aoptk_download_publication_data.xml b/tools/aoptk/aoptk_download_publication_data.xml index c4ad481c..acc0e9d6 100644 --- a/tools/aoptk/aoptk_download_publication_data.xml +++ b/tools/aoptk/aoptk_download_publication_data.xml @@ -18,30 +18,27 @@ import os from Bio import Entrez +from aoptk.literature.databases.pmc import PMC +from aoptk.literature.databases.europepmc import EuropePMC +from aoptk.literature.databases.pubmed import PubMed +from aoptk.literature.id import ID with open("$input_file", "r") as f: - ids = [line.strip() for line in f.readlines()] + raw_ids = [line.strip() for line in f.readlines()] + ids = [ID(id) for id in raw_ids] Entrez.email = os.environ.get("EMAIL") or None Entrez.api_key = os.environ.get("NCBI_API_KEY") or None #if $literature.database == "pmc": -from aoptk.literature.databases.pmc import PMC -database = PMC.__new__(PMC) -database.storage = "./" -database.figure_storage = "./figures" -database.id_list = ids +database = PMC(storage = "./", figure_storage="./figures") #elif $literature.database == "europepmc": -from aoptk.literature.databases.europepmc import EuropePMC -database = EuropePMC("", storage = "./", figure_storage="./figures") -database.id_list = ids +database = EuropePMC(storage = "./", figure_storage="./figures") #elif $literature.database == "pubmed": -from aoptk.literature.databases.pubmed import PubMed -database = PubMed("") -database.id_list = ids +database = PubMed() #else: raise ValueError("Select valid database.") @@ -49,7 +46,7 @@ raise ValueError("Select valid database.") #if $literature.type_choice == "publications": -publications = database.get_publications() +publications = database.get_publications(ids=ids, download_figures_enabled=False) for publication in publications: if publication is not None and publication.full_text is not None: print(publication.figures) @@ -57,13 +54,13 @@ for publication in publications: f.write(publication.full_text) #elif $literature.type_choice == "abstracts": -abstracts = database.get_abstracts() +abstracts = database.get_abstracts(ids=ids) for abstract in abstracts: with open(f"{abstract.id}.txt", "w") as f: f.write(abstract.text) #elif $literature.type_choice == "pdfs": -pdfs = database.get_pdfs() +pdfs = database.get_pdfs(ids=ids) #else: raise ValueError("Select valid data type.") @@ -137,7 +134,15 @@ raise ValueError("Select valid data type.") Download Publication Data ========================= -Tools to download publication data - full text publications (including abstracts) and figures, abstracts, or PDFs. +Tools to download publication data - full text publications (including abstracts), abstracts, or PDFs. + +NCBI settings +------------- +To speed up downloads from NCBI databases (PMC and PubMed), you may provide your personal API key. This information is treated as sensitive and is securely stored within the Galaxy credentials system. + +- Instructions for obtaining an API key are available here: https://support.nlm.nih.gov/kbArticle/?pn=KA-05317 + +- To comply with NCBI usage guidelines, you may also provide your email address. ]]> diff --git a/tools/aoptk/aoptk_parse_pdf.xml b/tools/aoptk/aoptk_parse_pdf.xml index e62dfb1d..ab126256 100644 --- a/tools/aoptk/aoptk_parse_pdf.xml +++ b/tools/aoptk/aoptk_parse_pdf.xml @@ -33,7 +33,7 @@ else: publication_id = os.path.splitext("$input_file.element_identifier")[0] if "${input_file.ext}" == "pdf": - text = PymupdfParser([PDF("$input_file")], text_generation=text_generation).get_publications()[0].full_text + text = PymupdfParser([PDF("$input_file")], text_generation=text_generation).get_publications(download_figures_enabled=False)[0].full_text with open(f"{publication_id}.txt", "w") as f: f.write(text) diff --git a/tools/aoptk/aoptk_query_literature.xml b/tools/aoptk/aoptk_query_literature.xml index db7d29e1..db6d34bf 100644 --- a/tools/aoptk/aoptk_query_literature.xml +++ b/tools/aoptk/aoptk_query_literature.xml @@ -19,26 +19,43 @@ import asyncio from Bio import Entrez import os +from aoptk.literature.databases.pubmed import PubMed +from aoptk.literature.databases.europepmc import EuropePMC +from aoptk.literature.databases.pmc import PMC +from aoptk.literature.query import Query email = os.environ.get("EMAIL") or None api_key = os.environ.get("NCBI_API_KEY") or None -if "${database}" == "pubmed": - from aoptk.literature.databases.pubmed import PubMed - Entrez.email = email - Entrez.api_key = api_key - ids = PubMed("${query}").get_ids() -elif "${database}" == "europepmc": - from aoptk.literature.databases.europepmc import EuropePMC - ids = EuropePMC("${query}", storage = "./", figure_storage="./figures").get_ids() -elif "${database}" == "pmc": - from aoptk.literature.databases.pmc import PMC - Entrez.email = email - Entrez.api_key = api_key - ids = asyncio.run(PMC("${query}", storage = "./", figure_storage="./figures").get_ids()) - -else: - raise ValueError("Select valid database.") +#if $literature.preprint_mode == "only": +only_preprint = True +#elif $literature.preprint_mode == "exclude": +only_preprint = False +#else: +only_preprint = False +#end if + +#if $literature.licensing == "all": +licensing = None +#else: +licensing = "$literature.licensing" +#end if + +query = Query(search_term="$search_term", full_text_subset=$literature.full_text_subset, only_preprint=only_preprint, licensing=licensing) + +#if $literature.database == "pubmed": +Entrez.email = email +Entrez.api_key = api_key +ids = PubMed(query).get_ids() +#elif $literature.database == "europepmc": +ids = EuropePMC(query, storage = "./", figure_storage="./figures").get_ids() +#elif $literature.database == "pmc": +Entrez.email = email +Entrez.api_key = api_key +ids = asyncio.run(PMC(query, storage = "./", figure_storage="./figures").get_ids()) +#else: +raise ValueError("Select valid database.") +#end if with open(f"ids.txt", "w") as f: for id in ids: @@ -48,31 +65,65 @@ with open(f"ids.txt", "w") as f: - - - - - - + + + + + + + + + + + + + + + + + + + + - + - - - - - + + + + + diff --git a/tools/aoptk/macros.xml b/tools/aoptk/macros.xml index 7f0b66ee..a9e239a3 100644 --- a/tools/aoptk/macros.xml +++ b/tools/aoptk/macros.xml @@ -27,8 +27,8 @@ - - + + @@ -192,6 +192,28 @@ + + + + + + + + + + + + + + + + + + + + + + From 329fb94979f8ee54cd9c60ef2cbe814c30e4688d Mon Sep 17 00:00:00 2001 From: rdurnik Date: Thu, 14 May 2026 17:09:43 +0200 Subject: [PATCH 144/173] modified descs --- tools/aoptk/aoptk_find_relationships_chemicals.xml | 11 +++++++++-- tools/aoptk/aoptk_match_chemical_lists.xml | 2 +- tools/aoptk/aoptk_parse_pdf.xml | 2 +- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/tools/aoptk/aoptk_find_relationships_chemicals.xml b/tools/aoptk/aoptk_find_relationships_chemicals.xml index 2d8229be..ebde091a 100644 --- a/tools/aoptk/aoptk_find_relationships_chemicals.xml +++ b/tools/aoptk/aoptk_find_relationships_chemicals.xml @@ -167,9 +167,16 @@ write_chemicals(chemicals) diff --git a/tools/aoptk/aoptk_match_chemical_lists.xml b/tools/aoptk/aoptk_match_chemical_lists.xml index 0ef11186..e3c6b3bd 100644 --- a/tools/aoptk/aoptk_match_chemical_lists.xml +++ b/tools/aoptk/aoptk_match_chemical_lists.xml @@ -51,7 +51,7 @@ merged_files.to_csv("merged_chemicals.tsv", sep="\t", index=False) Chemical Matching =================== -Tool to match chemical entities. +Tool to simply match two chemical lists based on heading column. ]]> diff --git a/tools/aoptk/aoptk_parse_pdf.xml b/tools/aoptk/aoptk_parse_pdf.xml index ab126256..7fe613a2 100644 --- a/tools/aoptk/aoptk_parse_pdf.xml +++ b/tools/aoptk/aoptk_parse_pdf.xml @@ -61,7 +61,7 @@ else: Parse PDF =================== -Tool to parse PDF files. API key for LLMs is optional and only needed for PDFs that are scanned images (usually old PDF files). +Tool to parse PDF files. ]]> From 5214698ff4bce5419b1fd18817b6a79015c3977e Mon Sep 17 00:00:00 2001 From: rdurnik Date: Thu, 14 May 2026 17:10:49 +0200 Subject: [PATCH 145/173] modified descs --- tools/aoptk/aoptk_find_relationships_chemicals.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/aoptk/aoptk_find_relationships_chemicals.xml b/tools/aoptk/aoptk_find_relationships_chemicals.xml index ebde091a..f2bf70cb 100644 --- a/tools/aoptk/aoptk_find_relationships_chemicals.xml +++ b/tools/aoptk/aoptk_find_relationships_chemicals.xml @@ -174,7 +174,7 @@ This tool extracts chemical entities from text or leverages a user-defined chemi Additional Information ---------------------- -- **Effect**: Simply specify the effect you want to analyze. For example, "liver fibrosis". +- **Effect**: Simply specify the effect you want to analyze (e.g., liver fibrosis). - **Relationship Type**: Choose whether you want to identify causative (e.g., chemical causes / does not cause liver fibrosis) or inhibitive relationships (e.g., chemical inhibits / does not inhibit gap juction intracellular communication). - **Prompt specification**: The LLM is preconfigured to identify relationships between chemicals and biological effects. You can further refine the analysis by specifying additional constraints—such as limiting results to a particular measurement method or experimental setup. Simply describe your requirements in the prompt, and the model will try to tailor the output accordingly. From a87762907481aac43a05772dbb1eaff8310df53d Mon Sep 17 00:00:00 2001 From: rdurnik Date: Mon, 18 May 2026 10:41:28 +0200 Subject: [PATCH 146/173] finalize output should work also without having to normalize the output first --- tools/aoptk/aoptk_finalize_output.xml | 18 +++++++++++++----- tools/aoptk/test-data/output_no_heading.tsv | 5 +++++ 2 files changed, 18 insertions(+), 5 deletions(-) create mode 100644 tools/aoptk/test-data/output_no_heading.tsv diff --git a/tools/aoptk/aoptk_finalize_output.xml b/tools/aoptk/aoptk_finalize_output.xml index dd7910f7..fbc50aa9 100644 --- a/tools/aoptk/aoptk_finalize_output.xml +++ b/tools/aoptk/aoptk_finalize_output.xml @@ -25,18 +25,22 @@ output = pd.read_csv("$input_file", sep="\t") file_name = output["effect"].unique()[0].strip().lower().replace(" ", "_") +group_column = "heading" if "heading" in output.columns else "chemical" +if group_column not in output.columns: + raise ValueError("Input must contain either a heading or chemical column.") + output["relationship"] = output["relationship"].astype(str).str.strip().str.lower() relationships = sorted(output["relationship"].unique()) -final_output = output[["heading"]].drop_duplicates() +final_output = output[[group_column]].drop_duplicates() for relationship in relationships: subset = output[output["relationship"] == relationship] - result = subset.groupby("heading", as_index=False, dropna=False).agg( + result = subset.groupby(group_column, as_index=False, dropna=False).agg( hits=("id", "size"), ids=("id", join_unique), ) result.rename(columns={"hits": f"{relationship}", "ids": f"{relationship} IDs"}, inplace=True) - final_output = final_output.merge(result, on="heading", how="left") + final_output = final_output.merge(result, on=group_column, how="left") final_output.to_excel("./final_output.xlsx", index=False) @@ -45,7 +49,7 @@ final_output.to_excel("./final_output.xlsx", index=False) - + @@ -54,7 +58,11 @@ final_output.to_excel("./final_output.xlsx", index=False) - + + + + + diff --git a/tools/aoptk/test-data/output_no_heading.tsv b/tools/aoptk/test-data/output_no_heading.tsv new file mode 100644 index 00000000..20ab9741 --- /dev/null +++ b/tools/aoptk/test-data/output_no_heading.tsv @@ -0,0 +1,5 @@ +id chemical effect relationship +11 acetaminophen liver fibrosis positive +11 thioacetamide liver fibrosis positive +12 paracetamol liver fibrosis negative +26 acetaminophen liver fibrosis negative From 6b5d774f95283a97b49f23363653fa1ad40a10eb Mon Sep 17 00:00:00 2001 From: rdurnik Date: Thu, 11 Jun 2026 16:49:26 +0200 Subject: [PATCH 147/173] updated download to match automatic write to file --- tools/aoptk/aoptk_download_publication_data.xml | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/tools/aoptk/aoptk_download_publication_data.xml b/tools/aoptk/aoptk_download_publication_data.xml index acc0e9d6..30d37d56 100644 --- a/tools/aoptk/aoptk_download_publication_data.xml +++ b/tools/aoptk/aoptk_download_publication_data.xml @@ -38,7 +38,7 @@ database = PMC(storage = "./", figure_storage="./figures") database = EuropePMC(storage = "./", figure_storage="./figures") #elif $literature.database == "pubmed": -database = PubMed() +database = PubMed(storage = "./") #else: raise ValueError("Select valid database.") @@ -47,17 +47,9 @@ raise ValueError("Select valid database.") #if $literature.type_choice == "publications": publications = database.get_publications(ids=ids, download_figures_enabled=False) -for publication in publications: - if publication is not None and publication.full_text is not None: - print(publication.figures) - with open(f"{publication.id}.txt", "w") as f: - f.write(publication.full_text) #elif $literature.type_choice == "abstracts": abstracts = database.get_abstracts(ids=ids) -for abstract in abstracts: - with open(f"{abstract.id}.txt", "w") as f: - f.write(abstract.text) #elif $literature.type_choice == "pdfs": pdfs = database.get_pdfs(ids=ids) From 3612c559148b7148df96d95c9c7ec19052550a88 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Thu, 11 Jun 2026 16:51:23 +0200 Subject: [PATCH 148/173] version update --- tools/aoptk/macros.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/aoptk/macros.xml b/tools/aoptk/macros.xml index a9e239a3..05ecfdaf 100644 --- a/tools/aoptk/macros.xml +++ b/tools/aoptk/macros.xml @@ -1,5 +1,5 @@ - 0.4.3 + 0.5.0 0 From 23e00e34278bdbca9665bff287820025a854a82d Mon Sep 17 00:00:00 2001 From: rdurnik Date: Thu, 11 Jun 2026 17:02:39 +0200 Subject: [PATCH 149/173] changed to relationship types --- .../aoptk/aoptk_find_relationships_chemicals.xml | 15 ++++++++------- tools/aoptk/macros.xml | 4 ++-- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/tools/aoptk/aoptk_find_relationships_chemicals.xml b/tools/aoptk/aoptk_find_relationships_chemicals.xml index f2bf70cb..92e8020b 100644 --- a/tools/aoptk/aoptk_find_relationships_chemicals.xml +++ b/tools/aoptk/aoptk_find_relationships_chemicals.xml @@ -73,15 +73,16 @@ write_chemicals(chemicals) #elif $analysis_purpose.method == "relationships" effects = [Effect("${effect}")] -if "$relationship_type" == "causative": - relationship_type = Causative() -elif "$relationship_type" == "inhibitive": - relationship_type = Inhibitive() +relationship_types = [] +if "$relationship_types" == "causative": + relationship_types.append(Causative()) +elif "$relationship_types" == "inhibitive": + relationship_types.append(Inhibitive()) else: raise ValueError(f"Unsupported relationship type: ${relationship_type}") chemicals = [Chemical(chem) for chem in pd.read_csv("$chemicals", sep="\t")["chemical"].tolist()] -relationships = TextGenerationAPI(model="$llm_models", api_key=litellm_api_key).find_relationships_in_text(text=text, chemicals=chemicals, effects=effects, relationship_type=relationship_type) +relationships = TextGenerationAPI(model="$llm_models", api_key=litellm_api_key).find_relationships_in_text(text=text, chemicals=chemicals, effects=effects, relationship_types=relationship_types) write_relationships(relationships) #elif $analysis_purpose.method == "chemicals" @@ -104,7 +105,7 @@ write_chemicals(chemicals) - + @@ -112,7 +113,7 @@ write_chemicals(chemicals) - + diff --git a/tools/aoptk/macros.xml b/tools/aoptk/macros.xml index 05ecfdaf..640a54cb 100644 --- a/tools/aoptk/macros.xml +++ b/tools/aoptk/macros.xml @@ -186,8 +186,8 @@ - - + + From e1900b0730a842646e46607205a1a23bce3d8916 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Thu, 11 Jun 2026 17:10:03 +0200 Subject: [PATCH 150/173] changed to multiple effects --- tools/aoptk/aoptk_find_relationships_chemicals.xml | 14 +++++++------- tools/aoptk/macros.xml | 10 ++++++---- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/tools/aoptk/aoptk_find_relationships_chemicals.xml b/tools/aoptk/aoptk_find_relationships_chemicals.xml index 92e8020b..0093d4a4 100644 --- a/tools/aoptk/aoptk_find_relationships_chemicals.xml +++ b/tools/aoptk/aoptk_find_relationships_chemicals.xml @@ -58,11 +58,11 @@ else: publication_id = os.path.splitext("$input_file.element_identifier")[0] #if $analysis_purpose.method == "relationships_chemicals" -effects = [Effect("${effect}")] -if "$relationship_type" == "causative": - relationship_type = Causative() -elif "$relationship_type" == "inhibitive": - relationship_type = Inhibitive() +effects = [Effect(effect) for effect in $effects] +if "$relationship_types" == "causative": + relationship_types.append(Causative()) +elif "$relationship_types" == "inhibitive": + relationship_types.append(Inhibitive()) else: raise ValueError(f"Unsupported relationship type: ${relationship_type}") @@ -103,7 +103,7 @@ write_chemicals(chemicals) - + @@ -111,7 +111,7 @@ write_chemicals(chemicals) - + diff --git a/tools/aoptk/macros.xml b/tools/aoptk/macros.xml index 640a54cb..f0586119 100644 --- a/tools/aoptk/macros.xml +++ b/tools/aoptk/macros.xml @@ -180,10 +180,12 @@ - - - [0-9a-zA-Z]+ - + + + + [0-9a-zA-Z]+ + + From a34636eec60f0d8cfb124802cca6fb5c6ab36379 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Thu, 11 Jun 2026 17:10:51 +0200 Subject: [PATCH 151/173] added new options for pmc and europepmc download --- tools/aoptk/aoptk_download_publication_data.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tools/aoptk/aoptk_download_publication_data.xml b/tools/aoptk/aoptk_download_publication_data.xml index 30d37d56..9450974e 100644 --- a/tools/aoptk/aoptk_download_publication_data.xml +++ b/tools/aoptk/aoptk_download_publication_data.xml @@ -79,11 +79,13 @@ raise ValueError("Select valid data type.") + + From bb4d1494608a7722524b11820be52f40529123cf Mon Sep 17 00:00:00 2001 From: rdurnik Date: Fri, 12 Jun 2026 12:05:41 +0200 Subject: [PATCH 152/173] outputs test file update --- tools/aoptk/test-data/output.tsv | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/tools/aoptk/test-data/output.tsv b/tools/aoptk/test-data/output.tsv index f0d08865..74a4b310 100644 --- a/tools/aoptk/test-data/output.tsv +++ b/tools/aoptk/test-data/output.tsv @@ -1,5 +1,8 @@ id chemical effect relationship heading -11 acetaminophen liver fibrosis positive acetaminophen -11 thioacetamide liver fibrosis positive thioacetamide -12 paracetamol liver fibrosis negative acetaminophen -26 acetaminophen liver fibrosis negative acetaminophen +11 acetaminophen liver fibrosis inhibits acetaminophen +11 thioacetamide liver fibrosis inhibits thioacetamide +12 paracetamol liver fibrosis does not inhibit acetaminophen +26 acetaminophen liver fibrosis does not inhibit acetaminophen +35 methotrexate cell death causes methotrexate +98 acetaminophen cell death does not cause acetaminophen +26 acetaminophen liver fibrosis does not cause acetaminophen \ No newline at end of file From 7f5a8caba7861a836868c584bffda4326e923a2e Mon Sep 17 00:00:00 2001 From: rdurnik Date: Fri, 12 Jun 2026 12:05:49 +0200 Subject: [PATCH 153/173] find chemical relationships fix --- .../aoptk_find_relationships_chemicals.xml | 54 ++++++++++++++----- 1 file changed, 42 insertions(+), 12 deletions(-) diff --git a/tools/aoptk/aoptk_find_relationships_chemicals.xml b/tools/aoptk/aoptk_find_relationships_chemicals.xml index 0093d4a4..65cd181c 100644 --- a/tools/aoptk/aoptk_find_relationships_chemicals.xml +++ b/tools/aoptk/aoptk_find_relationships_chemicals.xml @@ -23,8 +23,8 @@ from aoptk.literature.pdf import PDF from aoptk.literature.pymupdf_parser import PymupdfParser from aoptk.effect import Effect from aoptk.chemical import Chemical -from aoptk.relationship_type import Causative -from aoptk.relationship_type import Inhibitive +from aoptk.relationships.relationship_type import Causative +from aoptk.relationships.relationship_type import Inhibitive from aoptk.text_generation_api import TextGenerationAPI litellm_config_file = os.environ.get("LITELLM_CONFIG_FILE") @@ -47,31 +47,48 @@ def write_chemicals(chemicals): chemical_name = chemical.name if hasattr(chemical, "name") else chemical f_out.write(f"{chemical_name}\n") -if "${input_file.ext}" == "pdf": +#if $analysis_purpose.method == "relationships_chemicals" +input_file_ext = "$input_file.ext" +if input_file_ext == "pdf": text = PymupdfParser([PDF("$input_file")], text_generation=TextGenerationAPI(model="qwen3.5", api_key=litellm_api_key)).get_publications()[0].full_text -elif "${input_file.ext}" == "txt": +elif input_file_ext == "txt": with open("$input_file", "r") as f_in: text = f_in.read() else: - raise ValueError(f"Unsupported input file format: ${input_file.ext}") + raise ValueError(f"Unsupported input file format: {input_file_ext}") publication_id = os.path.splitext("$input_file.element_identifier")[0] -#if $analysis_purpose.method == "relationships_chemicals" -effects = [Effect(effect) for effect in $effects] +effects = [] +#for $effect in $effects +effects.append(Effect("$effect.effect")) +#end for + +relationship_types = [] if "$relationship_types" == "causative": relationship_types.append(Causative()) elif "$relationship_types" == "inhibitive": relationship_types.append(Inhibitive()) else: - raise ValueError(f"Unsupported relationship type: ${relationship_type}") + raise ValueError(f"Unsupported relationship type: $relationship_types") chemicals = TextGenerationAPI(model="$llm_models", api_key=litellm_api_key).find_chemicals(text) -relationships = TextGenerationAPI(model="$llm_models", api_key=litellm_api_key).find_relationships_in_text(text=text, chemicals=chemicals, effects=effects, relationship_type=relationship_type) +relationships = TextGenerationAPI(model="$llm_models", api_key=litellm_api_key).find_relationships_in_text(text=text, chemicals=chemicals, effects=effects, relationship_types=relationship_types) write_relationships(relationships) write_chemicals(chemicals) #elif $analysis_purpose.method == "relationships" +input_file_ext = "$input_file.ext" +if input_file_ext == "pdf": + text = PymupdfParser([PDF("$input_file")], text_generation=TextGenerationAPI(model="qwen3.5", api_key=litellm_api_key)).get_publications()[0].full_text +elif input_file_ext == "txt": + with open("$input_file", "r") as f_in: + text = f_in.read() +else: + raise ValueError(f"Unsupported input file format: {input_file_ext}") + +publication_id = os.path.splitext("$input_file.element_identifier")[0] + effects = [Effect("${effect}")] relationship_types = [] if "$relationship_types" == "causative": @@ -79,13 +96,24 @@ if "$relationship_types" == "causative": elif "$relationship_types" == "inhibitive": relationship_types.append(Inhibitive()) else: - raise ValueError(f"Unsupported relationship type: ${relationship_type}") + raise ValueError(f"Unsupported relationship type: $relationship_types") chemicals = [Chemical(chem) for chem in pd.read_csv("$chemicals", sep="\t")["chemical"].tolist()] relationships = TextGenerationAPI(model="$llm_models", api_key=litellm_api_key).find_relationships_in_text(text=text, chemicals=chemicals, effects=effects, relationship_types=relationship_types) write_relationships(relationships) #elif $analysis_purpose.method == "chemicals" +input_file_ext = "$input_file.ext" +if input_file_ext == "pdf": + text = PymupdfParser([PDF("$input_file")], text_generation=TextGenerationAPI(model="qwen3.5", api_key=litellm_api_key)).get_publications()[0].full_text +elif input_file_ext == "txt": + with open("$input_file", "r") as f_in: + text = f_in.read() +else: + raise ValueError(f"Unsupported input file format: {input_file_ext}") + +publication_id = os.path.splitext("$input_file.element_identifier")[0] + chemicals = TextGenerationAPI(model="$llm_models", api_key=litellm_api_key).find_chemicals(text) write_chemicals(chemicals) #end if @@ -140,8 +168,10 @@ write_chemicals(chemicals) - - + + + + From 719a8c7b415d6b5bfc96448b44f3bd3c165ca150 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Fri, 12 Jun 2026 13:06:54 +0200 Subject: [PATCH 154/173] config error fixes --- .../aoptk_find_relationships_chemicals.xml | 29 +++-------------- tools/aoptk/aoptk_parse_pdf.xml | 5 +-- tools/aoptk/aoptk_query_literature.xml | 31 +++++++++++++------ 3 files changed, 30 insertions(+), 35 deletions(-) diff --git a/tools/aoptk/aoptk_find_relationships_chemicals.xml b/tools/aoptk/aoptk_find_relationships_chemicals.xml index 65cd181c..3eafb28f 100644 --- a/tools/aoptk/aoptk_find_relationships_chemicals.xml +++ b/tools/aoptk/aoptk_find_relationships_chemicals.xml @@ -47,7 +47,6 @@ def write_chemicals(chemicals): chemical_name = chemical.name if hasattr(chemical, "name") else chemical f_out.write(f"{chemical_name}\n") -#if $analysis_purpose.method == "relationships_chemicals" input_file_ext = "$input_file.ext" if input_file_ext == "pdf": text = PymupdfParser([PDF("$input_file")], text_generation=TextGenerationAPI(model="qwen3.5", api_key=litellm_api_key)).get_publications()[0].full_text @@ -56,9 +55,9 @@ elif input_file_ext == "txt": text = f_in.read() else: raise ValueError(f"Unsupported input file format: {input_file_ext}") - publication_id = os.path.splitext("$input_file.element_identifier")[0] +#if $analysis_purpose.method == "relationships_chemicals" effects = [] #for $effect in $effects effects.append(Effect("$effect.effect")) @@ -78,18 +77,11 @@ write_relationships(relationships) write_chemicals(chemicals) #elif $analysis_purpose.method == "relationships" -input_file_ext = "$input_file.ext" -if input_file_ext == "pdf": - text = PymupdfParser([PDF("$input_file")], text_generation=TextGenerationAPI(model="qwen3.5", api_key=litellm_api_key)).get_publications()[0].full_text -elif input_file_ext == "txt": - with open("$input_file", "r") as f_in: - text = f_in.read() -else: - raise ValueError(f"Unsupported input file format: {input_file_ext}") - -publication_id = os.path.splitext("$input_file.element_identifier")[0] +effects = [] +#for $effect in $effects +effects.append(Effect("$effect.effect")) +#end for -effects = [Effect("${effect}")] relationship_types = [] if "$relationship_types" == "causative": relationship_types.append(Causative()) @@ -103,17 +95,6 @@ relationships = TextGenerationAPI(model="$llm_models", api_key=litellm_api_key). write_relationships(relationships) #elif $analysis_purpose.method == "chemicals" -input_file_ext = "$input_file.ext" -if input_file_ext == "pdf": - text = PymupdfParser([PDF("$input_file")], text_generation=TextGenerationAPI(model="qwen3.5", api_key=litellm_api_key)).get_publications()[0].full_text -elif input_file_ext == "txt": - with open("$input_file", "r") as f_in: - text = f_in.read() -else: - raise ValueError(f"Unsupported input file format: {input_file_ext}") - -publication_id = os.path.splitext("$input_file.element_identifier")[0] - chemicals = TextGenerationAPI(model="$llm_models", api_key=litellm_api_key).find_chemicals(text) write_chemicals(chemicals) #end if diff --git a/tools/aoptk/aoptk_parse_pdf.xml b/tools/aoptk/aoptk_parse_pdf.xml index 7fe613a2..cfa302ff 100644 --- a/tools/aoptk/aoptk_parse_pdf.xml +++ b/tools/aoptk/aoptk_parse_pdf.xml @@ -32,13 +32,14 @@ else: text_generation = None publication_id = os.path.splitext("$input_file.element_identifier")[0] -if "${input_file.ext}" == "pdf": +input_file_ext = "$input_file.ext" +if input_file_ext == "pdf": text = PymupdfParser([PDF("$input_file")], text_generation=text_generation).get_publications(download_figures_enabled=False)[0].full_text with open(f"{publication_id}.txt", "w") as f: f.write(text) else: - raise ValueError(f"Unsupported input file format: ${input_file.ext}") + raise ValueError(f"Unsupported input file format: {input_file_ext}") diff --git a/tools/aoptk/aoptk_query_literature.xml b/tools/aoptk/aoptk_query_literature.xml index db6d34bf..94205f89 100644 --- a/tools/aoptk/aoptk_query_literature.xml +++ b/tools/aoptk/aoptk_query_literature.xml @@ -22,39 +22,52 @@ import os from aoptk.literature.databases.pubmed import PubMed from aoptk.literature.databases.europepmc import EuropePMC from aoptk.literature.databases.pmc import PMC -from aoptk.literature.query import Query +from aoptk.literature.query import Query email = os.environ.get("EMAIL") or None api_key = os.environ.get("NCBI_API_KEY") or None +#if $literature.database == "pubmed": #if $literature.preprint_mode == "only": only_preprint = True -#elif $literature.preprint_mode == "exclude": -only_preprint = False #else: only_preprint = False #end if +licensing = None +#elif $literature.database in ["europepmc", "pmc"]: +#if $literature.preprint_mode == "only": +only_preprint = True +#else: +only_preprint = False +#end if #if $literature.licensing == "all": licensing = None #else: licensing = "$literature.licensing" #end if -query = Query(search_term="$search_term", full_text_subset=$literature.full_text_subset, only_preprint=only_preprint, licensing=licensing) +#else: +raise ValueError("Select valid database.") +#end if + +#if $full_text_subset +full_text_subset = True +#else: +full_text_subset = False +#end if +query = Query(search_term="$search_term", full_text_subset=full_text_subset, only_preprint=only_preprint, licensing=licensing) #if $literature.database == "pubmed": Entrez.email = email Entrez.api_key = api_key -ids = PubMed(query).get_ids() +ids = PubMed(query=query, storage = "./").get_ids() #elif $literature.database == "europepmc": -ids = EuropePMC(query, storage = "./", figure_storage="./figures").get_ids() +ids = EuropePMC(query=query, storage = "./", figure_storage="./figures").get_ids() #elif $literature.database == "pmc": Entrez.email = email Entrez.api_key = api_key -ids = asyncio.run(PMC(query, storage = "./", figure_storage="./figures").get_ids()) -#else: -raise ValueError("Select valid database.") +ids = PMC(query=query, storage = "./", figure_storage="./figures").get_ids() #end if with open(f"ids.txt", "w") as f: From 537c828fc1f4507ea855b12adef10d54b2d96673 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Fri, 12 Jun 2026 15:20:43 +0200 Subject: [PATCH 155/173] updated test output tsv --- tools/aoptk/test-data/output.tsv | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tools/aoptk/test-data/output.tsv b/tools/aoptk/test-data/output.tsv index 74a4b310..31151f56 100644 --- a/tools/aoptk/test-data/output.tsv +++ b/tools/aoptk/test-data/output.tsv @@ -5,4 +5,5 @@ id chemical effect relationship heading 26 acetaminophen liver fibrosis does not inhibit acetaminophen 35 methotrexate cell death causes methotrexate 98 acetaminophen cell death does not cause acetaminophen -26 acetaminophen liver fibrosis does not cause acetaminophen \ No newline at end of file +26 acetaminophen liver fibrosis does not cause acetaminophen +23 paracetamol liver fibrosis does not inhibit acetaminophen \ No newline at end of file From 906256a527de87cb06af7973d2a486d361200dbd Mon Sep 17 00:00:00 2001 From: rdurnik Date: Fri, 12 Jun 2026 15:21:43 +0200 Subject: [PATCH 156/173] finalize output modified for multiple effects --- tools/aoptk/aoptk_finalize_output.xml | 47 ++++++++++++++------------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/tools/aoptk/aoptk_finalize_output.xml b/tools/aoptk/aoptk_finalize_output.xml index fbc50aa9..c6c16fb9 100644 --- a/tools/aoptk/aoptk_finalize_output.xml +++ b/tools/aoptk/aoptk_finalize_output.xml @@ -14,7 +14,7 @@ ]]> - + + final_output.to_excel(f"./{file_name}.xlsx", index=False) +]]> @@ -53,17 +54,19 @@ final_output.to_excel("./final_output.xlsx", index=False) - + + + - + - + From 9f7c7484bcd0fe7a1f63f5b28cb4fab9b8816b35 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Fri, 12 Jun 2026 16:42:23 +0200 Subject: [PATCH 157/173] lint --- tools/aoptk/aoptk_finalize_output.xml | 8 ++++---- tools/aoptk/aoptk_find_relationships_chemicals.xml | 6 ++++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/tools/aoptk/aoptk_finalize_output.xml b/tools/aoptk/aoptk_finalize_output.xml index c6c16fb9..e48be8a4 100644 --- a/tools/aoptk/aoptk_finalize_output.xml +++ b/tools/aoptk/aoptk_finalize_output.xml @@ -54,19 +54,19 @@ for effect in output["effect"].unique(): - - + + - + - + diff --git a/tools/aoptk/aoptk_find_relationships_chemicals.xml b/tools/aoptk/aoptk_find_relationships_chemicals.xml index 3eafb28f..2a525c25 100644 --- a/tools/aoptk/aoptk_find_relationships_chemicals.xml +++ b/tools/aoptk/aoptk_find_relationships_chemicals.xml @@ -163,8 +163,10 @@ write_chemicals(chemicals) - - + + + + From fd5a7981c06ad3ab634a5442b47489f4c0db68ba Mon Sep 17 00:00:00 2001 From: rdurnik Date: Mon, 15 Jun 2026 13:46:35 +0200 Subject: [PATCH 158/173] test fixes --- .../aoptk/aoptk_download_publication_data.xml | 10 ++- tools/aoptk/aoptk_finalize_output.xml | 2 +- tools/aoptk/aoptk_match_chemical_lists.xml | 2 +- tools/aoptk/aoptk_query_literature.xml | 16 ++++- tools/aoptk/test-data/ids_europepmc.txt | 2 + tools/aoptk/test-data/ids_pmc.txt | 70 +++++++++++++++++++ tools/aoptk/test-data/ids_pubmed.txt | 42 +++++++++++ 7 files changed, 139 insertions(+), 5 deletions(-) create mode 100644 tools/aoptk/test-data/ids_europepmc.txt create mode 100644 tools/aoptk/test-data/ids_pmc.txt create mode 100644 tools/aoptk/test-data/ids_pubmed.txt diff --git a/tools/aoptk/aoptk_download_publication_data.xml b/tools/aoptk/aoptk_download_publication_data.xml index 9450974e..b5cb0523 100644 --- a/tools/aoptk/aoptk_download_publication_data.xml +++ b/tools/aoptk/aoptk_download_publication_data.xml @@ -108,7 +108,7 @@ raise ValueError("Select valid data type.") - + @@ -120,7 +120,13 @@ raise ValueError("Select valid data type.") - + + + + + + + diff --git a/tools/aoptk/aoptk_finalize_output.xml b/tools/aoptk/aoptk_finalize_output.xml index e48be8a4..8065a602 100644 --- a/tools/aoptk/aoptk_finalize_output.xml +++ b/tools/aoptk/aoptk_finalize_output.xml @@ -62,7 +62,7 @@ for effect in output["effect"].unique(): - + diff --git a/tools/aoptk/aoptk_match_chemical_lists.xml b/tools/aoptk/aoptk_match_chemical_lists.xml index e3c6b3bd..b56bb34e 100644 --- a/tools/aoptk/aoptk_match_chemical_lists.xml +++ b/tools/aoptk/aoptk_match_chemical_lists.xml @@ -43,7 +43,7 @@ merged_files.to_csv("merged_chemicals.tsv", sep="\t", index=False) - + diff --git a/tools/aoptk/aoptk_query_literature.xml b/tools/aoptk/aoptk_query_literature.xml index 94205f89..2c4fd984 100644 --- a/tools/aoptk/aoptk_query_literature.xml +++ b/tools/aoptk/aoptk_query_literature.xml @@ -108,7 +108,21 @@ with open(f"ids.txt", "w") as f: - + + + + + + + + + + + + + + + diff --git a/tools/aoptk/test-data/ids_europepmc.txt b/tools/aoptk/test-data/ids_europepmc.txt new file mode 100644 index 00000000..bb915739 --- /dev/null +++ b/tools/aoptk/test-data/ids_europepmc.txt @@ -0,0 +1,2 @@ +PMC4387231 +PMC9045590 diff --git a/tools/aoptk/test-data/ids_pmc.txt b/tools/aoptk/test-data/ids_pmc.txt new file mode 100644 index 00000000..abe60027 --- /dev/null +++ b/tools/aoptk/test-data/ids_pmc.txt @@ -0,0 +1,70 @@ +PMC13008360 +PMC12958302 +PMC12722370 +PMC12712776 +PMC12615732 +PMC12580166 +PMC12561621 +PMC12560119 +PMC12283783 +PMC12281243 +PMC12075197 +PMC12027150 +PMC11953279 +PMC11597081 +PMC11531151 +PMC11274902 +PMC11226731 +PMC11042069 +PMC10978534 +PMC10673607 +PMC10457767 +PMC10180184 +PMC10141038 +PMC10090537 +PMC10005888 +PMC9981852 +PMC9909725 +PMC9742412 +PMC9735468 +PMC9733013 +PMC9659232 +PMC9629974 +PMC9569683 +PMC9565252 +PMC9534590 +PMC9340459 +PMC9320734 +PMC9256002 +PMC8950395 +PMC8773802 +PMC8772881 +PMC8742652 +PMC8450120 +PMC8349706 +PMC8126138 +PMC7957963 +PMC7836452 +PMC7645917 +PMC7395068 +PMC7347206 +PMC6966667 +PMC6904863 +PMC6904905 +PMC6843249 +PMC6836491 +PMC6769037 +PMC6410160 +PMC6310944 +PMC6153761 +PMC6174638 +PMC5695905 +PMC5515971 +PMC5493342 +PMC5241492 +PMC5216757 +PMC5187530 +PMC5048025 +PMC4669991 +PMC4212306 +PMC3753504 diff --git a/tools/aoptk/test-data/ids_pubmed.txt b/tools/aoptk/test-data/ids_pubmed.txt new file mode 100644 index 00000000..9b515f43 --- /dev/null +++ b/tools/aoptk/test-data/ids_pubmed.txt @@ -0,0 +1,42 @@ +42225163 +42068642 +41893178 +41811695 +41747606 +39861177 +38714237 +39958782 +37952411 +36809998 +35092399 +34894963 +34829709 +33845730 +33806829 +33732322 +33407858 +32827326 +32821877 +32172678 +30784932 +29558627 +28802065 +28754618 +28694658 +28611265 +28440100 +27245430 +26733159 +25280007 +24611903 +24481875 +24396831 +24345506 +22138190 +21782536 +16054533 +16008752 +15763544 +15629516 +14739098 +11793227 From f5694c0ccae03faa1e203ca1a378912de5a41db2 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Mon, 15 Jun 2026 14:04:01 +0200 Subject: [PATCH 159/173] linter --- tools/aoptk/.shed.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/aoptk/.shed.yml b/tools/aoptk/.shed.yml index 9824bac6..513e852c 100644 --- a/tools/aoptk/.shed.yml +++ b/tools/aoptk/.shed.yml @@ -1,6 +1,6 @@ name: aoptk owner: recetox -remote_repository_url: "https://github.com/rdurnik/aoptk" +remote_repository_url: "https://github.com/RECETOX/galaxytools/tree/master/tools/aoptk" homepage_url: "https://github.com/rdurnik/aoptk" categories: - Machine Learning From bde45d266d267a174dfd098f9ac290de66893afd Mon Sep 17 00:00:00 2001 From: rdurnik Date: Mon, 15 Jun 2026 14:14:49 +0200 Subject: [PATCH 160/173] fixed forgot to add different databases --- tools/aoptk/aoptk_query_literature.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/aoptk/aoptk_query_literature.xml b/tools/aoptk/aoptk_query_literature.xml index 2c4fd984..6d87caeb 100644 --- a/tools/aoptk/aoptk_query_literature.xml +++ b/tools/aoptk/aoptk_query_literature.xml @@ -113,13 +113,13 @@ with open(f"ids.txt", "w") as f: - + - + From 3961f58bd791f0ca4e7b3d1e33dc01f258a4129e Mon Sep 17 00:00:00 2001 From: rdurnik Date: Mon, 15 Jun 2026 14:21:06 +0200 Subject: [PATCH 161/173] fixed links not working on CI --- tools/aoptk/aoptk_query_literature.xml | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/tools/aoptk/aoptk_query_literature.xml b/tools/aoptk/aoptk_query_literature.xml index 6d87caeb..7db904fa 100644 --- a/tools/aoptk/aoptk_query_literature.xml +++ b/tools/aoptk/aoptk_query_literature.xml @@ -143,13 +143,7 @@ To speed up downloads from NCBI databases (PMC and PubMed), you may provide your Search Term Modifications ------------------------- You can further refine your search by applying additional filters beyond those available in the user interface. Simply specify your search terms as you would in the original database interface (for example, to filter by publication date). -Please note that query modifications may vary depending on the database. For detailed guidance, refer to the official documentation for each source: - -- PMC: https://pmc.ncbi.nlm.nih.gov/about/userguide/ - -- Europe PMC: https://europepmc.org/searchSyntaxGuide - -- PubMed: https://pubmed.ncbi.nlm.nih.gov/help/ +Please note that query modifications may vary depending on the database. For detailed guidance, refer to the official documentation. ]]> From cd8e76e57b84bc43f5a0f455f934c8cb4d69051d Mon Sep 17 00:00:00 2001 From: rdurnik Date: Mon, 15 Jun 2026 14:52:53 +0200 Subject: [PATCH 162/173] fixed chemical identifier and publication tracker tests --- tools/aoptk/aoptk_chemical_identifier.xml | 4 ++-- tools/aoptk/aoptk_publication_tracker.xml | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tools/aoptk/aoptk_chemical_identifier.xml b/tools/aoptk/aoptk_chemical_identifier.xml index dbdc82c9..5b00156b 100644 --- a/tools/aoptk/aoptk_chemical_identifier.xml +++ b/tools/aoptk/aoptk_chemical_identifier.xml @@ -38,13 +38,13 @@ - + - + - + @@ -61,7 +61,7 @@ - + @@ -69,8 +69,8 @@ - - + + Date: Tue, 16 Jun 2026 09:31:17 +0200 Subject: [PATCH 163/173] fixed multiple relationship types --- .../aoptk_find_relationships_chemicals.xml | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/tools/aoptk/aoptk_find_relationships_chemicals.xml b/tools/aoptk/aoptk_find_relationships_chemicals.xml index 2a525c25..993451f8 100644 --- a/tools/aoptk/aoptk_find_relationships_chemicals.xml +++ b/tools/aoptk/aoptk_find_relationships_chemicals.xml @@ -64,12 +64,14 @@ effects.append(Effect("$effect.effect")) #end for relationship_types = [] -if "$relationship_types" == "causative": - relationship_types.append(Causative()) -elif "$relationship_types" == "inhibitive": - relationship_types.append(Inhibitive()) -else: - raise ValueError(f"Unsupported relationship type: $relationship_types") +types = [t.strip() for t in "$relationship_types".split(",")] +for type in types: + if type == "causative": + relationship_types.append(Causative()) + elif type == "inhibitive": + relationship_types.append(Inhibitive()) + else: + raise ValueError(f"Unsupported relationship type: {type}") chemicals = TextGenerationAPI(model="$llm_models", api_key=litellm_api_key).find_chemicals(text) relationships = TextGenerationAPI(model="$llm_models", api_key=litellm_api_key).find_relationships_in_text(text=text, chemicals=chemicals, effects=effects, relationship_types=relationship_types) @@ -83,12 +85,14 @@ effects.append(Effect("$effect.effect")) #end for relationship_types = [] -if "$relationship_types" == "causative": - relationship_types.append(Causative()) -elif "$relationship_types" == "inhibitive": - relationship_types.append(Inhibitive()) -else: - raise ValueError(f"Unsupported relationship type: $relationship_types") +types = [t.strip() for t in "$relationship_types".split(",")] +for type in types: + if type == "causative": + relationship_types.append(Causative()) + elif type == "inhibitive": + relationship_types.append(Inhibitive()) + else: + raise ValueError(f"Unsupported relationship type: {type}") chemicals = [Chemical(chem) for chem in pd.read_csv("$chemicals", sep="\t")["chemical"].tolist()] relationships = TextGenerationAPI(model="$llm_models", api_key=litellm_api_key).find_relationships_in_text(text=text, chemicals=chemicals, effects=effects, relationship_types=relationship_types) From 8a66ee8a1a37c5d6a3e4d13b885823d25333f87d Mon Sep 17 00:00:00 2001 From: rdurnik Date: Tue, 16 Jun 2026 09:33:48 +0200 Subject: [PATCH 164/173] updated test text --- tools/aoptk/test-data/text.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/aoptk/test-data/text.txt b/tools/aoptk/test-data/text.txt index 5b5f97da..9016b1eb 100644 --- a/tools/aoptk/test-data/text.txt +++ b/tools/aoptk/test-data/text.txt @@ -1 +1 @@ -Thioacetamide is a chemical that causes liver fibrosis. \ No newline at end of file +Thioacetamide is a chemical that causes liver fibrosis. Acetaminophen does not cause liver fibrosis. Methotrexate inhibits liver fibrosis. Thioacetamide does not inhibit cancer. Methotrexate does not cause cancer. \ No newline at end of file From b6dd3797f85c8f3d43c450dd4963b041a2f93fdb Mon Sep 17 00:00:00 2001 From: hechth Date: Tue, 16 Jun 2026 12:01:22 +0200 Subject: [PATCH 165/173] updated wrappers and minor fixes --- .../aoptk_find_relationships_chemicals.xml | 47 +++++++------------ tools/aoptk/aoptk_normalize_chemicals.xml | 2 +- tools/aoptk/aoptk_parse_pdf.xml | 14 +++--- tools/aoptk/macros.xml | 35 ++++++-------- 4 files changed, 38 insertions(+), 60 deletions(-) diff --git a/tools/aoptk/aoptk_find_relationships_chemicals.xml b/tools/aoptk/aoptk_find_relationships_chemicals.xml index 993451f8..9140014c 100644 --- a/tools/aoptk/aoptk_find_relationships_chemicals.xml +++ b/tools/aoptk/aoptk_find_relationships_chemicals.xml @@ -6,6 +6,7 @@ + + + @@ -115,26 +109,19 @@ write_chemicals(chemicals) - - - - - - - - + @@ -149,10 +136,10 @@ write_chemicals(chemicals) + + - - @@ -162,11 +149,11 @@ write_chemicals(chemicals) + + - - - - + + @@ -175,10 +162,10 @@ write_chemicals(chemicals) - + + + - - diff --git a/tools/aoptk/aoptk_normalize_chemicals.xml b/tools/aoptk/aoptk_normalize_chemicals.xml index e06a4c5f..60f8bd88 100644 --- a/tools/aoptk/aoptk_normalize_chemicals.xml +++ b/tools/aoptk/aoptk_normalize_chemicals.xml @@ -82,7 +82,7 @@ chemicals.to_csv("normalized_chemicals.tsv", sep="\t", index=False) - + diff --git a/tools/aoptk/aoptk_parse_pdf.xml b/tools/aoptk/aoptk_parse_pdf.xml index cfa302ff..f57e38f0 100644 --- a/tools/aoptk/aoptk_parse_pdf.xml +++ b/tools/aoptk/aoptk_parse_pdf.xml @@ -32,23 +32,21 @@ else: text_generation = None publication_id = os.path.splitext("$input_file.element_identifier")[0] -input_file_ext = "$input_file.ext" -if input_file_ext == "pdf": - text = PymupdfParser([PDF("$input_file")], text_generation=text_generation).get_publications(download_figures_enabled=False)[0].full_text - with open(f"{publication_id}.txt", "w") as f: - f.write(text) -else: - raise ValueError(f"Unsupported input file format: {input_file_ext}") +text = PymupdfParser([PDF("$input_file")], text_generation=$use_llms).get_publications(download_figures_enabled=False)[0].full_text +with open(f"{publication_id}.txt", "w") as f: + f.write(text) + + - + diff --git a/tools/aoptk/macros.xml b/tools/aoptk/macros.xml index f0586119..c5c1485d 100644 --- a/tools/aoptk/macros.xml +++ b/tools/aoptk/macros.xml @@ -27,8 +27,19 @@ + + + + + + + - + @@ -99,7 +110,7 @@ - + @@ -189,7 +200,7 @@ - + @@ -216,22 +227,4 @@ - - - -from matchms.importing import load_from_msp, scores_from_json -from matchms import Scores -#if $scores.use_scores == "True" -scores = scores_from_json("$scores_in") -join_type = "$scores.join_type" -#else -scores = Scores(references=list(load_from_msp("$references")), queries=list(load_from_msp("$queries")), is_symmetric=False) -join_type = "left" -#end if - - - -from matchms import set_matchms_logger_level -set_matchms_logger_level("WARNING") - From b34eff44b10d5abc9ea4bd3b1fa9f5c9fda321c6 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Tue, 16 Jun 2026 12:06:31 +0200 Subject: [PATCH 166/173] deleted match chemical lists --- tools/aoptk/aoptk_match_chemical_lists.xml | 62 ---------------------- 1 file changed, 62 deletions(-) delete mode 100644 tools/aoptk/aoptk_match_chemical_lists.xml diff --git a/tools/aoptk/aoptk_match_chemical_lists.xml b/tools/aoptk/aoptk_match_chemical_lists.xml deleted file mode 100644 index b56bb34e..00000000 --- a/tools/aoptk/aoptk_match_chemical_lists.xml +++ /dev/null @@ -1,62 +0,0 @@ - - Match chemical entities. - - macros.xml - - - - - - - - - - -import os -import pandas as pd - -chemicals_df_1 = pd.read_csv("$input_file_1", sep="\t") -chemicals_df_2 = pd.read_csv("$input_file_2", sep="\t") -merged_files = chemicals_df_1.merge( - chemicals_df_2, - left_on="heading", - right_on="heading", - how="outer", -) -merged_files.to_csv("merged_chemicals.tsv", sep="\t", index=False) - - - - - - - - - - - - - - - - - - - - - - - - - 10.5281/zenodo.20036704 - - - \ No newline at end of file From b9582e233a679b31ed08ec5e01048872c6d92e9b Mon Sep 17 00:00:00 2001 From: rdurnik Date: Tue, 16 Jun 2026 12:06:41 +0200 Subject: [PATCH 167/173] find chemicals stores ids --- tools/aoptk/aoptk_find_relationships_chemicals.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/aoptk/aoptk_find_relationships_chemicals.xml b/tools/aoptk/aoptk_find_relationships_chemicals.xml index 9140014c..95e3ffac 100644 --- a/tools/aoptk/aoptk_find_relationships_chemicals.xml +++ b/tools/aoptk/aoptk_find_relationships_chemicals.xml @@ -38,10 +38,10 @@ def write_relationships(relationships): def write_chemicals(chemicals): with open("chemicals.tsv", "w") as f_out: - f_out.write("chemical\n") + f_out.write("id\tchemical\n") for chemical in chemicals: chemical_name = chemical.name if hasattr(chemical, "name") else chemical - f_out.write(f"{chemical_name}\n") + f_out.write(f"{publication_id}\t{chemical_name}\n") with open("$input_file", "r") as f_in: From 7bac7e6cf9cb196078a0af1bd1c68792d7836e79 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Tue, 16 Jun 2026 13:00:52 +0200 Subject: [PATCH 168/173] option to add api keys as credentials --- .../aoptk_find_relationships_chemicals.xml | 11 +++++---- tools/aoptk/aoptk_normalize_chemicals.xml | 15 ++++++------ tools/aoptk/aoptk_parse_pdf.xml | 23 ++++++++++--------- tools/aoptk/macros.xml | 8 +++---- 4 files changed, 29 insertions(+), 28 deletions(-) diff --git a/tools/aoptk/aoptk_find_relationships_chemicals.xml b/tools/aoptk/aoptk_find_relationships_chemicals.xml index 95e3ffac..7bb64e4f 100644 --- a/tools/aoptk/aoptk_find_relationships_chemicals.xml +++ b/tools/aoptk/aoptk_find_relationships_chemicals.xml @@ -28,7 +28,8 @@ from aoptk.relationships.relationship_type import Causative from aoptk.relationships.relationship_type import Inhibitive from aoptk.text_generation_api import TextGenerationAPI -litellm_api_key = os.environ.get("LITELLM_API_KEY") +litellm_api_key = os.environ.get("OPENAI_API_KEY") +openai_endpoint_url = os.environ.get("OPENAI_ENDPOINT_URL") or "https://llm.ai.e-infra.cz/v1" def write_relationships(relationships): with open("relationships.tsv", "w") as f_out: @@ -65,8 +66,8 @@ for type in types: else: raise ValueError(f"Unsupported relationship type: {type}") -chemicals = TextGenerationAPI(model="$llm_models", api_key=litellm_api_key).find_chemicals(text) -relationships = TextGenerationAPI(model="$llm_models", api_key=litellm_api_key).find_relationships_in_text(text=text, chemicals=chemicals, effects=effects, relationship_types=relationship_types) +chemicals = TextGenerationAPI(model="$llm_models", api_key=litellm_api_key, url=openai_endpoint_url).find_chemicals(text) +relationships = TextGenerationAPI(model="$llm_models", api_key=litellm_api_key, url=openai_endpoint_url).find_relationships_in_text(text=text, chemicals=chemicals, effects=effects, relationship_types=relationship_types) write_relationships(relationships) write_chemicals(chemicals) @@ -87,11 +88,11 @@ for type in types: raise ValueError(f"Unsupported relationship type: {type}") chemicals = [Chemical(chem) for chem in pd.read_csv("$chemicals", sep="\t")["chemical"].tolist()] -relationships = TextGenerationAPI(model="$llm_models", api_key=litellm_api_key).find_relationships_in_text(text=text, chemicals=chemicals, effects=effects, relationship_types=relationship_types) +relationships = TextGenerationAPI(model="$llm_models", api_key=litellm_api_key, url=openai_endpoint_url).find_relationships_in_text(text=text, chemicals=chemicals, effects=effects, relationship_types=relationship_types) write_relationships(relationships) #elif $analysis_purpose.method == "chemicals" -chemicals = TextGenerationAPI(model="$llm_models", api_key=litellm_api_key).find_chemicals(text) +chemicals = TextGenerationAPI(model="$llm_models", api_key=litellm_api_key, url=openai_endpoint_url).find_chemicals(text) write_chemicals(chemicals) #end if diff --git a/tools/aoptk/aoptk_normalize_chemicals.xml b/tools/aoptk/aoptk_normalize_chemicals.xml index 60f8bd88..7b90d199 100644 --- a/tools/aoptk/aoptk_normalize_chemicals.xml +++ b/tools/aoptk/aoptk_normalize_chemicals.xml @@ -6,6 +6,7 @@ + + - + diff --git a/tools/aoptk/macros.xml b/tools/aoptk/macros.xml index c5c1485d..b50abc48 100644 --- a/tools/aoptk/macros.xml +++ b/tools/aoptk/macros.xml @@ -28,13 +28,13 @@ - + description="Provide OpenAI API key and URL for accessing the large language models."> - + From bcb9951da7468985174d1d2d51c18d6b97358bb1 Mon Sep 17 00:00:00 2001 From: rdurnik Date: Tue, 16 Jun 2026 13:05:45 +0200 Subject: [PATCH 169/173] lint: changed variable to secret for endpoint url --- tools/aoptk/macros.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/aoptk/macros.xml b/tools/aoptk/macros.xml index b50abc48..4be6ce15 100644 --- a/tools/aoptk/macros.xml +++ b/tools/aoptk/macros.xml @@ -34,7 +34,7 @@ optional="true" description="Provide OpenAI API key and URL for accessing the large language models."> - + From 0e7f80ff7831cf59957345b7a6d45449d860d9af Mon Sep 17 00:00:00 2001 From: hechth Date: Thu, 18 Jun 2026 10:03:41 +0200 Subject: [PATCH 170/173] fixed parse pdf test --- tools/aoptk/aoptk_parse_pdf.xml | 10 +++++++--- tools/aoptk/test-data/text.pdf | Bin 81026 -> 81438 bytes 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/tools/aoptk/aoptk_parse_pdf.xml b/tools/aoptk/aoptk_parse_pdf.xml index 56412b59..49116fe8 100644 --- a/tools/aoptk/aoptk_parse_pdf.xml +++ b/tools/aoptk/aoptk_parse_pdf.xml @@ -25,7 +25,7 @@ from aoptk.text_generation_api import TextGenerationAPI publication_id = os.path.splitext("$input_file.element_identifier")[0] -#if str($use_llms) == "true" +#if $use_llms == "true" litellm_api_key = os.environ.get("OPENAI_API_KEY") openai_endpoint_url = os.environ.get("OPENAI_ENDPOINT_URL") or "https://llm.ai.e-infra.cz/v1" text_generation=TextGenerationAPI(model="qwen3.5", api_key=litellm_api_key, url=openai_endpoint_url) @@ -43,7 +43,7 @@ with open(f"{publication_id}.txt", "w") as f: - + @@ -53,7 +53,11 @@ with open(f"{publication_id}.txt", "w") as f: - + + + + + diff --git a/tools/aoptk/test-data/text.pdf b/tools/aoptk/test-data/text.pdf index 4fc5b3dccb5a20a51371b3bba4f5bb55de1dad26..6f0433157c7e25bf656e934040b9fa22f68ce885 100644 GIT binary patch literal 81438 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

FA9!Citw+T7FJMa9M3!raB&!SuHQ-rodaVN@h@ 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 Date: Thu, 18 Jun 2026 10:33:58 +0200 Subject: [PATCH 171/173] fixed tests --- tools/aoptk/aoptk_find_relationships_chemicals.xml | 6 +++--- tools/aoptk/aoptk_normalize_chemicals.xml | 3 +-- tools/aoptk/aoptk_publication_tracker.xml | 8 ++++---- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/tools/aoptk/aoptk_find_relationships_chemicals.xml b/tools/aoptk/aoptk_find_relationships_chemicals.xml index 7bb64e4f..f5fdac63 100644 --- a/tools/aoptk/aoptk_find_relationships_chemicals.xml +++ b/tools/aoptk/aoptk_find_relationships_chemicals.xml @@ -136,7 +136,7 @@ write_chemicals(chemicals) - + @@ -149,7 +149,7 @@ write_chemicals(chemicals) - + @@ -162,7 +162,7 @@ write_chemicals(chemicals) - + diff --git a/tools/aoptk/aoptk_normalize_chemicals.xml b/tools/aoptk/aoptk_normalize_chemicals.xml index 7b90d199..dbf86892 100644 --- a/tools/aoptk/aoptk_normalize_chemicals.xml +++ b/tools/aoptk/aoptk_normalize_chemicals.xml @@ -92,13 +92,12 @@ chemicals.to_csv("normalized_chemicals.tsv", sep="\t", index=False) - + - diff --git a/tools/aoptk/aoptk_publication_tracker.xml b/tools/aoptk/aoptk_publication_tracker.xml index d61c1d84..b182884d 100644 --- a/tools/aoptk/aoptk_publication_tracker.xml +++ b/tools/aoptk/aoptk_publication_tracker.xml @@ -53,7 +53,7 @@ - + @@ -61,7 +61,7 @@ - + @@ -69,8 +69,8 @@ - - + + Date: Thu, 18 Jun 2026 11:57:42 +0200 Subject: [PATCH 172/173] made it compatible with usegalaxy litellm config --- .../aoptk_find_relationships_chemicals.xml | 19 +++-- tools/aoptk/aoptk_normalize_chemicals.xml | 15 ++-- tools/aoptk/aoptk_parse_pdf.xml | 18 +++-- tools/aoptk/macros.xml | 72 ++++++++++++++++++- tools/aoptk/tool-data/genai_models.loc.sample | 26 +++++++ tools/aoptk/tool_data_table_conf.xml.sample | 7 ++ 6 files changed, 134 insertions(+), 23 deletions(-) create mode 100644 tools/aoptk/tool-data/genai_models.loc.sample create mode 100644 tools/aoptk/tool_data_table_conf.xml.sample diff --git a/tools/aoptk/aoptk_find_relationships_chemicals.xml b/tools/aoptk/aoptk_find_relationships_chemicals.xml index f5fdac63..714e61a3 100644 --- a/tools/aoptk/aoptk_find_relationships_chemicals.xml +++ b/tools/aoptk/aoptk_find_relationships_chemicals.xml @@ -28,8 +28,7 @@ from aoptk.relationships.relationship_type import Causative from aoptk.relationships.relationship_type import Inhibitive from aoptk.text_generation_api import TextGenerationAPI -litellm_api_key = os.environ.get("OPENAI_API_KEY") -openai_endpoint_url = os.environ.get("OPENAI_ENDPOINT_URL") or "https://llm.ai.e-infra.cz/v1" +@MODEL_SETUP@ def write_relationships(relationships): with open("relationships.tsv", "w") as f_out: @@ -66,8 +65,8 @@ for type in types: else: raise ValueError(f"Unsupported relationship type: {type}") -chemicals = TextGenerationAPI(model="$llm_models", api_key=litellm_api_key, url=openai_endpoint_url).find_chemicals(text) -relationships = TextGenerationAPI(model="$llm_models", api_key=litellm_api_key, url=openai_endpoint_url).find_relationships_in_text(text=text, chemicals=chemicals, effects=effects, relationship_types=relationship_types) +chemicals = TextGenerationAPI(model="$model", api_key=litellm_api_key, url=litellm_base_url).find_chemicals(text) +relationships = TextGenerationAPI(model="$model", api_key=litellm_api_key, url=litellm_base_url).find_relationships_in_text(text=text, chemicals=chemicals, effects=effects, relationship_types=relationship_types) write_relationships(relationships) write_chemicals(chemicals) @@ -88,11 +87,11 @@ for type in types: raise ValueError(f"Unsupported relationship type: {type}") chemicals = [Chemical(chem) for chem in pd.read_csv("$chemicals", sep="\t")["chemical"].tolist()] -relationships = TextGenerationAPI(model="$llm_models", api_key=litellm_api_key, url=openai_endpoint_url).find_relationships_in_text(text=text, chemicals=chemicals, effects=effects, relationship_types=relationship_types) +relationships = TextGenerationAPI(model="$model", api_key=litellm_api_key, url=litellm_base_url).find_relationships_in_text(text=text, chemicals=chemicals, effects=effects, relationship_types=relationship_types) write_relationships(relationships) #elif $analysis_purpose.method == "chemicals" -chemicals = TextGenerationAPI(model="$llm_models", api_key=litellm_api_key, url=openai_endpoint_url).find_chemicals(text) +chemicals = TextGenerationAPI(model="$model", api_key=litellm_api_key, url=litellm_base_url).find_chemicals(text) write_chemicals(chemicals) #end if @@ -101,7 +100,7 @@ write_chemicals(chemicals) - + @@ -137,7 +136,7 @@ write_chemicals(chemicals) - + @@ -150,7 +149,7 @@ write_chemicals(chemicals) - + @@ -163,7 +162,7 @@ write_chemicals(chemicals) - + diff --git a/tools/aoptk/aoptk_normalize_chemicals.xml b/tools/aoptk/aoptk_normalize_chemicals.xml index dbf86892..5e4a340d 100644 --- a/tools/aoptk/aoptk_normalize_chemicals.xml +++ b/tools/aoptk/aoptk_normalize_chemicals.xml @@ -28,14 +28,13 @@ from aoptk.normalization.provide_mesh_term_dataframe_from_xml import ProvideMesh #if $normalization_method.method == "llm" -litellm_api_key = os.environ.get("OPENAI_API_KEY") -openai_endpoint_url = os.environ.get("OPENAI_ENDPOINT_URL") or "https://llm.ai.e-infra.cz/v1" +@MODEL_SETUP@ chemicals = pd.read_csv("$chemicals", sep="\t") chemical_list = pd.read_csv("$chemical_list", sep="\t")["chemical"].tolist() chemicals["chemical"] = chemicals["chemical"].apply( - lambda x: TextGenerationAPI(model="$llm_models", api_key=litellm_api_key, url=openai_endpoint_url).normalize_chemical(chemical=Chemical(x), chemical_list=[Chemical(chem) for chem in chemical_list]) + lambda x: TextGenerationAPI(model=model, api_key=litellm_api_key, url=litellm_base_url).normalize_chemical(chemical=Chemical(x), chemical_list=[Chemical(chem) for chem in chemical_list]) ) #elif $normalization_method.method == "pubchem" @@ -70,17 +69,17 @@ chemicals.to_csv("normalized_chemicals.tsv", sep="\t", index=False) - + - - - + + + - + diff --git a/tools/aoptk/aoptk_parse_pdf.xml b/tools/aoptk/aoptk_parse_pdf.xml index 49116fe8..351d7b7c 100644 --- a/tools/aoptk/aoptk_parse_pdf.xml +++ b/tools/aoptk/aoptk_parse_pdf.xml @@ -23,12 +23,13 @@ from aoptk.literature.pdf import PDF from aoptk.literature.pymupdf_parser import PymupdfParser from aoptk.text_generation_api import TextGenerationAPI + + publication_id = os.path.splitext("$input_file.element_identifier")[0] #if $use_llms == "true" -litellm_api_key = os.environ.get("OPENAI_API_KEY") -openai_endpoint_url = os.environ.get("OPENAI_ENDPOINT_URL") or "https://llm.ai.e-infra.cz/v1" -text_generation=TextGenerationAPI(model="qwen3.5", api_key=litellm_api_key, url=openai_endpoint_url) +@MODEL_SETUP@ +text_generation=TextGenerationAPI(model=model, api_key=litellm_api_key, url=litellm_base_url) #else text_generation = None @@ -43,7 +44,16 @@ with open(f"{publication_id}.txt", "w") as f: - + + + + + + + + + + diff --git a/tools/aoptk/macros.xml b/tools/aoptk/macros.xml index 4be6ce15..4ef3bf9e 100644 --- a/tools/aoptk/macros.xml +++ b/tools/aoptk/macros.xml @@ -34,7 +34,6 @@ optional="true" description="Provide OpenAI API key and URL for accessing the large language models."> - @@ -123,6 +122,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + True == value + + + + + + diff --git a/tools/aoptk/tool-data/genai_models.loc.sample b/tools/aoptk/tool-data/genai_models.loc.sample new file mode 100644 index 00000000..add60967 --- /dev/null +++ b/tools/aoptk/tool-data/genai_models.loc.sample @@ -0,0 +1,26 @@ +#This is a sample file distributed with Galaxy that is used to define litellm +#models, using 6 columns tab separated +#(longer whitespace are TAB characters): +# +# +# +#value: Unique identifier for the dropdown selection +#model_id: The actual model identifier to send to the LiteLLM API +#name: Display name shown to users in the Galaxy interface +#domain: Model type - text, image, or multimodal +#provider: Server identifier matching a key in the servers section of your YAML config +#free_tag: Optional tag that can be freely used by admins to specify additional filter options +# +#Examples: +# +# +#gpt-oss-20b gpt-oss-20b-llmlb GPT-OSS Local (20B) text uni-freiburg openai +#gpt-oss-120b gpt-oss-120b-llmlb GPT-OSS Pro (120B) text uni-freiburg openai +#deepseek-qwen-8b deepseek-r1-0528-qwen3-8b-llmlb DeepSeek Qwen Chat (8B) text uni-freiburg deepseek +#gemma-3-12b-freiburg gemma-3-12b-llmlb Gemma 3 (12B) - Freiburg multimodal uni-freiburg google +#gemma-3-12b-custom gemma-3-12b-llmlb Gemma 3 (12B) - Custom multimodal custom-server google +#llama-3.1-8b llama-3.1-8b-fp8-llmlb LLaMA 3.1 (8B) text uni-freiburg meta +#qwen2.5-vl-7b qwen2.5-vl-7b-llmlb Qwen 2.5 VL (7B) multimodal uni-freiburg alibaba +# +#Note: The same model (same model_id) can be provided by different servers by using +#different value identifiers and provider fields. See gemma-3-12b examples above. \ No newline at end of file diff --git a/tools/aoptk/tool_data_table_conf.xml.sample b/tools/aoptk/tool_data_table_conf.xml.sample new file mode 100644 index 00000000..0b2d0445 --- /dev/null +++ b/tools/aoptk/tool_data_table_conf.xml.sample @@ -0,0 +1,7 @@ + + + + value, model_id, name, domain, provider, free_tag + +
+
\ No newline at end of file From 6da73ff4a1dfafdad02c6f04be2d66a211f3a0c7 Mon Sep 17 00:00:00 2001 From: hechth Date: Thu, 18 Jun 2026 12:07:20 +0200 Subject: [PATCH 173/173] fixed tests --- .../aoptk/aoptk_find_relationships_chemicals.xml | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/tools/aoptk/aoptk_find_relationships_chemicals.xml b/tools/aoptk/aoptk_find_relationships_chemicals.xml index 714e61a3..641f7adb 100644 --- a/tools/aoptk/aoptk_find_relationships_chemicals.xml +++ b/tools/aoptk/aoptk_find_relationships_chemicals.xml @@ -136,7 +136,10 @@ write_chemicals(chemicals) - + + + + @@ -149,7 +152,10 @@ write_chemicals(chemicals) - + + + + @@ -162,7 +168,10 @@ write_chemicals(chemicals) - + + + +