commit 30f0e834ff7c95d89f335b3ab21436f2bba13c9d
parent b007a5c230fa5c4b3407d7e4a7e66888e76ce15e
Author: Debarghya Das <debarghya1108@gmail.com>
Date: Sat, 28 Feb 2026 20:23:36 +0000
Merge PR
Diffstat:
4 files changed, 178 insertions(+), 90 deletions(-)
diff --git a/backend/server/create-index.py b/backend/server/create-index.py
@@ -0,0 +1,48 @@
+"""
+Script to create payload indexes for filtering in Qdrant.
+Run this ONCE to enable filtering by 'crop' and 'stage'.
+"""
+
+import sys
+import os
+
+# Add project root to path
+current_dir = os.path.dirname(os.path.abspath(__file__))
+project_root = os.path.abspath(os.path.join(current_dir, '../../'))
+sys.path.append(project_root)
+
+from Qdrant.Client import client
+from Qdrant.Store import COLLECTION_NAME
+from qdrant_client.http import models
+
+def create_indexes():
+ """Create keyword indexes for crop and stage fields"""
+
+ print(f"š Creating payload indexes for collection: {COLLECTION_NAME}")
+
+ try:
+ # Create index for 'crop' field
+ client.create_payload_index(
+ collection_name=COLLECTION_NAME,
+ field_name="crop",
+ field_schema=models.PayloadSchemaType.KEYWORD
+ )
+ print("ā
Index created for 'crop' field")
+
+ # Create index for 'stage' field
+ client.create_payload_index(
+ collection_name=COLLECTION_NAME,
+ field_name="stage",
+ field_schema=models.PayloadSchemaType.KEYWORD
+ )
+ print("ā
Index created for 'stage' field")
+
+ print("\nš All indexes created successfully!")
+ print("You can now use filtered searches in your /search endpoint.")
+
+ except Exception as e:
+ print(f"ā Error creating indexes: {e}")
+ print("\nNote: If indexes already exist, this is normal.")
+
+if __name__ == "__main__":
+ create_indexes()
+\ No newline at end of file
diff --git a/backend/server/functions.py b/backend/server/functions.py
@@ -0,0 +1,110 @@
+import os
+import shutil
+import json
+from fastapi import UploadFile
+from Qdrant.Store import store_fmu, COLLECTION_NAME
+from Qdrant.Client import client
+from qdrant_client.http import models
+
+async def process_ingest(file: UploadFile, sensors_str: str, metadata_str: str, builder):
+ """
+ Handles file saving, FMU creation, and storage logic.
+ """
+ # 1. Save Image Temporarily
+ temp_filename = f"temp_{file.filename}"
+ with open(temp_filename, "wb") as buffer:
+ shutil.copyfileobj(file.file, buffer)
+
+ try:
+ # 2. Parse Data
+ sensor_data = json.loads(sensors_str)
+ meta_data = json.loads(metadata_str)
+
+ # 3. Create FMU
+ abs_image_path = os.path.abspath(temp_filename)
+ fmu = builder.create_fmu(abs_image_path, sensor_data, meta_data)
+
+ # 4. Store in Cloud
+ store_fmu(fmu)
+ return {"status": "success", "fmu_id": fmu.id}
+
+ finally:
+ if os.path.exists(temp_filename):
+ os.remove(temp_filename)
+
+async def process_search(file: UploadFile, sensors_str: str, builder):
+ """
+ Handles image processing, context extraction, and filtered Qdrant search.
+ """
+ temp_filename = f"temp_search_{file.filename}"
+ with open(temp_filename, "wb") as buffer:
+ shutil.copyfileobj(file.file, buffer)
+
+ try:
+ sensor_data = json.loads(sensors_str)
+ abs_image_path = os.path.abspath(temp_filename)
+
+ # --- STEP 1: Context Extraction ---
+ target_crop = sensor_data.get("crop")
+ target_stage = sensor_data.get("stage")
+
+ if not target_crop or not target_stage:
+ return {"status": "error", "message": f"Missing crop/stage in: {sensor_data}"}
+
+ print(f"š Context: Searching for {target_crop} ({target_stage})...")
+
+ # --- STEP 2: Separate Numeric Data vs Metadata ---
+ numeric_sensors = {
+ "pH": sensor_data.get("pH"),
+ "EC": sensor_data.get("EC"),
+ "temp": sensor_data.get("temp"),
+ "humidity": sensor_data.get("humidity")
+ }
+ metadata = {"crop": target_crop, "stage": target_stage}
+
+ # --- STEP 3: Create Filter ---
+ context_filter = models.Filter(
+ must=[
+ models.FieldCondition(key="crop", match=models.MatchValue(value=target_crop)),
+ models.FieldCondition(key="stage", match=models.MatchValue(value=target_stage))
+ ]
+ )
+
+ # --- STEP 4: Generate Vector ---
+ query_fmu = builder.create_fmu(abs_image_path, numeric_sensors, metadata=metadata)
+ query_vector = query_fmu.vector.tolist() if hasattr(query_fmu.vector, 'tolist') else query_fmu.vector
+
+ # --- STEP 5: Search ---
+ try:
+ response = client.query_points(
+ collection_name=COLLECTION_NAME,
+ query=query_vector,
+ query_filter=context_filter,
+ limit=5,
+ with_payload=True
+ )
+ hits = response.points
+ except Exception as filter_error:
+ # Fallback for missing indexes
+ if "Index required" in str(filter_error):
+ print("ā ļø Index missing. Falling back to unfiltered search.")
+ response = client.search(
+ collection_name=COLLECTION_NAME,
+ query_vector=query_vector,
+ limit=5,
+ with_payload=True
+ )
+ hits = response
+ else:
+ raise filter_error
+
+ # Format Results
+ results = [
+ {"id": hit.id, "score": hit.score, "payload": hit.payload}
+ for hit in hits
+ ]
+ return {"results": results}
+
+ finally:
+ if os.path.exists(temp_filename):
+ os.remove(temp_filename)
+\ No newline at end of file
diff --git a/backend/server/main.py b/backend/server/main.py
@@ -1,25 +1,21 @@
import sys
import os
-# --- PATH FIX: Add project root to system path ---
+# --- PATH FIX ---
current_dir = os.path.dirname(os.path.abspath(__file__))
project_root = os.path.abspath(os.path.join(current_dir, '../../'))
sys.path.append(project_root)
-# -------------------------------------------------
+# ----------------
-import shutil
-import json
from fastapi import FastAPI, UploadFile, File, Form
from fastapi.middleware.cors import CORSMiddleware
-
from Sentinel.agent import FMUBuilder
-from Qdrant.Store import store_fmu, COLLECTION_NAME # Import name to avoid typos
-from Qdrant.Client import client # This imports your CLOUD connection
-from Sentinel.Encoders.Vision import VisionEncoder
+
+# Import the new logic functions
+from backend.server.functions import process_ingest, process_search
app = FastAPI()
-# Allow connection from Next.js (port 3000)
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:3000"],
@@ -28,107 +24,37 @@ app.add_middleware(
allow_headers=["*"],
)
-# Initialize the Builder
+# Initialize Agents Once
print("š± Initializing Demeter Agents...")
builder = FMUBuilder()
print("ā
Agents Ready.")
@app.post("/ingest")
-async def ingest_fmu(
+async def ingest_endpoint(
file: UploadFile = File(...),
sensors: str = Form(...),
metadata: str = Form(...)
):
- """
- Endpoint to receive raw data, convert to FMU, and store in Qdrant.
- """
- print(f"š” Ingest Request Received: {file.filename}")
-
- temp_filename = f"temp_{file.filename}"
- with open(temp_filename, "wb") as buffer:
- shutil.copyfileobj(file.file, buffer)
-
try:
- # 2. Parse JSON data from frontend
- sensor_data = json.loads(sensors)
- meta_data = json.loads(metadata)
-
- # 3. Create FMU (Uses Sentinel Logic)
- abs_image_path = os.path.abspath(temp_filename)
- fmu = builder.create_fmu(abs_image_path, sensor_data, meta_data)
-
- # 4. Store in Qdrant (Uses Memory Logic)
- store_fmu(fmu)
-
- return {"status": "success", "fmu_id": fmu.id}
-
+ # Pass the builder instance to the route handler
+ return await process_ingest(file, sensors, metadata, builder)
except Exception as e:
- print(f"ā Error: {e}")
+ print(f"ā Ingest Error: {e}")
return {"status": "error", "message": str(e)}
- finally:
- # 5. Cleanup
- if os.path.exists(temp_filename):
- os.remove(temp_filename)
-
@app.post("/search")
-async def search_fmu(
+async def search_endpoint(
file: UploadFile = File(...),
sensors: str = Form(...)
):
- """
- Semantic Search:
- 1. Receives Image + Sensor Data
- 2. Uses FMUBuilder to create a 'Transient FMU' (Query Object)
- 3. Uses that FMU's vector to find neighbors in Qdrant
- """
- temp_filename = f"temp_search_{file.filename}"
- with open(temp_filename, "wb") as buffer:
- shutil.copyfileobj(file.file, buffer)
-
try:
- sensor_data = json.loads(sensors)
- abs_image_path = os.path.abspath(temp_filename)
-
- # 1. Create a "Query FMU"
- # We don't care about metadata for the query, just the vector
- query_fmu = builder.create_fmu(abs_image_path, sensor_data, metadata={})
-
- # 2. Search Qdrant using the FMU's vector
- print(f"š Searching Cloud for matches...")
-
- # Convert numpy array to list if needed
- query_vector = query_fmu.vector.tolist() if hasattr(query_fmu.vector, 'tolist') else query_fmu.vector
-
- response = client.query_points(
- collection_name="Farm_Memory",
- query=query_vector,
- limit=5,
- with_payload=True
- )
-
- # Extract the points list from QueryResponse object
- hits = response.points
-
- # 3. Format results
- results = []
- for hit in hits:
- results.append({
- "id": hit.id,
- "score": hit.score,
- "payload": hit.payload
- })
-
- return {"results": results}
-
+ return await process_search(file, sensors, builder)
except Exception as e:
- print(f"Search Error: {e}")
+ print(f"ā Search Error: {e}")
+ import traceback
+ traceback.print_exc()
return {"status": "error", "message": str(e)}
- finally:
- if os.path.exists(temp_filename):
- os.remove(temp_filename)
-
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
\ No newline at end of file
diff --git a/web/services/api.ts b/web/services/api.ts
@@ -39,7 +39,9 @@ export const IngestService = {
pH: parseFloat(sensors.pH),
EC: parseFloat(sensors.EC),
temp: parseFloat(sensors.temp),
- humidity: parseFloat(sensors.humidity)
+ humidity: parseFloat(sensors.humidity),
+ crop: sensors.crop, // <--- Must include this
+ stage: sensors.stage,
}));
try {