demeter

Autonomous Hydroponic Intelligence

agent/Sentinel/agent.py (4972B)


  1import uuid
  2import base64
  3import io
  4from datetime import datetime
  5import numpy as np
  6from pathlib import Path
  7
  8# Ensure these imports match your project structure
  9from Sentinel.Encoders.Vision import VisionEncoder
 10from Sentinel.Encoders.TimeSeries import SensorEncoder
 11from Sentinel.fmu import FMU
 12from Qdrant.Store import store_fmu
 13
 14class FMUBuilder:
 15    def __init__(self):
 16        self.vision = VisionEncoder()
 17        self.sensors = SensorEncoder()
 18
 19    def create_fmu(self, image_input, sensor_data, metadata=None):
 20        """
 21        Creates an FMU from either:
 22        - A file path (str/Path)
 23        - A Base64 encoded image string
 24        
 25        Args:
 26            image_input: Either a file path string or base64 string
 27            sensor_data: Dictionary of sensor readings
 28            metadata: Optional metadata dictionary
 29        """
 30        
 31        # Detect if input is base64 or file path
 32        if self._is_base64(image_input):
 33            # Handle Base64 input
 34            img_vec = self._encode_from_base64(image_input)
 35        else:
 36            # Handle file path input (original behavior)
 37            img_vec = self.vision.encode(image_input)
 38        
 39        # Encode sensor data
 40        sensor_vec = self.sensors.encode(sensor_data)
 41
 42        # Combine vectors
 43        fmu_vector = np.concatenate([img_vec, sensor_vec]).tolist()
 44
 45        # --- UPDATE START ---
 46        # 1. Ensure metadata is a dictionary
 47        if metadata is None:
 48            metadata = {}
 49
 50        final_payload = {
 51            "timestamp": datetime.utcnow().isoformat(),
 52            
 53            # ✅ STORE RAW SENSORS (For Humans/Frontend)
 54            "sensors": sensor_data, 
 55            
 56            # ✅ UNPACK METADATA (crop, stage, etc.)
 57            **metadata,
 58            
 59            # ✅ ENFORCE CRITICAL FIELDS (Defaults if missing)
 60            "crop_id": metadata.get("crop_id", "UNKNOWN_CROP"),
 61            "sequence_number": metadata.get("sequence_number", 1),
 62            "action_taken": metadata.get("action_taken", "PENDING_ACTION"),
 63            "outcome": metadata.get("outcome", "PENDING_OBSERVATION"),
 64            "explanation_log": metadata.get("explanation_log", "PENDING_ANALYSIS")
 65        }
 66
 67        return FMU(
 68            id=str(uuid.uuid4()),
 69            vector=fmu_vector,
 70            metadata=final_payload # This becomes the Qdrant Payload
 71        )
 72    
 73    def _is_base64(self, s):
 74        """
 75        Detect if string is base64 or a file path.
 76        Returns True if it looks like base64, False if it looks like a path.
 77        """
 78        if not isinstance(s, str):
 79            return False
 80            
 81        # If it has path separators, it's probably a path
 82        if '/' in s or '\\' in s or Path(s).exists():
 83            return False
 84        
 85        # If it has base64 header, it's definitely base64
 86        if s.startswith('data:image'):
 87            return True
 88            
 89        # Check if it's valid base64 (after removing potential header)
 90        test_str = s.split(',')[-1] if ',' in s else s
 91        
 92        # Base64 strings are typically very long and only contain valid b64 chars
 93        if len(test_str) > 100:  # Arbitrary threshold
 94            try:
 95                base64.b64decode(test_str, validate=True)
 96                return True
 97            except Exception:
 98                return False
 99        
100        return False
101    
102    def _encode_from_base64(self, image_base64):
103        """
104        Decode base64 string and encode the image.
105        """
106        # Remove header if present (e.g., "data:image/png;base64,...")
107        if "," in image_base64:
108            image_base64 = image_base64.split(",")[1]
109        
110        # Add padding if necessary (fix the "multiple of 4" error)
111        missing_padding = len(image_base64) % 4
112        if missing_padding:
113            image_base64 += '=' * (4 - missing_padding)
114        
115        # Decode to bytes
116        image_bytes = base64.b64decode(image_base64)
117        
118        # Create file-like object
119        image_stream = io.BytesIO(image_bytes)
120        
121        # Encode using VisionEncoder
122        return self.vision.encode(image_stream)
123
124
125# if __name__ == "__main__":
126#     builder = FMUBuilder()
127
128#     sensors = {
129#         "pH": 5.9,
130#         "EC": 1.3,
131#         "temp": 25.0,
132#         "humidity": 72.0
133#     }
134
135#     # Test with base64
136#     sample_base64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+ip1sAAAAASUVORK5CYII="
137    
138#     fmu = builder.create_fmu(sample_base64, sensors, {
139#         "crop": "lettuce",
140#         "stage": "vegetative"
141#     })
142
143#     print("✅ FMU ID:", fmu.id)
144#     print("✅ Vector length:", len(fmu.vector))
145    
146#     # Check for the new fields in the output
147#     print("\n🔍 Checking Schema:")
148#     print(f"   - Action: {fmu.metadata.get('action_taken')}")
149#     print(f"   - Outcome: {fmu.metadata.get('outcome')}")
150#     print(f"   - Sensors Saved: {'sensors' in fmu.metadata}")