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.
- aiagents4pharma/talk2scholars/__init__.py +2 -0
- aiagents4pharma/talk2scholars/agents/__init__.py +8 -0
- aiagents4pharma/talk2scholars/agents/zotero_agent.py +9 -7
- aiagents4pharma/talk2scholars/configs/__init__.py +2 -0
- aiagents4pharma/talk2scholars/configs/agents/__init__.py +2 -0
- aiagents4pharma/talk2scholars/configs/agents/talk2scholars/__init__.py +2 -0
- aiagents4pharma/talk2scholars/configs/agents/talk2scholars/zotero_agent/default.yaml +9 -15
- aiagents4pharma/talk2scholars/configs/app/__init__.py +2 -0
- aiagents4pharma/talk2scholars/configs/tools/__init__.py +9 -0
- aiagents4pharma/talk2scholars/configs/tools/zotero_write/default.yaml +55 -0
- aiagents4pharma/talk2scholars/state/__init__.py +4 -2
- aiagents4pharma/talk2scholars/state/state_talk2scholars.py +3 -0
- aiagents4pharma/talk2scholars/tests/test_routing_logic.py +1 -2
- aiagents4pharma/talk2scholars/tests/test_s2_multi.py +10 -8
- aiagents4pharma/talk2scholars/tests/test_s2_search.py +9 -5
- aiagents4pharma/talk2scholars/tests/test_s2_single.py +7 -7
- 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 +433 -1
- aiagents4pharma/talk2scholars/tests/test_zotero_read.py +57 -43
- aiagents4pharma/talk2scholars/tests/test_zotero_write.py +123 -588
- aiagents4pharma/talk2scholars/tools/__init__.py +3 -0
- aiagents4pharma/talk2scholars/tools/pdf/__init__.py +4 -2
- aiagents4pharma/talk2scholars/tools/s2/__init__.py +9 -0
- aiagents4pharma/talk2scholars/tools/s2/multi_paper_rec.py +9 -135
- aiagents4pharma/talk2scholars/tools/s2/search.py +8 -114
- aiagents4pharma/talk2scholars/tools/s2/single_paper_rec.py +8 -126
- aiagents4pharma/talk2scholars/tools/s2/utils/__init__.py +7 -0
- aiagents4pharma/talk2scholars/tools/s2/utils/multi_helper.py +194 -0
- aiagents4pharma/talk2scholars/tools/s2/utils/search_helper.py +175 -0
- aiagents4pharma/talk2scholars/tools/s2/utils/single_helper.py +186 -0
- aiagents4pharma/talk2scholars/tools/zotero/__init__.py +3 -0
- aiagents4pharma/talk2scholars/tools/zotero/utils/__init__.py +5 -0
- aiagents4pharma/talk2scholars/tools/zotero/utils/read_helper.py +167 -0
- aiagents4pharma/talk2scholars/tools/zotero/utils/review_helper.py +78 -0
- aiagents4pharma/talk2scholars/tools/zotero/utils/write_helper.py +197 -0
- aiagents4pharma/talk2scholars/tools/zotero/utils/zotero_path.py +126 -1
- aiagents4pharma/talk2scholars/tools/zotero/zotero_read.py +10 -139
- aiagents4pharma/talk2scholars/tools/zotero/zotero_review.py +164 -0
- aiagents4pharma/talk2scholars/tools/zotero/zotero_write.py +40 -229
- {aiagents4pharma-1.30.1.dist-info → aiagents4pharma-1.30.3.dist-info}/METADATA +3 -2
- {aiagents4pharma-1.30.1.dist-info → aiagents4pharma-1.30.3.dist-info}/RECORD +45 -35
- {aiagents4pharma-1.30.1.dist-info → aiagents4pharma-1.30.3.dist-info}/WHEEL +1 -1
- {aiagents4pharma-1.30.1.dist-info → aiagents4pharma-1.30.3.dist-info/licenses}/LICENSE +0 -0
- {aiagents4pharma-1.30.1.dist-info → aiagents4pharma-1.30.3.dist-info}/top_level.txt +0 -0
@@ -6,18 +6,13 @@ This tool is used to search for papers in Zotero library.
|
|
6
6
|
|
7
7
|
import logging
|
8
8
|
from typing import Annotated, Any
|
9
|
-
import hydra
|
10
|
-
from pyzotero import zotero
|
11
9
|
from langchain_core.messages import ToolMessage
|
12
10
|
from langchain_core.tools import tool
|
13
11
|
from langchain_core.tools.base import InjectedToolCallId
|
14
12
|
from langgraph.types import Command
|
15
13
|
from pydantic import BaseModel, Field
|
16
|
-
from
|
17
|
-
get_item_collections,
|
18
|
-
)
|
14
|
+
from .utils.read_helper import ZoteroSearchData
|
19
15
|
|
20
|
-
# pylint: disable=R0914,R0912,R0915
|
21
16
|
|
22
17
|
# Configure logging
|
23
18
|
logging.basicConfig(level=logging.INFO)
|
@@ -41,7 +36,7 @@ class ZoteroSearchInput(BaseModel):
|
|
41
36
|
|
42
37
|
|
43
38
|
@tool(args_schema=ZoteroSearchInput, parse_docstring=True)
|
44
|
-
def
|
39
|
+
def zotero_read(
|
45
40
|
query: str,
|
46
41
|
only_articles: bool,
|
47
42
|
tool_call_id: Annotated[str, InjectedToolCallId],
|
@@ -58,146 +53,22 @@ def zotero_search_tool(
|
|
58
53
|
Returns:
|
59
54
|
Dict[str, Any]: The search results and related information.
|
60
55
|
"""
|
61
|
-
#
|
62
|
-
|
63
|
-
cfg = hydra.compose(
|
64
|
-
config_name="config", overrides=["tools/zotero_read=default"]
|
65
|
-
)
|
66
|
-
logger.info("Loaded configuration for Zotero search tool")
|
67
|
-
cfg = cfg.tools.zotero_read
|
68
|
-
logger.info(
|
69
|
-
"Searching Zotero for query: '%s' (only_articles: %s, limit: %d)",
|
70
|
-
query,
|
71
|
-
only_articles,
|
72
|
-
limit,
|
73
|
-
)
|
56
|
+
# Create search data object to organize variables
|
57
|
+
search_data = ZoteroSearchData(query, only_articles, limit, tool_call_id)
|
74
58
|
|
75
|
-
#
|
76
|
-
|
77
|
-
|
78
|
-
# Fetch collection mapping once
|
79
|
-
item_to_collections = get_item_collections(zot)
|
80
|
-
|
81
|
-
# If the query is empty, fetch all items (up to max_limit), otherwise use the query
|
82
|
-
try:
|
83
|
-
if query.strip() == "":
|
84
|
-
logger.info(
|
85
|
-
"Empty query provided, fetching all items up to max_limit: %d",
|
86
|
-
cfg.zotero.max_limit,
|
87
|
-
)
|
88
|
-
items = zot.items(limit=cfg.zotero.max_limit)
|
89
|
-
else:
|
90
|
-
items = zot.items(q=query, limit=min(limit, cfg.zotero.max_limit))
|
91
|
-
except Exception as e:
|
92
|
-
logger.error("Failed to fetch items from Zotero: %s", e)
|
93
|
-
raise RuntimeError(
|
94
|
-
"Failed to fetch items from Zotero. Please retry the same query."
|
95
|
-
) from e
|
96
|
-
|
97
|
-
logger.info("Received %d items from Zotero", len(items))
|
98
|
-
|
99
|
-
if not items:
|
100
|
-
logger.error("No items returned from Zotero for query: '%s'", query)
|
101
|
-
raise RuntimeError(
|
102
|
-
"No items returned from Zotero. Please retry the same query."
|
103
|
-
)
|
104
|
-
|
105
|
-
# Define filter criteria
|
106
|
-
filter_item_types = cfg.zotero.filter_item_types if only_articles else []
|
107
|
-
logger.debug("Filtering item types: %s", filter_item_types)
|
108
|
-
# filter_excluded_types = (
|
109
|
-
# cfg.zotero.filter_excluded_types
|
110
|
-
# ) # Exclude non-research items
|
111
|
-
|
112
|
-
# Filter and format papers
|
113
|
-
filtered_papers = {}
|
114
|
-
|
115
|
-
for item in items:
|
116
|
-
if not isinstance(item, dict):
|
117
|
-
continue
|
118
|
-
|
119
|
-
data = item.get("data")
|
120
|
-
if not isinstance(data, dict):
|
121
|
-
continue
|
122
|
-
|
123
|
-
item_type = data.get("itemType", "N/A")
|
124
|
-
logger.debug("Item type: %s", item_type)
|
125
|
-
|
126
|
-
# Exclude attachments, notes, and other unwanted types
|
127
|
-
# if (
|
128
|
-
# not item_type
|
129
|
-
# or not isinstance(item_type, str)
|
130
|
-
# or item_type in filter_excluded_types # Skip attachments & notes
|
131
|
-
# or (
|
132
|
-
# only_articles and item_type not in filter_item_types
|
133
|
-
# ) # Skip non-research types
|
134
|
-
# ):
|
135
|
-
# continue
|
136
|
-
|
137
|
-
key = data.get("key")
|
138
|
-
if not key:
|
139
|
-
continue
|
140
|
-
|
141
|
-
# Use the imported utility function's mapping to get collection paths
|
142
|
-
collection_paths = item_to_collections.get(key, ["/Unknown"])
|
143
|
-
|
144
|
-
# Extract metadata safely
|
145
|
-
filtered_papers[key] = {
|
146
|
-
"Title": data.get("title", "N/A"),
|
147
|
-
"Abstract": data.get("abstractNote", "N/A"),
|
148
|
-
"Publication Date": data.get(
|
149
|
-
"date", "N/A"
|
150
|
-
), # Correct field for publication date
|
151
|
-
"URL": data.get("url", "N/A"),
|
152
|
-
"Type": item_type if isinstance(item_type, str) else "N/A",
|
153
|
-
"Collections": collection_paths, # Displays full collection paths
|
154
|
-
"Citation Count": data.get("citationCount", "N/A"), # Shows citations
|
155
|
-
"Venue": data.get("venue", "N/A"), # Displays venue
|
156
|
-
"Publication Venue": data.get(
|
157
|
-
"publicationTitle", "N/A"
|
158
|
-
), # Matches with Zotero Write
|
159
|
-
"Journal Name": data.get("journalAbbreviation", "N/A"), # Journal Name
|
160
|
-
# "Journal Volume": data.get("volume", "N/A"), # Journal Volume
|
161
|
-
# "Journal Pages": data.get("pages", "N/A"), # Journal Pages
|
162
|
-
"Authors": [
|
163
|
-
f"{creator.get('firstName', '')} {creator.get('lastName', '')}".strip()
|
164
|
-
for creator in data.get("creators", []) # Prevents NoneType error
|
165
|
-
if isinstance(creator, dict) and creator.get("creatorType") == "author"
|
166
|
-
],
|
167
|
-
}
|
168
|
-
|
169
|
-
if not filtered_papers:
|
170
|
-
logger.error("No matching papers returned from Zotero for query: '%s'", query)
|
171
|
-
raise RuntimeError(
|
172
|
-
"No matching papers returned from Zotero. Please retry the same query."
|
173
|
-
)
|
174
|
-
|
175
|
-
logger.info("Filtered %d items", len(filtered_papers))
|
176
|
-
|
177
|
-
# Prepare content with top 2 paper titles and types
|
178
|
-
top_papers = list(filtered_papers.values())[:2]
|
179
|
-
top_papers_info = "\n".join(
|
180
|
-
[
|
181
|
-
f"{i+1}. {paper['Title']} ({paper['Type']})"
|
182
|
-
for i, paper in enumerate(top_papers)
|
183
|
-
]
|
184
|
-
)
|
185
|
-
|
186
|
-
content = "Retrieval was successful. Papers are attached as an artifact."
|
187
|
-
content += " And here is a summary of the retrieval results:\n"
|
188
|
-
content += f"Number of papers found: {len(filtered_papers)}\n"
|
189
|
-
content += f"Query: {query}\n"
|
190
|
-
content += "Here are a few of these papers:\n" + top_papers_info
|
59
|
+
# Process the search
|
60
|
+
search_data.process_search()
|
61
|
+
results = search_data.get_search_results()
|
191
62
|
|
192
63
|
return Command(
|
193
64
|
update={
|
194
|
-
"zotero_read": filtered_papers,
|
65
|
+
"zotero_read": results["filtered_papers"],
|
195
66
|
"last_displayed_papers": "zotero_read",
|
196
67
|
"messages": [
|
197
68
|
ToolMessage(
|
198
|
-
content=content,
|
69
|
+
content=results["content"],
|
199
70
|
tool_call_id=tool_call_id,
|
200
|
-
artifact=filtered_papers,
|
71
|
+
artifact=results["filtered_papers"],
|
201
72
|
)
|
202
73
|
],
|
203
74
|
}
|
@@ -0,0 +1,164 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
|
3
|
+
"""
|
4
|
+
This tool implements human-in-the-loop review for Zotero write operations.
|
5
|
+
"""
|
6
|
+
|
7
|
+
import logging
|
8
|
+
from typing import Annotated, Any, Optional, Literal
|
9
|
+
from langchain_core.tools import tool
|
10
|
+
from langchain_core.messages import ToolMessage, HumanMessage
|
11
|
+
from langchain_core.tools.base import InjectedToolCallId
|
12
|
+
from langgraph.prebuilt import InjectedState
|
13
|
+
from langgraph.types import Command, interrupt
|
14
|
+
from pydantic import BaseModel, Field
|
15
|
+
from .utils.zotero_path import fetch_papers_for_save
|
16
|
+
from .utils.review_helper import ReviewData
|
17
|
+
|
18
|
+
# Configure logging
|
19
|
+
logging.basicConfig(level=logging.INFO)
|
20
|
+
logger = logging.getLogger(__name__)
|
21
|
+
|
22
|
+
|
23
|
+
class ZoteroReviewDecision(BaseModel):
|
24
|
+
"""
|
25
|
+
Structured output schema for the human review decision.
|
26
|
+
- decision: "approve", "reject", or "custom"
|
27
|
+
- custom_path: Optional custom collection path if the decision is "custom"
|
28
|
+
"""
|
29
|
+
|
30
|
+
decision: Literal["approve", "reject", "custom"]
|
31
|
+
custom_path: Optional[str] = None
|
32
|
+
|
33
|
+
|
34
|
+
class ZoteroReviewInput(BaseModel):
|
35
|
+
"""Input schema for the Zotero review tool."""
|
36
|
+
|
37
|
+
tool_call_id: Annotated[str, InjectedToolCallId]
|
38
|
+
collection_path: str = Field(
|
39
|
+
description="The path where the paper should be saved in the Zotero library."
|
40
|
+
)
|
41
|
+
state: Annotated[dict, InjectedState]
|
42
|
+
|
43
|
+
|
44
|
+
@tool(args_schema=ZoteroReviewInput, parse_docstring=True)
|
45
|
+
def zotero_review(
|
46
|
+
tool_call_id: Annotated[str, InjectedToolCallId],
|
47
|
+
collection_path: str,
|
48
|
+
state: Annotated[dict, InjectedState],
|
49
|
+
) -> Command[Any]:
|
50
|
+
"""
|
51
|
+
Use this tool to get human review and approval before saving papers to Zotero.
|
52
|
+
This tool should be called before the zotero_write to ensure the user approves
|
53
|
+
the operation.
|
54
|
+
|
55
|
+
Args:
|
56
|
+
tool_call_id (str): The tool call ID.
|
57
|
+
collection_path (str): The Zotero collection path where papers should be saved.
|
58
|
+
state (dict): The state containing previously fetched papers.
|
59
|
+
|
60
|
+
Returns:
|
61
|
+
Command[Any]: The next action to take based on human input.
|
62
|
+
"""
|
63
|
+
logger.info("Requesting human review for saving to collection: %s", collection_path)
|
64
|
+
|
65
|
+
# Use our utility function to fetch papers from state
|
66
|
+
fetched_papers = fetch_papers_for_save(state)
|
67
|
+
|
68
|
+
if not fetched_papers:
|
69
|
+
raise ValueError(
|
70
|
+
"No fetched papers were found to save. "
|
71
|
+
"Please retrieve papers using Zotero Read or Semantic Scholar first."
|
72
|
+
)
|
73
|
+
|
74
|
+
# Create review data object to organize variables
|
75
|
+
review_data = ReviewData(collection_path, fetched_papers, tool_call_id, state)
|
76
|
+
|
77
|
+
try:
|
78
|
+
# Interrupt the graph to get human approval
|
79
|
+
human_review = interrupt(review_data.review_info)
|
80
|
+
# Process human response using structured output via LLM
|
81
|
+
llm_model = state.get("llm_model")
|
82
|
+
if llm_model is None:
|
83
|
+
raise ValueError("LLM model is not available in the state.")
|
84
|
+
structured_llm = llm_model.with_structured_output(ZoteroReviewDecision)
|
85
|
+
# Convert the raw human response to a message for structured parsing
|
86
|
+
decision_response = structured_llm.invoke(
|
87
|
+
[HumanMessage(content=str(human_review))]
|
88
|
+
)
|
89
|
+
|
90
|
+
# Process the structured response
|
91
|
+
if decision_response.decision == "approve":
|
92
|
+
logger.info("User approved saving papers to Zotero")
|
93
|
+
return Command(
|
94
|
+
update={
|
95
|
+
"messages": [
|
96
|
+
ToolMessage(
|
97
|
+
content=review_data.get_approval_message(),
|
98
|
+
tool_call_id=tool_call_id,
|
99
|
+
)
|
100
|
+
],
|
101
|
+
"zotero_write_approval_status": {
|
102
|
+
"collection_path": review_data.collection_path,
|
103
|
+
"approved": True,
|
104
|
+
},
|
105
|
+
}
|
106
|
+
)
|
107
|
+
if decision_response.decision == "custom" and decision_response.custom_path:
|
108
|
+
logger.info(
|
109
|
+
"User approved with custom path: %s", decision_response.custom_path
|
110
|
+
)
|
111
|
+
return Command(
|
112
|
+
update={
|
113
|
+
"messages": [
|
114
|
+
ToolMessage(
|
115
|
+
content=review_data.get_custom_path_approval_message(
|
116
|
+
decision_response.custom_path
|
117
|
+
),
|
118
|
+
tool_call_id=tool_call_id,
|
119
|
+
)
|
120
|
+
],
|
121
|
+
"zotero_write_approval_status": {
|
122
|
+
"collection_path": decision_response.custom_path,
|
123
|
+
"approved": True,
|
124
|
+
},
|
125
|
+
}
|
126
|
+
)
|
127
|
+
logger.info("User rejected saving papers to Zotero")
|
128
|
+
return Command(
|
129
|
+
update={
|
130
|
+
"messages": [
|
131
|
+
ToolMessage(
|
132
|
+
content="Human rejected saving papers to Zotero.",
|
133
|
+
tool_call_id=tool_call_id,
|
134
|
+
)
|
135
|
+
],
|
136
|
+
"zotero_write_approval_status": {"approved": False},
|
137
|
+
}
|
138
|
+
)
|
139
|
+
# pylint: disable=broad-except
|
140
|
+
except Exception as e:
|
141
|
+
# If interrupt or structured output processing fails, fallback to explicit confirmation
|
142
|
+
logger.warning("Structured review processing failed: %s", e)
|
143
|
+
return Command(
|
144
|
+
update={
|
145
|
+
"messages": [
|
146
|
+
ToolMessage(
|
147
|
+
content=(
|
148
|
+
f"REVIEW REQUIRED: Would you like to save "
|
149
|
+
f"{review_data.total_papers} papers to Zotero collection "
|
150
|
+
f"'{review_data.collection_path}'?\n\n"
|
151
|
+
f"Papers to save:\n{review_data.papers_preview}\n\n"
|
152
|
+
"Please respond with 'Yes' to confirm or 'No' to cancel."
|
153
|
+
),
|
154
|
+
tool_call_id=tool_call_id,
|
155
|
+
)
|
156
|
+
],
|
157
|
+
"zotero_write_approval_status": {
|
158
|
+
"collection_path": review_data.collection_path,
|
159
|
+
"papers_reviewed": True,
|
160
|
+
"approved": False, # Not approved yet
|
161
|
+
"papers_count": review_data.total_papers,
|
162
|
+
},
|
163
|
+
}
|
164
|
+
)
|
@@ -1,24 +1,19 @@
|
|
1
1
|
#!/usr/bin/env python3
|
2
2
|
|
3
3
|
"""
|
4
|
-
This tool is used to save fetched papers to Zotero library.
|
4
|
+
This tool is used to save fetched papers to Zotero library after human approval.
|
5
5
|
"""
|
6
6
|
|
7
7
|
import logging
|
8
8
|
from typing import Annotated, Any
|
9
|
-
import hydra
|
10
|
-
from pyzotero import zotero
|
11
9
|
from langchain_core.messages import ToolMessage
|
12
10
|
from langchain_core.tools import tool
|
13
11
|
from langchain_core.tools.base import InjectedToolCallId
|
14
12
|
from langgraph.types import Command
|
15
13
|
from langgraph.prebuilt import InjectedState
|
16
14
|
from pydantic import BaseModel, Field
|
17
|
-
from
|
18
|
-
get_item_collections,
|
19
|
-
)
|
15
|
+
from .utils.write_helper import ZoteroWriteData
|
20
16
|
|
21
|
-
# pylint: disable=R0914,R0912,R0915
|
22
17
|
|
23
18
|
# Configure logging
|
24
19
|
logging.basicConfig(level=logging.INFO)
|
@@ -30,250 +25,66 @@ class ZoteroSaveInput(BaseModel):
|
|
30
25
|
|
31
26
|
tool_call_id: Annotated[str, InjectedToolCallId]
|
32
27
|
collection_path: str = Field(
|
33
|
-
|
34
|
-
description=(
|
35
|
-
"The path where the paper should be saved in the Zotero library."
|
36
|
-
"Example: '/machine/cern/mobile'"
|
37
|
-
),
|
28
|
+
description="The path where the paper should be saved in the Zotero library."
|
38
29
|
)
|
39
30
|
state: Annotated[dict, InjectedState]
|
40
31
|
|
41
32
|
|
42
33
|
@tool(args_schema=ZoteroSaveInput, parse_docstring=True)
|
43
|
-
def
|
34
|
+
def zotero_write(
|
44
35
|
tool_call_id: Annotated[str, InjectedToolCallId],
|
45
36
|
collection_path: str,
|
46
37
|
state: Annotated[dict, InjectedState],
|
47
38
|
) -> Command[Any]:
|
48
39
|
"""
|
49
40
|
Use this tool to save previously fetched papers from Semantic Scholar
|
50
|
-
to a specified Zotero collection.
|
41
|
+
to a specified Zotero collection after human approval.
|
42
|
+
|
43
|
+
This tool checks if the user has approved the save operation via the
|
44
|
+
zotero_review. If approved, it will save the papers to the
|
45
|
+
approved collection path.
|
51
46
|
|
52
47
|
Args:
|
53
48
|
tool_call_id (Annotated[str, InjectedToolCallId]): The tool call ID.
|
54
49
|
collection_path (str): The Zotero collection path where papers should be saved.
|
55
50
|
state (Annotated[dict, InjectedState]): The state containing previously fetched papers.
|
51
|
+
user_confirmation (str, optional): User confirmation message when interrupt is
|
52
|
+
not available.
|
56
53
|
|
57
54
|
Returns:
|
58
55
|
Command[Any]: The save results and related information.
|
59
56
|
"""
|
60
|
-
#
|
61
|
-
|
62
|
-
cfg = hydra.compose(
|
63
|
-
config_name="config", overrides=["tools/zotero_write=default"]
|
64
|
-
)
|
65
|
-
cfg = cfg.tools.zotero_write
|
66
|
-
logger.info("Loaded configuration for Zotero write tool")
|
67
|
-
logger.info(
|
68
|
-
"Saving fetched papers to Zotero under collection path: %s", collection_path
|
69
|
-
)
|
70
|
-
|
71
|
-
# Initialize Zotero client
|
72
|
-
zot = zotero.Zotero(cfg.user_id, cfg.library_type, cfg.api_key)
|
73
|
-
|
74
|
-
# Retrieve last displayed papers from the agent state
|
75
|
-
last_displayed_key = state.get("last_displayed_papers", {})
|
76
|
-
if isinstance(last_displayed_key, str):
|
77
|
-
# If it's a string (key to another state object), get that object
|
78
|
-
fetched_papers = state.get(last_displayed_key, {})
|
79
|
-
logger.info("Using papers from '%s' state key", last_displayed_key)
|
80
|
-
else:
|
81
|
-
# If it's already the papers object
|
82
|
-
fetched_papers = last_displayed_key
|
83
|
-
logger.info("Using papers directly from last_displayed_papers")
|
84
|
-
|
85
|
-
if not fetched_papers:
|
86
|
-
logger.warning("No fetched papers found to save.")
|
87
|
-
raise RuntimeError(
|
88
|
-
"No fetched papers were found to save. Please retry the same query."
|
89
|
-
)
|
90
|
-
|
91
|
-
# First, check if zotero_read exists in state and has collection data
|
92
|
-
zotero_read_data = state.get("zotero_read", {})
|
93
|
-
logger.info("Retrieved zotero_read from state: %d items", len(zotero_read_data))
|
94
|
-
|
95
|
-
# If zotero_read is empty, use get_item_collections as fallback
|
96
|
-
if not zotero_read_data:
|
97
|
-
logger.info(
|
98
|
-
"zotero_read is empty, fetching paths dynamically using get_item_collections"
|
99
|
-
)
|
100
|
-
try:
|
101
|
-
zotero_read_data = get_item_collections(zot)
|
102
|
-
logger.info(
|
103
|
-
"Successfully generated %d path mappings", len(zotero_read_data)
|
104
|
-
)
|
105
|
-
except Exception as e:
|
106
|
-
logger.error("Error generating path mappings: %s", str(e))
|
107
|
-
raise RuntimeError(
|
108
|
-
"Failed to generate collection path mappings. Please retry the same query."
|
109
|
-
) from e
|
110
|
-
|
111
|
-
# Get all collections to find the correct one
|
112
|
-
collections = zot.collections()
|
113
|
-
logger.info("Found %d collections", len(collections))
|
114
|
-
|
115
|
-
# Normalize the requested collection path (remove trailing slash, lowercase for comparison)
|
116
|
-
normalized_path = collection_path.rstrip("/").lower()
|
117
|
-
|
118
|
-
# Find matching collection
|
119
|
-
matched_collection_key = None
|
57
|
+
# Create write data object to organize variables
|
58
|
+
write_data = ZoteroWriteData(tool_call_id, collection_path, state)
|
120
59
|
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
matched_collection_key = key
|
133
|
-
logger.info("Found direct match in zotero_read: %s -> %s", paths, key)
|
134
|
-
break
|
135
|
-
|
136
|
-
# If not found in zotero_read, try matching by collection name
|
137
|
-
if not matched_collection_key:
|
138
|
-
for col in collections:
|
139
|
-
col_name = col["data"]["name"]
|
140
|
-
if f"/{col_name}".lower() == normalized_path:
|
141
|
-
matched_collection_key = col["key"]
|
142
|
-
logger.info(
|
143
|
-
"Found direct match by collection name: %s (key: %s)",
|
144
|
-
col_name,
|
145
|
-
col["key"],
|
146
|
-
)
|
147
|
-
break
|
148
|
-
|
149
|
-
# If still not found, try part-matching
|
150
|
-
if not matched_collection_key:
|
151
|
-
name_to_key = {col["data"]["name"].lower(): col["key"] for col in collections}
|
152
|
-
collection_name = normalized_path.lstrip("/")
|
153
|
-
if collection_name in name_to_key:
|
154
|
-
matched_collection_key = name_to_key[collection_name]
|
155
|
-
logger.info(
|
156
|
-
"Found match by collection name: %s -> %s",
|
157
|
-
collection_name,
|
158
|
-
matched_collection_key,
|
159
|
-
)
|
160
|
-
else:
|
161
|
-
path_parts = normalized_path.strip("/").split("/")
|
162
|
-
for part in path_parts:
|
163
|
-
if part in name_to_key:
|
164
|
-
matched_collection_key = name_to_key[part]
|
165
|
-
logger.info(
|
166
|
-
"Found match by path component: %s -> %s",
|
167
|
-
part,
|
168
|
-
matched_collection_key,
|
60
|
+
try:
|
61
|
+
# Process the write operation
|
62
|
+
results = write_data.process_write()
|
63
|
+
|
64
|
+
return Command(
|
65
|
+
update={
|
66
|
+
"messages": [
|
67
|
+
ToolMessage(
|
68
|
+
content=results["content"],
|
69
|
+
tool_call_id=tool_call_id,
|
70
|
+
artifact=results["fetched_papers"],
|
169
71
|
)
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
if not matched_collection_key:
|
174
|
-
logger.error(
|
175
|
-
"Invalid collection path: %s. No matching collection found in Zotero.",
|
176
|
-
collection_path,
|
177
|
-
)
|
178
|
-
|
179
|
-
available_paths = ", ".join(["/" + col["data"]["name"] for col in collections])
|
180
|
-
raise RuntimeError(
|
181
|
-
f"Error: The collection path '{collection_path}' does not exist in Zotero. "
|
182
|
-
f"Available collections are: {available_paths}"
|
72
|
+
],
|
73
|
+
"zotero_write_approval_status": {}, # Clear approval info
|
74
|
+
}
|
183
75
|
)
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
journal_name = paper.get("Journal Name", "N/A")
|
196
|
-
journal_volume = paper.get("Journal Volume", "N/A")
|
197
|
-
journal_pages = paper.get("Journal Pages", "N/A")
|
198
|
-
|
199
|
-
# Convert Authors list to Zotero format
|
200
|
-
authors = [
|
201
|
-
(
|
202
|
-
{
|
203
|
-
"creatorType": "author",
|
204
|
-
"firstName": name.split(" ")[0],
|
205
|
-
"lastName": " ".join(name.split(" ")[1:]),
|
76
|
+
except ValueError as e:
|
77
|
+
# Only handle collection not found errors with a Command
|
78
|
+
if "collection path" in str(e).lower():
|
79
|
+
return Command(
|
80
|
+
update={
|
81
|
+
"messages": [
|
82
|
+
ToolMessage(
|
83
|
+
content=str(e),
|
84
|
+
tool_call_id=tool_call_id,
|
85
|
+
)
|
86
|
+
],
|
206
87
|
}
|
207
|
-
if " " in name
|
208
|
-
else {"creatorType": "author", "lastName": name}
|
209
88
|
)
|
210
|
-
|
211
|
-
|
212
|
-
]
|
213
|
-
]
|
214
|
-
|
215
|
-
zotero_items.append(
|
216
|
-
{
|
217
|
-
"itemType": "journalArticle",
|
218
|
-
"title": title,
|
219
|
-
"abstractNote": abstract,
|
220
|
-
"date": publication_date, # Now saving full publication date
|
221
|
-
"url": url,
|
222
|
-
"extra": f"Paper ID: {paper_id}\nCitations: {citations}",
|
223
|
-
"collections": [matched_collection_key],
|
224
|
-
"publicationTitle": (
|
225
|
-
publication_venue if publication_venue != "N/A" else venue
|
226
|
-
), # Use publication venue if available
|
227
|
-
"journalAbbreviation": journal_name, # Save Journal Name
|
228
|
-
"volume": (
|
229
|
-
journal_volume if journal_volume != "N/A" else None
|
230
|
-
), # Save Journal Volume
|
231
|
-
"pages": (
|
232
|
-
journal_pages if journal_pages != "N/A" else None
|
233
|
-
), # Save Journal Pages
|
234
|
-
"creators": authors, # Save authors list properly
|
235
|
-
}
|
236
|
-
)
|
237
|
-
|
238
|
-
# Save items to Zotero
|
239
|
-
try:
|
240
|
-
response = zot.create_items(zotero_items)
|
241
|
-
logger.info("Papers successfully saved to Zotero: %s", response)
|
242
|
-
except Exception as e:
|
243
|
-
logger.error("Error saving to Zotero: %s", str(e))
|
244
|
-
raise RuntimeError(f"Error saving papers to Zotero: {str(e)}") from e
|
245
|
-
|
246
|
-
# Get the collection name for better feedback
|
247
|
-
collection_name = ""
|
248
|
-
for col in collections:
|
249
|
-
if col["key"] == matched_collection_key:
|
250
|
-
collection_name = col["data"]["name"]
|
251
|
-
break
|
252
|
-
|
253
|
-
content = (
|
254
|
-
f"Save was successful. Papers have been saved to Zotero collection '{collection_name}' "
|
255
|
-
f"with the requested path '{collection_path}'.\n"
|
256
|
-
)
|
257
|
-
content += "Summary of saved papers:\n"
|
258
|
-
content += f"Number of articles saved: {len(fetched_papers)}\n"
|
259
|
-
content += f"Query: {state.get('query', 'N/A')}\n"
|
260
|
-
top_papers = list(fetched_papers.values())[:2]
|
261
|
-
top_papers_info = "\n".join(
|
262
|
-
[
|
263
|
-
f"{i+1}. {paper.get('Title', 'N/A')} ({paper.get('URL', 'N/A')})"
|
264
|
-
for i, paper in enumerate(top_papers)
|
265
|
-
]
|
266
|
-
)
|
267
|
-
content += "Here are a few of these articles:\n" + top_papers_info
|
268
|
-
|
269
|
-
return Command(
|
270
|
-
update={
|
271
|
-
"messages": [
|
272
|
-
ToolMessage(
|
273
|
-
content=content,
|
274
|
-
tool_call_id=tool_call_id,
|
275
|
-
artifact=fetched_papers,
|
276
|
-
)
|
277
|
-
],
|
278
|
-
}
|
279
|
-
)
|
89
|
+
# Let other ValueErrors (like no papers) propagate up
|
90
|
+
raise
|
@@ -1,6 +1,6 @@
|
|
1
|
-
Metadata-Version: 2.
|
1
|
+
Metadata-Version: 2.4
|
2
2
|
Name: aiagents4pharma
|
3
|
-
Version: 1.30.
|
3
|
+
Version: 1.30.3
|
4
4
|
Summary: AI Agents for drug discovery, drug development, and other pharmaceutical R&D.
|
5
5
|
Classifier: Programming Language :: Python :: 3
|
6
6
|
Classifier: License :: OSI Approved :: MIT License
|
@@ -62,6 +62,7 @@ Requires-Dist: umap-learn==0.5.7
|
|
62
62
|
Requires-Dist: plotly-express==0.4.1
|
63
63
|
Requires-Dist: seaborn==0.13.2
|
64
64
|
Requires-Dist: scanpy==1.11.0
|
65
|
+
Dynamic: license-file
|
65
66
|
|
66
67
|
[](https://github.com/VirtualPatientEngine/AIAgents4Pharma/actions/workflows/tests_talk2biomodels.yml)
|
67
68
|
[](https://github.com/VirtualPatientEngine/AIAgents4Pharma/actions/workflows/tests_talk2cells.yml)
|