""" Streamlit front-end for the DREEF PUE report generator. Upload a community master-sheet workbook, set the developer / date (and any externally-supplied mini-grid capacities), and download a template-faithful Word report. Optional AI narratives use Gemini and require GOOGLE_API_KEY. Run locally: streamlit run app.py Deploy: push this folder to a Hugging Face Space (SDK = Streamlit). """ from __future__ import annotations import os import tempfile from io import BytesIO import streamlit as st from pue_report_agent import extract_master_sheet, recompute, validate st.set_page_config(page_title="PUE Report Generator", page_icon="📄", layout="centered") st.title("📄 PUE Report Generator") st.caption( "Turn a community PUE master-sheet workbook into a template-faithful Word " "report. Every number is recomputed from the sheet — the AI only ever " "touches prose, never figures." ) # --------------------------------------------------------------------------- # # Sidebar — options # --------------------------------------------------------------------------- # with st.sidebar: st.header("Report settings") developer = st.text_input("Mini-grid developer", value="the mini-grid developer") report_date = st.text_input("Report date (title page)", value="") st.subheader("Mini-grid capacities") st.caption( "Developer-supplied; only fill these if they aren't already in the " "sheet's Key Findings." ) solar_pv = st.text_input("Solar PV", value="", placeholder="e.g. 6.45 MWp") battery = st.text_input("Battery storage", value="", placeholder="e.g. 10 MWh") annual_consumption = st.text_input("Annual consumption", value="", placeholder="e.g. 5 MW") st.subheader("AI narratives") def _has_api_key() -> bool: if os.environ.get("GOOGLE_API_KEY"): return True try: return bool(st.secrets.get("GOOGLE_API_KEY")) except Exception: return False has_key = _has_api_key() want_narratives = st.toggle( "Generate AI prose sections (Gemini)", value=False, help="Needs GOOGLE_API_KEY in the Space secrets or environment. " "Without it, the three prose sections use a plain fallback.") if want_narratives and not has_key: st.warning("No GOOGLE_API_KEY found — narratives will be skipped.") # --------------------------------------------------------------------------- # # Main — upload & generate # --------------------------------------------------------------------------- # uploaded = st.file_uploader( "Upload the PUE master sheet (.xlsx)", type=["xlsx", "xlsm"]) if uploaded is None: st.info("Upload a master-sheet workbook to begin.") st.stop() if st.button("Generate report", type="primary"): with st.spinner("Extracting, recomputing, validating…"): # extract_master_sheet expects a path, so spill the upload to a temp file. with tempfile.NamedTemporaryFile(suffix=".xlsx", delete=False) as tmp: tmp.write(uploaded.getvalue()) xlsx_path = tmp.name try: data = extract_master_sheet(xlsx_path) data = recompute(data) # Merge any developer-supplied capacity overrides. if solar_pv: data.minigrid.solar_pv = solar_pv if battery: data.minigrid.battery_storage = battery if annual_consumption: data.minigrid.annual_consumption = annual_consumption data = validate(data) narrative_note = None if want_narratives: from pue_report_agent import generate_narratives if generate_narratives is None: narrative_note = ("langchain isn't installed — prose " "sections used the fallback.") elif not has_key: narrative_note = ("No API key — prose sections used the " "fallback.") else: data = generate_narratives(data) # Render to a temp .docx and read the bytes back for download. from pue_report_agent.render import render out_path = os.path.join(tempfile.gettempdir(), "PUE_Report.docx") render(data, out_path, developer=developer, report_date=report_date) with open(out_path, "rb") as fh: docx_bytes = fh.read() finally: try: os.unlink(xlsx_path) except OSError: pass st.success(f"Report generated for {data.community.name or 'the community'}.") if narrative_note: st.info(narrative_note) fname = f"{(data.community.name or 'Community').replace(' ', '_')}_PUE_Report.docx" st.download_button( "⬇️ Download Word report", data=docx_bytes, file_name=fname, mime="application/vnd.openxmlformats-officedocument.wordprocessingml.document", type="primary") # Quick at-a-glance summary. col1, col2, col3 = st.columns(3) col1.metric("Interviewed", data.interviews.total) col2.metric("Machines", len(data.processors.machinery)) col3.metric("Equipment cost (₦)", f"{(data.budget.grand_total or 0):,.0f}") if data.warnings: with st.expander(f"⚠️ {len(data.warnings)} item(s) to review", expanded=True): for w in data.warnings: st.write("• " + w) else: st.caption("No validation warnings.")