Spaces:
Sleeping
Sleeping
| """ | |
| ClimaIQ Gemma Layer — Bilingual Explanation Engine | |
| Converts ClimaIQ model output into plain language for farmers and loan officers. | |
| Inference modes: | |
| - cloud: Google AI Studio / GenAI API (GEMMA_API_KEY, model GEMMA_MODEL_CLOUD) | |
| - ollama: local Ollama HTTP API at OLLAMA_BASE_URL (default http://127.0.0.1:11434) | |
| Author: Krishna Dahale | |
| """ | |
| import json | |
| import os | |
| import urllib.error | |
| import urllib.request | |
| from typing import Optional, Tuple | |
| from google import genai | |
| # ─── Client Setup ────────────────────────────────────────────────────────────── | |
| def get_client(api_key: str = None): | |
| key = api_key or os.environ.get("GEMMA_API_KEY") | |
| if not key: | |
| raise ValueError("API key not found. Set GEMMA_API_KEY environment variable.") | |
| return genai.Client(api_key=key) | |
| GEMMA_MODEL_CLOUD = "gemma-4-26b-a4b-it" | |
| # Backwards compatibility | |
| GEMMA_MODEL = GEMMA_MODEL_CLOUD | |
| DEFAULT_OLLAMA_BASE = os.environ.get("OLLAMA_BASE_URL", "http://127.0.0.1:11434").rstrip("/") | |
| DEFAULT_OLLAMA_MODEL = os.environ.get("OLLAMA_MODEL", "gemma3:4b") | |
| CROP_WATER_CLASS = { | |
| "Rice": "very high", | |
| "Sugarcane": "very high", | |
| "Cotton": "moderate", | |
| "Wheat": "low", | |
| "Millets": "very low", | |
| } | |
| def _ollama_installed_name_matches(installed: str, requested: str) -> bool: | |
| """True if Ollama tags name matches user model (e.g. gemma3:4b vs gemma3:4b:latest).""" | |
| installed = (installed or "").strip() | |
| requested = (requested or "").strip() | |
| if not requested: | |
| return True | |
| if installed == requested: | |
| return True | |
| if installed.startswith(requested + ":"): | |
| return True | |
| return False | |
| def ollama_verify_connection( | |
| base_url: Optional[str] = None, | |
| model: Optional[str] = None, | |
| timeout: int = 12, | |
| ) -> Tuple[bool, str]: | |
| """ | |
| GET /api/tags — confirms Ollama is reachable and the chosen model is installed. | |
| Returns (success, human-readable message). No LLM inference run. | |
| """ | |
| base = (base_url or DEFAULT_OLLAMA_BASE).rstrip("/") | |
| want = (model or DEFAULT_OLLAMA_MODEL).strip() | |
| url = f"{base}/api/tags" | |
| try: | |
| with urllib.request.urlopen(url, timeout=timeout) as resp: | |
| data = json.loads(resp.read().decode("utf-8")) | |
| except urllib.error.HTTPError as e: | |
| body = e.read().decode("utf-8", errors="replace")[:300] if e.fp else "" | |
| return False, f"Ollama returned HTTP {e.code} at {url}. {body}" | |
| except Exception as e: | |
| return False, f"Cannot reach Ollama at {base}. Start the server (e.g. ollama serve), then try again. ({e})" | |
| models = data.get("models") or [] | |
| names = [m.get("name", "") for m in models if m.get("name")] | |
| if not names: | |
| hint = f"Run: ollama pull {want}" if want else "Run: ollama pull <model>" | |
| return False, f"Ollama responded but no models are installed yet. {hint}" | |
| if want: | |
| hits = [n for n in names if _ollama_installed_name_matches(n, want)] | |
| if not hits: | |
| preview = ", ".join(names[:10]) | |
| suffix = " …" if len(names) > 10 else "" | |
| return ( | |
| False, | |
| f"Ollama is running, but model '{want}' is not installed. " | |
| f"You have: {preview}{suffix}. Run: ollama pull {want}", | |
| ) | |
| return True, f"Connected locally at {base}. Model '{hits[0]}' is ready, so you can use offline narratives." | |
| return True, f"Connected at {base}. {len(names)} model(s) installed." | |
| def ollama_generate( | |
| prompt: str, | |
| base_url: Optional[str] = None, | |
| model: Optional[str] = None, | |
| timeout: int = 300, | |
| ) -> str: | |
| """ | |
| Call Ollama's /api/chat (non-streaming). Works offline once Ollama is running. | |
| """ | |
| base = (base_url or DEFAULT_OLLAMA_BASE).rstrip("/") | |
| m = model or DEFAULT_OLLAMA_MODEL | |
| url = f"{base}/api/chat" | |
| payload = json.dumps( | |
| { | |
| "model": m, | |
| "messages": [{"role": "user", "content": prompt}], | |
| "stream": False, | |
| } | |
| ).encode("utf-8") | |
| req = urllib.request.Request( | |
| url, | |
| data=payload, | |
| headers={"Content-Type": "application/json"}, | |
| method="POST", | |
| ) | |
| try: | |
| with urllib.request.urlopen(req, timeout=timeout) as resp: | |
| data = json.loads(resp.read().decode("utf-8")) | |
| except urllib.error.HTTPError as e: | |
| body = e.read().decode("utf-8", errors="replace") if e.fp else "" | |
| raise RuntimeError(f"Ollama HTTP {e.code}: {body[:500]}") from e | |
| except urllib.error.URLError as e: | |
| raise RuntimeError( | |
| f"Cannot reach Ollama at {base}. Is `ollama serve` running? ({e.reason})" | |
| ) from e | |
| msg = data.get("message") or {} | |
| text = (msg.get("content") or "").strip() | |
| if not text and data.get("error"): | |
| raise RuntimeError(str(data["error"])) | |
| return text | |
| def generate_explanation_text( | |
| prompt: str, | |
| *, | |
| inference_mode: str = "cloud", | |
| cloud_client=None, | |
| ollama_base: Optional[str] = None, | |
| ollama_model: Optional[str] = None, | |
| ) -> str: | |
| """Single entry: cloud (Google GenAI) or local (Ollama).""" | |
| if inference_mode == "ollama": | |
| return ollama_generate(prompt, base_url=ollama_base, model=ollama_model) | |
| if cloud_client is None: | |
| raise ValueError("Cloud mode requires a Google GenAI client and GEMMA_API_KEY.") | |
| response = cloud_client.models.generate_content( | |
| model=GEMMA_MODEL_CLOUD, | |
| contents=prompt, | |
| ) | |
| return response.text | |
| # ─── Prompt Templates ────────────────────────────────────────────────────────── | |
| def _build_farmer_prompt(farmer_data: dict, result: dict, language: str) -> str: | |
| drivers = "\n".join([f"- {d['display_name']}" for d in result["top_risk_drivers"]]) | |
| crop = str(farmer_data["crop_type"]) | |
| water_class = CROP_WATER_CLASS.get(crop, "moderate") | |
| if language == "Hindi": | |
| return f""" | |
| आप एक सहानुभूतिपूर्ण कृषि ऋण सलाहकार हैं जो किसानों को सरल हिंदी में समझाते हैं। | |
| किसान की जानकारी: | |
| - फसल: {farmer_data['crop_type']} | |
| - राज्य: {farmer_data['state']} | |
| - वार्षिक आय: ₹{farmer_data['annual_income_lakhs']} लाख | |
| - मांगा गया ऋण: ₹{farmer_data['loan_amount_lakhs']} लाख | |
| - इस मौसम में बारिश की कमी: {abs(farmer_data['rainfall_deficit_pct'])}% | |
| - सूखे की गंभीरता (SPI): {farmer_data['spi']} | |
| - लगातार सूखे के वर्ष: {farmer_data['consecutive_drought_years']} | |
| ClimaIQ परिणाम: | |
| - क्रेडिट स्कोर: {result['credit_score']} (300–850 में से) | |
| - जोखिम श्रेणी: {result['risk_band']} | |
| - डिफ़ॉल्ट संभावना: {result['default_probability']}% | |
| - मुख्य जोखिम कारण: | |
| {drivers} | |
| निम्नलिखित प्रारूप में 4-5 वाक्यों में उत्तर दें: | |
| 1. किसान को सरल भाषा में बताएं कि उनका स्कोर क्यों कम/अधिक है | |
| 2. मौसम और फसल का जोखिम पर क्या असर हुआ, यह समझाएं | |
| 3. किसान 3 व्यावहारिक कदम क्या उठा सकते हैं (बीमा, वैकल्पिक फसल, सिंचाई आदि) | |
| महत्वपूर्ण तथ्य: इस इंजन में {crop} की पानी-आधारित श्रेणी "{water_class}" मानी जाती है। पानी-आवश्यकता के बारे में इससे उल्टा दावा न करें। | |
| केवल हिंदी में उत्तर दें। जटिल शब्दों से बचें। | |
| """ | |
| else: | |
| return f""" | |
| You are a helpful agricultural credit advisor explaining a loan assessment to a farmer in simple English. | |
| Farmer Profile: | |
| - Crop: {farmer_data['crop_type']} | |
| - State: {farmer_data['state']} | |
| - Annual Income: ₹{farmer_data['annual_income_lakhs']} lakhs | |
| - Loan Requested: ₹{farmer_data['loan_amount_lakhs']} lakhs | |
| - Rainfall Deficit this season: {abs(farmer_data['rainfall_deficit_pct'])}% | |
| - Drought Severity (SPI): {farmer_data['spi']} | |
| - Consecutive Drought Years: {farmer_data['consecutive_drought_years']} | |
| ClimaIQ Result: | |
| - Credit Score: {result['credit_score']} out of 850 | |
| - Risk Band: {result['risk_band']} | |
| - Default Probability: {result['default_probability']}% | |
| - Key Risk Drivers: | |
| {drivers} | |
| In 4-5 simple sentences: | |
| 1. Explain why their score is what it is | |
| 2. Explain how the weather and crop type has affected their risk | |
| 3. Give 3 practical steps they can take (insurance, alternate crops, irrigation, etc.) | |
| Important fact guardrail: in this engine, {crop} is classified as "{water_class}" water-intensity. Do not claim the opposite. | |
| Use simple, non-technical language. Be empathetic and constructive. | |
| """ | |
| def _build_officer_prompt(farmer_data: dict, result: dict, language: str) -> str: | |
| drivers = "\n".join([f"- {d['display_name']}: {d['direction']}" for d in result["top_risk_drivers"]]) | |
| if language == "Hindi": | |
| return f""" | |
| आप एक वरिष्ठ कृषि ऋण अधिकारी हैं। नीचे दी गई जानकारी के आधार पर एक संक्षिप्त ऋण मूल्यांकन रिपोर्ट हिंदी में तैयार करें। | |
| किसान प्रोफाइल: | |
| - फसल: {farmer_data['crop_type']} | राज्य: {farmer_data['state']} | |
| - आयु: {farmer_data['age']} वर्ष | भूमि: {farmer_data['land_size_acres']} एकड़ | |
| - वार्षिक आय: ₹{farmer_data['annual_income_lakhs']} लाख | |
| - ऋण राशि: ₹{farmer_data['loan_amount_lakhs']} लाख | |
| - पिछले डिफ़ॉल्ट: {'हाँ' if farmer_data['previous_defaults'] else 'नहीं'} | |
| - बारिश की कमी: {abs(farmer_data['rainfall_deficit_pct'])}% | |
| - SPI: {farmer_data['spi']} | लगातार सूखा: {farmer_data['consecutive_drought_years']} वर्ष | |
| ClimaIQ स्कोर: {result['credit_score']} | जोखिम: {result['risk_band']} | डिफ़ॉल्ट संभावना: {result['default_probability']}% | |
| सुझाई गई कार्रवाई: {result['recommended_action']} | |
| मुख्य जोखिम कारण: | |
| {drivers} | |
| निम्नलिखित शीर्षक ठीक इसी रूप में अलग पंक्ति पर लिखें, फिर एक खाली पंक्ति, फिर अनुच्छेद: | |
| 1. ऋण मूल्यांकन सारांश | |
| 2. जलवायु जोखिम विश्लेषण | |
| 3. सुझाई गई कार्रवाई और शर्तें | |
| 4. निगरानी बिंदु | |
| शीर्षकों पर मार्कडाउन बोल्ड न लगाएँ। प्रत्येक खंड में लगभग 2 वाक्य रखें। | |
| """ | |
| else: | |
| return f""" | |
| You are a senior agricultural loan officer. Prepare a concise loan assessment report based on the ClimaIQ analysis below. | |
| Farmer Profile: | |
| - Crop: {farmer_data['crop_type']} | State: {farmer_data['state']} | |
| - Age: {farmer_data['age']} | Land: {farmer_data['land_size_acres']} acres | |
| - Annual Income: ₹{farmer_data['annual_income_lakhs']} lakhs | |
| - Loan Requested: ₹{farmer_data['loan_amount_lakhs']} lakhs | |
| - Previous Defaults: {'Yes' if farmer_data['previous_defaults'] else 'No'} | |
| - Rainfall Deficit: {abs(farmer_data['rainfall_deficit_pct'])}% | |
| - SPI: {farmer_data['spi']} | Consecutive Drought Years: {farmer_data['consecutive_drought_years']} | |
| ClimaIQ Score: {result['credit_score']} | Risk Band: {result['risk_band']} | Default Probability: {result['default_probability']}% | |
| Recommended Action: {result['recommended_action']} | |
| Top Risk Drivers: | |
| {drivers} | |
| Structure your report using these exact numbered headings, each on its own line, followed by a blank line, then the paragraph(s): | |
| 1. Assessment Summary | |
| 2. Climate Risk Analysis | |
| 3. Recommended Action with conditions | |
| 4. Monitoring flags if loan is approved | |
| Do not use markdown bold for the numbers. Keep each heading text exactly as shown (you may add detail after the blank line only). | |
| Be professional and specific. Reference the ClimaIQ score in your reasoning. | |
| """ | |
| def _build_portfolio_prompt(stress_results: list, language: str) -> str: | |
| scenario_text = "\n".join([ | |
| f"- {r['scenario']}: {r['avg_default_pct']}% avg default, ₹{r['total_loss_lakhs']}L estimated loss" | |
| for r in stress_results | |
| ]) | |
| if language == "Hindi": | |
| return f""" | |
| आप एक पोर्टफोलियो जोखिम प्रबंधक हैं। निम्नलिखित जलवायु तनाव परीक्षण परिणामों के आधार पर हिंदी में एक संक्षिप्त जोखिम रिपोर्ट तैयार करें। | |
| तनाव परीक्षण परिणाम ({stress_results[0]['portfolio_size']} ऋणों का पोर्टफोलियो): | |
| {scenario_text} | |
| निम्नलिखित पर रिपोर्ट दें: | |
| 1. सामान्य बनाम सूखे की स्थिति में जोखिम वृद्धि | |
| 2. सबसे गंभीर परिदृश्य का प्रभाव | |
| 3. पूंजी बफर और प्रावधान की सिफारिश | |
| 3-4 वाक्यों में, स्पष्ट और व्यावसायिक भाषा में। | |
| """ | |
| else: | |
| return f""" | |
| You are a portfolio risk manager at an agricultural lending institution. | |
| Based on the ClimaIQ stress test results below, write a concise risk narrative for senior management. | |
| Stress Test Results (Portfolio of {stress_results[0]['portfolio_size']} loans): | |
| {scenario_text} | |
| Cover: | |
| 1. Risk escalation from normal to stressed conditions | |
| 2. The non-linear jump — why losses multiply rather than add | |
| 3. Capital buffer and provisioning recommendation | |
| 4. Which drought scenario should trigger portfolio rebalancing | |
| Write in 4-5 sentences. Be specific about the numbers. Professional tone. | |
| """ | |
| # ─── Main Explanation Functions ──────────────────────────────────────────────── | |
| def explain_for_farmer( | |
| farmer_data: dict, | |
| result: dict, | |
| language: str = "Hindi", | |
| client=None, | |
| inference_mode: str = "cloud", | |
| ollama_base: Optional[str] = None, | |
| ollama_model: Optional[str] = None, | |
| ) -> str: | |
| """Generate farmer-facing explanation in Hindi or English.""" | |
| prompt = _build_farmer_prompt(farmer_data, result, language) | |
| return generate_explanation_text( | |
| prompt, | |
| inference_mode=inference_mode, | |
| cloud_client=client, | |
| ollama_base=ollama_base, | |
| ollama_model=ollama_model, | |
| ) | |
| def explain_for_officer( | |
| farmer_data: dict, | |
| result: dict, | |
| language: str = "English", | |
| client=None, | |
| inference_mode: str = "cloud", | |
| ollama_base: Optional[str] = None, | |
| ollama_model: Optional[str] = None, | |
| ) -> str: | |
| """Generate loan officer assessment report in Hindi or English.""" | |
| prompt = _build_officer_prompt(farmer_data, result, language) | |
| return generate_explanation_text( | |
| prompt, | |
| inference_mode=inference_mode, | |
| cloud_client=client, | |
| ollama_base=ollama_base, | |
| ollama_model=ollama_model, | |
| ) | |
| def explain_portfolio_stress( | |
| stress_results: list, | |
| language: str = "English", | |
| client=None, | |
| inference_mode: str = "cloud", | |
| ollama_base: Optional[str] = None, | |
| ollama_model: Optional[str] = None, | |
| ) -> str: | |
| """Generate portfolio-level stress test narrative.""" | |
| prompt = _build_portfolio_prompt(stress_results, language) | |
| return generate_explanation_text( | |
| prompt, | |
| inference_mode=inference_mode, | |
| cloud_client=client, | |
| ollama_base=ollama_base, | |
| ollama_model=ollama_model, | |
| ) | |
| # ─── Quick Test ──────────────────────────────────────────────────────────────── | |
| if __name__ == "__main__": | |
| import os | |
| from climaiq_engine import load_model, predict_single, run_stress_test | |
| API_KEY = input("Enter your Gemma API key: ").strip() | |
| client = get_client(API_KEY) | |
| model, scaler = load_model() | |
| sample_farmer = { | |
| "age": 42, | |
| "land_size_acres": 3.5, | |
| "annual_income_lakhs": 3.0, | |
| "loan_amount_lakhs": 2.0, | |
| "previous_defaults": 0, | |
| "crop_type": "Cotton", | |
| "state": "Maharashtra", | |
| "rainfall_deficit_pct": -35.0, | |
| "spi": -1.8, | |
| "consecutive_drought_years": 1 | |
| } | |
| result = predict_single(sample_farmer, model, scaler) | |
| print("\n" + "="*60) | |
| print("FARMER EXPLANATION (Hindi)") | |
| print("="*60) | |
| print(explain_for_farmer(sample_farmer, result, language="Hindi", client=client)) | |
| print("\n" + "="*60) | |
| print("OFFICER REPORT (English)") | |
| print("="*60) | |
| print(explain_for_officer(sample_farmer, result, language="English", client=client)) | |
| print("\n" + "="*60) | |
| print("PORTFOLIO STRESS TEST NARRATIVE") | |
| print("="*60) | |
| portfolio = [sample_farmer] * 50 | |
| stress = run_stress_test(model, scaler, portfolio) | |
| print(explain_portfolio_stress(stress, language="English", client=client)) | |