Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-13]
os: [ubuntu-latest, macos-15-intel]

steps:
- uses: actions/checkout@v4
Expand Down
34 changes: 30 additions & 4 deletions src/agent/profiles/base.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
from typing import Annotated, TypedDict
from typing import Annotated, Literal, TypedDict

from langchain_core.embeddings import Embeddings
from langchain_core.language_models.chat_models import BaseChatModel
from langchain_core.messages import BaseMessage
from langchain_core.runnables import Runnable, RunnableConfig
from langgraph.graph.message import add_messages

from agent.tasks.detect_language import create_language_detector
from agent.tasks.rephrase import create_rephrase_chain
from agent.tasks.safety_checker import create_safety_checker
from tools.external_search.state import SearchState, WebSearchResult
from tools.external_search.workflow import create_search_workflow

Expand All @@ -28,6 +30,11 @@ class BaseState(InputState, OutputState, total=False):
rephrased_input: str # LLM-generated query from user input
chat_history: Annotated[list[BaseMessage], add_messages]

# Preprocessing results
safety: str # "true" or "false" from safety check
reason_unsafe: str # Reason if unsafe
detected_language: str # Detected language


class BaseGraphBuilder:
# NOTE: Anything that is common to all graph builders goes here
Expand All @@ -38,21 +45,40 @@ def __init__(
embedding: Embeddings,
) -> None:
self.rephrase_chain: Runnable = create_rephrase_chain(llm)
self.safety_checker: Runnable = create_safety_checker(llm)
self.language_detector: Runnable = create_language_detector(llm)
self.search_workflow: Runnable = create_search_workflow(llm)

async def preprocess(self, state: BaseState, config: RunnableConfig) -> BaseState:
rephrased_input: str = await self.rephrase_chain.ainvoke(
{
"user_input": state["user_input"],
"chat_history": state["chat_history"],
"chat_history": state.get("chat_history", []),
},
config,
)
return BaseState(rephrased_input=rephrased_input)
safety_check: BaseState = await self.safety_checker.ainvoke(
{"rephrased_input": rephrased_input}, config
)
detected_language: str = await self.language_detector.ainvoke(
{"user_input": state["user_input"]}, config
)
return BaseState(
rephrased_input=rephrased_input,
safety=safety_check["safety"],
reason_unsafe=safety_check["reason_unsafe"],
detected_language=detected_language,
)

def proceed_with_research(self, state: BaseState) -> Literal["Continue", "Finish"]:
return "Continue" if state["safety"] == "true" else "Finish"

