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.
Files changed (44) hide show
  1. aiagents4pharma/talk2scholars/agents/main_agent.py +18 -10
  2. aiagents4pharma/talk2scholars/agents/paper_download_agent.py +5 -6
  3. aiagents4pharma/talk2scholars/agents/pdf_agent.py +4 -10
  4. aiagents4pharma/talk2scholars/agents/zotero_agent.py +9 -7
  5. aiagents4pharma/talk2scholars/configs/agents/talk2scholars/main_agent/default.yaml +18 -9
  6. aiagents4pharma/talk2scholars/configs/agents/talk2scholars/s2_agent/default.yaml +2 -2
  7. aiagents4pharma/talk2scholars/configs/agents/talk2scholars/zotero_agent/default.yaml +9 -15
  8. aiagents4pharma/talk2scholars/configs/app/frontend/default.yaml +1 -0
  9. aiagents4pharma/talk2scholars/configs/tools/multi_paper_recommendation/default.yaml +6 -1
  10. aiagents4pharma/talk2scholars/configs/tools/search/default.yaml +7 -1
  11. aiagents4pharma/talk2scholars/configs/tools/single_paper_recommendation/default.yaml +6 -1
  12. aiagents4pharma/talk2scholars/configs/tools/zotero_read/default.yaml +1 -1
  13. aiagents4pharma/talk2scholars/configs/tools/zotero_write/default.yaml +55 -0
  14. aiagents4pharma/talk2scholars/state/state_talk2scholars.py +7 -1
  15. aiagents4pharma/talk2scholars/tests/test_llm_main_integration.py +84 -53
  16. aiagents4pharma/talk2scholars/tests/test_main_agent.py +24 -0
  17. aiagents4pharma/talk2scholars/tests/test_question_and_answer_tool.py +79 -15
  18. aiagents4pharma/talk2scholars/tests/test_routing_logic.py +13 -10
  19. aiagents4pharma/talk2scholars/tests/test_s2_multi.py +27 -4
  20. aiagents4pharma/talk2scholars/tests/test_s2_search.py +19 -3
  21. aiagents4pharma/talk2scholars/tests/test_s2_single.py +27 -3
  22. aiagents4pharma/talk2scholars/tests/test_zotero_agent.py +3 -2
  23. aiagents4pharma/talk2scholars/tests/test_zotero_human_in_the_loop.py +273 -0
  24. aiagents4pharma/talk2scholars/tests/test_zotero_path.py +419 -1
  25. aiagents4pharma/talk2scholars/tests/test_zotero_read.py +25 -18
  26. aiagents4pharma/talk2scholars/tests/test_zotero_write.py +123 -588
  27. aiagents4pharma/talk2scholars/tools/paper_download/abstract_downloader.py +2 -0
  28. aiagents4pharma/talk2scholars/tools/paper_download/arxiv_downloader.py +11 -4
  29. aiagents4pharma/talk2scholars/tools/paper_download/download_arxiv_input.py +5 -1
  30. aiagents4pharma/talk2scholars/tools/pdf/question_and_answer.py +73 -26
  31. aiagents4pharma/talk2scholars/tools/s2/multi_paper_rec.py +46 -22
  32. aiagents4pharma/talk2scholars/tools/s2/query_results.py +1 -1
  33. aiagents4pharma/talk2scholars/tools/s2/search.py +40 -12
  34. aiagents4pharma/talk2scholars/tools/s2/single_paper_rec.py +42 -16
  35. aiagents4pharma/talk2scholars/tools/zotero/__init__.py +1 -0
  36. aiagents4pharma/talk2scholars/tools/zotero/utils/zotero_path.py +125 -0
  37. aiagents4pharma/talk2scholars/tools/zotero/zotero_read.py +35 -20
  38. aiagents4pharma/talk2scholars/tools/zotero/zotero_review.py +198 -0
  39. aiagents4pharma/talk2scholars/tools/zotero/zotero_write.py +86 -118
  40. {aiagents4pharma-1.30.0.dist-info → aiagents4pharma-1.30.2.dist-info}/METADATA +4 -3
  41. {aiagents4pharma-1.30.0.dist-info → aiagents4pharma-1.30.2.dist-info}/RECORD +44 -41
  42. {aiagents4pharma-1.30.0.dist-info → aiagents4pharma-1.30.2.dist-info}/WHEEL +1 -1
  43. {aiagents4pharma-1.30.0.dist-info → aiagents4pharma-1.30.2.dist-info/licenses}/LICENSE +0 -0
  44. {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
- zotero_search_tool,
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
- """test for Zotero search tool"""
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
- """test valid query"""
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 = zotero_search_tool.run(tool_input)
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
- """test empty query fetches all items"""
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 = zotero_search_tool.run(tool_input)
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
- """test no items returned from Zotero"""
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
- zotero_search_tool.run(tool_input)
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
- """test no matching papers returned from Zotero"""
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
- with self.assertRaises(RuntimeError) as context:
244
- zotero_search_tool.run(tool_input)
245
- self.assertIn("No matching papers returned from Zotero", str(context.exception))
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
- """test items API exception"""
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
- zotero_search_tool.run(tool_input)
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
- }, # missing key triggers line 136
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 = zotero_search_tool.run(tool_input)
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
- zotero_search_tool.run(tool_input)
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
- zotero_search_tool.run(tool_input)
418
+ zotero_read.run(tool_input)
412
419
  self.assertIn("No matching papers returned from Zotero", str(context.exception))