aiagents4pharma 1.22.4__py3-none-any.whl → 1.23.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.
- aiagents4pharma/talk2scholars/agents/__init__.py +3 -2
- aiagents4pharma/talk2scholars/agents/main_agent.py +51 -4
- aiagents4pharma/talk2scholars/agents/zotero_agent.py +120 -0
- aiagents4pharma/talk2scholars/configs/agents/talk2scholars/__init__.py +1 -0
- aiagents4pharma/talk2scholars/configs/agents/talk2scholars/main_agent/default.yaml +39 -19
- aiagents4pharma/talk2scholars/configs/agents/talk2scholars/s2_agent/default.yaml +26 -0
- aiagents4pharma/talk2scholars/configs/agents/talk2scholars/zotero_agent/__init__.py +3 -0
- aiagents4pharma/talk2scholars/configs/agents/talk2scholars/zotero_agent/default.yaml +35 -0
- aiagents4pharma/talk2scholars/configs/config.yaml +3 -1
- aiagents4pharma/talk2scholars/configs/tools/__init__.py +1 -0
- aiagents4pharma/talk2scholars/configs/tools/zotero_read/__init__.py +3 -0
- aiagents4pharma/talk2scholars/configs/tools/zotero_read/default.yaml +15 -0
- aiagents4pharma/talk2scholars/state/state_talk2scholars.py +2 -0
- aiagents4pharma/talk2scholars/tests/test_call_s2.py +99 -0
- aiagents4pharma/talk2scholars/tests/test_call_zotero.py +93 -0
- aiagents4pharma/talk2scholars/tests/test_main_agent.py +6 -42
- aiagents4pharma/talk2scholars/tests/test_routing_logic.py +71 -0
- aiagents4pharma/talk2scholars/tests/test_zotero_agent.py +160 -0
- aiagents4pharma/talk2scholars/tests/test_zotero_tool.py +171 -0
- aiagents4pharma/talk2scholars/tools/__init__.py +3 -2
- aiagents4pharma/talk2scholars/tools/s2/multi_paper_rec.py +17 -2
- aiagents4pharma/talk2scholars/tools/s2/search.py +14 -2
- aiagents4pharma/talk2scholars/tools/s2/single_paper_rec.py +17 -2
- aiagents4pharma/talk2scholars/tools/zotero/__init__.py +5 -0
- aiagents4pharma/talk2scholars/tools/zotero/zotero_read.py +142 -0
- {aiagents4pharma-1.22.4.dist-info → aiagents4pharma-1.23.0.dist-info}/METADATA +26 -14
- {aiagents4pharma-1.22.4.dist-info → aiagents4pharma-1.23.0.dist-info}/RECORD +30 -18
- {aiagents4pharma-1.22.4.dist-info → aiagents4pharma-1.23.0.dist-info}/LICENSE +0 -0
- {aiagents4pharma-1.22.4.dist-info → aiagents4pharma-1.23.0.dist-info}/WHEEL +0 -0
- {aiagents4pharma-1.22.4.dist-info → aiagents4pharma-1.23.0.dist-info}/top_level.txt +0 -0
@@ -5,6 +5,7 @@ Tests the supervisor agent's routing logic and state management.
|
|
5
5
|
|
6
6
|
# pylint: disable=redefined-outer-name
|
7
7
|
# pylint: disable=redefined-outer-name,too-few-public-methods
|
8
|
+
import random
|
8
9
|
from unittest.mock import Mock, patch, MagicMock
|
9
10
|
import pytest
|
10
11
|
from langchain_core.messages import HumanMessage, AIMessage
|
@@ -33,6 +34,7 @@ def test_get_app():
|
|
33
34
|
assert app is not None
|
34
35
|
assert "supervisor" in app.nodes
|
35
36
|
assert "s2_agent" in app.nodes # Ensure nodes exist
|
37
|
+
assert "zotero_agent" in app.nodes
|
36
38
|
|
37
39
|
|
38
40
|
def test_get_app_with_default_llm():
|
@@ -75,7 +77,7 @@ def test_supervisor_node_execution():
|
|
75
77
|
class MockRouter:
|
76
78
|
"""Mock router class."""
|
77
79
|
|
78
|
-
next = "s2_agent"
|
80
|
+
next = random.choice(["s2_agent", "zotero_agent"])
|
79
81
|
|
80
82
|
with (
|
81
83
|
patch.object(mock_llm, "with_structured_output", return_value=mock_llm),
|
@@ -84,7 +86,9 @@ def test_supervisor_node_execution():
|
|
84
86
|
supervisor_node = make_supervisor_node(mock_llm, thread_id)
|
85
87
|
mock_state = Talk2Scholars(messages=[HumanMessage(content="Find AI papers")])
|
86
88
|
result = supervisor_node(mock_state)
|
87
|
-
|
89
|
+
|
90
|
+
# Accept either "s2_agent" or "zotero_agent"
|
91
|
+
assert result.goto in ["s2_agent", "zotero_agent"]
|
88
92
|
|
89
93
|
|
90
94
|
def test_supervisor_node_finish():
|
@@ -114,43 +118,3 @@ def test_supervisor_node_finish():
|
|
114
118
|
assert "messages" in result.update
|
115
119
|
assert isinstance(result.update["messages"], AIMessage)
|
116
120
|
assert result.update["messages"].content == "Final AI Response"
|
117
|
-
|
118
|
-
|
119
|
-
def test_call_s2_agent_failure_in_get_app():
|
120
|
-
"""Test handling failure when calling s2_agent.get_app()."""
|
121
|
-
thread_id = "test_thread"
|
122
|
-
mock_state = Talk2Scholars(messages=[HumanMessage(content="Find AI papers")])
|
123
|
-
|
124
|
-
with patch(
|
125
|
-
"aiagents4pharma.talk2scholars.agents.s2_agent.get_app",
|
126
|
-
side_effect=Exception("S2 Agent Failure"),
|
127
|
-
):
|
128
|
-
with pytest.raises(Exception) as exc_info:
|
129
|
-
app = get_app(thread_id) # Get the compiled workflow
|
130
|
-
app.invoke(
|
131
|
-
mock_state,
|
132
|
-
{"configurable": {"config_id": thread_id, "thread_id": thread_id}},
|
133
|
-
)
|
134
|
-
|
135
|
-
assert "S2 Agent Failure" in str(exc_info.value)
|
136
|
-
|
137
|
-
|
138
|
-
def test_call_s2_agent_failure_in_invoke():
|
139
|
-
"""Test handling failure when invoking s2_agent app."""
|
140
|
-
thread_id = "test_thread"
|
141
|
-
mock_state = Talk2Scholars(messages=[HumanMessage(content="Find AI papers")])
|
142
|
-
|
143
|
-
mock_app = Mock()
|
144
|
-
mock_app.invoke.side_effect = Exception("S2 Agent Invoke Failure")
|
145
|
-
|
146
|
-
with patch(
|
147
|
-
"aiagents4pharma.talk2scholars.agents.s2_agent.get_app", return_value=mock_app
|
148
|
-
):
|
149
|
-
with pytest.raises(Exception) as exc_info:
|
150
|
-
app = get_app(thread_id) # Get the compiled workflow
|
151
|
-
app.invoke(
|
152
|
-
mock_state,
|
153
|
-
{"configurable": {"config_id": thread_id, "thread_id": thread_id}},
|
154
|
-
)
|
155
|
-
|
156
|
-
assert "S2 Agent Invoke Failure" in str(exc_info.value)
|
@@ -0,0 +1,71 @@
|
|
1
|
+
"""
|
2
|
+
Routing logic for zotero_agent through the main_agent
|
3
|
+
"""
|
4
|
+
|
5
|
+
import pytest
|
6
|
+
from langgraph.types import Command
|
7
|
+
from langgraph.graph import END
|
8
|
+
from langchain_core.messages import HumanMessage
|
9
|
+
from aiagents4pharma.talk2scholars.state.state_talk2scholars import Talk2Scholars
|
10
|
+
|
11
|
+
# pylint: disable=redefined-outer-name
|
12
|
+
|
13
|
+
|
14
|
+
@pytest.fixture
|
15
|
+
def mock_state():
|
16
|
+
"""Creates a mock state to simulate an ongoing conversation."""
|
17
|
+
return Talk2Scholars(messages=[])
|
18
|
+
|
19
|
+
|
20
|
+
@pytest.fixture
|
21
|
+
def mock_router():
|
22
|
+
"""Creates a mock supervisor router that routes based on keyword matching."""
|
23
|
+
|
24
|
+
def mock_supervisor_node(state):
|
25
|
+
query = state["messages"][-1].content.lower()
|
26
|
+
|
27
|
+
# Expanded keyword matching for S2 Agent
|
28
|
+
s2_keywords = [
|
29
|
+
"paper",
|
30
|
+
"research",
|
31
|
+
"citations",
|
32
|
+
"journal",
|
33
|
+
"articles",
|
34
|
+
"references",
|
35
|
+
]
|
36
|
+
zotero_keywords = ["zotero", "library", "saved papers", "academic library"]
|
37
|
+
|
38
|
+
if any(keyword in query for keyword in zotero_keywords):
|
39
|
+
return Command(goto="zotero_agent")
|
40
|
+
if any(keyword in query for keyword in s2_keywords):
|
41
|
+
return Command(goto="s2_agent")
|
42
|
+
|
43
|
+
# If no match, default to ending the conversation
|
44
|
+
return Command(goto=END)
|
45
|
+
|
46
|
+
return mock_supervisor_node
|
47
|
+
|
48
|
+
|
49
|
+
@pytest.mark.parametrize(
|
50
|
+
"user_query,expected_agent",
|
51
|
+
[
|
52
|
+
("Find papers on deep learning.", "s2_agent"),
|
53
|
+
("Show me my saved references in Zotero.", "zotero_agent"),
|
54
|
+
("I need some research articles.", "s2_agent"),
|
55
|
+
("Fetch my academic library.", "zotero_agent"),
|
56
|
+
("Retrieve citations.", "s2_agent"),
|
57
|
+
("Can you get journal articles?", "s2_agent"),
|
58
|
+
(
|
59
|
+
"Completely unrelated query.",
|
60
|
+
"__end__",
|
61
|
+
), # NEW: Should trigger the `END` case
|
62
|
+
],
|
63
|
+
)
|
64
|
+
def test_routing_logic(mock_state, mock_router, user_query, expected_agent):
|
65
|
+
"""Tests that the routing logic correctly assigns the right agent or ends conversation."""
|
66
|
+
mock_state["messages"].append(HumanMessage(content=user_query))
|
67
|
+
result = mock_router(mock_state)
|
68
|
+
|
69
|
+
print(f"\nDEBUG: Query '{user_query}' routed to: {result.goto}")
|
70
|
+
|
71
|
+
assert result.goto == expected_agent, f"Failed for query: {user_query}"
|
@@ -0,0 +1,160 @@
|
|
1
|
+
"""
|
2
|
+
Updated Unit Tests for the Zotero agent (Zotero Library Managent sub-agent).
|
3
|
+
"""
|
4
|
+
|
5
|
+
# pylint: disable=redefined-outer-name
|
6
|
+
from unittest import mock
|
7
|
+
import pytest
|
8
|
+
from langchain_core.messages import HumanMessage, AIMessage
|
9
|
+
from ..agents.zotero_agent import get_app
|
10
|
+
from ..state.state_talk2scholars import Talk2Scholars
|
11
|
+
|
12
|
+
|
13
|
+
@pytest.fixture(autouse=True)
|
14
|
+
def mock_hydra_fixture():
|
15
|
+
"""Mock Hydra configuration to prevent external dependencies."""
|
16
|
+
with mock.patch("hydra.initialize"), mock.patch("hydra.compose") as mock_compose:
|
17
|
+
cfg_mock = mock.MagicMock()
|
18
|
+
cfg_mock.agents.talk2scholars.zotero_agent.temperature = 0
|
19
|
+
cfg_mock.agents.talk2scholars.zotero_agent.zotero_agent = "Test prompt"
|
20
|
+
mock_compose.return_value = cfg_mock
|
21
|
+
yield mock_compose
|
22
|
+
|
23
|
+
|
24
|
+
@pytest.fixture
|
25
|
+
def mock_tools_fixture():
|
26
|
+
"""Mock tools to prevent execution of real API calls."""
|
27
|
+
with (
|
28
|
+
mock.patch(
|
29
|
+
"aiagents4pharma.talk2scholars.tools.s2.display_results.display_results"
|
30
|
+
) as mock_s2_display,
|
31
|
+
mock.patch(
|
32
|
+
"aiagents4pharma.talk2scholars.tools.s2.query_results.query_results"
|
33
|
+
) as mock_s2_query_results,
|
34
|
+
mock.patch(
|
35
|
+
"aiagents4pharma.talk2scholars.tools.s2."
|
36
|
+
"retrieve_semantic_scholar_paper_id."
|
37
|
+
"retrieve_semantic_scholar_paper_id"
|
38
|
+
) as mock_s2_retrieve_id,
|
39
|
+
mock.patch(
|
40
|
+
"aiagents4pharma.talk2scholars.tools.zotero.zotero_read.zotero_search_tool"
|
41
|
+
) as mock_zotero_query_results,
|
42
|
+
):
|
43
|
+
mock_s2_display.return_value = {"result": "Mock Display Result"}
|
44
|
+
mock_s2_query_results.return_value = {"result": "Mock Query Result"}
|
45
|
+
mock_s2_retrieve_id.return_value = {"paper_id": "MockPaper123"}
|
46
|
+
mock_zotero_query_results.return_value = {"result": "Mock Search Result"}
|
47
|
+
|
48
|
+
yield [
|
49
|
+
mock_s2_display,
|
50
|
+
mock_s2_query_results,
|
51
|
+
mock_s2_retrieve_id,
|
52
|
+
mock_zotero_query_results,
|
53
|
+
]
|
54
|
+
|
55
|
+
|
56
|
+
@pytest.mark.usefixtures("mock_hydra_fixture")
|
57
|
+
def test_zotero_agent_initialization():
|
58
|
+
"""Test that S2 agent initializes correctly with mock configuration."""
|
59
|
+
thread_id = "test_thread"
|
60
|
+
with mock.patch(
|
61
|
+
"aiagents4pharma.talk2scholars.agents.zotero_agent.create_react_agent"
|
62
|
+
) as mock_create:
|
63
|
+
mock_create.return_value = mock.Mock()
|
64
|
+
app = get_app(thread_id)
|
65
|
+
assert app is not None
|
66
|
+
assert mock_create.called
|
67
|
+
|
68
|
+
|
69
|
+
def test_zotero_agent_invocation():
|
70
|
+
"""Test that the S2 agent processes user input and returns a valid response."""
|
71
|
+
thread_id = "test_thread"
|
72
|
+
mock_state = Talk2Scholars(messages=[HumanMessage(content="Find AI papers")])
|
73
|
+
with mock.patch(
|
74
|
+
"aiagents4pharma.talk2scholars.agents.zotero_agent.create_react_agent"
|
75
|
+
) as mock_create:
|
76
|
+
mock_agent = mock.Mock()
|
77
|
+
mock_create.return_value = mock_agent
|
78
|
+
mock_agent.invoke.return_value = {
|
79
|
+
"messages": [AIMessage(content="Here are some AI papers")],
|
80
|
+
"papers": {"id123": "AI Research Paper"},
|
81
|
+
}
|
82
|
+
app = get_app(thread_id)
|
83
|
+
result = app.invoke(
|
84
|
+
mock_state,
|
85
|
+
config={
|
86
|
+
"configurable": {
|
87
|
+
"thread_id": thread_id,
|
88
|
+
"checkpoint_ns": "test_ns",
|
89
|
+
"checkpoint_id": "test_checkpoint",
|
90
|
+
}
|
91
|
+
},
|
92
|
+
)
|
93
|
+
assert "messages" in result
|
94
|
+
assert "papers" in result
|
95
|
+
assert result["papers"]["id123"] == "AI Research Paper"
|
96
|
+
|
97
|
+
|
98
|
+
def test_zotero_agent_tools_assignment(request):
|
99
|
+
"""Ensure that the correct tools are assigned to the agent."""
|
100
|
+
thread_id = "test_thread"
|
101
|
+
mock_tools = request.getfixturevalue("mock_tools_fixture")
|
102
|
+
with (
|
103
|
+
mock.patch(
|
104
|
+
"aiagents4pharma.talk2scholars.agents.zotero_agent.create_react_agent"
|
105
|
+
) as mock_create,
|
106
|
+
mock.patch(
|
107
|
+
"aiagents4pharma.talk2scholars.agents.zotero_agent.ToolNode"
|
108
|
+
) as mock_toolnode,
|
109
|
+
):
|
110
|
+
mock_agent = mock.Mock()
|
111
|
+
mock_create.return_value = mock_agent
|
112
|
+
mock_tool_instance = mock.Mock()
|
113
|
+
mock_tool_instance.tools = mock_tools
|
114
|
+
mock_toolnode.return_value = mock_tool_instance
|
115
|
+
get_app(thread_id)
|
116
|
+
assert mock_toolnode.called
|
117
|
+
assert len(mock_tool_instance.tools) == 4
|
118
|
+
|
119
|
+
|
120
|
+
def test_s2_query_results_tool():
|
121
|
+
"""Test if the query_results tool is correctly utilized by the agent."""
|
122
|
+
thread_id = "test_thread"
|
123
|
+
mock_state = Talk2Scholars(
|
124
|
+
messages=[HumanMessage(content="Query results for AI papers")]
|
125
|
+
)
|
126
|
+
with mock.patch(
|
127
|
+
"aiagents4pharma.talk2scholars.agents.zotero_agent.create_react_agent"
|
128
|
+
) as mock_create:
|
129
|
+
mock_agent = mock.Mock()
|
130
|
+
mock_create.return_value = mock_agent
|
131
|
+
mock_agent.invoke.return_value = {
|
132
|
+
"messages": [HumanMessage(content="Query results for AI papers")],
|
133
|
+
"last_displayed_papers": {},
|
134
|
+
"papers": {
|
135
|
+
"query_results": "Mock Query Result"
|
136
|
+
}, # Ensure the expected key is inside 'papers'
|
137
|
+
"multi_papers": {},
|
138
|
+
}
|
139
|
+
app = get_app(thread_id)
|
140
|
+
result = app.invoke(
|
141
|
+
mock_state,
|
142
|
+
config={
|
143
|
+
"configurable": {
|
144
|
+
"thread_id": thread_id,
|
145
|
+
"checkpoint_ns": "test_ns",
|
146
|
+
"checkpoint_id": "test_checkpoint",
|
147
|
+
}
|
148
|
+
},
|
149
|
+
)
|
150
|
+
assert "query_results" in result["papers"]
|
151
|
+
assert mock_agent.invoke.called
|
152
|
+
|
153
|
+
|
154
|
+
def test_zotero_agent_hydra_failure():
|
155
|
+
"""Test exception handling when Hydra fails to load config."""
|
156
|
+
thread_id = "test_thread"
|
157
|
+
with mock.patch("hydra.initialize", side_effect=Exception("Hydra error")):
|
158
|
+
with pytest.raises(Exception) as exc_info:
|
159
|
+
get_app(thread_id)
|
160
|
+
assert "Hydra error" in str(exc_info.value)
|
@@ -0,0 +1,171 @@
|
|
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"
|
@@ -133,11 +133,26 @@ def get_multi_paper_recommendations(
|
|
133
133
|
if paper.get("title") and paper.get("authors")
|
134
134
|
}
|
135
135
|
|
136
|
-
content
|
137
|
-
|
136
|
+
# Prepare content with top 3 paper titles and years
|
137
|
+
top_papers = list(filtered_papers.values())[:3]
|
138
|
+
top_papers_info = "\n".join(
|
139
|
+
[
|
140
|
+
f"{i+1}. {paper['Title']} ({paper['Year']})"
|
141
|
+
for i, paper in enumerate(top_papers)
|
142
|
+
]
|
143
|
+
)
|
144
|
+
|
145
|
+
logger.info("Filtered %d papers", len(filtered_papers))
|
146
|
+
|
147
|
+
content = (
|
148
|
+
"Recommendations based on multiple papers were successful. "
|
149
|
+
"Papers are attached as an artifact."
|
150
|
+
)
|
151
|
+
content += " Here is a summary of the recommendations:\n"
|
138
152
|
content += f"Number of papers found: {len(filtered_papers)}\n"
|
139
153
|
content += f"Query Paper IDs: {', '.join(paper_ids)}\n"
|
140
154
|
content += f"Year: {year}\n" if year else ""
|
155
|
+
content += "Top papers:\n" + top_papers_info
|
141
156
|
|
142
157
|
return Command(
|
143
158
|
update={
|
@@ -109,11 +109,23 @@ def search_tool(
|
|
109
109
|
|
110
110
|
logger.info("Filtered %d papers", len(filtered_papers))
|
111
111
|
|
112
|
-
content
|
113
|
-
|
112
|
+
# Prepare content with top 3 paper titles and years
|
113
|
+
top_papers = list(filtered_papers.values())[:3]
|
114
|
+
top_papers_info = "\n".join(
|
115
|
+
[
|
116
|
+
f"{i+1}. {paper['Title']} ({paper['Year']})"
|
117
|
+
for i, paper in enumerate(top_papers)
|
118
|
+
]
|
119
|
+
)
|
120
|
+
|
121
|
+
content = (
|
122
|
+
"Search was successful. Papers are attached as an artifact. "
|
123
|
+
"Here is a summary of the search results:\n"
|
124
|
+
)
|
114
125
|
content += f"Number of papers found: {len(filtered_papers)}\n"
|
115
126
|
content += f"Query: {query}\n"
|
116
127
|
content += f"Year: {year}\n" if year else ""
|
128
|
+
content += "Top papers:\n" + top_papers_info
|
117
129
|
|
118
130
|
return Command(
|
119
131
|
update={
|
@@ -131,11 +131,26 @@ def get_single_paper_recommendations(
|
|
131
131
|
if paper.get("title") and paper.get("authors")
|
132
132
|
}
|
133
133
|
|
134
|
-
|
135
|
-
|
134
|
+
# Prepare content with top 3 paper titles and years
|
135
|
+
top_papers = list(filtered_papers.values())[:3]
|
136
|
+
top_papers_info = "\n".join(
|
137
|
+
[
|
138
|
+
f"{i+1}. {paper['Title']} ({paper['Year']})"
|
139
|
+
for i, paper in enumerate(top_papers)
|
140
|
+
]
|
141
|
+
)
|
142
|
+
|
143
|
+
logger.info("Filtered %d papers", len(filtered_papers))
|
144
|
+
|
145
|
+
content = (
|
146
|
+
"Recommendations based on single paper were successful. "
|
147
|
+
"Papers are attached as an artifact."
|
148
|
+
)
|
149
|
+
content += " Here is a summary of the recommendations:\n"
|
136
150
|
content += f"Number of papers found: {len(filtered_papers)}\n"
|
137
151
|
content += f"Query Paper ID: {paper_id}\n"
|
138
152
|
content += f"Year: {year}\n" if year else ""
|
153
|
+
content += "Top papers:\n" + top_papers_info
|
139
154
|
|
140
155
|
return Command(
|
141
156
|
update={
|