Skip to content

Zotero Review

This tool implements human-in-the-loop review for Zotero write operations.

ZoteroReviewDecision

Bases: BaseModel

Structured output schema for the human review decision. - decision: "approve", "reject", or "custom" - custom_path: Optional custom collection path if the decision is "custom"

Source code in aiagents4pharma/talk2scholars/tools/zotero/zotero_review.py
23
24
25
26
27
28
29
30
31
class ZoteroReviewDecision(BaseModel):
    """
    Structured output schema for the human review decision.
    - decision: "approve", "reject", or "custom"
    - custom_path: Optional custom collection path if the decision is "custom"
    """

    decision: Literal["approve", "reject", "custom"]
    custom_path: Optional[str] = None

ZoteroReviewInput

Bases: BaseModel

Input schema for the Zotero review tool.

Source code in aiagents4pharma/talk2scholars/tools/zotero/zotero_review.py
34
35
36
37
38
39
40
41
class ZoteroReviewInput(BaseModel):
    """Input schema for the Zotero review tool."""

    tool_call_id: Annotated[str, InjectedToolCallId]
    collection_path: str = Field(
        description="The path where the paper should be saved in the Zotero library."
    )
    state: Annotated[dict, InjectedState]

zotero_review(tool_call_id, collection_path, state)

Use this tool to get human review and approval before saving papers to Zotero. This tool should be called before the zotero_write to ensure the user approves the operation.

Parameters:

Name Type Description Default
tool_call_id str

The tool call ID.

required
collection_path str

The Zotero collection path where papers should be saved.

required
state dict

The state containing previously fetched papers.

required

Returns:

Type Description
Command[Any]

Command[Any]: The next action to take based on human input.

Source code in aiagents4pharma/talk2scholars/tools/zotero/zotero_review.py
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
@tool(args_schema=ZoteroReviewInput, parse_docstring=True)
def zotero_review(
    tool_call_id: Annotated[str, InjectedToolCallId],
    collection_path: str,
    state: Annotated[dict, InjectedState],
) -> Command[Any]:
    """
    Use this tool to get human review and approval before saving papers to Zotero.
    This tool should be called before the zotero_write to ensure the user approves
    the operation.

    Args:
        tool_call_id (str): The tool call ID.
        collection_path (str): The Zotero collection path where papers should be saved.
        state (dict): The state containing previously fetched papers.

    Returns:
        Command[Any]: The next action to take based on human input.
    """
    logger.info("Requesting human review for saving to collection: %s", collection_path)

    # Use our utility function to fetch papers from state
    fetched_papers = fetch_papers_for_save(state)

    if not fetched_papers:
        raise ValueError(
            "No fetched papers were found to save. "
            "Please retrieve papers using Zotero Read or Semantic Scholar first."
        )

    # Create review data object to organize variables
    review_data = ReviewData(collection_path, fetched_papers, tool_call_id, state)

    try:
        # Interrupt the graph to get human approval
        human_review = interrupt(review_data.review_info)
        # Process human response using structured output via LLM
        llm_model = state.get("llm_model")
        if llm_model is None:
            raise ValueError("LLM model is not available in the state.")
        structured_llm = llm_model.with_structured_output(ZoteroReviewDecision)
        # Convert the raw human response to a message for structured parsing
        decision_response = structured_llm.invoke(
            [HumanMessage(content=str(human_review))]
        )

        # Process the structured response
        if decision_response.decision == "approve":
            logger.info("User approved saving papers to Zotero")
            return Command(
                update={
                    "messages": [
                        ToolMessage(
                            content=review_data.get_approval_message(),
                            tool_call_id=tool_call_id,
                        )
                    ],
                    "zotero_write_approval_status": {
                        "collection_path": review_data.collection_path,
                        "approved": True,
                    },
                }
            )
        if decision_response.decision == "custom" and decision_response.custom_path:
            logger.info(
                "User approved with custom path: %s", decision_response.custom_path
            )
            return Command(
                update={
                    "messages": [
                        ToolMessage(
                            content=review_data.get_custom_path_approval_message(
                                decision_response.custom_path
                            ),
                            tool_call_id=tool_call_id,
                        )
                    ],
                    "zotero_write_approval_status": {
                        "collection_path": decision_response.custom_path,
                        "approved": True,
                    },
                }
            )
        logger.info("User rejected saving papers to Zotero")
        return Command(
            update={
                "messages": [
                    ToolMessage(
                        content="Human rejected saving papers to Zotero.",
                        tool_call_id=tool_call_id,
                    )
                ],
                "zotero_write_approval_status": {"approved": False},
            }
        )
        # pylint: disable=broad-except
    except Exception as e:
        # If interrupt or structured output processing fails, fallback to explicit confirmation
        logger.warning("Structured review processing failed: %s", e)
        return Command(
            update={
                "messages": [
                    ToolMessage(
                        content=(
                            f"REVIEW REQUIRED: Would you like to save "
                            f"{review_data.total_papers} papers to Zotero collection "
                            f"'{review_data.collection_path}'?\n\n"
                            f"Papers to save:\n{review_data.papers_preview}\n\n"
                            "Please respond with 'Yes' to confirm or 'No' to cancel."
                        ),
                        tool_call_id=tool_call_id,
                    )
                ],
                "zotero_write_approval_status": {
                    "collection_path": review_data.collection_path,
                    "papers_reviewed": True,
                    "approved": False,  # Not approved yet
                    "papers_count": review_data.total_papers,
                },
            }
        )