demeter

Autonomous Hydroponic Intelligence

agent/memory.py (5947B)


  1import logging
  2from mem0 import Memory
  3import os
  4import time  
  5from dotenv import load_dotenv
  6from qdrant_client import QdrantClient, models
  7
  8load_dotenv()
  9
 10logging.getLogger("mem0").setLevel(logging.WARNING)
 11logging.getLogger("httpx").setLevel(logging.WARNING)
 12
 13class FarmMemory:
 14    def __init__(self):
 15        # 1. Setup Collection
 16        self._setup_collection()
 17        
 18        # --- NEW CODE: Map Azure variables for mem0 natively ---
 19        os.environ["LLM_AZURE_OPENAI_API_KEY"] = os.getenv("AZURE_OPENAI_API_KEY", "")
 20        os.environ["LLM_AZURE_DEPLOYMENT"] = os.getenv("AZURE_OPENAI_DEPLOYMENT_NAME", "gpt-4.1")
 21        os.environ["LLM_AZURE_ENDPOINT"] = os.getenv("AZURE_OPENAI_ENDPOINT", "")
 22        os.environ["LLM_AZURE_API_VERSION"] = os.getenv("AZURE_OPENAI_API_VERSION", "2024-12-01-preview")
 23        
 24        # 2. Initialize Mem0
 25        config = {
 26            "vector_store": {
 27                "provider": "qdrant",
 28                "config": {
 29                    "url": os.getenv("QDRANT_URL"),
 30                    "api_key": os.getenv("QDRANT_API_KEY"),
 31                    "collection_name": "Plant_Biographies_HF",
 32                    "port": 6333,
 33                }
 34            },
 35            "llm": {
 36                "provider": "azure_openai",
 37                "config": {
 38                    "max_tokens": 1500
 39                }
 40            },
 41            "embedder": {
 42                "provider": "huggingface",
 43                "config": {
 44                    "model": "all-MiniLM-L6-v2"
 45                }
 46            }
 47        }
 48        
 49        print("🧠 Initializing FarmMemory...")
 50        self.memory = Memory.from_config(config)
 51    
 52    def _setup_collection(self):
 53        """Create the collection manually to guarantee dimension match"""
 54        try:
 55            client = QdrantClient(
 56                url=os.getenv("QDRANT_URL"),
 57                api_key=os.getenv("QDRANT_API_KEY"),
 58                timeout=30 
 59            )
 60            
 61            collection_name = "Plant_Biographies_HF"
 62            
 63            if client.collection_exists(collection_name):
 64                print(f"✅ Collection '{collection_name}' verified.")
 65            else:
 66                print(f"📝 Creating '{collection_name}' (384 dims)...")
 67                client.create_collection(
 68                    collection_name=collection_name,
 69                    vectors_config=models.VectorParams(
 70                        size=384, 
 71                        distance=models.Distance.COSINE
 72                    )
 73                )
 74                print(f"✅ Collection created.")
 75        except Exception as e:
 76            print(f"⚠️ Collection setup warning: {e}")
 77
 78    def get_plant_history(self, crop_id, limit=3):
 79        """Retrieve the last N chronological entries for a crop"""
 80        try:
 81            print(f"🔍 [FarmMemory] Fetching last {limit} entries for: {crop_id}...")
 82            
 83            # 1. Use get_all() to fetch raw history (bypassing vector similarity)
 84            # This ensures we get the *actual* latest events, not just "relevant" ones
 85            history = self.memory.get_all(user_id=crop_id)
 86
 87            # 2. Extract List from response
 88            results = []
 89            if isinstance(history, dict):
 90                results = history.get("results", [])
 91            elif isinstance(history, list):
 92                results = history
 93            
 94            if not results:
 95                return f"No biography found for {crop_id}."
 96
 97            # 3. Sort by 'created_at' (Descending) to get Newest -> Oldest
 98            # Safe sort: defaults to empty string if 'created_at' is missing
 99            results.sort(key=lambda x: x.get('created_at', ''), reverse=True)
100            
101            # 4. Slice the top N (Past 3)
102            recent_results = results[:limit]
103            
104            # 5. Format the output
105            formatted_lines = []
106            for item in recent_results:
107                # Handle different mem0 versions where content might be in 'memory' or 'text'
108                text = item.get("memory", item.get("text", str(item)))
109                
110                # Optional: Add a timestamp to the output for verification
111                timestamp = item.get("created_at", "")
112                if timestamp:
113                    # Cleanup timestamp for readability (e.g., 2024-02-09T10:00:00 -> 2024-02-09 10:00)
114                    timestamp = timestamp.replace("T", " ").split(".")[0]
115                    formatted_lines.append(f"[{timestamp}] {text}")
116                else:
117                    formatted_lines.append(f"- {text}")
118            
119            clean_output = "\n".join(formatted_lines)
120
121            return clean_output
122
123        except Exception as e:
124            print(f"❌ [FarmMemory] Error: {e}")
125            return f"Error retrieving history: {str(e)}"
126
127    def log_event(self, crop_id, event_text):
128        """Log a new event in the plant's biography"""
129        try:
130            # Capture the result to check if LLM extracted it correctly
131            result = self.memory.add(event_text, user_id=crop_id)
132            print(f"🧠 Biography Updated for {crop_id}")
133        except Exception as e:
134            print(f"❌ Memory Write Error: {e}")
135
136
137# execute
138if __name__ == "__main__":
139    farm_memory = FarmMemory()
140    print("\n✅ System Online.\n")
141
142    # 1. WRITE: Manually log a test event
143    test_crop = "BATCH-VERDANT-X1"
144    # print(f"📝 Logging event for {test_crop}...")
145    farm_memory.log_event(test_crop, "CRITICAL TEST: Detected mild nutrient burn. Flushed system with pH 5.8 water.")
146
147    # CRITICAL FIX: Wait for Vector DB indexing
148    print("⏳ Waiting for memory indexing...")
149    time.sleep(2) 
150
151    # 2. READ: Fetch it back
152    print(f"\n🔍 Retrieving history for {test_crop}...")
153    history = farm_memory.get_plant_history(test_crop)
154    
155    print("-" * 40)
156    print(f"MEMORY OUTPUT:\n{history}")
157    print("-" * 40)