async def postprocess(self, state: BaseState, config: RunnableConfig) -> BaseState:
search_results: list[WebSearchResult] = []
if config["configurable"]["enable_postprocess"]:
if (
config["configurable"].get("enable_postprocess")
and state["safety"] == "true"
):
result: SearchState = await self.search_workflow.ainvoke(
SearchState(
input=state["rephrased_input"],
Expand Down
27 changes: 3 additions & 24 deletions src/agent/profiles/cross_database.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,11 @@
create_uniprot_rewriter_w_reactome
from agent.tasks.cross_database.summarize_reactome_uniprot import \
create_reactome_uniprot_summarizer
from agent.tasks.detect_language import create_language_detector
from agent.tasks.safety_checker import SafetyCheck, create_safety_checker
from retrievers.reactome.rag import create_reactome_rag
from retrievers.uniprot.rag import create_uniprot_rag


class CrossDatabaseState(BaseState):
safety: str # LLM-assessed safety level of the user input
query_language: str # language of the user input

reactome_query: str # LLM-generated query for Reactome
reactome_answer: str # LLM-generated answer from Reactome
reactome_completeness: str # LLM-assessed completeness of the Reactome answer
Expand All @@ -46,9 +41,7 @@ def __init__(
self.reactome_rag: Runnable = create_reactome_rag(llm, embedding)
self.uniprot_rag: Runnable = create_uniprot_rag(llm, embedding)

self.safety_checker = create_safety_checker(llm)
self.completeness_checker = create_completeness_grader(llm)
self.detect_language = create_language_detector(llm)
self.write_reactome_query = create_reactome_rewriter_w_uniprot(llm)
self.write_uniprot_query = create_uniprot_rewriter_w_reactome(llm)
self.summarize_final_answer = create_reactome_uniprot_summarizer(
Expand All @@ -74,7 +67,6 @@ def __init__(
state_graph.add_node("postprocess", self.postprocess)
# Set up edges
state_graph.set_entry_point("preprocess_question")
state_graph.add_edge("preprocess_question", "identify_query_language")
state_graph.add_edge("preprocess_question", "check_question_safety")
state_graph.add_conditional_edges(
"check_question_safety",
Expand Down Expand Up @@ -104,31 +96,18 @@ def __init__(

self.uncompiled_graph: StateGraph = state_graph

async def check_question_safety(
def check_question_safety(
self, state: CrossDatabaseState, config: RunnableConfig
) -> CrossDatabaseState:
result: SafetyCheck = await self.safety_checker.ainvoke(
{"input": state["rephrased_input"]},
config,
)
if result.binary_score == "No":
if state["safety"] != "true":
inappropriate_input = f"This is the user's question and it is NOT appropriate for you to answer: {state["user_input"]}. \n\n explain that you are unable to answer the question but you can answer questions about topics related to the Reactome Pathway Knowledgebase or UniProt Knowledgebas."
return CrossDatabaseState(
safety=result.binary_score,
user_input=inappropriate_input,
reactome_answer="",
uniprot_answer="",
)
else:
return CrossDatabaseState(safety=result.binary_score)

async def proceed_with_research(
self, state: CrossDatabaseState
) -> Literal["Continue", "Finish"]:
if state["safety"] == "Yes":
return "Continue"
else:
return "Finish"
return CrossDatabaseState()

async def identify_query_language(
self, state: CrossDatabaseState, config: RunnableConfig
Expand Down
29 changes: 28 additions & 1 deletion src/agent/profiles/react_to_me.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from langgraph.graph.state import StateGraph

from agent.profiles.base import BaseGraphBuilder, BaseState
from agent.tasks.unsafe_question import create_unsafe_answer_generator
from retrievers.reactome.rag import create_reactome_rag


Expand All @@ -23,6 +24,7 @@ def __init__(
super().__init__(llm, embedding)

# Create runnables (tasks & tools)
self.unsafe_answer_generator = create_unsafe_answer_generator(llm)
self.reactome_rag: Runnable = create_reactome_rag(
llm, embedding, streaming=True
)
Expand All @@ -32,15 +34,40 @@ def __init__(
# Set up nodes
state_graph.add_node("preprocess", self.preprocess)
state_graph.add_node("model", self.call_model)
state_graph.add_node("generate_unsafe_response", self.generate_unsafe_response)
state_graph.add_node("postprocess", self.postprocess)
# Set up edges
state_graph.set_entry_point("preprocess")
state_graph.add_edge("preprocess", "model")
state_graph.add_conditional_edges(
"preprocess",
self.proceed_with_research,
{"Continue": "model", "Finish": "generate_unsafe_response"},
)
state_graph.add_edge("model", "postprocess")
state_graph.add_edge("generate_unsafe_response", "postprocess")
state_graph.set_finish_point("postprocess")

self.uncompiled_graph: StateGraph = state_graph

async def generate_unsafe_response(
self, state: ReactToMeState, config: RunnableConfig
) -> ReactToMeState:
answer: str = await self.unsafe_answer_generator.ainvoke(
{
"language": state["detected_language"],
"user_input": state["rephrased_input"],
"reason_unsafe": state["reason_unsafe"],
},
config,
)
return ReactToMeState(
chat_history=[
HumanMessage(state["user_input"]),
AIMessage(answer),
],
answer=answer,
)

async def call_model(
self, state: ReactToMeState, config: RunnableConfig
) -> ReactToMeState:
Expand Down
53 changes: 45 additions & 8 deletions src/agent/tasks/safety_checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,62 @@
from pydantic import BaseModel, Field

safety_check_message = """
You are an expert grader with a strong understanding of ethical guidelines and responsible AI usage.
You are an expert scientific assistant. You have advanced training in scientific ethics, dual-use assessment, and responsible AI.

Your task is to determine whether a user-submitted question is appropriate and not harmful.
Provide a binary output as either:
Yes: The question is appropriate and not harmful.
No: The question is harmful, offensive, inappropriate, or unethical.
Tasks:

1. Safety & Appropriateness Check
- Assess the question and determine if it is safe and appropriate for responsible AI use.
- The question and any plausible answer to it must NOT be harmful, offensive, unethical, or violate scientific or ethical guidelines.
- The question or its likely answers must NOT request, describe, or explore how to carry out harmful, illegal, dangerous, or dual-use activities (such as modifying pathogens, increasing infectivity, or other forms of misuse)—even if the scenario is hypothetical, fictional, or indirect.
- The question must NOT request personal, medical, legal, or other types of advice outside the scope of scientific knowledge.
- If a question is ambiguous or could plausibly be used for harm, treat it as unsafe.
- Treat hypothetical, fictional, or made-up scenarios with the same level of scrutiny as real-world questions.

2. Reactome Relevance Check
- Determine if the question is relevant to biology, life sciences, molecular biology, or related topics.
- Mark questions as not relevant if they are about unrelated topics (such as programming, math, history, trivia, etc.).

IMPORTANT:
- If the standalone question is unsafe or not relevant return "safety": "false".
- If the standalone question is both safe **and** relevant, return "safety": "true".

Return only a JSON object in the following format:
"safety": "true" or "false", // Use string, not boolean. Mark as "false" if unsafe OR not relevant.
"reason_unsafe": "..." // If 'safety' is false, briefly state the reason

Examples:
1. Q: Describe methods for bypassing cellular immune responses to viral infection
"safety": "false",
"reason_unsafe": "Could be used for harmful or unethical manipulation of immune evasion."

2. Q: What are common side effects of statins in the treatment of high cholesterol?
"safety": "false",
"reason_unsafe": "Requests medical advice, which is out of scientific scope."

3. Q: How can gene drives be used to eliminate invasive species in an ecosystem?
"safety": "false",
"reason_unsafe": "Enables the use of gene drives, which have significant ecological and dual-use risks."

4. Q: What is the role of the immune system in the treatment of cancer?
"safety": "true",
"reason_unsafe": ""
"""

safety_check_prompt = ChatPromptTemplate.from_messages(
[
("system", safety_check_message),
("human", "User question: \n\n {input}"),
("human", "User question: \n\n {rephrased_input}"),
]
)


class SafetyCheck(BaseModel):
binary_score: str = Field(
description="Indicates whether the question is appropriate and related to molecular biology. Expected values: 'Yes' or 'No'."
safety: str = Field(
description="Indicates whether the question is appropriate and related to molecular biology. Expected values: 'true' or 'false'."
)
reason_unsafe: str = Field(
description="If 'safety' is false, briefly state the reason; if 'safety' is true, leave this field empty."
)


Expand Down
41 changes: 41 additions & 0 deletions src/agent/tasks/unsafe_question.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from langchain_core.language_models.chat_models import BaseChatModel
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import Runnable

safety_check_message = """
You are an expert scientific assistant operating under the React-to-Me platform. React-to-Me helps both experts and non-experts explore molecular biology using trusted data from the Reactome database.

You have advanced training in scientific ethics, dual-use research concerns, and responsible AI use.

You will receive three inputs:
1. The user's question.
2. A system-generated variable called `reason_unsafe`, which explains why the question cannot be answered.
3. The user's preferred language (as a language code or name).

Your task is to clearly, respectfully, and firmly explain to the user *why* their question cannot be answered, based solely on the `reason_unsafe` input. Do **not** attempt to answer, rephrase, or guide the user toward answering the original question.

You must:
- Respond in the user’s preferred language.
- Politely explain the refusal, grounded in the `reason_unsafe`.
- Emphasize React-to-Me’s mission: to support responsible exploration of molecular biology through trusted databases.
- Suggest examples of appropriate topics (e.g., protein function, pathways, gene interactions using Reactome/UniProt).

You must not provide any workaround, implicit answer, or redirection toward unsafe content.
"""

safety_check_prompt = ChatPromptTemplate.from_messages(
[
("system", safety_check_message),
(
"user",
"Language:{language}\n\nQuestion:{user_input}\n\n Reason for unsafe or out of scope: {reason_unsafe}",
),
]
)


def create_unsafe_answer_generator(llm: BaseChatModel) -> Runnable:
return (safety_check_prompt | llm | StrOutputParser()).with_config(
run_name="unsafe_answer_generator"
)
43 changes: 26 additions & 17 deletions src/retrievers/reactome/prompt.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,34 @@
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder

reactome_system_prompt = """
You are an expert in molecular biology with access to the Reactome Knowledgebase.
Your primary responsibility is to answer the user's questions comprehensively, accurately, and in an engaging manner, based strictly on the context provided from the Reactome Knowledgebase.
Provide any useful background information required to help the user better understand the significance of the answer.
Always provide citations and links to the documents you obtained the information from.
You are an expert in molecular biology with access to the **Reactome Knowledgebase**.
Your primary responsibility is to answer the user's questions **comprehensively, mechanistically, and with precision**, drawing strictly from the **Reactome Knowledgebase**.

When providing answers, please adhere to the following guidelines:
1. Provide answers **strictly based on the given context from the Reactome Knowledgebase**. Do **not** use or infer information from any external sources.
2. If the answer cannot be derived from the context provided, do **not** answer the question; instead explain that the information is not currently available in Reactome.
3. Answer the question comprehensively and accurately, providing useful background information based **only** on the context.
4. keep track of **all** the sources that are directly used to derive the final answer, ensuring **every** piece of information in your response is **explicitly cited**.
5. Create Citations for the sources used to generate the final asnwer according to the following:
- For Reactome always format citations in the following format: <a href="url">*Source_Name*</a>, where *Source_Name* is the name of the retrieved document.
Examples:
- <a href="https://reactome.org/content/detail/R-HSA-109581">Apoptosis</a>
- <a href="https://reactome.org/content/detail/R-HSA-1640170">Cell Cycle</a>
Your output must emphasize biological processes, molecular complexes, regulatory mechanisms, and interactions most relevant to the user’s question.
Provide an information-rich narrative that explains not only what is happening but also how and why, based only on Reactome context.

6. Always provide the citations you created in the format requested, in point-form at the end of the response paragraph, ensuring **every piece of information** provided in the final answer is cited.
7. Write in a conversational and engaging tone suitable for a chatbot.
8. Use clear, concise language to make complex topics accessible to a wide audience.

## **Answering Guidelines**
1. Strict source discipline: Use only the information explicitly provided from Reactome. Do not invent, infer, or draw from external knowledge.
- Use only information directly found in Reactome.
- Do **not** supplement, infer, generalize, or assume based on external biological knowledge.
- If no relevant information exists in Reactome, explain the information is not currently available in Reactome. Do **not** answer the question.
2. Inline citations required: Every factual statement must include ≥1 inline anchor citation in the format: <a href="URL">display_name</a>
- If multiple entries support the same fact, cite them together (space-separated).
3. Comprehensiveness: Capture all mechanistically relevant details available in Reactome, focusing on processes, complexes, regulations, and interactions.
4. Tone & Style:
- Write in a clear, engaging, and conversational tone.
- Use accessible language while maintaining technical precision.
- Ensure the narrative flows logically, presenting background, mechanisms, and significance
5. Source list at the end: After the main narrative, provide a bullet-point list of each unique citation anchor exactly once, in the same <a href="URL">Node Name</a> format.
- Examples:
- <a href="https://reactome.org/content/detail/R-HSA-109581">Apoptosis</a>
- <a href="https://reactome.org/content/detail/R-HSA-1640170">Cell Cycle</a>

## Internal QA (silent)
- All factual claims are cited correctly.
- No unverified claims or background knowledge are added.
- The Sources list is complete and de-duplicated.
"""

reactome_qa_prompt = ChatPromptTemplate.from_messages(
Expand Down
Loading