Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -9,11 +9,12 @@ from flask import (
|
|
| 9 |
make_response, redirect, url_for
|
| 10 |
)
|
| 11 |
from werkzeug.middleware.proxy_fix import ProxyFix
|
|
|
|
| 12 |
import google.generativeai as genai
|
| 13 |
from dotenv import load_dotenv
|
| 14 |
from cachelib import SimpleCache
|
| 15 |
|
| 16 |
-
# Load
|
| 17 |
load_dotenv()
|
| 18 |
|
| 19 |
# Logging
|
|
@@ -26,33 +27,40 @@ app = Flask(__name__, static_folder="static", template_folder="templates")
|
|
| 26 |
# Secret key
|
| 27 |
app.secret_key = os.getenv("FLASK_SECRET_KEY", os.urandom(24))
|
| 28 |
|
| 29 |
-
# ProxyFix
|
| 30 |
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_port=1, x_prefix=1)
|
| 31 |
|
| 32 |
-
#
|
| 33 |
-
#
|
| 34 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
disable_secure = os.getenv("DISABLE_SECURE_COOKIE", "0") in ("1", "true", "True")
|
| 36 |
app.config["SESSION_COOKIE_SAMESITE"] = "None"
|
| 37 |
app.config["SESSION_COOKIE_SECURE"] = False if disable_secure else True
|
| 38 |
-
app.config["PERMANENT_SESSION_LIFETIME"] = timedelta(days=7)
|
| 39 |
-
app.config["SESSION_PERMANENT"] = True
|
| 40 |
app.config["SESSION_COOKIE_NAME"] = os.getenv("SESSION_COOKIE_NAME", "hf_app_session")
|
| 41 |
|
| 42 |
-
#
|
| 43 |
-
|
|
|
|
|
|
|
|
|
|
| 44 |
|
| 45 |
-
#
|
| 46 |
GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
|
| 47 |
if GEMINI_API_KEY:
|
| 48 |
try:
|
| 49 |
genai.configure(api_key=GEMINI_API_KEY)
|
|
|
|
| 50 |
except Exception as e:
|
| 51 |
logger.exception("Failed to configure Gemini client: %s", e)
|
| 52 |
else:
|
| 53 |
-
logger.warning("GEMINI_API_KEY not
|
| 54 |
|
| 55 |
-
# Supported languages and list
|
| 56 |
LANGUAGES = {
|
| 57 |
"en": "English", "hi": "हिंदी (Hindi)", "bn": "বাংলা (Bengali)", "te": "తెలుగు (Telugu)",
|
| 58 |
"mr": "मराठी (Marathi)", "ta": "தமிழ் (Tamil)", "gu": "ગુજરાતી (Gujarati)", "ur": "اردو (Urdu)",
|
|
@@ -74,82 +82,69 @@ PESTS_DISEASES = [
|
|
| 74 |
{"id": 12, "name": "Fusarium Wilt", "type": "disease", "crop": "Banana, Tomato", "image_url": "/static/images/fusarium_wilt.jpg"}
|
| 75 |
]
|
| 76 |
|
| 77 |
-
#
|
| 78 |
def extract_json_from_response(content: str):
|
| 79 |
try:
|
| 80 |
return json.loads(content)
|
| 81 |
except json.JSONDecodeError:
|
| 82 |
-
# Attempt to extract codeblock containing JSON
|
| 83 |
json_match = re.search(r'```json(.*?)```', content, re.DOTALL)
|
| 84 |
if json_match:
|
| 85 |
json_str = json_match.group(1).strip()
|
| 86 |
else:
|
| 87 |
-
# Fallback: try to extract first {...} block
|
| 88 |
brace_match = re.search(r'(\{(?:.|\n)*\})', content)
|
| 89 |
json_str = brace_match.group(1) if brace_match else content
|
| 90 |
-
|
| 91 |
-
# Remove stray markdown markers and weird trailing commas
|
| 92 |
json_str = json_str.replace('```json', '').replace('```', '').strip()
|
| 93 |
json_str = re.sub(r',(\s*[\]\}])', r'\1', json_str)
|
| 94 |
-
|
| 95 |
try:
|
| 96 |
return json.loads(json_str)
|
| 97 |
except json.JSONDecodeError as e:
|
| 98 |
-
logger.error("JSON parsing error after cleanup: %s\
|
| 99 |
raise ValueError("Failed to parse JSON response from API")
|
| 100 |
|
| 101 |
-
# Root page
|
| 102 |
@app.route("/")
|
| 103 |
def index():
|
| 104 |
-
#
|
| 105 |
language = session.get("language") or request.cookies.get("language") or "en"
|
| 106 |
-
# Keep session in sync with cookie if needed
|
| 107 |
session["language"] = language
|
| 108 |
session.permanent = True
|
| 109 |
-
return render_template("
|
| 110 |
-
pests_diseases=PESTS_DISEASES,
|
| 111 |
-
languages=LANGUAGES,
|
| 112 |
-
current_language=language)
|
| 113 |
|
| 114 |
-
#
|
| 115 |
@app.route("/set_language", methods=["POST"])
|
| 116 |
def set_language():
|
| 117 |
language = request.form.get("language", "en")
|
| 118 |
if language not in LANGUAGES:
|
| 119 |
-
logger.warning("Attempt to set unsupported language: %s", language)
|
| 120 |
language = "en"
|
| 121 |
-
|
| 122 |
-
# Save in server-side session
|
| 123 |
session["language"] = language
|
| 124 |
session.permanent = True
|
| 125 |
-
logger.info("Language
|
| 126 |
|
| 127 |
-
# Also set a client cookie as reliable fallback for stateless platforms
|
| 128 |
resp = make_response(redirect(url_for("index")))
|
| 129 |
-
# cookie lifetime 7 days
|
| 130 |
max_age = 60 * 60 * 24 * 7
|
| 131 |
secure_flag = False if disable_secure else True
|
| 132 |
-
resp.set_cookie("language", language, max_age=max_age, secure=secure_flag, samesite="None", httponly=False)
|
| 133 |
return resp
|
| 134 |
|
| 135 |
-
# Endpoint to fetch details for a pest/disease (uses cache + Gemini)
|
| 136 |
@app.route("/get_details/<int:pest_id>")
|
| 137 |
-
def get_details(pest_id
|
| 138 |
-
#
|
| 139 |
-
|
|
|
|
|
|
|
|
|
|
| 140 |
|
| 141 |
pest = next((p for p in PESTS_DISEASES if p["id"] == pest_id), None)
|
| 142 |
-
if pest
|
| 143 |
return jsonify({"error": "Not found"}), 404
|
| 144 |
|
| 145 |
cache_key = f"pest_{pest_id}_{language}"
|
| 146 |
cached = cache.get(cache_key)
|
| 147 |
if cached:
|
| 148 |
-
logger.info("Cache hit for %s (%s)", pest_id, language)
|
| 149 |
return jsonify(cached)
|
| 150 |
|
| 151 |
if not GEMINI_API_KEY:
|
| 152 |
-
logger.warning("
|
| 153 |
result = {
|
| 154 |
**pest,
|
| 155 |
"details": {
|
|
@@ -166,7 +161,7 @@ def get_details(pest_id: int):
|
|
| 166 |
cache.set(cache_key, result)
|
| 167 |
return jsonify(result)
|
| 168 |
|
| 169 |
-
# Build prompt
|
| 170 |
lang_instructions = {
|
| 171 |
"en": "Respond in English",
|
| 172 |
"hi": "हिंदी में जवाब दें (Respond in Hindi)",
|
|
@@ -194,12 +189,11 @@ Each key must contain an object with "title" and "text" fields.
|
|
| 194 |
text = getattr(response, "text", "") or str(response)
|
| 195 |
detailed_info = extract_json_from_response(text)
|
| 196 |
|
| 197 |
-
# Ensure keys exist
|
| 198 |
required_keys = ["description", "lifecycle", "symptoms", "impact", "management", "prevention"]
|
| 199 |
for key in required_keys:
|
| 200 |
if key not in detailed_info or "text" not in detailed_info.get(key, {}):
|
| 201 |
detailed_info[key] = {"title": key.capitalize(), "text": "Information could not be generated for this section."}
|
| 202 |
-
logger.warning("
|
| 203 |
|
| 204 |
result = {
|
| 205 |
**pest,
|
|
@@ -210,12 +204,10 @@ Each key must contain an object with "title" and "text" fields.
|
|
| 210 |
cache.set(cache_key, result)
|
| 211 |
logger.info("Cached details for pest %s (%s)", pest_id, language)
|
| 212 |
return jsonify(result)
|
| 213 |
-
|
| 214 |
except Exception as e:
|
| 215 |
-
logger.exception("Error fetching
|
| 216 |
return jsonify({"error": "Failed to fetch information", "message": str(e)}), 500
|
| 217 |
|
| 218 |
-
|
| 219 |
if __name__ == "__main__":
|
| 220 |
port = int(os.environ.get("PORT", 7860))
|
| 221 |
host = os.environ.get("HOST", "0.0.0.0")
|
|
|
|
| 9 |
make_response, redirect, url_for
|
| 10 |
)
|
| 11 |
from werkzeug.middleware.proxy_fix import ProxyFix
|
| 12 |
+
from flask_session import Session
|
| 13 |
import google.generativeai as genai
|
| 14 |
from dotenv import load_dotenv
|
| 15 |
from cachelib import SimpleCache
|
| 16 |
|
| 17 |
+
# Load environment
|
| 18 |
load_dotenv()
|
| 19 |
|
| 20 |
# Logging
|
|
|
|
| 27 |
# Secret key
|
| 28 |
app.secret_key = os.getenv("FLASK_SECRET_KEY", os.urandom(24))
|
| 29 |
|
| 30 |
+
# ProxyFix for deployments behind proxies (Hugging Face)
|
| 31 |
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_port=1, x_prefix=1)
|
| 32 |
|
| 33 |
+
# Session configuration (filesystem-based) -- persists server-side during container life
|
| 34 |
+
# Flask-Session will write files under /tmp by default
|
| 35 |
+
app.config["SESSION_TYPE"] = "filesystem"
|
| 36 |
+
app.config["SESSION_FILE_DIR"] = os.getenv("SESSION_FILE_DIR", "/tmp/flask_session")
|
| 37 |
+
app.config["SESSION_PERMANENT"] = True
|
| 38 |
+
app.config["PERMANENT_SESSION_LIFETIME"] = timedelta(days=7)
|
| 39 |
+
|
| 40 |
+
# Cookie settings - browsers require SameSite=None + Secure for cross-site cookies
|
| 41 |
disable_secure = os.getenv("DISABLE_SECURE_COOKIE", "0") in ("1", "true", "True")
|
| 42 |
app.config["SESSION_COOKIE_SAMESITE"] = "None"
|
| 43 |
app.config["SESSION_COOKIE_SECURE"] = False if disable_secure else True
|
|
|
|
|
|
|
| 44 |
app.config["SESSION_COOKIE_NAME"] = os.getenv("SESSION_COOKIE_NAME", "hf_app_session")
|
| 45 |
|
| 46 |
+
# Initialize server-side sessions
|
| 47 |
+
Session(app)
|
| 48 |
+
|
| 49 |
+
# Simple in-memory cache (for API results)
|
| 50 |
+
cache = SimpleCache(threshold=200, default_timeout=60 * 60 * 24 * 7) # 7 days
|
| 51 |
|
| 52 |
+
# Gemini API
|
| 53 |
GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
|
| 54 |
if GEMINI_API_KEY:
|
| 55 |
try:
|
| 56 |
genai.configure(api_key=GEMINI_API_KEY)
|
| 57 |
+
logger.info("Gemini client configured.")
|
| 58 |
except Exception as e:
|
| 59 |
logger.exception("Failed to configure Gemini client: %s", e)
|
| 60 |
else:
|
| 61 |
+
logger.warning("GEMINI_API_KEY not set. API calls will return placeholders.")
|
| 62 |
|
| 63 |
+
# Supported languages and pest list (original)
|
| 64 |
LANGUAGES = {
|
| 65 |
"en": "English", "hi": "हिंदी (Hindi)", "bn": "বাংলা (Bengali)", "te": "తెలుగు (Telugu)",
|
| 66 |
"mr": "मराठी (Marathi)", "ta": "தமிழ் (Tamil)", "gu": "ગુજરાતી (Gujarati)", "ur": "اردو (Urdu)",
|
|
|
|
| 82 |
{"id": 12, "name": "Fusarium Wilt", "type": "disease", "crop": "Banana, Tomato", "image_url": "/static/images/fusarium_wilt.jpg"}
|
| 83 |
]
|
| 84 |
|
| 85 |
+
# JSON extraction helper (resilient to noisy model output)
|
| 86 |
def extract_json_from_response(content: str):
|
| 87 |
try:
|
| 88 |
return json.loads(content)
|
| 89 |
except json.JSONDecodeError:
|
|
|
|
| 90 |
json_match = re.search(r'```json(.*?)```', content, re.DOTALL)
|
| 91 |
if json_match:
|
| 92 |
json_str = json_match.group(1).strip()
|
| 93 |
else:
|
|
|
|
| 94 |
brace_match = re.search(r'(\{(?:.|\n)*\})', content)
|
| 95 |
json_str = brace_match.group(1) if brace_match else content
|
|
|
|
|
|
|
| 96 |
json_str = json_str.replace('```json', '').replace('```', '').strip()
|
| 97 |
json_str = re.sub(r',(\s*[\]\}])', r'\1', json_str)
|
|
|
|
| 98 |
try:
|
| 99 |
return json.loads(json_str)
|
| 100 |
except json.JSONDecodeError as e:
|
| 101 |
+
logger.error("JSON parsing error after cleanup: %s\nSnippet: %s", e, content[:1000])
|
| 102 |
raise ValueError("Failed to parse JSON response from API")
|
| 103 |
|
|
|
|
| 104 |
@app.route("/")
|
| 105 |
def index():
|
| 106 |
+
# Use session first, cookie fallback. This sets HTML lang attribute properly.
|
| 107 |
language = session.get("language") or request.cookies.get("language") or "en"
|
|
|
|
| 108 |
session["language"] = language
|
| 109 |
session.permanent = True
|
| 110 |
+
return render_template("index.html", pests_diseases=PESTS_DISEASES, languages=LANGUAGES, current_language=language)
|
|
|
|
|
|
|
|
|
|
| 111 |
|
| 112 |
+
# Keep this for compatibility; not required if client only sets cookie & passes lang in fetch
|
| 113 |
@app.route("/set_language", methods=["POST"])
|
| 114 |
def set_language():
|
| 115 |
language = request.form.get("language", "en")
|
| 116 |
if language not in LANGUAGES:
|
|
|
|
| 117 |
language = "en"
|
|
|
|
|
|
|
| 118 |
session["language"] = language
|
| 119 |
session.permanent = True
|
| 120 |
+
logger.info("Language set to %s in session", language)
|
| 121 |
|
|
|
|
| 122 |
resp = make_response(redirect(url_for("index")))
|
|
|
|
| 123 |
max_age = 60 * 60 * 24 * 7
|
| 124 |
secure_flag = False if disable_secure else True
|
| 125 |
+
resp.set_cookie("language", language, max_age=max_age, secure=secure_flag, samesite="None", httponly=False, path="/")
|
| 126 |
return resp
|
| 127 |
|
|
|
|
| 128 |
@app.route("/get_details/<int:pest_id>")
|
| 129 |
+
def get_details(pest_id):
|
| 130 |
+
# Priority: lang query param > session > cookie > default
|
| 131 |
+
lang_param = request.args.get("lang")
|
| 132 |
+
language = lang_param or session.get("language") or request.cookies.get("language") or "en"
|
| 133 |
+
session["language"] = language
|
| 134 |
+
session.permanent = True
|
| 135 |
|
| 136 |
pest = next((p for p in PESTS_DISEASES if p["id"] == pest_id), None)
|
| 137 |
+
if not pest:
|
| 138 |
return jsonify({"error": "Not found"}), 404
|
| 139 |
|
| 140 |
cache_key = f"pest_{pest_id}_{language}"
|
| 141 |
cached = cache.get(cache_key)
|
| 142 |
if cached:
|
| 143 |
+
logger.info("Cache hit for pest %s (%s)", pest_id, language)
|
| 144 |
return jsonify(cached)
|
| 145 |
|
| 146 |
if not GEMINI_API_KEY:
|
| 147 |
+
logger.warning("GEMINI_API_KEY missing; returning placeholder details")
|
| 148 |
result = {
|
| 149 |
**pest,
|
| 150 |
"details": {
|
|
|
|
| 161 |
cache.set(cache_key, result)
|
| 162 |
return jsonify(result)
|
| 163 |
|
| 164 |
+
# Build prompt for Gemini
|
| 165 |
lang_instructions = {
|
| 166 |
"en": "Respond in English",
|
| 167 |
"hi": "हिंदी में जवाब दें (Respond in Hindi)",
|
|
|
|
| 189 |
text = getattr(response, "text", "") or str(response)
|
| 190 |
detailed_info = extract_json_from_response(text)
|
| 191 |
|
|
|
|
| 192 |
required_keys = ["description", "lifecycle", "symptoms", "impact", "management", "prevention"]
|
| 193 |
for key in required_keys:
|
| 194 |
if key not in detailed_info or "text" not in detailed_info.get(key, {}):
|
| 195 |
detailed_info[key] = {"title": key.capitalize(), "text": "Information could not be generated for this section."}
|
| 196 |
+
logger.warning("Missing section in API response: %s", key)
|
| 197 |
|
| 198 |
result = {
|
| 199 |
**pest,
|
|
|
|
| 204 |
cache.set(cache_key, result)
|
| 205 |
logger.info("Cached details for pest %s (%s)", pest_id, language)
|
| 206 |
return jsonify(result)
|
|
|
|
| 207 |
except Exception as e:
|
| 208 |
+
logger.exception("Error fetching from Gemini: %s", e)
|
| 209 |
return jsonify({"error": "Failed to fetch information", "message": str(e)}), 500
|
| 210 |
|
|
|
|
| 211 |
if __name__ == "__main__":
|
| 212 |
port = int(os.environ.get("PORT", 7860))
|
| 213 |
host = os.environ.get("HOST", "0.0.0.0")
|