Update app.py
Browse files
app.py
CHANGED
|
@@ -485,82 +485,104 @@ def compute_gsd_metrics(diams: List[float], passing: List[float]) -> Dict[str, f
|
|
| 485 |
if "sites" not in st.session_state:
|
| 486 |
# initialize with a default site
|
| 487 |
st.session_state["sites"] = [{
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
|
| 496 |
-
|
| 497 |
-
|
| 498 |
-
|
| 499 |
-
|
| 500 |
-
|
| 501 |
-
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
|
| 505 |
-
|
| 506 |
-
|
| 507 |
-
|
| 508 |
-
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
|
| 524 |
-
|
| 525 |
-
|
| 526 |
-
|
| 527 |
-
|
| 528 |
-
|
| 529 |
-
|
| 530 |
-
|
| 531 |
-
|
| 532 |
-
|
| 533 |
-
|
| 534 |
-
|
| 535 |
-
|
| 536 |
-
|
| 537 |
-
|
| 538 |
-
|
| 539 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 540 |
"Rainfall": None,
|
| 541 |
"Temperature": None,
|
| 542 |
"Humidity": None
|
| 543 |
-
|
| 544 |
-
|
| 545 |
"AerosolOpticalDepth": None,
|
| 546 |
"NO2": None,
|
| 547 |
-
"CO": None
|
| 548 |
-
|
| 549 |
-
|
| 550 |
-
|
| 551 |
-
|
| 552 |
-
|
| 553 |
-
|
| 554 |
-
|
| 555 |
-
|
| 556 |
-
|
| 557 |
-
|
| 558 |
-
|
| 559 |
-
|
| 560 |
-
|
| 561 |
-
|
| 562 |
-
|
| 563 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 564 |
|
| 565 |
if "active_site" not in st.session_state:
|
| 566 |
st.session_state["active_site"] = 0
|
|
@@ -568,23 +590,59 @@ if "active_site" not in st.session_state:
|
|
| 568 |
if "llm_model" not in st.session_state:
|
| 569 |
st.session_state["llm_model"] = "groq/compound"
|
| 570 |
|
| 571 |
-
#
|
|
|
|
|
|
|
| 572 |
GROQ_API_KEY = os.environ.get("GROQ_API_KEY")
|
| 573 |
-
|
| 574 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 575 |
try:
|
| 576 |
-
|
| 577 |
-
model_name
|
| 578 |
-
|
| 579 |
-
|
| 580 |
-
|
| 581 |
-
|
| 582 |
-
|
| 583 |
-
|
| 584 |
-
|
| 585 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 586 |
except Exception as e:
|
| 587 |
-
return f"[LLM error
|
| 588 |
|
| 589 |
# 5) UI helper: nice CSS for chat bubbles & page styling
|
| 590 |
st.markdown("""
|
|
@@ -592,11 +650,9 @@ st.markdown("""
|
|
| 592 |
/* Background and card styling */
|
| 593 |
body { background: #0b0b0b; color: #e9eef6; }
|
| 594 |
.stApp > .main > .block-container { padding-top: 18px; }
|
| 595 |
-
|
| 596 |
/* Landing and cards */
|
| 597 |
.gm-card { background: linear-gradient(180deg, rgba(255,122,0,0.04), rgba(255,122,0,0.02)); border-radius:12px; padding:14px; border:1px solid rgba(255,122,0,0.06);}
|
| 598 |
.gm-cta { background: linear-gradient(90deg,#ff7a00,#ff3a3a); color:white; padding:10px 14px; border-radius:10px; font-weight:700; }
|
| 599 |
-
|
| 600 |
/* Chat bubbles */
|
| 601 |
.chat-bot { background: #0f1720; border-left:4px solid #FF7A00; padding:10px 12px; border-radius:12px; margin:6px 0; color:#e9eef6; }
|
| 602 |
.chat-user { background: #1a1f27; padding:10px 12px; border-radius:12px; margin:6px 0; color:#cfe6ff; text-align:right;}
|
|
@@ -615,7 +671,10 @@ with st.sidebar:
|
|
| 615 |
"llama-3.1-8b-instant",
|
| 616 |
"meta-llama/llama-guard-4-12b",
|
| 617 |
"llama-3.3-70b-versatile",
|
| 618 |
-
"groq/compound"
|
|
|
|
|
|
|
|
|
|
| 619 |
], index=0)
|
| 620 |
st.markdown("---")
|
| 621 |
|
|
@@ -636,78 +695,97 @@ with st.sidebar:
|
|
| 636 |
"Site Name": new_site_name.strip(),
|
| 637 |
"Project Name": "Project - " + new_site_name.strip(),
|
| 638 |
"Site ID": idx,
|
| 639 |
-
"Soil Class": None,
|
| 640 |
-
"Soil Recognizer Confidence": None,
|
| 641 |
"Coordinates": "",
|
| 642 |
"lat": None,
|
| 643 |
"lon": None,
|
| 644 |
"Project Description": "",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 645 |
# ---------------------------
|
| 646 |
# Site Characterization
|
| 647 |
# ---------------------------
|
| 648 |
-
"Topography": None,
|
| 649 |
-
"Drainage": None,
|
| 650 |
-
"Current Land Use": None,
|
| 651 |
-
"Regional Geology": None,
|
|
|
|
| 652 |
# ---------------------------
|
| 653 |
# Investigations & Lab
|
| 654 |
# ---------------------------
|
| 655 |
-
"Field Investigation": [],
|
| 656 |
-
"Laboratory Results": [],
|
| 657 |
-
|
| 658 |
-
"USCS": None,
|
| 659 |
-
"AASHTO": None,
|
| 660 |
-
"GI": None,
|
| 661 |
# ---------------------------
|
| 662 |
-
# Geotechnical Parameters
|
| 663 |
# ---------------------------
|
| 664 |
-
"Load Bearing Capacity": None,
|
| 665 |
-
"Skin Shear Strength": None,
|
| 666 |
-
"Relative Compaction": None,
|
| 667 |
-
"Rate of Consolidation": None,
|
| 668 |
-
"Nature of Construction": None,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 669 |
# ---------------------------
|
| 670 |
# Earth Engine Data
|
| 671 |
# ---------------------------
|
| 672 |
-
"Soil Profile": {
|
| 673 |
-
|
| 674 |
-
|
| 675 |
-
|
| 676 |
-
|
| 677 |
-
|
| 678 |
},
|
| 679 |
-
"Topo Data": None,
|
| 680 |
-
"Seismic Data": None,
|
| 681 |
-
"Flood Data": None,
|
| 682 |
-
"Environmental Data": {
|
| 683 |
-
|
| 684 |
-
|
| 685 |
-
|
| 686 |
},
|
| 687 |
-
"Weather Data": {
|
| 688 |
-
|
| 689 |
-
|
| 690 |
-
|
| 691 |
},
|
| 692 |
-
"Atmospheric Data": {
|
| 693 |
-
|
| 694 |
-
|
| 695 |
-
|
|
|
|
| 696 |
},
|
|
|
|
| 697 |
# ---------------------------
|
| 698 |
# Map & Visualization
|
| 699 |
# ---------------------------
|
| 700 |
-
"
|
|
|
|
|
|
|
|
|
|
| 701 |
# ---------------------------
|
| 702 |
# AI / Reporting
|
| 703 |
# ---------------------------
|
| 704 |
"chat_history": [],
|
| 705 |
-
"
|
| 706 |
-
"classifier_decision": None,
|
| 707 |
"report_convo_state": 0,
|
| 708 |
"report_missing_fields": [],
|
| 709 |
"report_answers": {}
|
| 710 |
-
|
| 711 |
|
| 712 |
|
| 713 |
st.success(f"Site '{new_site_name.strip()}' created.")
|
|
@@ -1169,6 +1247,12 @@ def locator_page():
|
|
| 1169 |
|
| 1170 |
# --- Map setup
|
| 1171 |
m = geemap.Map(center=[28.0, 72.0], zoom=5, plugin_Draw=True, draw_export=True, locate_control=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1172 |
|
| 1173 |
# Restore ROI (if available)
|
| 1174 |
if "roi_geojson" in st.session_state:
|
|
@@ -1340,31 +1424,65 @@ def locator_page():
|
|
| 1340 |
st.session_state["sites"][active]["map_snapshot"] = map_bytes
|
| 1341 |
st.image(map_bytes, caption="Map Snapshot", use_column_width=True)
|
| 1342 |
|
| 1343 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1344 |
st.subheader("π Summary")
|
|
|
|
| 1345 |
st.write(f"**Soil:** {soil_val}")
|
| 1346 |
-
st.write(f"**Elevation:** {elev_val}")
|
| 1347 |
st.write(f"**Seismic:** {seismic_val}")
|
| 1348 |
st.write(f"**Flood:** {flood_val}")
|
| 1349 |
st.write(f"**PM2.5 Index:** {pm25_val}")
|
| 1350 |
-
|
| 1351 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1352 |
if ndvi_ts:
|
|
|
|
| 1353 |
d, v = zip(*ndvi_ts)
|
| 1354 |
fig, ax = plt.subplots()
|
| 1355 |
-
ax.plot(d, v, marker="o"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1356 |
st.pyplot(fig)
|
| 1357 |
-
|
|
|
|
| 1358 |
if precip_ts:
|
|
|
|
| 1359 |
d, v = zip(*precip_ts)
|
| 1360 |
fig, ax = plt.subplots()
|
| 1361 |
-
ax.plot(d, v, marker="o", color=
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1362 |
st.pyplot(fig)
|
| 1363 |
-
|
|
|
|
| 1364 |
if temp_ts:
|
|
|
|
| 1365 |
d, v = zip(*temp_ts)
|
| 1366 |
fig, ax = plt.subplots()
|
| 1367 |
-
ax.plot(d, v, marker="o", color=
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1368 |
st.pyplot(fig)
|
| 1369 |
|
| 1370 |
|
|
@@ -1391,12 +1509,11 @@ def load_faiss():
|
|
| 1391 |
|
| 1392 |
vectorstore = load_faiss()
|
| 1393 |
|
| 1394 |
-
|
| 1395 |
# -------------------
|
| 1396 |
# RAG Chat Page
|
| 1397 |
# -------------------
|
| 1398 |
def rag_page():
|
| 1399 |
-
st.header("π€ GeoMate Ask (RAG +
|
| 1400 |
site = st.session_state["sites"][st.session_state["active_site"]]
|
| 1401 |
|
| 1402 |
# --- Ensure Site ID exists ---
|
|
@@ -1466,8 +1583,8 @@ def rag_page():
|
|
| 1466 |
f"If user provides numeric engineering values, return them in the format: [[FIELD: value unit]]."
|
| 1467 |
)
|
| 1468 |
|
| 1469 |
-
# Call
|
| 1470 |
-
resp =
|
| 1471 |
|
| 1472 |
# Save bot reply
|
| 1473 |
st.session_state["rag_history"][site_id].append({"who": "bot", "text": resp})
|
|
@@ -1544,45 +1661,69 @@ from reportlab.lib.units import mm
|
|
| 1544 |
|
| 1545 |
|
| 1546 |
# ----------------------------
|
| 1547 |
-
# LLM Helper (
|
| 1548 |
# ----------------------------
|
| 1549 |
-
def groq_llm_analyze(prompt: str, section_title: str,
|
| 1550 |
-
model_name: str = "deepseek-r1-distill-llama-70b",
|
| 1551 |
-
max_tokens: int = 500) -> str:
|
| 1552 |
"""
|
| 1553 |
-
Query
|
| 1554 |
-
|
|
|
|
| 1555 |
"""
|
| 1556 |
-
import requests
|
| 1557 |
|
| 1558 |
-
|
| 1559 |
-
|
| 1560 |
-
|
| 1561 |
-
|
| 1562 |
-
|
| 1563 |
-
|
| 1564 |
-
if not
|
| 1565 |
-
|
| 1566 |
-
|
| 1567 |
-
|
| 1568 |
-
|
| 1569 |
-
|
| 1570 |
-
|
| 1571 |
-
"
|
| 1572 |
-
|
| 1573 |
-
|
| 1574 |
-
|
| 1575 |
-
|
| 1576 |
-
"
|
| 1577 |
-
"
|
| 1578 |
-
|
| 1579 |
-
|
| 1580 |
-
|
| 1581 |
-
|
| 1582 |
-
|
| 1583 |
-
|
| 1584 |
-
|
| 1585 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1586 |
|
| 1587 |
|
| 1588 |
# =============================
|
|
@@ -1726,10 +1867,6 @@ def build_classification_pdf(
|
|
| 1726 |
doc.build(elems)
|
| 1727 |
return filename
|
| 1728 |
|
| 1729 |
-
|
| 1730 |
-
# -------------------------------
|
| 1731 |
-
# Reports Page
|
| 1732 |
-
# -------------------------------
|
| 1733 |
# -------------------------------
|
| 1734 |
# Reports Page
|
| 1735 |
# -------------------------------
|
|
|
|
| 485 |
if "sites" not in st.session_state:
|
| 486 |
# initialize with a default site
|
| 487 |
st.session_state["sites"] = [{
|
| 488 |
+
# ---------------------------
|
| 489 |
+
# Basic Project Info
|
| 490 |
+
# ---------------------------
|
| 491 |
+
"Site Name": None,
|
| 492 |
+
"Project Name": None,
|
| 493 |
+
"Site ID": None,
|
| 494 |
+
"Coordinates": "",
|
| 495 |
+
"lat": None,
|
| 496 |
+
"lon": None,
|
| 497 |
+
"Project Description": "",
|
| 498 |
+
|
| 499 |
+
# ---------------------------
|
| 500 |
+
# Soil Recognition / Classifier
|
| 501 |
+
# ---------------------------
|
| 502 |
+
"Soil Class": None,
|
| 503 |
+
"Soil Recognizer Confidence": None,
|
| 504 |
+
"USCS": None,
|
| 505 |
+
"AASHTO": None,
|
| 506 |
+
"GI": None,
|
| 507 |
+
"GSD": None, # Grain size distribution metrics
|
| 508 |
+
"classifier_inputs": {}, # User/lab/OCR extracted inputs
|
| 509 |
+
"classifier_decision": None, # Full text decision from classifier
|
| 510 |
+
|
| 511 |
+
# ---------------------------
|
| 512 |
+
# Site Characterization
|
| 513 |
+
# ---------------------------
|
| 514 |
+
"Topography": None, # manual topo entry
|
| 515 |
+
"Drainage": None, # manual drainage notes
|
| 516 |
+
"Current Land Use": None, # linked to environmental data
|
| 517 |
+
"Regional Geology": None, # manual geology notes
|
| 518 |
+
|
| 519 |
+
# ---------------------------
|
| 520 |
+
# Investigations & Lab
|
| 521 |
+
# ---------------------------
|
| 522 |
+
"Field Investigation": [], # e.g. borehole logs
|
| 523 |
+
"Laboratory Results": [], # lab test results
|
| 524 |
+
|
| 525 |
+
# ---------------------------
|
| 526 |
+
# Geotechnical Parameters (incl. REPORT_FIELDS)
|
| 527 |
+
# ---------------------------
|
| 528 |
+
"Load Bearing Capacity": None, # kPa or psf
|
| 529 |
+
"Skin Shear Strength": None, # kPa
|
| 530 |
+
"Relative Compaction": None, # %
|
| 531 |
+
"Rate of Consolidation": None, # mm/yr or days
|
| 532 |
+
"Nature of Construction": None, # text
|
| 533 |
+
"Borehole Count": None, # number
|
| 534 |
+
"Max Depth (m)": None, # m
|
| 535 |
+
"SPT N (avg)": None, # blows/ft
|
| 536 |
+
"CBR (%)": None, # %
|
| 537 |
+
"Allowable Bearing (kPa)": None, # kPa
|
| 538 |
+
|
| 539 |
+
# ---------------------------
|
| 540 |
+
# Earth Engine Data
|
| 541 |
+
# ---------------------------
|
| 542 |
+
"Soil Profile": { # SoilGrids / OpenLandMap
|
| 543 |
+
"Clay": None, # % clay
|
| 544 |
+
"Sand": None, # % sand
|
| 545 |
+
"Silt": None, # % silt
|
| 546 |
+
"OrganicCarbon": None, # % organic carbon
|
| 547 |
+
"pH": None # soil pH
|
| 548 |
+
},
|
| 549 |
+
"Topo Data": None, # Avg elevation
|
| 550 |
+
"Seismic Data": None, # PGA/g
|
| 551 |
+
"Flood Data": None, # % flood occurrence
|
| 552 |
+
"Environmental Data": {
|
| 553 |
+
"Landcover Stats": None, # histogram
|
| 554 |
+
"Forest Loss": None, # optional (Hansen)
|
| 555 |
+
"Urban Fraction": None # optional calc
|
| 556 |
+
},
|
| 557 |
+
"Weather Data": { # climate summaries
|
| 558 |
"Rainfall": None,
|
| 559 |
"Temperature": None,
|
| 560 |
"Humidity": None
|
| 561 |
+
},
|
| 562 |
+
"Atmospheric Data": { # pollution, aerosols
|
| 563 |
"AerosolOpticalDepth": None,
|
| 564 |
"NO2": None,
|
| 565 |
+
"CO": None,
|
| 566 |
+
"PM2.5": None # from Sentinel-5P aerosol index
|
| 567 |
+
},
|
| 568 |
+
|
| 569 |
+
# ---------------------------
|
| 570 |
+
# Map & Visualization
|
| 571 |
+
# ---------------------------
|
| 572 |
+
"ROI": None, # GeoJSON of ROI
|
| 573 |
+
"roi_coords": None, # Flattened coords (lat,lon)
|
| 574 |
+
"map_snapshot": None, # PNG snapshot
|
| 575 |
+
|
| 576 |
+
# ---------------------------
|
| 577 |
+
# AI / Reporting
|
| 578 |
+
# ---------------------------
|
| 579 |
+
"chat_history": [],
|
| 580 |
+
"LLM_Report_Text": None,
|
| 581 |
+
"report_convo_state": 0,
|
| 582 |
+
"report_missing_fields": [],
|
| 583 |
+
"report_answers": {}
|
| 584 |
+
}]
|
| 585 |
+
|
| 586 |
|
| 587 |
if "active_site" not in st.session_state:
|
| 588 |
st.session_state["active_site"] = 0
|
|
|
|
| 590 |
if "llm_model" not in st.session_state:
|
| 591 |
st.session_state["llm_model"] = "groq/compound"
|
| 592 |
|
| 593 |
+
# -------------------
|
| 594 |
+
# API Keys
|
| 595 |
+
# -------------------
|
| 596 |
GROQ_API_KEY = os.environ.get("GROQ_API_KEY")
|
| 597 |
+
GEM_API_KEY = os.environ.get("GEM_API")
|
| 598 |
+
if "GEM_API" in st.secrets:
|
| 599 |
+
GEM_API_KEY = st.secrets["GEM_API"]
|
| 600 |
+
|
| 601 |
+
# -------------------
|
| 602 |
+
# Universal LLM Call
|
| 603 |
+
# -------------------
|
| 604 |
+
def llm_generate(prompt: str, model: str = None, max_tokens: int = 512) -> str:
|
| 605 |
+
"""Universal LLM call for Groq, Gemini, DeepSeek."""
|
| 606 |
+
model_name = model or st.session_state["llm_model"]
|
| 607 |
+
|
| 608 |
try:
|
| 609 |
+
# ------------------- GEMINI -------------------
|
| 610 |
+
if model_name.lower().startswith("gemini"):
|
| 611 |
+
import requests
|
| 612 |
+
if not GEM_API_KEY:
|
| 613 |
+
return "[LLM error: No GEM_API key found in secrets]"
|
| 614 |
+
|
| 615 |
+
url = f"https://generativelanguage.googleapis.com/v1beta/models/{model_name}:generateContent?key={GEM_API_KEY}"
|
| 616 |
+
headers = {"Content-Type": "application/json"}
|
| 617 |
+
payload = {
|
| 618 |
+
"contents": [{"role": "user", "parts": [{"text": prompt}]}],
|
| 619 |
+
"generationConfig": {"temperature": 0.2, "maxOutputTokens": max_tokens}
|
| 620 |
+
}
|
| 621 |
+
resp = requests.post(url, headers=headers, json=payload, timeout=60)
|
| 622 |
+
resp.raise_for_status()
|
| 623 |
+
data = resp.json()
|
| 624 |
+
return data["candidates"][0]["content"]["parts"][0]["text"].strip()
|
| 625 |
+
|
| 626 |
+
# ------------------- GROQ / DEEPSEEK -------------------
|
| 627 |
+
elif "groq" in model_name or "llama" in model_name or "deepseek" in model_name:
|
| 628 |
+
from groq import Groq
|
| 629 |
+
if not GROQ_API_KEY:
|
| 630 |
+
return "[LLM error: No GROQ_API key found in secrets/env]"
|
| 631 |
+
|
| 632 |
+
client = Groq(api_key=GROQ_API_KEY)
|
| 633 |
+
completion = client.chat.completions.create(
|
| 634 |
+
model=model_name,
|
| 635 |
+
messages=[{"role": "user", "content": prompt}],
|
| 636 |
+
temperature=0.2,
|
| 637 |
+
max_tokens=max_tokens
|
| 638 |
+
)
|
| 639 |
+
return completion.choices[0].message.content
|
| 640 |
+
|
| 641 |
+
else:
|
| 642 |
+
return f"[LLM error: Unknown model {model_name}]"
|
| 643 |
+
|
| 644 |
except Exception as e:
|
| 645 |
+
return f"[LLM error: {e}]"
|
| 646 |
|
| 647 |
# 5) UI helper: nice CSS for chat bubbles & page styling
|
| 648 |
st.markdown("""
|
|
|
|
| 650 |
/* Background and card styling */
|
| 651 |
body { background: #0b0b0b; color: #e9eef6; }
|
| 652 |
.stApp > .main > .block-container { padding-top: 18px; }
|
|
|
|
| 653 |
/* Landing and cards */
|
| 654 |
.gm-card { background: linear-gradient(180deg, rgba(255,122,0,0.04), rgba(255,122,0,0.02)); border-radius:12px; padding:14px; border:1px solid rgba(255,122,0,0.06);}
|
| 655 |
.gm-cta { background: linear-gradient(90deg,#ff7a00,#ff3a3a); color:white; padding:10px 14px; border-radius:10px; font-weight:700; }
|
|
|
|
| 656 |
/* Chat bubbles */
|
| 657 |
.chat-bot { background: #0f1720; border-left:4px solid #FF7A00; padding:10px 12px; border-radius:12px; margin:6px 0; color:#e9eef6; }
|
| 658 |
.chat-user { background: #1a1f27; padding:10px 12px; border-radius:12px; margin:6px 0; color:#cfe6ff; text-align:right;}
|
|
|
|
| 671 |
"llama-3.1-8b-instant",
|
| 672 |
"meta-llama/llama-guard-4-12b",
|
| 673 |
"llama-3.3-70b-versatile",
|
| 674 |
+
"groq/compound",
|
| 675 |
+
"deepseek-r1-distill-llama-70b", # β
Added DeepSeek
|
| 676 |
+
"gemini-1.5-pro", # β
Added Gemini
|
| 677 |
+
"gemini-1.5-flash"
|
| 678 |
], index=0)
|
| 679 |
st.markdown("---")
|
| 680 |
|
|
|
|
| 695 |
"Site Name": new_site_name.strip(),
|
| 696 |
"Project Name": "Project - " + new_site_name.strip(),
|
| 697 |
"Site ID": idx,
|
|
|
|
|
|
|
| 698 |
"Coordinates": "",
|
| 699 |
"lat": None,
|
| 700 |
"lon": None,
|
| 701 |
"Project Description": "",
|
| 702 |
+
|
| 703 |
+
# ---------------------------
|
| 704 |
+
# Soil Recognition / Classifier
|
| 705 |
+
# ---------------------------
|
| 706 |
+
"Soil Class": None,
|
| 707 |
+
"Soil Recognizer Confidence": None,
|
| 708 |
+
"USCS": None,
|
| 709 |
+
"AASHTO": None,
|
| 710 |
+
"GI": None,
|
| 711 |
+
"GSD": None, # Grain size distribution metrics
|
| 712 |
+
"classifier_inputs": {}, # User/lab/OCR extracted inputs
|
| 713 |
+
"classifier_decision": None, # Full text decision from classifier
|
| 714 |
+
|
| 715 |
# ---------------------------
|
| 716 |
# Site Characterization
|
| 717 |
# ---------------------------
|
| 718 |
+
"Topography": None, # manual topo entry
|
| 719 |
+
"Drainage": None, # manual drainage notes
|
| 720 |
+
"Current Land Use": None, # linked to environmental data
|
| 721 |
+
"Regional Geology": None, # manual geology notes
|
| 722 |
+
|
| 723 |
# ---------------------------
|
| 724 |
# Investigations & Lab
|
| 725 |
# ---------------------------
|
| 726 |
+
"Field Investigation": [], # e.g. borehole logs
|
| 727 |
+
"Laboratory Results": [], # lab test results
|
| 728 |
+
|
|
|
|
|
|
|
|
|
|
| 729 |
# ---------------------------
|
| 730 |
+
# Geotechnical Parameters (incl. REPORT_FIELDS)
|
| 731 |
# ---------------------------
|
| 732 |
+
"Load Bearing Capacity": None, # kPa or psf
|
| 733 |
+
"Skin Shear Strength": None, # kPa
|
| 734 |
+
"Relative Compaction": None, # %
|
| 735 |
+
"Rate of Consolidation": None, # mm/yr or days
|
| 736 |
+
"Nature of Construction": None, # text
|
| 737 |
+
"Borehole Count": None, # number
|
| 738 |
+
"Max Depth (m)": None, # m
|
| 739 |
+
"SPT N (avg)": None, # blows/ft
|
| 740 |
+
"CBR (%)": None, # %
|
| 741 |
+
"Allowable Bearing (kPa)": None, # kPa
|
| 742 |
+
|
| 743 |
# ---------------------------
|
| 744 |
# Earth Engine Data
|
| 745 |
# ---------------------------
|
| 746 |
+
"Soil Profile": { # SoilGrids / OpenLandMap
|
| 747 |
+
"Clay": None, # % clay
|
| 748 |
+
"Sand": None, # % sand
|
| 749 |
+
"Silt": None, # % silt
|
| 750 |
+
"OrganicCarbon": None, # % organic carbon
|
| 751 |
+
"pH": None # soil pH
|
| 752 |
},
|
| 753 |
+
"Topo Data": None, # Avg elevation
|
| 754 |
+
"Seismic Data": None, # PGA/g
|
| 755 |
+
"Flood Data": None, # % flood occurrence
|
| 756 |
+
"Environmental Data": {
|
| 757 |
+
"Landcover Stats": None, # histogram
|
| 758 |
+
"Forest Loss": None, # optional (Hansen)
|
| 759 |
+
"Urban Fraction": None # optional calc
|
| 760 |
},
|
| 761 |
+
"Weather Data": { # climate summaries
|
| 762 |
+
"Rainfall": None,
|
| 763 |
+
"Temperature": None,
|
| 764 |
+
"Humidity": None
|
| 765 |
},
|
| 766 |
+
"Atmospheric Data": { # pollution, aerosols
|
| 767 |
+
"AerosolOpticalDepth": None,
|
| 768 |
+
"NO2": None,
|
| 769 |
+
"CO": None,
|
| 770 |
+
"PM2.5": None # from Sentinel-5P aerosol index
|
| 771 |
},
|
| 772 |
+
|
| 773 |
# ---------------------------
|
| 774 |
# Map & Visualization
|
| 775 |
# ---------------------------
|
| 776 |
+
"ROI": None, # GeoJSON of ROI
|
| 777 |
+
"roi_coords": None, # Flattened coords (lat,lon)
|
| 778 |
+
"map_snapshot": None, # PNG snapshot
|
| 779 |
+
|
| 780 |
# ---------------------------
|
| 781 |
# AI / Reporting
|
| 782 |
# ---------------------------
|
| 783 |
"chat_history": [],
|
| 784 |
+
"LLM_Report_Text": None,
|
|
|
|
| 785 |
"report_convo_state": 0,
|
| 786 |
"report_missing_fields": [],
|
| 787 |
"report_answers": {}
|
| 788 |
+
}]
|
| 789 |
|
| 790 |
|
| 791 |
st.success(f"Site '{new_site_name.strip()}' created.")
|
|
|
|
| 1247 |
|
| 1248 |
# --- Map setup
|
| 1249 |
m = geemap.Map(center=[28.0, 72.0], zoom=5, plugin_Draw=True, draw_export=True, locate_control=True)
|
| 1250 |
+
|
| 1251 |
+
# β
Add a basemap explicitly
|
| 1252 |
+
m.add_basemap("HYBRID") # Google Satellite Hybrid
|
| 1253 |
+
m.add_basemap("ROADMAP") # Google Roads
|
| 1254 |
+
m.add_basemap("Esri.WorldImagery")
|
| 1255 |
+
m.add_basemap("OpenStreetMap")
|
| 1256 |
|
| 1257 |
# Restore ROI (if available)
|
| 1258 |
if "roi_geojson" in st.session_state:
|
|
|
|
| 1424 |
st.session_state["sites"][active]["map_snapshot"] = map_bytes
|
| 1425 |
st.image(map_bytes, caption="Map Snapshot", use_column_width=True)
|
| 1426 |
|
| 1427 |
+
import matplotlib.pyplot as plt
|
| 1428 |
+
|
| 1429 |
+
# Custom theme colors (orange/black/gray)
|
| 1430 |
+
COLOR_SCHEME = ["#FF6600", "#333333", "#FF9933", "#666666", "#FFCC99"]
|
| 1431 |
+
|
| 1432 |
+
# --- Display results
|
| 1433 |
st.subheader("π Summary")
|
| 1434 |
+
|
| 1435 |
st.write(f"**Soil:** {soil_val}")
|
| 1436 |
+
st.write(f"**Elevation:** {elev_val} m")
|
| 1437 |
st.write(f"**Seismic:** {seismic_val}")
|
| 1438 |
st.write(f"**Flood:** {flood_val}")
|
| 1439 |
st.write(f"**PM2.5 Index:** {pm25_val}")
|
| 1440 |
+
|
| 1441 |
+
# --- Landcover pie chart
|
| 1442 |
+
if lc_stats:
|
| 1443 |
+
st.markdown("#### π± Landcover Distribution")
|
| 1444 |
+
labels = list(lc_stats.keys())
|
| 1445 |
+
values = list(lc_stats.values())
|
| 1446 |
+
fig, ax = plt.subplots()
|
| 1447 |
+
ax.pie(values, labels=labels, autopct="%1.1f%%", startangle=90,
|
| 1448 |
+
colors=COLOR_SCHEME, wedgeprops={'edgecolor': 'white'})
|
| 1449 |
+
ax.set_aspect("equal")
|
| 1450 |
+
st.pyplot(fig)
|
| 1451 |
+
|
| 1452 |
+
# --- NDVI time series
|
| 1453 |
if ndvi_ts:
|
| 1454 |
+
st.markdown("#### πΏ NDVI Trend (2 years)")
|
| 1455 |
d, v = zip(*ndvi_ts)
|
| 1456 |
fig, ax = plt.subplots()
|
| 1457 |
+
ax.plot(d, v, marker="o", color=COLOR_SCHEME[0])
|
| 1458 |
+
ax.set_title("NDVI", fontsize=12, color=COLOR_SCHEME[1])
|
| 1459 |
+
ax.set_xlabel("Date")
|
| 1460 |
+
ax.set_ylabel("NDVI")
|
| 1461 |
+
ax.grid(True, linestyle="--", alpha=0.6)
|
| 1462 |
st.pyplot(fig)
|
| 1463 |
+
|
| 1464 |
+
# --- Precipitation
|
| 1465 |
if precip_ts:
|
| 1466 |
+
st.markdown("#### π§οΈ Precipitation Trend (1 year)")
|
| 1467 |
d, v = zip(*precip_ts)
|
| 1468 |
fig, ax = plt.subplots()
|
| 1469 |
+
ax.plot(d, v, marker="o", color=COLOR_SCHEME[2])
|
| 1470 |
+
ax.set_title("Precipitation (mm)", fontsize=12, color=COLOR_SCHEME[1])
|
| 1471 |
+
ax.set_xlabel("Date")
|
| 1472 |
+
ax.set_ylabel("Rainfall (mm)")
|
| 1473 |
+
ax.grid(True, linestyle="--", alpha=0.6)
|
| 1474 |
st.pyplot(fig)
|
| 1475 |
+
|
| 1476 |
+
# --- Temperature
|
| 1477 |
if temp_ts:
|
| 1478 |
+
st.markdown("#### π‘οΈ Land Surface Temp (1 year)")
|
| 1479 |
d, v = zip(*temp_ts)
|
| 1480 |
fig, ax = plt.subplots()
|
| 1481 |
+
ax.plot(d, v, marker="o", color=COLOR_SCHEME[3])
|
| 1482 |
+
ax.set_title("LST Temperature (K)", fontsize=12, color=COLOR_SCHEME[1])
|
| 1483 |
+
ax.set_xlabel("Date")
|
| 1484 |
+
ax.set_ylabel("Kelvin (K)")
|
| 1485 |
+
ax.grid(True, linestyle="--", alpha=0.6)
|
| 1486 |
st.pyplot(fig)
|
| 1487 |
|
| 1488 |
|
|
|
|
| 1509 |
|
| 1510 |
vectorstore = load_faiss()
|
| 1511 |
|
|
|
|
| 1512 |
# -------------------
|
| 1513 |
# RAG Chat Page
|
| 1514 |
# -------------------
|
| 1515 |
def rag_page():
|
| 1516 |
+
st.header("π€ GeoMate Ask (RAG + LLM)")
|
| 1517 |
site = st.session_state["sites"][st.session_state["active_site"]]
|
| 1518 |
|
| 1519 |
# --- Ensure Site ID exists ---
|
|
|
|
| 1583 |
f"If user provides numeric engineering values, return them in the format: [[FIELD: value unit]]."
|
| 1584 |
)
|
| 1585 |
|
| 1586 |
+
# Call the unified LLM function
|
| 1587 |
+
resp = llm_generate(prompt, model=st.session_state["llm_model"], max_tokens=500)
|
| 1588 |
|
| 1589 |
# Save bot reply
|
| 1590 |
st.session_state["rag_history"][site_id].append({"who": "bot", "text": resp})
|
|
|
|
| 1661 |
|
| 1662 |
|
| 1663 |
# ----------------------------
|
| 1664 |
+
# LLM Helper (Reports Analysis with Orchestration)
|
| 1665 |
# ----------------------------
|
| 1666 |
+
def groq_llm_analyze(prompt: str, section_title: str, max_tokens: int = 500) -> str:
|
|
|
|
|
|
|
| 1667 |
"""
|
| 1668 |
+
Query the selected model (Groq / Gemini / DeepSeek).
|
| 1669 |
+
If token limit or error occurs, automatically switch to backup models
|
| 1670 |
+
and continue the analysis seamlessly.
|
| 1671 |
"""
|
|
|
|
| 1672 |
|
| 1673 |
+
# Primary model (user choice from sidebar)
|
| 1674 |
+
model_chain = [st.session_state.get("llm_model", "groq/compound")]
|
| 1675 |
+
|
| 1676 |
+
# Add fallback chain (priority order: DeepSeek β Gemini β Groq)
|
| 1677 |
+
if "deepseek" not in model_chain:
|
| 1678 |
+
model_chain.append("deepseek-r1-distill-llama-70b")
|
| 1679 |
+
if "gemini" not in model_chain:
|
| 1680 |
+
model_chain.append("gemini-1.5-pro")
|
| 1681 |
+
if "groq/compound" not in model_chain:
|
| 1682 |
+
model_chain.append("groq/compound")
|
| 1683 |
+
|
| 1684 |
+
system_message = (
|
| 1685 |
+
"You are GeoMate, a geotechnical engineering assistant. "
|
| 1686 |
+
"Respond professionally with concise analysis and insights."
|
| 1687 |
+
)
|
| 1688 |
+
|
| 1689 |
+
full_prompt = (
|
| 1690 |
+
f"{system_message}\n\n"
|
| 1691 |
+
f"Section: {section_title}\n\n"
|
| 1692 |
+
f"Input: {prompt}\n\n"
|
| 1693 |
+
f"Write a professional engineering analysis for this section."
|
| 1694 |
+
)
|
| 1695 |
+
|
| 1696 |
+
final_response = ""
|
| 1697 |
+
remaining_prompt = full_prompt
|
| 1698 |
+
|
| 1699 |
+
# Try each model in the chain until completion
|
| 1700 |
+
for model_name in model_chain:
|
| 1701 |
+
try:
|
| 1702 |
+
response = llm_generate(remaining_prompt, model=model_name, max_tokens=max_tokens)
|
| 1703 |
+
|
| 1704 |
+
if not response or "[LLM error" in response:
|
| 1705 |
+
# If failed, continue to next model
|
| 1706 |
+
continue
|
| 1707 |
+
|
| 1708 |
+
final_response += response.strip()
|
| 1709 |
+
|
| 1710 |
+
# If response length is close to max_tokens, assume it cut off β continue with next model
|
| 1711 |
+
if len(response.split()) >= (max_tokens - 20):
|
| 1712 |
+
# Add continuation instruction for the next model
|
| 1713 |
+
remaining_prompt = (
|
| 1714 |
+
f"Continue the analysis from where the last model stopped. "
|
| 1715 |
+
f"So far the draft is:\n\n{final_response}\n\n"
|
| 1716 |
+
f"Continue writing professionally without repeating."
|
| 1717 |
+
)
|
| 1718 |
+
continue
|
| 1719 |
+
else:
|
| 1720 |
+
break # Finished properly, exit loop
|
| 1721 |
+
|
| 1722 |
+
except Exception as e:
|
| 1723 |
+
final_response += f"\n[LLM orchestration error @ {model_name}: {e}]\n"
|
| 1724 |
+
continue
|
| 1725 |
+
|
| 1726 |
+
return final_response if final_response else "[LLM error: All models failed]"
|
| 1727 |
|
| 1728 |
|
| 1729 |
# =============================
|
|
|
|
| 1867 |
doc.build(elems)
|
| 1868 |
return filename
|
| 1869 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1870 |
# -------------------------------
|
| 1871 |
# Reports Page
|
| 1872 |
# -------------------------------
|