commit b3fc6028423798ebdd095b02fef3fb645e29728b
parent bb76d3df20c3a68e58b28b769ab4f7f87f9a45da
Author: Debarghya Das <debarghya1108@gmail.com>
Date: Tue, 3 Mar 2026 15:58:00 +0000
Merge PR
Diffstat:
6 files changed, 252 insertions(+), 94 deletions(-)
diff --git a/agent/Marl/bandit.py b/agent/Marl/bandit.py
@@ -0,0 +1,84 @@
+# agent/Marl/Bandit.py
+
+import numpy as np
+import pickle
+import os
+
+class ContextualBandit:
+ def __init__(self, n_actions=15, feature_dim=519):
+ """
+ LinGreedy Implementation (Pure Exploitation).
+ We removed 'alpha' because we do not want to explore.
+ """
+ self.n_actions = n_actions
+ self.d = feature_dim
+
+ # A: Covariance Matrix (Used for Ridge Regression learning)
+ self.A = [np.identity(self.d) for _ in range(self.n_actions)]
+
+ # b: Reward Vector
+ self.b = [np.zeros(self.d) for _ in range(self.n_actions)]
+
+ self.file_path = "model_bandit_greedy.pkl"
+ self.load()
+
+ def select_action(self, context_vector):
+ """
+ Returns: (action_index, debug_info)
+ Strictly picks the action with the highest PREDICTED reward.
+ """
+ predicted_rewards = np.zeros(self.n_actions)
+ confidences = np.zeros(self.n_actions)
+
+ for a in range(self.n_actions):
+ # 1. Calculate Mean Estimate (theta)
+ # theta = A^-1 * b
+ try:
+ theta = np.linalg.solve(self.A[a], self.b[a])
+ except np.linalg.LinAlgError:
+ # Fallback for singular matrix (rare with Identity init)
+ theta = np.zeros(self.d)
+
+ # 2. Expected Reward (Dot Product)
+ # This is the "Best Guess" for how good this action is.
+ pred = theta.dot(context_vector)
+ predicted_rewards[a] = pred
+
+ # 3. Calculate Confidence (Optional, for UI only)
+ # We calculate variance just to show the user "How sure are we?"
+ # But we do NOT add this to the score.
+ variance = context_vector.dot(np.linalg.solve(self.A[a], context_vector))
+ confidences[a] = 1.0 / (1.0 + variance) # Simple confidence score (0-1)
+
+ # 🟢 PURE EXPLOITATION: Pick max predicted reward
+ chosen_action = np.argmax(predicted_rewards)
+
+ return chosen_action, {
+ "scores": predicted_rewards.tolist(),
+ "confidences": confidences.tolist()
+ }
+
+ def update(self, action_idx, context_vector, reward):
+ """
+ Online Learning: The AI still gets smarter with every feedback.
+ """
+ # Update the regression model for the chosen arm
+ self.A[action_idx] += np.outer(context_vector, context_vector)
+ self.b[action_idx] += reward * context_vector
+
+ self.save()
+ print(f"📈 Greedy Model Updated | Action: {action_idx} | Reward: {reward}")
+
+ def save(self):
+ with open(self.file_path, 'wb') as f:
+ pickle.dump({'A': self.A, 'b': self.b}, f)
+
+ def load(self):
+ if os.path.exists(self.file_path):
+ try:
+ with open(self.file_path, 'rb') as f:
+ data = pickle.load(f)
+ self.A = data['A']
+ self.b = data['b']
+ except Exception:
+ print("⚠️ Could not load model, starting fresh.")
+\ No newline at end of file
diff --git a/agent/Marl/strategies.py b/agent/Marl/strategies.py
@@ -0,0 +1,28 @@
+# agent/Marl/strategies.py
+
+STRATEGIES = {
+ # --- 🟢 GLOBAL / PASSIVE ---
+ 0: "MAINTAIN_CURRENT", # Everything looks good, hold steady.
+ 1: "CALIBRATE_SENSORS", # Data looks weird/impossible (e.g., pH 0). Check hardware.
+
+ # --- 💧 NUTRIENT INTERVENTIONS ---
+ 2: "AGGRESSIVE_PH_DOWN", # pH is way too high (> 7.5).
+ 3: "AGGRESSIVE_PH_UP", # pH is way too low (< 4.5).
+ 4: "GENTLE_PH_BALANCING", # pH is slightly off, use mild correction.
+ 5: "INCREASE_EC_VEG", # Plants are hungry (Vegetative Nitrogen boost).
+ 6: "INCREASE_EC_BLOOM", # Plants are hungry (Flowering PK boost).
+ 7: "LOWER_EC_FLUSH", # Nutrient burn detected (Tips yellowing). Reduce EC.
+ 8: "CALMAG_BOOST", # Specific deficiency (Magnesium/Calcium).
+
+ # --- 🌤️ CLIMATE INTERVENTIONS ---
+ 9: "RAISE_TEMP_HUMIDITY", # "VPD Low" protocol.
+ 10: "LOWER_TEMP_HUMIDITY", # "VPD High" / Mold risk protocol.
+ 11: "MAX_AIR_CIRCULATION", # Stagnant air / weak stems.
+
+ # --- 🚑 DISEASE / BIO ---
+ 12: "FUNGAL_TREATMENT", # White powdery spots detected.
+ 13: "PEST_ISOLATION", # Bugs visible.
+ 14: "PRUNE_NECROTIC_LEAVES" # Remove dead plant matter to prevent spread.
+}
+
+NUM_ACTIONS = len(STRATEGIES)
+\ No newline at end of file
diff --git a/agent/sub_agents/Researcher.py b/agent/sub_agents/Researcher.py
@@ -2,10 +2,16 @@ import uuid
from qdrant_client import models
from fastembed import TextEmbedding
from Qdrant.Client import client # Import your existing cloud connection
+from groq import Groq
+import os
+from dotenv import load_dotenv
+
+load_dotenv()
class ResearcherAgent:
def __init__(self):
self.client = client
+ self.llm = Groq(api_key=os.getenv("GROQ_API_KEY"))
self.collection = "Knowledge_Base"
# FastEmbed is lightweight and runs locally on CPU
self.encoder = TextEmbedding(model_name="BAAI/bge-small-en-v1.5")
diff --git a/agent/sub_agents/Supervisor.py b/agent/sub_agents/Supervisor.py
@@ -1,98 +1,156 @@
+# agent/sub_agents/Supervisor.py
+
import json
-import os
-from dotenv import load_dotenv
-from openai import OpenAI
+import numpy as np
-# Load .env file relative to this script
-current_dir = os.path.dirname(os.path.abspath(__file__))
-env_path = os.path.join(current_dir, '../../.env')
-load_dotenv(env_path)
+# 👇 CORRECT IMPORTS based on your folder structure
+from agent.Marl.bandit import ContextualBandit
+from agent.Marl.strategies import STRATEGIES, NUM_ACTIONS
class SupervisorAgent:
- def __init__(self, researcher_agent):
- self.researcher = researcher_agent
+ def __init__(self, researcher):
+ self.llm = researcher.llm
- # ⚡ CONNECT TO GROQ CLOUD
- # CHECK: Ensure your .env file has 'GROK_API_KEY' or 'GROQ_API_KEY'
- # We use 'GROQ_API_KEY' here based on your previous messages
- api_key = os.getenv("GROQ_API_KEY")
-
- if not api_key:
- print("⚠️ WARNING: API Key not found. Supervisor may fail.")
-
- self.llm = OpenAI(
- base_url="https://api.groq.com/openai/v1",
- api_key=api_key
- )
+ # Initialize Bandit (15 actions, 515 dimensions)
+ self.bandit = ContextualBandit(n_actions=NUM_ACTIONS, feature_dim=519)
- def reason(self, current_fmu, similar_fmus, sub_agent_outputs):
+ def _build_context(self, fmu_vector, sensors):
"""
- The Core Reasoning Loop:
- 1. Contextualize -> 2. Research -> 3. Synthesize -> 4. Decide
+ Combines Visual Intuition (CLIP) with Explicit Sensors.
"""
+ # Ensure vector is numpy
+ vis_vec = np.array(fmu_vector) if isinstance(fmu_vector, list) else fmu_vector
- # --- STEP 1: Formulate the Research Question ---
- crop = current_fmu['metadata'].get('crop', 'Unknown Crop')
- stage = current_fmu['metadata'].get('stage', 'Unknown Stage')
-
- # E.g., "Lettuce Vegetative Low pH issues"
- research_query = f"{crop} {stage} {sub_agent_outputs.get('nutrient_analysis', '')} issues"
+ # Normalize sensors roughly to 0-1 range
+ s_vec = np.array([
+ (sensors.get('pH', 6.0) - 6.0) / 2.0,
+ sensors.get('EC', 1.0) / 3.0,
+ sensors.get('temp', 25.0) / 40.0
+ ])
- print(f"🤔 Supervisor is asking Researcher: '{research_query}'")
+ return np.concatenate([vis_vec, s_vec])
- # --- STEP 2: The Researcher Fetches Evidence (RAG) ---
- # This now returns a clean STRING, not a list
- scientific_context = self.researcher.search(research_query)
+ def _get_strategy_instruction(self, strategy_name):
+ """
+ Translates mathematical intent into LLM instructions.
+ """
+ instructions = {
+ "MAINTAIN_CURRENT": "Do NOT recommend changes. System is stable.",
+ "CALIBRATE_SENSORS": "Sensor readings are anomalous. Recommend hardware calibration.",
+ "AGGRESSIVE_PH_DOWN": "Priority: LOWER pH rapidly. Recommend strong acid buffers.",
+ "AGGRESSIVE_PH_UP": "Priority: RAISE pH rapidly. Recommend strong base buffers.",
+ "GENTLE_PH_BALANCING": "pH is drifting. Recommend gentle adjustments only.",
+ "INCREASE_EC_VEG": "Plant needs NITROGEN for vegetative growth.",
+ "INCREASE_EC_BLOOM": "Plant needs PHOSPHORUS/POTASSIUM for flowering.",
+ "LOWER_EC_FLUSH": "Nutrient burn detected. Recommend flushing reservoir.",
+ "CALMAG_BOOST": "Deficiency detected. Recommend Calcium/Magnesium supplement.",
+ "RAISE_TEMP_HUMIDITY": "Environment too cold/dry. Recommend heating/humidifying.",
+ "LOWER_TEMP_HUMIDITY": "Mold risk high. Recommend fans and dehumidifiers.",
+ "MAX_AIR_CIRCULATION": "Stagnant air. Recommend max fan speed.",
+ "FUNGAL_TREATMENT": "Fungal risk. Recommend fungicide and lower humidity.",
+ "PEST_ISOLATION": "Pests detected. Recommend isolation and organic pesticide.",
+ "PRUNE_NECROTIC_LEAVES": "Necrosis detected. Recommend pruning dead matter."
+ }
+ return instructions.get(strategy_name, "Follow standard procedures.")
- # --- STEP 3: Synthesize History (Memory) ---
- history_context = "\n".join([
- f"- Previous Case (Score {f['score']:.2f}): {f['payload'].get('outcome', 'No outcome recorded')}"
- for f in similar_fmus
- ])
+ def reason(self, current_fmu, similar_fmus, sub_agent_outputs):
+ sensors = current_fmu['payload']['sensors']
+ fmu_vector = current_fmu.get('vector')
- # --- STEP 4: The Final Prompt ---
- system_prompt = """
- You are the Chief Supervisor AI of a Hydroponic Facility.
- Your goal: Synthesize conflicting data to recommend the OPTIMAL action.
+ if fmu_vector is None:
+ fmu_vector = np.zeros(512)
+
+ # 1. 🟢 GET CONTEXT
+ context_vector = self._build_context(fmu_vector, sensors)
- PRINCIPLES:
- 1. Plant Health is Priority #1.
- 2. Verify Sub-Agent claims against the SCIENTIFIC KNOWLEDGE provided.
- 3. If History contradicts Science, prefer Science (Manuals), but note the anomaly.
- """
+ # 2. 🟢 BANDIT DECISION (The "Will")
+ action_idx, debug_info = self.bandit.select_action(context_vector)
+ strategic_intent = STRATEGIES[action_idx]
+ specific_order = self._get_strategy_instruction(strategic_intent)
+
+ # 3. 🟢 IDENTIFY RELEVANT SPECIALIST (The "Physics")
+ if "NUTRIENT" in strategic_intent or "PH" in strategic_intent or "EC" in strategic_intent:
+ highlighted_report = sub_agent_outputs.get("nutrient_report", "No Report")
+ focus_area = "NUTRIENT SPECIALIST"
+ elif "TEMP" in strategic_intent or "HUMIDITY" in strategic_intent or "AIR" in strategic_intent:
+ highlighted_report = sub_agent_outputs.get("atmosphere_report", "No Report")
+ focus_area = "ATMOSPHERE SPECIALIST"
+ elif "PEST" in strategic_intent or "FUNGAL" in strategic_intent:
+ highlighted_report = "Visual analysis indicates bio-threats."
+ focus_area = "BIO-SECURITY"
+ else:
+ highlighted_report = "Standard operational check."
+ focus_area = "ALL SECTORS"
- user_message = f"""
- ### SITUATION REPORT
- Target: {crop} ({stage})
- Sensors: {current_fmu['payload']['sensors']}
+ # 4. 🟢 FORMAT HISTORY (The "Precedent") <--- NEW SECTION
+ history_context = "No relevant historical cases found."
+ if similar_fmus and len(similar_fmus) > 0:
+ history_lines = []
+ for i, fmu in enumerate(similar_fmus):
+ # Extract key details from the past record
+ past_action = fmu['payload'].get('action_taken', 'Unknown')
+ past_outcome = fmu['payload'].get('outcome', 'Unknown')
+ score = fmu.get('score', 0.0)
+ history_lines.append(f"- Case #{i+1} (Match: {score:.1%}): Action '{past_action}' -> Result: '{past_outcome}'")
+ history_context = "\n".join(history_lines)
- ### SUB-AGENT ALERTS
- {json.dumps(sub_agent_outputs, indent=2)}
+ # 5. 🟢 SYNTHESIS PROMPT
+ system_prompt = f"""
+ You are the Supervisor of a Hydroponic Farm.
+
+ --- 🚨 CHAIN OF COMMAND INSTRUCTIONS 🚨 ---
+
+ 1. STRATEGIC GOAL (From RL General):
+ "{strategic_intent}" -> "{specific_order}"
+ *This is your MANDATORY objective.*
- ### SCIENTIFIC KNOWLEDGE (Verified Manuals)
- {scientific_context}
+ 2. INTELLIGENCE REPORT (From {focus_area}):
+ "{highlighted_report}"
+ *Use these specific calculations (VPD, Lockout, etc.) to justify your plan.*
- ### HISTORICAL MEMORY (Similar Past Events)
- {history_context}
+ 3. HISTORICAL PRECEDENT (Retrieval Memory):
+ {history_context}
+ *Reference these past cases to support (or warn against) specific implementation details.*
- ### COMMAND
- Analyze the situation. Resolve conflicts between agents using the Manuals.
- Output JSON: {{ "reasoning": "...", "action": "...", "confidence": 0.0-1.0 }}
+ 4. FULL CONTEXT:
+ Other Reports: {json.dumps(sub_agent_outputs)}
+
+ --- YOUR TASK ---
+ Generate a specific action plan that executes the Strategic Goal.
+
+ CRITICAL: You must synthesize the RL Order, the Physics Report, and the History.
+ - If History shows the RL Strategy failed recently, mention that risk and propose a safer variation.
+ - If History confirms success, cite it to build confidence.
+
+ RESPONSE FORMAT (JSON):
+ {{
+ "decision": "Brief summary",
+ "reasoning": "Detailed synthesis of Logic + Physics + History...",
+ "risk_matrix": {{ "nutrients": 5, "climate": 5, "visuals": 5, "history": 5 }}
+ }}
"""
- # --- STEP 5: Execute Reasoning on Groq ---
try:
response = self.llm.chat.completions.create(
- # We use Llama-3.1-8b because it is fast and smart enough for this logic
- model="llama-3.1-8b-instant",
+ model="llama-3.1-8b-instant",
messages=[
{"role": "system", "content": system_prompt},
- {"role": "user", "content": user_message}
+ {"role": "user", "content": f"Sub-Agent Reports: {json.dumps(sub_agent_outputs)}"}
],
- temperature=0.1, # Low temp for strict logic
- response_format={"type": "json_object"} # Force valid JSON
+ response_format={"type": "json_object"}
)
- return json.loads(response.choices[0].message.content)
-
+ decision_json = json.loads(response.choices[0].message.content)
+
+ # 🟢 Attach Metadata for Training
+ decision_json["strategic_intent"] = strategic_intent
+ decision_json["bandit_action_idx"] = int(action_idx)
+
+ return decision_json
+
except Exception as e:
- return {"error": str(e), "reasoning": "Groq Connection Failed"}
-\ No newline at end of file
+ return {
+ "decision": "Error in reasoning",
+ "reasoning": str(e),
+ "strategic_intent": strategic_intent,
+ "bandit_action_idx": int(action_idx)
+ }
+\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
@@ -18,7 +18,7 @@ tqdm
fastembed
openai
pypdf
-
+groq
# --- OpenAI CLIP (Vision Encoder) ---
# This installs directly from GitHub because it's not on standard PyPI
diff --git a/web/app/upload/page.tsx b/web/app/upload/page.tsx
@@ -83,6 +83,7 @@ export default function UnifiedPage() {
}
setSearchResults(response.search_results || []);
+ console.log("Search Results:", response.search_results);
if (response.agent_decision) {
setDecision(response.agent_decision);
@@ -367,28 +368,7 @@ export default function UnifiedPage() {
)}
</div>
)}
- {/* 👆 END OF NEW COMPONENT 👆 */}
- {/* {decision && currentQueryId && (
- <div className="mt-4 bg-slate-900 p-4 rounded-xl border border-slate-700">
- <h4 className="text-white font-bold mb-2">Report Outcome</h4>
- <div className="flex gap-2">
- <button
- onClick={() => submitFeedback(currentQueryId, decision.action, "Effective")}
- className="px-4 py-2 bg-green-600 rounded text-sm hover:bg-green-500"
- >
- It Worked!
- </button>
- <button
- onClick={() => submitFeedback(currentQueryId, decision.action, "Ineffective")}
- className="px-4 py-2 bg-red-600 rounded text-sm hover:bg-red-500"
- >
- Failed
- </button>
- </div>
- </div>
-)} */}
-
-
+
{/* Bottom Section: Search Results */}
{searchResults.length > 0 && (
<div className="max-w-6xl w-full animate-in fade-in slide-in-from-bottom-10 duration-500">