aiagents4pharma 1.30.1__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/zotero_agent.py +9 -7
- aiagents4pharma/talk2scholars/configs/agents/talk2scholars/zotero_agent/default.yaml +9 -15
- aiagents4pharma/talk2scholars/configs/tools/zotero_write/default.yaml +55 -0
- aiagents4pharma/talk2scholars/state/state_talk2scholars.py +3 -0
- aiagents4pharma/talk2scholars/tests/test_routing_logic.py +1 -2
- 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 +9 -9
- aiagents4pharma/talk2scholars/tests/test_zotero_write.py +123 -588
- 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 +2 -4
- aiagents4pharma/talk2scholars/tools/zotero/zotero_review.py +198 -0
- aiagents4pharma/talk2scholars/tools/zotero/zotero_write.py +47 -111
- {aiagents4pharma-1.30.1.dist-info → aiagents4pharma-1.30.2.dist-info}/METADATA +3 -2
- {aiagents4pharma-1.30.1.dist-info → aiagents4pharma-1.30.2.dist-info}/RECORD +20 -17
- {aiagents4pharma-1.30.1.dist-info → aiagents4pharma-1.30.2.dist-info}/WHEEL +1 -1
- {aiagents4pharma-1.30.1.dist-info → aiagents4pharma-1.30.2.dist-info/licenses}/LICENSE +0 -0
- {aiagents4pharma-1.30.1.dist-info → aiagents4pharma-1.30.2.dist-info}/top_level.txt +0 -0
@@ -9,6 +9,7 @@ import logging
|
|
9
9
|
# Configure logging
|
10
10
|
logging.basicConfig(level=logging.INFO)
|
11
11
|
logger = logging.getLogger(__name__)
|
12
|
+
# pylint: disable=broad-exception-caught
|
12
13
|
|
13
14
|
|
14
15
|
def get_item_collections(zot):
|
@@ -34,6 +35,7 @@ def get_item_collections(zot):
|
|
34
35
|
|
35
36
|
# Build full paths for collections
|
36
37
|
def build_collection_path(col_key):
|
38
|
+
"""build collection path from collection key"""
|
37
39
|
path = []
|
38
40
|
while col_key:
|
39
41
|
path.insert(0, collection_map.get(col_key, "Unknown"))
|
@@ -61,3 +63,126 @@ def get_item_collections(zot):
|
|
61
63
|
logger.info("Successfully mapped items to collection paths.")
|
62
64
|
|
63
65
|
return item_to_collections
|
66
|
+
|
67
|
+
|
68
|
+
def find_or_create_collection(zot, path, create_missing=False):
|
69
|
+
"""find collection or create if missing"""
|
70
|
+
logger.info(
|
71
|
+
"Finding collection for path: %s (create_missing=%s)", path, create_missing
|
72
|
+
)
|
73
|
+
# Normalize path: remove leading/trailing slashes and convert to lowercase
|
74
|
+
normalized = path.strip("/").lower()
|
75
|
+
path_parts = normalized.split("/") if normalized else []
|
76
|
+
|
77
|
+
if not path_parts:
|
78
|
+
logger.warning("Empty path provided")
|
79
|
+
return None
|
80
|
+
|
81
|
+
# Get all collections from Zotero
|
82
|
+
all_collections = zot.collections()
|
83
|
+
logger.info("Found %d collections in Zotero", len(all_collections))
|
84
|
+
|
85
|
+
# Determine target name (last part) and, if nested, find the parent's key
|
86
|
+
target_name = path_parts[-1]
|
87
|
+
parent_key = None
|
88
|
+
if len(path_parts) > 1:
|
89
|
+
parent_name = path_parts[-2]
|
90
|
+
# Look for a collection with name matching the parent (case-insensitive)
|
91
|
+
for col in all_collections:
|
92
|
+
if col["data"]["name"].lower() == parent_name:
|
93
|
+
parent_key = col["key"]
|
94
|
+
break
|
95
|
+
|
96
|
+
# Try to find an existing collection by direct match (ignoring hierarchy)
|
97
|
+
for col in all_collections:
|
98
|
+
if col["data"]["name"].lower() == target_name:
|
99
|
+
logger.info("Found direct match for %s: %s", target_name, col["key"])
|
100
|
+
return col["key"]
|
101
|
+
|
102
|
+
# No match found: create one if allowed
|
103
|
+
if create_missing:
|
104
|
+
payload = {"name": target_name}
|
105
|
+
if parent_key:
|
106
|
+
payload["parentCollection"] = parent_key
|
107
|
+
try:
|
108
|
+
result = zot.create_collection(payload)
|
109
|
+
# Interpret result based on structure
|
110
|
+
if "success" in result:
|
111
|
+
new_key = result["success"]["0"]
|
112
|
+
else:
|
113
|
+
new_key = result["successful"]["0"]["data"]["key"]
|
114
|
+
logger.info("Created collection %s with key %s", target_name, new_key)
|
115
|
+
return new_key
|
116
|
+
except Exception as e:
|
117
|
+
logger.error("Failed to create collection: %s", e)
|
118
|
+
return None
|
119
|
+
else:
|
120
|
+
logger.warning("No matching collection found for %s", target_name)
|
121
|
+
return None
|
122
|
+
|
123
|
+
|
124
|
+
def get_all_collection_paths(zot):
|
125
|
+
"""
|
126
|
+
Get all available collection paths in Zotero.
|
127
|
+
|
128
|
+
Args:
|
129
|
+
zot (Zotero): An initialized Zotero client.
|
130
|
+
|
131
|
+
Returns:
|
132
|
+
list: List of all available collection paths
|
133
|
+
"""
|
134
|
+
logger.info("Getting all collection paths")
|
135
|
+
collections = zot.collections()
|
136
|
+
|
137
|
+
# Create mappings: collection key → name and collection key → parent key
|
138
|
+
collection_map = {col["key"]: col["data"]["name"] for col in collections}
|
139
|
+
parent_map = {
|
140
|
+
col["key"]: col["data"].get("parentCollection") for col in collections
|
141
|
+
}
|
142
|
+
|
143
|
+
# Build full paths for collections
|
144
|
+
def build_collection_path(col_key):
|
145
|
+
path = []
|
146
|
+
while col_key:
|
147
|
+
path.insert(0, collection_map.get(col_key, "Unknown"))
|
148
|
+
col_key = parent_map.get(col_key)
|
149
|
+
return "/" + "/".join(path)
|
150
|
+
|
151
|
+
collection_paths = [build_collection_path(key) for key in collection_map]
|
152
|
+
logger.info("Found %d collection paths", len(collection_paths))
|
153
|
+
return collection_paths
|
154
|
+
|
155
|
+
|
156
|
+
def fetch_papers_for_save(state):
|
157
|
+
"""
|
158
|
+
Retrieve papers from the state for saving to Zotero.
|
159
|
+
|
160
|
+
Args:
|
161
|
+
state (dict): The state containing previously fetched papers.
|
162
|
+
|
163
|
+
Returns:
|
164
|
+
dict: Dictionary of papers to save, or None if no papers found
|
165
|
+
"""
|
166
|
+
logger.info("Fetching papers from state for saving")
|
167
|
+
|
168
|
+
# Retrieve last displayed papers from the agent state
|
169
|
+
last_displayed_key = state.get("last_displayed_papers", "")
|
170
|
+
|
171
|
+
if not last_displayed_key:
|
172
|
+
logger.warning("No last_displayed_papers key in state")
|
173
|
+
return None
|
174
|
+
|
175
|
+
if isinstance(last_displayed_key, str):
|
176
|
+
# If it's a string (key to another state object), get that object
|
177
|
+
fetched_papers = state.get(last_displayed_key, {})
|
178
|
+
logger.info("Using papers from '%s' state key", last_displayed_key)
|
179
|
+
else:
|
180
|
+
# If it's already the papers object
|
181
|
+
fetched_papers = last_displayed_key
|
182
|
+
logger.info("Using papers directly from last_displayed_papers")
|
183
|
+
|
184
|
+
if not fetched_papers:
|
185
|
+
logger.warning("No fetched papers found to save.")
|
186
|
+
return None
|
187
|
+
|
188
|
+
return fetched_papers
|
@@ -13,9 +13,7 @@ from langchain_core.tools import tool
|
|
13
13
|
from langchain_core.tools.base import InjectedToolCallId
|
14
14
|
from langgraph.types import Command
|
15
15
|
from pydantic import BaseModel, Field
|
16
|
-
from
|
17
|
-
get_item_collections,
|
18
|
-
)
|
16
|
+
from .utils.zotero_path import get_item_collections
|
19
17
|
|
20
18
|
# pylint: disable=R0914,R0912,R0915
|
21
19
|
|
@@ -41,7 +39,7 @@ class ZoteroSearchInput(BaseModel):
|
|
41
39
|
|
42
40
|
|
43
41
|
@tool(args_schema=ZoteroSearchInput, parse_docstring=True)
|
44
|
-
def
|
42
|
+
def zotero_read(
|
45
43
|
query: str,
|
46
44
|
only_articles: bool,
|
47
45
|
tool_call_id: Annotated[str, InjectedToolCallId],
|
@@ -0,0 +1,198 @@
|
|
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
|
+
|
17
|
+
# pylint: disable=R0914,R0911,R0912,R0915
|
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
|
+
# Prepare papers summary for review
|
75
|
+
papers_summary = []
|
76
|
+
for paper_id, paper in list(fetched_papers.items())[
|
77
|
+
:5
|
78
|
+
]: # Limit to 5 papers for readability
|
79
|
+
logger.info("Paper ID: %s", paper_id)
|
80
|
+
title = paper.get("Title", "N/A")
|
81
|
+
authors = ", ".join(
|
82
|
+
[author.split(" (ID: ")[0] for author in paper.get("Authors", [])[:2]]
|
83
|
+
)
|
84
|
+
if len(paper.get("Authors", [])) > 2:
|
85
|
+
authors += " et al."
|
86
|
+
papers_summary.append(f"- {title} by {authors}")
|
87
|
+
|
88
|
+
if len(fetched_papers) > 5:
|
89
|
+
papers_summary.append(f"... and {len(fetched_papers) - 5} more papers")
|
90
|
+
|
91
|
+
papers_preview = "\n".join(papers_summary)
|
92
|
+
total_papers = len(fetched_papers)
|
93
|
+
|
94
|
+
# Prepare review information
|
95
|
+
review_info = {
|
96
|
+
"action": "save_to_zotero",
|
97
|
+
"collection_path": collection_path,
|
98
|
+
"total_papers": total_papers,
|
99
|
+
"papers_preview": papers_preview,
|
100
|
+
"message": (
|
101
|
+
f"Would you like to save {total_papers} papers to Zotero "
|
102
|
+
f"collection '{collection_path}'? Please respond with a structured decision "
|
103
|
+
f"using one of the following options: 'approve', 'reject', or 'custom' "
|
104
|
+
f"(with a custom_path)."
|
105
|
+
),
|
106
|
+
}
|
107
|
+
|
108
|
+
try:
|
109
|
+
# Interrupt the graph to get human approval
|
110
|
+
human_review = interrupt(review_info)
|
111
|
+
# Process human response using structured output via LLM
|
112
|
+
llm_model = state.get("llm_model")
|
113
|
+
if llm_model is None:
|
114
|
+
raise ValueError("LLM model is not available in the state.")
|
115
|
+
structured_llm = llm_model.with_structured_output(ZoteroReviewDecision)
|
116
|
+
# Convert the raw human response to a message for structured parsing
|
117
|
+
decision_response = structured_llm.invoke(
|
118
|
+
[HumanMessage(content=str(human_review))]
|
119
|
+
)
|
120
|
+
|
121
|
+
# Process the structured response
|
122
|
+
if decision_response.decision == "approve":
|
123
|
+
logger.info("User approved saving papers to Zotero")
|
124
|
+
return Command(
|
125
|
+
update={
|
126
|
+
"messages": [
|
127
|
+
ToolMessage(
|
128
|
+
content=(
|
129
|
+
f"Human approved saving {total_papers} papers to Zotero "
|
130
|
+
f"collection '{collection_path}'."
|
131
|
+
),
|
132
|
+
tool_call_id=tool_call_id,
|
133
|
+
)
|
134
|
+
],
|
135
|
+
"zotero_write_approval_status": {
|
136
|
+
"collection_path": collection_path,
|
137
|
+
"approved": True,
|
138
|
+
},
|
139
|
+
}
|
140
|
+
)
|
141
|
+
if decision_response.decision == "custom" and decision_response.custom_path:
|
142
|
+
logger.info(
|
143
|
+
"User approved with custom path: %s", decision_response.custom_path
|
144
|
+
)
|
145
|
+
return Command(
|
146
|
+
update={
|
147
|
+
"messages": [
|
148
|
+
ToolMessage(
|
149
|
+
content=(
|
150
|
+
f"Human approved saving papers to custom Zotero "
|
151
|
+
f"collection '{decision_response.custom_path}'."
|
152
|
+
),
|
153
|
+
tool_call_id=tool_call_id,
|
154
|
+
)
|
155
|
+
],
|
156
|
+
"zotero_write_approval_status": {
|
157
|
+
"collection_path": decision_response.custom_path,
|
158
|
+
"approved": True,
|
159
|
+
},
|
160
|
+
}
|
161
|
+
)
|
162
|
+
logger.info("User rejected saving papers to Zotero")
|
163
|
+
return Command(
|
164
|
+
update={
|
165
|
+
"messages": [
|
166
|
+
ToolMessage(
|
167
|
+
content="Human rejected saving papers to Zotero.",
|
168
|
+
tool_call_id=tool_call_id,
|
169
|
+
)
|
170
|
+
],
|
171
|
+
"zotero_write_approval_status": {"approved": False},
|
172
|
+
}
|
173
|
+
)
|
174
|
+
# pylint: disable=broad-except
|
175
|
+
except Exception as e:
|
176
|
+
# If interrupt or structured output processing fails, fallback to explicit confirmation
|
177
|
+
logger.warning("Structured review processing failed: %s", e)
|
178
|
+
return Command(
|
179
|
+
update={
|
180
|
+
"messages": [
|
181
|
+
ToolMessage(
|
182
|
+
content=(
|
183
|
+
f"REVIEW REQUIRED: Would you like to save {total_papers} papers "
|
184
|
+
f"to Zotero collection '{collection_path}'?\n\n"
|
185
|
+
f"Papers to save:\n{papers_preview}\n\n"
|
186
|
+
"Please respond with 'Yes' to confirm or 'No' to cancel."
|
187
|
+
),
|
188
|
+
tool_call_id=tool_call_id,
|
189
|
+
)
|
190
|
+
],
|
191
|
+
"zotero_write_approval_status": {
|
192
|
+
"collection_path": collection_path,
|
193
|
+
"papers_reviewed": True,
|
194
|
+
"approved": False, # Not approved yet
|
195
|
+
"papers_count": total_papers,
|
196
|
+
},
|
197
|
+
}
|
198
|
+
)
|
@@ -1,7 +1,7 @@
|
|
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
|
@@ -14,11 +14,13 @@ from langchain_core.tools.base import InjectedToolCallId
|
|
14
14
|
from langgraph.types import Command
|
15
15
|
from langgraph.prebuilt import InjectedState
|
16
16
|
from pydantic import BaseModel, Field
|
17
|
-
from
|
18
|
-
|
17
|
+
from .utils.zotero_path import (
|
18
|
+
find_or_create_collection,
|
19
|
+
fetch_papers_for_save,
|
19
20
|
)
|
20
21
|
|
21
|
-
|
22
|
+
|
23
|
+
# pylint: disable=R0914,R0911,R0912,R0915
|
22
24
|
|
23
25
|
# Configure logging
|
24
26
|
logging.basicConfig(level=logging.INFO)
|
@@ -30,29 +32,31 @@ class ZoteroSaveInput(BaseModel):
|
|
30
32
|
|
31
33
|
tool_call_id: Annotated[str, InjectedToolCallId]
|
32
34
|
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
|
-
),
|
35
|
+
description="The path where the paper should be saved in the Zotero library."
|
38
36
|
)
|
39
37
|
state: Annotated[dict, InjectedState]
|
40
38
|
|
41
39
|
|
42
40
|
@tool(args_schema=ZoteroSaveInput, parse_docstring=True)
|
43
|
-
def
|
41
|
+
def zotero_write(
|
44
42
|
tool_call_id: Annotated[str, InjectedToolCallId],
|
45
43
|
collection_path: str,
|
46
44
|
state: Annotated[dict, InjectedState],
|
47
45
|
) -> Command[Any]:
|
48
46
|
"""
|
49
47
|
Use this tool to save previously fetched papers from Semantic Scholar
|
50
|
-
to a specified Zotero collection.
|
48
|
+
to a specified Zotero collection after human approval.
|
49
|
+
|
50
|
+
This tool checks if the user has approved the save operation via the
|
51
|
+
zotero_review. If approved, it will save the papers to the
|
52
|
+
approved collection path.
|
51
53
|
|
52
54
|
Args:
|
53
55
|
tool_call_id (Annotated[str, InjectedToolCallId]): The tool call ID.
|
54
56
|
collection_path (str): The Zotero collection path where papers should be saved.
|
55
57
|
state (Annotated[dict, InjectedState]): The state containing previously fetched papers.
|
58
|
+
user_confirmation (str, optional): User confirmation message when interrupt is
|
59
|
+
not available.
|
56
60
|
|
57
61
|
Returns:
|
58
62
|
Command[Any]: The save results and related information.
|
@@ -71,115 +75,44 @@ def zotero_save_tool(
|
|
71
75
|
# Initialize Zotero client
|
72
76
|
zot = zotero.Zotero(cfg.user_id, cfg.library_type, cfg.api_key)
|
73
77
|
|
74
|
-
#
|
75
|
-
|
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")
|
78
|
+
# Use our utility function to fetch papers from state
|
79
|
+
fetched_papers = fetch_papers_for_save(state)
|
84
80
|
|
85
81
|
if not fetched_papers:
|
86
|
-
|
87
|
-
|
88
|
-
"
|
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"
|
82
|
+
raise ValueError(
|
83
|
+
"No fetched papers were found to save. "
|
84
|
+
"Please retrieve papers using Zotero Read or Semantic Scholar first."
|
99
85
|
)
|
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
86
|
|
111
|
-
#
|
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)
|
87
|
+
# Normalize the requested collection path
|
116
88
|
normalized_path = collection_path.rstrip("/").lower()
|
117
89
|
|
118
|
-
#
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
if isinstance(paths, list):
|
124
|
-
for path in paths:
|
125
|
-
if path.lower() == normalized_path:
|
126
|
-
matched_collection_key = key
|
127
|
-
logger.info(
|
128
|
-
"Found direct match in zotero_read: %s -> %s", path, key
|
129
|
-
)
|
130
|
-
break
|
131
|
-
elif isinstance(paths, str) and paths.lower() == normalized_path:
|
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
|
90
|
+
# Use our utility function to find or optionally create the collection
|
91
|
+
# First try to find the exact collection
|
92
|
+
matched_collection_key = find_or_create_collection(
|
93
|
+
zot, normalized_path, create_missing=False # First try without creating
|
94
|
+
)
|
148
95
|
|
149
|
-
# If still not found, try part-matching
|
150
96
|
if not matched_collection_key:
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
part,
|
168
|
-
matched_collection_key,
|
97
|
+
# Get all collection names without hierarchy for clearer display
|
98
|
+
available_collections = zot.collections()
|
99
|
+
collection_names = [col["data"]["name"] for col in available_collections]
|
100
|
+
names_display = ", ".join(collection_names)
|
101
|
+
|
102
|
+
return Command(
|
103
|
+
update={
|
104
|
+
"messages": [
|
105
|
+
ToolMessage(
|
106
|
+
content=(
|
107
|
+
f"Error: The collection path '{collection_path}' does "
|
108
|
+
f"not exist in Zotero. "
|
109
|
+
f"Available collections are: {names_display}. "
|
110
|
+
f"Please try saving to one of these existing collections."
|
111
|
+
),
|
112
|
+
tool_call_id=tool_call_id,
|
169
113
|
)
|
170
|
-
|
171
|
-
|
172
|
-
# Do not fall back to a default collection: raise error if no match found
|
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}"
|
114
|
+
],
|
115
|
+
}
|
183
116
|
)
|
184
117
|
|
185
118
|
# Format papers for Zotero and assign to the specified collection
|
@@ -244,6 +177,7 @@ def zotero_save_tool(
|
|
244
177
|
raise RuntimeError(f"Error saving papers to Zotero: {str(e)}") from e
|
245
178
|
|
246
179
|
# Get the collection name for better feedback
|
180
|
+
collections = zot.collections()
|
247
181
|
collection_name = ""
|
248
182
|
for col in collections:
|
249
183
|
if col["key"] == matched_collection_key:
|
@@ -266,6 +200,7 @@ def zotero_save_tool(
|
|
266
200
|
)
|
267
201
|
content += "Here are a few of these articles:\n" + top_papers_info
|
268
202
|
|
203
|
+
# Clear the approval info so it's not reused
|
269
204
|
return Command(
|
270
205
|
update={
|
271
206
|
"messages": [
|
@@ -275,5 +210,6 @@ def zotero_save_tool(
|
|
275
210
|
artifact=fetched_papers,
|
276
211
|
)
|
277
212
|
],
|
213
|
+
"zotero_write_approval_status": {}, # Clear approval info
|
278
214
|
}
|
279
215
|
)
|
@@ -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.2
|
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)
|