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.
Files changed (45) hide show
  1. aiagents4pharma/talk2scholars/__init__.py +2 -0
  2. aiagents4pharma/talk2scholars/agents/__init__.py +8 -0
  3. aiagents4pharma/talk2scholars/agents/zotero_agent.py +9 -7
  4. aiagents4pharma/talk2scholars/configs/__init__.py +2 -0
  5. aiagents4pharma/talk2scholars/configs/agents/__init__.py +2 -0
  6. aiagents4pharma/talk2scholars/configs/agents/talk2scholars/__init__.py +2 -0
  7. aiagents4pharma/talk2scholars/configs/agents/talk2scholars/zotero_agent/default.yaml +9 -15
  8. aiagents4pharma/talk2scholars/configs/app/__init__.py +2 -0
  9. aiagents4pharma/talk2scholars/configs/tools/__init__.py +9 -0
  10. aiagents4pharma/talk2scholars/configs/tools/zotero_write/default.yaml +55 -0
  11. aiagents4pharma/talk2scholars/state/__init__.py +4 -2
  12. aiagents4pharma/talk2scholars/state/state_talk2scholars.py +3 -0
  13. aiagents4pharma/talk2scholars/tests/test_routing_logic.py +1 -2
  14. aiagents4pharma/talk2scholars/tests/test_s2_multi.py +10 -8
  15. aiagents4pharma/talk2scholars/tests/test_s2_search.py +9 -5
  16. aiagents4pharma/talk2scholars/tests/test_s2_single.py +7 -7
  17. aiagents4pharma/talk2scholars/tests/test_zotero_agent.py +3 -2
  18. aiagents4pharma/talk2scholars/tests/test_zotero_human_in_the_loop.py +273 -0
  19. aiagents4pharma/talk2scholars/tests/test_zotero_path.py +433 -1
  20. aiagents4pharma/talk2scholars/tests/test_zotero_read.py +57 -43
  21. aiagents4pharma/talk2scholars/tests/test_zotero_write.py +123 -588
  22. aiagents4pharma/talk2scholars/tools/__init__.py +3 -0
  23. aiagents4pharma/talk2scholars/tools/pdf/__init__.py +4 -2
  24. aiagents4pharma/talk2scholars/tools/s2/__init__.py +9 -0
  25. aiagents4pharma/talk2scholars/tools/s2/multi_paper_rec.py +9 -135
  26. aiagents4pharma/talk2scholars/tools/s2/search.py +8 -114
  27. aiagents4pharma/talk2scholars/tools/s2/single_paper_rec.py +8 -126
  28. aiagents4pharma/talk2scholars/tools/s2/utils/__init__.py +7 -0
  29. aiagents4pharma/talk2scholars/tools/s2/utils/multi_helper.py +194 -0
  30. aiagents4pharma/talk2scholars/tools/s2/utils/search_helper.py +175 -0
  31. aiagents4pharma/talk2scholars/tools/s2/utils/single_helper.py +186 -0
  32. aiagents4pharma/talk2scholars/tools/zotero/__init__.py +3 -0
  33. aiagents4pharma/talk2scholars/tools/zotero/utils/__init__.py +5 -0
  34. aiagents4pharma/talk2scholars/tools/zotero/utils/read_helper.py +167 -0
  35. aiagents4pharma/talk2scholars/tools/zotero/utils/review_helper.py +78 -0
  36. aiagents4pharma/talk2scholars/tools/zotero/utils/write_helper.py +197 -0
  37. aiagents4pharma/talk2scholars/tools/zotero/utils/zotero_path.py +126 -1
  38. aiagents4pharma/talk2scholars/tools/zotero/zotero_read.py +10 -139
  39. aiagents4pharma/talk2scholars/tools/zotero/zotero_review.py +164 -0
  40. aiagents4pharma/talk2scholars/tools/zotero/zotero_write.py +40 -229
  41. {aiagents4pharma-1.30.1.dist-info → aiagents4pharma-1.30.3.dist-info}/METADATA +3 -2
  42. {aiagents4pharma-1.30.1.dist-info → aiagents4pharma-1.30.3.dist-info}/RECORD +45 -35
  43. {aiagents4pharma-1.30.1.dist-info → aiagents4pharma-1.30.3.dist-info}/WHEEL +1 -1
  44. {aiagents4pharma-1.30.1.dist-info → aiagents4pharma-1.30.3.dist-info/licenses}/LICENSE +0 -0
  45. {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 aiagents4pharma.talk2scholars.tools.zotero.utils.zotero_path import (
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 zotero_search_tool(
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
- # Load hydra configuration
62
- with hydra.initialize(version_base=None, config_path="../../configs"):
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
- # Initialize Zotero client
76
- zot = zotero.Zotero(cfg.user_id, cfg.library_type, cfg.api_key)
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 aiagents4pharma.talk2scholars.tools.zotero.utils.zotero_path import (
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
- default=None,
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 zotero_save_tool(
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
- # Load hydra configuration
61
- with hydra.initialize(version_base=None, config_path="../../configs"):
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
- # First, try to directly find the collection key in zotero_read data
122
- for key, paths in zotero_read_data.items():
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
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
- break
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}"
72
+ ],
73
+ "zotero_write_approval_status": {}, # Clear approval info
74
+ }
183
75
  )
184
-
185
- # Format papers for Zotero and assign to the specified collection
186
- zotero_items = []
187
- for paper_id, paper in fetched_papers.items():
188
- title = paper.get("Title", "N/A")
189
- abstract = paper.get("Abstract", "N/A")
190
- publication_date = paper.get("Publication Date", "N/A") # Use Publication Date
191
- url = paper.get("URL", "N/A")
192
- citations = paper.get("Citation Count", "N/A")
193
- venue = paper.get("Venue", "N/A")
194
- publication_venue = paper.get("Publication Venue", "N/A")
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
- for name in [
211
- author.split(" (ID: ")[0] for author in paper.get("Authors", [])
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.2
1
+ Metadata-Version: 2.4
2
2
  Name: aiagents4pharma
3
- Version: 1.30.1
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
  [![Talk2BioModels](https://github.com/VirtualPatientEngine/AIAgents4Pharma/actions/workflows/tests_talk2biomodels.yml/badge.svg)](https://github.com/VirtualPatientEngine/AIAgents4Pharma/actions/workflows/tests_talk2biomodels.yml)
67
68
  [![Talk2Cells](https://github.com/VirtualPatientEngine/AIAgents4Pharma/actions/workflows/tests_talk2cells.yml/badge.svg)](https://github.com/VirtualPatientEngine/AIAgents4Pharma/actions/workflows/tests_talk2cells.yml)