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.
Files changed (30) hide show
  1. aiagents4pharma/talk2scholars/agents/__init__.py +3 -2
  2. aiagents4pharma/talk2scholars/agents/main_agent.py +51 -4
  3. aiagents4pharma/talk2scholars/agents/zotero_agent.py +120 -0
  4. aiagents4pharma/talk2scholars/configs/agents/talk2scholars/__init__.py +1 -0
  5. aiagents4pharma/talk2scholars/configs/agents/talk2scholars/main_agent/default.yaml +39 -19
  6. aiagents4pharma/talk2scholars/configs/agents/talk2scholars/s2_agent/default.yaml +26 -0
  7. aiagents4pharma/talk2scholars/configs/agents/talk2scholars/zotero_agent/__init__.py +3 -0
  8. aiagents4pharma/talk2scholars/configs/agents/talk2scholars/zotero_agent/default.yaml +35 -0
  9. aiagents4pharma/talk2scholars/configs/config.yaml +3 -1
  10. aiagents4pharma/talk2scholars/configs/tools/__init__.py +1 -0
  11. aiagents4pharma/talk2scholars/configs/tools/zotero_read/__init__.py +3 -0
  12. aiagents4pharma/talk2scholars/configs/tools/zotero_read/default.yaml +15 -0
  13. aiagents4pharma/talk2scholars/state/state_talk2scholars.py +2 -0
  14. aiagents4pharma/talk2scholars/tests/test_call_s2.py +99 -0
  15. aiagents4pharma/talk2scholars/tests/test_call_zotero.py +93 -0
  16. aiagents4pharma/talk2scholars/tests/test_main_agent.py +6 -42
  17. aiagents4pharma/talk2scholars/tests/test_routing_logic.py +71 -0
  18. aiagents4pharma/talk2scholars/tests/test_zotero_agent.py +160 -0
  19. aiagents4pharma/talk2scholars/tests/test_zotero_tool.py +171 -0
  20. aiagents4pharma/talk2scholars/tools/__init__.py +3 -2
  21. aiagents4pharma/talk2scholars/tools/s2/multi_paper_rec.py +17 -2
  22. aiagents4pharma/talk2scholars/tools/s2/search.py +14 -2
  23. aiagents4pharma/talk2scholars/tools/s2/single_paper_rec.py +17 -2
  24. aiagents4pharma/talk2scholars/tools/zotero/__init__.py +5 -0
  25. aiagents4pharma/talk2scholars/tools/zotero/zotero_read.py +142 -0
  26. {aiagents4pharma-1.22.4.dist-info → aiagents4pharma-1.23.0.dist-info}/METADATA +26 -14
  27. {aiagents4pharma-1.22.4.dist-info → aiagents4pharma-1.23.0.dist-info}/RECORD +30 -18
  28. {aiagents4pharma-1.22.4.dist-info → aiagents4pharma-1.23.0.dist-info}/LICENSE +0 -0
  29. {aiagents4pharma-1.22.4.dist-info → aiagents4pharma-1.23.0.dist-info}/WHEEL +0 -0
  30. {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
- assert result.goto == "s2_agent"
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"
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env python3
2
2
 
3
- '''
3
+ """
4
4
  Import statements
5
- '''
5
+ """
6
6
 
7
7
  from . import s2
8
+ from . import zotero
@@ -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 = "Recommendations based on multiple papers was successful."
137
- content += " Here is a summary of the recommendations:"
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 = "Search was successful."
113
- content += " Here is a summary of the search results:"
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
- content = "Recommendations based on a single paper were successful."
135
- content += " Here is a summary of the recommendations:"
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={
@@ -0,0 +1,5 @@
1
+ """
2
+ Import statements
3
+ """
4
+
5
+ from . import zotero_read