rajkhanke's picture
Update app.py
05fec25 verified
from flask import Flask, request, jsonify, render_template
import json
import requests
import random
import math
import os
from dotenv import load_dotenv
load_dotenv() # Load environment variables from .env file
app = Flask(__name__)
GEMINI_API_KEY = os.getenv('GEMINI_API_KEY')
NOMINATIM_USER_AGENT = "GeoSafeConstruct/1.0 ([email protected])" # IMPORTANT: Change to your app name and contact email
# --- Nominatim API Functions ---
def get_location_name_from_coords(lat, lon):
"""Fetches a location name/address from coordinates using Nominatim."""
headers = {"User-Agent": NOMINATIM_USER_AGENT}
url = f"https://nominatim.openstreetmap.org/reverse?format=json&lat={lat}&lon={lon}&zoom=16&addressdetails=1"
try:
response = requests.get(url, headers=headers, timeout=10)
response.raise_for_status()
data = response.json()
# Construct a readable address, prioritizing certain fields
address = data.get('address', {})
parts = [
address.get('road'), address.get('neighbourhood'), address.get('suburb'),
address.get('city_district'), address.get('city'), address.get('town'),
address.get('village'), address.get('county'), address.get('state'),
address.get('postcode'), address.get('country')
]
# Filter out None values and join
display_name = data.get('display_name', "Unknown Location")
filtered_parts = [part for part in parts if part]
if len(filtered_parts) > 3: # If many parts, use display_name for brevity
return display_name
elif filtered_parts:
return ", ".join(filtered_parts[:3]) # Take first few relevant parts
return display_name # Fallback to full display_name
except requests.exceptions.RequestException as e:
print(f"Nominatim reverse geocoding error: {e}")
return f"Area around {lat:.3f}, {lon:.3f}"
except (json.JSONDecodeError, KeyError) as e:
print(f"Nominatim response parsing error: {e}")
return f"Area around {lat:.3f}, {lon:.3f}"
def get_coords_from_location_name(query):
"""Fetches coordinates from a location name/address using Nominatim."""
headers = {"User-Agent": NOMINATIM_USER_AGENT}
url = f"https://nominatim.openstreetmap.org/search?q={requests.utils.quote(query)}&format=json&limit=1"
try:
response = requests.get(url, headers=headers, timeout=10)
response.raise_for_status()
data = response.json()
if data and isinstance(data, list) and len(data) > 0:
return {
"latitude": float(data[0].get("lat")),
"longitude": float(data[0].get("lon")),
"display_name": data[0].get("display_name")
}
return None
except requests.exceptions.RequestException as e:
print(f"Nominatim geocoding error: {e}")
return None
except (json.JSONDecodeError, KeyError, IndexError) as e:
print(f"Nominatim geocoding response parsing error: {e}")
return None
# --- Gemini API Functions ---
def call_gemini_api(prompt_text):
"""Generic function to call Gemini API."""
if not GEMINI_API_KEY:
print("GEMINI_API_KEY not found in environment variables.")
return None, "Gemini API key not configured."
url = "https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent"
headers = {
"Content-Type": "application/json",
"x-goog-api-key": GEMINI_API_KEY
}
payload = {
"contents": [{"parts": [{"text": prompt_text}]}],
"generationConfig": {
"response_mime_type": "application/json",
"temperature": 0.6, # Adjust for creativity vs. factuality
"topP": 0.9,
"topK": 40
},
"safetySettings": [ # Add safety settings if needed
{"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_MEDIUM_AND_ABOVE"},
{"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_MEDIUM_AND_ABOVE"},
{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "BLOCK_MEDIUM_AND_ABOVE"},
{"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "BLOCK_MEDIUM_AND_ABOVE"}
]
}
try:
response = requests.post(url, json=payload, headers=headers, timeout=60) # Increased timeout
response.raise_for_status()
gemini_response_data = response.json()
if 'candidates' not in gemini_response_data or not gemini_response_data['candidates']:
print("Gemini API Error: No candidates in response.")
print("Full Gemini Response:", gemini_response_data)
if 'promptFeedback' in gemini_response_data and 'blockReason' in gemini_response_data['promptFeedback']:
return None, f"Content blocked by API: {gemini_response_data['promptFeedback']['blockReason']}"
return None, "Gemini API returned no content."
json_text_content = gemini_response_data['candidates'][0]['content']['parts'][0]['text']
# Attempt to clean JSON string if it's wrapped in markdown
if json_text_content.strip().startswith("```json"):
json_text_content = json_text_content.strip()[7:]
if json_text_content.strip().endswith("```"):
json_text_content = json_text_content.strip()[:-3]
return json.loads(json_text_content.strip()), None
except requests.exceptions.Timeout:
print("Gemini API call timed out.")
return None, "Analysis request timed out. Please try again."
except requests.exceptions.RequestException as e:
print(f"Error calling Gemini API (RequestException): {e}")
return None, f"Could not connect to analysis service: {e}"
except json.JSONDecodeError as e:
print(f"Error decoding JSON from Gemini API: {e}")
print(f"Received text for JSON parsing: {json_text_content if 'json_text_content' in locals() else 'N/A'}")
return None, "AI returned an invalid response format. Please try again."
except (KeyError, IndexError) as e:
print(f"Error parsing Gemini API response structure: {e}")
print(f"Gemini response: {gemini_response_data if 'gemini_response_data' in locals() else 'N/A'}")
return None, "AI returned an unexpected response structure."
except Exception as e:
print(f"An unexpected error occurred during Gemini call: {e}")
return None, f"An unexpected error occurred: {e}"
def get_safety_analysis_with_gemini(latitude, longitude, location_address):
"""
Calls Gemini API to analyze construction safety.
"""
prompt = f"""
**Objective:** Provide a detailed and realistic construction site safety analysis for the location: {location_address} (Latitude: {latitude}, Longitude: {longitude}).
**Output Format:** STRICTLY JSON. Do NOT include any text outside the JSON structure (e.g., no '```json' or '```' wrappers).
**JSON Structure:**
{{
"location_name": "{location_address}", // Use the provided address
"safety_score": "integer (0-100, overall safety score, be critical and realistic)",
"suitability_statement": "string (e.g., 'Generally Suitable with Mitigations', 'High Caution Advised', 'Not Recommended without Major Intervention')",
"summary_assessment": "string (3-4 sentences summarizing key findings, suitability, and major concerns. Be specific.)",
"geological_risks": {{
"earthquake_risk": {{
"level": "string ('Low', 'Medium', 'High', 'Very High', 'Not Assessed')",
"details": "string (Specifics like proximity to faults, historical activity, soil liquefaction potential if known. If not assessed, state why.)"
}},
"landslide_risk": {{
"level": "string ('Low', 'Medium', 'High', 'Very High', 'Not Assessed')",
"details": "string (Slope stability, soil type, vegetation, historical incidents. If not assessed, state why.)"
}},
"soil_stability": {{
"type": "string ('Stable', 'Moderately Stable', 'Unstable', 'Variable', 'Requires Investigation')",
"concerns": ["string array (e.g., 'Expansive clays present', 'High water table', 'Poor drainage', 'Subsidence risk')"]
}}
}},
"hydrological_risks": {{
"flood_risk": {{
"level": "string ('Low', 'Medium', 'High', 'Very High', 'Not Assessed')",
"details": "string (Proximity to water bodies, floodplain maps, historical flooding, drainage issues. If not assessed, state why.)"
}},
"tsunami_risk": {{ // Only include if geographically relevant (coastal)
"level": "string ('Negligible', 'Low', 'Medium', 'High')",
"details": "string (Distance from coast, elevation, historical data.)"
}}
}},
"other_environmental_risks": [ // Array of objects, include if relevant
{{ "type": "Wildfire", "level": "string ('Low', 'Medium', 'High')", "details": "string (Vegetation, climate, fire history)" }},
{{ "type": "Extreme Weather (Hurricanes/Tornadoes/High Winds)", "level": "string ('Low', 'Medium', 'High')", "details": "string (Regional patterns, building code requirements)" }},
{{ "type": "Industrial Hazards", "level": "string ('Low', 'Medium', 'High')", "details": "string (Proximity to industrial plants, pipelines, hazardous material routes)" }}
],
"key_risk_factors_summary": ["string array (Bulleted list of 3-5 most critical risk factors for this specific site)"],
"mitigation_recommendations": ["string array (Actionable, specific recommendations, e.g., 'Conduct Level 2 Geotechnical Survey focusing on shear strength', 'Implement earthquake-resistant design to Zone IV standards', 'Elevate foundation by 1.5m above base flood elevation')"],
"further_investigations_needed": ["string array (e.g., 'Detailed hydrological study', 'Environmental Impact Assessment', 'Traffic impact study')"],
"alternative_locations_analysis": [ // Up to 2-3 alternatives. If none are significantly better, state that.
{{
"name_suggestion": "string (Suggest a conceptual name like 'North Ridge Site' or 'Valley View Plot')",
"latitude": "float (as string, e.g., '18.5234')",
"longitude": "float (as string, e.g., '73.8567')",
"estimated_distance_km": "float (as string, e.g., '8.5')",
"brief_justification": "string (Why is this a potential alternative? E.g., 'Appears to be on more stable ground', 'Further from flood plain')",
"potential_pros": ["string array"],
"potential_cons": ["string array"],
"comparative_safety_score_estimate": "integer (0-100, relative to primary site)"
}}
],
"data_confidence_level": "string ('Low', 'Medium', 'High' - based on assumed availability of public data for this general region, not specific site data which is unknown to you)",
"disclaimer": "This AI-generated analysis is for preliminary informational purposes only and not a substitute for professional engineering, geological, and environmental assessments. On-site investigations are crucial."
}}
**Guidelines for Content:**
- make sumre rnaodmness in score generation not same view ppijn t everytime
- Be realistic. If data for a specific risk is unlikely to be publicly available for a random coordinate, mark it 'Not Assessed' and explain.
- Focus on actionable insights.
- For alternative locations, provide *different* lat/lon coordinates that are plausibly nearby (within 5-20km).
- Ensure all string values are properly quoted. Ensure latitudes and longitudes for alternatives are strings representing floats.
- The safety_score should reflect a comprehensive evaluation of all risks.
- proper spacing and upo down margins shoudl be there
"""
return call_gemini_api(prompt)
# --- Flask Routes ---
@app.route('/')
def index():
return render_template('index.html')
@app.route('/api/geocode', methods=['POST'])
def geocode_location():
data = request.json
query = data.get('query')
if not query:
return jsonify({"error": "Query parameter is required."}), 400
coords_data = get_coords_from_location_name(query)
if coords_data:
return jsonify(coords_data), 200
else:
return jsonify({"error": "Location not found or geocoding failed."}), 404
@app.route('/api/analyze_location', methods=['POST'])
def analyze_location_route():
data = request.json
try:
latitude = float(data.get('latitude'))
longitude = float(data.get('longitude'))
except (TypeError, ValueError):
return jsonify({"error": "Invalid latitude or longitude provided."}), 400
# Get a human-readable name for the primary location
primary_location_address = get_location_name_from_coords(latitude, longitude)
# Call Gemini for the main analysis
analysis_data, error = get_safety_analysis_with_gemini(latitude, longitude, primary_location_address)
if error:
# Fallback to a simpler mock-like structure if Gemini fails critically
return jsonify({
"error_message": error,
"location_name": primary_location_address,
"safety_score": random.randint(20, 50), # Low score to indicate issue
"summary_assessment": f"Could not perform detailed analysis due to: {error}. Basic location: {primary_location_address}.",
"suitability_statement": "Analysis Incomplete",
"geological_risks": {"earthquake_risk": {"level": "Not Assessed", "details": error}},
"hydrological_risks": {"flood_risk": {"level": "Not Assessed", "details": error}},
"disclaimer": "A technical issue prevented full analysis."
}), 500
# Post-process alternative locations to get their names
if analysis_data.get("alternative_locations_analysis"):
for alt_loc in analysis_data["alternative_locations_analysis"]:
try:
alt_lat = float(alt_loc.get("latitude"))
alt_lon = float(alt_loc.get("longitude"))
alt_loc["actual_name"] = get_location_name_from_coords(alt_lat, alt_lon)
# Ensure distance is calculated if not provided by Gemini or if it's nonsensical
if not alt_loc.get("estimated_distance_km") or float(alt_loc.get("estimated_distance_km", 0)) == 0:
alt_loc["estimated_distance_km"] = str(
calculate_haversine_distance(latitude, longitude, alt_lat, alt_lon))
except (TypeError, ValueError, KeyError) as e:
print(f"Error processing alternative location coords: {e}, data: {alt_loc}")
alt_loc["actual_name"] = "Nearby Area (details unavailable)"
# Fallback if lat/lon are missing or invalid for an alternative
if "latitude" not in alt_loc or "longitude" not in alt_loc:
alt_loc["latitude"] = str(latitude + (random.random() - 0.5) * 0.05) # Small offset
alt_loc["longitude"] = str(longitude + (random.random() - 0.5) * 0.05)
return jsonify(analysis_data), 200
def calculate_haversine_distance(lat1, lon1, lat2, lon2):
R = 6371 # Radius of Earth in kilometers
try:
lat1_rad, lon1_rad = math.radians(float(lat1)), math.radians(float(lon1))
lat2_rad, lon2_rad = math.radians(float(lat2)), math.radians(float(lon2))
except (ValueError, TypeError):
print(f"Error converting lat/lon to float for Haversine: {lat1}, {lon1}, {lat2}, {lon2}")
return 0.0 # Fallback
dlon = lon2_rad - lon1_rad
dlat = lat2_rad - lat1_rad
a = math.sin(dlat / 2) ** 2 + math.cos(lat1_rad) * math.cos(lat2_rad) * math.sin(dlon / 2) ** 2
c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
distance = R * c
return round(distance, 1)
if __name__ == '__main__':
if not GEMINI_API_KEY:
print("CRITICAL ERROR: GEMINI_API_KEY is not set in the environment.")
print("Please create a .env file with GEMINI_API_KEY=YOUR_API_KEY or set it as an environment variable.")
app.run(debug=True, port=5000)