aiagents4pharma 1.27.2__py3-none-any.whl → 1.29.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.
- aiagents4pharma/talk2scholars/agents/__init__.py +1 -0
- aiagents4pharma/talk2scholars/agents/main_agent.py +35 -209
- aiagents4pharma/talk2scholars/agents/pdf_agent.py +106 -0
- aiagents4pharma/talk2scholars/agents/s2_agent.py +10 -6
- aiagents4pharma/talk2scholars/agents/zotero_agent.py +12 -6
- aiagents4pharma/talk2scholars/configs/agents/talk2scholars/__init__.py +1 -0
- aiagents4pharma/talk2scholars/configs/agents/talk2scholars/main_agent/default.yaml +2 -48
- aiagents4pharma/talk2scholars/configs/agents/talk2scholars/pdf_agent/__init__.py +3 -0
- aiagents4pharma/talk2scholars/configs/agents/talk2scholars/s2_agent/default.yaml +5 -28
- aiagents4pharma/talk2scholars/configs/agents/talk2scholars/zotero_agent/default.yaml +5 -21
- aiagents4pharma/talk2scholars/configs/config.yaml +3 -0
- aiagents4pharma/talk2scholars/configs/tools/__init__.py +2 -0
- aiagents4pharma/talk2scholars/configs/tools/multi_paper_recommendation/default.yaml +1 -1
- aiagents4pharma/talk2scholars/configs/tools/question_and_answer/__init__.py +3 -0
- aiagents4pharma/talk2scholars/configs/tools/search/default.yaml +1 -1
- aiagents4pharma/talk2scholars/configs/tools/single_paper_recommendation/default.yaml +1 -1
- aiagents4pharma/talk2scholars/configs/tools/zotero_read/default.yaml +42 -1
- aiagents4pharma/talk2scholars/configs/tools/zotero_write/__inti__.py +3 -0
- aiagents4pharma/talk2scholars/state/state_talk2scholars.py +1 -0
- aiagents4pharma/talk2scholars/tests/test_main_agent.py +186 -111
- aiagents4pharma/talk2scholars/tests/test_pdf_agent.py +126 -0
- aiagents4pharma/talk2scholars/tests/test_question_and_answer_tool.py +186 -0
- aiagents4pharma/talk2scholars/tests/test_s2_display.py +74 -0
- aiagents4pharma/talk2scholars/tests/test_s2_multi.py +282 -0
- aiagents4pharma/talk2scholars/tests/test_s2_query.py +78 -0
- aiagents4pharma/talk2scholars/tests/test_s2_retrieve.py +65 -0
- aiagents4pharma/talk2scholars/tests/test_s2_search.py +266 -0
- aiagents4pharma/talk2scholars/tests/test_s2_single.py +274 -0
- aiagents4pharma/talk2scholars/tests/test_zotero_path.py +57 -0
- aiagents4pharma/talk2scholars/tests/test_zotero_read.py +412 -0
- aiagents4pharma/talk2scholars/tests/test_zotero_write.py +626 -0
- aiagents4pharma/talk2scholars/tools/__init__.py +1 -0
- aiagents4pharma/talk2scholars/tools/pdf/__init__.py +5 -0
- aiagents4pharma/talk2scholars/tools/pdf/question_and_answer.py +170 -0
- aiagents4pharma/talk2scholars/tools/s2/multi_paper_rec.py +50 -34
- aiagents4pharma/talk2scholars/tools/s2/query_results.py +1 -1
- aiagents4pharma/talk2scholars/tools/s2/retrieve_semantic_scholar_paper_id.py +8 -8
- aiagents4pharma/talk2scholars/tools/s2/search.py +36 -23
- aiagents4pharma/talk2scholars/tools/s2/single_paper_rec.py +44 -38
- aiagents4pharma/talk2scholars/tools/zotero/__init__.py +2 -0
- aiagents4pharma/talk2scholars/tools/zotero/utils/__init__.py +5 -0
- aiagents4pharma/talk2scholars/tools/zotero/utils/zotero_path.py +63 -0
- aiagents4pharma/talk2scholars/tools/zotero/zotero_read.py +64 -19
- aiagents4pharma/talk2scholars/tools/zotero/zotero_write.py +247 -0
- {aiagents4pharma-1.27.2.dist-info → aiagents4pharma-1.29.0.dist-info}/METADATA +6 -5
- {aiagents4pharma-1.27.2.dist-info → aiagents4pharma-1.29.0.dist-info}/RECORD +49 -33
- aiagents4pharma/talk2scholars/tests/test_call_s2.py +0 -100
- aiagents4pharma/talk2scholars/tests/test_call_zotero.py +0 -94
- aiagents4pharma/talk2scholars/tests/test_s2_tools.py +0 -355
- aiagents4pharma/talk2scholars/tests/test_zotero_tool.py +0 -171
- {aiagents4pharma-1.27.2.dist-info → aiagents4pharma-1.29.0.dist-info}/LICENSE +0 -0
- {aiagents4pharma-1.27.2.dist-info → aiagents4pharma-1.29.0.dist-info}/WHEEL +0 -0
- {aiagents4pharma-1.27.2.dist-info → aiagents4pharma-1.29.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,626 @@
|
|
1
|
+
"""
|
2
|
+
Unit tests for Zotero write tool in zotero_write.py.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from types import SimpleNamespace
|
6
|
+
import unittest
|
7
|
+
from unittest.mock import patch, MagicMock
|
8
|
+
from langgraph.types import Command
|
9
|
+
from aiagents4pharma.talk2scholars.tools.zotero.zotero_write import (
|
10
|
+
zotero_save_tool,
|
11
|
+
)
|
12
|
+
|
13
|
+
# Dummy Hydra configuration for the Zotero write tool
|
14
|
+
dummy_zotero_write_config = SimpleNamespace(
|
15
|
+
user_id="dummy_user_write",
|
16
|
+
library_type="user",
|
17
|
+
api_key="dummy_api_key_write",
|
18
|
+
)
|
19
|
+
dummy_cfg = SimpleNamespace(
|
20
|
+
tools=SimpleNamespace(zotero_write=dummy_zotero_write_config)
|
21
|
+
)
|
22
|
+
|
23
|
+
|
24
|
+
class TestZoteroSaveTool(unittest.TestCase):
|
25
|
+
"""a test class for the Zotero save tool"""
|
26
|
+
|
27
|
+
@patch("aiagents4pharma.talk2scholars.tools.zotero.zotero_write.hydra.initialize")
|
28
|
+
@patch("aiagents4pharma.talk2scholars.tools.zotero.zotero_write.hydra.compose")
|
29
|
+
@patch("aiagents4pharma.talk2scholars.tools.zotero.zotero_write.zotero.Zotero")
|
30
|
+
@patch(
|
31
|
+
"aiagents4pharma.talk2scholars.tools.zotero.zotero_write.get_item_collections"
|
32
|
+
)
|
33
|
+
def test_successful_save_direct_state(
|
34
|
+
self,
|
35
|
+
mock_get_item_collections,
|
36
|
+
mock_zotero_class,
|
37
|
+
mock_hydra_compose,
|
38
|
+
mock_hydra_init,
|
39
|
+
):
|
40
|
+
"""
|
41
|
+
Test successful saving when the fetched papers are directly provided in the state.
|
42
|
+
"""
|
43
|
+
mock_hydra_compose.return_value = dummy_cfg
|
44
|
+
mock_hydra_init.return_value.__enter__.return_value = None
|
45
|
+
|
46
|
+
state = {
|
47
|
+
"last_displayed_papers": {
|
48
|
+
"paper1": {
|
49
|
+
"Title": "Test Paper 1",
|
50
|
+
"Abstract": "Abstract 1",
|
51
|
+
"Date": "2021",
|
52
|
+
"URL": "http://example.com",
|
53
|
+
"Citations": "0",
|
54
|
+
},
|
55
|
+
"paper2": {
|
56
|
+
"Title": "Test Paper 2",
|
57
|
+
"Abstract": "Abstract 2",
|
58
|
+
"Date": "2022",
|
59
|
+
"URL": "http://example2.com",
|
60
|
+
"Citations": "1",
|
61
|
+
},
|
62
|
+
},
|
63
|
+
"zotero_read": {"paper1": ["/Test Collection"]},
|
64
|
+
"query": "dummy query",
|
65
|
+
}
|
66
|
+
|
67
|
+
fake_zot = MagicMock()
|
68
|
+
fake_zot.collections.return_value = [
|
69
|
+
{"key": "col1", "data": {"name": "Test Collection"}}
|
70
|
+
]
|
71
|
+
fake_zot.create_items.return_value = {"success": True}
|
72
|
+
mock_zotero_class.return_value = fake_zot
|
73
|
+
mock_get_item_collections.return_value = {}
|
74
|
+
|
75
|
+
tool_call_id = "test_call_1"
|
76
|
+
tool_input = {
|
77
|
+
"tool_call_id": tool_call_id,
|
78
|
+
"collection_path": "/Test Collection",
|
79
|
+
"state": state,
|
80
|
+
}
|
81
|
+
result = zotero_save_tool.run(tool_input)
|
82
|
+
|
83
|
+
self.assertIsInstance(result, Command)
|
84
|
+
messages = result.update.get("messages", [])
|
85
|
+
self.assertTrue(len(messages) > 0)
|
86
|
+
content = messages[0].content
|
87
|
+
self.assertIn("Save was successful", content)
|
88
|
+
self.assertIn("Test Collection", content)
|
89
|
+
|
90
|
+
@patch("aiagents4pharma.talk2scholars.tools.zotero.zotero_write.hydra.initialize")
|
91
|
+
@patch("aiagents4pharma.talk2scholars.tools.zotero.zotero_write.hydra.compose")
|
92
|
+
@patch("aiagents4pharma.talk2scholars.tools.zotero.zotero_write.zotero.Zotero")
|
93
|
+
@patch(
|
94
|
+
"aiagents4pharma.talk2scholars.tools.zotero.zotero_write.get_item_collections"
|
95
|
+
)
|
96
|
+
def test_successful_save_state_key(
|
97
|
+
self,
|
98
|
+
mock_get_item_collections,
|
99
|
+
mock_zotero_class,
|
100
|
+
mock_hydra_compose,
|
101
|
+
mock_hydra_init,
|
102
|
+
):
|
103
|
+
"""
|
104
|
+
Test successful saving when the state's last_displayed_papers is a key referencing
|
105
|
+
the actual fetched papers.
|
106
|
+
"""
|
107
|
+
mock_hydra_compose.return_value = dummy_cfg
|
108
|
+
mock_hydra_init.return_value.__enter__.return_value = None
|
109
|
+
|
110
|
+
state = {
|
111
|
+
"last_displayed_papers": "papers_key",
|
112
|
+
"papers_key": {
|
113
|
+
"paper1": {
|
114
|
+
"Title": "Test Paper 1",
|
115
|
+
"Abstract": "Abstract 1",
|
116
|
+
"Date": "2021",
|
117
|
+
"URL": "http://example.com",
|
118
|
+
"Citations": "0",
|
119
|
+
}
|
120
|
+
},
|
121
|
+
"zotero_read": {"paper1": ["/Test Collection"]},
|
122
|
+
"query": "dummy query",
|
123
|
+
}
|
124
|
+
|
125
|
+
fake_zot = MagicMock()
|
126
|
+
fake_zot.collections.return_value = [
|
127
|
+
{"key": "col1", "data": {"name": "Test Collection"}}
|
128
|
+
]
|
129
|
+
fake_zot.create_items.return_value = {"success": True}
|
130
|
+
mock_zotero_class.return_value = fake_zot
|
131
|
+
mock_get_item_collections.return_value = {}
|
132
|
+
|
133
|
+
tool_call_id = "test_call_2"
|
134
|
+
tool_input = {
|
135
|
+
"tool_call_id": tool_call_id,
|
136
|
+
"collection_path": "/Test Collection",
|
137
|
+
"state": state,
|
138
|
+
}
|
139
|
+
result = zotero_save_tool.run(tool_input)
|
140
|
+
self.assertIsInstance(result, Command)
|
141
|
+
messages = result.update.get("messages", [])
|
142
|
+
self.assertTrue(len(messages) > 0)
|
143
|
+
content = messages[0].content
|
144
|
+
self.assertIn("Save was successful", content)
|
145
|
+
self.assertIn("Test Collection", content)
|
146
|
+
|
147
|
+
@patch("aiagents4pharma.talk2scholars.tools.zotero.zotero_write.hydra.initialize")
|
148
|
+
@patch("aiagents4pharma.talk2scholars.tools.zotero.zotero_write.hydra.compose")
|
149
|
+
@patch("aiagents4pharma.talk2scholars.tools.zotero.zotero_write.zotero.Zotero")
|
150
|
+
@patch(
|
151
|
+
"aiagents4pharma.talk2scholars.tools.zotero.zotero_write.get_item_collections"
|
152
|
+
)
|
153
|
+
def test_no_fetched_papers(
|
154
|
+
self,
|
155
|
+
mock_get_item_collections,
|
156
|
+
mock_zotero_class,
|
157
|
+
mock_hydra_compose,
|
158
|
+
mock_hydra_init,
|
159
|
+
):
|
160
|
+
"""
|
161
|
+
Test that a RuntimeError is raised when there are no fetched papers in the state.
|
162
|
+
"""
|
163
|
+
mock_hydra_compose.return_value = dummy_cfg
|
164
|
+
mock_get_item_collections.return_value = {}
|
165
|
+
mock_hydra_init.return_value.__enter__.return_value = None
|
166
|
+
|
167
|
+
state = {
|
168
|
+
"last_displayed_papers": {},
|
169
|
+
"zotero_read": {"paper1": ["/Test Collection"]},
|
170
|
+
"query": "dummy query",
|
171
|
+
}
|
172
|
+
|
173
|
+
fake_zot = MagicMock()
|
174
|
+
fake_zot.collections.return_value = [
|
175
|
+
{"key": "col1", "data": {"name": "Test Collection"}}
|
176
|
+
]
|
177
|
+
mock_zotero_class.return_value = fake_zot
|
178
|
+
|
179
|
+
tool_call_id = "test_call_3"
|
180
|
+
tool_input = {
|
181
|
+
"tool_call_id": tool_call_id,
|
182
|
+
"collection_path": "/Test Collection",
|
183
|
+
"state": state,
|
184
|
+
}
|
185
|
+
with self.assertRaises(RuntimeError) as context:
|
186
|
+
zotero_save_tool.run(tool_input)
|
187
|
+
self.assertIn("No fetched papers were found to save", str(context.exception))
|
188
|
+
|
189
|
+
@patch("aiagents4pharma.talk2scholars.tools.zotero.zotero_write.hydra.initialize")
|
190
|
+
@patch("aiagents4pharma.talk2scholars.tools.zotero.zotero_write.hydra.compose")
|
191
|
+
@patch("aiagents4pharma.talk2scholars.tools.zotero.zotero_write.zotero.Zotero")
|
192
|
+
@patch(
|
193
|
+
"aiagents4pharma.talk2scholars.tools.zotero.zotero_write.get_item_collections"
|
194
|
+
)
|
195
|
+
def test_fallback_get_item_collections(
|
196
|
+
self,
|
197
|
+
mock_get_item_collections,
|
198
|
+
mock_zotero_class,
|
199
|
+
mock_hydra_compose,
|
200
|
+
mock_hydra_init,
|
201
|
+
):
|
202
|
+
"""
|
203
|
+
Test that if 'zotero_read' in the state is empty, the fallback
|
204
|
+
using get_item_collections is used.
|
205
|
+
"""
|
206
|
+
mock_hydra_compose.return_value = dummy_cfg
|
207
|
+
mock_hydra_init.return_value.__enter__.return_value = None
|
208
|
+
|
209
|
+
state = {
|
210
|
+
"last_displayed_papers": {
|
211
|
+
"paper1": {
|
212
|
+
"Title": "Test Paper 1",
|
213
|
+
"Abstract": "Abstract 1",
|
214
|
+
"Date": "2021",
|
215
|
+
"URL": "http://example.com",
|
216
|
+
"Citations": "0",
|
217
|
+
}
|
218
|
+
},
|
219
|
+
"zotero_read": {}, # empty mapping triggers fallback
|
220
|
+
"query": "dummy query",
|
221
|
+
}
|
222
|
+
|
223
|
+
fake_zot = MagicMock()
|
224
|
+
fake_zot.collections.return_value = [
|
225
|
+
{"key": "col1", "data": {"name": "Test Collection"}}
|
226
|
+
]
|
227
|
+
fake_zot.create_items.return_value = {"success": True}
|
228
|
+
mock_zotero_class.return_value = fake_zot
|
229
|
+
|
230
|
+
# Simulate get_item_collections returning a mapping that includes a match.
|
231
|
+
mock_get_item_collections.return_value = {"paper1": ["/Test Collection"]}
|
232
|
+
|
233
|
+
tool_call_id = "test_call_4"
|
234
|
+
tool_input = {
|
235
|
+
"tool_call_id": tool_call_id,
|
236
|
+
"collection_path": "/Test Collection",
|
237
|
+
"state": state,
|
238
|
+
}
|
239
|
+
result = zotero_save_tool.run(tool_input)
|
240
|
+
messages = result.update.get("messages", [])
|
241
|
+
self.assertTrue(len(messages) > 0)
|
242
|
+
self.assertIn("Save was successful", messages[0].content)
|
243
|
+
|
244
|
+
@patch("aiagents4pharma.talk2scholars.tools.zotero.zotero_write.hydra.initialize")
|
245
|
+
@patch("aiagents4pharma.talk2scholars.tools.zotero.zotero_write.hydra.compose")
|
246
|
+
@patch("aiagents4pharma.talk2scholars.tools.zotero.zotero_write.zotero.Zotero")
|
247
|
+
@patch(
|
248
|
+
"aiagents4pharma.talk2scholars.tools.zotero.zotero_write.get_item_collections"
|
249
|
+
)
|
250
|
+
def test_invalid_collection_path(
|
251
|
+
self,
|
252
|
+
mock_get_item_collections,
|
253
|
+
mock_zotero_class,
|
254
|
+
mock_hydra_compose,
|
255
|
+
mock_hydra_init,
|
256
|
+
):
|
257
|
+
"""
|
258
|
+
Test that a RuntimeError is raised when the provided collection
|
259
|
+
path does not match any collection.
|
260
|
+
"""
|
261
|
+
mock_hydra_compose.return_value = dummy_cfg
|
262
|
+
mock_hydra_init.return_value.__enter__.return_value = None
|
263
|
+
|
264
|
+
state = {
|
265
|
+
"last_displayed_papers": {
|
266
|
+
"paper1": {
|
267
|
+
"Title": "Test Paper 1",
|
268
|
+
"Abstract": "Abstract 1",
|
269
|
+
"Date": "2021",
|
270
|
+
"URL": "http://example.com",
|
271
|
+
"Citations": "0",
|
272
|
+
}
|
273
|
+
},
|
274
|
+
"zotero_read": {}, # empty mapping; no match available
|
275
|
+
"query": "dummy query",
|
276
|
+
}
|
277
|
+
|
278
|
+
fake_zot = MagicMock()
|
279
|
+
fake_zot.collections.return_value = [
|
280
|
+
{"key": "col1", "data": {"name": "Test Collection"}}
|
281
|
+
]
|
282
|
+
mock_zotero_class.return_value = fake_zot
|
283
|
+
mock_get_item_collections.return_value = {}
|
284
|
+
|
285
|
+
tool_call_id = "test_call_5"
|
286
|
+
tool_input = {
|
287
|
+
"tool_call_id": tool_call_id,
|
288
|
+
"collection_path": "/Nonexistent",
|
289
|
+
"state": state,
|
290
|
+
}
|
291
|
+
with self.assertRaises(RuntimeError) as context:
|
292
|
+
zotero_save_tool.run(tool_input)
|
293
|
+
self.assertIn("does not exist in Zotero", str(context.exception))
|
294
|
+
|
295
|
+
@patch("aiagents4pharma.talk2scholars.tools.zotero.zotero_write.hydra.initialize")
|
296
|
+
@patch("aiagents4pharma.talk2scholars.tools.zotero.zotero_write.hydra.compose")
|
297
|
+
@patch("aiagents4pharma.talk2scholars.tools.zotero.zotero_write.zotero.Zotero")
|
298
|
+
@patch(
|
299
|
+
"aiagents4pharma.talk2scholars.tools.zotero.zotero_write.get_item_collections"
|
300
|
+
)
|
301
|
+
def test_save_failure(
|
302
|
+
self,
|
303
|
+
mock_get_item_collections,
|
304
|
+
mock_zotero_class,
|
305
|
+
mock_hydra_compose,
|
306
|
+
mock_hydra_init,
|
307
|
+
):
|
308
|
+
"""
|
309
|
+
Test that if the Zotero client raises an exception during
|
310
|
+
create_items, a RuntimeError is raised.
|
311
|
+
"""
|
312
|
+
mock_hydra_compose.return_value = dummy_cfg
|
313
|
+
mock_hydra_init.return_value.__enter__.return_value = None
|
314
|
+
|
315
|
+
state = {
|
316
|
+
"last_displayed_papers": {
|
317
|
+
"paper1": {
|
318
|
+
"Title": "Test Paper 1",
|
319
|
+
"Abstract": "Abstract 1",
|
320
|
+
"Date": "2021",
|
321
|
+
"URL": "http://example.com",
|
322
|
+
"Citations": "0",
|
323
|
+
}
|
324
|
+
},
|
325
|
+
"zotero_read": {"paper1": ["/Test Collection"]},
|
326
|
+
"query": "dummy query",
|
327
|
+
}
|
328
|
+
|
329
|
+
fake_zot = MagicMock()
|
330
|
+
fake_zot.collections.return_value = [
|
331
|
+
{"key": "col1", "data": {"name": "Test Collection"}}
|
332
|
+
]
|
333
|
+
fake_zot.create_items.side_effect = Exception("Creation error")
|
334
|
+
mock_zotero_class.return_value = fake_zot
|
335
|
+
mock_get_item_collections.return_value = {}
|
336
|
+
|
337
|
+
tool_call_id = "test_call_6"
|
338
|
+
tool_input = {
|
339
|
+
"tool_call_id": tool_call_id,
|
340
|
+
"collection_path": "/Test Collection",
|
341
|
+
"state": state,
|
342
|
+
}
|
343
|
+
with self.assertRaises(RuntimeError) as context:
|
344
|
+
zotero_save_tool.run(tool_input)
|
345
|
+
self.assertIn("Error saving papers to Zotero", str(context.exception))
|
346
|
+
|
347
|
+
# --- Additional tests to cover missing lines ---
|
348
|
+
|
349
|
+
@patch("aiagents4pharma.talk2scholars.tools.zotero.zotero_write.hydra.initialize")
|
350
|
+
@patch("aiagents4pharma.talk2scholars.tools.zotero.zotero_write.hydra.compose")
|
351
|
+
@patch("aiagents4pharma.talk2scholars.tools.zotero.zotero_write.zotero.Zotero")
|
352
|
+
@patch(
|
353
|
+
"aiagents4pharma.talk2scholars.tools.zotero.zotero_write.get_item_collections"
|
354
|
+
)
|
355
|
+
def test_get_item_collections_exception(
|
356
|
+
self,
|
357
|
+
mock_get_item_collections,
|
358
|
+
mock_zotero_class,
|
359
|
+
mock_hydra_compose,
|
360
|
+
mock_hydra_init,
|
361
|
+
):
|
362
|
+
"""
|
363
|
+
Test that if get_item_collections raises an exception, the fallback branch
|
364
|
+
raises a RuntimeError.
|
365
|
+
"""
|
366
|
+
mock_hydra_compose.return_value = dummy_cfg
|
367
|
+
mock_hydra_init.return_value.__enter__.return_value = None
|
368
|
+
|
369
|
+
# Provide valid fetched papers so we bypass earlier error
|
370
|
+
state = {
|
371
|
+
"last_displayed_papers": {
|
372
|
+
"paper1": {
|
373
|
+
"Title": "Paper 1",
|
374
|
+
"Abstract": "Abstract 1",
|
375
|
+
"Date": "2021",
|
376
|
+
"URL": "http://example.com",
|
377
|
+
"Citations": "0",
|
378
|
+
}
|
379
|
+
},
|
380
|
+
"zotero_read": {}, # empty so fallback is triggered
|
381
|
+
"query": "dummy query",
|
382
|
+
}
|
383
|
+
|
384
|
+
fake_zot = MagicMock()
|
385
|
+
fake_zot.collections.return_value = [
|
386
|
+
{"key": "col1", "data": {"name": "Test Collection"}}
|
387
|
+
]
|
388
|
+
mock_zotero_class.return_value = fake_zot
|
389
|
+
|
390
|
+
# Simulate exception in get_item_collections
|
391
|
+
mock_get_item_collections.side_effect = Exception("Mapping error")
|
392
|
+
|
393
|
+
tool_call_id = "test_call_7"
|
394
|
+
tool_input = {
|
395
|
+
"tool_call_id": tool_call_id,
|
396
|
+
"collection_path": "/Test Collection",
|
397
|
+
"state": state,
|
398
|
+
}
|
399
|
+
with self.assertRaises(RuntimeError) as context:
|
400
|
+
zotero_save_tool.run(tool_input)
|
401
|
+
self.assertIn(
|
402
|
+
"Failed to generate collection path mappings", str(context.exception)
|
403
|
+
)
|
404
|
+
|
405
|
+
@patch("aiagents4pharma.talk2scholars.tools.zotero.zotero_write.hydra.initialize")
|
406
|
+
@patch("aiagents4pharma.talk2scholars.tools.zotero.zotero_write.hydra.compose")
|
407
|
+
@patch("aiagents4pharma.talk2scholars.tools.zotero.zotero_write.zotero.Zotero")
|
408
|
+
@patch(
|
409
|
+
"aiagents4pharma.talk2scholars.tools.zotero.zotero_write.get_item_collections"
|
410
|
+
)
|
411
|
+
def test_zotero_read_string_match(
|
412
|
+
self,
|
413
|
+
mock_get_item_collections,
|
414
|
+
mock_zotero_class,
|
415
|
+
mock_hydra_compose,
|
416
|
+
mock_hydra_init,
|
417
|
+
):
|
418
|
+
"""
|
419
|
+
Test that if an entry in zotero_read is a string that matches the normalized path,
|
420
|
+
it is used as the collection key
|
421
|
+
"""
|
422
|
+
mock_hydra_compose.return_value = dummy_cfg
|
423
|
+
mock_hydra_init.return_value.__enter__.return_value = None
|
424
|
+
|
425
|
+
state = {
|
426
|
+
"last_displayed_papers": {
|
427
|
+
"paper1": {
|
428
|
+
"Title": "Paper 1",
|
429
|
+
"Abstract": "Abstract 1",
|
430
|
+
"Date": "2021",
|
431
|
+
"URL": "http://example.com",
|
432
|
+
"Citations": "0",
|
433
|
+
}
|
434
|
+
},
|
435
|
+
# zotero_read entry is a string, not a list
|
436
|
+
"zotero_read": {"match_key": "/test collection"},
|
437
|
+
"query": "dummy query",
|
438
|
+
}
|
439
|
+
|
440
|
+
# Return a collection with key "match_key" to simulate a successful match.
|
441
|
+
fake_zot = MagicMock()
|
442
|
+
fake_zot.collections.return_value = [
|
443
|
+
{"key": "match_key", "data": {"name": "Test Collection"}}
|
444
|
+
]
|
445
|
+
fake_zot.create_items.return_value = {"success": True}
|
446
|
+
mock_zotero_class.return_value = fake_zot
|
447
|
+
# get_item_collections is not used in this branch.
|
448
|
+
mock_get_item_collections.return_value = {}
|
449
|
+
|
450
|
+
tool_call_id = "test_call_8"
|
451
|
+
tool_input = {
|
452
|
+
"tool_call_id": tool_call_id,
|
453
|
+
"collection_path": "/test collection",
|
454
|
+
"state": state,
|
455
|
+
}
|
456
|
+
result = zotero_save_tool.run(tool_input)
|
457
|
+
messages = result.update.get("messages", [])
|
458
|
+
self.assertTrue(len(messages) > 0)
|
459
|
+
# Check for the correct substring in the returned message.
|
460
|
+
self.assertIn(
|
461
|
+
"Papers have been saved to Zotero collection", messages[0].content
|
462
|
+
)
|
463
|
+
|
464
|
+
@patch("aiagents4pharma.talk2scholars.tools.zotero.zotero_write.hydra.initialize")
|
465
|
+
@patch("aiagents4pharma.talk2scholars.tools.zotero.zotero_write.hydra.compose")
|
466
|
+
@patch("aiagents4pharma.talk2scholars.tools.zotero.zotero_write.zotero.Zotero")
|
467
|
+
@patch(
|
468
|
+
"aiagents4pharma.talk2scholars.tools.zotero.zotero_write.get_item_collections"
|
469
|
+
)
|
470
|
+
def test_direct_match_by_collection_name(
|
471
|
+
self,
|
472
|
+
mock_get_item_collections,
|
473
|
+
mock_zotero_class,
|
474
|
+
mock_hydra_compose,
|
475
|
+
mock_hydra_init,
|
476
|
+
):
|
477
|
+
"""
|
478
|
+
Test that if zotero_read does not yield a match, the tool finds
|
479
|
+
a direct match by collection name
|
480
|
+
in the collections list
|
481
|
+
"""
|
482
|
+
mock_hydra_compose.return_value = dummy_cfg
|
483
|
+
mock_hydra_init.return_value.__enter__.return_value = None
|
484
|
+
|
485
|
+
state = {
|
486
|
+
"last_displayed_papers": {
|
487
|
+
"paper1": {
|
488
|
+
"Title": "Paper 1",
|
489
|
+
"Abstract": "Abstract 1",
|
490
|
+
"Date": "2021",
|
491
|
+
"URL": "http://example.com",
|
492
|
+
"Citations": "0",
|
493
|
+
}
|
494
|
+
},
|
495
|
+
# Non-matching zotero_read data
|
496
|
+
"zotero_read": {"dummy": ["/other"]},
|
497
|
+
"query": "dummy query",
|
498
|
+
}
|
499
|
+
|
500
|
+
fake_zot = MagicMock()
|
501
|
+
# Collection with name "Test Collection" should match because
|
502
|
+
# f"/Test Collection".lower() equals normalized path.
|
503
|
+
fake_zot.collections.return_value = [
|
504
|
+
{"key": "col1", "data": {"name": "Test Collection"}}
|
505
|
+
]
|
506
|
+
fake_zot.create_items.return_value = {"success": True}
|
507
|
+
mock_zotero_class.return_value = fake_zot
|
508
|
+
mock_get_item_collections.return_value = {}
|
509
|
+
|
510
|
+
tool_call_id = "test_call_9"
|
511
|
+
tool_input = {
|
512
|
+
"tool_call_id": tool_call_id,
|
513
|
+
"collection_path": "/Test Collection",
|
514
|
+
"state": state,
|
515
|
+
}
|
516
|
+
result = zotero_save_tool.run(tool_input)
|
517
|
+
messages = result.update.get("messages", [])
|
518
|
+
self.assertTrue(len(messages) > 0)
|
519
|
+
self.assertIn("Test Collection", messages[0].content)
|
520
|
+
|
521
|
+
@patch("aiagents4pharma.talk2scholars.tools.zotero.zotero_write.hydra.initialize")
|
522
|
+
@patch("aiagents4pharma.talk2scholars.tools.zotero.zotero_write.hydra.compose")
|
523
|
+
@patch("aiagents4pharma.talk2scholars.tools.zotero.zotero_write.zotero.Zotero")
|
524
|
+
@patch(
|
525
|
+
"aiagents4pharma.talk2scholars.tools.zotero.zotero_write.get_item_collections"
|
526
|
+
)
|
527
|
+
def test_match_by_stripped_name(
|
528
|
+
self,
|
529
|
+
mock_get_item_collections,
|
530
|
+
mock_zotero_class,
|
531
|
+
mock_hydra_compose,
|
532
|
+
mock_hydra_init,
|
533
|
+
):
|
534
|
+
"""
|
535
|
+
Test that if no direct match is found, a match is found by comparing
|
536
|
+
the stripped collection path.
|
537
|
+
"""
|
538
|
+
mock_hydra_compose.return_value = dummy_cfg
|
539
|
+
mock_hydra_init.return_value.__enter__.return_value = None
|
540
|
+
|
541
|
+
# Provide state with non-matching zotero_read
|
542
|
+
state = {
|
543
|
+
"last_displayed_papers": {
|
544
|
+
"paper1": {
|
545
|
+
"Title": "Paper 1",
|
546
|
+
"Abstract": "Abstract 1",
|
547
|
+
"Date": "2021",
|
548
|
+
"URL": "http://example.com",
|
549
|
+
"Citations": "0",
|
550
|
+
}
|
551
|
+
},
|
552
|
+
"zotero_read": {"dummy": ["/other"]},
|
553
|
+
"query": "dummy query",
|
554
|
+
}
|
555
|
+
|
556
|
+
fake_zot = MagicMock()
|
557
|
+
# Set collection_path without leading slash so that direct matching fails,
|
558
|
+
# but normalized_path.lstrip("/") yields "test", which matches the collection name.
|
559
|
+
fake_zot.collections.return_value = [{"key": "colX", "data": {"name": "test"}}]
|
560
|
+
fake_zot.create_items.return_value = {"success": True}
|
561
|
+
mock_zotero_class.return_value = fake_zot
|
562
|
+
mock_get_item_collections.return_value = {}
|
563
|
+
|
564
|
+
tool_call_id = "test_call_10"
|
565
|
+
tool_input = {
|
566
|
+
"tool_call_id": tool_call_id,
|
567
|
+
"collection_path": "test", # no leading slash
|
568
|
+
"state": state,
|
569
|
+
}
|
570
|
+
result = zotero_save_tool.run(tool_input)
|
571
|
+
messages = result.update.get("messages", [])
|
572
|
+
self.assertTrue(len(messages) > 0)
|
573
|
+
self.assertIn("test", messages[0].content)
|
574
|
+
|
575
|
+
@patch("aiagents4pharma.talk2scholars.tools.zotero.zotero_write.hydra.initialize")
|
576
|
+
@patch("aiagents4pharma.talk2scholars.tools.zotero.zotero_write.hydra.compose")
|
577
|
+
@patch("aiagents4pharma.talk2scholars.tools.zotero.zotero_write.zotero.Zotero")
|
578
|
+
@patch(
|
579
|
+
"aiagents4pharma.talk2scholars.tools.zotero.zotero_write.get_item_collections"
|
580
|
+
)
|
581
|
+
def test_match_by_path_component(
|
582
|
+
self,
|
583
|
+
mock_get_item_collections,
|
584
|
+
mock_zotero_class,
|
585
|
+
mock_hydra_compose,
|
586
|
+
mock_hydra_init,
|
587
|
+
):
|
588
|
+
"""
|
589
|
+
Test that if no full-string match is found, the tool can match
|
590
|
+
by one of the path components.
|
591
|
+
"""
|
592
|
+
mock_hydra_compose.return_value = dummy_cfg
|
593
|
+
mock_hydra_init.return_value.__enter__.return_value = None
|
594
|
+
|
595
|
+
state = {
|
596
|
+
"last_displayed_papers": {
|
597
|
+
"paper1": {
|
598
|
+
"Title": "Paper 1",
|
599
|
+
"Abstract": "Abstract 1",
|
600
|
+
"Date": "2021",
|
601
|
+
"URL": "http://example.com",
|
602
|
+
"Citations": "0",
|
603
|
+
}
|
604
|
+
},
|
605
|
+
"zotero_read": {"dummy": ["/other"]},
|
606
|
+
"query": "dummy query",
|
607
|
+
}
|
608
|
+
|
609
|
+
fake_zot = MagicMock()
|
610
|
+
# Collection name "bar" should be found via a path component when
|
611
|
+
# collection_path is "/foo/bar"
|
612
|
+
fake_zot.collections.return_value = [{"key": "colBar", "data": {"name": "bar"}}]
|
613
|
+
fake_zot.create_items.return_value = {"success": True}
|
614
|
+
mock_zotero_class.return_value = fake_zot
|
615
|
+
mock_get_item_collections.return_value = {}
|
616
|
+
|
617
|
+
tool_call_id = "test_call_11"
|
618
|
+
tool_input = {
|
619
|
+
"tool_call_id": tool_call_id,
|
620
|
+
"collection_path": "/foo/bar",
|
621
|
+
"state": state,
|
622
|
+
}
|
623
|
+
result = zotero_save_tool.run(tool_input)
|
624
|
+
messages = result.update.get("messages", [])
|
625
|
+
self.assertTrue(len(messages) > 0)
|
626
|
+
self.assertIn("bar", messages[0].content)
|