Spaces:
Sleeping
Sleeping
| """ | |
| 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.") | |