aiagents4pharma 1.28.0__py3-none-any.whl → 1.29.0__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 (41) hide show
  1. aiagents4pharma/talk2scholars/agents/main_agent.py +35 -209
  2. aiagents4pharma/talk2scholars/agents/s2_agent.py +10 -6
  3. aiagents4pharma/talk2scholars/agents/zotero_agent.py +12 -6
  4. aiagents4pharma/talk2scholars/configs/agents/talk2scholars/main_agent/default.yaml +2 -48
  5. aiagents4pharma/talk2scholars/configs/agents/talk2scholars/s2_agent/default.yaml +5 -28
  6. aiagents4pharma/talk2scholars/configs/agents/talk2scholars/zotero_agent/default.yaml +5 -21
  7. aiagents4pharma/talk2scholars/configs/config.yaml +1 -0
  8. aiagents4pharma/talk2scholars/configs/tools/__init__.py +1 -0
  9. aiagents4pharma/talk2scholars/configs/tools/multi_paper_recommendation/default.yaml +1 -1
  10. aiagents4pharma/talk2scholars/configs/tools/search/default.yaml +1 -1
  11. aiagents4pharma/talk2scholars/configs/tools/single_paper_recommendation/default.yaml +1 -1
  12. aiagents4pharma/talk2scholars/configs/tools/zotero_read/default.yaml +42 -1
  13. aiagents4pharma/talk2scholars/configs/tools/zotero_write/__inti__.py +3 -0
  14. aiagents4pharma/talk2scholars/tests/test_main_agent.py +186 -111
  15. aiagents4pharma/talk2scholars/tests/test_s2_display.py +74 -0
  16. aiagents4pharma/talk2scholars/tests/test_s2_multi.py +282 -0
  17. aiagents4pharma/talk2scholars/tests/test_s2_query.py +78 -0
  18. aiagents4pharma/talk2scholars/tests/test_s2_retrieve.py +65 -0
  19. aiagents4pharma/talk2scholars/tests/test_s2_search.py +266 -0
  20. aiagents4pharma/talk2scholars/tests/test_s2_single.py +274 -0
  21. aiagents4pharma/talk2scholars/tests/test_zotero_path.py +57 -0
  22. aiagents4pharma/talk2scholars/tests/test_zotero_read.py +412 -0
  23. aiagents4pharma/talk2scholars/tests/test_zotero_write.py +626 -0
  24. aiagents4pharma/talk2scholars/tools/s2/multi_paper_rec.py +50 -34
  25. aiagents4pharma/talk2scholars/tools/s2/retrieve_semantic_scholar_paper_id.py +8 -8
  26. aiagents4pharma/talk2scholars/tools/s2/search.py +36 -23
  27. aiagents4pharma/talk2scholars/tools/s2/single_paper_rec.py +44 -38
  28. aiagents4pharma/talk2scholars/tools/zotero/__init__.py +2 -0
  29. aiagents4pharma/talk2scholars/tools/zotero/utils/__init__.py +5 -0
  30. aiagents4pharma/talk2scholars/tools/zotero/utils/zotero_path.py +63 -0
  31. aiagents4pharma/talk2scholars/tools/zotero/zotero_read.py +64 -19
  32. aiagents4pharma/talk2scholars/tools/zotero/zotero_write.py +247 -0
  33. {aiagents4pharma-1.28.0.dist-info → aiagents4pharma-1.29.0.dist-info}/METADATA +6 -5
  34. {aiagents4pharma-1.28.0.dist-info → aiagents4pharma-1.29.0.dist-info}/RECORD +37 -28
  35. aiagents4pharma/talk2scholars/tests/test_call_s2.py +0 -100
  36. aiagents4pharma/talk2scholars/tests/test_call_zotero.py +0 -94
  37. aiagents4pharma/talk2scholars/tests/test_s2_tools.py +0 -355
  38. aiagents4pharma/talk2scholars/tests/test_zotero_tool.py +0 -171
  39. {aiagents4pharma-1.28.0.dist-info → aiagents4pharma-1.29.0.dist-info}/LICENSE +0 -0
  40. {aiagents4pharma-1.28.0.dist-info → aiagents4pharma-1.29.0.dist-info}/WHEEL +0 -0
  41. {aiagents4pharma-1.28.0.dist-info → aiagents4pharma-1.29.0.dist-info}/top_level.txt +0 -0
