Update app.py
Browse files
app.py
CHANGED
@@ -1,307 +1,307 @@
|
|
1 |
-
from flask import Flask, request, jsonify, render_template
|
2 |
-
import json
|
3 |
-
import requests
|
4 |
-
import random
|
5 |
-
import math
|
6 |
-
import os
|
7 |
-
from dotenv import load_dotenv
|
8 |
-
|
9 |
-
load_dotenv() # Load environment variables from .env file
|
10 |
-
|
11 |
-
app = Flask(__name__)
|
12 |
-
|
13 |
-
GEMINI_API_KEY =
|
14 |
-
NOMINATIM_USER_AGENT = "GeoSafeConstruct/1.0 ([email protected])" # IMPORTANT: Change to your app name and contact email
|
15 |
-
|
16 |
-
|
17 |
-
# --- Nominatim API Functions ---
|
18 |
-
def get_location_name_from_coords(lat, lon):
|
19 |
-
"""Fetches a location name/address from coordinates using Nominatim."""
|
20 |
-
headers = {"User-Agent": NOMINATIM_USER_AGENT}
|
21 |
-
url = f"https://nominatim.openstreetmap.org/reverse?format=json&lat={lat}&lon={lon}&zoom=16&addressdetails=1"
|
22 |
-
try:
|
23 |
-
response = requests.get(url, headers=headers, timeout=10)
|
24 |
-
response.raise_for_status()
|
25 |
-
data = response.json()
|
26 |
-
# Construct a readable address, prioritizing certain fields
|
27 |
-
address = data.get('address', {})
|
28 |
-
parts = [
|
29 |
-
address.get('road'), address.get('neighbourhood'), address.get('suburb'),
|
30 |
-
address.get('city_district'), address.get('city'), address.get('town'),
|
31 |
-
address.get('village'), address.get('county'), address.get('state'),
|
32 |
-
address.get('postcode'), address.get('country')
|
33 |
-
]
|
34 |
-
# Filter out None values and join
|
35 |
-
display_name = data.get('display_name', "Unknown Location")
|
36 |
-
filtered_parts = [part for part in parts if part]
|
37 |
-
if len(filtered_parts) > 3: # If many parts, use display_name for brevity
|
38 |
-
return display_name
|
39 |
-
elif filtered_parts:
|
40 |
-
return ", ".join(filtered_parts[:3]) # Take first few relevant parts
|
41 |
-
return display_name # Fallback to full display_name
|
42 |
-
except requests.exceptions.RequestException as e:
|
43 |
-
print(f"Nominatim reverse geocoding error: {e}")
|
44 |
-
return f"Area around {lat:.3f}, {lon:.3f}"
|
45 |
-
except (json.JSONDecodeError, KeyError) as e:
|
46 |
-
print(f"Nominatim response parsing error: {e}")
|
47 |
-
return f"Area around {lat:.3f}, {lon:.3f}"
|
48 |
-
|
49 |
-
|
50 |
-
def get_coords_from_location_name(query):
|
51 |
-
"""Fetches coordinates from a location name/address using Nominatim."""
|
52 |
-
headers = {"User-Agent": NOMINATIM_USER_AGENT}
|
53 |
-
url = f"https://nominatim.openstreetmap.org/search?q={requests.utils.quote(query)}&format=json&limit=1"
|
54 |
-
try:
|
55 |
-
response = requests.get(url, headers=headers, timeout=10)
|
56 |
-
response.raise_for_status()
|
57 |
-
data = response.json()
|
58 |
-
if data and isinstance(data, list) and len(data) > 0:
|
59 |
-
return {
|
60 |
-
"latitude": float(data[0].get("lat")),
|
61 |
-
"longitude": float(data[0].get("lon")),
|
62 |
-
"display_name": data[0].get("display_name")
|
63 |
-
}
|
64 |
-
return None
|
65 |
-
except requests.exceptions.RequestException as e:
|
66 |
-
print(f"Nominatim geocoding error: {e}")
|
67 |
-
return None
|
68 |
-
except (json.JSONDecodeError, KeyError, IndexError) as e:
|
69 |
-
print(f"Nominatim geocoding response parsing error: {e}")
|
70 |
-
return None
|
71 |
-
|
72 |
-
|
73 |
-
# --- Gemini API Functions ---
|
74 |
-
def call_gemini_api(prompt_text):
|
75 |
-
"""Generic function to call Gemini API."""
|
76 |
-
if not GEMINI_API_KEY:
|
77 |
-
print("GEMINI_API_KEY not found in environment variables.")
|
78 |
-
return None, "Gemini API key not configured."
|
79 |
-
|
80 |
-
url = "https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent"
|
81 |
-
headers = {
|
82 |
-
"Content-Type": "application/json",
|
83 |
-
"x-goog-api-key": GEMINI_API_KEY
|
84 |
-
}
|
85 |
-
payload = {
|
86 |
-
"contents": [{"parts": [{"text": prompt_text}]}],
|
87 |
-
"generationConfig": {
|
88 |
-
"response_mime_type": "application/json",
|
89 |
-
"temperature": 0.6, # Adjust for creativity vs. factuality
|
90 |
-
"topP": 0.9,
|
91 |
-
"topK": 40
|
92 |
-
},
|
93 |
-
"safetySettings": [ # Add safety settings if needed
|
94 |
-
{"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_MEDIUM_AND_ABOVE"},
|
95 |
-
{"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_MEDIUM_AND_ABOVE"},
|
96 |
-
{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "BLOCK_MEDIUM_AND_ABOVE"},
|
97 |
-
{"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "BLOCK_MEDIUM_AND_ABOVE"}
|
98 |
-
]
|
99 |
-
}
|
100 |
-
|
101 |
-
try:
|
102 |
-
response = requests.post(url, json=payload, headers=headers, timeout=60) # Increased timeout
|
103 |
-
response.raise_for_status()
|
104 |
-
gemini_response_data = response.json()
|
105 |
-
|
106 |
-
if 'candidates' not in gemini_response_data or not gemini_response_data['candidates']:
|
107 |
-
print("Gemini API Error: No candidates in response.")
|
108 |
-
print("Full Gemini Response:", gemini_response_data)
|
109 |
-
if 'promptFeedback' in gemini_response_data and 'blockReason' in gemini_response_data['promptFeedback']:
|
110 |
-
return None, f"Content blocked by API: {gemini_response_data['promptFeedback']['blockReason']}"
|
111 |
-
return None, "Gemini API returned no content."
|
112 |
-
|
113 |
-
json_text_content = gemini_response_data['candidates'][0]['content']['parts'][0]['text']
|
114 |
-
|
115 |
-
# Attempt to clean JSON string if it's wrapped in markdown
|
116 |
-
if json_text_content.strip().startswith("```json"):
|
117 |
-
json_text_content = json_text_content.strip()[7:]
|
118 |
-
if json_text_content.strip().endswith("```"):
|
119 |
-
json_text_content = json_text_content.strip()[:-3]
|
120 |
-
|
121 |
-
return json.loads(json_text_content.strip()), None
|
122 |
-
except requests.exceptions.Timeout:
|
123 |
-
print("Gemini API call timed out.")
|
124 |
-
return None, "Analysis request timed out. Please try again."
|
125 |
-
except requests.exceptions.RequestException as e:
|
126 |
-
print(f"Error calling Gemini API (RequestException): {e}")
|
127 |
-
return None, f"Could not connect to analysis service: {e}"
|
128 |
-
except json.JSONDecodeError as e:
|
129 |
-
print(f"Error decoding JSON from Gemini API: {e}")
|
130 |
-
print(f"Received text for JSON parsing: {json_text_content if 'json_text_content' in locals() else 'N/A'}")
|
131 |
-
return None, "AI returned an invalid response format. Please try again."
|
132 |
-
except (KeyError, IndexError) as e:
|
133 |
-
print(f"Error parsing Gemini API response structure: {e}")
|
134 |
-
print(f"Gemini response: {gemini_response_data if 'gemini_response_data' in locals() else 'N/A'}")
|
135 |
-
return None, "AI returned an unexpected response structure."
|
136 |
-
except Exception as e:
|
137 |
-
print(f"An unexpected error occurred during Gemini call: {e}")
|
138 |
-
return None, f"An unexpected error occurred: {e}"
|
139 |
-
|
140 |
-
|
141 |
-
def get_safety_analysis_with_gemini(latitude, longitude, location_address):
|
142 |
-
"""
|
143 |
-
Calls Gemini API to analyze construction safety.
|
144 |
-
"""
|
145 |
-
prompt = f"""
|
146 |
-
**Objective:** Provide a detailed and realistic construction site safety analysis for the location: {location_address} (Latitude: {latitude}, Longitude: {longitude}).
|
147 |
-
|
148 |
-
**Output Format:** STRICTLY JSON. Do NOT include any text outside the JSON structure (e.g., no '```json' or '```' wrappers).
|
149 |
-
|
150 |
-
**JSON Structure:**
|
151 |
-
{{
|
152 |
-
"location_name": "{location_address}", // Use the provided address
|
153 |
-
"safety_score": "integer (0-100, overall safety score, be critical and realistic)",
|
154 |
-
"suitability_statement": "string (e.g., 'Generally Suitable with Mitigations', 'High Caution Advised', 'Not Recommended without Major Intervention')",
|
155 |
-
"summary_assessment": "string (3-4 sentences summarizing key findings, suitability, and major concerns. Be specific.)",
|
156 |
-
"geological_risks": {{
|
157 |
-
"earthquake_risk": {{
|
158 |
-
"level": "string ('Low', 'Medium', 'High', 'Very High', 'Not Assessed')",
|
159 |
-
"details": "string (Specifics like proximity to faults, historical activity, soil liquefaction potential if known. If not assessed, state why.)"
|
160 |
-
}},
|
161 |
-
"landslide_risk": {{
|
162 |
-
"level": "string ('Low', 'Medium', 'High', 'Very High', 'Not Assessed')",
|
163 |
-
"details": "string (Slope stability, soil type, vegetation, historical incidents. If not assessed, state why.)"
|
164 |
-
}},
|
165 |
-
"soil_stability": {{
|
166 |
-
"type": "string ('Stable', 'Moderately Stable', 'Unstable', 'Variable', 'Requires Investigation')",
|
167 |
-
"concerns": ["string array (e.g., 'Expansive clays present', 'High water table', 'Poor drainage', 'Subsidence risk')"]
|
168 |
-
}}
|
169 |
-
}},
|
170 |
-
"hydrological_risks": {{
|
171 |
-
"flood_risk": {{
|
172 |
-
"level": "string ('Low', 'Medium', 'High', 'Very High', 'Not Assessed')",
|
173 |
-
"details": "string (Proximity to water bodies, floodplain maps, historical flooding, drainage issues. If not assessed, state why.)"
|
174 |
-
}},
|
175 |
-
"tsunami_risk": {{ // Only include if geographically relevant (coastal)
|
176 |
-
"level": "string ('Negligible', 'Low', 'Medium', 'High')",
|
177 |
-
"details": "string (Distance from coast, elevation, historical data.)"
|
178 |
-
}}
|
179 |
-
}},
|
180 |
-
"other_environmental_risks": [ // Array of objects, include if relevant
|
181 |
-
{{ "type": "Wildfire", "level": "string ('Low', 'Medium', 'High')", "details": "string (Vegetation, climate, fire history)" }},
|
182 |
-
{{ "type": "Extreme Weather (Hurricanes/Tornadoes/High Winds)", "level": "string ('Low', 'Medium', 'High')", "details": "string (Regional patterns, building code requirements)" }},
|
183 |
-
{{ "type": "Industrial Hazards", "level": "string ('Low', 'Medium', 'High')", "details": "string (Proximity to industrial plants, pipelines, hazardous material routes)" }}
|
184 |
-
],
|
185 |
-
"key_risk_factors_summary": ["string array (Bulleted list of 3-5 most critical risk factors for this specific site)"],
|
186 |
-
"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')"],
|
187 |
-
"further_investigations_needed": ["string array (e.g., 'Detailed hydrological study', 'Environmental Impact Assessment', 'Traffic impact study')"],
|
188 |
-
"alternative_locations_analysis": [ // Up to 2-3 alternatives. If none are significantly better, state that.
|
189 |
-
{{
|
190 |
-
"name_suggestion": "string (Suggest a conceptual name like 'North Ridge Site' or 'Valley View Plot')",
|
191 |
-
"latitude": "float (as string, e.g., '18.5234')",
|
192 |
-
"longitude": "float (as string, e.g., '73.8567')",
|
193 |
-
"estimated_distance_km": "float (as string, e.g., '8.5')",
|
194 |
-
"brief_justification": "string (Why is this a potential alternative? E.g., 'Appears to be on more stable ground', 'Further from flood plain')",
|
195 |
-
"potential_pros": ["string array"],
|
196 |
-
"potential_cons": ["string array"],
|
197 |
-
"comparative_safety_score_estimate": "integer (0-100, relative to primary site)"
|
198 |
-
}}
|
199 |
-
],
|
200 |
-
"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)",
|
201 |
-
"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."
|
202 |
-
}}
|
203 |
-
|
204 |
-
**Guidelines for Content:**
|
205 |
-
- make sumre rnaodmness in score generation not same view ppijn t everytime
|
206 |
-
- Be realistic. If data for a specific risk is unlikely to be publicly available for a random coordinate, mark it 'Not Assessed' and explain.
|
207 |
-
- Focus on actionable insights.
|
208 |
-
- For alternative locations, provide *different* lat/lon coordinates that are plausibly nearby (within 5-20km).
|
209 |
-
- Ensure all string values are properly quoted. Ensure latitudes and longitudes for alternatives are strings representing floats.
|
210 |
-
- The safety_score should reflect a comprehensive evaluation of all risks.
|
211 |
-
- proper spacing and upo down margins shoudl be there
|
212 |
-
"""
|
213 |
-
return call_gemini_api(prompt)
|
214 |
-
|
215 |
-
|
216 |
-
# --- Flask Routes ---
|
217 |
-
@app.route('/')
|
218 |
-
def index():
|
219 |
-
return render_template('index.html')
|
220 |
-
|
221 |
-
|
222 |
-
@app.route('/api/geocode', methods=['POST'])
|
223 |
-
def geocode_location():
|
224 |
-
data = request.json
|
225 |
-
query = data.get('query')
|
226 |
-
if not query:
|
227 |
-
return jsonify({"error": "Query parameter is required."}), 400
|
228 |
-
|
229 |
-
coords_data = get_coords_from_location_name(query)
|
230 |
-
if coords_data:
|
231 |
-
return jsonify(coords_data), 200
|
232 |
-
else:
|
233 |
-
return jsonify({"error": "Location not found or geocoding failed."}), 404
|
234 |
-
|
235 |
-
|
236 |
-
@app.route('/api/analyze_location', methods=['POST'])
|
237 |
-
def analyze_location_route():
|
238 |
-
data = request.json
|
239 |
-
try:
|
240 |
-
latitude = float(data.get('latitude'))
|
241 |
-
longitude = float(data.get('longitude'))
|
242 |
-
except (TypeError, ValueError):
|
243 |
-
return jsonify({"error": "Invalid latitude or longitude provided."}), 400
|
244 |
-
|
245 |
-
# Get a human-readable name for the primary location
|
246 |
-
primary_location_address = get_location_name_from_coords(latitude, longitude)
|
247 |
-
|
248 |
-
# Call Gemini for the main analysis
|
249 |
-
analysis_data, error = get_safety_analysis_with_gemini(latitude, longitude, primary_location_address)
|
250 |
-
|
251 |
-
if error:
|
252 |
-
# Fallback to a simpler mock-like structure if Gemini fails critically
|
253 |
-
return jsonify({
|
254 |
-
"error_message": error,
|
255 |
-
"location_name": primary_location_address,
|
256 |
-
"safety_score": random.randint(20, 50), # Low score to indicate issue
|
257 |
-
"summary_assessment": f"Could not perform detailed analysis due to: {error}. Basic location: {primary_location_address}.",
|
258 |
-
"suitability_statement": "Analysis Incomplete",
|
259 |
-
"geological_risks": {"earthquake_risk": {"level": "Not Assessed", "details": error}},
|
260 |
-
"hydrological_risks": {"flood_risk": {"level": "Not Assessed", "details": error}},
|
261 |
-
"disclaimer": "A technical issue prevented full analysis."
|
262 |
-
}), 500
|
263 |
-
|
264 |
-
# Post-process alternative locations to get their names
|
265 |
-
if analysis_data.get("alternative_locations_analysis"):
|
266 |
-
for alt_loc in analysis_data["alternative_locations_analysis"]:
|
267 |
-
try:
|
268 |
-
alt_lat = float(alt_loc.get("latitude"))
|
269 |
-
alt_lon = float(alt_loc.get("longitude"))
|
270 |
-
alt_loc["actual_name"] = get_location_name_from_coords(alt_lat, alt_lon)
|
271 |
-
# Ensure distance is calculated if not provided by Gemini or if it's nonsensical
|
272 |
-
if not alt_loc.get("estimated_distance_km") or float(alt_loc.get("estimated_distance_km", 0)) == 0:
|
273 |
-
alt_loc["estimated_distance_km"] = str(
|
274 |
-
calculate_haversine_distance(latitude, longitude, alt_lat, alt_lon))
|
275 |
-
|
276 |
-
except (TypeError, ValueError, KeyError) as e:
|
277 |
-
print(f"Error processing alternative location coords: {e}, data: {alt_loc}")
|
278 |
-
alt_loc["actual_name"] = "Nearby Area (details unavailable)"
|
279 |
-
# Fallback if lat/lon are missing or invalid for an alternative
|
280 |
-
if "latitude" not in alt_loc or "longitude" not in alt_loc:
|
281 |
-
alt_loc["latitude"] = str(latitude + (random.random() - 0.5) * 0.05) # Small offset
|
282 |
-
alt_loc["longitude"] = str(longitude + (random.random() - 0.5) * 0.05)
|
283 |
-
|
284 |
-
return jsonify(analysis_data), 200
|
285 |
-
|
286 |
-
|
287 |
-
def calculate_haversine_distance(lat1, lon1, lat2, lon2):
|
288 |
-
R = 6371 # Radius of Earth in kilometers
|
289 |
-
try:
|
290 |
-
lat1_rad, lon1_rad = math.radians(float(lat1)), math.radians(float(lon1))
|
291 |
-
lat2_rad, lon2_rad = math.radians(float(lat2)), math.radians(float(lon2))
|
292 |
-
except (ValueError, TypeError):
|
293 |
-
print(f"Error converting lat/lon to float for Haversine: {lat1}, {lon1}, {lat2}, {lon2}")
|
294 |
-
return 0.0 # Fallback
|
295 |
-
dlon = lon2_rad - lon1_rad
|
296 |
-
dlat = lat2_rad - lat1_rad
|
297 |
-
a = math.sin(dlat / 2) ** 2 + math.cos(lat1_rad) * math.cos(lat2_rad) * math.sin(dlon / 2) ** 2
|
298 |
-
c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
|
299 |
-
distance = R * c
|
300 |
-
return round(distance, 1)
|
301 |
-
|
302 |
-
|
303 |
-
if __name__ == '__main__':
|
304 |
-
if not GEMINI_API_KEY:
|
305 |
-
print("CRITICAL ERROR: GEMINI_API_KEY is not set in the environment.")
|
306 |
-
print("Please create a .env file with GEMINI_API_KEY=YOUR_API_KEY or set it as an environment variable.")
|
307 |
app.run(debug=True, port=5000)
|
|
|
1 |
+
from flask import Flask, request, jsonify, render_template
|
2 |
+
import json
|
3 |
+
import requests
|
4 |
+
import random
|
5 |
+
import math
|
6 |
+
import os
|
7 |
+
from dotenv import load_dotenv
|
8 |
+
|
9 |
+
load_dotenv() # Load environment variables from .env file
|
10 |
+
|
11 |
+
app = Flask(__name__)
|
12 |
+
|
13 |
+
GEMINI_API_KEY = os.getenv('GEMINI_API_KEY')
|
14 |
+
NOMINATIM_USER_AGENT = "GeoSafeConstruct/1.0 ([email protected])" # IMPORTANT: Change to your app name and contact email
|
15 |
+
|
16 |
+
|
17 |
+
# --- Nominatim API Functions ---
|
18 |
+
def get_location_name_from_coords(lat, lon):
|
19 |
+
"""Fetches a location name/address from coordinates using Nominatim."""
|
20 |
+
headers = {"User-Agent": NOMINATIM_USER_AGENT}
|
21 |
+
url = f"https://nominatim.openstreetmap.org/reverse?format=json&lat={lat}&lon={lon}&zoom=16&addressdetails=1"
|
22 |
+
try:
|
23 |
+
response = requests.get(url, headers=headers, timeout=10)
|
24 |
+
response.raise_for_status()
|
25 |
+
data = response.json()
|
26 |
+
# Construct a readable address, prioritizing certain fields
|
27 |
+
address = data.get('address', {})
|
28 |
+
parts = [
|
29 |
+
address.get('road'), address.get('neighbourhood'), address.get('suburb'),
|
30 |
+
address.get('city_district'), address.get('city'), address.get('town'),
|
31 |
+
address.get('village'), address.get('county'), address.get('state'),
|
32 |
+
address.get('postcode'), address.get('country')
|
33 |
+
]
|
34 |
+
# Filter out None values and join
|
35 |
+
display_name = data.get('display_name', "Unknown Location")
|
36 |
+
filtered_parts = [part for part in parts if part]
|
37 |
+
if len(filtered_parts) > 3: # If many parts, use display_name for brevity
|
38 |
+
return display_name
|
39 |
+
elif filtered_parts:
|
40 |
+
return ", ".join(filtered_parts[:3]) # Take first few relevant parts
|
41 |
+
return display_name # Fallback to full display_name
|
42 |
+
except requests.exceptions.RequestException as e:
|
43 |
+
print(f"Nominatim reverse geocoding error: {e}")
|
44 |
+
return f"Area around {lat:.3f}, {lon:.3f}"
|
45 |
+
except (json.JSONDecodeError, KeyError) as e:
|
46 |
+
print(f"Nominatim response parsing error: {e}")
|
47 |
+
return f"Area around {lat:.3f}, {lon:.3f}"
|
48 |
+
|
49 |
+
|
50 |
+
def get_coords_from_location_name(query):
|
51 |
+
"""Fetches coordinates from a location name/address using Nominatim."""
|
52 |
+
headers = {"User-Agent": NOMINATIM_USER_AGENT}
|
53 |
+
url = f"https://nominatim.openstreetmap.org/search?q={requests.utils.quote(query)}&format=json&limit=1"
|
54 |
+
try:
|
55 |
+
response = requests.get(url, headers=headers, timeout=10)
|
56 |
+
response.raise_for_status()
|
57 |
+
data = response.json()
|
58 |
+
if data and isinstance(data, list) and len(data) > 0:
|
59 |
+
return {
|
60 |
+
"latitude": float(data[0].get("lat")),
|
61 |
+
"longitude": float(data[0].get("lon")),
|
62 |
+
"display_name": data[0].get("display_name")
|
63 |
+
}
|
64 |
+
return None
|
65 |
+
except requests.exceptions.RequestException as e:
|
66 |
+
print(f"Nominatim geocoding error: {e}")
|
67 |
+
return None
|
68 |
+
except (json.JSONDecodeError, KeyError, IndexError) as e:
|
69 |
+
print(f"Nominatim geocoding response parsing error: {e}")
|
70 |
+
return None
|
71 |
+
|
72 |
+
|
73 |
+
# --- Gemini API Functions ---
|
74 |
+
def call_gemini_api(prompt_text):
|
75 |
+
"""Generic function to call Gemini API."""
|
76 |
+
if not GEMINI_API_KEY:
|
77 |
+
print("GEMINI_API_KEY not found in environment variables.")
|
78 |
+
return None, "Gemini API key not configured."
|
79 |
+
|
80 |
+
url = "https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent"
|
81 |
+
headers = {
|
82 |
+
"Content-Type": "application/json",
|
83 |
+
"x-goog-api-key": GEMINI_API_KEY
|
84 |
+
}
|
85 |
+
payload = {
|
86 |
+
"contents": [{"parts": [{"text": prompt_text}]}],
|
87 |
+
"generationConfig": {
|
88 |
+
"response_mime_type": "application/json",
|
89 |
+
"temperature": 0.6, # Adjust for creativity vs. factuality
|
90 |
+
"topP": 0.9,
|
91 |
+
"topK": 40
|
92 |
+
},
|
93 |
+
"safetySettings": [ # Add safety settings if needed
|
94 |
+
{"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_MEDIUM_AND_ABOVE"},
|
95 |
+
{"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_MEDIUM_AND_ABOVE"},
|
96 |
+
{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "BLOCK_MEDIUM_AND_ABOVE"},
|
97 |
+
{"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "BLOCK_MEDIUM_AND_ABOVE"}
|
98 |
+
]
|
99 |
+
}
|
100 |
+
|
101 |
+
try:
|
102 |
+
response = requests.post(url, json=payload, headers=headers, timeout=60) # Increased timeout
|
103 |
+
response.raise_for_status()
|
104 |
+
gemini_response_data = response.json()
|
105 |
+
|
106 |
+
if 'candidates' not in gemini_response_data or not gemini_response_data['candidates']:
|
107 |
+
print("Gemini API Error: No candidates in response.")
|
108 |
+
print("Full Gemini Response:", gemini_response_data)
|
109 |
+
if 'promptFeedback' in gemini_response_data and 'blockReason' in gemini_response_data['promptFeedback']:
|
110 |
+
return None, f"Content blocked by API: {gemini_response_data['promptFeedback']['blockReason']}"
|
111 |
+
return None, "Gemini API returned no content."
|
112 |
+
|
113 |
+
json_text_content = gemini_response_data['candidates'][0]['content']['parts'][0]['text']
|
114 |
+
|
115 |
+
# Attempt to clean JSON string if it's wrapped in markdown
|
116 |
+
if json_text_content.strip().startswith("```json"):
|
117 |
+
json_text_content = json_text_content.strip()[7:]
|
118 |
+
if json_text_content.strip().endswith("```"):
|
119 |
+
json_text_content = json_text_content.strip()[:-3]
|
120 |
+
|
121 |
+
return json.loads(json_text_content.strip()), None
|
122 |
+
except requests.exceptions.Timeout:
|
123 |
+
print("Gemini API call timed out.")
|
124 |
+
return None, "Analysis request timed out. Please try again."
|
125 |
+
except requests.exceptions.RequestException as e:
|
126 |
+
print(f"Error calling Gemini API (RequestException): {e}")
|
127 |
+
return None, f"Could not connect to analysis service: {e}"
|
128 |
+
except json.JSONDecodeError as e:
|
129 |
+
print(f"Error decoding JSON from Gemini API: {e}")
|
130 |
+
print(f"Received text for JSON parsing: {json_text_content if 'json_text_content' in locals() else 'N/A'}")
|
131 |
+
return None, "AI returned an invalid response format. Please try again."
|
132 |
+
except (KeyError, IndexError) as e:
|
133 |
+
print(f"Error parsing Gemini API response structure: {e}")
|
134 |
+
print(f"Gemini response: {gemini_response_data if 'gemini_response_data' in locals() else 'N/A'}")
|
135 |
+
return None, "AI returned an unexpected response structure."
|
136 |
+
except Exception as e:
|
137 |
+
print(f"An unexpected error occurred during Gemini call: {e}")
|
138 |
+
return None, f"An unexpected error occurred: {e}"
|
139 |
+
|
140 |
+
|
141 |
+
def get_safety_analysis_with_gemini(latitude, longitude, location_address):
|
142 |
+
"""
|
143 |
+
Calls Gemini API to analyze construction safety.
|
144 |
+
"""
|
145 |
+
prompt = f"""
|
146 |
+
**Objective:** Provide a detailed and realistic construction site safety analysis for the location: {location_address} (Latitude: {latitude}, Longitude: {longitude}).
|
147 |
+
|
148 |
+
**Output Format:** STRICTLY JSON. Do NOT include any text outside the JSON structure (e.g., no '```json' or '```' wrappers).
|
149 |
+
|
150 |
+
**JSON Structure:**
|
151 |
+
{{
|
152 |
+
"location_name": "{location_address}", // Use the provided address
|
153 |
+
"safety_score": "integer (0-100, overall safety score, be critical and realistic)",
|
154 |
+
"suitability_statement": "string (e.g., 'Generally Suitable with Mitigations', 'High Caution Advised', 'Not Recommended without Major Intervention')",
|
155 |
+
"summary_assessment": "string (3-4 sentences summarizing key findings, suitability, and major concerns. Be specific.)",
|
156 |
+
"geological_risks": {{
|
157 |
+
"earthquake_risk": {{
|
158 |
+
"level": "string ('Low', 'Medium', 'High', 'Very High', 'Not Assessed')",
|
159 |
+
"details": "string (Specifics like proximity to faults, historical activity, soil liquefaction potential if known. If not assessed, state why.)"
|
160 |
+
}},
|
161 |
+
"landslide_risk": {{
|
162 |
+
"level": "string ('Low', 'Medium', 'High', 'Very High', 'Not Assessed')",
|
163 |
+
"details": "string (Slope stability, soil type, vegetation, historical incidents. If not assessed, state why.)"
|
164 |
+
}},
|
165 |
+
"soil_stability": {{
|
166 |
+
"type": "string ('Stable', 'Moderately Stable', 'Unstable', 'Variable', 'Requires Investigation')",
|
167 |
+
"concerns": ["string array (e.g., 'Expansive clays present', 'High water table', 'Poor drainage', 'Subsidence risk')"]
|
168 |
+
}}
|
169 |
+
}},
|
170 |
+
"hydrological_risks": {{
|
171 |
+
"flood_risk": {{
|
172 |
+
"level": "string ('Low', 'Medium', 'High', 'Very High', 'Not Assessed')",
|
173 |
+
"details": "string (Proximity to water bodies, floodplain maps, historical flooding, drainage issues. If not assessed, state why.)"
|
174 |
+
}},
|
175 |
+
"tsunami_risk": {{ // Only include if geographically relevant (coastal)
|
176 |
+
"level": "string ('Negligible', 'Low', 'Medium', 'High')",
|
177 |
+
"details": "string (Distance from coast, elevation, historical data.)"
|
178 |
+
}}
|
179 |
+
}},
|
180 |
+
"other_environmental_risks": [ // Array of objects, include if relevant
|
181 |
+
{{ "type": "Wildfire", "level": "string ('Low', 'Medium', 'High')", "details": "string (Vegetation, climate, fire history)" }},
|
182 |
+
{{ "type": "Extreme Weather (Hurricanes/Tornadoes/High Winds)", "level": "string ('Low', 'Medium', 'High')", "details": "string (Regional patterns, building code requirements)" }},
|
183 |
+
{{ "type": "Industrial Hazards", "level": "string ('Low', 'Medium', 'High')", "details": "string (Proximity to industrial plants, pipelines, hazardous material routes)" }}
|
184 |
+
],
|
185 |
+
"key_risk_factors_summary": ["string array (Bulleted list of 3-5 most critical risk factors for this specific site)"],
|
186 |
+
"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')"],
|
187 |
+
"further_investigations_needed": ["string array (e.g., 'Detailed hydrological study', 'Environmental Impact Assessment', 'Traffic impact study')"],
|
188 |
+
"alternative_locations_analysis": [ // Up to 2-3 alternatives. If none are significantly better, state that.
|
189 |
+
{{
|
190 |
+
"name_suggestion": "string (Suggest a conceptual name like 'North Ridge Site' or 'Valley View Plot')",
|
191 |
+
"latitude": "float (as string, e.g., '18.5234')",
|
192 |
+
"longitude": "float (as string, e.g., '73.8567')",
|
193 |
+
"estimated_distance_km": "float (as string, e.g., '8.5')",
|
194 |
+
"brief_justification": "string (Why is this a potential alternative? E.g., 'Appears to be on more stable ground', 'Further from flood plain')",
|
195 |
+
"potential_pros": ["string array"],
|
196 |
+
"potential_cons": ["string array"],
|
197 |
+
"comparative_safety_score_estimate": "integer (0-100, relative to primary site)"
|
198 |
+
}}
|
199 |
+
],
|
200 |
+
"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)",
|
201 |
+
"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."
|
202 |
+
}}
|
203 |
+
|
204 |
+
**Guidelines for Content:**
|
205 |
+
- make sumre rnaodmness in score generation not same view ppijn t everytime
|
206 |
+
- Be realistic. If data for a specific risk is unlikely to be publicly available for a random coordinate, mark it 'Not Assessed' and explain.
|
207 |
+
- Focus on actionable insights.
|
208 |
+
- For alternative locations, provide *different* lat/lon coordinates that are plausibly nearby (within 5-20km).
|
209 |
+
- Ensure all string values are properly quoted. Ensure latitudes and longitudes for alternatives are strings representing floats.
|
210 |
+
- The safety_score should reflect a comprehensive evaluation of all risks.
|
211 |
+
- proper spacing and upo down margins shoudl be there
|
212 |
+
"""
|
213 |
+
return call_gemini_api(prompt)
|
214 |
+
|
215 |
+
|
216 |
+
# --- Flask Routes ---
|
217 |
+
@app.route('/')
|
218 |
+
def index():
|
219 |
+
return render_template('index.html')
|
220 |
+
|
221 |
+
|
222 |
+
@app.route('/api/geocode', methods=['POST'])
|
223 |
+
def geocode_location():
|
224 |
+
data = request.json
|
225 |
+
query = data.get('query')
|
226 |
+
if not query:
|
227 |
+
return jsonify({"error": "Query parameter is required."}), 400
|
228 |
+
|
229 |
+
coords_data = get_coords_from_location_name(query)
|
230 |
+
if coords_data:
|
231 |
+
return jsonify(coords_data), 200
|
232 |
+
else:
|
233 |
+
return jsonify({"error": "Location not found or geocoding failed."}), 404
|
234 |
+
|
235 |
+
|
236 |
+
@app.route('/api/analyze_location', methods=['POST'])
|
237 |
+
def analyze_location_route():
|
238 |
+
data = request.json
|
239 |
+
try:
|
240 |
+
latitude = float(data.get('latitude'))
|
241 |
+
longitude = float(data.get('longitude'))
|
242 |
+
except (TypeError, ValueError):
|
243 |
+
return jsonify({"error": "Invalid latitude or longitude provided."}), 400
|
244 |
+
|
245 |
+
# Get a human-readable name for the primary location
|
246 |
+
primary_location_address = get_location_name_from_coords(latitude, longitude)
|
247 |
+
|
248 |
+
# Call Gemini for the main analysis
|
249 |
+
analysis_data, error = get_safety_analysis_with_gemini(latitude, longitude, primary_location_address)
|
250 |
+
|
251 |
+
if error:
|
252 |
+
# Fallback to a simpler mock-like structure if Gemini fails critically
|
253 |
+
return jsonify({
|
254 |
+
"error_message": error,
|
255 |
+
"location_name": primary_location_address,
|
256 |
+
"safety_score": random.randint(20, 50), # Low score to indicate issue
|
257 |
+
"summary_assessment": f"Could not perform detailed analysis due to: {error}. Basic location: {primary_location_address}.",
|
258 |
+
"suitability_statement": "Analysis Incomplete",
|
259 |
+
"geological_risks": {"earthquake_risk": {"level": "Not Assessed", "details": error}},
|
260 |
+
"hydrological_risks": {"flood_risk": {"level": "Not Assessed", "details": error}},
|
261 |
+
"disclaimer": "A technical issue prevented full analysis."
|
262 |
+
}), 500
|
263 |
+
|
264 |
+
# Post-process alternative locations to get their names
|
265 |
+
if analysis_data.get("alternative_locations_analysis"):
|
266 |
+
for alt_loc in analysis_data["alternative_locations_analysis"]:
|
267 |
+
try:
|
268 |
+
alt_lat = float(alt_loc.get("latitude"))
|
269 |
+
alt_lon = float(alt_loc.get("longitude"))
|
270 |
+
alt_loc["actual_name"] = get_location_name_from_coords(alt_lat, alt_lon)
|
271 |
+
# Ensure distance is calculated if not provided by Gemini or if it's nonsensical
|
272 |
+
if not alt_loc.get("estimated_distance_km") or float(alt_loc.get("estimated_distance_km", 0)) == 0:
|
273 |
+
alt_loc["estimated_distance_km"] = str(
|
274 |
+
calculate_haversine_distance(latitude, longitude, alt_lat, alt_lon))
|
275 |
+
|
276 |
+
except (TypeError, ValueError, KeyError) as e:
|
277 |
+
print(f"Error processing alternative location coords: {e}, data: {alt_loc}")
|
278 |
+
alt_loc["actual_name"] = "Nearby Area (details unavailable)"
|
279 |
+
# Fallback if lat/lon are missing or invalid for an alternative
|
280 |
+
if "latitude" not in alt_loc or "longitude" not in alt_loc:
|
281 |
+
alt_loc["latitude"] = str(latitude + (random.random() - 0.5) * 0.05) # Small offset
|
282 |
+
alt_loc["longitude"] = str(longitude + (random.random() - 0.5) * 0.05)
|
283 |
+
|
284 |
+
return jsonify(analysis_data), 200
|
285 |
+
|
286 |
+
|
287 |
+
def calculate_haversine_distance(lat1, lon1, lat2, lon2):
|
288 |
+
R = 6371 # Radius of Earth in kilometers
|
289 |
+
try:
|
290 |
+
lat1_rad, lon1_rad = math.radians(float(lat1)), math.radians(float(lon1))
|
291 |
+
lat2_rad, lon2_rad = math.radians(float(lat2)), math.radians(float(lon2))
|
292 |
+
except (ValueError, TypeError):
|
293 |
+
print(f"Error converting lat/lon to float for Haversine: {lat1}, {lon1}, {lat2}, {lon2}")
|
294 |
+
return 0.0 # Fallback
|
295 |
+
dlon = lon2_rad - lon1_rad
|
296 |
+
dlat = lat2_rad - lat1_rad
|
297 |
+
a = math.sin(dlat / 2) ** 2 + math.cos(lat1_rad) * math.cos(lat2_rad) * math.sin(dlon / 2) ** 2
|
298 |
+
c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
|
299 |
+
distance = R * c
|
300 |
+
return round(distance, 1)
|
301 |
+
|
302 |
+
|
303 |
+
if __name__ == '__main__':
|
304 |
+
if not GEMINI_API_KEY:
|
305 |
+
print("CRITICAL ERROR: GEMINI_API_KEY is not set in the environment.")
|
306 |
+
print("Please create a .env file with GEMINI_API_KEY=YOUR_API_KEY or set it as an environment variable.")
|
307 |
app.run(debug=True, port=5000)
|