Spaces:
Paused
Paused
Upload 7 files
Browse files- .env.example +20 -0
- AGENTS.md +114 -0
- README.md +157 -7
- app.py +926 -0
- ecoagent-plan.md +271 -0
- requirements.txt +18 -0
- watsonx_client.py +520 -0
.env.example
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# EcoAgent β environment variable template
|
| 2 |
+
# Copy this file to .env and fill in your values.
|
| 3 |
+
# NEVER commit .env with real API keys to version control.
|
| 4 |
+
|
| 5 |
+
# IBM Cloud API Key
|
| 6 |
+
# Get from: https://cloud.ibm.com β Manage β Access (IAM) β API keys
|
| 7 |
+
WATSONX_API_KEY=your-ibm-cloud-api-key-here
|
| 8 |
+
|
| 9 |
+
# watsonx.ai endpoint β choose your region:
|
| 10 |
+
# eu-de (Frankfurt): https://eu-de.ml.cloud.ibm.com <- default
|
| 11 |
+
# us-south (Dallas): https://us-south.ml.cloud.ibm.com
|
| 12 |
+
# eu-gb (London): https://eu-gb.ml.cloud.ibm.com
|
| 13 |
+
# jp-tok (Tokyo): https://jp-tok.ml.cloud.ibm.com
|
| 14 |
+
# au-syd (Sydney): https://au-syd.ml.cloud.ibm.com
|
| 15 |
+
WATSONX_URL= your-region-endpoint
|
| 16 |
+
|
| 17 |
+
# watsonx.ai Studio Project ID (UUID)
|
| 18 |
+
# Get from: https://eu-de.dataplatform.cloud.ibm.com
|
| 19 |
+
# β open your project β Manage tab β General β Project ID
|
| 20 |
+
WATSONX_PROJECT_ID=your-project-id-uuid-here
|
AGENTS.md
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# AGENTS.md
|
| 2 |
+
|
| 3 |
+
This file provides guidance to agents when working with code in this repository.
|
| 4 |
+
|
| 5 |
+
## Project
|
| 6 |
+
|
| 7 |
+
EcoAgent v2 β A Gradio Blocks web app for eco lifestyle advice, powered by **IBM Granite** (`ibm/granite-4-h-small`) via the `ibm-watsonx-ai` SDK. India-focused with 4 tabs: Chat, Dashboard, Recycling Guide, Household Profile.
|
| 8 |
+
|
| 9 |
+
**Classification:** Prompt-Engineered LLM Application (not RAG, not Agentic AI)
|
| 10 |
+
**IBM Orchestrate REST API is NOT used** β direct SDK calls to watsonx.ai only.
|
| 11 |
+
|
| 12 |
+
## Stack
|
| 13 |
+
|
| 14 |
+
- **Runtime:** Python 3.14 via `uv` (venv at `.venv/`)
|
| 15 |
+
- **Package manager:** `uv` β always use `uv pip install` / `uv run python`, NOT bare `pip` or `python`
|
| 16 |
+
- **UI Framework:** Gradio 6.20 (ultra-light eco green theme)
|
| 17 |
+
- **AI Model:** IBM Granite 4 H Small (`ibm/granite-4-h-small`) via watsonx.ai eu-de
|
| 18 |
+
- **SDK:** `ibm-watsonx-ai` >= 1.1.15 (APIClient + ModelInference pattern)
|
| 19 |
+
- **System Python is 3.11 (Miniconda)** β unrelated to this project's venv
|
| 20 |
+
- **Target deploy:** Hugging Face Spaces (Gradio SDK)
|
| 21 |
+
|
| 22 |
+
## Key Commands
|
| 23 |
+
|
| 24 |
+
```bash
|
| 25 |
+
uv pip install -r requirements.txt # install deps
|
| 26 |
+
uv run python app.py # run locally β http://localhost:7860
|
| 27 |
+
```
|
| 28 |
+
|
| 29 |
+
## Critical Gotchas
|
| 30 |
+
|
| 31 |
+
- **`.env` is gitignored** β cannot be written by file tools. Use `Set-Content` PowerShell command instead.
|
| 32 |
+
- **`WATSONX_PROJECT_ID` is mandatory** β the SDK raises an error without it. Get it from: `https://eu-de.dataplatform.cloud.ibm.com` β project β Manage β General β Project ID.
|
| 33 |
+
- **Region is eu-de (Frankfurt)** β `WATSONX_URL=https://eu-de.ml.cloud.ibm.com`. Do not use `us-south`.
|
| 34 |
+
- **Model lazy-init** β `_get_model()` in `watsonx_client.py` initialises `ModelInference` on the first call, not at import. Import-time errors = missing env vars. First-call errors = bad project ID or model access.
|
| 35 |
+
- **`load_dotenv(".env")` explicit path** β both `app.py` and `watsonx_client.py` call this. The default `load_dotenv()` looks for `.env` by name; the explicit path ensures it works regardless of CWD.
|
| 36 |
+
- **Chat history format (Gradio 6.x)** β History uses structured content blocks: `{"role": "user", "content": [{"type": "text", "text": "..."}]}`. The `chat_submit()` function extracts text for the API call and returns structured blocks for display.
|
| 37 |
+
- **`dashboard_html` is defined inside the Tab 2 block** β it's referenced by `save_profile()` in Tab 4. Both must be inside the same `gr.Blocks` context.
|
| 38 |
+
- **Theme/CSS in Gradio 6.x** β `theme` and `css` parameters go in `demo.launch()`, NOT in `gr.Blocks()` constructor.
|
| 39 |
+
- **SSL_CERT_FILE** β The app auto-detects and sets the correct certifi path on startup.
|
| 40 |
+
|
| 41 |
+
## Architecture
|
| 42 |
+
|
| 43 |
+
```
|
| 44 |
+
.env
|
| 45 |
+
ββ WATSONX_API_KEY, WATSONX_PROJECT_ID, WATSONX_URL
|
| 46 |
+
β
|
| 47 |
+
βΌ
|
| 48 |
+
watsonx_client.py
|
| 49 |
+
ββ AGENT_INSTRUCTIONS β 86-line system prompt (persona, tone, rules, format)
|
| 50 |
+
ββ IMPACT_TABLE β 20 actions with CO2/water/waste lookup values
|
| 51 |
+
ββ PRODUCT_RECS β static eco-product recs per material category
|
| 52 |
+
ββ INDIAN_CITIES β 15 major Indian cities for recycling guide
|
| 53 |
+
ββ _get_model() β lazy-init APIClient + ModelInference ( Granite 4 H Small )
|
| 54 |
+
ββ get_eco_answer() β multi-turn chat, builds [system]+messages list
|
| 55 |
+
ββ get_recycling_guide() β single-turn recycling lookup call
|
| 56 |
+
ββ compute_session_impact() β aggregates logged actions β metric dict
|
| 57 |
+
β
|
| 58 |
+
βΌ
|
| 59 |
+
app.py (Gradio Blocks β ultra-light theme)
|
| 60 |
+
ββ Tab 1: Chat β chatbot + action chip logger + eco score ring
|
| 61 |
+
ββ Tab 2: Dashboard β HTML metric cards from compute_session_impact()
|
| 62 |
+
ββ Tab 3: Recycling β material+city dropdowns β get_recycling_guide()
|
| 63 |
+
ββ Tab 4: Profile β household form β profile_state (gr.State)
|
| 64 |
+
```
|
| 65 |
+
|
| 66 |
+
## Environment Variables
|
| 67 |
+
|
| 68 |
+
| Variable | Source | Purpose |
|
| 69 |
+
|---|---|---|
|
| 70 |
+
| `WATSONX_API_KEY` | `.env.example` | IBM Cloud API key |
|
| 71 |
+
| `WATSONX_PROJECT_ID` | watsonx.ai Studio | SDK project scope β required |
|
| 72 |
+
| `WATSONX_URL` | `https://eu-de.ml.cloud.ibm.com` | eu-de watsonx.ai endpoint |
|
| 73 |
+
|
| 74 |
+
## Prompt Engineering Pattern
|
| 75 |
+
|
| 76 |
+
This project uses **prompt engineering** to transform IBM Granite into a domain-specific eco advisor:
|
| 77 |
+
|
| 78 |
+
- **System Prompt:** 86-line `AGENT_INSTRUCTIONS` defining persona, output format, focus areas, and guardrails
|
| 79 |
+
- **Static Knowledge:** `IMPACT_TABLE` (20 eco actions) and `PRODUCT_RECS` (8 material categories) injected via prompt
|
| 80 |
+
- **Dynamic Context:** Household profile (members, location, habits) injected per session
|
| 81 |
+
- **Output Format:** Fixed 4-part structure (Quick Tip β Why it Matters β Impact β Optional Resource)
|
| 82 |
+
- **Guardrails:** Never invent stats, label [Lookup] vs [Estimate], no medical/financial advice
|
| 83 |
+
|
| 84 |
+
## UI Theme
|
| 85 |
+
|
| 86 |
+
Ultra-light eco green theme:
|
| 87 |
+
- Background: `#fcfcfd` (near white)
|
| 88 |
+
- Primary accent: `#2e7d50` (green)
|
| 89 |
+
- Cards: `#ffffff` with subtle shadows
|
| 90 |
+
- Text: `#1c1c1e` (near black), muted: `#4a4a4e`
|
| 91 |
+
- Borders: `#e4e4e7` (light gray)
|
| 92 |
+
- CheckboxGroup styled as selectable pills/chips
|
| 93 |
+
- Chatbot with welcome placeholder and white-flash prevention CSS
|
| 94 |
+
|
| 95 |
+
## Project Files
|
| 96 |
+
|
| 97 |
+
| File | Purpose |
|
| 98 |
+
|---|---|
|
| 99 |
+
| `app.py` | Gradio Blocks UI β 4 tabs, callbacks, CSS theme |
|
| 100 |
+
| `watsonx_client.py` | IBM watsonx.ai SDK wrapper, agent config, impact data |
|
| 101 |
+
| `requirements.txt` | Pinned Python dependencies (4 packages) |
|
| 102 |
+
| `.env` | Local credentials (gitignored) |
|
| 103 |
+
| `.env.example` | Template β safe to commit |
|
| 104 |
+
| `AGENTS.md` | This file β agent guidance |
|
| 105 |
+
| `README.md` | HF Spaces front-matter, setup instructions |
|
| 106 |
+
| `ecoagent-plan.md` | Implementation plan and architecture decisions |
|
| 107 |
+
| `architecture.png` | Architecture blueprint diagram |
|
| 108 |
+
| `fill.txt` | PPT content fill for presentation |
|
| 109 |
+
|
| 110 |
+
## Deleted Files (v1 β v2)
|
| 111 |
+
|
| 112 |
+
- `rag_pipeline.py` β replaced by `watsonx_client.py`
|
| 113 |
+
- `ibm-credentials.env` β replaced by `.env`
|
| 114 |
+
- `embed.txt` β was a debugging artifact; deleted
|
README.md
CHANGED
|
@@ -1,14 +1,164 @@
|
|
| 1 |
---
|
| 2 |
title: EcoAgent
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version: 6.20.0
|
| 8 |
-
python_version: '3.12'
|
| 9 |
app_file: app.py
|
| 10 |
pinned: false
|
| 11 |
-
short_description: Agent adopting a greener lifestyle for users
|
| 12 |
---
|
| 13 |
|
| 14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
title: EcoAgent
|
| 3 |
+
emoji: π±
|
| 4 |
+
colorFrom: green
|
| 5 |
+
colorTo: pink
|
| 6 |
sdk: gradio
|
| 7 |
+
sdk_version: "6.20.0"
|
|
|
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
|
|
|
| 10 |
---
|
| 11 |
|
| 12 |
+
# π± EcoAgent β AI-Powered Eco Lifestyle Assistant
|
| 13 |
+
|
| 14 |
+
An India-focused sustainable living assistant powered by **IBM Granite** via **watsonx.ai**. Chat about eco habits, explore a household impact dashboard, look up local recycling guides, and build a family sustainability profile.
|
| 15 |
+
|
| 16 |
+
**Classification:** Prompt-Engineered LLM Application β not RAG, not Agentic AI.
|
| 17 |
+
|
| 18 |
+
---
|
| 19 |
+
|
| 20 |
+
## Features
|
| 21 |
+
|
| 22 |
+
| Tab | What it does |
|
| 23 |
+
|---|---|
|
| 24 |
+
| π¬ **Chat** | Multi-turn conversation with IBM Granite β personalised eco tips, government schemes, impact estimates |
|
| 25 |
+
| π **Dashboard** | Session-based eco score (0β100), COβ/water/waste savings tracker, household summary |
|
| 26 |
+
| β»οΈ **Recycling Guide** | City-specific recycling instructions for 8 material categories + eco-friendly product alternatives |
|
| 27 |
+
| π‘ **Profile** | Household members, Indian city, current eco habits β personalises all chat responses |
|
| 28 |
+
|
| 29 |
+
---
|
| 30 |
+
|
| 31 |
+
## Architecture
|
| 32 |
+
|
| 33 |
+
```
|
| 34 |
+
.env (WATSONX_API_KEY, WATSONX_PROJECT_ID, WATSONX_URL)
|
| 35 |
+
β
|
| 36 |
+
βΌ
|
| 37 |
+
watsonx_client.py β IBM Granite ModelInference, AGENT_INSTRUCTIONS, IMPACT_TABLE
|
| 38 |
+
β
|
| 39 |
+
βΌ
|
| 40 |
+
app.py β Gradio Blocks (4 tabs, ultra-light eco green theme, session state)
|
| 41 |
+
```
|
| 42 |
+
|
| 43 |
+
**Model:** `ibm/granite-4-h-small` (eu-de region, watsonx.ai)
|
| 44 |
+
**Auth:** IBM Cloud API key β watsonx.ai SDK (no Orchestrate REST API)
|
| 45 |
+
|
| 46 |
+
---
|
| 47 |
+
|
| 48 |
+
## How It Works
|
| 49 |
+
|
| 50 |
+
EcoAgent uses **prompt engineering** to transform IBM Granite into a domain-specific eco advisor:
|
| 51 |
+
|
| 52 |
+
1. **System Prompt:** 86-line `AGENT_INSTRUCTIONS` defining persona, output format, focus areas, and guardrails
|
| 53 |
+
2. **Static Knowledge:** `IMPACT_TABLE` (20 eco actions) and `PRODUCT_RECS` (8 material categories) injected via prompt
|
| 54 |
+
3. **Dynamic Context:** Household profile (members, location, habits) injected per session
|
| 55 |
+
4. **Output Format:** Fixed 4-part structure (Quick Tip β Why it Matters β Impact β Optional Resource)
|
| 56 |
+
5. **Guardrails:** Never invent stats, label [Lookup] vs [Estimate], no medical/financial advice
|
| 57 |
+
|
| 58 |
+
---
|
| 59 |
+
|
| 60 |
+
## UI Theme
|
| 61 |
+
|
| 62 |
+
Ultra-light eco green theme with near-white background and green accents:
|
| 63 |
+
|
| 64 |
+
- Background: `#fcfcfd` (near white)
|
| 65 |
+
- Primary accent: `#2e7d50` (green)
|
| 66 |
+
- Cards: `#ffffff` with subtle shadows
|
| 67 |
+
- Text: `#1c1c1e` (near black), muted: `#4a4a4e`
|
| 68 |
+
- Borders: `#e4e4e7` (light gray)
|
| 69 |
+
- CheckboxGroup styled as selectable pills/chips
|
| 70 |
+
|
| 71 |
+
---
|
| 72 |
+
|
| 73 |
+
## Environment Variables
|
| 74 |
+
|
| 75 |
+
Set these as **Secrets** in your Hugging Face Space (Settings β Variables and Secrets)
|
| 76 |
+
or in a local `.env` file (never commit with real values).
|
| 77 |
+
|
| 78 |
+
| Variable | Required | Description |
|
| 79 |
+
|---|---|---|
|
| 80 |
+
| `WATSONX_API_KEY` | β
| IBM Cloud API key β [get one here](https://cloud.ibm.com/iam/apikeys) |
|
| 81 |
+
| `WATSONX_PROJECT_ID` | β
| watsonx.ai Studio Project ID (UUID) β see below |
|
| 82 |
+
| `WATSONX_URL` | β
| watsonx.ai endpoint for your region β default `https://eu-de.ml.cloud.ibm.com` |
|
| 83 |
+
|
| 84 |
+
### How to get `WATSONX_PROJECT_ID`
|
| 85 |
+
|
| 86 |
+
1. Go to [https://eu-de.dataplatform.cloud.ibm.com](https://eu-de.dataplatform.cloud.ibm.com)
|
| 87 |
+
2. Create or open a project
|
| 88 |
+
3. Click the **Manage** tab β **General** section
|
| 89 |
+
4. Copy the **Project ID** (a UUID like `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`)
|
| 90 |
+
5. Add it to `.env` as `WATSONX_PROJECT_ID=<your-uuid>`
|
| 91 |
+
|
| 92 |
+
### Regional watsonx.ai URLs
|
| 93 |
+
|
| 94 |
+
| Region | URL |
|
| 95 |
+
|---|---|
|
| 96 |
+
| EU Frankfurt (default) | `https://eu-de.ml.cloud.ibm.com` |
|
| 97 |
+
| US Dallas | `https://us-south.ml.cloud.ibm.com` |
|
| 98 |
+
| UK London | `https://eu-gb.ml.cloud.ibm.com` |
|
| 99 |
+
| Japan Tokyo | `https://jp-tok.ml.cloud.ibm.com` |
|
| 100 |
+
| Australia Sydney | `https://au-syd.ml.cloud.ibm.com` |
|
| 101 |
+
|
| 102 |
+
---
|
| 103 |
+
|
| 104 |
+
## Local Development
|
| 105 |
+
|
| 106 |
+
### Prerequisites
|
| 107 |
+
- Python 3.10+ (Python 3.14 via `uv` is configured in `.venv`)
|
| 108 |
+
- [`uv`](https://github.com/astral-sh/uv) (recommended) or `pip`
|
| 109 |
+
|
| 110 |
+
### Steps
|
| 111 |
+
|
| 112 |
+
```bash
|
| 113 |
+
# 1. Clone the repo
|
| 114 |
+
git clone https://huggingface.co/spaces/<your-username>/ecoagent
|
| 115 |
+
cd ecoagent
|
| 116 |
+
|
| 117 |
+
# 2. Install dependencies
|
| 118 |
+
uv pip install -r requirements.txt
|
| 119 |
+
# or: pip install -r requirements.txt
|
| 120 |
+
|
| 121 |
+
# 3. Configure credentials
|
| 122 |
+
# Copy .env.example to .env and fill in your values:
|
| 123 |
+
cp .env.example .env
|
| 124 |
+
# Edit .env β set WATSONX_API_KEY, WATSONX_PROJECT_ID, WATSONX_URL
|
| 125 |
+
|
| 126 |
+
# 4. Run
|
| 127 |
+
uv run python app.py
|
| 128 |
+
# β Open http://localhost:7860
|
| 129 |
+
```
|
| 130 |
+
|
| 131 |
+
---
|
| 132 |
+
|
| 133 |
+
## Customising the Agent
|
| 134 |
+
|
| 135 |
+
Open [`watsonx_client.py`](watsonx_client.py) and edit the `AGENT_INSTRUCTIONS` constant at the top of the file. You can change:
|
| 136 |
+
|
| 137 |
+
- **Persona & tone** β make it more formal, more playful, multilingual, etc.
|
| 138 |
+
- **Focus areas** β add specific sustainability topics (e.g. marine conservation)
|
| 139 |
+
- **India-specific context** β add regional schemes, local brands, city-specific advice
|
| 140 |
+
- **Safety rules** β tighten or relax what the agent will/won't say
|
| 141 |
+
- **Answer structure** β change the tip β why β impact β resource format
|
| 142 |
+
|
| 143 |
+
The `IMPACT_TABLE` dict below it controls the carbon/water/waste numbers shown in the Dashboard tab β add new actions or update existing values there.
|
| 144 |
+
|
| 145 |
+
---
|
| 146 |
+
|
| 147 |
+
## Project Files
|
| 148 |
+
|
| 149 |
+
| File | Purpose |
|
| 150 |
+
|---|---|
|
| 151 |
+
| `app.py` | Gradio Blocks UI β 4 tabs, callbacks, CSS theme |
|
| 152 |
+
| `watsonx_client.py` | IBM watsonx.ai SDK wrapper, agent config, impact data |
|
| 153 |
+
| `requirements.txt` | Pinned Python dependencies (4 packages) |
|
| 154 |
+
| `.env` | Local credentials (gitignored) |
|
| 155 |
+
| `.env.example` | Template β safe to commit |
|
| 156 |
+
| `ecoagent-plan.md` | Implementation plan and architecture decisions |
|
| 157 |
+
| `architecture.png` | Architecture blueprint diagram |
|
| 158 |
+
| `fill.txt` | PPT content fill for presentation |
|
| 159 |
+
|
| 160 |
+
---
|
| 161 |
+
|
| 162 |
+
## Disclaimer
|
| 163 |
+
|
| 164 |
+
Answers are AI-generated by IBM Granite. Impact figures labelled `[Lookup]` are sourced from IPCC AR6, BEE India, and CPCB data. Figures labelled `[Estimate]` are model-generated approximations. Always verify government schemes and legal/financial details with official sources.
|
app.py
ADDED
|
@@ -0,0 +1,926 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
app.py
|
| 3 |
+
------
|
| 4 |
+
EcoAgent β Gradio Blocks UI with 4 tabs:
|
| 5 |
+
Chat | Dashboard | Recycling Guide | Household Profile
|
| 6 |
+
|
| 7 |
+
Features:
|
| 8 |
+
- IBM Granite 4 H Small via watsonx.ai (ibm-watsonx-ai APIClient)
|
| 9 |
+
- Clean light-theme CSS, no dark mode
|
| 10 |
+
- Session-based eco score, action logging, impact dashboard
|
| 11 |
+
- India-specific recycling guide and product recommendations
|
| 12 |
+
- Household/family profile with habit tracking
|
| 13 |
+
- Hugging Face Spaces compatible
|
| 14 |
+
"""
|
| 15 |
+
|
| 16 |
+
import os
|
| 17 |
+
import logging
|
| 18 |
+
|
| 19 |
+
# Fix SSL certificate path for environments where SSL_CERT_FILE points to wrong Python
|
| 20 |
+
if "SSL_CERT_FILE" not in os.environ or not os.path.isfile(os.environ.get("SSL_CERT_FILE", "")):
|
| 21 |
+
try:
|
| 22 |
+
import certifi
|
| 23 |
+
os.environ["SSL_CERT_FILE"] = certifi.where()
|
| 24 |
+
os.environ["REQUESTS_CA_BUNDLE"] = certifi.where()
|
| 25 |
+
except ImportError:
|
| 26 |
+
pass
|
| 27 |
+
|
| 28 |
+
import gradio as gr
|
| 29 |
+
from dotenv import load_dotenv
|
| 30 |
+
|
| 31 |
+
from watsonx_client import (
|
| 32 |
+
get_eco_answer,
|
| 33 |
+
get_recycling_guide,
|
| 34 |
+
compute_session_impact,
|
| 35 |
+
IMPACT_TABLE,
|
| 36 |
+
PRODUCT_RECS,
|
| 37 |
+
INDIAN_CITIES,
|
| 38 |
+
)
|
| 39 |
+
|
| 40 |
+
# ---------------------------------------------------------------------------
|
| 41 |
+
load_dotenv(".env")
|
| 42 |
+
logging.basicConfig(level=logging.INFO)
|
| 43 |
+
logger = logging.getLogger(__name__)
|
| 44 |
+
|
| 45 |
+
# ---------------------------------------------------------------------------
|
| 46 |
+
# Custom CSS β light-only eco green theme, polished cards and layout
|
| 47 |
+
# ---------------------------------------------------------------------------
|
| 48 |
+
CUSTOM_CSS = """
|
| 49 |
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
| 50 |
+
|
| 51 |
+
:root {
|
| 52 |
+
--eco-primary: #2e7d50;
|
| 53 |
+
--eco-secondary: #43a868;
|
| 54 |
+
--eco-light: #f0f7f3;
|
| 55 |
+
--eco-bg: #fcfcfd;
|
| 56 |
+
--eco-card: #ffffff;
|
| 57 |
+
--eco-text: #1c1c1e;
|
| 58 |
+
--eco-muted: #6b6b6f;
|
| 59 |
+
--eco-border: #e4e4e7;
|
| 60 |
+
--eco-shadow: 0 1px 3px rgba(0,0,0,0.04);
|
| 61 |
+
--radius: 6px;
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
body, .gradio-container {
|
| 65 |
+
font-family: 'Inter', system-ui, sans-serif !important;
|
| 66 |
+
background: var(--eco-bg) !important;
|
| 67 |
+
color: var(--eco-text) !important;
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
/* ββ Gradio component overrides for ultra-light look ββ */
|
| 71 |
+
.gradio-container { background: var(--eco-bg) !important; }
|
| 72 |
+
.gradio-container .wrap { background: var(--eco-bg) !important; }
|
| 73 |
+
|
| 74 |
+
/* Tabs */
|
| 75 |
+
.tab-nav button {
|
| 76 |
+
font-weight: 500 !important;
|
| 77 |
+
color: var(--eco-muted) !important;
|
| 78 |
+
border: none !important;
|
| 79 |
+
border-bottom: 2px solid transparent !important;
|
| 80 |
+
background: transparent !important;
|
| 81 |
+
border-radius: 0 !important;
|
| 82 |
+
padding: 8px 16px !important;
|
| 83 |
+
}
|
| 84 |
+
.tab-nav button.selected {
|
| 85 |
+
color: var(--eco-primary) !important;
|
| 86 |
+
border-bottom: 2.5px solid var(--eco-primary) !important;
|
| 87 |
+
font-weight: 600 !important;
|
| 88 |
+
}
|
| 89 |
+
.tab-nav button:hover {
|
| 90 |
+
color: var(--eco-primary) !important;
|
| 91 |
+
background: var(--eco-light) !important;
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
/* Buttons */
|
| 95 |
+
button.primary, button.primary.lg {
|
| 96 |
+
background: var(--eco-primary) !important;
|
| 97 |
+
border-color: var(--eco-primary) !important;
|
| 98 |
+
color: #fff !important;
|
| 99 |
+
border-radius: var(--radius) !important;
|
| 100 |
+
font-weight: 600 !important;
|
| 101 |
+
}
|
| 102 |
+
button.primary:hover {
|
| 103 |
+
background: #256a42 !important;
|
| 104 |
+
border-color: #256a42 !important;
|
| 105 |
+
}
|
| 106 |
+
button.secondary, button.secondary.lg {
|
| 107 |
+
background: var(--eco-light) !important;
|
| 108 |
+
border: 1px solid var(--eco-border) !important;
|
| 109 |
+
color: var(--eco-text) !important;
|
| 110 |
+
border-radius: var(--radius) !important;
|
| 111 |
+
font-weight: 500 !important;
|
| 112 |
+
}
|
| 113 |
+
button.secondary:hover {
|
| 114 |
+
background: #e4eff0 !important;
|
| 115 |
+
border-color: var(--eco-border) !important;
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
/* Textbox / Dropdown / Slider */
|
| 119 |
+
textarea, input[type="text"], .wrap textarea, .wrap input[type="text"] {
|
| 120 |
+
border: 1px solid var(--eco-border) !important;
|
| 121 |
+
border-radius: var(--radius) !important;
|
| 122 |
+
background: var(--eco-card) !important;
|
| 123 |
+
color: var(--eco-text) !important;
|
| 124 |
+
}
|
| 125 |
+
textarea:focus, input[type="text"]:focus {
|
| 126 |
+
border-color: var(--eco-primary) !important;
|
| 127 |
+
box-shadow: 0 0 0 2px rgba(46,125,80,0.12) !important;
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
/* Fix Gradio 6 Dropdown*/
|
| 131 |
+
|
| 132 |
+
.gr-dropdown-menu,
|
| 133 |
+
.gr-select-dropdown,
|
| 134 |
+
.gradio-dropdown-menu,
|
| 135 |
+
[role="listbox"] {
|
| 136 |
+
background: #ffffff !important;
|
| 137 |
+
border: 1px solid #dcdcdc !important;
|
| 138 |
+
color: #1c1c1e !important;
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
.gr-dropdown-menu li,
|
| 142 |
+
.gr-select-dropdown li,
|
| 143 |
+
[role="option"] {
|
| 144 |
+
background: #ffffff !important;
|
| 145 |
+
color: #1c1c1e !important;
|
| 146 |
+
padding: 10px 14px !important;
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
.gr-dropdown-menu li:hover,
|
| 150 |
+
.gr-select-dropdown li:hover,
|
| 151 |
+
[role="option"]:hover,
|
| 152 |
+
[aria-selected="true"] {
|
| 153 |
+
background: #e8f5e9 !important;
|
| 154 |
+
color: #2e7d50 !important;
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
/* Slider */
|
| 158 |
+
input[type="range"] {
|
| 159 |
+
accent-color: #2e7d50 !important;
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
input[type="range"]::-webkit-slider-thumb {
|
| 163 |
+
background: #2e7d50 !important;
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
input[type="range"]::-webkit-slider-runnable-track {
|
| 167 |
+
background: #d8e9dc !important;
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
input[type="range"]::-moz-range-thumb {
|
| 171 |
+
background: #2e7d50 !important;
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
input[type="range"]::-moz-range-track {
|
| 175 |
+
background: #d8e9dc !important;
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
/* CheckboxGroup β pills/chips style */
|
| 179 |
+
/* ---------- CheckboxGroup ---------- */
|
| 180 |
+
|
| 181 |
+
.gr-checkboxgroup label,
|
| 182 |
+
.gradio-checkbox label {
|
| 183 |
+
background: #f0f7f3 !important;
|
| 184 |
+
border: 1px solid #d6e7da !important;
|
| 185 |
+
color: #1c1c1e !important;
|
| 186 |
+
border-radius: 18px !important;
|
| 187 |
+
padding: 8px 14px !important;
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
.gr-checkboxgroup label:hover,
|
| 191 |
+
.gradio-checkbox label:hover {
|
| 192 |
+
background: #e6f3ea !important;
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
.gr-checkboxgroup input:checked + span,
|
| 196 |
+
.gradio-checkbox input:checked + span {
|
| 197 |
+
color: white !important;
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
.gr-checkboxgroup label:has(input:checked),
|
| 201 |
+
.gradio-checkbox label:has(input:checked) {
|
| 202 |
+
background: #2e7d50 !important;
|
| 203 |
+
border-color: #2e7d50 !important;
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
/* Chatbot */
|
| 207 |
+
.message.user {
|
| 208 |
+
background: var(--eco-light) !important;
|
| 209 |
+
border: 1px solid var(--eco-border) !important;
|
| 210 |
+
border-radius: var(--radius) !important;
|
| 211 |
+
}
|
| 212 |
+
.message.bot {
|
| 213 |
+
background: var(--eco-card) !important;
|
| 214 |
+
border: 1px solid var(--eco-border) !important;
|
| 215 |
+
border-radius: var(--radius) !important;
|
| 216 |
+
}
|
| 217 |
+
/* Prevent white flash during model thinking */
|
| 218 |
+
#chatbot, #chatbot > .wrap, #chatbot > .wrap > .panel {
|
| 219 |
+
background: var(--eco-bg) !important;
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
/* Markdown headings inside tabs */
|
| 223 |
+
h3, h2 { color: var(--eco-text) !important; }
|
| 224 |
+
|
| 225 |
+
/* ββ Header ββ */
|
| 226 |
+
.eco-header {
|
| 227 |
+
background: var(--eco-card);
|
| 228 |
+
border: 1px solid var(--eco-border);
|
| 229 |
+
border-radius: var(--radius);
|
| 230 |
+
padding: 22px 28px;
|
| 231 |
+
margin-bottom: 16px;
|
| 232 |
+
color: var(--eco-text);
|
| 233 |
+
box-shadow: var(--eco-shadow);
|
| 234 |
+
}
|
| 235 |
+
.eco-header h1 {
|
| 236 |
+
margin: 0 0 4px;
|
| 237 |
+
font-size: 1.5rem;
|
| 238 |
+
font-weight: 700;
|
| 239 |
+
letter-spacing: -0.3px;
|
| 240 |
+
color: var(--eco-primary);
|
| 241 |
+
}
|
| 242 |
+
.eco-header p {
|
| 243 |
+
margin: 0;
|
| 244 |
+
font-size: 0.85rem;
|
| 245 |
+
color: var(--eco-muted);
|
| 246 |
+
}
|
| 247 |
+
.eco-model-badge {
|
| 248 |
+
display: inline-block;
|
| 249 |
+
margin-top: 8px;
|
| 250 |
+
background: var(--eco-light);
|
| 251 |
+
border: 1px solid var(--eco-border);
|
| 252 |
+
border-radius: 20px;
|
| 253 |
+
padding: 3px 12px;
|
| 254 |
+
font-size: 0.73rem;
|
| 255 |
+
font-weight: 600;
|
| 256 |
+
color: var(--eco-primary);
|
| 257 |
+
letter-spacing: 0.3px;
|
| 258 |
+
}
|
| 259 |
+
|
| 260 |
+
/* ββ Metric cards ββ */
|
| 261 |
+
.metric-card {
|
| 262 |
+
background: var(--eco-card);
|
| 263 |
+
border: 1px solid var(--eco-border);
|
| 264 |
+
border-radius: var(--radius);
|
| 265 |
+
padding: 18px 16px;
|
| 266 |
+
text-align: center;
|
| 267 |
+
box-shadow: var(--eco-shadow);
|
| 268 |
+
}
|
| 269 |
+
.metric-value {
|
| 270 |
+
font-size: 1.8rem;
|
| 271 |
+
font-weight: 700;
|
| 272 |
+
color: var(--eco-primary);
|
| 273 |
+
line-height: 1.1;
|
| 274 |
+
}
|
| 275 |
+
.metric-label {
|
| 276 |
+
font-size: 0.7rem;
|
| 277 |
+
color: var(--eco-muted);
|
| 278 |
+
margin-top: 3px;
|
| 279 |
+
text-transform: uppercase;
|
| 280 |
+
letter-spacing: 0.6px;
|
| 281 |
+
font-weight: 500;
|
| 282 |
+
}
|
| 283 |
+
.metric-unit {
|
| 284 |
+
font-size: 0.82rem;
|
| 285 |
+
color: var(--eco-muted);
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
/* ββ Score ring ββ */
|
| 289 |
+
.score-ring-wrap {
|
| 290 |
+
display: flex;
|
| 291 |
+
flex-direction: column;
|
| 292 |
+
align-items: center;
|
| 293 |
+
gap: 6px;
|
| 294 |
+
}
|
| 295 |
+
.score-ring {
|
| 296 |
+
width: 80px; height: 80px; border-radius: 50%;
|
| 297 |
+
background: conic-gradient(var(--eco-secondary) calc(var(--pct) * 1%), #eaeaea 0);
|
| 298 |
+
display: flex; align-items: center; justify-content: center;
|
| 299 |
+
position: relative;
|
| 300 |
+
}
|
| 301 |
+
.score-ring::after {
|
| 302 |
+
content: '';
|
| 303 |
+
width: 60px; height: 60px;
|
| 304 |
+
background: var(--eco-card);
|
| 305 |
+
border-radius: 50%;
|
| 306 |
+
position: absolute;
|
| 307 |
+
}
|
| 308 |
+
.score-number {
|
| 309 |
+
font-size: 1rem; font-weight: 700;
|
| 310 |
+
color: var(--eco-primary);
|
| 311 |
+
position: relative; z-index: 1;
|
| 312 |
+
}
|
| 313 |
+
.score-label {
|
| 314 |
+
font-size: 0.68rem;
|
| 315 |
+
color: var(--eco-muted);
|
| 316 |
+
font-weight: 600;
|
| 317 |
+
text-transform: uppercase;
|
| 318 |
+
letter-spacing: 0.5px;
|
| 319 |
+
}
|
| 320 |
+
.score-count {
|
| 321 |
+
background: var(--eco-light);
|
| 322 |
+
color: var(--eco-primary);
|
| 323 |
+
border: 1px solid var(--eco-border);
|
| 324 |
+
border-radius: 20px;
|
| 325 |
+
padding: 2px 10px;
|
| 326 |
+
font-size: 0.72rem;
|
| 327 |
+
font-weight: 600;
|
| 328 |
+
}
|
| 329 |
+
|
| 330 |
+
/* ββ Recycling output ββ */
|
| 331 |
+
.recycling-output {
|
| 332 |
+
background: var(--eco-card);
|
| 333 |
+
border: 1px solid var(--eco-border);
|
| 334 |
+
border-radius: var(--radius);
|
| 335 |
+
padding: 14px;
|
| 336 |
+
box-shadow: var(--eco-shadow);
|
| 337 |
+
}
|
| 338 |
+
|
| 339 |
+
/* ββ Product cards ββ */
|
| 340 |
+
.product-card {
|
| 341 |
+
background: var(--eco-card);
|
| 342 |
+
border-left: 3px solid var(--eco-secondary);
|
| 343 |
+
border-radius: 0 var(--radius) var(--radius) 0;
|
| 344 |
+
padding: 10px 14px;
|
| 345 |
+
margin: 5px 0;
|
| 346 |
+
box-shadow: var(--eco-shadow);
|
| 347 |
+
font-size: 0.86rem;
|
| 348 |
+
color: var(--eco-text);
|
| 349 |
+
}
|
| 350 |
+
|
| 351 |
+
/* ββ Profile card ββ */
|
| 352 |
+
.profile-card {
|
| 353 |
+
background: var(--eco-card);
|
| 354 |
+
border: 1px solid var(--eco-border);
|
| 355 |
+
border-radius: var(--radius);
|
| 356 |
+
padding: 14px;
|
| 357 |
+
box-shadow: var(--eco-shadow);
|
| 358 |
+
}
|
| 359 |
+
|
| 360 |
+
/* ββ Disclaimer ββ */
|
| 361 |
+
.disclaimer {
|
| 362 |
+
font-size: 0.72rem;
|
| 363 |
+
color: var(--eco-muted);
|
| 364 |
+
text-align: center;
|
| 365 |
+
padding: 8px 0 4px;
|
| 366 |
+
border-top: 1px solid var(--eco-border);
|
| 367 |
+
margin-top: 10px;
|
| 368 |
+
}
|
| 369 |
+
"""
|
| 370 |
+
|
| 371 |
+
# ---------------------------------------------------------------------------
|
| 372 |
+
# Static HTML β clean light header, no dark-mode button
|
| 373 |
+
# ---------------------------------------------------------------------------
|
| 374 |
+
HEADER_HTML = """
|
| 375 |
+
<div class="eco-header">
|
| 376 |
+
<h1>EcoAgent</h1>
|
| 377 |
+
<p>Your AI guide to greener living β personalized, practical, powered by IBM Granite and watsonx.ai</p>
|
| 378 |
+
<span class="eco-model-badge">ibm/granite-4-h-small</span>
|
| 379 |
+
</div>
|
| 380 |
+
"""
|
| 381 |
+
|
| 382 |
+
DISCLAIMER_HTML = """
|
| 383 |
+
<div class="disclaimer">
|
| 384 |
+
Answers are AI-generated by IBM Granite. Impact figures marked [Lookup] are from IPCC/BEE/CPCB data;
|
| 385 |
+
[Estimate] figures are model-generated. Verify government schemes at official sources.
|
| 386 |
+
Not financial or legal advice.
|
| 387 |
+
</div>
|
| 388 |
+
"""
|
| 389 |
+
|
| 390 |
+
# ---------------------------------------------------------------------------
|
| 391 |
+
# Quick-action chips for logging eco actions
|
| 392 |
+
# ---------------------------------------------------------------------------
|
| 393 |
+
ACTION_CHIPS = [
|
| 394 |
+
("cloth_bags", "Used cloth bags"),
|
| 395 |
+
("led_bulbs", "Switched to LED"),
|
| 396 |
+
("short_shower", "Short shower"),
|
| 397 |
+
("no_plastic_bottles", "Used steel bottle"),
|
| 398 |
+
("public_transport", "Public transport"),
|
| 399 |
+
("composting", "Composted waste"),
|
| 400 |
+
("segregate_waste", "Segregated waste"),
|
| 401 |
+
("seasonal_local_produce","Bought local produce"),
|
| 402 |
+
("line_dry_clothes", "Line-dried clothes"),
|
| 403 |
+
("fix_water_leaks", "Fixed a water leak"),
|
| 404 |
+
]
|
| 405 |
+
|
| 406 |
+
EXAMPLE_PROMPTS = [
|
| 407 |
+
"How can I reduce plastic use in my Indian kitchen?",
|
| 408 |
+
"What government schemes help with solar panel installation in India?",
|
| 409 |
+
"Give me a week-long plan to reduce my family's water usage.",
|
| 410 |
+
"How do I properly dispose of old mobile phones and laptops?",
|
| 411 |
+
"What are the most eco-friendly travel options in a Tier-2 Indian city?",
|
| 412 |
+
]
|
| 413 |
+
|
| 414 |
+
|
| 415 |
+
# ---------------------------------------------------------------------------
|
| 416 |
+
# Dashboard HTML builder
|
| 417 |
+
# ---------------------------------------------------------------------------
|
| 418 |
+
def _build_dashboard_html(impact: dict, profile: dict) -> str:
|
| 419 |
+
score = impact.get("eco_score", 0)
|
| 420 |
+
co2 = impact.get("co2_kg_year", 0.0)
|
| 421 |
+
water = impact.get("water_L_day", 0.0)
|
| 422 |
+
waste = impact.get("waste_kg_year", 0.0)
|
| 423 |
+
actions = impact.get("actions_count", 0)
|
| 424 |
+
location = profile.get("location", "India") if profile else "India"
|
| 425 |
+
members = profile.get("members", 1) if profile else 1
|
| 426 |
+
|
| 427 |
+
return f"""
|
| 428 |
+
<div style="font-family:Inter,sans-serif;color:#1c1c1e;">
|
| 429 |
+
<div style="display:flex;gap:14px;flex-wrap:wrap;margin-bottom:14px;align-items:stretch;">
|
| 430 |
+
<div class="metric-card" style="flex:1;min-width:150px;">
|
| 431 |
+
<div class="score-ring-wrap">
|
| 432 |
+
<div class="score-ring" style="--pct:{score};">
|
| 433 |
+
<span class="score-number">{score}</span>
|
| 434 |
+
</div>
|
| 435 |
+
<span class="score-label">Eco Score</span>
|
| 436 |
+
<span class="score-count">{actions} action{'s' if actions != 1 else ''} logged</span>
|
| 437 |
+
</div>
|
| 438 |
+
</div>
|
| 439 |
+
<div class="metric-card" style="flex:1;min-width:150px;">
|
| 440 |
+
<div class="metric-value">{members}</div>
|
| 441 |
+
<div class="metric-unit">household member{'s' if members != 1 else ''}</div>
|
| 442 |
+
<div class="metric-label">{location}</div>
|
| 443 |
+
</div>
|
| 444 |
+
</div>
|
| 445 |
+
<p style="font-size:0.76rem;color:#6b6b6f;margin:0 0 8px;font-weight:600;text-transform:uppercase;letter-spacing:0.5px;">
|
| 446 |
+
Estimated annual savings if logged actions are sustained
|
| 447 |
+
</p>
|
| 448 |
+
<div style="display:flex;gap:10px;flex-wrap:wrap;">
|
| 449 |
+
<div class="metric-card" style="flex:1;min-width:130px;">
|
| 450 |
+
<div class="metric-value">{co2:,.0f}</div>
|
| 451 |
+
<div class="metric-unit">kg CO₂</div>
|
| 452 |
+
<div class="metric-label">Carbon Avoided</div>
|
| 453 |
+
</div>
|
| 454 |
+
<div class="metric-card" style="flex:1;min-width:130px;">
|
| 455 |
+
<div class="metric-value">{water:,.0f}</div>
|
| 456 |
+
<div class="metric-unit">L / day</div>
|
| 457 |
+
<div class="metric-label">Water Saved</div>
|
| 458 |
+
</div>
|
| 459 |
+
<div class="metric-card" style="flex:1;min-width:130px;">
|
| 460 |
+
<div class="metric-value">{waste:,.0f}</div>
|
| 461 |
+
<div class="metric-unit">kg / year</div>
|
| 462 |
+
<div class="metric-label">Waste Diverted</div>
|
| 463 |
+
</div>
|
| 464 |
+
</div>
|
| 465 |
+
{_equivalencies_html(co2, water) if co2 > 0 or water > 0 else ""}
|
| 466 |
+
{_actions_html(impact)}
|
| 467 |
+
</div>
|
| 468 |
+
"""
|
| 469 |
+
|
| 470 |
+
|
| 471 |
+
def _equivalencies_html(co2: float, water: float) -> str:
|
| 472 |
+
lines = []
|
| 473 |
+
if co2 > 0:
|
| 474 |
+
trees = round(co2 / 21)
|
| 475 |
+
lines.append(f"CO₂ savings equivalent to planting <b>{trees} trees</b> per year")
|
| 476 |
+
if water > 0:
|
| 477 |
+
baths = round(water / 150)
|
| 478 |
+
lines.append(f"Water savings equivalent to <b>{baths} bucket baths</b> per day")
|
| 479 |
+
if not lines:
|
| 480 |
+
return ""
|
| 481 |
+
return (
|
| 482 |
+
"<div style='background:#f0f7f3;border:1px solid #e4e4e7;border-radius:6px;"
|
| 483 |
+
"padding:10px 14px;margin-top:12px;font-size:0.82rem;color:#2e7d50;'>"
|
| 484 |
+
+ "<br>".join(lines)
|
| 485 |
+
+ "</div>"
|
| 486 |
+
)
|
| 487 |
+
|
| 488 |
+
|
| 489 |
+
def _actions_html(impact: dict) -> str:
|
| 490 |
+
count = impact.get("actions_count", 0)
|
| 491 |
+
if count == 0:
|
| 492 |
+
return (
|
| 493 |
+
"<p style='color:#6b6b6f;font-size:0.82rem;margin-top:10px;'>"
|
| 494 |
+
"No actions logged yet. Chat with EcoAgent, then use the action buttons "
|
| 495 |
+
"to log what you have done today.</p>"
|
| 496 |
+
)
|
| 497 |
+
return (
|
| 498 |
+
f"<p style='color:#6b6b6f;font-size:0.80rem;margin-top:10px;'>"
|
| 499 |
+
f"You have logged <b>{count} unique action{'s' if count != 1 else ''}</b> this session. "
|
| 500 |
+
f"Each sustained action compounds over the year.</p>"
|
| 501 |
+
)
|
| 502 |
+
|
| 503 |
+
|
| 504 |
+
# ---------------------------------------------------------------------------
|
| 505 |
+
# Recycling products HTML builder
|
| 506 |
+
# ---------------------------------------------------------------------------
|
| 507 |
+
def _build_products_html(material: str) -> str:
|
| 508 |
+
recs = PRODUCT_RECS.get(material, [])
|
| 509 |
+
if not recs:
|
| 510 |
+
return ""
|
| 511 |
+
items = "".join(f'<div class="product-card">{r}</div>' for r in recs)
|
| 512 |
+
return (
|
| 513 |
+
f"<div style='margin-top:14px;'>"
|
| 514 |
+
f"<p style='font-weight:600;color:#2e7d50;font-size:0.86rem;margin-bottom:4px;'>"
|
| 515 |
+
f"Eco-friendly alternatives and resources for {material}:</p>"
|
| 516 |
+
f"{items}</div>"
|
| 517 |
+
)
|
| 518 |
+
|
| 519 |
+
|
| 520 |
+
# ---------------------------------------------------------------------------
|
| 521 |
+
# Profile summary HTML builder
|
| 522 |
+
# ---------------------------------------------------------------------------
|
| 523 |
+
def _build_profile_html(profile: dict) -> str:
|
| 524 |
+
if not profile or not profile.get("name"):
|
| 525 |
+
return "<p style='color:#6b6b6f;font-size:0.84rem;'>No profile saved yet.</p>"
|
| 526 |
+
habits = profile.get("habits", [])
|
| 527 |
+
habit_list = (
|
| 528 |
+
"<ul style='margin:4px 0;padding-left:18px;font-size:0.82rem;'>"
|
| 529 |
+
+ "".join(f"<li>{h}</li>" for h in habits)
|
| 530 |
+
+ "</ul>"
|
| 531 |
+
if habits
|
| 532 |
+
else "<p style='color:#6b6b6f;font-size:0.82rem;'>No habits recorded.</p>"
|
| 533 |
+
)
|
| 534 |
+
return f"""
|
| 535 |
+
<div class="profile-card">
|
| 536 |
+
<p style="font-size:0.93rem;font-weight:700;color:#2e7d50;margin:0 0 6px;">{profile['name']}</p>
|
| 537 |
+
<p style="font-size:0.82rem;margin:2px 0;"><b>Location:</b> {profile.get('location','β')}</p>
|
| 538 |
+
<p style="font-size:0.82rem;margin:2px 0;"><b>Members:</b> {profile.get('members',1)}</p>
|
| 539 |
+
<p style="font-size:0.82rem;margin:6px 0 2px;"><b>Current eco habits:</b></p>
|
| 540 |
+
{habit_list}
|
| 541 |
+
</div>
|
| 542 |
+
"""
|
| 543 |
+
|
| 544 |
+
|
| 545 |
+
# ---------------------------------------------------------------------------
|
| 546 |
+
# Core chat callback
|
| 547 |
+
# ---------------------------------------------------------------------------
|
| 548 |
+
def chat_submit(
|
| 549 |
+
message: str,
|
| 550 |
+
history: list,
|
| 551 |
+
profile_state: dict,
|
| 552 |
+
actions_state: list,
|
| 553 |
+
):
|
| 554 |
+
"""Handle a user message: call Granite, update history.
|
| 555 |
+
|
| 556 |
+
Gradio 6.x passes history as list[dict] where content is a list of
|
| 557 |
+
content blocks: [{"role": "user", "content": [{"type": "text", "text": "..."}]}].
|
| 558 |
+
We extract plain text for the API call and return structured blocks for display.
|
| 559 |
+
"""
|
| 560 |
+
if not message or not message.strip():
|
| 561 |
+
return history, profile_state, actions_state, _score_html(actions_state)
|
| 562 |
+
|
| 563 |
+
# Gradio 6 delivers history with structured content blocks
|
| 564 |
+
# Extract plain text for the watsonx API call
|
| 565 |
+
messages: list[dict] = []
|
| 566 |
+
for turn in history:
|
| 567 |
+
role = turn.get("role", "")
|
| 568 |
+
content = turn.get("content", "")
|
| 569 |
+
if role in ("user", "assistant") and content:
|
| 570 |
+
# Content can be a string or a list of content blocks
|
| 571 |
+
if isinstance(content, list):
|
| 572 |
+
text = " ".join(
|
| 573 |
+
block.get("text", "") for block in content
|
| 574 |
+
if isinstance(block, dict) and block.get("type") == "text"
|
| 575 |
+
)
|
| 576 |
+
else:
|
| 577 |
+
text = str(content)
|
| 578 |
+
if text:
|
| 579 |
+
messages.append({"role": role, "content": text})
|
| 580 |
+
|
| 581 |
+
messages.append({"role": "user", "content": message})
|
| 582 |
+
|
| 583 |
+
try:
|
| 584 |
+
reply = get_eco_answer(messages, profile_state)
|
| 585 |
+
except EnvironmentError as exc:
|
| 586 |
+
reply = (
|
| 587 |
+
"**Setup required.**\n\n"
|
| 588 |
+
f"{exc}\n\n"
|
| 589 |
+
"Please set your credentials in `.env` and restart the app."
|
| 590 |
+
)
|
| 591 |
+
except RuntimeError as exc:
|
| 592 |
+
logger.error("watsonx error: %s", exc)
|
| 593 |
+
reply = (
|
| 594 |
+
"**EcoAgent is temporarily unavailable.**\n\n"
|
| 595 |
+
f"Error: {exc}\n\n"
|
| 596 |
+
"Please check your internet connection or try again in a moment."
|
| 597 |
+
)
|
| 598 |
+
except Exception as exc: # noqa: BLE001
|
| 599 |
+
logger.exception("Unexpected error in chat_submit")
|
| 600 |
+
reply = f"An unexpected error occurred ({type(exc).__name__}). Please try again."
|
| 601 |
+
|
| 602 |
+
# Append in Gradio 6 structured content block format
|
| 603 |
+
history = history + [
|
| 604 |
+
{"role": "user", "content": [{"type": "text", "text": message}]},
|
| 605 |
+
{"role": "assistant", "content": [{"type": "text", "text": reply}]},
|
| 606 |
+
]
|
| 607 |
+
return history, profile_state, actions_state, _score_html(actions_state)
|
| 608 |
+
|
| 609 |
+
|
| 610 |
+
def _score_html(actions_state: list) -> str:
|
| 611 |
+
impact = compute_session_impact(actions_state)
|
| 612 |
+
score = impact["eco_score"]
|
| 613 |
+
count = impact["actions_count"]
|
| 614 |
+
return (
|
| 615 |
+
f"<div class='score-ring-wrap' style='padding:6px 0;'>"
|
| 616 |
+
f"<div class='score-ring' style='--pct:{score};width:72px;height:72px;'>"
|
| 617 |
+
f"<span class='score-number'>{score}</span></div>"
|
| 618 |
+
f"<span class='score-label'>Eco Score</span>"
|
| 619 |
+
f"<span class='score-count'>{count} action{'s' if count != 1 else ''}</span></div>"
|
| 620 |
+
)
|
| 621 |
+
|
| 622 |
+
|
| 623 |
+
def log_action(slug: str, actions_state: list) -> tuple[list, str]:
|
| 624 |
+
"""Toggle an action slug in the logged actions list."""
|
| 625 |
+
if slug in actions_state:
|
| 626 |
+
actions_state = [a for a in actions_state if a != slug]
|
| 627 |
+
else:
|
| 628 |
+
actions_state = actions_state + [slug]
|
| 629 |
+
return actions_state, _score_html(actions_state)
|
| 630 |
+
|
| 631 |
+
|
| 632 |
+
def update_dashboard(actions_state: list, profile_state: dict) -> str:
|
| 633 |
+
members = profile_state.get("members", 1) if profile_state else 1
|
| 634 |
+
impact = compute_session_impact(actions_state, members)
|
| 635 |
+
return _build_dashboard_html(impact, profile_state or {})
|
| 636 |
+
|
| 637 |
+
|
| 638 |
+
def get_recycling(material: str, city: str) -> tuple[str, str]:
|
| 639 |
+
"""Fetch guide + product recs for a material/city combination."""
|
| 640 |
+
if not material or not city:
|
| 641 |
+
return "Please select both a material and a city.", ""
|
| 642 |
+
try:
|
| 643 |
+
guide = get_recycling_guide(material, city)
|
| 644 |
+
except EnvironmentError as exc:
|
| 645 |
+
guide = f"Setup required: {exc}"
|
| 646 |
+
except RuntimeError as exc:
|
| 647 |
+
guide = f"Could not fetch guide: {exc}"
|
| 648 |
+
products_html = _build_products_html(material)
|
| 649 |
+
return guide, products_html
|
| 650 |
+
|
| 651 |
+
|
| 652 |
+
def save_profile(
|
| 653 |
+
name: str,
|
| 654 |
+
location: str,
|
| 655 |
+
members: int,
|
| 656 |
+
habits: list,
|
| 657 |
+
actions_state: list,
|
| 658 |
+
) -> tuple[dict, str, str]:
|
| 659 |
+
"""Save household profile and return updated state + HTML summary."""
|
| 660 |
+
profile = {
|
| 661 |
+
"name": name.strip() if name else "My Household",
|
| 662 |
+
"location": location,
|
| 663 |
+
"members": int(members),
|
| 664 |
+
"habits": habits,
|
| 665 |
+
}
|
| 666 |
+
summary = _build_profile_html(profile)
|
| 667 |
+
dashboard = update_dashboard(actions_state, profile)
|
| 668 |
+
return profile, summary, dashboard
|
| 669 |
+
|
| 670 |
+
|
| 671 |
+
# ===========================================================================
|
| 672 |
+
# GRADIO BLOCKS APP
|
| 673 |
+
# ===========================================================================
|
| 674 |
+
with gr.Blocks(title="EcoAgent β Eco Lifestyle Assistant") as demo:
|
| 675 |
+
|
| 676 |
+
profile_state = gr.State({})
|
| 677 |
+
actions_state = gr.State([])
|
| 678 |
+
|
| 679 |
+
gr.HTML(HEADER_HTML)
|
| 680 |
+
|
| 681 |
+
with gr.Tabs(elem_classes="tab-nav"):
|
| 682 |
+
|
| 683 |
+
# ================================================================
|
| 684 |
+
# TAB 1: CHAT
|
| 685 |
+
# ================================================================
|
| 686 |
+
with gr.Tab("Chat"):
|
| 687 |
+
with gr.Row():
|
| 688 |
+
with gr.Column(scale=3):
|
| 689 |
+
chatbot = gr.Chatbot(
|
| 690 |
+
height=430,
|
| 691 |
+
show_label=False,
|
| 692 |
+
elem_id="chatbot",
|
| 693 |
+
placeholder=(
|
| 694 |
+
"<div style='text-align:center;padding:40px 20px;background:#fcfcfd;border-radius:6px;'>"
|
| 695 |
+
"<p style='font-size:1rem;font-weight:600;margin:0 0 6px;color:#2e7d50;'>Welcome to EcoAgent</p>"
|
| 696 |
+
"<p style='font-size:0.84rem;margin:0;color:#4a4a4e;'>Ask me anything about sustainable living, "
|
| 697 |
+
"eco travel, recycling, or Indian government green schemes.</p>"
|
| 698 |
+
"</div>"
|
| 699 |
+
),
|
| 700 |
+
)
|
| 701 |
+
with gr.Row():
|
| 702 |
+
msg_input = gr.Textbox(
|
| 703 |
+
placeholder="Ask EcoAgent a question...",
|
| 704 |
+
show_label=False,
|
| 705 |
+
scale=5,
|
| 706 |
+
lines=1,
|
| 707 |
+
)
|
| 708 |
+
send_btn = gr.Button("Send", variant="primary", scale=1)
|
| 709 |
+
|
| 710 |
+
gr.Markdown("**Quick start questions:**")
|
| 711 |
+
with gr.Row():
|
| 712 |
+
for prompt in EXAMPLE_PROMPTS[:3]:
|
| 713 |
+
ex_btn = gr.Button(prompt, size="sm", variant="secondary")
|
| 714 |
+
ex_btn.click(lambda p=prompt: p, outputs=msg_input)
|
| 715 |
+
with gr.Row():
|
| 716 |
+
for prompt in EXAMPLE_PROMPTS[3:]:
|
| 717 |
+
ex_btn = gr.Button(prompt, size="sm", variant="secondary")
|
| 718 |
+
ex_btn.click(lambda p=prompt: p, outputs=msg_input)
|
| 719 |
+
|
| 720 |
+
with gr.Column(scale=1, min_width=190):
|
| 721 |
+
score_display = gr.HTML(_score_html([]))
|
| 722 |
+
gr.Markdown("**Log today's actions:**")
|
| 723 |
+
chip_btns = []
|
| 724 |
+
for slug, label in ACTION_CHIPS:
|
| 725 |
+
btn = gr.Button(label, size="sm", variant="secondary")
|
| 726 |
+
chip_btns.append((slug, btn))
|
| 727 |
+
|
| 728 |
+
def _submit(message, history, profile, actions):
|
| 729 |
+
return chat_submit(message, history, profile, actions)
|
| 730 |
+
|
| 731 |
+
send_btn.click(
|
| 732 |
+
_submit,
|
| 733 |
+
inputs=[msg_input, chatbot, profile_state, actions_state],
|
| 734 |
+
outputs=[chatbot, profile_state, actions_state, score_display],
|
| 735 |
+
).then(lambda: "", outputs=msg_input)
|
| 736 |
+
|
| 737 |
+
msg_input.submit(
|
| 738 |
+
_submit,
|
| 739 |
+
inputs=[msg_input, chatbot, profile_state, actions_state],
|
| 740 |
+
outputs=[chatbot, profile_state, actions_state, score_display],
|
| 741 |
+
).then(lambda: "", outputs=msg_input)
|
| 742 |
+
|
| 743 |
+
for slug, btn in chip_btns:
|
| 744 |
+
btn.click(
|
| 745 |
+
lambda a, s=slug: log_action(s, a),
|
| 746 |
+
inputs=[actions_state],
|
| 747 |
+
outputs=[actions_state, score_display],
|
| 748 |
+
)
|
| 749 |
+
|
| 750 |
+
# ================================================================
|
| 751 |
+
# TAB 2: DASHBOARD
|
| 752 |
+
# ================================================================
|
| 753 |
+
with gr.Tab("Dashboard"):
|
| 754 |
+
gr.Markdown(
|
| 755 |
+
"### Your Eco Impact Dashboard\n"
|
| 756 |
+
"Log actions in the Chat tab to see your estimated savings grow."
|
| 757 |
+
)
|
| 758 |
+
dashboard_html = gr.HTML(
|
| 759 |
+
_build_dashboard_html(compute_session_impact([]), {}),
|
| 760 |
+
)
|
| 761 |
+
refresh_btn = gr.Button("Refresh Dashboard", variant="secondary")
|
| 762 |
+
refresh_btn.click(
|
| 763 |
+
update_dashboard,
|
| 764 |
+
inputs=[actions_state, profile_state],
|
| 765 |
+
outputs=dashboard_html,
|
| 766 |
+
)
|
| 767 |
+
|
| 768 |
+
# ================================================================
|
| 769 |
+
# TAB 3: RECYCLING GUIDE
|
| 770 |
+
# ================================================================
|
| 771 |
+
with gr.Tab("Recycling Guide"):
|
| 772 |
+
gr.Markdown(
|
| 773 |
+
"### Local Recycling Guide\n"
|
| 774 |
+
"Select a waste material and your city to get India-specific "
|
| 775 |
+
"recycling instructions and eco-friendly product alternatives."
|
| 776 |
+
)
|
| 777 |
+
with gr.Row():
|
| 778 |
+
material_dd = gr.Dropdown(
|
| 779 |
+
choices=list(PRODUCT_RECS.keys()),
|
| 780 |
+
label="Waste Material",
|
| 781 |
+
value="E-waste",
|
| 782 |
+
scale=1,
|
| 783 |
+
)
|
| 784 |
+
city_dd = gr.Dropdown(
|
| 785 |
+
choices=INDIAN_CITIES,
|
| 786 |
+
label="Your City",
|
| 787 |
+
value="Bangalore",
|
| 788 |
+
scale=1,
|
| 789 |
+
)
|
| 790 |
+
guide_btn = gr.Button("Get Recycling Guide", variant="primary", scale=1)
|
| 791 |
+
|
| 792 |
+
guide_output = gr.Markdown(
|
| 793 |
+
"*Select a material and city above, then click Get Recycling Guide.*",
|
| 794 |
+
label="Recycling Instructions",
|
| 795 |
+
elem_classes="recycling-output",
|
| 796 |
+
)
|
| 797 |
+
products_output = gr.HTML("", label="Eco-Friendly Alternatives")
|
| 798 |
+
|
| 799 |
+
guide_btn.click(
|
| 800 |
+
get_recycling,
|
| 801 |
+
inputs=[material_dd, city_dd],
|
| 802 |
+
outputs=[guide_output, products_output],
|
| 803 |
+
)
|
| 804 |
+
material_dd.change(
|
| 805 |
+
lambda m: _build_products_html(m),
|
| 806 |
+
inputs=material_dd,
|
| 807 |
+
outputs=products_output,
|
| 808 |
+
)
|
| 809 |
+
|
| 810 |
+
# ================================================================
|
| 811 |
+
# TAB 4: PROFILE
|
| 812 |
+
# ================================================================
|
| 813 |
+
with gr.Tab("Profile"):
|
| 814 |
+
gr.Markdown(
|
| 815 |
+
"### Household Profile\n"
|
| 816 |
+
"Set your household details so EcoAgent can give personalised, "
|
| 817 |
+
"family-scale advice relevant to your location."
|
| 818 |
+
)
|
| 819 |
+
with gr.Row():
|
| 820 |
+
with gr.Column(scale=2):
|
| 821 |
+
hh_name = gr.Textbox(
|
| 822 |
+
label="Household Name",
|
| 823 |
+
placeholder="e.g. The Sharma Family",
|
| 824 |
+
)
|
| 825 |
+
with gr.Row():
|
| 826 |
+
hh_location = gr.Dropdown(
|
| 827 |
+
choices=INDIAN_CITIES + [
|
| 828 |
+
"Other - North India", "Other - South India",
|
| 829 |
+
"Other - East India", "Other - West India",
|
| 830 |
+
"Rural area",
|
| 831 |
+
],
|
| 832 |
+
label="City / Location",
|
| 833 |
+
value="Bangalore",
|
| 834 |
+
scale=2,
|
| 835 |
+
)
|
| 836 |
+
hh_members = gr.Slider(
|
| 837 |
+
minimum=1, maximum=12, value=4, step=1,
|
| 838 |
+
label="Household Members",
|
| 839 |
+
scale=2,
|
| 840 |
+
)
|
| 841 |
+
hh_habits = gr.CheckboxGroup(
|
| 842 |
+
choices=[
|
| 843 |
+
"Vegetarian / vegan diet",
|
| 844 |
+
"Use public transport regularly",
|
| 845 |
+
"Have solar panels installed",
|
| 846 |
+
"Compost kitchen waste",
|
| 847 |
+
"Use cloth / jute bags",
|
| 848 |
+
"Avoid single-use plastics",
|
| 849 |
+
"Harvest rainwater",
|
| 850 |
+
"Segregate wet and dry waste",
|
| 851 |
+
"Use LED bulbs throughout",
|
| 852 |
+
"Grow some food at home",
|
| 853 |
+
],
|
| 854 |
+
label="Current Eco Habits (select all that apply)",
|
| 855 |
+
)
|
| 856 |
+
save_btn = gr.Button("Save Profile", variant="primary")
|
| 857 |
+
|
| 858 |
+
with gr.Column(scale=1):
|
| 859 |
+
profile_summary = gr.HTML(
|
| 860 |
+
"<p style='color:#6b6b6f;font-size:0.84rem;'>"
|
| 861 |
+
"Fill in your details and click Save Profile.</p>",
|
| 862 |
+
)
|
| 863 |
+
|
| 864 |
+
# Save updates profile_state, profile HTML, and dashboard
|
| 865 |
+
save_btn.click(
|
| 866 |
+
save_profile,
|
| 867 |
+
inputs=[hh_name, hh_location, hh_members, hh_habits, actions_state],
|
| 868 |
+
outputs=[profile_state, profile_summary, dashboard_html],
|
| 869 |
+
)
|
| 870 |
+
|
| 871 |
+
# ββ Footer disclaimer ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 872 |
+
gr.HTML(DISCLAIMER_HTML)
|
| 873 |
+
|
| 874 |
+
|
| 875 |
+
# ---------------------------------------------------------------------------
|
| 876 |
+
# Entry point
|
| 877 |
+
# ---------------------------------------------------------------------------
|
| 878 |
+
if __name__ == "__main__":
|
| 879 |
+
eco_theme = gr.themes.Soft(
|
| 880 |
+
primary_hue="green",
|
| 881 |
+
secondary_hue="emerald",
|
| 882 |
+
neutral_hue="gray",
|
| 883 |
+
font="Inter, system-ui, sans-serif",
|
| 884 |
+
font_mono="Fira Code, monospace",
|
| 885 |
+
).set(
|
| 886 |
+
body_background_fill="#fcfcfd",
|
| 887 |
+
body_background_fill_dark="#fcfcfd",
|
| 888 |
+
body_text_color="#1c1c1e",
|
| 889 |
+
body_text_color_dark="#1c1c1e",
|
| 890 |
+
block_background_fill="#ffffff",
|
| 891 |
+
block_border_color="#e4e4e7",
|
| 892 |
+
block_label_text_color="#4a4a4e",
|
| 893 |
+
block_title_text_color="#1c1c1e",
|
| 894 |
+
input_background_fill="#ffffff",
|
| 895 |
+
input_border_color="#e4e4e7",
|
| 896 |
+
input_border_color_focus="#2e7d50",
|
| 897 |
+
button_primary_background_fill="#2e7d50",
|
| 898 |
+
button_primary_background_fill_hover="#256a42",
|
| 899 |
+
button_primary_border_color="#2e7d50",
|
| 900 |
+
button_primary_text_color="#ffffff",
|
| 901 |
+
button_secondary_background_fill="#f0f7f3",
|
| 902 |
+
button_secondary_background_fill_hover="#e4eff0",
|
| 903 |
+
button_secondary_border_color="#e4e4e7",
|
| 904 |
+
button_secondary_text_color="#1c1c1e",
|
| 905 |
+
checkbox_background_color="#ffffff",
|
| 906 |
+
checkbox_background_color_selected="#2e7d50",
|
| 907 |
+
checkbox_border_color="#e4e4e7",
|
| 908 |
+
checkbox_border_color_selected="#2e7d50",
|
| 909 |
+
checkbox_label_text_color="#1c1c1e",
|
| 910 |
+
checkbox_label_text_color_selected="#ffffff",
|
| 911 |
+
checkbox_label_background_fill="#f0f7f3",
|
| 912 |
+
checkbox_label_background_fill_hover="#e4eff0",
|
| 913 |
+
checkbox_label_background_fill_selected="#2e7d50",
|
| 914 |
+
checkbox_label_border_color="#e4e4e7",
|
| 915 |
+
checkbox_label_border_color_selected="#2e7d50",
|
| 916 |
+
checkbox_label_border_width="1px",
|
| 917 |
+
slider_color="#2e7d50",
|
| 918 |
+
)
|
| 919 |
+
|
| 920 |
+
demo.launch(
|
| 921 |
+
server_name="0.0.0.0",
|
| 922 |
+
server_port=int(os.environ.get("PORT", 7860)),
|
| 923 |
+
share=False,
|
| 924 |
+
theme=eco_theme,
|
| 925 |
+
css=CUSTOM_CSS,
|
| 926 |
+
)
|
ecoagent-plan.md
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# EcoAgent v2 β Complete Rebuild Plan
|
| 2 |
+
|
| 3 |
+
## Top-Level Overview
|
| 4 |
+
|
| 5 |
+
**Goal:** Rebuild EcoAgent from scratch as a full-featured Gradio web application using `ibm-watsonx-ai` SDK directly (ModelInference.chat() with `ibm/granite-4-h-small`). The IBM Orchestrate REST API integration is abandoned β it was not reachable via a public REST endpoint. The IBM Cloud API key (`CLOUD_API` from `.env.example`) works for direct watsonx.ai SDK calls.
|
| 6 |
+
|
| 7 |
+
**Classification:** Prompt-Engineered LLM Application (not RAG, not Agentic AI)
|
| 8 |
+
|
| 9 |
+
**What gets replaced/deleted:**
|
| 10 |
+
- `rag_pipeline.py` β replaced by `watsonx_client.py`
|
| 11 |
+
- `app.py` β fully rewritten
|
| 12 |
+
- `ibm-credentials.env` β replaced by `.env` (simpler var names)
|
| 13 |
+
- `requirements.txt` β updated
|
| 14 |
+
- `ecoagent-plan.md` β this file supersedes it
|
| 15 |
+
|
| 16 |
+
**Architecture:**
|
| 17 |
+
```
|
| 18 |
+
.env
|
| 19 |
+
ββ WATSONX_API_KEY, WATSONX_PROJECT_ID, WATSONX_URL
|
| 20 |
+
β
|
| 21 |
+
βΌ
|
| 22 |
+
watsonx_client.py β ModelInference wrapper, AGENT_INSTRUCTIONS, impact lookup table
|
| 23 |
+
β
|
| 24 |
+
βΌ
|
| 25 |
+
app.py β Gradio Blocks UI: Chat + Dashboard + Recycling + Profile
|
| 26 |
+
β
|
| 27 |
+
ββ gr.Blocks (ultra-light eco green theme)
|
| 28 |
+
ββ Chat tab β primary interaction, eco score streak
|
| 29 |
+
ββ Dashboard tab β COβ/water/waste savings visualised
|
| 30 |
+
ββ Recycling tab β material lookup β guidelines + product recommendations
|
| 31 |
+
ββ Profile tab β household members, habits, location (India-specific)
|
| 32 |
+
```
|
| 33 |
+
|
| 34 |
+
**IBM SDK usage (confirmed working):**
|
| 35 |
+
```python
|
| 36 |
+
from ibm_watsonx_ai import APIClient, Credentials
|
| 37 |
+
from ibm_watsonx_ai.foundation_models import ModelInference
|
| 38 |
+
model = ModelInference(model_id="ibm/granite-4-h-small", credentials=creds, project_id=...)
|
| 39 |
+
response = model.chat(messages=[{"role":"system","content":"..."}, {"role":"user","content":"..."}])
|
| 40 |
+
answer = response["choices"][0]["message"]["content"]
|
| 41 |
+
```
|
| 42 |
+
|
| 43 |
+
**Non-Goals:**
|
| 44 |
+
- No Exa MCP / web search (user dropped that requirement in the new prompt)
|
| 45 |
+
- No IBM Orchestrate REST API
|
| 46 |
+
- No FAISS / local embeddings
|
| 47 |
+
|
| 48 |
+
---
|
| 49 |
+
|
| 50 |
+
## Environment Variables (new `.env`)
|
| 51 |
+
|
| 52 |
+
| Variable | Value | Purpose |
|
| 53 |
+
|---|---|---|
|
| 54 |
+
| `WATSONX_API_KEY` | `CLOUD_API` value from `.env.example` | IBM Cloud API key for watsonx.ai |
|
| 55 |
+
| `WATSONX_PROJECT_ID` | User must fill in from watsonx.ai Studio | Required by ModelInference |
|
| 56 |
+
| `WATSONX_URL` | `https://eu-de.ml.cloud.ibm.com` | watsonx.ai eu-de (Frankfurt) endpoint |
|
| 57 |
+
|
| 58 |
+
**Region confirmed:** `eu-de` (Frankfurt) β platform at `https://eu-de.dataplatform.cloud.ibm.com`
|
| 59 |
+
**`ibm-credentials.env`**: Delete β fully superseded by `.env`.
|
| 60 |
+
|
| 61 |
+
---
|
| 62 |
+
|
| 63 |
+
## Sub-Tasks
|
| 64 |
+
|
| 65 |
+
---
|
| 66 |
+
|
| 67 |
+
### Sub-Task 1: Create `.env` and update `ibm-credentials.env`
|
| 68 |
+
|
| 69 |
+
**Intent:** Replace the sprawling `ibm-credentials.env` with a clean `.env` using the new variable names. Keep `ibm-credentials.env` as a legacy backup comment only.
|
| 70 |
+
|
| 71 |
+
**Expected Outcomes:**
|
| 72 |
+
- `.env` exists with `WATSONX_API_KEY`, `WATSONX_PROJECT_ID`, `WATSONX_URL`
|
| 73 |
+
- `.env.example` updated to document the new vars (without real values for PROJECT_ID)
|
| 74 |
+
- `ibm-credentials.env` cleared out or noted as superseded
|
| 75 |
+
|
| 76 |
+
**Todo List:**
|
| 77 |
+
1. Write `.env` with `CLOUD_API` value mapped to `WATSONX_API_KEY`
|
| 78 |
+
2. Set `WATSONX_URL=https://eu-de.ml.cloud.ibm.com`
|
| 79 |
+
3. Leave `WATSONX_PROJECT_ID=` blank (user must fill in from watsonx.ai Studio)
|
| 80 |
+
4. Update `.env.example` to document all three vars without values
|
| 81 |
+
|
| 82 |
+
**Status:** `[x] completed`
|
| 83 |
+
|
| 84 |
+
---
|
| 85 |
+
|
| 86 |
+
### Sub-Task 2: Create `watsonx_client.py`
|
| 87 |
+
|
| 88 |
+
**Intent:** All IBM watsonx.ai SDK logic in one file: `AGENT_INSTRUCTIONS` block, carbon impact lookup table, and `get_eco_answer(messages, profile)` function.
|
| 89 |
+
|
| 90 |
+
**Expected Outcomes:**
|
| 91 |
+
- `watsonx_client.py` is importable and `get_eco_answer()` returns a string
|
| 92 |
+
- `AGENT_INSTRUCTIONS` is a clearly labelled, easily editable multi-line string at the top
|
| 93 |
+
- Carbon/resource impact lookup table covers β₯15 common actions with COβ/water/waste values
|
| 94 |
+
- If SDK call fails, raises `RuntimeError` with a descriptive message
|
| 95 |
+
- `project_id` must be set; if missing raises a clear error at import time
|
| 96 |
+
|
| 97 |
+
**Todo List:**
|
| 98 |
+
1. `load_dotenv(".env")` at module top
|
| 99 |
+
2. Write `AGENT_INSTRUCTIONS` constant β covers:
|
| 100 |
+
- Persona/tone: friendly, concise, India-aware eco advisor
|
| 101 |
+
- Answer structure: quick tip β why it matters (1 line) β optional resource
|
| 102 |
+
- Safety rules: no invented stats, label estimates vs lookup values
|
| 103 |
+
- Sustainability focus areas: plastic, energy, water, travel, food, waste
|
| 104 |
+
- India-specific context: regional schemes (PM Surya Ghar, Swachh Bharat, FAME), local recycling norms, common household practices
|
| 105 |
+
3. Write `IMPACT_TABLE: dict[str, dict]` β keyed by action slug, values: `{co2_kg_year, water_L_day, waste_kg_year, label}`; cover actions like: cloth bags, LED bulbs, solar panels, composting, public transport, short showers, rainwater harvesting, etc.
|
| 106 |
+
4. Write `_build_system_prompt(profile: dict) -> str` β injects profile (location, members, habits) into the system message alongside `AGENT_INSTRUCTIONS`
|
| 107 |
+
5. Write `get_eco_answer(messages: list[dict], profile: dict) -> str`:
|
| 108 |
+
- Builds `[system_msg] + messages` list
|
| 109 |
+
- Calls `ModelInference.chat()` with `max_tokens=800, temperature=0.7`
|
| 110 |
+
- Returns `response["choices"][0]["message"]["content"]`
|
| 111 |
+
6. Write `get_recycling_guide(material: str, location: str) -> str` β a focused single-turn call to Granite to return recycling instructions for a material in an Indian city context
|
| 112 |
+
7. Startup validation: raise `EnvironmentError` if `WATSONX_API_KEY` or `WATSONX_URL` is missing
|
| 113 |
+
|
| 114 |
+
**Relevant Context:**
|
| 115 |
+
- SDK: `ibm_watsonx_ai` >= 1.1.15
|
| 116 |
+
- `Credentials(url=WATSONX_URL, api_key=WATSONX_API_KEY)`
|
| 117 |
+
- `APIClient(credentials, project_id)`
|
| 118 |
+
- `ModelInference(model_id="ibm/granite-4-h-small", credentials=creds, project_id=WATSONX_PROJECT_ID)`
|
| 119 |
+
- Chat response: `response["choices"][0]["message"]["content"]`
|
| 120 |
+
- `WATSONX_PROJECT_ID` may be empty during dev β handle gracefully
|
| 121 |
+
|
| 122 |
+
**Status:** `[x] completed`
|
| 123 |
+
|
| 124 |
+
---
|
| 125 |
+
|
| 126 |
+
### Sub-Task 3: Create `app.py` β Gradio Blocks UI
|
| 127 |
+
|
| 128 |
+
**Intent:** Full Gradio Blocks application with 4 tabs, ultra-light eco green theme, and session state.
|
| 129 |
+
|
| 130 |
+
**Expected Outcomes:**
|
| 131 |
+
- `python app.py` launches on `http://localhost:7860`
|
| 132 |
+
- Chat tab works end-to-end with Granite via `watsonx_client.get_eco_answer()`
|
| 133 |
+
- Dashboard tab shows live session-based eco score + cumulative COβ/water/waste savings
|
| 134 |
+
- Recycling tab has a material input + location dropdown + Granite-powered guide output
|
| 135 |
+
- Profile tab captures household members, location (Indian cities), habits
|
| 136 |
+
- Mobile-responsive via CSS
|
| 137 |
+
- All state is session-based (gr.State)
|
| 138 |
+
|
| 139 |
+
**Todo List:**
|
| 140 |
+
|
| 141 |
+
**3a. Custom CSS/JS:**
|
| 142 |
+
1. Define `CUSTOM_CSS` string: ultra-light eco green palette (`#fcfcfd` background, `#2e7d50` primary accent), card shadows, chat bubble styles, animated eco score ring
|
| 143 |
+
2. Define eco score calculation: `score = min(100, len(logged_actions) * 10)` per session
|
| 144 |
+
|
| 145 |
+
**3b. Chat Tab:**
|
| 146 |
+
1. `gr.Chatbot` with welcome placeholder and structured content blocks for Gradio 6.x
|
| 147 |
+
2. Message input + Send button
|
| 148 |
+
3. Under the chat: "π± Log this action" checkbox group β increments session action counter β updates eco score
|
| 149 |
+
4. Eco score display: circular progress indicator (CSS-only, value from gr.State)
|
| 150 |
+
5. Weekly streak counter (session days active β session-based approximation)
|
| 151 |
+
6. `chat_submit(message, history, profile_state, actions_state)` β calls `get_eco_answer()` β returns updated history + updated state
|
| 152 |
+
|
| 153 |
+
**3c. Dashboard Tab:**
|
| 154 |
+
1. Header: "Your Eco Impact This Session"
|
| 155 |
+
2. Three metric cards: COβ saved (kg), Water saved (L), Waste diverted (kg) β computed from logged actions via `IMPACT_TABLE`
|
| 156 |
+
3. Eco score gauge (HTML/CSS ring)
|
| 157 |
+
4. Household summary: member count, location
|
| 158 |
+
5. `update_dashboard(actions_state, profile_state)` β returns HTML string of metric cards
|
| 159 |
+
|
| 160 |
+
**3d. Recycling Guide Tab:**
|
| 161 |
+
1. Material dropdown: Paper, Plastic, Glass, E-waste, Metal, Organic, Batteries, Clothing
|
| 162 |
+
2. City dropdown: 15 major Indian cities
|
| 163 |
+
3. "Get Guide" button β calls `get_recycling_guide()` β renders in `gr.Markdown`
|
| 164 |
+
4. Below: eco-friendly product recommendation section (hardcoded lookup per material category)
|
| 165 |
+
|
| 166 |
+
**3e. Profile Tab:**
|
| 167 |
+
1. Household name input
|
| 168 |
+
2. Location dropdown (Indian states + major cities)
|
| 169 |
+
3. Member count slider (1β10)
|
| 170 |
+
4. Habits checklist: vegetarian diet, uses public transport, has solar panels, composts, uses cloth bags, avoids single-use plastic, harvests rainwater
|
| 171 |
+
5. "Save Profile" button β updates `profile_state`
|
| 172 |
+
6. Profile summary card shown after save
|
| 173 |
+
|
| 174 |
+
**3f. App assembly:**
|
| 175 |
+
1. `gr.Blocks()` with `css` parameter in `launch()` for Gradio 6.x compatibility
|
| 176 |
+
2. All tabs inside `gr.Tabs`
|
| 177 |
+
3. `demo.launch(server_name="0.0.0.0", server_port=int(os.environ.get("PORT", 7860)))`
|
| 178 |
+
4. `if __name__ == "__main__":` guard
|
| 179 |
+
|
| 180 |
+
**Status:** `[x] completed`
|
| 181 |
+
|
| 182 |
+
---
|
| 183 |
+
|
| 184 |
+
### Sub-Task 4: Update `requirements.txt`
|
| 185 |
+
|
| 186 |
+
**Intent:** Minimal, pinned dependency list for the new architecture.
|
| 187 |
+
|
| 188 |
+
**Expected Outcomes:**
|
| 189 |
+
- Only packages actually used are listed
|
| 190 |
+
- Installs cleanly on Python 3.10+ in a fresh HF Spaces environment
|
| 191 |
+
|
| 192 |
+
**Todo List:**
|
| 193 |
+
1. Keep: `gradio`, `ibm-watsonx-ai`, `python-dotenv`, `requests`
|
| 194 |
+
2. Remove: `langchain`, `langchain-community`, `faiss-cpu`, `sentence-transformers` (not used)
|
| 195 |
+
3. Pin versions based on what is installed in `.venv`
|
| 196 |
+
|
| 197 |
+
**Status:** `[x] completed`
|
| 198 |
+
|
| 199 |
+
---
|
| 200 |
+
|
| 201 |
+
### Sub-Task 5: Update `README.md` and `AGENTS.md`
|
| 202 |
+
|
| 203 |
+
**Intent:** Update docs to reflect the new architecture. HF Spaces front-matter, new env var names, deployment steps.
|
| 204 |
+
|
| 205 |
+
**Todo List:**
|
| 206 |
+
1. Update `README.md` HF Spaces YAML: keep gradio SDK, update description
|
| 207 |
+
2. Document the 3 new env vars (`WATSONX_API_KEY`, `WATSONX_PROJECT_ID`, `WATSONX_URL`)
|
| 208 |
+
3. Add "How to get a `WATSONX_PROJECT_ID`" section (watsonx.ai Studio β create project β copy ID)
|
| 209 |
+
4. Update `AGENTS.md` to note `rag_pipeline.py` is deleted, new entry point is `watsonx_client.py`
|
| 210 |
+
|
| 211 |
+
**Status:** `[x] completed`
|
| 212 |
+
|
| 213 |
+
---
|
| 214 |
+
|
| 215 |
+
## Implementation Order
|
| 216 |
+
|
| 217 |
+
```
|
| 218 |
+
Sub-Task 1 (.env) β completed
|
| 219 |
+
β
|
| 220 |
+
Sub-Task 2 (watsonx_client.py) β completed
|
| 221 |
+
β
|
| 222 |
+
Sub-Task 3 (app.py) β completed
|
| 223 |
+
β
|
| 224 |
+
Sub-Task 4 (requirements.txt) β completed
|
| 225 |
+
β
|
| 226 |
+
Sub-Task 5 (README + AGENTS.md) β completed
|
| 227 |
+
```
|
| 228 |
+
|
| 229 |
+
---
|
| 230 |
+
|
| 231 |
+
## Files Deleted After Implementation
|
| 232 |
+
|
| 233 |
+
- `rag_pipeline.py` β replaced by `watsonx_client.py`
|
| 234 |
+
- `embed.txt` β was a debugging artifact, no longer needed
|
| 235 |
+
- `ibm-credentials.env` β replaced by `.env`
|
| 236 |
+
- `__pycache__/` β stale cache from old modules
|
| 237 |
+
|
| 238 |
+
---
|
| 239 |
+
|
| 240 |
+
## UI Theme
|
| 241 |
+
|
| 242 |
+
Ultra-light eco green theme (implemented in `app.py`):
|
| 243 |
+
- Background: `#fcfcfd` (near white)
|
| 244 |
+
- Primary accent: `#2e7d50` (green)
|
| 245 |
+
- Cards: `#ffffff` with subtle shadows
|
| 246 |
+
- Text: `#1c1c1e` (near black), muted: `#4a4a4e`
|
| 247 |
+
- Borders: `#e4e4e7` (light gray)
|
| 248 |
+
- CheckboxGroup styled as selectable pills/chips
|
| 249 |
+
- Chatbot with welcome placeholder and white-flash prevention CSS
|
| 250 |
+
|
| 251 |
+
---
|
| 252 |
+
|
| 253 |
+
## Prompt Engineering Pattern
|
| 254 |
+
|
| 255 |
+
This project uses **prompt engineering** to transform IBM Granite into a domain-specific eco advisor:
|
| 256 |
+
|
| 257 |
+
- **System Prompt:** 86-line `AGENT_INSTRUCTIONS` defining persona, output format, focus areas, and guardrails
|
| 258 |
+
- **Static Knowledge:** `IMPACT_TABLE` (20 eco actions) and `PRODUCT_RECS` (8 material categories) injected via prompt
|
| 259 |
+
- **Dynamic Context:** Household profile (members, location, habits) injected per session
|
| 260 |
+
- **Output Format:** Fixed 4-part structure (Quick Tip β Why it Matters β Impact β Optional Resource)
|
| 261 |
+
- **Guardrails:** Never invent stats, label [Lookup] vs [Estimate], no medical/financial advice
|
| 262 |
+
|
| 263 |
+
---
|
| 264 |
+
|
| 265 |
+
## Open Questions / Notes
|
| 266 |
+
|
| 267 |
+
1. **`WATSONX_PROJECT_ID`** is required by the SDK for `ModelInference`. The user must create a project in watsonx.ai Studio (https://eu-de.dataplatform.cloud.ibm.com/) and copy the project ID. The app shows a clear setup error if it is missing.
|
| 268 |
+
2. **`ibm/granite-4-h-small`** is the primary model. Fallback: `ibm/granite-3-3-8b-instruct` if granite-4-h-small is not available.
|
| 269 |
+
3. **Session state**: All eco score and action logging is in-memory `gr.State` β no database. This resets on page refresh, which is acceptable per the spec ("session-based is fine").
|
| 270 |
+
4. **Impact numbers in `IMPACT_TABLE`**: Sourced from well-known lifecycle assessment references (IPCC, EPA, Indian BEE data). Each entry notes its source category (lookup vs estimate).
|
| 271 |
+
5. **Gradio 6.x compatibility**: `theme` and `css` parameters go in `launch()`, not `Blocks()` constructor. History uses structured content blocks, not plain string pairs.
|
requirements.txt
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -----------------------------------------------------------------------
|
| 2 |
+
# EcoAgent v2 β Python dependencies
|
| 3 |
+
# Python 3.10+ | Target: Hugging Face Spaces (CPU tier)
|
| 4 |
+
# Install: pip install -r requirements.txt
|
| 5 |
+
# or: uv pip install -r requirements.txt
|
| 6 |
+
# -----------------------------------------------------------------------
|
| 7 |
+
|
| 8 |
+
# Gradio UI framework β pinned to installed version
|
| 9 |
+
gradio==6.20.0
|
| 10 |
+
|
| 11 |
+
# IBM watsonx.ai SDK (ModelInference + Credentials)
|
| 12 |
+
ibm-watsonx-ai>=1.1.15
|
| 13 |
+
|
| 14 |
+
# HTTP client (used by ibm-watsonx-ai internally and for any direct calls)
|
| 15 |
+
requests>=2.32.3
|
| 16 |
+
|
| 17 |
+
# Credential / .env loading
|
| 18 |
+
python-dotenv>=1.0.1
|
watsonx_client.py
ADDED
|
@@ -0,0 +1,520 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
watsonx_client.py
|
| 3 |
+
-----------------
|
| 4 |
+
IBM watsonx.ai SDK client for EcoAgent.
|
| 5 |
+
|
| 6 |
+
Provides:
|
| 7 |
+
- AGENT_INSTRUCTIONS : Editable agent behaviour / persona block
|
| 8 |
+
- IMPACT_TABLE : CO2/water/waste lookup for 20 common eco actions
|
| 9 |
+
- get_eco_answer() : Multi-turn chat via IBM Granite
|
| 10 |
+
- get_recycling_guide(): Single-turn recycling lookup for Indian cities
|
| 11 |
+
|
| 12 |
+
Region: eu-de (Frankfurt) | Model: ibm/granite-4-h-small
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
import os
|
| 16 |
+
import logging
|
| 17 |
+
from dotenv import load_dotenv
|
| 18 |
+
|
| 19 |
+
# ---------------------------------------------------------------------------
|
| 20 |
+
# Load credentials from .env
|
| 21 |
+
# ---------------------------------------------------------------------------
|
| 22 |
+
load_dotenv(".env")
|
| 23 |
+
|
| 24 |
+
logger = logging.getLogger(__name__)
|
| 25 |
+
|
| 26 |
+
# ===========================================================================
|
| 27 |
+
# AGENT INSTRUCTIONS
|
| 28 |
+
# Edit this block to customise persona, tone, focus areas, and rules.
|
| 29 |
+
# ===========================================================================
|
| 30 |
+
AGENT_INSTRUCTIONS = """
|
| 31 |
+
You are EcoAgent β a friendly, knowledgeable, and action-focused eco lifestyle
|
| 32 |
+
advisor specialised in the Indian context. Your goal is to help Indian households
|
| 33 |
+
live more sustainably through practical, affordable, and culturally relevant advice.
|
| 34 |
+
|
| 35 |
+
## Persona & Tone
|
| 36 |
+
- Warm, encouraging, and non-preachy
|
| 37 |
+
- Concise: lead with the action, not the theory
|
| 38 |
+
- Use simple English; avoid jargon
|
| 39 |
+
- Celebrate small wins β every action counts
|
| 40 |
+
|
| 41 |
+
## Answer Structure (ALWAYS follow this)
|
| 42 |
+
1. **Quick Tip** (1β2 sentences): the specific action the user should take
|
| 43 |
+
2. **Why it Matters** (1 sentence): the environmental/health/cost benefit
|
| 44 |
+
3. **Impact** (1 line): if the action is in the impact table, state the exact figure
|
| 45 |
+
and label it "[Lookup]"; otherwise estimate and label it "[Estimate]"
|
| 46 |
+
4. **Optional Resource** (1 line): a relevant Indian scheme, website, or product
|
| 47 |
+
β only include if genuinely useful, never invent URLs
|
| 48 |
+
|
| 49 |
+
## Sustainability Focus Areas
|
| 50 |
+
- Plastic reduction and single-use alternatives
|
| 51 |
+
- Energy efficiency (LED, appliances, solar)
|
| 52 |
+
- Water conservation (short showers, rainwater harvesting, drip irrigation)
|
| 53 |
+
- Eco-friendly travel (public transport, cycling, EVs under FAME scheme)
|
| 54 |
+
- Food choices (reduce meat, local/seasonal produce, reduce food waste)
|
| 55 |
+
- Waste management (segregation, composting, e-waste disposal)
|
| 56 |
+
|
| 57 |
+
## India-Specific Context
|
| 58 |
+
- Always reference Indian government schemes where applicable:
|
| 59 |
+
* PM Surya Ghar Muft Bijli Yojana (rooftop solar, up to 300 units free/month)
|
| 60 |
+
* FAME II / PM e-DRIVE (EV subsidies for 2W and 3W vehicles)
|
| 61 |
+
* Swachh Bharat Mission (waste management, ODF)
|
| 62 |
+
* Jal Jeevan Mission (clean water, conservation)
|
| 63 |
+
* UJALA scheme (LED bulb distribution at subsidised prices)
|
| 64 |
+
* National Biogas Programme (biogas plants for households)
|
| 65 |
+
- Reference Indian brands and local alternatives where helpful
|
| 66 |
+
(e.g., Bamboo India, Bare Necessities, The Better India marketplace)
|
| 67 |
+
- Recycling norms vary by city β acknowledge this and advise accordingly
|
| 68 |
+
- Common Indian household practices to acknowledge:
|
| 69 |
+
* Pressure cookers, clay pots, steel utensils (already eco-friendly)
|
| 70 |
+
* Festivals with high waste (Diwali crackers, Holi colours)
|
| 71 |
+
* Joint family structures β household-level advice is very relevant
|
| 72 |
+
|
| 73 |
+
## Safety & Accuracy Rules
|
| 74 |
+
- NEVER invent statistics or make up government scheme details
|
| 75 |
+
- If you are not sure of a specific number, say "approximately" and label [Estimate]
|
| 76 |
+
- Do not recommend products with specific prices (prices change)
|
| 77 |
+
- If the user's question is outside your eco domain, politely redirect
|
| 78 |
+
- Do not provide medical, legal, or financial advice
|
| 79 |
+
|
| 80 |
+
## Household Context
|
| 81 |
+
When a household profile is provided, tailor advice to:
|
| 82 |
+
- The number of family members (scale savings accordingly)
|
| 83 |
+
- Current habits (avoid advising things they already do)
|
| 84 |
+
- Location (city-specific recycling facilities, local schemes)
|
| 85 |
+
- Specific constraints (e.g., rented accommodation β skip solar panel advice)
|
| 86 |
+
"""
|
| 87 |
+
|
| 88 |
+
# ===========================================================================
|
| 89 |
+
# CARBON / RESOURCE IMPACT LOOKUP TABLE
|
| 90 |
+
# Sources: IPCC AR6, EPA GHG equivalencies, BEE India, WRI India reports,
|
| 91 |
+
# Central Pollution Control Board (CPCB) India data.
|
| 92 |
+
# Each action maps to annual savings for ONE person unless noted.
|
| 93 |
+
# ===========================================================================
|
| 94 |
+
IMPACT_TABLE: dict[str, dict] = {
|
| 95 |
+
"cloth_bags": {
|
| 96 |
+
"label": "Switch to cloth/jute bags",
|
| 97 |
+
"co2_kg_year": 3.0,
|
| 98 |
+
"water_L_day": 0,
|
| 99 |
+
"waste_kg_year": 5.0,
|
| 100 |
+
"source": "Lookup",
|
| 101 |
+
"note": "Avoids ~150 plastic bags/year @ 20g CO2 each",
|
| 102 |
+
},
|
| 103 |
+
"led_bulbs": {
|
| 104 |
+
"label": "Replace all bulbs with LED",
|
| 105 |
+
"co2_kg_year": 45.0,
|
| 106 |
+
"water_L_day": 0,
|
| 107 |
+
"waste_kg_year": 0,
|
| 108 |
+
"source": "Lookup",
|
| 109 |
+
"note": "Avg Indian home 8 bulbs; 60Wβ9W LED, 6h/day, Indian grid 0.82 kg CO2/kWh",
|
| 110 |
+
},
|
| 111 |
+
"solar_panels": {
|
| 112 |
+
"label": "Install rooftop solar (1 kW)",
|
| 113 |
+
"co2_kg_year": 820.0,
|
| 114 |
+
"water_L_day": 0,
|
| 115 |
+
"waste_kg_year": 0,
|
| 116 |
+
"source": "Lookup",
|
| 117 |
+
"note": "1 kW @ 4.5 peak sun hours, 0.82 kg CO2/kWh displaced",
|
| 118 |
+
},
|
| 119 |
+
"composting": {
|
| 120 |
+
"label": "Compost kitchen waste",
|
| 121 |
+
"co2_kg_year": 120.0,
|
| 122 |
+
"water_L_day": 0,
|
| 123 |
+
"waste_kg_year": 150.0,
|
| 124 |
+
"source": "Lookup",
|
| 125 |
+
"note": "Avg 400g/day organic waste; avoids landfill methane",
|
| 126 |
+
},
|
| 127 |
+
"public_transport": {
|
| 128 |
+
"label": "Use public transport instead of car",
|
| 129 |
+
"co2_kg_year": 1200.0,
|
| 130 |
+
"water_L_day": 0,
|
| 131 |
+
"waste_kg_year": 0,
|
| 132 |
+
"source": "Lookup",
|
| 133 |
+
"note": "20 km/day commute; 180g CO2/km (petrol car) vs 30g CO2/km (metro/bus)",
|
| 134 |
+
},
|
| 135 |
+
"short_shower": {
|
| 136 |
+
"label": "Reduce shower time by 2 minutes",
|
| 137 |
+
"co2_kg_year": 12.0,
|
| 138 |
+
"water_L_day": 20.0,
|
| 139 |
+
"waste_kg_year": 0,
|
| 140 |
+
"source": "Lookup",
|
| 141 |
+
"note": "10 L/min showerhead; 2 min Γ 10 L = 20 L/day saved",
|
| 142 |
+
},
|
| 143 |
+
"rainwater_harvesting": {
|
| 144 |
+
"label": "Install rainwater harvesting",
|
| 145 |
+
"co2_kg_year": 8.0,
|
| 146 |
+
"water_L_day": 80.0,
|
| 147 |
+
"waste_kg_year": 0,
|
| 148 |
+
"source": "Lookup",
|
| 149 |
+
"note": "Avg 100 sqm roof; 800mm annual rainfall region",
|
| 150 |
+
},
|
| 151 |
+
"vegetarian_diet": {
|
| 152 |
+
"label": "Switch to vegetarian diet",
|
| 153 |
+
"co2_kg_year": 550.0,
|
| 154 |
+
"water_L_day": 800.0,
|
| 155 |
+
"waste_kg_year": 0,
|
| 156 |
+
"source": "Lookup",
|
| 157 |
+
"note": "Meat diet 2.5 kg CO2/day vs veg 1.0 kg CO2/day; water footprint halved",
|
| 158 |
+
},
|
| 159 |
+
"no_plastic_bottles": {
|
| 160 |
+
"label": "Use refillable steel/copper water bottle",
|
| 161 |
+
"co2_kg_year": 6.5,
|
| 162 |
+
"water_L_day": 0,
|
| 163 |
+
"waste_kg_year": 8.0,
|
| 164 |
+
"source": "Lookup",
|
| 165 |
+
"note": "Avoids ~500 plastic bottles/year; 13g CO2 per PET bottle",
|
| 166 |
+
},
|
| 167 |
+
"drip_irrigation": {
|
| 168 |
+
"label": "Switch to drip irrigation (garden/farm)",
|
| 169 |
+
"co2_kg_year": 0,
|
| 170 |
+
"water_L_day": 200.0,
|
| 171 |
+
"waste_kg_year": 0,
|
| 172 |
+
"source": "Lookup",
|
| 173 |
+
"note": "Drip uses 30β50% less water than flood irrigation; 40% saving assumed",
|
| 174 |
+
},
|
| 175 |
+
"smart_powerstrip": {
|
| 176 |
+
"label": "Use smart power strip / switch off standby",
|
| 177 |
+
"co2_kg_year": 28.0,
|
| 178 |
+
"water_L_day": 0,
|
| 179 |
+
"waste_kg_year": 0,
|
| 180 |
+
"source": "Lookup",
|
| 181 |
+
"note": "Standby power ~10% of home electricity; 350 kWh/year at 0.82 kg CO2/kWh",
|
| 182 |
+
},
|
| 183 |
+
"electric_two_wheeler": {
|
| 184 |
+
"label": "Switch from petrol 2W to electric",
|
| 185 |
+
"co2_kg_year": 380.0,
|
| 186 |
+
"water_L_day": 0,
|
| 187 |
+
"waste_kg_year": 0,
|
| 188 |
+
"source": "Lookup",
|
| 189 |
+
"note": "30 km/day; petrol scooter 70g CO2/km vs EV 15g CO2/km (Indian grid)",
|
| 190 |
+
},
|
| 191 |
+
"reusable_bags_produce": {
|
| 192 |
+
"label": "Use mesh bags for fruits/vegetables",
|
| 193 |
+
"co2_kg_year": 1.5,
|
| 194 |
+
"water_L_day": 0,
|
| 195 |
+
"waste_kg_year": 3.0,
|
| 196 |
+
"source": "Lookup",
|
| 197 |
+
"note": "Avoids ~150 thin plastic produce bags/year",
|
| 198 |
+
},
|
| 199 |
+
"fix_water_leaks": {
|
| 200 |
+
"label": "Fix dripping taps and leaking pipes",
|
| 201 |
+
"co2_kg_year": 3.0,
|
| 202 |
+
"water_L_day": 30.0,
|
| 203 |
+
"waste_kg_year": 0,
|
| 204 |
+
"source": "Lookup",
|
| 205 |
+
"note": "A dripping tap wastes ~15 L/day; 2 taps assumed",
|
| 206 |
+
},
|
| 207 |
+
"line_dry_clothes": {
|
| 208 |
+
"label": "Line-dry clothes instead of electric dryer",
|
| 209 |
+
"co2_kg_year": 100.0,
|
| 210 |
+
"water_L_day": 0,
|
| 211 |
+
"waste_kg_year": 0,
|
| 212 |
+
"source": "Lookup",
|
| 213 |
+
"note": "Electric dryer ~3 kWh/load, 3 loads/week; 0.82 kg CO2/kWh",
|
| 214 |
+
},
|
| 215 |
+
"seasonal_local_produce": {
|
| 216 |
+
"label": "Buy seasonal and locally grown produce",
|
| 217 |
+
"co2_kg_year": 60.0,
|
| 218 |
+
"water_L_day": 0,
|
| 219 |
+
"waste_kg_year": 0,
|
| 220 |
+
"source": "Lookup",
|
| 221 |
+
"note": "Reduces food transport emissions; avg 200g CO2/km per tonne",
|
| 222 |
+
},
|
| 223 |
+
"segregate_waste": {
|
| 224 |
+
"label": "Segregate wet/dry/hazardous waste at home",
|
| 225 |
+
"co2_kg_year": 90.0,
|
| 226 |
+
"water_L_day": 0,
|
| 227 |
+
"waste_kg_year": 200.0,
|
| 228 |
+
"source": "Lookup",
|
| 229 |
+
"note": "Enables recycling of 55% of household waste; avoids landfill methane",
|
| 230 |
+
},
|
| 231 |
+
"pressure_cooker": {
|
| 232 |
+
"label": "Use pressure cooker instead of open pot",
|
| 233 |
+
"co2_kg_year": 18.0,
|
| 234 |
+
"water_L_day": 0,
|
| 235 |
+
"waste_kg_year": 0,
|
| 236 |
+
"source": "Lookup",
|
| 237 |
+
"note": "70% faster cooking β 70% less LPG; 1 kg LPG = 3 kg CO2",
|
| 238 |
+
},
|
| 239 |
+
"no_single_use_plastic": {
|
| 240 |
+
"label": "Eliminate single-use plastics (cutlery, straws, cups)",
|
| 241 |
+
"co2_kg_year": 5.0,
|
| 242 |
+
"water_L_day": 0,
|
| 243 |
+
"waste_kg_year": 10.0,
|
| 244 |
+
"source": "Lookup",
|
| 245 |
+
"note": "India banned SUP Jul 2022; alternatives: bamboo, steel, areca leaf",
|
| 246 |
+
},
|
| 247 |
+
"organic_farming": {
|
| 248 |
+
"label": "Switch to organic / natural farming inputs",
|
| 249 |
+
"co2_kg_year": 200.0,
|
| 250 |
+
"water_L_day": 50.0,
|
| 251 |
+
"waste_kg_year": 0,
|
| 252 |
+
"source": "Estimate",
|
| 253 |
+
"note": "Avoids synthetic fertiliser (4 kg CO2 per kg N); varies widely by crop",
|
| 254 |
+
},
|
| 255 |
+
}
|
| 256 |
+
|
| 257 |
+
|
| 258 |
+
# ===========================================================================
|
| 259 |
+
# ECO-FRIENDLY PRODUCT RECOMMENDATIONS BY CATEGORY
|
| 260 |
+
# (Used in the Recycling & Products tab)
|
| 261 |
+
# ===========================================================================
|
| 262 |
+
PRODUCT_RECS: dict[str, list[str]] = {
|
| 263 |
+
"Plastic": [
|
| 264 |
+
"Bamboo India β bamboo toothbrushes, combs, straws",
|
| 265 |
+
"Bare Necessities β zero-waste personal care products",
|
| 266 |
+
"StorTi β stainless steel food storage containers",
|
| 267 |
+
"Paperwala β kraft paper bags for shopping",
|
| 268 |
+
],
|
| 269 |
+
"Paper": [
|
| 270 |
+
"Use both sides before recycling",
|
| 271 |
+
"Switch to digital billing to reduce paper waste",
|
| 272 |
+
"Recycled paper products: Haathi Chaap (elephant-dung paper crafts)",
|
| 273 |
+
"Paper log briquettes for biomass energy",
|
| 274 |
+
],
|
| 275 |
+
"Glass": [
|
| 276 |
+
"Milkbasket / local dairy β refillable glass bottles",
|
| 277 |
+
"Borosil glass containers as plastic-free food storage",
|
| 278 |
+
"Reuse glass jars for storage (zero cost!)",
|
| 279 |
+
],
|
| 280 |
+
"E-waste": [
|
| 281 |
+
"E-Parisaraa β India's first e-waste recycler (Bangalore)",
|
| 282 |
+
"Karma Recycling β e-waste pick-up across major Indian cities",
|
| 283 |
+
"Attero Recycling β certified e-waste management",
|
| 284 |
+
"Check manufacturer take-back: Dell, HP, Samsung have return programmes",
|
| 285 |
+
],
|
| 286 |
+
"Metal": [
|
| 287 |
+
"Scrap dealers (kabadiwala) for steel, copper, aluminium",
|
| 288 |
+
"Steel Recycling Institute of India (SRII) facility locator",
|
| 289 |
+
"Avoid single-use aluminium foil; use beeswax wraps instead",
|
| 290 |
+
],
|
| 291 |
+
"Organic": [
|
| 292 |
+
"Daily Dump β home composting kits (Bangalore, ships PAN India)",
|
| 293 |
+
"Kambha composting pots β traditional Indian clay composters",
|
| 294 |
+
"SBI (Solid Biomass India) β biogas kits for kitchen waste",
|
| 295 |
+
"Vermi-composting kits via TNAU / KVK agricultural centres",
|
| 296 |
+
],
|
| 297 |
+
"Batteries": [
|
| 298 |
+
"Exide / Amaron authorised collection centres for lead-acid batteries",
|
| 299 |
+
"Panasonic / Duracell β collect at Croma / Reliance Digital stores",
|
| 300 |
+
"Switch to rechargeable NiMH batteries (Envie brand India)",
|
| 301 |
+
"Solar lanterns: Greenlight Planet / Minda (avoid disposables)",
|
| 302 |
+
],
|
| 303 |
+
"Clothing": [
|
| 304 |
+
"ThriftMyFashion / The Loom (pre-owned clothing platforms)",
|
| 305 |
+
"Ekgaon β organic cotton and natural dye clothing",
|
| 306 |
+
"Upasana Design Studio β sustainable handloom fashion",
|
| 307 |
+
"Goonj β donate old clothes for rural upcycling",
|
| 308 |
+
"Repair before discarding: local darzi (tailor) network",
|
| 309 |
+
],
|
| 310 |
+
}
|
| 311 |
+
|
| 312 |
+
# Indian cities for recycling guide
|
| 313 |
+
INDIAN_CITIES = [
|
| 314 |
+
"Mumbai", "Delhi", "Bangalore", "Hyderabad", "Chennai",
|
| 315 |
+
"Kolkata", "Pune", "Ahmedabad", "Jaipur", "Lucknow",
|
| 316 |
+
"Kochi", "Chandigarh", "Bhopal", "Indore", "Surat",
|
| 317 |
+
]
|
| 318 |
+
|
| 319 |
+
# ===========================================================================
|
| 320 |
+
# watsonx.ai CLIENT SETUP
|
| 321 |
+
# Uses APIClient pattern with set_default_project(), matching IBM example code.
|
| 322 |
+
# ===========================================================================
|
| 323 |
+
_WATSONX_API_KEY = os.environ.get("WATSONX_API_KEY", "")
|
| 324 |
+
_WATSONX_URL = os.environ.get("WATSONX_URL", "https://eu-de.ml.cloud.ibm.com")
|
| 325 |
+
_WATSONX_PROJECT_ID = os.environ.get("WATSONX_PROJECT_ID", "")
|
| 326 |
+
|
| 327 |
+
# IBM Granite 4 H Small β official watsonx.ai model ID for the Granite 4 "H" (tiny) series.
|
| 328 |
+
# The SDK fetches the live model list at runtime; no enum entry is required.
|
| 329 |
+
# Fallback to Granite 3.3 if the project plan does not include Granite 4 access.
|
| 330 |
+
_MODEL_ID_PRIMARY = "ibm/granite-4-h-small"
|
| 331 |
+
_MODEL_ID_FALLBACK = "ibm/granite-3-3-8b-instruct"
|
| 332 |
+
|
| 333 |
+
_model = None # ModelInference instance β lazy-initialised on first call
|
| 334 |
+
_api_client = None # APIClient instance β reused across calls
|
| 335 |
+
|
| 336 |
+
|
| 337 |
+
def _get_model():
|
| 338 |
+
"""Return a cached ModelInference instance, initialising on first call.
|
| 339 |
+
|
| 340 |
+
Uses the APIClient + set_default_project() pattern so the client is
|
| 341 |
+
authenticated once and reused for every subsequent chat call.
|
| 342 |
+
"""
|
| 343 |
+
global _model, _api_client
|
| 344 |
+
if _model is not None:
|
| 345 |
+
return _model
|
| 346 |
+
|
| 347 |
+
if not _WATSONX_API_KEY:
|
| 348 |
+
raise EnvironmentError(
|
| 349 |
+
"WATSONX_API_KEY is not set. "
|
| 350 |
+
"Add it to your .env file (see .env.example)."
|
| 351 |
+
)
|
| 352 |
+
if not _WATSONX_PROJECT_ID:
|
| 353 |
+
raise EnvironmentError(
|
| 354 |
+
"WATSONX_PROJECT_ID is not set.\n"
|
| 355 |
+
"How to get it:\n"
|
| 356 |
+
" 1. Go to https://eu-de.dataplatform.cloud.ibm.com\n"
|
| 357 |
+
" 2. Open your project -> Manage tab -> General -> copy Project ID\n"
|
| 358 |
+
" 3. Add WATSONX_PROJECT_ID=<uuid> to your .env file"
|
| 359 |
+
)
|
| 360 |
+
|
| 361 |
+
try:
|
| 362 |
+
from ibm_watsonx_ai import APIClient, Credentials
|
| 363 |
+
from ibm_watsonx_ai.foundation_models import ModelInference
|
| 364 |
+
except ImportError as exc:
|
| 365 |
+
raise ImportError(
|
| 366 |
+
"ibm-watsonx-ai is not installed. Run: pip install ibm-watsonx-ai"
|
| 367 |
+
) from exc
|
| 368 |
+
|
| 369 |
+
# Build credentials and APIClient β mirrors the IBM example code exactly:
|
| 370 |
+
# credentials = Credentials(url=..., api_key=...)
|
| 371 |
+
# api_client = APIClient(credentials, space_id)
|
| 372 |
+
# api_client.set.default_project(space_id)
|
| 373 |
+
credentials = Credentials(url=_WATSONX_URL, api_key=_WATSONX_API_KEY)
|
| 374 |
+
_api_client = APIClient(credentials, _WATSONX_PROJECT_ID)
|
| 375 |
+
_api_client.set.default_project(_WATSONX_PROJECT_ID)
|
| 376 |
+
logger.info("watsonx APIClient initialised (project=%s)", _WATSONX_PROJECT_ID)
|
| 377 |
+
|
| 378 |
+
# Try primary model, fall back silently if unavailable in this project
|
| 379 |
+
for model_id in (_MODEL_ID_PRIMARY, _MODEL_ID_FALLBACK):
|
| 380 |
+
try:
|
| 381 |
+
_model = ModelInference(
|
| 382 |
+
model_id=model_id,
|
| 383 |
+
api_client=_api_client,
|
| 384 |
+
)
|
| 385 |
+
logger.info("watsonx ModelInference initialised: %s", model_id)
|
| 386 |
+
return _model
|
| 387 |
+
except Exception as exc: # noqa: BLE001
|
| 388 |
+
logger.warning(
|
| 389 |
+
"Model %s unavailable (%s) β trying fallback", model_id, exc
|
| 390 |
+
)
|
| 391 |
+
|
| 392 |
+
raise RuntimeError(
|
| 393 |
+
f"Neither {_MODEL_ID_PRIMARY!r} nor {_MODEL_ID_FALLBACK!r} could be "
|
| 394 |
+
"initialised. Check your watsonx.ai project has access to these models."
|
| 395 |
+
)
|
| 396 |
+
|
| 397 |
+
|
| 398 |
+
def _build_system_prompt(profile: dict) -> str:
|
| 399 |
+
"""Inject household profile context into the system message."""
|
| 400 |
+
profile_block = ""
|
| 401 |
+
if profile:
|
| 402 |
+
members = profile.get("members", 1)
|
| 403 |
+
location = profile.get("location", "India")
|
| 404 |
+
habits = profile.get("habits", [])
|
| 405 |
+
name = profile.get("name", "")
|
| 406 |
+
profile_block = (
|
| 407 |
+
f"\n\n## Current Household Profile\n"
|
| 408 |
+
f"- Household name: {name or 'Not provided'}\n"
|
| 409 |
+
f"- Location: {location}\n"
|
| 410 |
+
f"- Members: {members}\n"
|
| 411 |
+
f"- Current eco habits: {', '.join(habits) if habits else 'None specified'}\n"
|
| 412 |
+
f"\nScale all impact estimates to {members} person(s) where relevant. "
|
| 413 |
+
f"Do not re-recommend habits the household already practises."
|
| 414 |
+
)
|
| 415 |
+
return AGENT_INSTRUCTIONS.strip() + profile_block
|
| 416 |
+
|
| 417 |
+
|
| 418 |
+
def get_eco_answer(messages: list[dict], profile: dict | None = None) -> str:
|
| 419 |
+
"""Send a multi-turn conversation to Granite and return the reply.
|
| 420 |
+
|
| 421 |
+
Args:
|
| 422 |
+
messages: List of {"role": "user"|"assistant", "content": str} dicts.
|
| 423 |
+
Do NOT include a system message β this function prepends it.
|
| 424 |
+
profile: Optional household profile dict from the Profile tab.
|
| 425 |
+
|
| 426 |
+
Returns:
|
| 427 |
+
The assistant's reply as a plain string.
|
| 428 |
+
|
| 429 |
+
Raises:
|
| 430 |
+
EnvironmentError: Missing credentials (caught by app.py).
|
| 431 |
+
RuntimeError: API call failure (caught by app.py).
|
| 432 |
+
"""
|
| 433 |
+
model = _get_model()
|
| 434 |
+
system_prompt = _build_system_prompt(profile or {})
|
| 435 |
+
|
| 436 |
+
full_messages = [{"role": "system", "content": system_prompt}] + messages
|
| 437 |
+
|
| 438 |
+
try:
|
| 439 |
+
response = model.chat(
|
| 440 |
+
messages=full_messages,
|
| 441 |
+
params={
|
| 442 |
+
"max_tokens": 800,
|
| 443 |
+
"temperature": 0.7,
|
| 444 |
+
"top_p": 0.95,
|
| 445 |
+
},
|
| 446 |
+
)
|
| 447 |
+
return response["choices"][0]["message"]["content"].strip()
|
| 448 |
+
except KeyError as exc:
|
| 449 |
+
raise RuntimeError(
|
| 450 |
+
f"Unexpected response format from watsonx.ai: missing key {exc}. "
|
| 451 |
+
f"Raw response: {str(response)[:300]}"
|
| 452 |
+
) from exc
|
| 453 |
+
except Exception as exc: # noqa: BLE001
|
| 454 |
+
raise RuntimeError(f"watsonx.ai call failed: {exc}") from exc
|
| 455 |
+
|
| 456 |
+
|
| 457 |
+
def get_recycling_guide(material: str, city: str) -> str:
|
| 458 |
+
"""Ask Granite for recycling instructions for a specific material and city.
|
| 459 |
+
|
| 460 |
+
Args:
|
| 461 |
+
material: One of the material categories (e.g. "E-waste", "Plastic").
|
| 462 |
+
city: Indian city name for local context.
|
| 463 |
+
|
| 464 |
+
Returns:
|
| 465 |
+
Formatted recycling guide as a markdown string.
|
| 466 |
+
"""
|
| 467 |
+
prompt = (
|
| 468 |
+
f"Provide a practical recycling guide for **{material}** waste in {city}, India. "
|
| 469 |
+
f"Include:\n"
|
| 470 |
+
f"1. How to prepare/segregate this waste at home\n"
|
| 471 |
+
f"2. Where to drop it off or how to get it collected in {city}\n"
|
| 472 |
+
f"3. What happens to it after collection (briefly)\n"
|
| 473 |
+
f"4. One eco-friendly alternative to reduce this waste type\n"
|
| 474 |
+
f"Keep the response concise, practical, and India-specific. "
|
| 475 |
+
f"Use bullet points. Label any uncertain details as [Estimate]."
|
| 476 |
+
)
|
| 477 |
+
model = _get_model()
|
| 478 |
+
try:
|
| 479 |
+
response = model.chat(
|
| 480 |
+
messages=[
|
| 481 |
+
{"role": "system", "content": AGENT_INSTRUCTIONS.strip()},
|
| 482 |
+
{"role": "user", "content": prompt},
|
| 483 |
+
],
|
| 484 |
+
params={"max_tokens": 500, "temperature": 0.4},
|
| 485 |
+
)
|
| 486 |
+
return response["choices"][0]["message"]["content"].strip()
|
| 487 |
+
except Exception as exc: # noqa: BLE001
|
| 488 |
+
raise RuntimeError(f"Recycling guide call failed: {exc}") from exc
|
| 489 |
+
|
| 490 |
+
|
| 491 |
+
def compute_session_impact(logged_actions: list[str], members: int = 1) -> dict:
|
| 492 |
+
"""Aggregate CO2/water/waste savings for a list of logged action slugs.
|
| 493 |
+
|
| 494 |
+
Args:
|
| 495 |
+
logged_actions: List of action slug strings from IMPACT_TABLE keys.
|
| 496 |
+
members: Household size to scale savings.
|
| 497 |
+
|
| 498 |
+
Returns:
|
| 499 |
+
Dict with keys: co2_kg_year, water_L_day, waste_kg_year, eco_score (0β100).
|
| 500 |
+
"""
|
| 501 |
+
co2 = 0.0
|
| 502 |
+
water = 0.0
|
| 503 |
+
waste = 0.0
|
| 504 |
+
unique = set(logged_actions)
|
| 505 |
+
|
| 506 |
+
for slug in unique:
|
| 507 |
+
entry = IMPACT_TABLE.get(slug)
|
| 508 |
+
if entry:
|
| 509 |
+
co2 += entry.get("co2_kg_year", 0) * members
|
| 510 |
+
water += entry.get("water_L_day", 0) * members
|
| 511 |
+
waste += entry.get("waste_kg_year", 0) * members
|
| 512 |
+
|
| 513 |
+
eco_score = min(100, len(unique) * 8) # 8 pts per unique action, cap 100
|
| 514 |
+
return {
|
| 515 |
+
"co2_kg_year": round(co2, 1),
|
| 516 |
+
"water_L_day": round(water, 1),
|
| 517 |
+
"waste_kg_year": round(waste, 1),
|
| 518 |
+
"eco_score": eco_score,
|
| 519 |
+
"actions_count": len(unique),
|
| 520 |
+
}
|