@@ -1,355 +0,0 @@
1
- """
2
- Unit tests for S2 tools functionality.
3
- """
4
-
5
- # pylint: disable=redefined-outer-name
6
- from unittest.mock import patch
7
- from unittest.mock import MagicMock
8
- import pytest
9
- from langgraph.types import Command
10
- from ..tools.s2.display_results import (
11
- display_results,
12
- NoPapersFoundError as raised_error,
13
- )
14
- from ..tools.s2.multi_paper_rec import get_multi_paper_recommendations
15
- from ..tools.s2.search import search_tool
16
- from ..tools.s2.single_paper_rec import get_single_paper_recommendations
17
- from ..tools.s2.query_results import query_results, NoPapersFoundError
18
- from ..tools.s2.retrieve_semantic_scholar_paper_id import (
19
- retrieve_semantic_scholar_paper_id,
20
- )
21
-
22
-
23
- @pytest.fixture
24
- def initial_state():
25
- """Provides an empty initial state for tests."""
26
- return {"papers": {}, "multi_papers": {}}
27
-
28
-
29
- # Fixed test data for deterministic results
30
- MOCK_SEARCH_RESPONSE = {
31
- "data": [
32
- {
33
- "paperId": "123",
34
- "title": "Machine Learning Basics",
35
- "abstract": "An introduction to ML",
36
- "year": 2023,
37
- "citationCount": 100,
38
- "url": "https://example.com/paper1",
39
- "authors": [{"name": "Test Author"}],
40
- }
41
- ]
42
- }
43
-
44
- MOCK_STATE_PAPER = {
45
- "123": {
46
- "Title": "Machine Learning Basics",
47
- "Abstract": "An introduction to ML",
48
- "Year": 2023,
49
- "Citation Count": 100,
50
- "URL": "https://example.com/paper1",
51
- }
52
- }
53
-
54
-
55
- class TestS2Tools:
56
- """Unit tests for individual S2 tools"""
57
-
58
- def test_display_results_empty_state(self, initial_state):
59
- """Verifies display_results tool behavior when state is empty and raises an exception"""
60
- with pytest.raises(
61
- raised_error,
62
- match="No papers found. A search/rec needs to be performed first.",
63
- ):
64
- display_results.invoke({"state": initial_state, "tool_call_id": "test123"})
65
-
66
- def test_display_results_shows_papers(self, initial_state):
67
- """Verifies display_results tool correctly returns papers from state"""
68
- state = initial_state.copy()
69
- state["last_displayed_papers"] = "papers"
70
- state["papers"] = MOCK_STATE_PAPER
71
-
72
- result = display_results.invoke(
73
- input={"state": state, "tool_call_id": "test123"}
74
- )
75
-
76
- assert isinstance(result, Command) # Expect a Command object
77
- assert isinstance(result.update, dict) # Ensure update is a dictionary
78
- assert "messages" in result.update
79
- assert len(result.update["messages"]) == 1
80
- assert (
81
- "1 papers found. Papers are attached as an artifact."
82
- in result.update["messages"][0].content
83
- )
84
-
85
- @patch("requests.get")
86
- def test_search_finds_papers(self, mock_get):
87
- """Verifies search tool finds and formats papers correctly"""
88
- mock_get.return_value.json.return_value = MOCK_SEARCH_RESPONSE
89
- mock_get.return_value.status_code = 200
90
-
91
- result = search_tool.invoke(
92
- input={
93
- "query": "machine learning",
94
- "limit": 1,
95
- "tool_call_id": "test123",
96
- "id": "test123",
97
- }
98
- )
99
-
100
- assert "papers" in result.update
101
- assert "messages" in result.update
102
- papers = result.update["papers"]
103
- assert isinstance(papers, dict)
104
- assert len(papers) > 0
105
- paper = next(iter(papers.values()))
106
- assert paper["Title"] == "Machine Learning Basics"
107
- assert paper["Year"] == 2023
108
-
109
- @patch("requests.get")
110
- def test_search_finds_papers_with_year(self, mock_get):
111
- """Verifies search tool works with year parameter"""
112
- mock_get.return_value.json.return_value = MOCK_SEARCH_RESPONSE
113
- mock_get.return_value.status_code = 200
114
-
115
- result = search_tool.invoke(
116
- input={
117
- "query": "machine learning",
118
- "limit": 1,
119
- "year": "2023-",
120
- "tool_call_id": "test123",
121
- "id": "test123",
122
- }
123
- )
124
-
125
- assert "papers" in result.update
126
- assert "messages" in result.update
127
- papers = result.update["papers"]
128
- assert isinstance(papers, dict)
129
- assert len(papers) > 0
130
-
131
- @patch("requests.get")
132
- def test_search_filters_invalid_papers(self, mock_get):
133
- """Verifies search tool properly filters papers without title or authors"""
134
- mock_response = {
135
- "data": [
136
- {
137
- "paperId": "123",
138
- "abstract": "An introduction to ML",
139
- "year": 2023,
140
- "citationCount": 100,
141
- "url": "https://example.com/paper1",
142
- # Missing title and authors
143
- },
144
- MOCK_SEARCH_RESPONSE["data"][0], # This one is valid
145
- ]
146
- }
147
- mock_get.return_value.json.return_value = mock_response
148
- mock_get.return_value.status_code = 200
149
-
150
- result = search_tool.invoke(
151
- input={
152
- "query": "machine learning",
153
- "limit": 2,
154
- "tool_call_id": "test123",
155
- "id": "test123",
156
- }
157
- )
158
-
159
- assert "papers" in result.update
160
- papers = result.update["papers"]
161
- assert len(papers) == 1 # Only the valid paper should be included
162
-
163
- @patch("requests.get")
164
- def test_single_paper_rec_basic(self, mock_get):
165
- """Tests basic single paper recommendation functionality"""
166
- mock_get.return_value.json.return_value = {
167
- "recommendedPapers": [MOCK_SEARCH_RESPONSE["data"][0]]
168
- }
169
- mock_get.return_value.status_code = 200
170
-
171
- result = get_single_paper_recommendations.invoke(
172
- input={"paper_id": "123", "limit": 1, "tool_call_id": "test123"}
173
- )
174
-
175
- assert isinstance(result, Command)
176
- assert "papers" in result.update
177
- assert len(result.update["messages"]) == 1
178
-
179
- @patch("requests.get")
180
- def test_single_paper_rec_with_optional_params(self, mock_get):
181
- """Tests single paper recommendations with year parameter"""
182
- mock_get.return_value.json.return_value = {
183
- "recommendedPapers": [MOCK_SEARCH_RESPONSE["data"][0]]
184
- }
185
- mock_get.return_value.status_code = 200
186
-
187
- result = get_single_paper_recommendations.invoke(
188
- input={
189
- "paper_id": "123",
190
- "limit": 1,
191
- "year": "2023-",
192
- "tool_call_id": "test123",
193
- "id": "test123",
194
- }
195
- )
196
- assert "papers" in result.update
197
-
198
- @patch("requests.post")
199
- def test_multi_paper_rec_basic(self, mock_post):
200
- """Tests basic multi-paper recommendation functionality"""
201
- mock_post.return_value.json.return_value = {
202
- "recommendedPapers": [MOCK_SEARCH_RESPONSE["data"][0]]
203
- }
204
- mock_post.return_value.status_code = 200
205
-
206
- result = get_multi_paper_recommendations.invoke(
207
- input={"paper_ids": ["123", "456"], "limit": 1, "tool_call_id": "test123"}
208
- )
209
-
210
- assert isinstance(result, Command)
211
- assert "multi_papers" in result.update
212
- assert len(result.update["messages"]) == 1
213
-
214
- @patch("requests.post")
215
- def test_multi_paper_rec_with_optional_params(self, mock_post):
216
- """Tests multi-paper recommendations with all optional parameters"""
217
- mock_post.return_value.json.return_value = {
218
- "recommendedPapers": [MOCK_SEARCH_RESPONSE["data"][0]]
219
- }
220
- mock_post.return_value.status_code = 200
221
-
222
- result = get_multi_paper_recommendations.invoke(
223
- input={
224
- "paper_ids": ["123", "456"],
225
- "limit": 1,
226
- "year": "2023-",
227
- "tool_call_id": "test123",
228
- "id": "test123",
229
- }
230
- )
231
- assert "multi_papers" in result.update
232
- assert len(result.update["messages"]) == 1
233
-
234
- @patch("requests.get")
235
- def test_search_tool_finds_papers(self, mock_get):
236
- """Verifies search tool finds and formats papers correctly"""
237
- mock_get.return_value.json.return_value = MOCK_SEARCH_RESPONSE
238
- mock_get.return_value.status_code = 200
239
-
240
- result = search_tool.invoke(
241
- input={"query": "machine learning", "limit": 1, "tool_call_id": "test123"}
242
- )
243
-
244
- assert isinstance(result, Command) # Expect a Command object
245
- assert "papers" in result.update
246
- assert len(result.update["papers"]) > 0
247
-
248
- def test_query_results_empty_state(self, initial_state):
249
- """Tests query_results tool behavior when no papers are found."""
250
- with pytest.raises(
251
- NoPapersFoundError,
252
- match="No papers found. A search needs to be performed first.",
253
- ):
254
- query_results.invoke(
255
- {"question": "List all papers", "state": initial_state}
256
- )
257
-
258
- @patch(
259
- "aiagents4pharma.talk2scholars.tools.s2.query_results.create_pandas_dataframe_agent"
260
- )
261
- def test_query_results_with_papers(self, mock_create_agent, initial_state):
262
- """Tests querying papers when data is available."""
263
- state = initial_state.copy()
264
- state["last_displayed_papers"] = "papers"
265
- state["papers"] = MOCK_STATE_PAPER
266
-
267
- # Mock the dataframe agent instead of the LLM
268
- mock_agent = MagicMock()
269
- mock_agent.invoke.return_value = {"output": "Mocked response"}
270
-
271
- mock_create_agent.return_value = (
272
- mock_agent # Mock the function returning the agent
273
- )
274
-
275
- # Ensure that the output of query_results is correctly structured
276
- result = query_results.invoke({"question": "List all papers", "state": state})
277
-
278
- assert isinstance(result, str) # Ensure output is a string
279
- assert result == "Mocked response" # Validate the expected response
280
-
281
- @patch("requests.get")
282
- def test_retrieve_semantic_scholar_paper_id(self, mock_get):
283
- """Tests retrieving a paper ID from Semantic Scholar."""
284
- mock_get.return_value.json.return_value = MOCK_SEARCH_RESPONSE
285
- mock_get.return_value.status_code = 200
286
-
287
- result = retrieve_semantic_scholar_paper_id.invoke(
288
- input={"paper_title": "Machine Learning Basics", "tool_call_id": "test123"}
289
- )
290
-
291
- assert isinstance(result, Command)
292
- assert "messages" in result.update
293
- assert (
294
- "Paper ID for 'Machine Learning Basics' is: 123"
295
- in result.update["messages"][0].content
296
- )
297
-
298
- def test_retrieve_semantic_scholar_paper_id_no_results(self):
299
- """Test retrieving a paper ID when no results are found."""
300
- with pytest.raises(ValueError, match="No papers found for query: UnknownPaper"):
301
- retrieve_semantic_scholar_paper_id.invoke(
302
- input={"paper_title": "UnknownPaper", "tool_call_id": "test123"}
303
- )
304
-
305
- def test_single_paper_rec_invalid_id(self):
306
- """Test single paper recommendation with an invalid ID."""
307
- with pytest.raises(ValueError, match="Invalid paper ID or API error."):
308
- get_single_paper_recommendations.invoke(
309
- input={"paper_id": "", "tool_call_id": "test123"} # Empty ID case
310
- )
311
-
312
- @patch("requests.post")
313
- def test_multi_paper_rec_no_recommendations(self, mock_post):
314
- """Tests behavior when multi-paper recommendation API returns no results."""
315
- mock_post.return_value.json.return_value = {"recommendedPapers": []}
316
- mock_post.return_value.status_code = 200
317
-
318
- result = get_multi_paper_recommendations.invoke(
319
- input={"paper_ids": ["123", "456"], "limit": 1, "tool_call_id": "test123"}
320
- )
321
-
322
- assert isinstance(result, Command)
323
- assert "messages" in result.update
324
- assert (
325
- "No recommendations found based on multiple papers."
326
- in result.update["messages"][0].content
327
- )
328
-
329
- @patch("requests.get")
330
- def test_search_no_results(self, mock_get):
331
- """Tests behavior when search API returns no results."""
332
- mock_get.return_value.json.return_value = {"data": []}
333
- mock_get.return_value.status_code = 200
334
-
335
- result = search_tool.invoke(
336
- input={"query": "nonexistent topic", "limit": 1, "tool_call_id": "test123"}
337
- )
338
-
339
- assert isinstance(result, Command)
340
- assert "messages" in result.update
341
- assert "No papers found." in result.update["messages"][0].content
342
-
343
- @patch("requests.get")
344
- def test_single_paper_rec_no_recommendations(self, mock_get):
345
- """Tests behavior when single paper recommendation API returns no results."""
346
- mock_get.return_value.json.return_value = {"recommendedPapers": []}
347
- mock_get.return_value.status_code = 200
348
-
349
- result = get_single_paper_recommendations.invoke(
350
- input={"paper_id": "123", "limit": 1, "tool_call_id": "test123"}
351
- )
352
-
353
- assert isinstance(result, Command)
354
- assert "messages" in result.update
355
- assert "No recommendations found for" in result.update["messages"][0].content
@@ -1,171 +0,0 @@
1
- """
2
- Unit tests for Zotero search tool in zotero_read.py.
3
- """
4
-
5
- from unittest.mock import patch
6
- from langgraph.types import Command
7
- from ..tools.zotero.zotero_read import zotero_search_tool
8
-
9
-
10
- # Mock data for Zotero API response
11
- MOCK_ZOTERO_RESPONSE = [
12
- {
13
- "data": {
14
- "key": "ABC123",
15
- "title": "Deep Learning in Medicine",
16
- "abstractNote": "An overview of deep learning applications in medicine.",
17
- "date": "2022",
18
- "url": "https://example.com/paper1",
19
- "itemType": "journalArticle",
20
- }
21
- },
22
- {
23
- "data": {
24
- "key": "XYZ789",
25
- "title": "Advances in AI",
26
- "abstractNote": "Recent advancements in AI research.",
27
- "date": "2023",
28
- "url": "https://example.com/paper2",
29
- "itemType": "conferencePaper",
30
- }
31
- },
32
- ]
33
-
34
-
35
- class TestZoteroRead:
36
- """Unit tests for Zotero search tool"""
37
-
38
- @patch("pyzotero.zotero.Zotero.items")
39
- def test_zotero_success(self, mock_zotero):
40
- """Verifies successful retrieval of papers from Zotero"""
41
- mock_zotero.return_value = MOCK_ZOTERO_RESPONSE
42
-
43
- result = zotero_search_tool.invoke(
44
- input={
45
- "query": "deep learning",
46
- "only_articles": True,
47
- "limit": 2,
48
- "tool_call_id": "test123",
49
- }
50
- )
51
-
52
- assert isinstance(result, Command)
53
- assert "zotero_read" in result.update
54
- assert "messages" in result.update
55
- papers = result.update["zotero_read"]
56
- assert len(papers) == 2 # Should return 2 papers
57
- assert papers["ABC123"]["Title"] == "Deep Learning in Medicine"
58
- assert papers["XYZ789"]["Title"] == "Advances in AI"
59
-
60
- @patch("pyzotero.zotero.Zotero.items")
61
- def test_zotero_no_papers_found(self, mock_zotero):
62
- """Verifies Zotero tool behavior when no papers are found"""
63
- mock_zotero.return_value = [] # Simulating empty response
64
-
65
- result = zotero_search_tool.invoke(
66
- input={
67
- "query": "nonexistent topic",
68
- "only_articles": True,
69
- "limit": 2,
70
- "tool_call_id": "test123",
71
- }
72
- )
73
-
74
- assert isinstance(result, Command)
75
- assert "zotero_read" in result.update
76
- assert len(result.update["zotero_read"]) == 0 # No papers found
77
- assert "messages" in result.update
78
- assert "Number of papers found: 0" in result.update["messages"][0].content
79
-
80
- @patch("pyzotero.zotero.Zotero.items")
81
- def test_zotero_only_articles_filtering(self, mock_zotero):
82
- """Ensures only journal articles and conference papers are returned"""
83
- mock_response = [
84
- {
85
- "data": {
86
- "key": "DEF456",
87
- "title": "A Book on AI",
88
- "abstractNote": "Book about AI advancements.",
89
- "date": "2021",
90
- "url": "https://example.com/book",
91
- "itemType": "book",
92
- }
93
- },
94
- MOCK_ZOTERO_RESPONSE[0], # Valid journal article
95
- ]
96
- mock_zotero.return_value = mock_response
97
-
98
- result = zotero_search_tool.invoke(
99
- input={
100
- "query": "AI",
101
- "only_articles": True,
102
- "limit": 2,
103
- "tool_call_id": "test123",
104
- }
105
- )
106
-
107
- assert isinstance(result, Command)
108
- assert "zotero_read" in result.update
109
- papers = result.update["zotero_read"]
110
- assert len(papers) == 1 # The book should be filtered out
111
- assert "ABC123" in papers # Journal article should be included
112
-
113
- @patch("pyzotero.zotero.Zotero.items")
114
- def test_zotero_invalid_response(self, mock_zotero):
115
- """Tests handling of malformed API response"""
116
- mock_zotero.return_value = [
117
- {"data": None}, # Invalid response format
118
- {}, # Empty object
119
- {"data": {"title": "Missing Key", "itemType": "journalArticle"}}, # No key
120
- ]
121
-
122
- result = zotero_search_tool.invoke(
123
- input={
124
- "query": "AI ethics",
125
- "only_articles": True,
126
- "limit": 2,
127
- "tool_call_id": "test123",
128
- }
129
- )
130
-
131
- assert isinstance(result, Command)
132
- assert "zotero_read" in result.update
133
- assert (
134
- len(result.update["zotero_read"]) == 0
135
- ) # Should filter out invalid entries
136
- assert "messages" in result.update
137
- assert "Number of papers found: 0" in result.update["messages"][0].content
138
-
139
- @patch("pyzotero.zotero.Zotero.items")
140
- def test_zotero_handles_non_dict_items(self, mock_zotero):
141
- """Ensures that Zotero tool correctly skips non-dictionary items (covers line 86)"""
142
-
143
- # Simulate Zotero returning an invalid item (e.g., `None` and a string)
144
- mock_zotero.return_value = [
145
- None,
146
- "invalid_string",
147
- {
148
- "data": {
149
- "key": "123",
150
- "title": "Valid Paper",
151
- "itemType": "journalArticle",
152
- }
153
- },
154
- ]
155
-
156
- result = zotero_search_tool.invoke(
157
- input={
158
- "query": "AI ethics",
159
- "only_articles": True,
160
- "limit": 2,
161
- "tool_call_id": "test123",
162
- }
163
- )
164
-
165
- assert isinstance(result, Command)
166
- assert "zotero_read" in result.update
167
-
168
- # Expect only valid items to be processed
169
- assert (
170
- len(result.update["zotero_read"]) == 1
171
- ), "Only valid dictionary items should be processed"