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
```bash
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.