demeter

Autonomous Hydroponic Intelligence
commit 10d67e93d63b43c2578ab9377fe5a8461634960c
parent e35872fb3fbf787d11ae601fd4df4c969ac39b3c
Author: maydayv7 <maydayv7@gmail.com>
Date:   Sat, 28 Mar 2026 00:58:38 +0530

Proper .env import

Diffstat:
Magent/Qdrant/Client.py | 5++++-
Magent/main_agent.py | 17+++++++++--------
Mbackend/node_server/config/db.js | 96++++++++++++++++++++++++++++++++++++++++---------------------------------------
Mbackend/node_server/index.js | 21+++++++++++----------
Mreadme.md | 19++++++++++---------
Mrequirements.txt | 1+
Msimulator/main.py | 159+++++++++++++++++++++++++++++++++++++++++++++++--------------------------------
7 files changed, 179 insertions(+), 139 deletions(-)

diff --git a/agent/Qdrant/Client.py b/agent/Qdrant/Client.py @@ -2,7 +2,10 @@ import os from dotenv import load_dotenv from qdrant_client import QdrantClient -load_dotenv() +# Load env from root +current_dir = os.path.dirname(os.path.abspath(__file__)) +env_path = os.path.join(current_dir, "..", "..", ".env") +load_dotenv(env_path) client = QdrantClient( url=os.getenv("QDRANT_URL", "http://localhost:6333"), diff --git a/agent/main_agent.py b/agent/main_agent.py @@ -4,7 +4,10 @@ import requests import time from dotenv import load_dotenv -load_dotenv() +# Load env from root +current_dir = os.path.dirname(os.path.abspath(__file__)) +env_path = os.path.join(current_dir, "..", ".env") +load_dotenv(env_path) current_dir = os.path.dirname(os.path.abspath(__file__)) sys.path.append(current_dir) @@ -20,6 +23,7 @@ SIMULATOR_ACTION_URL = os.getenv( "SIMULATOR_ACTION_URL", "http://localhost:8001/simulation/action" ) + def main(): print("šŸš€ Initializing Demeter Orchestrator...") @@ -63,7 +67,7 @@ def main(): time.sleep(1) strat_name, strat_instr, action_idx = supervisor.get_strategic_goal(fmu) print(f"\nšŸŽ° BANDIT STRATEGY: {strat_name}") - + crop = fmu.metadata.get("crop", "unknown") stage = fmu.metadata.get("stage", "unknown") query = f"optimal hydroponic conditions for {crop} in {stage} stage" @@ -100,10 +104,7 @@ def main(): strategy_info=(strat_name, strat_instr, action_idx), ) - batch_actions.append({ - "crop_id": crop_id, - "action": final_action - }) + batch_actions.append({"crop_id": crop_id, "action": final_action}) print(f"\nāœ… Final Action for {crop_id}: {final_action}") try: @@ -116,5 +117,6 @@ def main(): print("\nzzz Sleeping 2 minutes...") time.sleep(120) + if __name__ == "__main__": - main() -\ No newline at end of file + main() diff --git a/backend/node_server/config/db.js b/backend/node_server/config/db.js @@ -1,64 +1,67 @@ -require('dotenv').config(); -const { QdrantClient } = require('@qdrant/js-client-rest'); -const {mongoose} = require('mongoose'); +const path = require("path"); +require("dotenv").config({ + path: path.join(__dirname, "..", "..", "..", ".env"), +}); +const { QdrantClient } = require("@qdrant/js-client-rest"); +const { mongoose } = require("mongoose"); -const COLLECTION_NAME = 'Farm_Memory'; +const COLLECTION_NAME = "Farm_Memory"; // 1. Initialize Client with the Fix console.log("šŸ”§ Initializing Qdrant Client...", process.env.QDRANT_URL); const client = new QdrantClient({ - url: process.env.QDRANT_URL, - apiKey: process.env.QDRANT_API_KEY, - checkCompatibility: false, // šŸ‘ˆ FIX: Skips the failing version check + url: process.env.QDRANT_URL, + apiKey: process.env.QDRANT_API_KEY, + checkCompatibility: false, // šŸ‘ˆ FIX: Skips the failing version check }); // 2. Indexing & Collection Setup Function const initDB = async () => { - try { - // Test connection first - // If this fails, check your QDRANT_URL in .env - const result = await client.getCollections(); - - const exists = result.collections.some(c => c.name === COLLECTION_NAME); + try { + // Test connection first + // If this fails, check your QDRANT_URL in .env + const result = await client.getCollections(); - // A. Create Collection if missing - if (!exists) { - await client.createCollection(COLLECTION_NAME, { - vectors: { size: 4, distance: 'Cosine' }, - }); - console.log(`āœ… Collection '${COLLECTION_NAME}' created.`); - } + const exists = result.collections.some((c) => c.name === COLLECTION_NAME); - // B. Create Index - // Using try-catch here specifically for index creation to prevent crashing if it already exists - try { - await client.createPayloadIndex(COLLECTION_NAME, { - field_name: "crop_id", - field_schema: "keyword", - }); - console.log("āœ… Indexes verified."); - } catch (indexError) { - // Ignore error if index already exists - if (!indexError.message.includes("already exists")) { - console.warn("āš ļø Note on Index:", indexError.message); - } - } + // A. Create Collection if missing + if (!exists) { + await client.createCollection(COLLECTION_NAME, { + vectors: { size: 4, distance: "Cosine" }, + }); + console.log(`āœ… Collection '${COLLECTION_NAME}' created.`); + } - } catch (err) { - console.error("āŒ DB Connection Failed:"); - console.error(" Reason:", err.message); - console.error(" Check your QDRANT_URL in .env. It must start with 'http://' or 'https://'"); + // B. Create Index + // Using try-catch here specifically for index creation to prevent crashing if it already exists + try { + await client.createPayloadIndex(COLLECTION_NAME, { + field_name: "crop_id", + field_schema: "keyword", + }); + console.log("āœ… Indexes verified."); + } catch (indexError) { + // Ignore error if index already exists + if (!indexError.message.includes("already exists")) { + console.warn("āš ļø Note on Index:", indexError.message); + } } + } catch (err) { + console.error("āŒ DB Connection Failed:"); + console.error(" Reason:", err.message); + console.error( + " Check your QDRANT_URL in .env. It must start with 'http://' or 'https://'", + ); + } }; const connectMongoDB = async () => { - try { - await mongoose.connect(process.env.MONGODB_URI, { - }); - console.log("āœ… Connected to MongoDB"); - } catch (err) { - console.error("āŒ MongoDB Connection Failed:", err.message); - } + try { + await mongoose.connect(process.env.MONGODB_URI, {}); + console.log("āœ… Connected to MongoDB"); + } catch (err) { + console.error("āŒ MongoDB Connection Failed:", err.message); + } }; -module.exports = { client, initDB, connectMongoDB, COLLECTION_NAME }; -\ No newline at end of file +module.exports = { client, initDB, connectMongoDB, COLLECTION_NAME }; diff --git a/backend/node_server/index.js b/backend/node_server/index.js @@ -1,8 +1,10 @@ -const express = require('express'); -const cors = require('cors'); -const { initDB, connectMongoDB } = require('./config/db'); -const farmRoutes = require('./routes/farmRoutes'); -const cropRoutes = require('./routes/cropRoutes'); +const path = require("path"); +require("dotenv").config({ path: path.join(__dirname, "..", "..", ".env") }); +const express = require("express"); +const cors = require("cors"); +const { initDB, connectMongoDB } = require("./config/db"); +const farmRoutes = require("./routes/farmRoutes"); +const cropRoutes = require("./routes/cropRoutes"); const app = express(); const PORT = process.env.PORT || 3001; @@ -17,9 +19,9 @@ connectMongoDB(); // Mount Routes // All routes inside farmRoutes will be prefixed with /api -app.use('/api', farmRoutes); -app.use('/api/crops', cropRoutes); +app.use("/api", farmRoutes); +app.use("/api/crops", cropRoutes); app.listen(PORT, () => { - console.log(`šŸš€ Server running on port ${PORT}`); -}); -\ No newline at end of file + console.log(`šŸš€ Server running on port ${PORT}`); +}); diff --git a/readme.md b/readme.md @@ -39,7 +39,7 @@ Built for the **Microsoft AI Unlocked - AI for India** hackathon, Demeter addres - `Knowledge_Base` - agronomic research documents (RAG) - `Plant_Biographies_HF` - long-term per-crop memory (via Mem0) - **Mem0** - semantic plant biography system backed by Azure OpenAI -- **Node.js + MongoDB** - structured crop metadata and event logs +- **NodeJS + MongoDB** - structured crop metadata and event logs ### Physics Simulator @@ -276,7 +276,7 @@ demeter/ ### Prerequisites - Python 3.10+ -- Node.js 18+ +- NodeJS 18+ - A running [Qdrant](https://qdrant.tech/) instance (local Docker or Qdrant Cloud) - Azure OpenAI resource with GPT-4.1 deployment - (Optional) Azure Digital Twins instance @@ -321,10 +321,13 @@ SIMULATOR_PORT=8001 # Azure Digital Twins ADT_URL=your-adt-instance.digitaltwins.azure.net +# Backend +PORT=3001 +MONGODB_URI=your_mongodb_url + # Frontend REACT_APP_AGENT_API_URL=http://localhost:8000 REACT_APP_FARM_API_URL=http://localhost:3001/api -PORT=3001 ``` ### 3. Start Qdrant Database @@ -361,8 +364,7 @@ Otherwise, download [model_bandit_greedy.pkl](https://drive.google.com/file/d/1s ### 6. Start the Simulator ```bash -cd simulator -python main.py +python simulator/main.py # Runs on http://localhost:8001 ``` @@ -372,10 +374,9 @@ Download [plant_disease_model.pt](https://drive.google.com/file/d/1NkdGt0CFS7tx4 ```bash # Python FastAPI server -cd backend/server -uvicorn main:app --reload --port 8000 +python backend/server/main.py -# Node.js Express server +# NodeJS Express server cd backend/node_server npm install && npm start ``` @@ -397,4 +398,4 @@ python agent/main_agent.py --- -**Made with ā¤ļø for the future of sustainable agriculture** +<center>Made with ā¤ļø for the future of sustainable agriculture</center> diff --git a/requirements.txt b/requirements.txt @@ -48,3 +48,4 @@ openai-whisper>=20231117 # Utilities scipy>=1.13.0 +pymongo>=4.16.0 diff --git a/simulator/main.py b/simulator/main.py @@ -13,9 +13,12 @@ from dotenv import load_dotenv from pymongo import MongoClient, ReturnDocument from datetime import datetime -load_dotenv() +# Load env from root +current_dir = os.path.dirname(os.path.abspath(__file__)) +env_path = os.path.join(current_dir, "..", ".env") +load_dotenv(env_path) -MONGO_URI = os.environ.get("MONGO_URI", "mongodb+srv://abhi:lovesv7@demeter.qfvttv1.mongodb.net/?appName=Demeter") +MONGO_URI = os.environ.get("MONGODB_URI") mongo_client = MongoClient(MONGO_URI) db = mongo_client["test"] crops_collection = db["cropstates"] @@ -29,7 +32,7 @@ CROP_LIFECYCLES = { "stages": [ {"name": "seedling", "end_hour": 168}, {"name": "vegetative", "end_hour": 504}, - {"name": "harvest", "end_hour": 999999} + {"name": "harvest", "end_hour": 999999}, ] }, "tomato": { @@ -37,14 +40,14 @@ CROP_LIFECYCLES = { {"name": "seedling", "end_hour": 336}, {"name": "vegetative", "end_hour": 1008}, {"name": "flowering", "end_hour": 1680}, - {"name": "fruiting", "end_hour": 999999} + {"name": "fruiting", "end_hour": 999999}, ] }, "basil": { "stages": [ {"name": "seedling", "end_hour": 168}, {"name": "vegetative", "end_hour": 672}, - {"name": "harvest", "end_hour": 999999} + {"name": "harvest", "end_hour": 999999}, ] }, "strawberry": { @@ -52,11 +55,12 @@ CROP_LIFECYCLES = { {"name": "seedling", "end_hour": 336}, {"name": "vegetative", "end_hour": 1008}, {"name": "flowering", "end_hour": 1512}, - {"name": "fruiting", "end_hour": 999999} + {"name": "fruiting", "end_hour": 999999}, ] - } + }, } + class FarmAction(BaseModel): acid_dosage_ml: float = 0.0 base_dosage_ml: float = 0.0 @@ -65,10 +69,12 @@ class FarmAction(BaseModel): water_refill_l: float = 0.0 debug_force_ph: float | None = None + class BatchActionRequest(BaseModel): crop_id: str action: FarmAction + class ResidualPhysicsNet(torch.nn.Module): def __init__(self, state_dim, action_dim): super().__init__() @@ -84,6 +90,7 @@ class ResidualPhysicsNet(torch.nn.Module): x = torch.cat([state, action], dim=-1) return self.net(x) + class DigitalTwin: def __init__(self, crop_id: str, initial_state: list): self.crop_id = crop_id @@ -112,12 +119,15 @@ class DigitalTwin: if action is None: action = FarmAction() - u = np.array([ - action.acid_dosage_ml / 10.0, - action.base_dosage_ml / 10.0, - action.nutrient_dosage_ml / 20.0, - action.fan_speed_pct / 100.0, - ], dtype=np.float32) + u = np.array( + [ + action.acid_dosage_ml / 10.0, + action.base_dosage_ml / 10.0, + action.nutrient_dosage_ml / 20.0, + action.fan_speed_pct / 100.0, + ], + dtype=np.float32, + ) ph, ec, wt, at, hum, vpd, bio = self.state @@ -133,10 +143,14 @@ class DigitalTwin: stress = abs(vpd - 1.0) growth = 0.1 * bio * (1.0 - min(stress, 1.0)) - physics_delta = np.array([d_ph, d_ec, 0, d_at, d_hum, 0, growth], dtype=np.float32) + physics_delta = np.array( + [d_ph, d_ec, 0, d_at, d_hum, 0, growth], dtype=np.float32 + ) with torch.no_grad(): - nn_delta = self.residual_model(torch.tensor(self.state), torch.tensor(u)).numpy() + nn_delta = self.residual_model( + torch.tensor(self.state), torch.tensor(u) + ).numpy() self.state += physics_delta + (nn_delta * 0.05) self.state[3] = np.clip(self.state[3], 0, 50) @@ -146,7 +160,9 @@ class DigitalTwin: ph_score = max(0, 1.0 - abs(self.state[0] - 6.0)) vpd_score = max(0, 1.0 - abs(self.state[5] - 1.0)) - self.plant_health = max(0.0, min(100.0, (float(ph_score) + float(vpd_score)) * 50.0)) + self.plant_health = max( + 0.0, min(100.0, (float(ph_score) + float(vpd_score)) * 50.0) + ) self._update_history() return self._generate_image() @@ -167,18 +183,21 @@ class DigitalTwin: return Image.open(filename) return Image.new("RGB", (512, 512), (50, 50, 50)) + app = FastAPI() simulators = {} + def sync_simulators_from_db(): db_crops = crops_collection.find({}) for crop in db_crops: cid = crop.get("crop_id") if not cid: continue - + if cid not in simulators: sensors = crop.get("sensors", {}) + ph_val = sensors.get("pH", [6.0]) ec_val = sensors.get("EC", [1.5]) temp_val = sensors.get("temp", [24.0]) @@ -187,53 +206,58 @@ def sync_simulators_from_db(): state = [ ph_val[-1] if isinstance(ph_val, list) else ph_val, ec_val[-1] if isinstance(ec_val, list) else ec_val, - 20.0, + 20.0, temp_val[-1] if isinstance(temp_val, list) else temp_val, hum_val[-1] if isinstance(hum_val, list) else hum_val, - 1.0, - 10.0 + 1.0, + 10.0, ] simulators[cid] = DigitalTwin(cid, state) + @app.get("/simulation/state") async def get_all_states(): clock = sim_state_collection.find_one_and_update( {"_id": "global_clock"}, {"$inc": {"tick_hours": 1}}, upsert=True, - return_document=ReturnDocument.AFTER + return_document=ReturnDocument.AFTER, ) current_tick = clock["tick_hours"] crops_collection.update_many({}, {"$inc": {"simulated_age_hours": 1}}) sync_simulators_from_db() - + db_crops = list(crops_collection.find({})) response = [] - + for crop in db_crops: cid = crop.get("crop_id") if not cid or cid not in simulators: continue - + crop_type = crop.get("crop", "lettuce").lower() age_hours = crop.get("sequence_number", 0) * crop.get("cycle_duration_hours", 1) print(f"Simulating Crop ID: {cid} | Type: {crop_type} | Age (hrs): {age_hours}") cycle_duration = crop.get("cycle_duration_hours", 1) - + new_stage = crop.get("stage", "seedling") if crop_type in CROP_LIFECYCLES: for stage_info in CROP_LIFECYCLES[crop_type]["stages"]: if age_hours <= stage_info["end_hour"]: new_stage = stage_info["name"] break - + if new_stage != crop.get("stage"): - crops_collection.update_one({"crop_id": cid}, {"$set": {"stage": new_stage}}) - - print(f" current_tick: {current_tick} | crop_id: {cid} | age_hours: {age_hours} | stage: {new_stage} | cycle_duration: {cycle_duration}") + crops_collection.update_one( + {"crop_id": cid}, {"$set": {"stage": new_stage}} + ) + + print( + f" current_tick: {current_tick} | crop_id: {cid} | age_hours: {age_hours} | stage: {new_stage} | cycle_duration: {cycle_duration}" + ) if current_tick % cycle_duration != 0: continue @@ -244,33 +268,38 @@ async def get_all_states(): pil_img.save(buf, format="PNG") img_b64 = base64.b64encode(buf.getvalue()).decode("utf-8") - response.append({ - "crop_id": cid, - "sensor_window": {k: list(v) for k, v in sim.history.items()}, - "metadata": { - "crop": crop_type, - "stage": new_stage, - "health": round(float(sim.plant_health), 1), - "biomass_est": round(float(sim.state[6]), 2), - "age_hours": age_hours, - "global_tick": current_tick - }, - "image": img_b64, - }) - + response.append( + { + "crop_id": cid, + "sensor_window": {k: list(v) for k, v in sim.history.items()}, + "metadata": { + "crop": crop_type, + "stage": new_stage, + "health": round(float(sim.plant_health), 1), + "biomass_est": round(float(sim.state[6]), 2), + "age_hours": age_hours, + "global_tick": current_tick, + }, + "image": img_b64, + } + ) + return response + @app.post("/simulation/action") async def take_batch_actions(payload: List[BatchActionRequest]): sync_simulators_from_db() - + results = [] for req in payload: cid = req.crop_id if cid not in simulators: - results.append({"crop_id": cid, "status": "error", "message": "Crop not found"}) + results.append( + {"crop_id": cid, "status": "error", "message": "Crop not found"} + ) continue - + sim = simulators[cid] sim.step(req.action) @@ -278,36 +307,39 @@ async def take_batch_actions(payload: List[BatchActionRequest]): "sensors.pH": {"$each": [float(sim.state[0])], "$slice": -5}, "sensors.EC": {"$each": [float(sim.state[1])], "$slice": -5}, "sensors.temp": {"$each": [float(sim.state[3])], "$slice": -5}, - "sensors.humidity": {"$each": [float(sim.state[4])], "$slice": -5} + "sensors.humidity": {"$each": [float(sim.state[4])], "$slice": -5}, } - + set_payload = { "action_taken": req.action.dict(), - "last_updated": datetime.utcnow() + "last_updated": datetime.utcnow(), } - + crops_collection.update_one( {"crop_id": cid}, { "$push": push_payload, "$set": set_payload, - "$inc": {"sequence_number": 1} - } + "$inc": {"sequence_number": 1}, + }, ) - results.append({ - "crop_id": cid, - "status": "success", - "new_state": { - "pH": float(sim.state[0]), - "EC": float(sim.state[1]), - "temp": float(sim.state[3]), - "humidity": float(sim.state[4]) + results.append( + { + "crop_id": cid, + "status": "success", + "new_state": { + "pH": float(sim.state[0]), + "EC": float(sim.state[1]), + "temp": float(sim.state[3]), + "humidity": float(sim.state[4]), + }, } - }) - + ) + return {"updated_crops": results} + if __name__ == "__main__": port = int(os.environ.get("SIMULATOR_PORT", 8001)) - uvicorn.run(app, host="0.0.0.0", port=port) -\ No newline at end of file + uvicorn.run(app, host="0.0.0.0", port=port)