commit 10d67e93d63b43c2578ab9377fe5a8461634960c
parent e35872fb3fbf787d11ae601fd4df4c969ac39b3c
Author: maydayv7 <maydayv7@gmail.com>
Date: Sat, 28 Mar 2026 00:58:38 +0530
Proper .env import
Diffstat:
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)