demeter
Autonomous Hydroponic Intelligencegit clone https://github.com/maydayv7/demeter
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}")