commit 6170813c84e62da966edaa0dafd0f26ff42a9bff
parent 310ecc90900a22d1e6abd476230bd82df5231a18
Author: Debarghya Das <debarghya1108@gmail.com>
Date: Mon, 2 Mar 2026 11:48:40 +0000
Merge PR
Diffstat:
4 files changed, 133 insertions(+), 19 deletions(-)
diff --git a/agent/Sentinel/Encoders/TimeSeries.py b/agent/Sentinel/Encoders/TimeSeries.py
@@ -1,24 +1,53 @@
import numpy as np
-class SensorEncoder: # <--- Renamed to match agent.py
+class SensorEncoder:
+ # š§ CONFIG: Define the maximum possible value for each sensor.
+ # We divide raw values by this to get a 0-1 range.
+ SCALERS = {
+ "pH": 14.0, # pH Scale is 0-14
+ "EC": 5.0, # EC rarely exceeds 3.0-4.0 in hydroponics
+ "temp": 50.0, # 50°C (122°F) is a safe max for plants
+ "humidity": 100.0 # 0-100%
+ }
+
def encode(self, sensor_data: dict) -> np.ndarray:
"""
- Encodes dictionary of sensor values/windows into a flat vector.
+ Encodes sensor data into a NORMALIZED vector for balanced search.
+
+ Example:
+ Input: {'humidity': 72.0}
+ Vector: [0.72] (Balanced for math)
+ Payload: {'humidity': 72.0} (Readable for humans)
"""
features = []
- # Sort keys to ensure vector consistency
+
+ # Sort keys to ensure vector consistency (EC, humidity, pH, temp)
for key in sorted(sensor_data.keys()):
- val = sensor_data[key]
+ raw_val = sensor_data[key]
- if isinstance(val, list) and val:
- # Handle window of data (Mean, Std, Last)
- arr = np.array(val)
- features.extend([float(np.mean(arr)), float(np.std(arr)), float(arr[-1])])
- elif isinstance(val, (int, float)):
- # Handle single value
- features.append(float(val))
+ # Determine the divisor (Default to 100.0 if unknown sensor)
+ max_val = self.SCALERS.get(key, 100.0)
+
+ if isinstance(raw_val, list) and raw_val:
+ # Handle Window (Mean, Std, Last)
+ arr = np.array(raw_val, dtype=float)
+
+ # Normalize each statistic
+ mean_norm = np.mean(arr) / max_val
+ std_norm = np.std(arr) / max_val
+ last_norm = arr[-1] / max_val
+
+ features.extend([mean_norm, std_norm, last_norm])
+
+ elif isinstance(raw_val, (int, float)):
+ # Handle Single Value
+ norm_val = float(raw_val) / max_val
+
+ # Clamp to ensure we never break the 0-1 scale (e.g. if temp is 55)
+ norm_val = max(0.0, min(1.0, norm_val))
+
+ features.append(norm_val)
else:
- # Fallback
features.append(0.0)
return np.array(features, dtype=np.float32)
\ No newline at end of file
diff --git a/agent/Sentinel/agent.py b/agent/Sentinel/agent.py
@@ -47,20 +47,21 @@ class FMUBuilder:
if metadata is None:
metadata = {}
- # 2. Construct the full payload for Qdrant
- # We merge sensor data + metadata + new schema fields
final_payload = {
"timestamp": datetime.utcnow().isoformat(),
- "sensors": sensor_data, # Critical: Store raw values for Frontend display
- **metadata, # Unpack crop, stage, etc.
+
+ # ā
STORE RAW SENSORS (For Humans/Frontend)
+ "sensors": sensor_data,
+
+ # ā
UNPACK METADATA (crop, stage, etc.)
+ **metadata,
+
+ # ā
ENFORCE CRITICAL FIELDS (Defaults if missing)
"crop_id": metadata.get("crop_id", "UNKNOWN_CROP"),
"sequence_number": metadata.get("sequence_number", 1),
-
- # š NEW SCHEMA PARAMETERS (Initialized with Placeholders)
"action_taken": metadata.get("action_taken", "PENDING_ACTION"),
"outcome": metadata.get("outcome", "PENDING_OBSERVATION")
}
- # --- UPDATE END ---
return FMU(
id=str(uuid.uuid4()),
diff --git a/backend/server/functions.py b/backend/server/functions.py
@@ -103,6 +103,7 @@ async def process_ingest(file: UploadFile, sensors_str: str, metadata_str: str,
meta_data.update({
"crop_id": target_crop_id,
"sequence_number": seq_num,
+ "sensor_data": sensor_data,
# Ensure placeholders exist if not provided
"action_taken": meta_data.get("action_taken", "PENDING_ACTION"),
"outcome": meta_data.get("outcome", "PENDING_OBSERVATION")
diff --git a/backend/server/reset-db.py b/backend/server/reset-db.py
@@ -0,0 +1,82 @@
+import sys
+import os
+from dotenv import load_dotenv
+from qdrant_client import QdrantClient
+from qdrant_client.http import models
+
+# --- CONFIGURATION ---
+COLLECTION_NAME = "Farm_Memory"
+VECTOR_SIZE = 516 # 512 (Vision) + 4 (Sensors: pH, EC, Temp, Humid)
+
+# 1. Load Environment Variables
+current_dir = os.path.dirname(os.path.abspath(__file__))
+project_root = os.path.abspath(os.path.join(current_dir, '../../'))
+env_path = os.path.join(project_root, '.env')
+load_dotenv(env_path)
+
+# 2. Connect to Qdrant (Handles Cloud or Local)
+qdrant_url = os.getenv("QDRANT_URL", "http://localhost:6333")
+qdrant_key = os.getenv("QDRANT_API_KEY", None)
+
+print(f"š Connecting to Qdrant at: {qdrant_url}...")
+client = QdrantClient(url=qdrant_url, api_key=qdrant_key)
+
+def reset_db():
+ # 3. Check if Collection Exists and Delete it
+ if client.collection_exists(COLLECTION_NAME):
+ print(f"š„ Deleting existing collection '{COLLECTION_NAME}'...")
+ client.delete_collection(COLLECTION_NAME)
+ print("ā
Deleted.")
+ else:
+ print(f"ā ļø Collection '{COLLECTION_NAME}' did not exist.")
+
+ # 4. Create the New Collection
+ print(f"š ļø Creating collection '{COLLECTION_NAME}' with {VECTOR_SIZE} dimensions...")
+ client.create_collection(
+ collection_name=COLLECTION_NAME,
+ vectors_config=models.VectorParams(
+ size=VECTOR_SIZE,
+ distance=models.Distance.COSINE
+ )
+ )
+ print("ā
Collection created.")
+
+ # 5. Create Payload Indexes (CRITICAL STEP)
+ print("šļø Creating Payload Indexes...")
+
+ # A. Text Fields (Keyword)
+ text_indexes = [
+ "crop", # "Lettuce"
+ "stage", # "Vegetative"
+ "crop_id", # "Batch_A1"
+ "outcome", # "Negative"
+ "action_taken" # "Add CalMag"
+ ]
+
+ for field in text_indexes:
+ try:
+ client.create_payload_index(
+ collection_name=COLLECTION_NAME,
+ field_name=field,
+ field_schema=models.PayloadSchemaType.KEYWORD
+ )
+ print(f" š Indexed (Keyword): '{field}'")
+ except Exception as e:
+ print(f" ā ļø Error indexing '{field}': {e}")
+
+ # B. Numeric Fields (Integer)
+ # š NEW: Index sequence_number so we can sort by it later
+ try:
+ client.create_payload_index(
+ collection_name=COLLECTION_NAME,
+ field_name="sequence_number",
+ field_schema=models.PayloadSchemaType.INTEGER
+ )
+ print(f" š Indexed (Integer): 'sequence_number'")
+ except Exception as e:
+ print(f" ā ļø Error indexing 'sequence_number': {e}")
+
+ print("\nš Database Reset Complete! You are ready to ingest data.")
+
+if __name__ == "__main__":
+ reset_db()
+\ No newline at end of file