aiagents4pharma 1.30.1__py3-none-any.whl → 1.30.3__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/__init__.py +2 -0
- aiagents4pharma/talk2scholars/agents/__init__.py +8 -0
- aiagents4pharma/talk2scholars/agents/zotero_agent.py +9 -7
- aiagents4pharma/talk2scholars/configs/__init__.py +2 -0
- aiagents4pharma/talk2scholars/configs/agents/__init__.py +2 -0
- aiagents4pharma/talk2scholars/configs/agents/talk2scholars/__init__.py +2 -0
- aiagents4pharma/talk2scholars/configs/agents/talk2scholars/zotero_agent/default.yaml +9 -15
- aiagents4pharma/talk2scholars/configs/app/__init__.py +2 -0
- aiagents4pharma/talk2scholars/configs/tools/__init__.py +9 -0
- aiagents4pharma/talk2scholars/configs/tools/zotero_write/default.yaml +55 -0
- aiagents4pharma/talk2scholars/state/__init__.py +4 -2
- aiagents4pharma/talk2scholars/state/state_talk2scholars.py +3 -0
- aiagents4pharma/talk2scholars/tests/test_routing_logic.py +1 -2
- aiagents4pharma/talk2scholars/tests/test_s2_multi.py +10 -8
- aiagents4pharma/talk2scholars/tests/test_s2_search.py +9 -5
- aiagents4pharma/talk2scholars/tests/test_s2_single.py +7 -7
- aiagents4pharma/talk2scholars/tests/test_zotero_agent.py +3 -2
- aiagents4pharma/talk2scholars/tests/test_zotero_human_in_the_loop.py +273 -0
- aiagents4pharma/talk2scholars/tests/test_zotero_path.py +433 -1
- aiagents4pharma/talk2scholars/tests/test_zotero_read.py +57 -43
- aiagents4pharma/talk2scholars/tests/test_zotero_write.py +123 -588
- aiagents4pharma/talk2scholars/tools/__init__.py +3 -0
- aiagents4pharma/talk2scholars/tools/pdf/__init__.py +4 -2
- aiagents4pharma/talk2scholars/tools/s2/__init__.py +9 -0
- aiagents4pharma/talk2scholars/tools/s2/multi_paper_rec.py +9 -135
- aiagents4pharma/talk2scholars/tools/s2/search.py +8 -114
- aiagents4pharma/talk2scholars/tools/s2/single_paper_rec.py +8 -126
- aiagents4pharma/talk2scholars/tools/s2/utils/__init__.py +7 -0
- aiagents4pharma/talk2scholars/tools/s2/utils/multi_helper.py +194 -0
- aiagents4pharma/talk2scholars/tools/s2/utils/search_helper.py +175 -0
- aiagents4pharma/talk2scholars/tools/s2/utils/single_helper.py +186 -0
- aiagents4pharma/talk2scholars/tools/zotero/__init__.py +3 -0
- aiagents4pharma/talk2scholars/tools/zotero/utils/__init__.py +5 -0
- aiagents4pharma/talk2scholars/tools/zotero/utils/read_helper.py +167 -0
- aiagents4pharma/talk2scholars/tools/zotero/utils/review_helper.py +78 -0
- aiagents4pharma/talk2scholars/tools/zotero/utils/write_helper.py +197 -0
- aiagents4pharma/talk2scholars/tools/zotero/utils/zotero_path.py +126 -1
- aiagents4pharma/talk2scholars/tools/zotero/zotero_read.py +10 -139
- aiagents4pharma/talk2scholars/tools/zotero/zotero_review.py +164 -0
- aiagents4pharma/talk2scholars/tools/zotero/zotero_write.py +40 -229
- {aiagents4pharma-1.30.1.dist-info → aiagents4pharma-1.30.3.dist-info}/METADATA +3 -2
- {aiagents4pharma-1.30.1.dist-info → aiagents4pharma-1.30.3.dist-info}/RECORD +45 -35
- {aiagents4pharma-1.30.1.dist-info → aiagents4pharma-1.30.3.dist-info}/WHEEL +1 -1
- {aiagents4pharma-1.30.1.dist-info → aiagents4pharma-1.30.3.dist-info/licenses}/LICENSE +0 -0
- {aiagents4pharma-1.30.1.dist-info → aiagents4pharma-1.30.3.dist-info}/top_level.txt +0 -0
@@ -2,19 +2,14 @@
|
|
2
2
|
Unit tests for Zotero write tool in zotero_write.py.
|
3
3
|
"""
|
4
4
|
|
5
|
-
from types import SimpleNamespace
|
6
5
|
import unittest
|
6
|
+
from types import SimpleNamespace
|
7
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
8
|
|
13
|
-
|
9
|
+
from aiagents4pharma.talk2scholars.tools.zotero.zotero_write import zotero_write
|
10
|
+
|
14
11
|
dummy_zotero_write_config = SimpleNamespace(
|
15
|
-
user_id="
|
16
|
-
library_type="user",
|
17
|
-
api_key="dummy_api_key_write",
|
12
|
+
user_id="dummy", library_type="user", api_key="dummy"
|
18
13
|
)
|
19
14
|
dummy_cfg = SimpleNamespace(
|
20
15
|
tools=SimpleNamespace(zotero_write=dummy_zotero_write_config)
|
@@ -22,605 +17,145 @@ dummy_cfg = SimpleNamespace(
|
|
22
17
|
|
23
18
|
|
24
19
|
class TestZoteroSaveTool(unittest.TestCase):
|
25
|
-
"""
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
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)
|
20
|
+
"""Test class for Zotero save tool"""
|
21
|
+
|
22
|
+
def setUp(self):
|
23
|
+
"""Patch Hydra and Zotero client globally"""
|
24
|
+
self.hydra_init = patch(
|
25
|
+
"aiagents4pharma.talk2scholars.tools.zotero.utils.write_helper.hydra.initialize"
|
26
|
+
).start()
|
27
|
+
self.hydra_compose = patch(
|
28
|
+
"aiagents4pharma.talk2scholars.tools.zotero.utils.write_helper.hydra.compose",
|
29
|
+
return_value=dummy_cfg,
|
30
|
+
).start()
|
31
|
+
self.zotero_class = patch(
|
32
|
+
"aiagents4pharma.talk2scholars.tools.zotero.utils.write_helper.zotero.Zotero"
|
33
|
+
).start()
|
34
|
+
|
35
|
+
self.fake_zot = MagicMock()
|
36
|
+
self.zotero_class.return_value = self.fake_zot
|
37
|
+
|
38
|
+
def tearDown(self):
|
39
|
+
"""Stop all patches"""
|
40
|
+
patch.stopall()
|
41
|
+
|
42
|
+
def make_state(self, papers=None, approved=True, path="/Test Collection"):
|
43
|
+
"""Create a state dictionary with optional papers and approval info"""
|
44
|
+
state = {}
|
45
|
+
if approved:
|
46
|
+
state["zotero_write_approval_status"] = {
|
47
|
+
"approved": True,
|
48
|
+
"collection_path": path,
|
49
|
+
}
|
50
|
+
if papers is not None:
|
51
|
+
# When papers is provided as dict, use it directly.
|
52
|
+
state["last_displayed_papers"] = (
|
53
|
+
papers if isinstance(papers, dict) else "papers"
|
54
|
+
)
|
55
|
+
if isinstance(papers, dict):
|
56
|
+
state["papers"] = papers
|
57
|
+
return state
|
82
58
|
|
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
59
|
@patch(
|
94
|
-
"aiagents4pharma.talk2scholars.tools.zotero.
|
60
|
+
"aiagents4pharma.talk2scholars.tools.zotero.utils.write_helper.fetch_papers_for_save",
|
61
|
+
return_value=None,
|
95
62
|
)
|
96
|
-
def
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
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",
|
63
|
+
def test_no_papers_after_approval(self, mock_fetch):
|
64
|
+
"""Test when no fetched papers are found after approval"""
|
65
|
+
with self.assertRaises(ValueError) as cm:
|
66
|
+
zotero_write.run(
|
67
|
+
{
|
68
|
+
"tool_call_id": "id",
|
69
|
+
"collection_path": "/Test Collection",
|
70
|
+
"state": self.make_state({}, True),
|
119
71
|
}
|
120
|
-
|
121
|
-
|
122
|
-
|
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 = {}
|
72
|
+
)
|
73
|
+
self.assertIn("No fetched papers were found to save", str(cm.exception))
|
74
|
+
mock_fetch.assert_called_once()
|
132
75
|
|
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
76
|
@patch(
|
151
|
-
"aiagents4pharma.talk2scholars.tools.zotero.
|
77
|
+
"aiagents4pharma.talk2scholars.tools.zotero.utils.write_helper.fetch_papers_for_save",
|
78
|
+
return_value={"p1": {"Title": "X"}},
|
152
79
|
)
|
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
80
|
@patch(
|
193
|
-
"aiagents4pharma.talk2scholars.tools.zotero.
|
81
|
+
"aiagents4pharma.talk2scholars.tools.zotero.utils.write_helper.find_or_create_collection",
|
82
|
+
return_value=None,
|
194
83
|
)
|
195
|
-
def
|
196
|
-
|
197
|
-
|
198
|
-
|
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"}}
|
84
|
+
def test_invalid_collection(self, mock_find, mock_fetch):
|
85
|
+
"""Test when collection path is invalid"""
|
86
|
+
self.fake_zot.collections.return_value = [
|
87
|
+
{"key": "k1", "data": {"name": "Existing"}}
|
226
88
|
]
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
89
|
+
# Provide a valid papers dict so we don't hit the no-papers error.
|
90
|
+
state = self.make_state({"p1": {"Title": "X"}}, True)
|
91
|
+
result = zotero_write.run(
|
92
|
+
{
|
93
|
+
"tool_call_id": "id",
|
94
|
+
"collection_path": "/DoesNotExist",
|
95
|
+
"state": state,
|
96
|
+
}
|
97
|
+
)
|
98
|
+
# Remove outdated assertions and check for updated message content.
|
99
|
+
content = result.update["messages"][0].content
|
100
|
+
self.assertIn("does not exist in Zotero", content)
|
101
|
+
self.assertIn("/DoesNotExist", content)
|
102
|
+
self.assertIn("Existing", content)
|
103
|
+
mock_fetch.return_value = {"p1": {"Title": "X"}}
|
104
|
+
mock_find.return_value = None
|
243
105
|
|
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
106
|
@patch(
|
248
|
-
"aiagents4pharma.talk2scholars.tools.zotero.
|
107
|
+
"aiagents4pharma.talk2scholars.tools.zotero.utils.write_helper.fetch_papers_for_save",
|
108
|
+
return_value={"p1": {"Title": "X"}},
|
249
109
|
)
|
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
110
|
@patch(
|
299
|
-
"aiagents4pharma.talk2scholars.tools.zotero.
|
111
|
+
"aiagents4pharma.talk2scholars.tools.zotero.utils.write_helper.find_or_create_collection",
|
112
|
+
return_value="colKey",
|
300
113
|
)
|
301
|
-
def test_save_failure(
|
302
|
-
|
303
|
-
|
304
|
-
|
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"}}
|
114
|
+
def test_save_failure(self, mock_find, mock_fetch):
|
115
|
+
"""Test when Zotero save operation fails"""
|
116
|
+
self.fake_zot.collections.return_value = [
|
117
|
+
{"key": "colKey", "data": {"name": "Test Collection"}}
|
332
118
|
]
|
333
|
-
fake_zot.create_items.side_effect = Exception("Creation error")
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
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",
|
119
|
+
self.fake_zot.create_items.side_effect = Exception("Creation error")
|
120
|
+
state = self.make_state({"p1": {"Title": "X"}}, True)
|
121
|
+
with self.assertRaises(RuntimeError) as cm:
|
122
|
+
zotero_write.run(
|
123
|
+
{
|
124
|
+
"tool_call_id": "id",
|
125
|
+
"collection_path": "/Test Collection",
|
126
|
+
"state": state,
|
378
127
|
}
|
379
|
-
|
380
|
-
|
381
|
-
|
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
|
128
|
+
)
|
129
|
+
self.assertIn("Error saving papers to Zotero", str(cm.exception))
|
130
|
+
mock_fetch.return_value = {"p1": {"Title": "X"}}
|
131
|
+
mock_find.return_value = "colKey"
|
389
132
|
|
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
133
|
@patch(
|
409
|
-
"aiagents4pharma.talk2scholars.tools.zotero.
|
134
|
+
"aiagents4pharma.talk2scholars.tools.zotero.utils.write_helper.fetch_papers_for_save",
|
135
|
+
return_value={"p1": {"Title": "X"}},
|
410
136
|
)
|
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
137
|
@patch(
|
468
|
-
"aiagents4pharma.talk2scholars.tools.zotero.
|
138
|
+
"aiagents4pharma.talk2scholars.tools.zotero.utils.write_helper.find_or_create_collection",
|
139
|
+
return_value="colKey",
|
469
140
|
)
|
470
|
-
def
|
471
|
-
|
472
|
-
|
473
|
-
|
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"}}
|
141
|
+
def test_successful_save(self, mock_find, mock_fetch):
|
142
|
+
"""Test when Zotero save operation is successful"""
|
143
|
+
self.fake_zot.collections.return_value = [
|
144
|
+
{"key": "colKey", "data": {"name": "Test Collection"}}
|
505
145
|
]
|
506
|
-
fake_zot.create_items.return_value = {
|
507
|
-
|
508
|
-
|
509
|
-
|
510
|
-
|
511
|
-
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
|
519
|
-
|
520
|
-
|
521
|
-
|
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)
|
146
|
+
self.fake_zot.create_items.return_value = {
|
147
|
+
"successful": {"0": {"key": "item1"}}
|
148
|
+
}
|
149
|
+
mock_fetch.return_value = {"p1": {"Title": "X"}}
|
150
|
+
mock_find.return_value = "colKey"
|
151
|
+
|
152
|
+
result = zotero_write.run(
|
153
|
+
{
|
154
|
+
"tool_call_id": "id",
|
155
|
+
"collection_path": "/Test Collection",
|
156
|
+
"state": self.make_state({"p1": {"Title": "X"}}, True),
|
157
|
+
}
|
158
|
+
)
|
159
|
+
content = result.update["messages"][0].content
|
160
|
+
self.assertIn("Save was successful", content)
|
161
|
+
self.assertIn("Test Collection", content)
|