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
@@ -3,10 +3,20 @@ Unit tests for Zotero path utility in zotero_path.py.
|
|
3
3
|
"""
|
4
4
|
|
5
5
|
import unittest
|
6
|
-
from unittest.mock import MagicMock
|
6
|
+
from unittest.mock import MagicMock, patch
|
7
|
+
import pytest
|
7
8
|
from aiagents4pharma.talk2scholars.tools.zotero.utils.zotero_path import (
|
9
|
+
fetch_papers_for_save,
|
10
|
+
find_or_create_collection,
|
11
|
+
get_all_collection_paths,
|
8
12
|
get_item_collections,
|
9
13
|
)
|
14
|
+
from aiagents4pharma.talk2scholars.tools.zotero.zotero_read import (
|
15
|
+
zotero_read,
|
16
|
+
)
|
17
|
+
from aiagents4pharma.talk2scholars.tools.zotero.zotero_write import (
|
18
|
+
zotero_write,
|
19
|
+
)
|
10
20
|
|
11
21
|
|
12
22
|
class TestGetItemCollections(unittest.TestCase):
|
@@ -55,3 +65,425 @@ class TestGetItemCollections(unittest.TestCase):
|
|
55
65
|
|
56
66
|
result = get_item_collections(fake_zot)
|
57
67
|
self.assertEqual(result, expected_mapping)
|
68
|
+
|
69
|
+
|
70
|
+
class TestFindOrCreateCollectionExtra(unittest.TestCase):
|
71
|
+
"""Extra tests for the find_or_create_collection function."""
|
72
|
+
|
73
|
+
def setUp(self):
|
74
|
+
"""Set up a fake Zotero client with some default collections."""
|
75
|
+
# Set up a fake Zotero client with some default collections.
|
76
|
+
self.fake_zot = MagicMock()
|
77
|
+
self.fake_zot.collections.return_value = [
|
78
|
+
{"key": "parent1", "data": {"name": "Parent", "parentCollection": None}},
|
79
|
+
{"key": "child1", "data": {"name": "Child", "parentCollection": "parent1"}},
|
80
|
+
]
|
81
|
+
|
82
|
+
def test_empty_path(self):
|
83
|
+
"""Test that an empty path returns None."""
|
84
|
+
result = find_or_create_collection(self.fake_zot, "", create_missing=False)
|
85
|
+
self.assertIsNone(result)
|
86
|
+
|
87
|
+
def test_create_collection_with_success_key(self):
|
88
|
+
"""
|
89
|
+
Test that when create_missing is True and the response contains a "success" key,
|
90
|
+
the function returns the new collection key.
|
91
|
+
"""
|
92
|
+
# Simulate no existing collections (so direct match fails)
|
93
|
+
self.fake_zot.collections.return_value = []
|
94
|
+
# Simulate create_collection returning a dict with a "success" key.
|
95
|
+
self.fake_zot.create_collection.return_value = {
|
96
|
+
"success": {"0": "new_key_success"}
|
97
|
+
}
|
98
|
+
result = find_or_create_collection(
|
99
|
+
self.fake_zot, "/NewCollection", create_missing=True
|
100
|
+
)
|
101
|
+
self.assertEqual(result, "new_key_success")
|
102
|
+
# Verify payload formatting: for a simple (non-nested) path, no parentCollection.
|
103
|
+
args, _ = self.fake_zot.create_collection.call_args
|
104
|
+
payload = args[0]
|
105
|
+
self.assertEqual(payload["name"], "newcollection")
|
106
|
+
self.assertNotIn("parentCollection", payload)
|
107
|
+
|
108
|
+
def test_create_collection_with_successful_key(self):
|
109
|
+
"""
|
110
|
+
Test that when create_missing is True and the response contains a "successful" key,
|
111
|
+
the function returns the new collection key.
|
112
|
+
"""
|
113
|
+
self.fake_zot.collections.return_value = []
|
114
|
+
self.fake_zot.create_collection.return_value = {
|
115
|
+
"successful": {"0": {"data": {"key": "new_key_successful"}}}
|
116
|
+
}
|
117
|
+
result = find_or_create_collection(
|
118
|
+
self.fake_zot, "/NewCollection", create_missing=True
|
119
|
+
)
|
120
|
+
self.assertEqual(result, "new_key_successful")
|
121
|
+
|
122
|
+
def test_create_collection_exception(self):
|
123
|
+
"""
|
124
|
+
Test that if create_collection raises an exception,
|
125
|
+
the function logs the error and returns None.
|
126
|
+
"""
|
127
|
+
self.fake_zot.collections.return_value = []
|
128
|
+
self.fake_zot.create_collection.side_effect = Exception("Creation error")
|
129
|
+
result = find_or_create_collection(
|
130
|
+
self.fake_zot, "/NewCollection", create_missing=True
|
131
|
+
)
|
132
|
+
self.assertIsNone(result)
|
133
|
+
|
134
|
+
|
135
|
+
class TestZoteroPath:
|
136
|
+
"""Tests for the zotero_path utility functions."""
|
137
|
+
|
138
|
+
def test_fetch_papers_for_save_no_papers(self):
|
139
|
+
"""Test that fetch_papers_for_save returns None when no papers are available."""
|
140
|
+
# Empty state
|
141
|
+
state = {}
|
142
|
+
assert fetch_papers_for_save(state) is None
|
143
|
+
|
144
|
+
# State with empty last_displayed_papers
|
145
|
+
state = {"last_displayed_papers": ""}
|
146
|
+
assert fetch_papers_for_save(state) is None
|
147
|
+
|
148
|
+
# State with last_displayed_papers pointing to non-existent key
|
149
|
+
state = {"last_displayed_papers": "nonexistent_key"}
|
150
|
+
assert fetch_papers_for_save(state) is None
|
151
|
+
|
152
|
+
def test_fetch_papers_for_save_with_papers(self):
|
153
|
+
"""Test that fetch_papers_for_save correctly retrieves papers from state."""
|
154
|
+
# State with direct papers
|
155
|
+
sample_papers = {"paper1": {"Title": "Test Paper"}}
|
156
|
+
state = {"last_displayed_papers": sample_papers}
|
157
|
+
assert fetch_papers_for_save(state) == sample_papers
|
158
|
+
|
159
|
+
# State with papers referenced by key
|
160
|
+
state = {"last_displayed_papers": "zotero_read", "zotero_read": sample_papers}
|
161
|
+
assert fetch_papers_for_save(state) == sample_papers
|
162
|
+
|
163
|
+
@patch("pyzotero.zotero.Zotero")
|
164
|
+
def test_find_or_create_collection_exact_match(self, mock_zotero):
|
165
|
+
"""Test that find_or_create_collection correctly finds an exact match."""
|
166
|
+
# Setup mock
|
167
|
+
mock_zot = MagicMock()
|
168
|
+
mock_zotero.return_value = mock_zot
|
169
|
+
|
170
|
+
# Setup collections
|
171
|
+
collections = [
|
172
|
+
{"key": "abc123", "data": {"name": "Curiosity", "parentCollection": None}},
|
173
|
+
{
|
174
|
+
"key": "def456",
|
175
|
+
"data": {"name": "Curiosity1", "parentCollection": "abc123"},
|
176
|
+
},
|
177
|
+
{"key": "ghi789", "data": {"name": "Random", "parentCollection": None}},
|
178
|
+
{"key": "rad123", "data": {"name": "radiation", "parentCollection": None}},
|
179
|
+
]
|
180
|
+
mock_zot.collections.return_value = collections
|
181
|
+
|
182
|
+
# Test finding "Curiosity"
|
183
|
+
result = find_or_create_collection(mock_zot, "/Curiosity")
|
184
|
+
assert result == "abc123"
|
185
|
+
|
186
|
+
# Test finding with different case
|
187
|
+
result = find_or_create_collection(mock_zot, "/curiosity")
|
188
|
+
assert result == "abc123"
|
189
|
+
|
190
|
+
# Test finding "radiation" - direct match
|
191
|
+
result = find_or_create_collection(mock_zot, "/radiation")
|
192
|
+
assert result == "rad123"
|
193
|
+
|
194
|
+
# Test finding without leading slash
|
195
|
+
result = find_or_create_collection(mock_zot, "radiation")
|
196
|
+
assert result == "rad123"
|
197
|
+
|
198
|
+
@patch("pyzotero.zotero.Zotero")
|
199
|
+
def test_find_or_create_collection_no_match(self, mock_zotero):
|
200
|
+
"""Test that find_or_create_collection returns None for non-existent collections."""
|
201
|
+
# Setup mock
|
202
|
+
mock_zot = MagicMock()
|
203
|
+
mock_zotero.return_value = mock_zot
|
204
|
+
|
205
|
+
# Setup collections
|
206
|
+
collections = [
|
207
|
+
{"key": "abc123", "data": {"name": "Curiosity", "parentCollection": None}},
|
208
|
+
{
|
209
|
+
"key": "def456",
|
210
|
+
"data": {"name": "Curiosity1", "parentCollection": "abc123"},
|
211
|
+
},
|
212
|
+
]
|
213
|
+
mock_zot.collections.return_value = collections
|
214
|
+
|
215
|
+
# Test finding non-existent "Curiosity2"
|
216
|
+
result = find_or_create_collection(mock_zot, "/Curiosity2")
|
217
|
+
assert result is None
|
218
|
+
|
219
|
+
# Test finding non-existent nested path
|
220
|
+
result = find_or_create_collection(mock_zot, "/Curiosity/Curiosity2")
|
221
|
+
assert result is None
|
222
|
+
|
223
|
+
@patch("pyzotero.zotero.Zotero")
|
224
|
+
def test_find_or_create_collection_with_creation(self, mock_zotero):
|
225
|
+
"""Test that find_or_create_collection creates collections when requested."""
|
226
|
+
# Setup mock
|
227
|
+
mock_zot = MagicMock()
|
228
|
+
mock_zotero.return_value = mock_zot
|
229
|
+
|
230
|
+
# Setup collections
|
231
|
+
collections = [
|
232
|
+
{"key": "abc123", "data": {"name": "Curiosity", "parentCollection": None}}
|
233
|
+
]
|
234
|
+
mock_zot.collections.return_value = collections
|
235
|
+
|
236
|
+
# Setup create_collection response
|
237
|
+
mock_zot.create_collection.return_value = {
|
238
|
+
"successful": {"0": {"data": {"key": "new_key"}}}
|
239
|
+
}
|
240
|
+
|
241
|
+
# Test creating "Curiosity2" - note we're expecting lowercase in the call
|
242
|
+
result = find_or_create_collection(mock_zot, "/Curiosity2", create_missing=True)
|
243
|
+
assert result == "new_key"
|
244
|
+
# Use case-insensitive check for the collection name
|
245
|
+
mock_zot.create_collection.assert_called_once()
|
246
|
+
call_args = mock_zot.create_collection.call_args[0][0]
|
247
|
+
assert "name" in call_args
|
248
|
+
assert call_args["name"].lower() == "curiosity2"
|
249
|
+
|
250
|
+
# Test creating nested "Curiosity/Curiosity2"
|
251
|
+
mock_zot.create_collection.reset_mock()
|
252
|
+
result = find_or_create_collection(
|
253
|
+
mock_zot, "/Curiosity/Curiosity2", create_missing=True
|
254
|
+
)
|
255
|
+
assert result == "new_key"
|
256
|
+
# Check that the call includes parentCollection
|
257
|
+
mock_zot.create_collection.assert_called_once()
|
258
|
+
call_args = mock_zot.create_collection.call_args[0][0]
|
259
|
+
assert "name" in call_args
|
260
|
+
assert "parentCollection" in call_args
|
261
|
+
assert call_args["name"].lower() == "curiosity2"
|
262
|
+
assert call_args["parentCollection"] == "abc123"
|
263
|
+
|
264
|
+
@patch("pyzotero.zotero.Zotero")
|
265
|
+
def test_get_all_collection_paths(self, mock_zotero):
|
266
|
+
"""Test that get_all_collection_paths returns correct paths."""
|
267
|
+
# Setup mock
|
268
|
+
mock_zot = MagicMock()
|
269
|
+
mock_zotero.return_value = mock_zot
|
270
|
+
|
271
|
+
# Setup collections
|
272
|
+
collections = [
|
273
|
+
{"key": "abc123", "data": {"name": "Curiosity", "parentCollection": None}},
|
274
|
+
{
|
275
|
+
"key": "def456",
|
276
|
+
"data": {"name": "Curiosity1", "parentCollection": "abc123"},
|
277
|
+
},
|
278
|
+
{"key": "ghi789", "data": {"name": "Random", "parentCollection": None}},
|
279
|
+
]
|
280
|
+
mock_zot.collections.return_value = collections
|
281
|
+
|
282
|
+
# Test getting all paths
|
283
|
+
result = get_all_collection_paths(mock_zot)
|
284
|
+
assert "/Curiosity" in result
|
285
|
+
assert "/Random" in result
|
286
|
+
assert "/Curiosity/Curiosity1" in result
|
287
|
+
|
288
|
+
|
289
|
+
class TestZoteroWrite:
|
290
|
+
"""Integration tests for zotero_write.py."""
|
291
|
+
|
292
|
+
@pytest.fixture
|
293
|
+
def mock_hydra(self):
|
294
|
+
"""Fixture to mock hydra configuration."""
|
295
|
+
with patch(
|
296
|
+
"aiagents4pharma.talk2scholars.tools.zotero.utils.write_helper.hydra.compose"
|
297
|
+
) as mock_compose:
|
298
|
+
cfg = MagicMock()
|
299
|
+
cfg.tools.zotero_write.user_id = "test_user"
|
300
|
+
cfg.tools.zotero_write.library_type = "user"
|
301
|
+
cfg.tools.zotero_write.api_key = "test_key"
|
302
|
+
cfg.tools.zotero_write.zotero = MagicMock()
|
303
|
+
cfg.tools.zotero_write.zotero.max_limit = 50
|
304
|
+
mock_compose.return_value = cfg
|
305
|
+
yield cfg
|
306
|
+
|
307
|
+
@pytest.fixture
|
308
|
+
def mock_zotero(self):
|
309
|
+
"""Fixture to mock Zotero client."""
|
310
|
+
with patch(
|
311
|
+
"aiagents4pharma.talk2scholars.tools.zotero.utils.write_helper.zotero.Zotero"
|
312
|
+
) as mock_zot_class:
|
313
|
+
mock_zot = MagicMock()
|
314
|
+
mock_zot_class.return_value = mock_zot
|
315
|
+
yield mock_zot
|
316
|
+
|
317
|
+
@patch(
|
318
|
+
"aiagents4pharma.talk2scholars.tools.zotero.utils.write_helper.fetch_papers_for_save"
|
319
|
+
)
|
320
|
+
def test_zotero_write_no_papers(self, mock_fetch):
|
321
|
+
"""When no papers exist (even after approval), the function raises a ValueError."""
|
322
|
+
mock_fetch.return_value = None
|
323
|
+
|
324
|
+
state = {
|
325
|
+
"zotero_write_approval_status": {
|
326
|
+
"approved": True,
|
327
|
+
"collection_path": "/Curiosity",
|
328
|
+
}
|
329
|
+
}
|
330
|
+
|
331
|
+
with pytest.raises(ValueError) as excinfo:
|
332
|
+
zotero_write.run(
|
333
|
+
{
|
334
|
+
"tool_call_id": "test_id",
|
335
|
+
"collection_path": "/Curiosity",
|
336
|
+
"state": state,
|
337
|
+
}
|
338
|
+
)
|
339
|
+
assert "No fetched papers were found to save" in str(excinfo.value)
|
340
|
+
|
341
|
+
@patch(
|
342
|
+
"aiagents4pharma.talk2scholars.tools.zotero.utils.write_helper.fetch_papers_for_save"
|
343
|
+
)
|
344
|
+
@patch(
|
345
|
+
"aiagents4pharma.talk2scholars.tools.zotero.utils.write_helper.find_or_create_collection"
|
346
|
+
)
|
347
|
+
def test_zotero_write_invalid_collection(
|
348
|
+
self, mock_find, mock_fetch, mock_zotero
|
349
|
+
):
|
350
|
+
"""Saving to a nonexistent Zotero collection returns an error Command."""
|
351
|
+
sample = {"paper1": {"Title": "Test Paper"}}
|
352
|
+
mock_fetch.return_value = sample
|
353
|
+
mock_find.return_value = None
|
354
|
+
mock_zotero.collections.return_value = [
|
355
|
+
{"key": "k1", "data": {"name": "Curiosity"}},
|
356
|
+
{"key": "k2", "data": {"name": "Random"}},
|
357
|
+
]
|
358
|
+
|
359
|
+
state = {
|
360
|
+
"zotero_write_approval_status": {
|
361
|
+
"approved": True,
|
362
|
+
"collection_path": "/NonExistent",
|
363
|
+
},
|
364
|
+
"last_displayed_papers": "papers",
|
365
|
+
"papers": sample,
|
366
|
+
}
|
367
|
+
|
368
|
+
result = zotero_write.run(
|
369
|
+
{
|
370
|
+
"tool_call_id": "test_id",
|
371
|
+
"collection_path": "/NonExistent",
|
372
|
+
"state": state,
|
373
|
+
}
|
374
|
+
)
|
375
|
+
|
376
|
+
msg = result.update["messages"][0].content
|
377
|
+
assert "does not exist in Zotero" in msg
|
378
|
+
assert "Curiosity, Random" in msg
|
379
|
+
|
380
|
+
@patch(
|
381
|
+
"aiagents4pharma.talk2scholars.tools.zotero.utils.write_helper.fetch_papers_for_save"
|
382
|
+
)
|
383
|
+
@patch(
|
384
|
+
"aiagents4pharma.talk2scholars.tools.zotero.utils.write_helper.find_or_create_collection"
|
385
|
+
)
|
386
|
+
def test_zotero_write_success(
|
387
|
+
self, mock_find, mock_fetch, mock_hydra, mock_zotero
|
388
|
+
):
|
389
|
+
"""A valid approved save returns a success Command with summary."""
|
390
|
+
sample = {"paper1": {"Title": "Test Paper", "Authors": ["Test Author"]}}
|
391
|
+
mock_fetch.return_value = sample
|
392
|
+
mock_find.return_value = "abc123"
|
393
|
+
mock_zotero.collections.return_value = [
|
394
|
+
{"key": "abc123", "data": {"name": "radiation"}}
|
395
|
+
]
|
396
|
+
mock_zotero.create_items.return_value = {
|
397
|
+
"successful": {"0": {"key": "item123"}}
|
398
|
+
}
|
399
|
+
mock_hydra.tools.zotero_write.zotero.max_limit = 50
|
400
|
+
|
401
|
+
state = {
|
402
|
+
"zotero_write_approval_status": {
|
403
|
+
"approved": True,
|
404
|
+
"collection_path": "/radiation",
|
405
|
+
},
|
406
|
+
"last_displayed_papers": "papers",
|
407
|
+
"papers": sample,
|
408
|
+
}
|
409
|
+
|
410
|
+
result = zotero_write.run(
|
411
|
+
{
|
412
|
+
"tool_call_id": "test_id",
|
413
|
+
"collection_path": "/radiation",
|
414
|
+
"state": state,
|
415
|
+
}
|
416
|
+
)
|
417
|
+
|
418
|
+
msg = result.update["messages"][0].content
|
419
|
+
assert "Save was successful" in msg
|
420
|
+
assert "radiation" in msg
|
421
|
+
|
422
|
+
|
423
|
+
class TestZoteroRead:
|
424
|
+
"""Integration tests for zotero_read.py."""
|
425
|
+
|
426
|
+
@pytest.fixture
|
427
|
+
def mock_hydra(self):
|
428
|
+
"""Fixture to mock hydra configuration."""
|
429
|
+
with patch(
|
430
|
+
"aiagents4pharma.talk2scholars.tools.zotero.utils.write_helper.hydra.initialize"
|
431
|
+
), patch(
|
432
|
+
"aiagents4pharma.talk2scholars.tools.zotero.utils.write_helper.hydra.compose"
|
433
|
+
) as mock_compose:
|
434
|
+
cfg = MagicMock()
|
435
|
+
cfg.tools.zotero_read.user_id = "test_user"
|
436
|
+
cfg.tools.zotero_read.library_type = "user"
|
437
|
+
cfg.tools.zotero_read.api_key = "test_key"
|
438
|
+
cfg.tools.zotero_read.zotero = MagicMock()
|
439
|
+
cfg.tools.zotero_read.zotero.max_limit = 50
|
440
|
+
cfg.tools.zotero_read.zotero.filter_item_types = [
|
441
|
+
"journalArticle",
|
442
|
+
"conferencePaper",
|
443
|
+
]
|
444
|
+
mock_compose.return_value = cfg
|
445
|
+
yield cfg
|
446
|
+
|
447
|
+
@pytest.fixture
|
448
|
+
def mock_zotero(self):
|
449
|
+
"""Fixture to mock Zotero client."""
|
450
|
+
with patch(
|
451
|
+
"aiagents4pharma.talk2scholars.tools.zotero.utils.write_helper.zotero.Zotero"
|
452
|
+
) as mock_zot_class:
|
453
|
+
mock_zot = MagicMock()
|
454
|
+
mock_zot_class.return_value = mock_zot
|
455
|
+
yield mock_zot
|
456
|
+
|
457
|
+
@patch(
|
458
|
+
"aiagents4pharma.talk2scholars.tools.zotero.utils.zotero_path.get_item_collections"
|
459
|
+
)
|
460
|
+
def test_zotero_read_item_collections_error(
|
461
|
+
self, mock_get_collections, mock_hydra, mock_zotero
|
462
|
+
):
|
463
|
+
"""Test that zotero_read handles errors in get_item_collections."""
|
464
|
+
|
465
|
+
mock_get_collections.side_effect = Exception("Test error")
|
466
|
+
|
467
|
+
mock_zotero.items.return_value = [
|
468
|
+
{
|
469
|
+
"data": {
|
470
|
+
"key": "paper1",
|
471
|
+
"title": "Test Paper",
|
472
|
+
"itemType": "journalArticle",
|
473
|
+
}
|
474
|
+
}
|
475
|
+
]
|
476
|
+
mock_hydra.tools.zotero_read.zotero.max_limit = 50
|
477
|
+
|
478
|
+
result = zotero_read.run(
|
479
|
+
{
|
480
|
+
"query": "test",
|
481
|
+
"only_articles": True,
|
482
|
+
"tool_call_id": "test_id",
|
483
|
+
"limit": 2,
|
484
|
+
}
|
485
|
+
)
|
486
|
+
|
487
|
+
assert result is not None
|
488
|
+
assert isinstance(result.update, dict)
|
489
|
+
assert "zotero_read" in result.update
|
@@ -6,9 +6,7 @@ from types import SimpleNamespace
|
|
6
6
|
import unittest
|
7
7
|
from unittest.mock import patch, MagicMock
|
8
8
|
from langgraph.types import Command
|
9
|
-
from aiagents4pharma.talk2scholars.tools.zotero.zotero_read import
|
10
|
-
zotero_search_tool,
|
11
|
-
)
|
9
|
+
from aiagents4pharma.talk2scholars.tools.zotero.zotero_read import zotero_read
|
12
10
|
|
13
11
|
|
14
12
|
# Dummy Hydra configuration to be used in tests
|
@@ -29,11 +27,13 @@ class TestZoteroSearchTool(unittest.TestCase):
|
|
29
27
|
"""Tests for Zotero search tool."""
|
30
28
|
|
31
29
|
@patch(
|
32
|
-
"aiagents4pharma.talk2scholars.tools.zotero.
|
30
|
+
"aiagents4pharma.talk2scholars.tools.zotero.utils.zotero_path.get_item_collections"
|
31
|
+
)
|
32
|
+
@patch("aiagents4pharma.talk2scholars.tools.zotero.utils.read_helper.zotero.Zotero")
|
33
|
+
@patch("aiagents4pharma.talk2scholars.tools.zotero.utils.read_helper.hydra.compose")
|
34
|
+
@patch(
|
35
|
+
"aiagents4pharma.talk2scholars.tools.zotero.utils.read_helper.hydra.initialize"
|
33
36
|
)
|
34
|
-
@patch("aiagents4pharma.talk2scholars.tools.zotero.zotero_read.zotero.Zotero")
|
35
|
-
@patch("aiagents4pharma.talk2scholars.tools.zotero.zotero_read.hydra.compose")
|
36
|
-
@patch("aiagents4pharma.talk2scholars.tools.zotero.zotero_read.hydra.initialize")
|
37
37
|
def test_valid_query(
|
38
38
|
self,
|
39
39
|
mock_hydra_init,
|
@@ -87,7 +87,7 @@ class TestZoteroSearchTool(unittest.TestCase):
|
|
87
87
|
"tool_call_id": tool_call_id,
|
88
88
|
"limit": 2,
|
89
89
|
}
|
90
|
-
result =
|
90
|
+
result = zotero_read.run(tool_input)
|
91
91
|
|
92
92
|
# Verify the Command update structure and contents
|
93
93
|
self.assertIsInstance(result, Command)
|
@@ -104,11 +104,13 @@ class TestZoteroSearchTool(unittest.TestCase):
|
|
104
104
|
self.assertIn("Number of papers found: 2", message_content)
|
105
105
|
|
106
106
|
@patch(
|
107
|
-
"aiagents4pharma.talk2scholars.tools.zotero.
|
107
|
+
"aiagents4pharma.talk2scholars.tools.zotero.utils.zotero_path.get_item_collections"
|
108
|
+
)
|
109
|
+
@patch("aiagents4pharma.talk2scholars.tools.zotero.utils.read_helper.zotero.Zotero")
|
110
|
+
@patch("aiagents4pharma.talk2scholars.tools.zotero.utils.read_helper.hydra.compose")
|
111
|
+
@patch(
|
112
|
+
"aiagents4pharma.talk2scholars.tools.zotero.utils.read_helper.hydra.initialize"
|
108
113
|
)
|
109
|
-
@patch("aiagents4pharma.talk2scholars.tools.zotero.zotero_read.zotero.Zotero")
|
110
|
-
@patch("aiagents4pharma.talk2scholars.tools.zotero.zotero_read.hydra.compose")
|
111
|
-
@patch("aiagents4pharma.talk2scholars.tools.zotero.zotero_read.hydra.initialize")
|
112
114
|
def test_empty_query_fetch_all_items(
|
113
115
|
self,
|
114
116
|
mock_hydra_init,
|
@@ -144,7 +146,7 @@ class TestZoteroSearchTool(unittest.TestCase):
|
|
144
146
|
"tool_call_id": tool_call_id,
|
145
147
|
"limit": 2,
|
146
148
|
}
|
147
|
-
result =
|
149
|
+
result = zotero_read.run(tool_input)
|
148
150
|
|
149
151
|
update = result.update
|
150
152
|
filtered_papers = update["zotero_read"]
|
@@ -154,11 +156,13 @@ class TestZoteroSearchTool(unittest.TestCase):
|
|
154
156
|
)
|
155
157
|
|
156
158
|
@patch(
|
157
|
-
"aiagents4pharma.talk2scholars.tools.zotero.
|
159
|
+
"aiagents4pharma.talk2scholars.tools.zotero.utils.zotero_path.get_item_collections"
|
160
|
+
)
|
161
|
+
@patch("aiagents4pharma.talk2scholars.tools.zotero.utils.read_helper.zotero.Zotero")
|
162
|
+
@patch("aiagents4pharma.talk2scholars.tools.zotero.utils.read_helper.hydra.compose")
|
163
|
+
@patch(
|
164
|
+
"aiagents4pharma.talk2scholars.tools.zotero.utils.read_helper.hydra.initialize"
|
158
165
|
)
|
159
|
-
@patch("aiagents4pharma.talk2scholars.tools.zotero.zotero_read.zotero.Zotero")
|
160
|
-
@patch("aiagents4pharma.talk2scholars.tools.zotero.zotero_read.hydra.compose")
|
161
|
-
@patch("aiagents4pharma.talk2scholars.tools.zotero.zotero_read.hydra.initialize")
|
162
166
|
def test_no_items_returned(
|
163
167
|
self,
|
164
168
|
mock_hydra_init,
|
@@ -183,15 +187,17 @@ class TestZoteroSearchTool(unittest.TestCase):
|
|
183
187
|
"limit": 2,
|
184
188
|
}
|
185
189
|
with self.assertRaises(RuntimeError) as context:
|
186
|
-
|
190
|
+
zotero_read.run(tool_input)
|
187
191
|
self.assertIn("No items returned from Zotero", str(context.exception))
|
188
192
|
|
189
193
|
@patch(
|
190
|
-
"aiagents4pharma.talk2scholars.tools.zotero.
|
194
|
+
"aiagents4pharma.talk2scholars.tools.zotero.utils.zotero_path.get_item_collections"
|
195
|
+
)
|
196
|
+
@patch("aiagents4pharma.talk2scholars.tools.zotero.utils.read_helper.zotero.Zotero")
|
197
|
+
@patch("aiagents4pharma.talk2scholars.tools.zotero.utils.read_helper.hydra.compose")
|
198
|
+
@patch(
|
199
|
+
"aiagents4pharma.talk2scholars.tools.zotero.utils.read_helper.hydra.initialize"
|
191
200
|
)
|
192
|
-
@patch("aiagents4pharma.talk2scholars.tools.zotero.zotero_read.zotero.Zotero")
|
193
|
-
@patch("aiagents4pharma.talk2scholars.tools.zotero.zotero_read.hydra.compose")
|
194
|
-
@patch("aiagents4pharma.talk2scholars.tools.zotero.zotero_read.hydra.initialize")
|
195
201
|
def test_filtering_no_matching_papers(
|
196
202
|
self,
|
197
203
|
mock_hydra_init,
|
@@ -244,7 +250,7 @@ class TestZoteroSearchTool(unittest.TestCase):
|
|
244
250
|
"limit": 2,
|
245
251
|
}
|
246
252
|
# Instead of expecting a RuntimeError, we now expect both items to be returned.
|
247
|
-
result =
|
253
|
+
result = zotero_read.run(tool_input)
|
248
254
|
update = result.update
|
249
255
|
filtered_papers = update["zotero_read"]
|
250
256
|
self.assertIn("paper1", filtered_papers)
|
@@ -252,11 +258,13 @@ class TestZoteroSearchTool(unittest.TestCase):
|
|
252
258
|
self.assertEqual(len(filtered_papers), 2)
|
253
259
|
|
254
260
|
@patch(
|
255
|
-
"aiagents4pharma.talk2scholars.tools.zotero.
|
261
|
+
"aiagents4pharma.talk2scholars.tools.zotero.utils.zotero_path.get_item_collections"
|
262
|
+
)
|
263
|
+
@patch("aiagents4pharma.talk2scholars.tools.zotero.utils.read_helper.zotero.Zotero")
|
264
|
+
@patch("aiagents4pharma.talk2scholars.tools.zotero.utils.read_helper.hydra.compose")
|
265
|
+
@patch(
|
266
|
+
"aiagents4pharma.talk2scholars.tools.zotero.utils.read_helper.hydra.initialize"
|
256
267
|
)
|
257
|
-
@patch("aiagents4pharma.talk2scholars.tools.zotero.zotero_read.zotero.Zotero")
|
258
|
-
@patch("aiagents4pharma.talk2scholars.tools.zotero.zotero_read.hydra.compose")
|
259
|
-
@patch("aiagents4pharma.talk2scholars.tools.zotero.zotero_read.hydra.initialize")
|
260
268
|
def test_items_api_exception(
|
261
269
|
self,
|
262
270
|
mock_hydra_init,
|
@@ -281,15 +289,17 @@ class TestZoteroSearchTool(unittest.TestCase):
|
|
281
289
|
"limit": 2,
|
282
290
|
}
|
283
291
|
with self.assertRaises(RuntimeError) as context:
|
284
|
-
|
292
|
+
zotero_read.run(tool_input)
|
285
293
|
self.assertIn("Failed to fetch items from Zotero", str(context.exception))
|
286
294
|
|
287
295
|
@patch(
|
288
|
-
"aiagents4pharma.talk2scholars.tools.zotero.
|
296
|
+
"aiagents4pharma.talk2scholars.tools.zotero.utils.zotero_path.get_item_collections"
|
297
|
+
)
|
298
|
+
@patch("aiagents4pharma.talk2scholars.tools.zotero.utils.read_helper.zotero.Zotero")
|
299
|
+
@patch("aiagents4pharma.talk2scholars.tools.zotero.utils.read_helper.hydra.compose")
|
300
|
+
@patch(
|
301
|
+
"aiagents4pharma.talk2scholars.tools.zotero.utils.read_helper.hydra.initialize"
|
289
302
|
)
|
290
|
-
@patch("aiagents4pharma.talk2scholars.tools.zotero.zotero_read.zotero.Zotero")
|
291
|
-
@patch("aiagents4pharma.talk2scholars.tools.zotero.zotero_read.hydra.compose")
|
292
|
-
@patch("aiagents4pharma.talk2scholars.tools.zotero.zotero_read.hydra.initialize")
|
293
303
|
def test_missing_key_in_item(
|
294
304
|
self,
|
295
305
|
mock_hydra_init,
|
@@ -336,7 +346,7 @@ class TestZoteroSearchTool(unittest.TestCase):
|
|
336
346
|
"tool_call_id": tool_call_id,
|
337
347
|
"limit": 2,
|
338
348
|
}
|
339
|
-
result =
|
349
|
+
result = zotero_read.run(tool_input)
|
340
350
|
|
341
351
|
update = result.update
|
342
352
|
filtered_papers = update["zotero_read"]
|
@@ -344,11 +354,13 @@ class TestZoteroSearchTool(unittest.TestCase):
|
|
344
354
|
self.assertEqual(len(filtered_papers), 1)
|
345
355
|
|
346
356
|
@patch(
|
347
|
-
"aiagents4pharma.talk2scholars.tools.zotero.
|
357
|
+
"aiagents4pharma.talk2scholars.tools.zotero.utils.zotero_path.get_item_collections"
|
358
|
+
)
|
359
|
+
@patch("aiagents4pharma.talk2scholars.tools.zotero.utils.read_helper.zotero.Zotero")
|
360
|
+
@patch("aiagents4pharma.talk2scholars.tools.zotero.utils.read_helper.hydra.compose")
|
361
|
+
@patch(
|
362
|
+
"aiagents4pharma.talk2scholars.tools.zotero.utils.read_helper.hydra.initialize"
|
348
363
|
)
|
349
|
-
@patch("aiagents4pharma.talk2scholars.tools.zotero.zotero_read.zotero.Zotero")
|
350
|
-
@patch("aiagents4pharma.talk2scholars.tools.zotero.zotero_read.hydra.compose")
|
351
|
-
@patch("aiagents4pharma.talk2scholars.tools.zotero.zotero_read.hydra.initialize")
|
352
364
|
def test_item_not_dict(
|
353
365
|
self,
|
354
366
|
mock_hydra_init,
|
@@ -378,15 +390,17 @@ class TestZoteroSearchTool(unittest.TestCase):
|
|
378
390
|
"limit": 2,
|
379
391
|
}
|
380
392
|
with self.assertRaises(RuntimeError) as context:
|
381
|
-
|
393
|
+
zotero_read.run(tool_input)
|
382
394
|
self.assertIn("No matching papers returned from Zotero", str(context.exception))
|
383
395
|
|
384
396
|
@patch(
|
385
|
-
"aiagents4pharma.talk2scholars.tools.zotero.
|
397
|
+
"aiagents4pharma.talk2scholars.tools.zotero.utils.zotero_path.get_item_collections"
|
398
|
+
)
|
399
|
+
@patch("aiagents4pharma.talk2scholars.tools.zotero.utils.read_helper.zotero.Zotero")
|
400
|
+
@patch("aiagents4pharma.talk2scholars.tools.zotero.utils.read_helper.hydra.compose")
|
401
|
+
@patch(
|
402
|
+
"aiagents4pharma.talk2scholars.tools.zotero.utils.read_helper.hydra.initialize"
|
386
403
|
)
|
387
|
-
@patch("aiagents4pharma.talk2scholars.tools.zotero.zotero_read.zotero.Zotero")
|
388
|
-
@patch("aiagents4pharma.talk2scholars.tools.zotero.zotero_read.hydra.compose")
|
389
|
-
@patch("aiagents4pharma.talk2scholars.tools.zotero.zotero_read.hydra.initialize")
|
390
404
|
def test_data_not_dict(
|
391
405
|
self,
|
392
406
|
mock_hydra_init,
|
@@ -415,5 +429,5 @@ class TestZoteroSearchTool(unittest.TestCase):
|
|
415
429
|
"limit": 2,
|
416
430
|
}
|
417
431
|
with self.assertRaises(RuntimeError) as context:
|
418
|
-
|
432
|
+
zotero_read.run(tool_input)
|
419
433
|
self.assertIn("No matching papers returned from Zotero", str(context.exception))
|