aiagents4pharma 0.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (336) hide show
  1. aiagents4pharma/__init__.py +11 -0
  2. aiagents4pharma/talk2aiagents4pharma/.dockerignore +13 -0
  3. aiagents4pharma/talk2aiagents4pharma/Dockerfile +133 -0
  4. aiagents4pharma/talk2aiagents4pharma/README.md +1 -0
  5. aiagents4pharma/talk2aiagents4pharma/__init__.py +5 -0
  6. aiagents4pharma/talk2aiagents4pharma/agents/__init__.py +6 -0
  7. aiagents4pharma/talk2aiagents4pharma/agents/main_agent.py +70 -0
  8. aiagents4pharma/talk2aiagents4pharma/configs/__init__.py +5 -0
  9. aiagents4pharma/talk2aiagents4pharma/configs/agents/__init__.py +5 -0
  10. aiagents4pharma/talk2aiagents4pharma/configs/agents/main_agent/default.yaml +29 -0
  11. aiagents4pharma/talk2aiagents4pharma/configs/app/__init__.py +0 -0
  12. aiagents4pharma/talk2aiagents4pharma/configs/app/frontend/__init__.py +0 -0
  13. aiagents4pharma/talk2aiagents4pharma/configs/app/frontend/default.yaml +102 -0
  14. aiagents4pharma/talk2aiagents4pharma/configs/config.yaml +4 -0
  15. aiagents4pharma/talk2aiagents4pharma/docker-compose/cpu/.env.example +23 -0
  16. aiagents4pharma/talk2aiagents4pharma/docker-compose/cpu/docker-compose.yml +93 -0
  17. aiagents4pharma/talk2aiagents4pharma/docker-compose/gpu/.env.example +23 -0
  18. aiagents4pharma/talk2aiagents4pharma/docker-compose/gpu/docker-compose.yml +108 -0
  19. aiagents4pharma/talk2aiagents4pharma/install.md +154 -0
  20. aiagents4pharma/talk2aiagents4pharma/states/__init__.py +5 -0
  21. aiagents4pharma/talk2aiagents4pharma/states/state_talk2aiagents4pharma.py +18 -0
  22. aiagents4pharma/talk2aiagents4pharma/tests/__init__.py +3 -0
  23. aiagents4pharma/talk2aiagents4pharma/tests/test_main_agent.py +312 -0
  24. aiagents4pharma/talk2biomodels/.dockerignore +13 -0
  25. aiagents4pharma/talk2biomodels/Dockerfile +104 -0
  26. aiagents4pharma/talk2biomodels/README.md +1 -0
  27. aiagents4pharma/talk2biomodels/__init__.py +5 -0
  28. aiagents4pharma/talk2biomodels/agents/__init__.py +6 -0
  29. aiagents4pharma/talk2biomodels/agents/t2b_agent.py +104 -0
  30. aiagents4pharma/talk2biomodels/api/__init__.py +5 -0
  31. aiagents4pharma/talk2biomodels/api/ols.py +75 -0
  32. aiagents4pharma/talk2biomodels/api/uniprot.py +36 -0
  33. aiagents4pharma/talk2biomodels/configs/__init__.py +5 -0
  34. aiagents4pharma/talk2biomodels/configs/agents/__init__.py +5 -0
  35. aiagents4pharma/talk2biomodels/configs/agents/t2b_agent/__init__.py +3 -0
  36. aiagents4pharma/talk2biomodels/configs/agents/t2b_agent/default.yaml +14 -0
  37. aiagents4pharma/talk2biomodels/configs/app/__init__.py +0 -0
  38. aiagents4pharma/talk2biomodels/configs/app/frontend/__init__.py +0 -0
  39. aiagents4pharma/talk2biomodels/configs/app/frontend/default.yaml +72 -0
  40. aiagents4pharma/talk2biomodels/configs/config.yaml +7 -0
  41. aiagents4pharma/talk2biomodels/configs/tools/__init__.py +5 -0
  42. aiagents4pharma/talk2biomodels/configs/tools/ask_question/__init__.py +3 -0
  43. aiagents4pharma/talk2biomodels/configs/tools/ask_question/default.yaml +30 -0
  44. aiagents4pharma/talk2biomodels/configs/tools/custom_plotter/__init__.py +3 -0
  45. aiagents4pharma/talk2biomodels/configs/tools/custom_plotter/default.yaml +8 -0
  46. aiagents4pharma/talk2biomodels/configs/tools/get_annotation/__init__.py +3 -0
  47. aiagents4pharma/talk2biomodels/configs/tools/get_annotation/default.yaml +8 -0
  48. aiagents4pharma/talk2biomodels/install.md +63 -0
  49. aiagents4pharma/talk2biomodels/models/__init__.py +5 -0
  50. aiagents4pharma/talk2biomodels/models/basico_model.py +125 -0
  51. aiagents4pharma/talk2biomodels/models/sys_bio_model.py +60 -0
  52. aiagents4pharma/talk2biomodels/states/__init__.py +6 -0
  53. aiagents4pharma/talk2biomodels/states/state_talk2biomodels.py +49 -0
  54. aiagents4pharma/talk2biomodels/tests/BIOMD0000000449_url.xml +1585 -0
  55. aiagents4pharma/talk2biomodels/tests/__init__.py +3 -0
  56. aiagents4pharma/talk2biomodels/tests/article_on_model_537.pdf +0 -0
  57. aiagents4pharma/talk2biomodels/tests/test_api.py +31 -0
  58. aiagents4pharma/talk2biomodels/tests/test_ask_question.py +42 -0
  59. aiagents4pharma/talk2biomodels/tests/test_basico_model.py +67 -0
  60. aiagents4pharma/talk2biomodels/tests/test_get_annotation.py +190 -0
  61. aiagents4pharma/talk2biomodels/tests/test_getmodelinfo.py +92 -0
  62. aiagents4pharma/talk2biomodels/tests/test_integration.py +116 -0
  63. aiagents4pharma/talk2biomodels/tests/test_load_biomodel.py +35 -0
  64. aiagents4pharma/talk2biomodels/tests/test_param_scan.py +71 -0
  65. aiagents4pharma/talk2biomodels/tests/test_query_article.py +184 -0
  66. aiagents4pharma/talk2biomodels/tests/test_save_model.py +47 -0
  67. aiagents4pharma/talk2biomodels/tests/test_search_models.py +35 -0
  68. aiagents4pharma/talk2biomodels/tests/test_simulate_model.py +44 -0
  69. aiagents4pharma/talk2biomodels/tests/test_steady_state.py +86 -0
  70. aiagents4pharma/talk2biomodels/tests/test_sys_bio_model.py +67 -0
  71. aiagents4pharma/talk2biomodels/tools/__init__.py +17 -0
  72. aiagents4pharma/talk2biomodels/tools/ask_question.py +125 -0
  73. aiagents4pharma/talk2biomodels/tools/custom_plotter.py +165 -0
  74. aiagents4pharma/talk2biomodels/tools/get_annotation.py +342 -0
  75. aiagents4pharma/talk2biomodels/tools/get_modelinfo.py +159 -0
  76. aiagents4pharma/talk2biomodels/tools/load_arguments.py +134 -0
  77. aiagents4pharma/talk2biomodels/tools/load_biomodel.py +44 -0
  78. aiagents4pharma/talk2biomodels/tools/parameter_scan.py +310 -0
  79. aiagents4pharma/talk2biomodels/tools/query_article.py +64 -0
  80. aiagents4pharma/talk2biomodels/tools/save_model.py +98 -0
  81. aiagents4pharma/talk2biomodels/tools/search_models.py +96 -0
  82. aiagents4pharma/talk2biomodels/tools/simulate_model.py +137 -0
  83. aiagents4pharma/talk2biomodels/tools/steady_state.py +187 -0
  84. aiagents4pharma/talk2biomodels/tools/utils.py +23 -0
  85. aiagents4pharma/talk2cells/README.md +1 -0
  86. aiagents4pharma/talk2cells/__init__.py +5 -0
  87. aiagents4pharma/talk2cells/agents/__init__.py +6 -0
  88. aiagents4pharma/talk2cells/agents/scp_agent.py +87 -0
  89. aiagents4pharma/talk2cells/states/__init__.py +6 -0
  90. aiagents4pharma/talk2cells/states/state_talk2cells.py +15 -0
  91. aiagents4pharma/talk2cells/tests/scp_agent/test_scp_agent.py +22 -0
  92. aiagents4pharma/talk2cells/tools/__init__.py +6 -0
  93. aiagents4pharma/talk2cells/tools/scp_agent/__init__.py +6 -0
  94. aiagents4pharma/talk2cells/tools/scp_agent/display_studies.py +27 -0
  95. aiagents4pharma/talk2cells/tools/scp_agent/search_studies.py +78 -0
  96. aiagents4pharma/talk2knowledgegraphs/.dockerignore +13 -0
  97. aiagents4pharma/talk2knowledgegraphs/Dockerfile +131 -0
  98. aiagents4pharma/talk2knowledgegraphs/README.md +1 -0
  99. aiagents4pharma/talk2knowledgegraphs/__init__.py +5 -0
  100. aiagents4pharma/talk2knowledgegraphs/agents/__init__.py +5 -0
  101. aiagents4pharma/talk2knowledgegraphs/agents/t2kg_agent.py +99 -0
  102. aiagents4pharma/talk2knowledgegraphs/configs/__init__.py +5 -0
  103. aiagents4pharma/talk2knowledgegraphs/configs/agents/t2kg_agent/__init__.py +3 -0
  104. aiagents4pharma/talk2knowledgegraphs/configs/agents/t2kg_agent/default.yaml +62 -0
  105. aiagents4pharma/talk2knowledgegraphs/configs/app/__init__.py +5 -0
  106. aiagents4pharma/talk2knowledgegraphs/configs/app/frontend/__init__.py +3 -0
  107. aiagents4pharma/talk2knowledgegraphs/configs/app/frontend/default.yaml +79 -0
  108. aiagents4pharma/talk2knowledgegraphs/configs/config.yaml +13 -0
  109. aiagents4pharma/talk2knowledgegraphs/configs/tools/__init__.py +5 -0
  110. aiagents4pharma/talk2knowledgegraphs/configs/tools/graphrag_reasoning/__init__.py +3 -0
  111. aiagents4pharma/talk2knowledgegraphs/configs/tools/graphrag_reasoning/default.yaml +24 -0
  112. aiagents4pharma/talk2knowledgegraphs/configs/tools/multimodal_subgraph_extraction/__init__.py +0 -0
  113. aiagents4pharma/talk2knowledgegraphs/configs/tools/multimodal_subgraph_extraction/default.yaml +33 -0
  114. aiagents4pharma/talk2knowledgegraphs/configs/tools/subgraph_extraction/__init__.py +3 -0
  115. aiagents4pharma/talk2knowledgegraphs/configs/tools/subgraph_extraction/default.yaml +43 -0
  116. aiagents4pharma/talk2knowledgegraphs/configs/tools/subgraph_summarization/__init__.py +3 -0
  117. aiagents4pharma/talk2knowledgegraphs/configs/tools/subgraph_summarization/default.yaml +9 -0
  118. aiagents4pharma/talk2knowledgegraphs/configs/utils/database/milvus/__init__.py +3 -0
  119. aiagents4pharma/talk2knowledgegraphs/configs/utils/database/milvus/default.yaml +61 -0
  120. aiagents4pharma/talk2knowledgegraphs/configs/utils/enrichments/ols_terms/default.yaml +3 -0
  121. aiagents4pharma/talk2knowledgegraphs/configs/utils/enrichments/reactome_pathways/default.yaml +3 -0
  122. aiagents4pharma/talk2knowledgegraphs/configs/utils/enrichments/uniprot_proteins/default.yaml +6 -0
  123. aiagents4pharma/talk2knowledgegraphs/configs/utils/pubchem_utils/default.yaml +5 -0
  124. aiagents4pharma/talk2knowledgegraphs/datasets/__init__.py +5 -0
  125. aiagents4pharma/talk2knowledgegraphs/datasets/biobridge_primekg.py +607 -0
  126. aiagents4pharma/talk2knowledgegraphs/datasets/dataset.py +25 -0
  127. aiagents4pharma/talk2knowledgegraphs/datasets/primekg.py +212 -0
  128. aiagents4pharma/talk2knowledgegraphs/datasets/starkqa_primekg.py +210 -0
  129. aiagents4pharma/talk2knowledgegraphs/docker-compose/cpu/.env.example +23 -0
  130. aiagents4pharma/talk2knowledgegraphs/docker-compose/cpu/docker-compose.yml +93 -0
  131. aiagents4pharma/talk2knowledgegraphs/docker-compose/gpu/.env.example +23 -0
  132. aiagents4pharma/talk2knowledgegraphs/docker-compose/gpu/docker-compose.yml +108 -0
  133. aiagents4pharma/talk2knowledgegraphs/entrypoint.sh +180 -0
  134. aiagents4pharma/talk2knowledgegraphs/install.md +165 -0
  135. aiagents4pharma/talk2knowledgegraphs/milvus_data_dump.py +886 -0
  136. aiagents4pharma/talk2knowledgegraphs/states/__init__.py +5 -0
  137. aiagents4pharma/talk2knowledgegraphs/states/state_talk2knowledgegraphs.py +40 -0
  138. aiagents4pharma/talk2knowledgegraphs/tests/__init__.py +0 -0
  139. aiagents4pharma/talk2knowledgegraphs/tests/test_agents_t2kg_agent.py +318 -0
  140. aiagents4pharma/talk2knowledgegraphs/tests/test_datasets_biobridge_primekg.py +248 -0
  141. aiagents4pharma/talk2knowledgegraphs/tests/test_datasets_dataset.py +33 -0
  142. aiagents4pharma/talk2knowledgegraphs/tests/test_datasets_primekg.py +86 -0
  143. aiagents4pharma/talk2knowledgegraphs/tests/test_datasets_starkqa_primekg.py +125 -0
  144. aiagents4pharma/talk2knowledgegraphs/tests/test_tools_graphrag_reasoning.py +257 -0
  145. aiagents4pharma/talk2knowledgegraphs/tests/test_tools_milvus_multimodal_subgraph_extraction.py +1444 -0
  146. aiagents4pharma/talk2knowledgegraphs/tests/test_tools_multimodal_subgraph_extraction.py +159 -0
  147. aiagents4pharma/talk2knowledgegraphs/tests/test_tools_subgraph_extraction.py +152 -0
  148. aiagents4pharma/talk2knowledgegraphs/tests/test_tools_subgraph_summarization.py +201 -0
  149. aiagents4pharma/talk2knowledgegraphs/tests/test_utils_database_milvus_connection_manager.py +812 -0
  150. aiagents4pharma/talk2knowledgegraphs/tests/test_utils_embeddings_embeddings.py +51 -0
  151. aiagents4pharma/talk2knowledgegraphs/tests/test_utils_embeddings_huggingface.py +49 -0
  152. aiagents4pharma/talk2knowledgegraphs/tests/test_utils_embeddings_nim_molmim.py +59 -0
  153. aiagents4pharma/talk2knowledgegraphs/tests/test_utils_embeddings_ollama.py +63 -0
  154. aiagents4pharma/talk2knowledgegraphs/tests/test_utils_embeddings_sentencetransformer.py +47 -0
  155. aiagents4pharma/talk2knowledgegraphs/tests/test_utils_enrichments_enrichments.py +40 -0
  156. aiagents4pharma/talk2knowledgegraphs/tests/test_utils_enrichments_ollama.py +94 -0
  157. aiagents4pharma/talk2knowledgegraphs/tests/test_utils_enrichments_ols.py +70 -0
  158. aiagents4pharma/talk2knowledgegraphs/tests/test_utils_enrichments_pubchem.py +45 -0
  159. aiagents4pharma/talk2knowledgegraphs/tests/test_utils_enrichments_reactome.py +44 -0
  160. aiagents4pharma/talk2knowledgegraphs/tests/test_utils_enrichments_uniprot.py +48 -0
  161. aiagents4pharma/talk2knowledgegraphs/tests/test_utils_extractions_milvus_multimodal_pcst.py +759 -0
  162. aiagents4pharma/talk2knowledgegraphs/tests/test_utils_kg_utils.py +78 -0
  163. aiagents4pharma/talk2knowledgegraphs/tests/test_utils_pubchem_utils.py +123 -0
  164. aiagents4pharma/talk2knowledgegraphs/tools/__init__.py +11 -0
  165. aiagents4pharma/talk2knowledgegraphs/tools/graphrag_reasoning.py +138 -0
  166. aiagents4pharma/talk2knowledgegraphs/tools/load_arguments.py +22 -0
  167. aiagents4pharma/talk2knowledgegraphs/tools/milvus_multimodal_subgraph_extraction.py +965 -0
  168. aiagents4pharma/talk2knowledgegraphs/tools/multimodal_subgraph_extraction.py +374 -0
  169. aiagents4pharma/talk2knowledgegraphs/tools/subgraph_extraction.py +291 -0
  170. aiagents4pharma/talk2knowledgegraphs/tools/subgraph_summarization.py +123 -0
  171. aiagents4pharma/talk2knowledgegraphs/utils/__init__.py +5 -0
  172. aiagents4pharma/talk2knowledgegraphs/utils/database/__init__.py +5 -0
  173. aiagents4pharma/talk2knowledgegraphs/utils/database/milvus_connection_manager.py +586 -0
  174. aiagents4pharma/talk2knowledgegraphs/utils/embeddings/__init__.py +5 -0
  175. aiagents4pharma/talk2knowledgegraphs/utils/embeddings/embeddings.py +81 -0
  176. aiagents4pharma/talk2knowledgegraphs/utils/embeddings/huggingface.py +111 -0
  177. aiagents4pharma/talk2knowledgegraphs/utils/embeddings/nim_molmim.py +54 -0
  178. aiagents4pharma/talk2knowledgegraphs/utils/embeddings/ollama.py +87 -0
  179. aiagents4pharma/talk2knowledgegraphs/utils/embeddings/sentence_transformer.py +73 -0
  180. aiagents4pharma/talk2knowledgegraphs/utils/enrichments/__init__.py +12 -0
  181. aiagents4pharma/talk2knowledgegraphs/utils/enrichments/enrichments.py +37 -0
  182. aiagents4pharma/talk2knowledgegraphs/utils/enrichments/ollama.py +129 -0
  183. aiagents4pharma/talk2knowledgegraphs/utils/enrichments/ols_terms.py +89 -0
  184. aiagents4pharma/talk2knowledgegraphs/utils/enrichments/pubchem_strings.py +78 -0
  185. aiagents4pharma/talk2knowledgegraphs/utils/enrichments/reactome_pathways.py +71 -0
  186. aiagents4pharma/talk2knowledgegraphs/utils/enrichments/uniprot_proteins.py +98 -0
  187. aiagents4pharma/talk2knowledgegraphs/utils/extractions/__init__.py +5 -0
  188. aiagents4pharma/talk2knowledgegraphs/utils/extractions/milvus_multimodal_pcst.py +762 -0
  189. aiagents4pharma/talk2knowledgegraphs/utils/extractions/multimodal_pcst.py +298 -0
  190. aiagents4pharma/talk2knowledgegraphs/utils/extractions/pcst.py +229 -0
  191. aiagents4pharma/talk2knowledgegraphs/utils/kg_utils.py +67 -0
  192. aiagents4pharma/talk2knowledgegraphs/utils/pubchem_utils.py +104 -0
  193. aiagents4pharma/talk2scholars/.dockerignore +13 -0
  194. aiagents4pharma/talk2scholars/Dockerfile +104 -0
  195. aiagents4pharma/talk2scholars/README.md +1 -0
  196. aiagents4pharma/talk2scholars/__init__.py +7 -0
  197. aiagents4pharma/talk2scholars/agents/__init__.py +13 -0
  198. aiagents4pharma/talk2scholars/agents/main_agent.py +89 -0
  199. aiagents4pharma/talk2scholars/agents/paper_download_agent.py +96 -0
  200. aiagents4pharma/talk2scholars/agents/pdf_agent.py +101 -0
  201. aiagents4pharma/talk2scholars/agents/s2_agent.py +135 -0
  202. aiagents4pharma/talk2scholars/agents/zotero_agent.py +127 -0
  203. aiagents4pharma/talk2scholars/configs/__init__.py +7 -0
  204. aiagents4pharma/talk2scholars/configs/agents/__init__.py +7 -0
  205. aiagents4pharma/talk2scholars/configs/agents/talk2scholars/__init__.py +7 -0
  206. aiagents4pharma/talk2scholars/configs/agents/talk2scholars/main_agent/__init__.py +3 -0
  207. aiagents4pharma/talk2scholars/configs/agents/talk2scholars/main_agent/default.yaml +52 -0
  208. aiagents4pharma/talk2scholars/configs/agents/talk2scholars/paper_download_agent/__init__.py +3 -0
  209. aiagents4pharma/talk2scholars/configs/agents/talk2scholars/paper_download_agent/default.yaml +19 -0
  210. aiagents4pharma/talk2scholars/configs/agents/talk2scholars/pdf_agent/__init__.py +3 -0
  211. aiagents4pharma/talk2scholars/configs/agents/talk2scholars/pdf_agent/default.yaml +19 -0
  212. aiagents4pharma/talk2scholars/configs/agents/talk2scholars/s2_agent/__init__.py +3 -0
  213. aiagents4pharma/talk2scholars/configs/agents/talk2scholars/s2_agent/default.yaml +44 -0
  214. aiagents4pharma/talk2scholars/configs/agents/talk2scholars/zotero_agent/__init__.py +3 -0
  215. aiagents4pharma/talk2scholars/configs/agents/talk2scholars/zotero_agent/default.yaml +19 -0
  216. aiagents4pharma/talk2scholars/configs/app/__init__.py +7 -0
  217. aiagents4pharma/talk2scholars/configs/app/frontend/__init__.py +3 -0
  218. aiagents4pharma/talk2scholars/configs/app/frontend/default.yaml +72 -0
  219. aiagents4pharma/talk2scholars/configs/config.yaml +16 -0
  220. aiagents4pharma/talk2scholars/configs/tools/__init__.py +21 -0
  221. aiagents4pharma/talk2scholars/configs/tools/multi_paper_recommendation/__init__.py +3 -0
  222. aiagents4pharma/talk2scholars/configs/tools/multi_paper_recommendation/default.yaml +26 -0
  223. aiagents4pharma/talk2scholars/configs/tools/paper_download/__init__.py +3 -0
  224. aiagents4pharma/talk2scholars/configs/tools/paper_download/default.yaml +124 -0
  225. aiagents4pharma/talk2scholars/configs/tools/question_and_answer/__init__.py +3 -0
  226. aiagents4pharma/talk2scholars/configs/tools/question_and_answer/default.yaml +62 -0
  227. aiagents4pharma/talk2scholars/configs/tools/retrieve_semantic_scholar_paper_id/__init__.py +3 -0
  228. aiagents4pharma/talk2scholars/configs/tools/retrieve_semantic_scholar_paper_id/default.yaml +12 -0
  229. aiagents4pharma/talk2scholars/configs/tools/search/__init__.py +3 -0
  230. aiagents4pharma/talk2scholars/configs/tools/search/default.yaml +26 -0
  231. aiagents4pharma/talk2scholars/configs/tools/single_paper_recommendation/__init__.py +3 -0
  232. aiagents4pharma/talk2scholars/configs/tools/single_paper_recommendation/default.yaml +26 -0
  233. aiagents4pharma/talk2scholars/configs/tools/zotero_read/__init__.py +3 -0
  234. aiagents4pharma/talk2scholars/configs/tools/zotero_read/default.yaml +57 -0
  235. aiagents4pharma/talk2scholars/configs/tools/zotero_write/__inti__.py +3 -0
  236. aiagents4pharma/talk2scholars/configs/tools/zotero_write/default.yaml +55 -0
  237. aiagents4pharma/talk2scholars/docker-compose/cpu/.env.example +21 -0
  238. aiagents4pharma/talk2scholars/docker-compose/cpu/docker-compose.yml +90 -0
  239. aiagents4pharma/talk2scholars/docker-compose/gpu/.env.example +21 -0
  240. aiagents4pharma/talk2scholars/docker-compose/gpu/docker-compose.yml +105 -0
  241. aiagents4pharma/talk2scholars/install.md +122 -0
  242. aiagents4pharma/talk2scholars/state/__init__.py +7 -0
  243. aiagents4pharma/talk2scholars/state/state_talk2scholars.py +98 -0
  244. aiagents4pharma/talk2scholars/tests/__init__.py +3 -0
  245. aiagents4pharma/talk2scholars/tests/test_agents_main_agent.py +256 -0
  246. aiagents4pharma/talk2scholars/tests/test_agents_paper_agents_download_agent.py +139 -0
  247. aiagents4pharma/talk2scholars/tests/test_agents_pdf_agent.py +114 -0
  248. aiagents4pharma/talk2scholars/tests/test_agents_s2_agent.py +198 -0
  249. aiagents4pharma/talk2scholars/tests/test_agents_zotero_agent.py +160 -0
  250. aiagents4pharma/talk2scholars/tests/test_s2_tools_display_dataframe.py +91 -0
  251. aiagents4pharma/talk2scholars/tests/test_s2_tools_query_dataframe.py +191 -0
  252. aiagents4pharma/talk2scholars/tests/test_states_state.py +38 -0
  253. aiagents4pharma/talk2scholars/tests/test_tools_paper_downloader.py +507 -0
  254. aiagents4pharma/talk2scholars/tests/test_tools_question_and_answer_tool.py +105 -0
  255. aiagents4pharma/talk2scholars/tests/test_tools_s2_multi.py +307 -0
  256. aiagents4pharma/talk2scholars/tests/test_tools_s2_retrieve.py +67 -0
  257. aiagents4pharma/talk2scholars/tests/test_tools_s2_search.py +286 -0
  258. aiagents4pharma/talk2scholars/tests/test_tools_s2_single.py +298 -0
  259. aiagents4pharma/talk2scholars/tests/test_utils_arxiv_downloader.py +469 -0
  260. aiagents4pharma/talk2scholars/tests/test_utils_base_paper_downloader.py +598 -0
  261. aiagents4pharma/talk2scholars/tests/test_utils_biorxiv_downloader.py +669 -0
  262. aiagents4pharma/talk2scholars/tests/test_utils_medrxiv_downloader.py +500 -0
  263. aiagents4pharma/talk2scholars/tests/test_utils_nvidia_nim_reranker.py +117 -0
  264. aiagents4pharma/talk2scholars/tests/test_utils_pdf_answer_formatter.py +67 -0
  265. aiagents4pharma/talk2scholars/tests/test_utils_pdf_batch_processor.py +92 -0
  266. aiagents4pharma/talk2scholars/tests/test_utils_pdf_collection_manager.py +173 -0
  267. aiagents4pharma/talk2scholars/tests/test_utils_pdf_document_processor.py +68 -0
  268. aiagents4pharma/talk2scholars/tests/test_utils_pdf_generate_answer.py +72 -0
  269. aiagents4pharma/talk2scholars/tests/test_utils_pdf_gpu_detection.py +129 -0
  270. aiagents4pharma/talk2scholars/tests/test_utils_pdf_paper_loader.py +116 -0
  271. aiagents4pharma/talk2scholars/tests/test_utils_pdf_rag_pipeline.py +88 -0
  272. aiagents4pharma/talk2scholars/tests/test_utils_pdf_retrieve_chunks.py +190 -0
  273. aiagents4pharma/talk2scholars/tests/test_utils_pdf_singleton_manager.py +159 -0
  274. aiagents4pharma/talk2scholars/tests/test_utils_pdf_vector_normalization.py +121 -0
  275. aiagents4pharma/talk2scholars/tests/test_utils_pdf_vector_store.py +406 -0
  276. aiagents4pharma/talk2scholars/tests/test_utils_pubmed_downloader.py +1007 -0
  277. aiagents4pharma/talk2scholars/tests/test_utils_read_helper_utils.py +106 -0
  278. aiagents4pharma/talk2scholars/tests/test_utils_s2_utils_ext_ids.py +403 -0
  279. aiagents4pharma/talk2scholars/tests/test_utils_tool_helper_utils.py +85 -0
  280. aiagents4pharma/talk2scholars/tests/test_utils_zotero_human_in_the_loop.py +266 -0
  281. aiagents4pharma/talk2scholars/tests/test_utils_zotero_path.py +496 -0
  282. aiagents4pharma/talk2scholars/tests/test_utils_zotero_pdf_downloader_utils.py +46 -0
  283. aiagents4pharma/talk2scholars/tests/test_utils_zotero_read.py +743 -0
  284. aiagents4pharma/talk2scholars/tests/test_utils_zotero_write.py +151 -0
  285. aiagents4pharma/talk2scholars/tools/__init__.py +9 -0
  286. aiagents4pharma/talk2scholars/tools/paper_download/__init__.py +12 -0
  287. aiagents4pharma/talk2scholars/tools/paper_download/paper_downloader.py +442 -0
  288. aiagents4pharma/talk2scholars/tools/paper_download/utils/__init__.py +22 -0
  289. aiagents4pharma/talk2scholars/tools/paper_download/utils/arxiv_downloader.py +207 -0
  290. aiagents4pharma/talk2scholars/tools/paper_download/utils/base_paper_downloader.py +336 -0
  291. aiagents4pharma/talk2scholars/tools/paper_download/utils/biorxiv_downloader.py +313 -0
  292. aiagents4pharma/talk2scholars/tools/paper_download/utils/medrxiv_downloader.py +196 -0
  293. aiagents4pharma/talk2scholars/tools/paper_download/utils/pubmed_downloader.py +323 -0
  294. aiagents4pharma/talk2scholars/tools/pdf/__init__.py +7 -0
  295. aiagents4pharma/talk2scholars/tools/pdf/question_and_answer.py +170 -0
  296. aiagents4pharma/talk2scholars/tools/pdf/utils/__init__.py +37 -0
  297. aiagents4pharma/talk2scholars/tools/pdf/utils/answer_formatter.py +62 -0
  298. aiagents4pharma/talk2scholars/tools/pdf/utils/batch_processor.py +198 -0
  299. aiagents4pharma/talk2scholars/tools/pdf/utils/collection_manager.py +172 -0
  300. aiagents4pharma/talk2scholars/tools/pdf/utils/document_processor.py +76 -0
  301. aiagents4pharma/talk2scholars/tools/pdf/utils/generate_answer.py +97 -0
  302. aiagents4pharma/talk2scholars/tools/pdf/utils/get_vectorstore.py +59 -0
  303. aiagents4pharma/talk2scholars/tools/pdf/utils/gpu_detection.py +150 -0
  304. aiagents4pharma/talk2scholars/tools/pdf/utils/nvidia_nim_reranker.py +97 -0
  305. aiagents4pharma/talk2scholars/tools/pdf/utils/paper_loader.py +123 -0
  306. aiagents4pharma/talk2scholars/tools/pdf/utils/rag_pipeline.py +113 -0
  307. aiagents4pharma/talk2scholars/tools/pdf/utils/retrieve_chunks.py +197 -0
  308. aiagents4pharma/talk2scholars/tools/pdf/utils/singleton_manager.py +140 -0
  309. aiagents4pharma/talk2scholars/tools/pdf/utils/tool_helper.py +86 -0
  310. aiagents4pharma/talk2scholars/tools/pdf/utils/vector_normalization.py +150 -0
  311. aiagents4pharma/talk2scholars/tools/pdf/utils/vector_store.py +327 -0
  312. aiagents4pharma/talk2scholars/tools/s2/__init__.py +21 -0
  313. aiagents4pharma/talk2scholars/tools/s2/display_dataframe.py +110 -0
  314. aiagents4pharma/talk2scholars/tools/s2/multi_paper_rec.py +111 -0
  315. aiagents4pharma/talk2scholars/tools/s2/query_dataframe.py +233 -0
  316. aiagents4pharma/talk2scholars/tools/s2/retrieve_semantic_scholar_paper_id.py +128 -0
  317. aiagents4pharma/talk2scholars/tools/s2/search.py +101 -0
  318. aiagents4pharma/talk2scholars/tools/s2/single_paper_rec.py +102 -0
  319. aiagents4pharma/talk2scholars/tools/s2/utils/__init__.py +5 -0
  320. aiagents4pharma/talk2scholars/tools/s2/utils/multi_helper.py +223 -0
  321. aiagents4pharma/talk2scholars/tools/s2/utils/search_helper.py +205 -0
  322. aiagents4pharma/talk2scholars/tools/s2/utils/single_helper.py +216 -0
  323. aiagents4pharma/talk2scholars/tools/zotero/__init__.py +7 -0
  324. aiagents4pharma/talk2scholars/tools/zotero/utils/__init__.py +7 -0
  325. aiagents4pharma/talk2scholars/tools/zotero/utils/read_helper.py +270 -0
  326. aiagents4pharma/talk2scholars/tools/zotero/utils/review_helper.py +74 -0
  327. aiagents4pharma/talk2scholars/tools/zotero/utils/write_helper.py +194 -0
  328. aiagents4pharma/talk2scholars/tools/zotero/utils/zotero_path.py +180 -0
  329. aiagents4pharma/talk2scholars/tools/zotero/utils/zotero_pdf_downloader.py +133 -0
  330. aiagents4pharma/talk2scholars/tools/zotero/zotero_read.py +105 -0
  331. aiagents4pharma/talk2scholars/tools/zotero/zotero_review.py +162 -0
  332. aiagents4pharma/talk2scholars/tools/zotero/zotero_write.py +91 -0
  333. aiagents4pharma-0.0.0.dist-info/METADATA +335 -0
  334. aiagents4pharma-0.0.0.dist-info/RECORD +336 -0
  335. aiagents4pharma-0.0.0.dist-info/WHEEL +4 -0
  336. aiagents4pharma-0.0.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,180 @@
