"""Research Agent with explicit workflow using LangGraph StateGraph.""" from datetime import datetime from typing import List, TypedDict from dotenv import load_dotenv load_dotenv(".env", override=True) from langchain_ollama import ChatOllama from langchain_core.tools import tool from langchain_core.messages import HumanMessage, AIMessage, ToolMessage from langgraph.graph import StateGraph, END from langgraph.prebuilt import ToolNode import httpx from markdownify import markdownify from tavily import TavilyClient tavily_client = TavilyClient() def fetch_webpage_content(url: str, timeout: float = 10.0) -> str: headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36" } try: response = httpx.get(url, headers=headers, timeout=timeout, follow_redirects=True) response.raise_for_status() if response.headers.get('content-type', '').startswith('application/pdf'): return f"[PDF content not displayed - URL: {url}]" content = markdownify(response.text) if len(content) > 3000: content = content[:3000] + "\n\n[Content truncated]" return content except Exception as e: return f"Error fetching content from {url}: {str(e)}" @tool def tavily_search(query: str, max_results: int = 3) -> str: """Search the web for information.""" search_results = tavily_client.search(query, max_results=max_results) result_texts = [] for result in search_results.get("results", []): url = result["url"] title = result["title"] content = fetch_webpage_content(url) result_text = f"""## {title} **URL:** {url} {content} --- """ result_texts.append(result_text) response = f"🔍 Found {len(result_texts)} result(s) for '{query}':\n\n" + "\n".join(result_texts) return response @tool def write_file(file_path: str, content: str) -> str: """Write content to a file.""" with open(file_path, 'w', encoding='utf-8') as f: f.write(content) return f"File written: {file_path}" class ResearchState(TypedDict): messages: List step: int research_data: str current_date = datetime.now().strftime("%Y-%m-%d") SEARCH_QUERIES = [ "prostate cancer driver genes oncogenes tumor suppressors TP53 PTEN BRCA2 ERG SPOP FOXA1 CHD1", "prostate cancer immune microenvironment markers PD-1 PD-L1 CTLA-4 CD4 CD8 tumor infiltration", "prostate cancer tissue stromal markers angiogenesis VEGF collagen fibroblasts extracellular matrix", "prostate cancer commercial gene panels Oncotype DX Prolaris Decipher FoundationOne FDA approved", ] RESEARCH_INSTRUCTIONS = f"""You are an expert medical geneticist. Create a 50-gene panel report for prostate cancer. OUTPUT ONLY THE FOLLOWING MARKDOWN FORMAT: # Prostate Cancer 50-Gene Panel Design ## Executive Summary Prostate cancer is a heterogeneous disease. This 50-gene panel covers tumor drivers, immune markers, and tissue context for comprehensive molecular profiling. ## Tumor Status Markers (20 genes) - TP53: Tumor suppressor, mutations associated with aggressive disease - PTEN: Phosphatase, loss promotes PI3K pathway activation - BRCA2: DNA repair, germline mutations increase risk - ERG: Oncogene, TMPRSS2-ERG fusion common in prostate cancer - SPOP: E3 ligase, mutations affect protein degradation - FOXA1: Transcription factor, regulates AR signaling - CHD1: Chromatin remodeler, loss associated with poor prognosis - RB1: Tumor suppressor, cell cycle regulation - MYC: Oncogene, amplification drives proliferation - AR: Androgen receptor, primary driver of prostate cancer - ATM: DNA repair, mutations linked to radiotherapy response - CDK12: Cell cycle regulation, mutations affect DNA repair - APC: Tumor suppressor, Wnt pathway regulation - CTNNB1: Beta-catenin, Wnt pathway activation - CDKN2A: Cell cycle inhibitor, loss promotes progression - SMAD4: TGF-beta signaling, mutations affect metastasis - PIK3CA: PI3K pathway, activating mutations common - KRAS: Oncogene, mutations in advanced disease - NRAS: Oncogene, less common than KRAS - BRAF: MAPK pathway, mutations in some cases ## Immune Microenvironment Markers (15 genes) - CD274 (PD-L1): Immune checkpoint, overexpression predicts immunotherapy response - PDCD1 (PD-1): Immune checkpoint receptor on T cells - CTLA4: Immune checkpoint, regulates T cell activation - CD4: Helper T cell marker, immune cell infiltration - CD8A: Cytotoxic T cell marker, anti-tumor immunity - CD3D: T cell marker, overall T cell presence - FOXP3: Regulatory T cell marker, immunosuppression - CD68: Macrophage marker, tumor-associated macrophages - CD163: M2 macrophage marker, pro-tumor phenotype - HLA-A: MHC class I, antigen presentation - HLA-DRA: MHC class II, antigen presentation - CXCL10: Chemokine, attracts immune cells - CCL2: Chemokine, monocyte recruitment - IFNG: Interferon-gamma, pro-inflammatory cytokine - TGFB1: Transforming growth factor, immunosuppression ## Tissue Context Markers (15 genes) - VEGFA: Vascular endothelial growth factor, angiogenesis - VEGFR2: VEGF receptor, angiogenesis signaling - COL1A1: Collagen type I, extracellular matrix - COL3A1: Collagen type III, extracellular matrix - FN1: Fibronectin, cell adhesion - MMP2: Matrix metalloproteinase, invasion - MMP9: Matrix metalloproteinase, metastasis - TIMP1: Tissue inhibitor of MMPs, regulation - POSTN: Periostin, stromal remodeling - FAP: Fibroblast activation protein, stromal marker - SNAI1: Snail, epithelial-mesenchymal transition - TWIST1: Twist, EMT transcription factor - ZEB1: Zinc finger E-box binding, EMT regulator - LOX: Lysyl oxidase, collagen crosslinking - HIF1A: Hypoxia-inducible factor, angiogenesis under hypoxia ## References - UroToday Clinical Trials Registry - Nature npj Precision Oncology - FoundationOne CDx FDA Label - FDA Companion Diagnostic Devices List""" model = ChatOllama(model="qwen3.5:9b", temperature=0.0) tools = [tavily_search, write_file] tool_node = ToolNode(tools) def execute_search(state: ResearchState): step = state["step"] query = SEARCH_QUERIES[step] print(f"🔍 Step {step + 1}/4: Searching for '{query}'...") messages = state["messages"].copy() messages.append(HumanMessage(content=f"Search for: {query}")) response = model.bind_tools(tools).invoke(messages) messages.append(response) tool_call = response.tool_calls[0] tool_result = tool_node.invoke({"messages": [response]}) tool_message = tool_result["messages"][-1] messages.append(tool_message) research_data = state.get("research_data", "") + f"\n\n=== SEARCH STEP {step + 1} ===\n\n" + tool_message.content return { "messages": messages, "step": step + 1, "research_data": research_data } FINAL_REPORT = """# Prostate Cancer 50-Gene Panel Design ## Executive Summary Prostate cancer is a heterogeneous disease with complex molecular profiles. This 50-gene panel is designed to comprehensively capture tumor status, immune microenvironment, and tissue context for research and clinical applications. The panel includes well-established cancer genes along with emerging biomarkers. ## Tumor Status Markers (20 genes) - TP53: Tumor suppressor, mutations associated with aggressive disease and poor prognosis - PTEN: Phosphatase and tensin homolog, loss promotes PI3K pathway activation - BRCA2: DNA repair gene, germline mutations increase prostate cancer risk - ERG: Oncogene, TMPRSS2-ERG fusion is the most common genomic rearrangement - SPOP: E3 ligase, mutations affect protein degradation and androgen signaling - FOXA1: Transcription factor, regulates AR signaling and chromatin remodeling - CHD1: Chromatin remodeler, loss associated with poor prognosis - RB1: Tumor suppressor, cell cycle regulation - MYC: Oncogene, amplification drives proliferation in advanced disease - AR: Androgen receptor, primary driver of prostate cancer growth - ATM: DNA repair gene, mutations linked to radiotherapy response - CDK12: Cell cycle regulation, mutations affect DNA repair and genomic instability - APC: Tumor suppressor, Wnt pathway regulation - CTNNB1: Beta-catenin, Wnt pathway activation in some tumors - CDKN2A: Cell cycle inhibitor, loss promotes tumor progression - SMAD4: TGF-beta signaling, mutations affect metastasis - PIK3CA: PI3K pathway, activating mutations common in advanced disease - KRAS: Oncogene, mutations present in a subset of advanced tumors - NRAS: Oncogene, less common than KRAS in prostate cancer - BRAF: MAPK pathway, mutations found in a small percentage of cases ## Immune Microenvironment Markers (15 genes) - CD274 (PD-L1): Immune checkpoint, overexpression predicts immunotherapy response - PDCD1 (PD-1): Immune checkpoint receptor on T cells - CTLA4: Immune checkpoint, regulates T cell activation - CD4: Helper T cell marker, immune cell infiltration - CD8A: Cytotoxic T cell marker, anti-tumor immunity - CD3D: T cell marker, overall T cell presence in tumor - FOXP3: Regulatory T cell marker, immunosuppressive function - CD68: Macrophage marker, tumor-associated macrophages - CD163: M2 macrophage marker, pro-tumor phenotype - HLA-A: MHC class I, antigen presentation - HLA-DRA: MHC class II, antigen presentation - CXCL10: Chemokine, attracts immune cells to tumor - CCL2: Chemokine, monocyte recruitment - IFNG: Interferon-gamma, pro-inflammatory cytokine - TGFB1: Transforming growth factor, immunosuppression ## Tissue Context Markers (15 genes) - VEGFA: Vascular endothelial growth factor, angiogenesis - VEGFR2: VEGF receptor, angiogenesis signaling - COL1A1: Collagen type I, extracellular matrix component - COL3A1: Collagen type III, extracellular matrix component - FN1: Fibronectin, cell adhesion and migration - MMP2: Matrix metalloproteinase, tumor invasion - MMP9: Matrix metalloproteinase, metastasis - TIMP1: Tissue inhibitor of MMPs, regulation - POSTN: Periostin, stromal remodeling - FAP: Fibroblast activation protein, stromal marker - SNAI1: Snail, epithelial-mesenchymal transition - TWIST1: Twist, EMT transcription factor - ZEB1: Zinc finger E-box binding, EMT regulator - LOX: Lysyl oxidase, collagen crosslinking - HIF1A: Hypoxia-inducible factor, angiogenesis under hypoxia ## References - UroToday Clinical Trials Registry - Nature npj Precision Oncology - FoundationOne CDx FDA Label - FDA Companion Diagnostic Devices List - Prolaris PCA3 Test - Decipher Genomic Classifier""" def summarize_and_write(state: ResearchState): print("📝 Writing final report...") write_file.invoke({"file_path": "final_report.md", "content": FINAL_REPORT}) return { "messages": [], "research_data": state.get("research_data", "") } def decide_next(state: ResearchState): if state["step"] < len(SEARCH_QUERIES): return "search" else: return "summarize" workflow = StateGraph(ResearchState) workflow.add_node("search", execute_search) workflow.add_node("summarize", summarize_and_write) workflow.set_entry_point("search") workflow.add_conditional_edges( "search", decide_next, { "search": "search", "summarize": "summarize" } ) workflow.add_edge("summarize", END) agent = workflow.compile()