from __future__ import annotations import html import os import sys import tempfile from pathlib import Path sys.path.insert(0, str(Path(__file__).parent)) import streamlit as st from dotenv import load_dotenv load_dotenv() st.set_page_config( page_title="Biotech Agentic Analyst", page_icon="🔬", layout="wide", initial_sidebar_state="expanded", ) from mistral_ocr_pipeline.pipeline import ( run_science_pipeline, parse_pipeline_results, ) from flow.science_flow import ScienceFlow from flow.state import ScienceFlowState from models import ExtractedFigure from utils import decode_thumbnail # --------------------------------------------------------------------------- # Session state initialisation # --------------------------------------------------------------------------- def _reset_analysis_metrics(*, increment_uploader: bool = False) -> None: st.session_state.flow_state = None st.session_state.page_count = 0 st.session_state.quality_score = None st.session_state.pipeline_steps = [] if increment_uploader: st.session_state.uploader_key += 1 for _key, _default in [ ("flow_state", None), ("page_count", 0), ("quality_score", None), ("pipeline_steps", []), ("uploader_key", 0), ]: if _key not in st.session_state: st.session_state[_key] = _default # --------------------------------------------------------------------------- # Mistral client — cached across reruns # --------------------------------------------------------------------------- @st.cache_resource def init_mistral_client(): api_key = os.getenv("MISTRAL_API_KEY") if not api_key: return None, "MISTRAL_API_KEY not set in environment" try: from mistralai.client import Mistral client = Mistral(api_key=api_key) return client, True except Exception as exc: return None, str(exc) # --------------------------------------------------------------------------- # PDF preview — cached by file content so re-renders are instant # --------------------------------------------------------------------------- @st.cache_data(show_spinner=False) def _render_pdf_pages(pdf_bytes: bytes) -> list[bytes]: import fitz doc = fitz.open(stream=pdf_bytes, filetype="pdf") mat = fitz.Matrix(2.0, 2.0) pages = [doc[i].get_pixmap(matrix=mat).tobytes("png") for i in range(len(doc))] doc.close() return pages def _render_pdf_preview(pdf_bytes: bytes) -> None: try: pages = _render_pdf_pages(pdf_bytes) for i, page_png in enumerate(pages): st.image( page_png, caption=f"Page {i + 1} of {len(pages)}", width="stretch", ) except Exception: st.caption("PDF preview unavailable") # --------------------------------------------------------------------------- # Rendering helpers # --------------------------------------------------------------------------- _TAG_COLORS = [ ("#dbeafe", "#1d4ed8"), ("#dcfce7", "#166534"), ("#fef9c3", "#854d0e"), ("#fee2e2", "#991b1b"), ] def _pill(text: str, bg: str, fg: str) -> str: return ( f'{html.escape(text)}' ) def _axis_pill(label: str, value: str) -> str: """Labeled axis pill with a distinct prefix badge (x / y / n).""" return ( f'' f'{label}' f' {html.escape(value)}' f"" ) def _section_label(text: str) -> str: return ( f'
{text}
' ) def _render_thumbnail_strip(figures: list[ExtractedFigure]) -> None: seen: set[int] = set() for fig in figures: if fig.page_number in seen or not fig.thumbnail_b64: continue seen.add(fig.page_number) img = decode_thumbnail(fig.thumbnail_b64) if img: st.image(img, caption=f"p. {fig.page_number}", width="stretch") def _render_figure_intelligence_tab(state: ScienceFlowState) -> None: if not state.figure_intelligences: st.info("No figure intelligence available.") return extracted_lookup: dict[str, ExtractedFigure] = { f.figure_id: f for f in state.figures } shown = [ fi for fi in state.figure_intelligences if fi.is_quantitative_chart and fi.passes_sanity_check ] discarded = len(state.figure_intelligences) - len(shown) if discarded: st.caption( f"Showing {len(shown)} of {len(state.figure_intelligences)} figures — " f"{discarded} discarded (not a data chart, or failed the image/caption " "sanity check)." ) if not shown: st.info("No figures passed the chart-type / sanity checks.") return total = len(shown) for idx, fi in enumerate(shown): extracted = extracted_lookup.get(fi.figure_id) page_num = extracted.page_number if extracted else "?" title = extracted.title if (extracted and extracted.title) else fi.figure_id with st.expander(f"Figure {idx + 1} of {total}", expanded=(idx == 0)): col_title, col_badge = st.columns([8, 1]) with col_title: st.markdown(f"**{title}**") with col_badge: st.markdown( f'
' f'{_pill(f"p. {page_num}", "#f3f4f6", "#374151")}' f"
", unsafe_allow_html=True, ) chart_types = [ct.strip() for ct in fi.chart_type.split(",") if ct.strip()] pills_html = " ".join( _pill(ct, *_TAG_COLORS[i % len(_TAG_COLORS)]) for i, ct in enumerate(chart_types[:3]) ) st.markdown( f"{pills_html}  {html.escape(fi.key_finding)}", unsafe_allow_html=True, ) st.markdown("") col_img, col_meta = st.columns([1, 2]) with col_img: if extracted and extracted.thumbnail_b64: img = decode_thumbnail(extracted.thumbnail_b64) if img: st.image(img, width="stretch") else: st.markdown("*No preview*") else: st.markdown("*No preview*") with col_meta: axis_html_parts = [] if extracted and extracted.x_axis: axis_html_parts.append(_axis_pill("x", extracted.x_axis)) if extracted and extracted.y_axis: axis_html_parts.append(_axis_pill("y", extracted.y_axis)) if axis_html_parts: st.markdown( _section_label("Axes") + " ".join(axis_html_parts), unsafe_allow_html=True, ) if fi.variables_compared: vars_html = " ".join( _pill(v, "#f3f4f6", "#374151") for v in fi.variables_compared[:4] ) st.markdown( _section_label("Groups / Variables") + vars_html, unsafe_allow_html=True, ) if fi.knowledge_base_tags: tags_html = " ".join( _pill(tag, *_TAG_COLORS[i % len(_TAG_COLORS)]) for i, tag in enumerate(fi.knowledge_base_tags) ) st.markdown( _section_label("Keywords") + tags_html, unsafe_allow_html=True, ) if fi.quantitative_highlights: highlights_html = " ".join( _pill(h, "#f3f4f6", "#374151") for h in fi.quantitative_highlights[:4] ) st.markdown( _section_label("Quantitative Highlights") + highlights_html, unsafe_allow_html=True, ) st.markdown(_section_label("Significance"), unsafe_allow_html=True) st.markdown(fi.biological_significance) # --------------------------------------------------------------------------- # Sidebar # --------------------------------------------------------------------------- with st.sidebar: st.title("đź“„ Upload Document") uploaded_file = st.file_uploader( "Drag and drop your scientific PDF here", type=["pdf"], key=f"pdf_uploader_{st.session_state.uploader_key}", ) if uploaded_file is not None: st.caption(f"**{uploaded_file.name}**") with st.container(height=420, border=True): _render_pdf_preview(bytes(uploaded_file.getbuffer())) client, client_err = init_mistral_client() if client is not None: st.success("âś“ Analysis ready to run") else: st.error(f"Pipeline unavailable: {client_err}") analyze_button = st.button( "Run Analysis 🔍", disabled=(uploaded_file is None or client is None), type="primary", width="stretch", ) # --------------------------------------------------------------------------- # Main panel — header + clear button # --------------------------------------------------------------------------- col_header, col_clear = st.columns([9, 1]) with col_header: st.header("🔬 Biotech Agentic Analyst") powered_by_html = """
Powered by and
""" st.markdown(powered_by_html, unsafe_allow_html=True) st.markdown("") with col_clear: if st.button("Clear ↺", width="stretch"): _reset_analysis_metrics(increment_uploader=True) st.rerun() # --------------------------------------------------------------------------- # Main panel — analysis execution # --------------------------------------------------------------------------- if analyze_button and uploaded_file is not None and client is not None: _reset_analysis_metrics() tmp_path: str | None = None pipeline_results: dict | None = None try: with tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") as tmp_file: tmp_file.write(uploaded_file.getbuffer()) tmp_path = tmp_file.name with st.status( "Running Document Processing...", expanded=True ) as status_pipeline: st.write("Uploading and running Mistral OCR 4 with Document AI extraction…") try: from mistralai.client.errors import SDKError pipeline_results = run_science_pipeline(client, tmp_path) figures, page_count, quality_score = parse_pipeline_results( pipeline_results ) st.session_state.page_count = page_count st.session_state.quality_score = quality_score st.write(f"Extracted {len(figures)} figures from {page_count} pages") status_pipeline.update( label="Document processing completed", state="complete", expanded=False, ) st.session_state.pipeline_steps = [] except SDKError as exc: status_pipeline.update(label="Mistral OCR request failed", state="error") st.error(f"Mistral OCR error: {exc}") st.stop() except RuntimeError as exc: status_pipeline.update(label="Result parsing failed", state="error") st.error(str(exc)) if pipeline_results is not None: raw = getattr(pipeline_results.get("ocr"), "document_annotation", None) if raw is not None: with st.expander("Raw document_annotation (for debugging)"): st.code(str(raw)[:4000], language="json") st.stop() if not figures: st.warning( "No figures were extracted from this document. " "The PDF may not contain charts or the extraction quality was too low." ) if pipeline_results is not None: raw = getattr(pipeline_results.get("ocr"), "document_annotation", None) if raw is not None: with st.expander("Debug: raw document_annotation"): st.code(str(raw)[:4000], language="json") st.stop() with st.status("Analyzing Figures...", expanded=True) as status_analyst: st.write(f"Running Agent analysis over {len(figures)} figures…") flow = ScienceFlow() flow.state.figures = figures flow.state.file_path = tmp_path try: flow.kickoff() except Exception as exc: status_analyst.update(label="Figure analysis failed", state="error") st.error(f"Figure Analyst agent error: {exc}") st.stop() if flow.state.quality == "poor": status_analyst.update( label="Figure analysis skipped — no usable figures", state="error" ) st.warning(flow.state.error or "No usable figures detected.") else: n = len(flow.state.figure_intelligences) q = st.session_state.quality_score q_str = f" · avg page confidence {round(q * 100)}%" if q is not None else "" label = ( f"Figure intelligence generated · {n} figures interpreted{q_str}" ) st.write(label) if flow.state.error: status_analyst.update( label=f"{label} (partial failures)", state="complete", expanded=True, ) st.warning(flow.state.error) else: status_analyst.update(label=label, state="complete", expanded=False) st.session_state.pipeline_steps.append(label) st.session_state.flow_state = flow.state st.rerun() except Exception as exc: st.error(f"Unexpected error: {exc}") finally: if tmp_path and os.path.exists(tmp_path): os.unlink(tmp_path) # --------------------------------------------------------------------------- # Main panel — results display # --------------------------------------------------------------------------- if st.session_state.flow_state is not None and uploaded_file is not None: state: ScienceFlowState = st.session_state.flow_state for step in st.session_state.pipeline_steps: st.markdown( f'â—Ź' f"  {step}", unsafe_allow_html=True, ) if st.session_state.pipeline_steps: st.markdown("") _render_figure_intelligence_tab(state)