PUE_Report_Generator_AI / DEVELOPER_NOTES.md
Israelbliz's picture
Upload DEVELOPER_NOTES.md
fce2380 verified
|
Raw
History Blame Contribute Delete
7.17 kB

PUE Report Agent

Turns a DREEF Productive Use of Energy (PUE) master-sheet workbook into a community PUE report, using LangChain for the prose and deterministic Python for every number.

This skeleton was reverse-engineered from two real report/master-sheet pairs (Jaji and Kwarua Tasha, both Kaduna State). It already extracts cleanly from both.


Why it's split the way it is

The single most important design decision: the LLM never touches a number.

When a report's figures are re-typed by a language model, they drift β€” that is literally how one of the source reports ended up with budget and equipment numbers that didn't match its own master sheet. So the architecture is:

master.xlsx
   β”‚
   β–Ό  extractors.py          (pure Python, openpyxl β€” deterministic)
PUEReportData
   β”‚
   β–Ό  calc.py                (recompute every total from line items)
PUEReportData (correct totals)
   β”‚
   β–Ό  validate.py            (reconciliation; appends warnings)
PUEReportData (+warnings)
   β”‚
   β”œβ”€β–Ί  agent.py  generate_narratives()    LLM β€” PROSE ONLY
   β”‚      β€’ Community Overview  ............  Gemini  (gemini-2.5-flash)
   β”‚      β€’ Demographic narrative  .........  Gemini
   β”‚      β€’ Processing Insights  ...........  Gemini
   β”‚
   β–Ό  render.py              β†’  template-faithful .docx
  • schema.py β€” Pydantic models. The contract between every stage.
  • extractors.py β€” reads the workbook into the schema. Numbers come from here, never from an LLM. Budget extraction is header-aware (each section maps its own columns), so irregular blocks like "OTHERS" parse correctly.
  • calc.py β€” recomputes every total from its line items: budget section totals, BOQ grand total, equipment kW/quantity totals, energy projections, financial-model monthly totals. The template shipped with a Grand Total of ₦30,756,000 when its line items summed to ₦37,256,000; the agent never reproduces that kind of error, and records any disagreement as a warning.
  • validate.py β€” checks interview counts agree, flags stale template text.
  • templates.py β€” the boilerplate sections, verbatim from the reference template (introduction, methodology, infrastructure specs, governance, safety, conditions for scale-up, MER, risks, SDGs, challenges, AI disclosure), filled with str.format. Never sent to a model.
  • agent.py β€” the only LLM code. All three narratives (Community Overview, demographic, processing insights) are generated with Gemini (gemini-2.5-flash). The model is swappable per call, so individual chains can move to another provider later without changing the chains.
  • render.py β€” produces a Word document that mirrors the reference template's structure exactly.

What render.py reproduces from the template

  • Title page in the template's layout.
  • Auto Table of Contents β€” a real Word TOC field that builds and renumbers itself on open.
  • Auto List of Tables β€” a Word Table-of-Figures field keyed to the "Table" caption sequence.
  • SEQ-field caption numbering β€” every caption is Table + a SEQ Table field (or Figure + SEQ Figure). Word computes the numbers, so they are always sequential and correct no matter how many tables a community needs β€” nothing is hard-numbered.
  • Every section in template order with the template's headings and tone.
  • Community-adaptive tables β€” one machinery table per processed crop, one equipment table per section, one budget table per section, each with a recomputed TOTAL row. The skeleton stays identical; the rows follow the data.
  • Fields are set to update on open, so the TOC, List of Tables and all caption numbers populate the first time the file is opened in Word. (LibreOffice's headless PDF convert doesn't run that update β€” open in Word, or press Ctrl+A then F9, to populate them.)

Quick start

pip install -r requirements.txt

# Extraction + recompute + validation + render β€” no API key needed.
# Narratives are skipped with --no-llm, but the full template still renders:
python run.py "Jaji-_Kaduna_PUE_Master_Sheet.xlsx" --no-llm \
    --developer "Green Edge Consortium" --date "February 2026" \
    --solar-pv "6.45 MWp" --battery "10 MWh" \
    --docx-out jaji_report.docx

# Full run with AI narratives (all narratives use Gemini):
export GOOGLE_API_KEY=...
python run.py "Jaji-_Kaduna_PUE_Master_Sheet.xlsx" \
    --developer "Green Edge Consortium" --date "February 2026" \
    --docx-out jaji_report.docx --json-out jaji.json

Open the resulting .docx in Word; the Table of Contents, List of Tables and all caption numbers populate automatically on first open.


What the data layers in the master sheet mean

Layer Sheets Role
Raw survey Minigrid Farmers Input, Processors, SME, E-Mobility, Market, ESG One row per respondent
Analysis * Analysis Sheet Pre-aggregated label/value blocks
Key Findings Key Findings Report-ready tables (the extractor's main source)
Equipment / Budget / Finance Proposed * PUE Equipment, Budget, Individual Model, Agrohub Model, Summary Plans & costs

The extractor prefers raw/analysis sheets for survey facts and only uses Key Findings for developer-supplied figures (capacities, revenue-per-user, projected impact) that aren't derivable from survey data β€” because Key Findings is where stale template text from other communities tends to survive.

Things that are NOT in the survey data

  • Mini-grid capacities (Solar PV / Battery / Annual Consumption) β€” developer input.
  • Revenue-per-user table β€” developer input, often shared across communities. Pass these in explicitly; the validator warns if they're missing.

Extending to a new community / new sheet layout

  1. Run python run.py new_sheet.xlsx --no-llm.
  2. Read the warnings and eyeball the summary against the source.
  3. If a block comes back empty, the label probably moved. The extractor finds blocks by label text scanned across all columns (_label_col) rather than fixed cell addresses, so usually only the label string needs updating.
  4. Add a validate.py check for any new invariant you discover.

Adapting to a community whose data is shaped differently

The renderer is data-driven, so new crops, new SME types, more or fewer budget sections, etc. flow through automatically β€” the template skeleton stays fixed while the rows follow the master sheet. The two things to supply per community that are NOT in the survey data:

  • Mini-grid / micro capacities (Solar PV, battery, annual consumption, distribution metrics) β€” developer figures; pass via the CLI flags or set data.minigrid / data.microgrid before rendering.
  • Report date for the title page (--date).

Tested against

  • Jaji-_Kaduna_PUE_Master_Sheet.xlsx β€” clean extraction, 3 advisory warnings.
  • Kwarua_Tasha_-_Kaduna_PUE_Master_Sheet.xlsx β€” clean extraction, 6 warnings correctly flagging real defects in that workbook.