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
@@ -0,0 +1,273 @@
|
|
1
|
+
"""
|
2
|
+
Unit tests for Zotero human in the loop in zotero_review.py with structured output.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import unittest
|
6
|
+
from unittest.mock import patch, MagicMock
|
7
|
+
from aiagents4pharma.talk2scholars.tools.zotero.zotero_review import zotero_review
|
8
|
+
|
9
|
+
|
10
|
+
class TestZoteroReviewTool(unittest.TestCase):
|
11
|
+
"""Test class for Zotero review tool with structured LLM output."""
|
12
|
+
|
13
|
+
def setUp(self):
|
14
|
+
self.tool_call_id = "tc"
|
15
|
+
self.collection_path = "/Col"
|
16
|
+
# Create a sample fetched papers dictionary with one paper.
|
17
|
+
self.sample_papers = {"p1": {"Title": "T1", "Authors": ["A1", "A2", "A3"]}}
|
18
|
+
|
19
|
+
@patch(
|
20
|
+
"aiagents4pharma.talk2scholars.tools.zotero.zotero_review.fetch_papers_for_save",
|
21
|
+
return_value=None,
|
22
|
+
)
|
23
|
+
def test_no_fetched_papers(self, mock_fetch):
|
24
|
+
"""Test when no fetched papers are found."""
|
25
|
+
with self.assertRaises(ValueError) as context:
|
26
|
+
zotero_review.run(
|
27
|
+
{
|
28
|
+
"tool_call_id": self.tool_call_id,
|
29
|
+
"collection_path": self.collection_path,
|
30
|
+
"state": {},
|
31
|
+
}
|
32
|
+
)
|
33
|
+
self.assertIn("No fetched papers were found to save", str(context.exception))
|
34
|
+
mock_fetch.assert_called_once()
|
35
|
+
|
36
|
+
@patch(
|
37
|
+
"aiagents4pharma.talk2scholars.tools.zotero.zotero_review.fetch_papers_for_save",
|
38
|
+
return_value={"p1": {"Title": "T1", "Authors": ["A1", "A2"]}},
|
39
|
+
)
|
40
|
+
@patch(
|
41
|
+
"aiagents4pharma.talk2scholars.tools.zotero.zotero_review.interrupt",
|
42
|
+
return_value="dummy_response",
|
43
|
+
)
|
44
|
+
def test_missing_llm_model(self, mock_interrupt, mock_fetch):
|
45
|
+
"""Test when LLM model is not available in state, expecting fallback confirmation."""
|
46
|
+
state = {"last_displayed_papers": self.sample_papers} # llm_model missing
|
47
|
+
result = zotero_review.run(
|
48
|
+
{
|
49
|
+
"tool_call_id": self.tool_call_id,
|
50
|
+
"collection_path": self.collection_path,
|
51
|
+
"state": state,
|
52
|
+
}
|
53
|
+
)
|
54
|
+
upd = result.update
|
55
|
+
# The fallback message should start with "REVIEW REQUIRED:"
|
56
|
+
self.assertTrue(upd["messages"][0].content.startswith("REVIEW REQUIRED:"))
|
57
|
+
# Check that the approval status is set as fallback values.
|
58
|
+
approval = upd["zotero_write_approval_status"]
|
59
|
+
self.assertEqual(approval["collection_path"], self.collection_path)
|
60
|
+
self.assertTrue(approval["papers_reviewed"])
|
61
|
+
self.assertFalse(approval["approved"])
|
62
|
+
self.assertEqual(approval["papers_count"], len(self.sample_papers))
|
63
|
+
mock_fetch.assert_called_once()
|
64
|
+
mock_interrupt.assert_called_once()
|
65
|
+
|
66
|
+
@patch(
|
67
|
+
"aiagents4pharma.talk2scholars.tools.zotero.zotero_review.fetch_papers_for_save",
|
68
|
+
return_value={"p1": {"Title": "T1", "Authors": ["A1", "A2"]}},
|
69
|
+
)
|
70
|
+
@patch(
|
71
|
+
"aiagents4pharma.talk2scholars.tools.zotero.zotero_review.interrupt",
|
72
|
+
return_value="dummy_response",
|
73
|
+
)
|
74
|
+
def test_human_approve(self, mock_interrupt, mock_fetch):
|
75
|
+
"""Test when human approves saving papers using structured output."""
|
76
|
+
# Prepare a fake llm_model with structured output.
|
77
|
+
fake_structured_llm = MagicMock()
|
78
|
+
# Simulate invoke() returns an object with decision "approve"
|
79
|
+
fake_decision = MagicMock()
|
80
|
+
fake_decision.decision = "approve"
|
81
|
+
fake_structured_llm.invoke.return_value = fake_decision
|
82
|
+
|
83
|
+
fake_llm_model = MagicMock()
|
84
|
+
fake_llm_model.with_structured_output.return_value = fake_structured_llm
|
85
|
+
|
86
|
+
state = {
|
87
|
+
"last_displayed_papers": self.sample_papers,
|
88
|
+
"llm_model": fake_llm_model,
|
89
|
+
}
|
90
|
+
|
91
|
+
result = zotero_review.run(
|
92
|
+
{
|
93
|
+
"tool_call_id": self.tool_call_id,
|
94
|
+
"collection_path": self.collection_path,
|
95
|
+
"state": state,
|
96
|
+
}
|
97
|
+
)
|
98
|
+
|
99
|
+
upd = result.update
|
100
|
+
self.assertEqual(
|
101
|
+
upd["zotero_write_approval_status"],
|
102
|
+
{"collection_path": self.collection_path, "approved": True},
|
103
|
+
)
|
104
|
+
self.assertIn(
|
105
|
+
f"Human approved saving 1 papers to Zotero collection '{self.collection_path}'",
|
106
|
+
upd["messages"][0].content,
|
107
|
+
)
|
108
|
+
mock_fetch.assert_called_once()
|
109
|
+
mock_interrupt.assert_called_once()
|
110
|
+
|
111
|
+
@patch(
|
112
|
+
"aiagents4pharma.talk2scholars.tools.zotero.zotero_review.fetch_papers_for_save",
|
113
|
+
return_value={"p1": {"Title": "T1", "Authors": ["A1", "A2"]}},
|
114
|
+
)
|
115
|
+
@patch(
|
116
|
+
"aiagents4pharma.talk2scholars.tools.zotero.zotero_review.interrupt",
|
117
|
+
return_value="dummy_response",
|
118
|
+
)
|
119
|
+
def test_human_approve_custom(self, mock_interrupt, mock_fetch):
|
120
|
+
"""Test when human approves with a custom collection path."""
|
121
|
+
fake_structured_llm = MagicMock()
|
122
|
+
fake_decision = MagicMock()
|
123
|
+
fake_decision.decision = "custom"
|
124
|
+
fake_decision.custom_path = "/Custom"
|
125
|
+
fake_structured_llm.invoke.return_value = fake_decision
|
126
|
+
|
127
|
+
fake_llm_model = MagicMock()
|
128
|
+
fake_llm_model.with_structured_output.return_value = fake_structured_llm
|
129
|
+
|
130
|
+
state = {
|
131
|
+
"last_displayed_papers": self.sample_papers,
|
132
|
+
"llm_model": fake_llm_model,
|
133
|
+
}
|
134
|
+
|
135
|
+
result = zotero_review.run(
|
136
|
+
{
|
137
|
+
"tool_call_id": self.tool_call_id,
|
138
|
+
"collection_path": self.collection_path,
|
139
|
+
"state": state,
|
140
|
+
}
|
141
|
+
)
|
142
|
+
|
143
|
+
upd = result.update
|
144
|
+
self.assertEqual(
|
145
|
+
upd["zotero_write_approval_status"],
|
146
|
+
{"collection_path": "/Custom", "approved": True},
|
147
|
+
)
|
148
|
+
self.assertIn(
|
149
|
+
"Human approved saving papers to custom Zotero collection '/Custom'",
|
150
|
+
upd["messages"][0].content,
|
151
|
+
)
|
152
|
+
mock_fetch.assert_called_once()
|
153
|
+
mock_interrupt.assert_called_once()
|
154
|
+
|
155
|
+
@patch(
|
156
|
+
"aiagents4pharma.talk2scholars.tools.zotero.zotero_review.fetch_papers_for_save",
|
157
|
+
return_value={"p1": {"Title": "T1", "Authors": ["A1", "A2"]}},
|
158
|
+
)
|
159
|
+
@patch(
|
160
|
+
"aiagents4pharma.talk2scholars.tools.zotero.zotero_review.interrupt",
|
161
|
+
return_value="dummy_response",
|
162
|
+
)
|
163
|
+
def test_human_reject(self, mock_interrupt, mock_fetch):
|
164
|
+
"""Test when human rejects saving papers via structured output."""
|
165
|
+
fake_structured_llm = MagicMock()
|
166
|
+
fake_decision = MagicMock()
|
167
|
+
fake_decision.decision = "reject"
|
168
|
+
fake_structured_llm.invoke.return_value = fake_decision
|
169
|
+
|
170
|
+
fake_llm_model = MagicMock()
|
171
|
+
fake_llm_model.with_structured_output.return_value = fake_structured_llm
|
172
|
+
|
173
|
+
state = {
|
174
|
+
"last_displayed_papers": self.sample_papers,
|
175
|
+
"llm_model": fake_llm_model,
|
176
|
+
}
|
177
|
+
|
178
|
+
result = zotero_review.run(
|
179
|
+
{
|
180
|
+
"tool_call_id": self.tool_call_id,
|
181
|
+
"collection_path": self.collection_path,
|
182
|
+
"state": state,
|
183
|
+
}
|
184
|
+
)
|
185
|
+
|
186
|
+
upd = result.update
|
187
|
+
self.assertEqual(upd["zotero_write_approval_status"], {"approved": False})
|
188
|
+
self.assertIn(
|
189
|
+
"Human rejected saving papers to Zotero", upd["messages"][0].content
|
190
|
+
)
|
191
|
+
mock_fetch.assert_called_once()
|
192
|
+
mock_interrupt.assert_called_once()
|
193
|
+
|
194
|
+
@patch(
|
195
|
+
"aiagents4pharma.talk2scholars.tools.zotero.zotero_review.fetch_papers_for_save"
|
196
|
+
)
|
197
|
+
@patch("aiagents4pharma.talk2scholars.tools.zotero.zotero_review.interrupt")
|
198
|
+
def test_structured_processing_failure(self, mock_interrupt, mock_fetch):
|
199
|
+
"""Test fallback when structured review processing fails."""
|
200
|
+
# Simulate valid fetched papers with multiple entries.
|
201
|
+
papers = {
|
202
|
+
f"p{i}": {"Title": f"Title{i}", "Authors": [f"A{i}"]} for i in range(1, 8)
|
203
|
+
}
|
204
|
+
mock_fetch.return_value = papers
|
205
|
+
mock_interrupt.return_value = "dummy_response"
|
206
|
+
# Provide a fake llm_model whose invoke() raises an exception.
|
207
|
+
fake_structured_llm = MagicMock()
|
208
|
+
fake_structured_llm.invoke.side_effect = Exception("structured error")
|
209
|
+
fake_llm_model = MagicMock()
|
210
|
+
fake_llm_model.with_structured_output.return_value = fake_structured_llm
|
211
|
+
|
212
|
+
state = {"last_displayed_papers": papers, "llm_model": fake_llm_model}
|
213
|
+
|
214
|
+
result = zotero_review.run(
|
215
|
+
{
|
216
|
+
"tool_call_id": self.tool_call_id,
|
217
|
+
"collection_path": "/MyCol",
|
218
|
+
"state": state,
|
219
|
+
}
|
220
|
+
)
|
221
|
+
|
222
|
+
upd = result.update
|
223
|
+
content = upd["messages"][0].content
|
224
|
+
# The fallback message should start with "REVIEW REQUIRED:".
|
225
|
+
self.assertTrue(content.startswith("REVIEW REQUIRED:"))
|
226
|
+
self.assertIn("Would you like to save 7 papers", content)
|
227
|
+
self.assertIn("... and 2 more papers", content)
|
228
|
+
|
229
|
+
approved = upd["zotero_write_approval_status"]
|
230
|
+
self.assertEqual(approved["collection_path"], "/MyCol")
|
231
|
+
self.assertTrue(approved["papers_reviewed"])
|
232
|
+
self.assertFalse(approved["approved"])
|
233
|
+
self.assertEqual(approved["papers_count"], 7)
|
234
|
+
|
235
|
+
@patch(
|
236
|
+
"aiagents4pharma.talk2scholars.tools.zotero.zotero_review.fetch_papers_for_save",
|
237
|
+
return_value={
|
238
|
+
"p1": {"Title": "Test Paper", "Authors": ["Alice", "Bob", "Charlie"]}
|
239
|
+
},
|
240
|
+
)
|
241
|
+
@patch(
|
242
|
+
"aiagents4pharma.talk2scholars.tools.zotero.zotero_review.interrupt",
|
243
|
+
return_value="dummy_response",
|
244
|
+
)
|
245
|
+
def test_authors_et_al_in_summary(self, mock_interrupt, mock_fetch):
|
246
|
+
"""
|
247
|
+
Test that the papers summary includes 'et al.' when a paper has more than two authors.
|
248
|
+
This is achieved by forcing a fallback (structured processing failure) so that the fallback
|
249
|
+
message with the papers summary is generated.
|
250
|
+
"""
|
251
|
+
# Create a fake llm_model whose structured output processing fails.
|
252
|
+
fake_structured_llm = MagicMock()
|
253
|
+
fake_structured_llm.invoke.side_effect = Exception("structured error")
|
254
|
+
fake_llm_model = MagicMock()
|
255
|
+
fake_llm_model.with_structured_output.return_value = fake_structured_llm
|
256
|
+
|
257
|
+
state = {
|
258
|
+
"last_displayed_papers": {
|
259
|
+
"p1": {"Title": "Test Paper", "Authors": ["Alice", "Bob", "Charlie"]}
|
260
|
+
},
|
261
|
+
"llm_model": fake_llm_model,
|
262
|
+
}
|
263
|
+
result = zotero_review.run(
|
264
|
+
{
|
265
|
+
"tool_call_id": self.tool_call_id,
|
266
|
+
"collection_path": self.collection_path,
|
267
|
+
"state": state,
|
268
|
+
}
|
269
|
+
)
|
270
|
+
fallback_message = result.update["messages"][0].content
|
271
|
+
self.assertIn("et al.", fallback_message)
|
272
|
+
mock_fetch.assert_called_once()
|
273
|
+
mock_interrupt.assert_called_once()
|