demeter

Autonomous Hydroponic Intelligence

agent/sub_agents/fetching_agent.py (4639B)


  1import sys
  2import os
  3import requests
  4from pathlib import Path
  5import base64
  6from io import BytesIO
  7from PIL import Image
  8
  9current_file = Path(__file__).resolve()
 10project_root = current_file.parent.parent.parent
 11sys.path.append(str(project_root))
 12
 13from qdrant_client import models
 14from Sentinel.agent import FMUBuilder
 15from Qdrant.Store import COLLECTION_NAME
 16from Qdrant.Client import client
 17
 18class FetchingAgent:
 19    def __init__(self, simulator_url=None):
 20        if simulator_url is None:
 21            simulator_url = os.environ.get(
 22                "SIMULATOR_STATE_URL", "http://localhost:8001/simulation/state"
 23            )
 24        self.sim_url = simulator_url
 25        self.builder = FMUBuilder()
 26
 27    def fetch_and_process(self):
 28        print(f"[Fetcher] 📡 Requesting data from {self.sim_url}...")
 29
 30        try:
 31            response = requests.get(self.sim_url)
 32
 33            if response.status_code == 200:
 34                data_list = response.json()
 35                
 36                if not isinstance(data_list, list):
 37                    data_list = [data_list]
 38
 39                processed_crops = []
 40
 41                for data in data_list:
 42                    window_data = data.get("sensor_window", {})
 43                    image_b64 = data.get("image", "")
 44                    raw_meta = data.get("metadata", {})
 45
 46                    if not image_b64:
 47                        img = Image.new("RGB", (512, 512), (50, 50, 50))
 48                        buf = BytesIO()
 49                        img.save(buf, format="PNG")
 50                        image_b64 = base64.b64encode(buf.getvalue()).decode("utf-8")
 51
 52                    wanted_keys = {
 53                        "ph": "pH",
 54                        "ec": "EC",
 55                        "humidity": "humidity",
 56                        "temp": "temp",
 57                        "air_temp": "temp",
 58                    }
 59                    sensor_snapshot = {}
 60                    for key, value_list in window_data.items():
 61                        key_lower = key.lower()
 62                        if key_lower in wanted_keys:
 63                            out_name = wanted_keys[key_lower]
 64                            val = (
 65                                value_list[-1]
 66                                if isinstance(value_list, list) and value_list
 67                                else 0.0
 68                            )
 69                            sensor_snapshot[out_name] = val
 70
 71                    crop_id = raw_meta.get("crop_id", data.get("crop_id", "UNKNOWN_CROP"))
 72                    next_seq = self._get_next_sequence(crop_id)
 73
 74                    filtered_metadata = {
 75                        "crop": raw_meta.get("crop", "unknown"),
 76                        "stage": raw_meta.get("stage", "unknown"),
 77                        "crop_id": crop_id,
 78                        "sequence_number": next_seq,
 79                        "image_b64": image_b64,
 80                    }
 81
 82                    fmu = self.builder.create_fmu(
 83                        image_b64, sensor_snapshot, filtered_metadata
 84                    )
 85
 86                    search_results = self.find_similar_instances(fmu)
 87
 88                    processed_crops.append({
 89                        "crop_id": crop_id,
 90                        "fmu": fmu,
 91                        "sensor_snapshot": sensor_snapshot,
 92                        "history": search_results,
 93                        "image_b64": image_b64
 94                    })
 95
 96                return processed_crops
 97            else:
 98                print(f"[Fetcher] ❌ Error: Simulator returned {response.status_code}")
 99                return []
100
101        except Exception as e:
102            print(f"[Fetcher] ❌ Critical Error: {e}")
103            return []
104
105    def _get_next_sequence(self, crop_id):
106        try:
107            count_filter = models.Filter(
108                must=[
109                    models.FieldCondition(
110                        key="crop_id", match=models.MatchValue(value=crop_id)
111                    )
112                ]
113            )
114            count_result = client.count(
115                collection_name=COLLECTION_NAME, count_filter=count_filter
116            )
117            return count_result.count + 1
118        except Exception:
119            return 1
120
121    def find_similar_instances(self, current_fmu):
122        try:
123            hits = client.search(
124                collection_name=COLLECTION_NAME,
125                query_vector=current_fmu.vector,
126                limit=3,
127                with_payload=True,
128            )
129            return [{"payload": hit.payload} for hit in hits]
130        except Exception:
131            return []