demeter

Autonomous Hydroponic Intelligence
commit 6170813c84e62da966edaa0dafd0f26ff42a9bff
parent 310ecc90900a22d1e6abd476230bd82df5231a18
Author: Debarghya Das <debarghya1108@gmail.com>
Date:   Mon,  2 Mar 2026 11:48:40 +0000

Merge PR

Diffstat:
Magent/Sentinel/Encoders/TimeSeries.py | 53+++++++++++++++++++++++++++++++++++++++++------------
Magent/Sentinel/agent.py | 15++++++++-------
Mbackend/server/functions.py | 1+
Abackend/server/reset-db.py | 83+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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