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.
Files changed (45) hide show
  1. aiagents4pharma/talk2scholars/__init__.py +2 -0
  2. aiagents4pharma/talk2scholars/agents/__init__.py +8 -0
  3. aiagents4pharma/talk2scholars/agents/zotero_agent.py +9 -7
  4. aiagents4pharma/talk2scholars/configs/__init__.py +2 -0
  5. aiagents4pharma/talk2scholars/configs/agents/__init__.py +2 -0
  6. aiagents4pharma/talk2scholars/configs/agents/talk2scholars/__init__.py +2 -0
  7. aiagents4pharma/talk2scholars/configs/agents/talk2scholars/zotero_agent/default.yaml +9 -15
  8. aiagents4pharma/talk2scholars/configs/app/__init__.py +2 -0
  9. aiagents4pharma/talk2scholars/configs/tools/__init__.py +9 -0
  10. aiagents4pharma/talk2scholars/configs/tools/zotero_write/default.yaml +55 -0
  11. aiagents4pharma/talk2scholars/state/__init__.py +4 -2
  12. aiagents4pharma/talk2scholars/state/state_talk2scholars.py +3 -0
  13. aiagents4pharma/talk2scholars/tests/test_routing_logic.py +1 -2
  14. aiagents4pharma/talk2scholars/tests/test_s2_multi.py +10 -8
  15. aiagents4pharma/talk2scholars/tests/test_s2_search.py +9 -5
  16. aiagents4pharma/talk2scholars/tests/test_s2_single.py +7 -7
  17. aiagents4pharma/talk2scholars/tests/test_zotero_agent.py +3 -2
  18. aiagents4pharma/talk2scholars/tests/test_zotero_human_in_the_loop.py +273 -0
  19. aiagents4pharma/talk2scholars/tests/test_zotero_path.py +433 -1
  20. aiagents4pharma/talk2scholars/tests/test_zotero_read.py +57 -43
  21. aiagents4pharma/talk2scholars/tests/test_zotero_write.py +123 -588
  22. aiagents4pharma/talk2scholars/tools/__init__.py +3 -0
  23. aiagents4pharma/talk2scholars/tools/pdf/__init__.py +4 -2
  24. aiagents4pharma/talk2scholars/tools/s2/__init__.py +9 -0
  25. aiagents4pharma/talk2scholars/tools/s2/multi_paper_rec.py +9 -135
  26. aiagents4pharma/talk2scholars/tools/s2/search.py +8 -114
  27. aiagents4pharma/talk2scholars/tools/s2/single_paper_rec.py +8 -126
  28. aiagents4pharma/talk2scholars/tools/s2/utils/__init__.py +7 -0
  29. aiagents4pharma/talk2scholars/tools/s2/utils/multi_helper.py +194 -0
  30. aiagents4pharma/talk2scholars/tools/s2/utils/search_helper.py +175 -0
  31. aiagents4pharma/talk2scholars/tools/s2/utils/single_helper.py +186 -0
  32. aiagents4pharma/talk2scholars/tools/zotero/__init__.py +3 -0
  33. aiagents4pharma/talk2scholars/tools/zotero/utils/__init__.py +5 -0
  34. aiagents4pharma/talk2scholars/tools/zotero/utils/read_helper.py +167 -0
  35. aiagents4pharma/talk2scholars/tools/zotero/utils/review_helper.py +78 -0
  36. aiagents4pharma/talk2scholars/tools/zotero/utils/write_helper.py +197 -0
  37. aiagents4pharma/talk2scholars/tools/zotero/utils/zotero_path.py +126 -1
  38. aiagents4pharma/talk2scholars/tools/zotero/zotero_read.py +10 -139
  39. aiagents4pharma/talk2scholars/tools/zotero/zotero_review.py +164 -0
  40. aiagents4pharma/talk2scholars/tools/zotero/zotero_write.py +40 -229
  41. {aiagents4pharma-1.30.1.dist-info → aiagents4pharma-1.30.3.dist-info}/METADATA +3 -2
  42. {aiagents4pharma-1.30.1.dist-info → aiagents4pharma-1.30.3.dist-info}/RECORD +45 -35
  43. {aiagents4pharma-1.30.1.dist-info → aiagents4pharma-1.30.3.dist-info}/WHEEL +1 -1
  44. {aiagents4pharma-1.30.1.dist-info → aiagents4pharma-1.30.3.dist-info/licenses}/LICENSE +0 -0
  45. {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.zotero_read.get_item_collections"
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 = 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)
@@ -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.zotero_read.get_item_collections"
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 = zotero_search_tool.run(tool_input)
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.zotero_read.get_item_collections"
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
- zotero_search_tool.run(tool_input)
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.zotero_read.get_item_collections"
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 = zotero_search_tool.run(tool_input)
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.zotero_read.get_item_collections"
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
- zotero_search_tool.run(tool_input)
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.zotero_read.get_item_collections"
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 = zotero_search_tool.run(tool_input)
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.zotero_read.get_item_collections"
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
- zotero_search_tool.run(tool_input)
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.zotero_read.get_item_collections"
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
- zotero_search_tool.run(tool_input)
432
+ zotero_read.run(tool_input)
419
433
  self.assertIn("No matching papers returned from Zotero", str(context.exception))