1
+ #!/usr/bin/env python3
2
+
3
+ """
4
+ Utility functions for Zotero path operations.
5
+ """
6
+
7
+ import logging
8
+
9
+ # Configure logging
10
+ logging.basicConfig(level=logging.INFO)
11
+ logger = logging.getLogger(__name__)
12
+ # pylint: disable=broad-exception-caught
13
+
14
+
15
+ def get_item_collections(zot):
16
+ """
17
+ Fetch all Zotero collections and map item keys to their full collection paths.
18
+
19
+ Args:
20
+ zot (Zotero): An initialized Zotero client.
21
+
22
+ Returns:
23
+ dict: A dictionary mapping item keys to a list of full collection paths.
24
+ """
25
+ logger.info("Fetching Zotero collections...")
26
+
27
+ # Fetch all collections
28
+ collections = zot.collections()
29
+
30
+ # Create mappings: collection key → name and collection key → parent key
31
+ collection_map = {col["key"]: col["data"]["name"] for col in collections}
32
+ parent_map = {col["key"]: col["data"].get("parentCollection") for col in collections}
33
+
34
+ # Build full paths for collections
35
+ def build_collection_path(col_key):
36
+ """build collection path from collection key"""
37
+ path = []
38
+ while col_key:
39
+ path.insert(0, collection_map.get(col_key, "Unknown"))
40
+ col_key = parent_map.get(col_key)
41
+ return "/" + "/".join(path) # Convert to "/path/to/collection"
42
+
43
+ collection_paths = {key: build_collection_path(key) for key in collection_map}
44
+
45
+ # Manually create an item-to-collection mapping with full paths
46
+ item_to_collections = {}
47
+
48
+ for collection in collections:
49
+ collection_key = collection["key"]
50
+ collection_items = zot.collection_items(collection_key) # Fetch items in the collection
51
+
52
+ for item in collection_items:
53
+ item_key = item["data"]["key"]
54
+ if item_key in item_to_collections:
55
+ item_to_collections[item_key].append(collection_paths[collection_key])
56
+ else:
57
+ item_to_collections[item_key] = [collection_paths[collection_key]]
58
+
59
+ logger.info("Successfully mapped items to collection paths.")
60
+
61
+ return item_to_collections
62
+
63
+
64
+ def find_or_create_collection(zot, path, create_missing=False):
65
+ """find collection or create if missing"""
66
+ logger.info("Finding collection for path: %s (create_missing=%s)", path, create_missing)
67
+ # Normalize path: remove leading/trailing slashes and convert to lowercase
68
+ normalized = path.strip("/").lower()
69
+ path_parts = normalized.split("/") if normalized else []
70
+
71
+ if not path_parts:
72
+ logger.warning("Empty path provided")
73
+ return None
74
+
75
+ # Get all collections from Zotero
76
+ all_collections = zot.collections()
77
+ logger.info("Found %d collections in Zotero", len(all_collections))
78
+
79
+ # Determine target name (last part) and, if nested, find the parent's key
80
+ target_name = path_parts[-1]
81
+ parent_key = None
82
+ if len(path_parts) > 1:
83
+ parent_name = path_parts[-2]
84
+ # Look for a collection with name matching the parent (case-insensitive)
85
+ for col in all_collections:
86
+ if col["data"]["name"].lower() == parent_name:
87
+ parent_key = col["key"]
88
+ break
89
+
90
+ # Try to find an existing collection by direct match (ignoring hierarchy)
91
+ for col in all_collections:
92
+ if col["data"]["name"].lower() == target_name:
93
+ logger.info("Found direct match for %s: %s", target_name, col["key"])
94
+ return col["key"]
95
+
96
+ # No match found: create one if allowed
97
+ if create_missing:
98
+ payload = {"name": target_name}
99
+ if parent_key:
100
+ payload["parentCollection"] = parent_key
101
+ try:
102
+ result = zot.create_collection(payload)
103
+ # Interpret result based on structure
104
+ if "success" in result:
105
+ new_key = result["success"]["0"]
106
+ else:
107
+ new_key = result["successful"]["0"]["data"]["key"]
108
+ logger.info("Created collection %s with key %s", target_name, new_key)
109
+ return new_key
110
+ except Exception as e:
111
+ logger.error("Failed to create collection: %s", e)
112
+ return None
113
+ else:
114
+ logger.warning("No matching collection found for %s", target_name)
115
+ return None
116
+
117
+
118
+ def get_all_collection_paths(zot):
119
+ """
120
+ Get all available collection paths in Zotero.
121
+
122
+ Args:
123
+ zot (Zotero): An initialized Zotero client.
124
+
125
+ Returns:
126
+ list: List of all available collection paths
127
+ """
128
+ logger.info("Getting all collection paths")
129
+ collections = zot.collections()
130
+
131
+ # Create mappings: collection key → name and collection key → parent key
132
+ collection_map = {col["key"]: col["data"]["name"] for col in collections}
133
+ parent_map = {col["key"]: col["data"].get("parentCollection") for col in collections}
134
+
135
+ # Build full paths for collections
136
+ def build_collection_path(col_key):
137
+ path = []
138
+ while col_key:
139
+ path.insert(0, collection_map.get(col_key, "Unknown"))
140
+ col_key = parent_map.get(col_key)
141
+ return "/" + "/".join(path)
142
+
143
+ collection_paths = [build_collection_path(key) for key in collection_map]
144
+ logger.info("Found %d collection paths", len(collection_paths))
145
+ return collection_paths
146
+
147
+
148
+ def fetch_papers_for_save(state):
149
+ """
150
+ Retrieve papers from the state for saving to Zotero.
151
+
152
+ Args:
153
+ state (dict): The state containing previously fetched papers.
154
+
155
+ Returns:
156
+ dict: Dictionary of papers to save, or None if no papers found
157
+ """
158
+ logger.info("Fetching papers from state for saving")
159
+
160
+ # Retrieve last displayed papers from the agent state
161
+ last_displayed_key = state.get("last_displayed_papers", "")
162
+
163
+ if not last_displayed_key:
164
+ logger.warning("No last_displayed_papers key in state")
165
+ return None
166
+
167
+ if isinstance(last_displayed_key, str):
168
+ # If it's a string (key to another state object), get that object
169
+ fetched_papers = state.get(last_displayed_key, {})
170
+ logger.info("Using papers from '%s' state key", last_displayed_key)
171
+ else:
172
+ # If it's already the papers object
173
+ fetched_papers = last_displayed_key
174
+ logger.info("Using papers directly from last_displayed_papers")
175
+
176
+ if not fetched_papers:
177
+ logger.warning("No fetched papers found to save.")
178
+ return None
179
+
180
+ return fetched_papers
@@ -0,0 +1,133 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Utility functions for downloading PDFs from Zotero.
4
+ """
5
+
6
+ import concurrent.futures
7
+ import logging
8
+ import tempfile
9
+
10
+ import requests
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ def download_zotero_pdf(
16
+ session: requests.Session,
17
+ user_id: str,
18
+ api_key: str,
19
+ attachment_key: str,
20
+ **kwargs,
21
+ ) -> tuple[str, str] | None:
22
+ """
23
+ Download a PDF from Zotero by attachment key.
24
+
25
+ Args:
26
+ session: requests.Session for HTTP requests.
27
+ user_id: Zotero user ID.
28
+ api_key: Zotero API key.
29
+ attachment_key: Zotero attachment item key.
30
+ kwargs:
31
+ timeout (int): Request timeout in seconds (default: 10).
32
+ chunk_size (int, optional): Chunk size for streaming.
33
+
34
+ Returns:
35
+ Tuple of (local_file_path, filename) if successful, else None.
36
+ """
37
+ # Extract optional parameters
38
+ timeout = kwargs.get("timeout", 10)
39
+ chunk_size = kwargs.get("chunk_size")
40
+ # Log configured parameters for verification
41
+ logger.info("download_zotero_pdf params -> timeout=%s, chunk_size=%s", timeout, chunk_size)
42
+ # Log download start
43
+ logger.info("Downloading Zotero PDF for attachment %s from Zotero API", attachment_key)
44
+ zotero_pdf_url = f"https://api.zotero.org/users/{user_id}/items/{attachment_key}/file"
45
+ headers = {"Zotero-API-Key": api_key}
46
+
47
+ try:
48
+ response = session.get(zotero_pdf_url, headers=headers, stream=True, timeout=timeout)
49
+ response.raise_for_status()
50
+
51
+ # Download to a temporary file first
52
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") as temp_file:
53
+ for chunk in response.iter_content(chunk_size=chunk_size):
54
+ temp_file.write(chunk)
55
+ temp_file_path = temp_file.name
56
+ # Temp file written to %s
57
+ logger.info("Zotero PDF downloaded to temporary file: %s", temp_file_path)
58
+
59
+ # Determine filename from Content-Disposition header or default
60
+ if "filename=" in response.headers.get("Content-Disposition", ""):
61
+ filename = (
62
+ response.headers.get("Content-Disposition", "").split("filename=")[-1].strip('"')
63
+ )
64
+ else:
65
+ filename = "downloaded.pdf"
66
+
67
+ return temp_file_path, filename
68
+
69
+ except (requests.exceptions.RequestException, OSError) as e:
70
+ logger.error("Failed to download Zotero PDF for attachment %s: %s", attachment_key, e)
71
+ return None
72
+
73
+
74
+ def download_pdfs_in_parallel(
75
+ session: requests.Session,
76
+ user_id: str,
77
+ api_key: str,
78
+ attachment_item_map: dict[str, str],
79
+ **kwargs,
80
+ ) -> dict[str, tuple[str, str, str]]:
81
+ """
82
+ Download multiple PDFs in parallel using ThreadPoolExecutor.
83
+
84
+ Args:
85
+ session: requests.Session for HTTP requests.
86
+ user_id: Zotero user ID.
87
+ api_key: Zotero API key.
88
+ attachment_item_map: Mapping of attachment_key to parent item_key.
89
+ kwargs:
90
+ max_workers (int, optional): Maximum number of worker threads (default: min(10, n)).
91
+ chunk_size (int, optional): Chunk size for streaming.
92
+
93
+ Returns:
94
+ Mapping of parent item_key to (local_file_path, filename, attachment_key).
95
+ """
96
+ # Extract optional parameters
97
+ max_workers = kwargs.get("max_workers")
98
+ chunk_size = kwargs.get("chunk_size")
99
+ # Log configured parameters for verification
100
+ logger.info(
101
+ "download_pdfs_in_parallel params -> max_workers=%s, chunk_size=%s",
102
+ max_workers,
103
+ chunk_size,
104
+ )
105
+ results: dict[str, tuple[str, str, str]] = {}
106
+ if not attachment_item_map:
107
+ return results
108
+
109
+ with concurrent.futures.ThreadPoolExecutor(
110
+ max_workers=(max_workers if max_workers is not None else min(10, len(attachment_item_map)))
111
+ ) as executor:
112
+ future_to_keys = {
113
+ executor.submit(
114
+ download_zotero_pdf,
115
+ session,
116
+ user_id,
117
+ api_key,
118
+ attachment_key,
119
+ chunk_size=chunk_size,
120
+ ): (attachment_key, item_key)
121
+ for attachment_key, item_key in attachment_item_map.items()
122
+ }
123
+
124
+ for future in concurrent.futures.as_completed(future_to_keys):
125
+ attachment_key, item_key = future_to_keys[future]
126
+ try:
127
+ res = future.result()
128
+ if res:
129
+ results[item_key] = (*res, attachment_key)
130
+ except (requests.exceptions.RequestException, OSError) as e:
131
+ logger.error("Failed to download PDF for key %s: %s", attachment_key, e)
132
+
133
+ return results
@@ -0,0 +1,105 @@
1
+ #!/usr/bin/env python3
2
+
3
+ """
4
+ Zotero Read Tool
5
+
6
+ This LangGraph tool searches a user's Zotero library for items matching a query
7
+ and optionally downloads their PDF attachments. It returns structured metadata
8
+ for each found item and makes the results available as an artifact.
9
+ """
10
+
11
+ import logging
12
+ from typing import Annotated, Any
13
+
14
+ from langchain_core.messages import ToolMessage
15
+ from langchain_core.tools import tool
16
+ from langchain_core.tools.base import InjectedToolCallId
17
+ from langgraph.types import Command
18
+ from pydantic import BaseModel, Field
19
+
20
+ from .utils.read_helper import ZoteroSearchData
21
+
22
+ # Configure logging
23
+ logging.basicConfig(level=logging.INFO)
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ class ZoteroSearchInput(BaseModel):
28
+ """Input schema for the Zotero search tool.
29
+
30
+ Attributes:
31
+ query (str): Search string to match against item metadata.
32
+ only_articles (bool): If True, restrict results to 'journalArticle' and similar types.
33
+ limit (int): Maximum number of items to fetch from Zotero.
34
+ download_pdfs (bool): If True, download PDF attachments for each item.
35
+ tool_call_id (str): Internal identifier for this tool invocation.
36
+ """
37
+
38
+ query: str = Field(description="Search query string to find papers in Zotero library.")
39
+ only_articles: bool = Field(
40
+ default=True,
41
+ description="Whether to only search for journal articles/conference papers.",
42
+ )
43
+ limit: int = Field(default=2, description="Maximum number of results to return", ge=1, le=100)
44
+ download_pdfs: bool = Field(
45
+ default=False,
46
+ description="Whether to download PDF attachments immediately (default True).",
47
+ )
48
+ tool_call_id: Annotated[str, InjectedToolCallId]
49
+
50
+
51
+ @tool(args_schema=ZoteroSearchInput, parse_docstring=True)
52
+ def zotero_read(
53
+ query: str,
54
+ only_articles: bool,
55
+ tool_call_id: Annotated[str, InjectedToolCallId],
56
+ limit: int = 2,
57
+ download_pdfs: bool = False,
58
+ ) -> Command[Any]:
59
+ """
60
+ Execute a search on the Zotero library and return matching items.
61
+
62
+ Args:
63
+ query (str): Text query to search in titles, abstracts, tags, etc.
64
+ only_articles (bool): When True, only include items of type 'journalArticle'
65
+ or 'conferencePaper'.
66
+ tool_call_id (str): Internal ID injected by LangGraph to track this tool call.
67
+ limit (int, optional): Max number of items to return (1–100). Defaults to 2.
68
+ download_pdfs (bool, optional): If True, PDFs for each returned item will be downloaded now.
69
+ If False, only metadata is fetched. Defaults to False.
70
+
71
+ Returns:
72
+ Command[Any]: A LangGraph Command updating the agent state:
73
+ - 'article_data': dict mapping item keys to metadata (and 'pdf_url' if downloaded).
74
+ - 'last_displayed_papers': identifier pointing to the articles in state.
75
+ - 'messages': list containing a ToolMessage with a human-readable summary
76
+ and an 'artifact' referencing the raw article_data.
77
+ """
78
+ # Create search data object to organize variables
79
+ # download_pdfs flag controls whether PDFs are fetched now or deferred
80
+ search_data = ZoteroSearchData(
81
+ query=query,
82
+ only_articles=only_articles,
83
+ limit=limit,
84
+ download_pdfs=download_pdfs,
85
+ tool_call_id=tool_call_id,
86
+ )
87
+
88
+ # Process the search
89
+ search_data.process_search()
90
+ results = search_data.get_search_results()
91
+
92
+ return Command(
93
+ update={
94
+ "article_data": results["article_data"],
95
+ # Store the latest article_data mapping directly for display
96
+ "last_displayed_papers": results["article_data"],
97
+ "messages": [
98
+ ToolMessage(
99
+ content=results["content"],
100
+ tool_call_id=tool_call_id,
101
+ artifact=results["article_data"],
102
+ )
103
+ ],
104
+ }
105
+ )
@@ -0,0 +1,162 @@
1
+ #!/usr/bin/env python3
2
+
3
+ """
4
+ This tool implements human-in-the-loop review for Zotero write operations.
5
+ """
6
+
7
+ import logging
8
+ from typing import Annotated, Any, Literal
9
+
10
+ from langchain_core.messages import HumanMessage, ToolMessage
11
+ from langchain_core.tools import tool
12
+ from langchain_core.tools.base import InjectedToolCallId
13
+ from langgraph.prebuilt import InjectedState
14
+ from langgraph.types import Command, interrupt
15
+ from pydantic import BaseModel, Field
16
+
17
+ from .utils.review_helper import ReviewData
18
+ from .utils.zotero_path import fetch_papers_for_save
19
+
20
+ # Configure logging
21
+ logging.basicConfig(level=logging.INFO)
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ class ZoteroReviewDecision(BaseModel):
26
+ """
27
+ Structured output schema for the human review decision.
28
+ - decision: "approve", "reject", or "custom"
29
+ - custom_path: Optional custom collection path if the decision is "custom"
30
+ """
31
+
32
+ decision: Literal["approve", "reject", "custom"]
33
+ custom_path: str | None = None
34
+
35
+
36
+ class ZoteroReviewInput(BaseModel):
37
+ """Input schema for the Zotero review tool."""
38
+
39
+ tool_call_id: Annotated[str, InjectedToolCallId]
40
+ collection_path: str = Field(
41
+ description="The path where the paper should be saved in the Zotero library."
42
+ )
43
+ state: Annotated[dict, InjectedState]
44
+
45
+
46
+ @tool(args_schema=ZoteroReviewInput, parse_docstring=True)
47
+ def zotero_review(
48
+ tool_call_id: Annotated[str, InjectedToolCallId],
49
+ collection_path: str,
50
+ state: Annotated[dict, InjectedState],
51
+ ) -> Command[Any]:
52
+ """
53
+ Use this tool to get human review and approval before saving papers to Zotero.
54
+ This tool should be called before the zotero_write to ensure the user approves
55
+ the operation.
56
+
57
+ Args:
58
+ tool_call_id (str): The tool call ID.
59
+ collection_path (str): The Zotero collection path where papers should be saved.
60
+ state (dict): The state containing previously fetched papers.
61
+
62
+ Returns:
63
+ Command[Any]: The next action to take based on human input.
64
+ """
65
+ logger.info("Requesting human review for saving to collection: %s", collection_path)
66
+
67
+ # Use our utility function to fetch papers from state
68
+ fetched_papers = fetch_papers_for_save(state)
69
+
70
+ if not fetched_papers:
71
+ raise ValueError(
72
+ "No fetched papers were found to save. "
73
+ "Please retrieve papers using Zotero Read or Semantic Scholar first."
74
+ )
75
+
76
+ # Create review data object to organize variables
77
+ review_data = ReviewData(collection_path, fetched_papers, tool_call_id, state)
78
+
79
+ try:
80
+ # Interrupt the graph to get human approval
81
+ human_review = interrupt(review_data.review_info)
82
+ # Process human response using structured output via LLM
83
+ llm_model = state.get("llm_model")
84
+ if llm_model is None:
85
+ raise ValueError("LLM model is not available in the state.")
86
+ structured_llm = llm_model.with_structured_output(ZoteroReviewDecision)
87
+ # Convert the raw human response to a message for structured parsing
88
+ decision_response = structured_llm.invoke([HumanMessage(content=str(human_review))])
89
+
90
+ # Process the structured response
91
+ if decision_response.decision == "approve":
92
+ logger.info("User approved saving papers to Zotero")
93
+ return Command(
94
+ update={
95
+ "messages": [
96
+ ToolMessage(
97
+ content=review_data.get_approval_message(),
98
+ tool_call_id=tool_call_id,
99
+ )
100
+ ],
101
+ "zotero_write_approval_status": {
102
+ "collection_path": review_data.collection_path,
103
+ "approved": True,
104
+ },
105
+ }
106
+ )
107
+ if decision_response.decision == "custom" and decision_response.custom_path:
108
+ logger.info("User approved with custom path: %s", decision_response.custom_path)
109
+ return Command(
110
+ update={
111
+ "messages": [
112
+ ToolMessage(
113
+ content=review_data.get_custom_path_approval_message(
114
+ decision_response.custom_path
115
+ ),
116
+ tool_call_id=tool_call_id,
117
+ )
118
+ ],
119
+ "zotero_write_approval_status": {
120
+ "collection_path": decision_response.custom_path,
121
+ "approved": True,
122
+ },
123
+ }
124
+ )
125
+ logger.info("User rejected saving papers to Zotero")
126
+ return Command(
127
+ update={
128
+ "messages": [
129
+ ToolMessage(
130
+ content="Human rejected saving papers to Zotero.",
131
+ tool_call_id=tool_call_id,
132
+ )
133
+ ],
134
+ "zotero_write_approval_status": {"approved": False},
135
+ }
136
+ )
137
+ # pylint: disable=broad-except
138
+ except Exception as e:
139
+ # If interrupt or structured output processing fails, fallback to explicit confirmation
140
+ logger.warning("Structured review processing failed: %s", e)
141
+ return Command(
142
+ update={
143
+ "messages": [
144
+ ToolMessage(
145
+ content=(
146
+ f"REVIEW REQUIRED: Would you like to save "
147
+ f"{review_data.total_papers} papers to Zotero collection "
148
+ f"'{review_data.collection_path}'?\n\n"
149
+ f"Papers to save:\n{review_data.papers_preview}\n\n"
150
+ "Please respond with 'Yes' to confirm or 'No' to cancel."
151
+ ),
152
+ tool_call_id=tool_call_id,
153
+ )
154
+ ],
155
+ "zotero_write_approval_status": {
156
+ "collection_path": review_data.collection_path,
157
+ "papers_reviewed": True,
158
+ "approved": False, # Not approved yet
159
+ "papers_count": review_data.total_papers,
160
+ },
161
+ }
162
+ )
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env python3
2
+
3
+ """
4
+ This tool is used to save fetched papers to Zotero library after human approval.
5
+ """
6
+
7
+ import logging
8
+ from typing import Annotated, Any
9
+
10
+ from langchain_core.messages import ToolMessage
11
+ from langchain_core.tools import tool
12
+ from langchain_core.tools.base import InjectedToolCallId
13
+ from langgraph.prebuilt import InjectedState
14
+ from langgraph.types import Command
15
+ from pydantic import BaseModel, Field
16
+
17
+ from .utils.write_helper import ZoteroWriteData
18
+
19
+ # Configure logging
20
+ logging.basicConfig(level=logging.INFO)
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ class ZoteroSaveInput(BaseModel):
25
+ """Input schema for the Zotero save tool."""
26
+
27
+ tool_call_id: Annotated[str, InjectedToolCallId]
28
+ collection_path: str = Field(
29
+ description="The path where the paper should be saved in the Zotero library."
30
+ )
31
+ state: Annotated[dict, InjectedState]
32
+
33
+
34
+ @tool(args_schema=ZoteroSaveInput, parse_docstring=True)
35
+ def zotero_write(
36
+ tool_call_id: Annotated[str, InjectedToolCallId],
37
+ collection_path: str,
38
+ state: Annotated[dict, InjectedState],
39
+ ) -> Command[Any]:
40
+ """
41
+ Use this tool to save previously fetched papers from Semantic Scholar
42
+ to a specified Zotero collection after human approval.
43
+
44
+ This tool checks if the user has approved the save operation via the
45
+ zotero_review. If approved, it will save the papers to the
46
+ approved collection path.
47
+
48
+ Args:
49
+ tool_call_id (Annotated[str, InjectedToolCallId]): The tool call ID.
50
+ collection_path (str): The Zotero collection path where papers should be saved.
51
+ state (Annotated[dict, InjectedState]): The state containing previously fetched papers.
52
+ user_confirmation (str, optional): User confirmation message when interrupt is
53
+ not available.
54
+
55
+ Returns:
56
+ Command[Any]: The save results and related information.
57
+ """
58
+ # Create write data object to organize variables
59
+ write_data = ZoteroWriteData(tool_call_id, collection_path, state)
60
+
61
+ try:
62
+ # Process the write operation
63
+ results = write_data.process_write()
64
+
65
+ return Command(
66
+ update={
67
+ "messages": [
68
+ ToolMessage(
69
+ content=results["content"],
70
+ tool_call_id=tool_call_id,
71
+ artifact=results["fetched_papers"],
72
+ )
73
+ ],
74
+ "zotero_write_approval_status": {}, # Clear approval info
75
+ }
76
+ )
77
+ except ValueError as e:
78
+ # Only handle collection not found errors with a Command
79
+ if "collection path" in str(e).lower():
80
+ return Command(
81
+ update={
82
+ "messages": [
83
+ ToolMessage(
84
+ content=str(e),
85
+ tool_call_id=tool_call_id,
86
+ )
87
+ ],
88
+ }
89
+ )
90
+ # Let other ValueErrors (like no papers) propagate up
91
+ raise