aiagents4pharma 1.30.0__py3-none-any.whl → 1.30.2__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/main_agent.py +18 -10
- aiagents4pharma/talk2scholars/agents/paper_download_agent.py +5 -6
- aiagents4pharma/talk2scholars/agents/pdf_agent.py +4 -10
- aiagents4pharma/talk2scholars/agents/zotero_agent.py +9 -7
- aiagents4pharma/talk2scholars/configs/agents/talk2scholars/main_agent/default.yaml +18 -9
- aiagents4pharma/talk2scholars/configs/agents/talk2scholars/s2_agent/default.yaml +2 -2
- aiagents4pharma/talk2scholars/configs/agents/talk2scholars/zotero_agent/default.yaml +9 -15
- aiagents4pharma/talk2scholars/configs/app/frontend/default.yaml +1 -0
- aiagents4pharma/talk2scholars/configs/tools/multi_paper_recommendation/default.yaml +6 -1
- aiagents4pharma/talk2scholars/configs/tools/search/default.yaml +7 -1
- aiagents4pharma/talk2scholars/configs/tools/single_paper_recommendation/default.yaml +6 -1
- aiagents4pharma/talk2scholars/configs/tools/zotero_read/default.yaml +1 -1
- aiagents4pharma/talk2scholars/configs/tools/zotero_write/default.yaml +55 -0
- aiagents4pharma/talk2scholars/state/state_talk2scholars.py +7 -1
- aiagents4pharma/talk2scholars/tests/test_llm_main_integration.py +84 -53
- aiagents4pharma/talk2scholars/tests/test_main_agent.py +24 -0
- aiagents4pharma/talk2scholars/tests/test_question_and_answer_tool.py +79 -15
- aiagents4pharma/talk2scholars/tests/test_routing_logic.py +13 -10
- aiagents4pharma/talk2scholars/tests/test_s2_multi.py +27 -4
- aiagents4pharma/talk2scholars/tests/test_s2_search.py +19 -3
- aiagents4pharma/talk2scholars/tests/test_s2_single.py +27 -3
- 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 +419 -1
- aiagents4pharma/talk2scholars/tests/test_zotero_read.py +25 -18
- aiagents4pharma/talk2scholars/tests/test_zotero_write.py +123 -588
- aiagents4pharma/talk2scholars/tools/paper_download/abstract_downloader.py +2 -0
- aiagents4pharma/talk2scholars/tools/paper_download/arxiv_downloader.py +11 -4
- aiagents4pharma/talk2scholars/tools/paper_download/download_arxiv_input.py +5 -1
- aiagents4pharma/talk2scholars/tools/pdf/question_and_answer.py +73 -26
- aiagents4pharma/talk2scholars/tools/s2/multi_paper_rec.py +46 -22
- aiagents4pharma/talk2scholars/tools/s2/query_results.py +1 -1
- aiagents4pharma/talk2scholars/tools/s2/search.py +40 -12
- aiagents4pharma/talk2scholars/tools/s2/single_paper_rec.py +42 -16
- aiagents4pharma/talk2scholars/tools/zotero/__init__.py +1 -0
- aiagents4pharma/talk2scholars/tools/zotero/utils/zotero_path.py +125 -0
- aiagents4pharma/talk2scholars/tools/zotero/zotero_read.py +35 -20
- aiagents4pharma/talk2scholars/tools/zotero/zotero_review.py +198 -0
- aiagents4pharma/talk2scholars/tools/zotero/zotero_write.py +86 -118
- {aiagents4pharma-1.30.0.dist-info → aiagents4pharma-1.30.2.dist-info}/METADATA +4 -3
- {aiagents4pharma-1.30.0.dist-info → aiagents4pharma-1.30.2.dist-info}/RECORD +44 -41
- {aiagents4pharma-1.30.0.dist-info → aiagents4pharma-1.30.2.dist-info}/WHEEL +1 -1
- {aiagents4pharma-1.30.0.dist-info → aiagents4pharma-1.30.2.dist-info/licenses}/LICENSE +0 -0
- {aiagents4pharma-1.30.0.dist-info → aiagents4pharma-1.30.2.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,411 @@ 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("hydra.compose") as mock_compose:
|
296
|
+
cfg = MagicMock()
|
297
|
+
cfg.tools.zotero_write.user_id = "test_user"
|
298
|
+
cfg.tools.zotero_write.library_type = "user"
|
299
|
+
cfg.tools.zotero_write.api_key = "test_key"
|
300
|
+
cfg.tools.zotero_write.zotero = MagicMock()
|
301
|
+
cfg.tools.zotero_write.zotero.max_limit = 50
|
302
|
+
mock_compose.return_value = cfg
|
303
|
+
yield cfg
|
304
|
+
|
305
|
+
@pytest.fixture
|
306
|
+
def mock_zotero(self):
|
307
|
+
"""Fixture to mock Zotero client."""
|
308
|
+
with patch("pyzotero.zotero.Zotero") as mock_zot_class:
|
309
|
+
mock_zot = MagicMock()
|
310
|
+
mock_zot_class.return_value = mock_zot
|
311
|
+
yield mock_zot
|
312
|
+
|
313
|
+
@patch(
|
314
|
+
"aiagents4pharma.talk2scholars.tools.zotero.utils.zotero_path.fetch_papers_for_save"
|
315
|
+
)
|
316
|
+
def test_zotero_write_no_papers(self, mock_fetch):
|
317
|
+
"""When no papers exist (even after approval), the function raises a ValueError."""
|
318
|
+
mock_fetch.return_value = None
|
319
|
+
|
320
|
+
state = {
|
321
|
+
"zotero_write_approval_status": {
|
322
|
+
"approved": True,
|
323
|
+
"collection_path": "/Curiosity",
|
324
|
+
}
|
325
|
+
}
|
326
|
+
|
327
|
+
with pytest.raises(ValueError) as excinfo:
|
328
|
+
zotero_write.run(
|
329
|
+
{
|
330
|
+
"tool_call_id": "test_id",
|
331
|
+
"collection_path": "/Curiosity",
|
332
|
+
"state": state,
|
333
|
+
}
|
334
|
+
)
|
335
|
+
assert "No fetched papers were found to save" in str(excinfo.value)
|
336
|
+
|
337
|
+
@patch(
|
338
|
+
"aiagents4pharma.talk2scholars.tools.zotero.utils.zotero_path.fetch_papers_for_save"
|
339
|
+
)
|
340
|
+
@patch(
|
341
|
+
"aiagents4pharma.talk2scholars.tools.zotero.utils.zotero_path.find_or_create_collection"
|
342
|
+
)
|
343
|
+
def test_zotero_write_invalid_collection(self, mock_find, mock_fetch, mock_zotero):
|
344
|
+
"""Saving to a nonexistent Zotero collection returns an error Command."""
|
345
|
+
sample = {"paper1": {"Title": "Test Paper"}}
|
346
|
+
mock_fetch.return_value = sample
|
347
|
+
mock_find.return_value = None
|
348
|
+
mock_zotero.collections.return_value = [
|
349
|
+
{"key": "k1", "data": {"name": "Curiosity"}},
|
350
|
+
{"key": "k2", "data": {"name": "Random"}},
|
351
|
+
]
|
352
|
+
|
353
|
+
state = {
|
354
|
+
"zotero_write_approval_status": {
|
355
|
+
"approved": True,
|
356
|
+
"collection_path": "/NonExistent",
|
357
|
+
},
|
358
|
+
"last_displayed_papers": "papers",
|
359
|
+
"papers": sample,
|
360
|
+
}
|
361
|
+
|
362
|
+
result = zotero_write.run(
|
363
|
+
{
|
364
|
+
"tool_call_id": "test_id",
|
365
|
+
"collection_path": "/NonExistent",
|
366
|
+
"state": state,
|
367
|
+
}
|
368
|
+
)
|
369
|
+
|
370
|
+
msg = result.update["messages"][0].content
|
371
|
+
assert "does not exist in Zotero" in msg
|
372
|
+
assert "Curiosity, Random" in msg
|
373
|
+
|
374
|
+
@patch(
|
375
|
+
"aiagents4pharma.talk2scholars.tools.zotero.utils.zotero_path.fetch_papers_for_save"
|
376
|
+
)
|
377
|
+
@patch(
|
378
|
+
"aiagents4pharma.talk2scholars.tools.zotero.utils.zotero_path.find_or_create_collection"
|
379
|
+
)
|
380
|
+
def test_zotero_write_success(self, mock_find, mock_fetch, mock_hydra, mock_zotero):
|
381
|
+
"""A valid approved save returns a success Command with summary."""
|
382
|
+
sample = {"paper1": {"Title": "Test Paper", "Authors": ["Test Author"]}}
|
383
|
+
mock_fetch.return_value = sample
|
384
|
+
mock_find.return_value = "abc123"
|
385
|
+
mock_zotero.collections.return_value = [
|
386
|
+
{"key": "abc123", "data": {"name": "radiation"}}
|
387
|
+
]
|
388
|
+
mock_zotero.create_items.return_value = {
|
389
|
+
"successful": {"0": {"key": "item123"}}
|
390
|
+
}
|
391
|
+
mock_hydra.tools.zotero_write.zotero.max_limit = 50
|
392
|
+
|
393
|
+
state = {
|
394
|
+
"zotero_write_approval_status": {
|
395
|
+
"approved": True,
|
396
|
+
"collection_path": "/radiation",
|
397
|
+
},
|
398
|
+
"last_displayed_papers": "papers",
|
399
|
+
"papers": sample,
|
400
|
+
}
|
401
|
+
|
402
|
+
result = zotero_write.run(
|
403
|
+
{
|
404
|
+
"tool_call_id": "test_id",
|
405
|
+
"collection_path": "/radiation",
|
406
|
+
"state": state,
|
407
|
+
}
|
408
|
+
)
|
409
|
+
|
410
|
+
msg = result.update["messages"][0].content
|
411
|
+
assert "Save was successful" in msg
|
412
|
+
assert "radiation" in msg
|
413
|
+
|
414
|
+
|
415
|
+
class TestZoteroRead:
|
416
|
+
"""Integration tests for zotero_read.py."""
|
417
|
+
|
418
|
+
@pytest.fixture
|
419
|
+
def mock_hydra(self):
|
420
|
+
"""Fixture to mock hydra configuration."""
|
421
|
+
with patch("hydra.initialize"), patch("hydra.compose") as mock_compose:
|
422
|
+
cfg = MagicMock()
|
423
|
+
cfg.tools.zotero_read.user_id = "test_user"
|
424
|
+
cfg.tools.zotero_read.library_type = "user"
|
425
|
+
cfg.tools.zotero_read.api_key = "test_key"
|
426
|
+
cfg.tools.zotero_read.zotero = MagicMock()
|
427
|
+
cfg.tools.zotero_read.zotero.max_limit = 50
|
428
|
+
cfg.tools.zotero_read.zotero.filter_item_types = [
|
429
|
+
"journalArticle",
|
430
|
+
"conferencePaper",
|
431
|
+
]
|
432
|
+
mock_compose.return_value = cfg
|
433
|
+
yield cfg
|
434
|
+
|
435
|
+
@pytest.fixture
|
436
|
+
def mock_zotero(self):
|
437
|
+
"""Fixture to mock Zotero client."""
|
438
|
+
with patch("pyzotero.zotero.Zotero") as mock_zot_class:
|
439
|
+
mock_zot = MagicMock()
|
440
|
+
mock_zot_class.return_value = mock_zot
|
441
|
+
yield mock_zot
|
442
|
+
|
443
|
+
@patch(
|
444
|
+
"aiagents4pharma.talk2scholars.tools.zotero.utils.zotero_path.get_item_collections"
|
445
|
+
)
|
446
|
+
def test_zotero_read_item_collections_error(
|
447
|
+
self, mock_get_collections, mock_hydra, mock_zotero
|
448
|
+
):
|
449
|
+
"""Test that zotero_read handles errors in get_item_collections."""
|
450
|
+
|
451
|
+
mock_get_collections.side_effect = Exception("Test error")
|
452
|
+
|
453
|
+
mock_zotero.items.return_value = [
|
454
|
+
{
|
455
|
+
"data": {
|
456
|
+
"key": "paper1",
|
457
|
+
"title": "Test Paper",
|
458
|
+
"itemType": "journalArticle",
|
459
|
+
}
|
460
|
+
}
|
461
|
+
]
|
462
|
+
mock_hydra.tools.zotero_read.zotero.max_limit = 50
|
463
|
+
|
464
|
+
result = zotero_read.run(
|
465
|
+
{
|
466
|
+
"query": "test",
|
467
|
+
"only_articles": True,
|
468
|
+
"tool_call_id": "test_id",
|
469
|
+
"limit": 2,
|
470
|
+
}
|
471
|
+
)
|
472
|
+
|
473
|
+
assert result is not None
|
474
|
+
assert isinstance(result.update, dict)
|
475
|
+
assert "zotero_read" in result.update
|
@@ -7,7 +7,7 @@ import unittest
|
|
7
7
|
from unittest.mock import patch, MagicMock
|
8
8
|
from langgraph.types import Command
|
9
9
|
from aiagents4pharma.talk2scholars.tools.zotero.zotero_read import (
|
10
|
-
|
10
|
+
zotero_read,
|
11
11
|
)
|
12
12
|
|
13
13
|
|
@@ -26,7 +26,7 @@ dummy_cfg = SimpleNamespace(tools=SimpleNamespace(zotero_read=dummy_zotero_read_
|
|
26
26
|
|
27
27
|
|
28
28
|
class TestZoteroSearchTool(unittest.TestCase):
|
29
|
-
"""
|
29
|
+
"""Tests for Zotero search tool."""
|
30
30
|
|
31
31
|
@patch(
|
32
32
|
"aiagents4pharma.talk2scholars.tools.zotero.zotero_read.get_item_collections"
|
@@ -41,7 +41,7 @@ class TestZoteroSearchTool(unittest.TestCase):
|
|
41
41
|
mock_zotero_class,
|
42
42
|
mock_get_item_collections,
|
43
43
|
):
|
44
|
-
"""
|
44
|
+
"""Test valid query returns correct Command output."""
|
45
45
|
# Setup Hydra mocks
|
46
46
|
mock_hydra_compose.return_value = dummy_cfg
|
47
47
|
mock_hydra_init.return_value.__enter__.return_value = None
|
@@ -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)
|
@@ -116,7 +116,7 @@ class TestZoteroSearchTool(unittest.TestCase):
|
|
116
116
|
mock_zotero_class,
|
117
117
|
mock_get_item_collections,
|
118
118
|
):
|
119
|
-
"""
|
119
|
+
"""Test empty query fetches all items."""
|
120
120
|
mock_hydra_compose.return_value = dummy_cfg
|
121
121
|
mock_hydra_init.return_value.__enter__.return_value = None
|
122
122
|
|
@@ -144,7 +144,7 @@ class TestZoteroSearchTool(unittest.TestCase):
|
|
144
144
|
"tool_call_id": tool_call_id,
|
145
145
|
"limit": 2,
|
146
146
|
}
|
147
|
-
result =
|
147
|
+
result = zotero_read.run(tool_input)
|
148
148
|
|
149
149
|
update = result.update
|
150
150
|
filtered_papers = update["zotero_read"]
|
@@ -166,7 +166,7 @@ class TestZoteroSearchTool(unittest.TestCase):
|
|
166
166
|
mock_zotero_class,
|
167
167
|
mock_get_item_collections,
|
168
168
|
):
|
169
|
-
"""
|
169
|
+
"""Test no items returned from Zotero."""
|
170
170
|
mock_hydra_compose.return_value = dummy_cfg
|
171
171
|
mock_hydra_init.return_value.__enter__.return_value = None
|
172
172
|
|
@@ -183,7 +183,7 @@ class TestZoteroSearchTool(unittest.TestCase):
|
|
183
183
|
"limit": 2,
|
184
184
|
}
|
185
185
|
with self.assertRaises(RuntimeError) as context:
|
186
|
-
|
186
|
+
zotero_read.run(tool_input)
|
187
187
|
self.assertIn("No items returned from Zotero", str(context.exception))
|
188
188
|
|
189
189
|
@patch(
|
@@ -199,7 +199,10 @@ class TestZoteroSearchTool(unittest.TestCase):
|
|
199
199
|
mock_zotero_class,
|
200
200
|
mock_get_item_collections,
|
201
201
|
):
|
202
|
-
"""
|
202
|
+
"""
|
203
|
+
Test that when non-research items (e.g. attachments, notes) are returned,
|
204
|
+
they are still included since filtering is disabled.
|
205
|
+
"""
|
203
206
|
mock_hydra_compose.return_value = dummy_cfg
|
204
207
|
mock_hydra_init.return_value.__enter__.return_value = None
|
205
208
|
|
@@ -240,9 +243,13 @@ class TestZoteroSearchTool(unittest.TestCase):
|
|
240
243
|
"tool_call_id": tool_call_id,
|
241
244
|
"limit": 2,
|
242
245
|
}
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
+
# Instead of expecting a RuntimeError, we now expect both items to be returned.
|
247
|
+
result = zotero_read.run(tool_input)
|
248
|
+
update = result.update
|
249
|
+
filtered_papers = update["zotero_read"]
|
250
|
+
self.assertIn("paper1", filtered_papers)
|
251
|
+
self.assertIn("paper2", filtered_papers)
|
252
|
+
self.assertEqual(len(filtered_papers), 2)
|
246
253
|
|
247
254
|
@patch(
|
248
255
|
"aiagents4pharma.talk2scholars.tools.zotero.zotero_read.get_item_collections"
|
@@ -257,7 +264,7 @@ class TestZoteroSearchTool(unittest.TestCase):
|
|
257
264
|
mock_zotero_class,
|
258
265
|
mock_get_item_collections,
|
259
266
|
):
|
260
|
-
"""
|
267
|
+
"""Test items API exception is properly raised."""
|
261
268
|
mock_hydra_compose.return_value = dummy_cfg
|
262
269
|
mock_hydra_init.return_value.__enter__.return_value = None
|
263
270
|
mock_get_item_collections.return_value = {}
|
@@ -274,7 +281,7 @@ class TestZoteroSearchTool(unittest.TestCase):
|
|
274
281
|
"limit": 2,
|
275
282
|
}
|
276
283
|
with self.assertRaises(RuntimeError) as context:
|
277
|
-
|
284
|
+
zotero_read.run(tool_input)
|
278
285
|
self.assertIn("Failed to fetch items from Zotero", str(context.exception))
|
279
286
|
|
280
287
|
@patch(
|
@@ -306,7 +313,7 @@ class TestZoteroSearchTool(unittest.TestCase):
|
|
306
313
|
"url": "http://example.com",
|
307
314
|
"itemType": "journalArticle",
|
308
315
|
}
|
309
|
-
}, #
|
316
|
+
}, # Missing 'key' field
|
310
317
|
{
|
311
318
|
"data": {
|
312
319
|
"key": "paper_valid",
|
@@ -329,7 +336,7 @@ class TestZoteroSearchTool(unittest.TestCase):
|
|
329
336
|
"tool_call_id": tool_call_id,
|
330
337
|
"limit": 2,
|
331
338
|
}
|
332
|
-
result =
|
339
|
+
result = zotero_read.run(tool_input)
|
333
340
|
|
334
341
|
update = result.update
|
335
342
|
filtered_papers = update["zotero_read"]
|
@@ -371,7 +378,7 @@ class TestZoteroSearchTool(unittest.TestCase):
|
|
371
378
|
"limit": 2,
|
372
379
|
}
|
373
380
|
with self.assertRaises(RuntimeError) as context:
|
374
|
-
|
381
|
+
zotero_read.run(tool_input)
|
375
382
|
self.assertIn("No matching papers returned from Zotero", str(context.exception))
|
376
383
|
|
377
384
|
@patch(
|
@@ -408,5 +415,5 @@ class TestZoteroSearchTool(unittest.TestCase):
|
|
408
415
|
"limit": 2,
|
409
416
|
}
|
410
417
|
with self.assertRaises(RuntimeError) as context:
|
411
|
-
|
418
|
+
zotero_read.run(tool_input)
|
412
419
|
self.assertIn("No matching papers returned from Zotero", str(context.exception))
|