commit 756e9917c24a850a845d0d097742187736f0bc28
parent a301b5fbe31e3215dee2e5f4dab6a2b544d880b7
Author: AbhinavRai01 <abhinavrai004@gmail.com>
Date: Sun, 1 Mar 2026 18:55:04 +0000
trying to connect shit up
Diffstat:
6 files changed, 367 insertions(+), 208 deletions(-)
diff --git a/agent/main_agent.py b/agent/main_agent.py
@@ -0,0 +1,95 @@
+import sys
+import os
+import requests
+import time
+from pathlib import Path
+
+# --- PATH SETUP ---
+current_dir = os.path.dirname(os.path.abspath(__file__))
+sys.path.append(current_dir)
+
+# Import Agents
+from sub_agents.fetching_agent import FetchingAgent
+from sub_agents.atmospheric_agent import AtmosphericAgent
+from sub_agents.water_agent import WaterAgent
+from sub_agents.Researcher import ResearcherAgent # Ensure this file exists
+from sub_agents.Supervisor import SupervisorAgent
+
+# Simulator Action URL
+SIMULATOR_ACTION_URL = "https://unexhumed-melaine-bouncingly.ngrok-free.dev/simulation/action"
+
+def main():
+ print("š Initializing Demeter Orchestrator...")
+
+ # 1. Instantiate All Agents
+ fetcher = FetchingAgent()
+ researcher = ResearcherAgent()
+ atmos_agent = AtmosphericAgent()
+ water_agent = WaterAgent()
+ supervisor = SupervisorAgent()
+
+ while True:
+ print("\n" + "="*50)
+ print("ā±ļø STARTING NEW CYCLE")
+ print("="*50)
+
+ # 2. Fetch Reality (Current State + History)
+ fmu, sensor_snapshot, history = fetcher.fetch_and_process()
+
+ if not fmu:
+ print("ā Fetch failed or Simulator offline. Retrying in 10s...")
+ time.sleep(10)
+ continue
+
+ # Extract Context
+ crop = fmu.metadata.get("crop", "unknown")
+ stage = fmu.metadata.get("stage", "unknown")
+
+ print(f"\n[Context] Crop: {crop} | Stage: {stage}")
+ print(f"[Context] Current Sensors: {sensor_snapshot}")
+
+ # 3. Get Research Knowledge
+ research_context = researcher.consult_knowledge_base(crop, stage)
+
+ # 4. Domain Agent Reasoning
+ # They analyze the SENSORS against the RESEARCH
+ print("\nš§ Domain Agents Deliberating...")
+
+ # Atmospheric Agent (Controls CO2, Light, Air Temp, Humidity)
+ atmos_plan = atmos_agent.reason(sensor_snapshot, research_context)
+ print(f" š Atmospheric Plan: {atmos_plan}")
+
+ # Water Agent (Controls pH, EC, Water Temp)
+ water_plan = water_agent.reason(sensor_snapshot, research_context)
+ print(f" š Water Plan: {water_plan}")
+
+ # 5. Supervisor Synthesis & Validation
+ # Merges plans and checks against HISTORY for safety
+ print("\nš® Supervisor Validating...")
+ final_action = supervisor.synthesize_plan(
+ atmos_plan,
+ water_plan,
+ fmu.metadata,
+ history
+ )
+
+ print(f"šÆ FINAL COMMAND: {final_action}")
+
+ # 6. Execute (Send to Simulator)
+ try:
+ print(f"š” Sending command to Simulator...")
+ resp = requests.post(SIMULATOR_ACTION_URL, json=final_action)
+
+ if resp.status_code == 200:
+ print("ā
Action accepted by Simulator.")
+ else:
+ print(f"ā ļø Simulator rejected action: {resp.status_code} - {resp.text}")
+ except Exception as e:
+ print(f"ā Connection error: {e}")
+
+ # Wait for next cycle
+ print("\nzzz Sleeping 15s...")
+ time.sleep(15)
+
+if __name__ == "__main__":
+ main()
+\ No newline at end of file
diff --git a/agent/sub_agents/Supervisor.py b/agent/sub_agents/Supervisor.py
@@ -1,98 +1,51 @@
-import json
import os
-from dotenv import load_dotenv
from openai import OpenAI
-# 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)
+MODEL_ID = "llama3-70b-8192"
+API_KEY = os.environ.get("GROQ_API_KEY")
class SupervisorAgent:
- def __init__(self, researcher_agent):
- self.researcher = researcher_agent
-
- # ā” 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
- )
-
- def reason(self, current_fmu, similar_fmus, sub_agent_outputs):
+ def __init__(self):
+ self.name = "Supervisor"
+ if not API_KEY:
+ self.client = None
+ else:
+ self.client = OpenAI(base_url="https://api.groq.com/openai/v1", api_key=API_KEY)
+
+ def synthesize_plan(self, atmos_plan: dict, water_plan: dict, current_meta: dict, history: list) -> dict:
"""
- The Core Reasoning Loop:
- 1. Contextualize -> 2. Research -> 3. Synthesize -> 4. Decide
- """
-
- # --- 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"
-
- print(f"š¤ Supervisor is asking Researcher: '{research_query}'")
-
- # --- STEP 2: The Researcher Fetches Evidence (RAG) ---
- # This now returns a clean STRING, not a list
- scientific_context = self.researcher.search(research_query)
-
- # --- 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
- ])
-
- # --- 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.
-
- 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.
- """
-
- user_message = f"""
- ### SITUATION REPORT
- Target: {crop} ({stage})
- Sensors: {current_fmu['payload']['sensors']}
-
- ### SUB-AGENT ALERTS
- {json.dumps(sub_agent_outputs, indent=2)}
-
- ### SCIENTIFIC KNOWLEDGE (Verified Manuals)
- {scientific_context}
-
- ### HISTORICAL MEMORY (Similar Past Events)
- {history_context}
-
- ### COMMAND
- Analyze the situation. Resolve conflicts between agents using the Manuals.
- Output JSON: {{ "reasoning": "...", "action": "...", "confidence": 0.0-1.0 }}
+ Takes plans from sub-agents and creates the final JSON payload for the Simulator.
"""
+ print(f"[{self.name}] š® Validating and merging plans...")
+
+ # 1. Merge the plans
+ # We start with the sub-agent recommendations
+ combined_action = {**atmos_plan, **water_plan}
+
+ # 2. Reasoning (Optional: Check for conflicts)
+ prompt = (
+ f"You are the Farm Supervisor.\n"
+ f"Proposed Atmospheric Actions: {atmos_plan}\n"
+ f"Proposed Water Actions: {water_plan}\n"
+ f"Current Crop: {current_meta.get('crop')} ({current_meta.get('stage')})\n"
+ f"Similar History: {history}\n\n"
+ f"TASK: Review these actions. If they are safe, merge them into a single JSON object.\n"
+ f"If there is a conflict (e.g., high Temp but low CO2), adjust them.\n"
+ f"OUTPUT: A clean JSON object strictly matching the simulator's expected action keys.\n"
+ f"Do not add markdown."
+ )
- # --- 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",
- messages=[
- {"role": "system", "content": system_prompt},
- {"role": "user", "content": user_message}
- ],
- temperature=0.1, # Low temp for strict logic
- response_format={"type": "json_object"} # Force valid JSON
+ # For speed, we might just return the combined dict,
+ # but here we ask the LLM to validate/format it.
+ response = self.client.chat.completions.create(
+ model=MODEL_ID,
+ messages=[{"role": "user", "content": prompt}]
)
- return json.loads(response.choices[0].message.content)
-
- except Exception as e:
- return {"error": str(e), "reasoning": "Groq Connection Failed"}
-\ No newline at end of file
+ content = response.choices[0].message.content.replace("```json", "").replace("```", "").strip()
+ import ast
+ final_json = ast.literal_eval(content)
+ return final_json
+ except Exception:
+ # Fallback: Just return the merged dicts
+ return combined_action
+\ No newline at end of file
diff --git a/agent/sub_agents/atmospheric_agent.py b/agent/sub_agents/atmospheric_agent.py
@@ -0,0 +1,49 @@
+import os
+from openai import OpenAI
+
+# Configuration
+MODEL_ID = "llama3-70b-8192"
+API_KEY = os.environ.get("GROQ_API_KEY")
+
+class AtmosphericAgent:
+ def __init__(self):
+ self.name = "Atmospheric Agent"
+ if not API_KEY:
+ print(f"[{self.name}] ā ļø GROQ_API_KEY missing.")
+ self.client = None
+ else:
+ self.client = OpenAI(base_url="https://api.groq.com/openai/v1", api_key=API_KEY)
+
+ def reason(self, current_state: dict, research_context: str) -> dict:
+ """
+ Decides on CO2, Light, Temp, Humidity changes.
+ Returns a dict of actions (e.g., {"CO2": 1200, "Light": 800})
+ """
+ print(f"[{self.name}] š¤ļø Analyzing Air conditions...")
+
+ prompt = (
+ f"You are the Atmospheric Control System.\n"
+ f"RESEARCH GUIDELINES:\n{research_context}\n\n"
+ f"CURRENT STATE:\n{current_state}\n\n"
+ f"TASK: Output a JSON dictionary ONLY of target values for 'co2' (ppm), 'light_intensity' (umol), "
+ f"'air_temp' (C), and 'humidity' (%).\n"
+ f"Example format: {{'co2': 1000, 'light_intensity': 600, 'air_temp': 24, 'humidity': 60}}\n"
+ f"Do not add markdown formatting or explanation."
+ )
+
+ if not self.client:
+ return {"co2": 400, "light_intensity": 500} # Defaults
+
+ try:
+ response = self.client.chat.completions.create(
+ model=MODEL_ID,
+ messages=[{"role": "user", "content": prompt}]
+ )
+ # Simple cleaning to ensure valid JSON
+ content = response.choices[0].message.content.replace("```json", "").replace("```", "").strip()
+ # In a real system, use json.loads(content) with error handling
+ import ast
+ return ast.literal_eval(content)
+ except Exception as e:
+ print(f"[{self.name}] Error: {e}")
+ return {}
+\ No newline at end of file
diff --git a/agent/sub_agents/base_agent.py b/agent/sub_agents/base_agent.py
@@ -0,0 +1,48 @@
+import os
+from openai import OpenAI
+
+# --- GROQ CONFIGURATION ---
+# Common Groq Models: "llama3-70b-8192", "mixtral-8x7b-32768"
+MODEL_ID = "llama3-70b-8192"
+API_KEY = os.environ.get("GROQ_API_KEY")
+
+class BaseReasoningAgent:
+ def __init__(self, name):
+ self.name = name
+
+ # ā” Connect to Groq via OpenAI Client
+ if not API_KEY:
+ print(f"[{self.name}] ā ļø WARNING: GROQ_API_KEY not found in environment.")
+ self.client = None
+ else:
+ try:
+ self.client = OpenAI(
+ base_url="https://api.groq.com/openai/v1",
+ api_key=API_KEY
+ )
+ except Exception as e:
+ print(f"[{self.name}] ā ļø Groq Connection Error: {e}")
+ self.client = None
+
+ def _call_llm(self, prompt):
+ """
+ Helper method to send prompts to Groq Cloud.
+ """
+ if not self.client:
+ return "Error: LLM Client not connected (Check API Key)."
+
+ try:
+ # Groq/OpenAI Chat Completion Structure
+ response = self.client.chat.completions.create(
+ model=MODEL_ID,
+ messages=[
+ {"role": "system", "content": f"You are the {self.name} Agent for a high-tech hydroponic farm."},
+ {"role": "user", "content": prompt}
+ ],
+ temperature=0.6, # Slightly lower temp for more stable control decisions
+ max_tokens=1024
+ )
+ return response.choices[0].message.content
+
+ except Exception as e:
+ return f"Reasoning Error: {e}"
+\ No newline at end of file
diff --git a/agent/sub_agents/fetching_agent.py b/agent/sub_agents/fetching_agent.py
@@ -5,9 +5,8 @@ import json
from pathlib import Path
# --- PATH FIX ---
-# Ensures Python can find 'Sentinel' and 'Qdrant' folders
current_file = Path(__file__).resolve()
-project_root = current_file.parent.parent
+project_root = current_file.parent.parent.parent # Adjusted for sub_agents nesting
sys.path.append(str(project_root))
# ----------------
@@ -16,25 +15,83 @@ from Sentinel.agent import FMUBuilder
from Qdrant.Store import store_fmu, COLLECTION_NAME
from Qdrant.Client import client
-# ==========================================
-# šµļø HISTORIAN AGENT
-# ==========================================
-class HistorianAgent:
- def __init__(self):
- self.client = client # Use the shared Qdrant client
- self.collection = COLLECTION_NAME
+class FetchingAgent:
+ def __init__(self, simulator_url="https://unexhumed-melaine-bouncingly.ngrok-free.dev/simulation/state"):
+ self.sim_url = simulator_url
+ self.builder = FMUBuilder()
- def consult_history(self, fmu):
- """
- Takes an FMU, extracts its metadata/vector, and searches
- for similar past instances with the same Crop & Stage.
- """
- target_crop = fmu.metadata.get("crop")
- target_stage = fmu.metadata.get("stage")
+ def fetch_and_process(self):
+ print(f"[Fetcher] š” Requesting data from {self.sim_url}...")
+
+ try:
+ response = requests.get(self.sim_url)
+
+ if response.status_code == 200:
+ data = response.json()
+
+ # --- 1. EXTRACT RAW DATA ---
+ window_data = data.get("sensor_window", {})
+ image_b64 = data.get("image", "")
+ raw_meta = data.get("metadata", {})
+
+ # --- 2. FILTER SENSORS (Strictly the 4 requested) ---
+ wanted_keys = {
+ "ph": "pH",
+ "ec": "EC",
+ "humidity": "humidity",
+ "temp": "temp",
+ "air_temp": "temp"
+ }
+
+ sensor_snapshot = {}
+ for key, value_list in window_data.items():
+ key_lower = key.lower()
+ if key_lower in wanted_keys:
+ out_name = wanted_keys[key_lower]
+ val = value_list[-1] if isinstance(value_list, list) and value_list else 0.0
+ if out_name not in sensor_snapshot:
+ sensor_snapshot[out_name] = val
+
+ # --- 3. FILTER METADATA (Crop & Stage) ---
+ filtered_metadata = {
+ "crop": raw_meta.get("crop", "unknown"),
+ "stage": raw_meta.get("stage", "unknown")
+ }
+
+ # --- 4. CREATE FMU ---
+ fmu = self.builder.create_fmu(image_b64, sensor_snapshot, filtered_metadata)
+
+ print(f"\n[Fetcher] š§ FMU Created (ID: {fmu.id})")
+ print(f"[Fetcher] Sensors: {sensor_snapshot}")
+
+ # --- 5. STORE IN QDRANT ---
+ store_fmu(fmu)
+
+ # --- 6. HISTORIAN SEARCH (Integrated) ---
+ search_results = self.find_similar_instances(fmu)
+
+ print(f"[Historian] š Found {len(search_results)} similar past events.")
+
+ # RETURN EVERYTHING THE ORCHESTRATOR NEEDS
+ return fmu, sensor_snapshot, search_results
+ else:
+ print(f"[Fetcher] ā Error: Simulator returned {response.status_code}")
+ return None, None, None
- print(f"\n[Historian] š Consulting archives for {target_crop} ({target_stage})...")
+ except Exception as e:
+ print(f"[Fetcher] ā Critical Error: {e}")
+ import traceback
+ traceback.print_exc()
+ return None, None, None
- # 1. Create Context Filter
+ def find_similar_instances(self, current_fmu):
+ """
+ Uses the current FMU's vector and metadata to filter and search Qdrant.
+ """
+ target_crop = current_fmu.metadata.get("crop")
+ target_stage = current_fmu.metadata.get("stage")
+
+ # Create Filter
context_filter = models.Filter(
must=[
models.FieldCondition(key="crop", match=models.MatchValue(value=target_crop)),
@@ -42,111 +99,21 @@ class HistorianAgent:
]
)
- # 2. Search Qdrant
try:
- # We use the vector from the FMU directly
- results = self.client.search(
- collection_name=self.collection,
- query_vector=fmu.vector,
+ # Use the vector we just generated
+ response = client.search(
+ collection_name=COLLECTION_NAME,
+ query_vector=current_fmu.vector,
query_filter=context_filter,
- limit=3,
- with_payload=True
- )
- except Exception as e:
- print(f"[Historian] ā ļø Search Error: {e}. Trying unfiltered search...")
- # Fallback if filters fail (e.g. missing payload index)
- results = self.client.search(
- collection_name=self.collection,
- query_vector=fmu.vector,
- limit=3,
+ limit=5,
with_payload=True
)
-
- # 3. Report Results
- if not results:
- print("[Historian] 𤷠No similar history found.")
- return []
-
- print(f"[Historian] ā
Found {len(results)} matches:")
- formatted_history = []
- for hit in results:
- print(f" - ID: {hit.id} | Score: {hit.score:.4f}")
- formatted_history.append(hit.payload)
-
- return formatted_history
-
-
-# ==========================================
-# š” FETCHING AGENT
-# ==========================================
-class FetchingAgent:
- def __init__(self, simulator_url="https://unexhumed-melaine-bouncingly.ngrok-free.dev/simulation/state"):
- self.sim_url = simulator_url
- self.builder = FMUBuilder()
-
- def run_cycle(self):
- print(f"\n[Fetcher] š” Requesting data from {self.sim_url}...")
-
- try:
- response = requests.get(self.sim_url)
-
- if response.status_code != 200:
- print(f"[Fetcher] ā Error: Simulator returned {response.status_code}")
- return None
-
- data = response.json()
-
- # --- 1. Filter Sensors (pH, EC, Temp, Humidity) ---
- window_data = data.get("sensor_window", {})
- wanted_keys = {"ph": "pH", "ec": "EC", "humidity": "humidity", "temp": "temp", "air_temp": "temp"}
-
- sensor_snapshot = {}
- for key, value_list in window_data.items():
- key_lower = key.lower()
- if key_lower in wanted_keys:
- out_name = wanted_keys[key_lower]
- val = value_list[-1] if isinstance(value_list, list) and value_list else 0.0
- if out_name not in sensor_snapshot:
- sensor_snapshot[out_name] = val
-
- # --- 2. Filter Metadata (Crop, Stage) ---
- raw_meta = data.get("metadata", {})
- filtered_metadata = {
- "crop": raw_meta.get("crop", "unknown"),
- "stage": raw_meta.get("stage", "unknown")
- }
-
- # --- 3. Create FMU ---
- image_b64 = data.get("image", "")
- fmu = self.builder.create_fmu(image_b64, sensor_snapshot, filtered_metadata)
-
- print(f"[Fetcher] š§ FMU Created (ID: {fmu.id})")
- print(f"[Fetcher] Sensors: {sensor_snapshot}")
-
- # --- 4. Store in DB ---
- store_fmu(fmu)
+ return [{"id": hit.id, "score": hit.score, "payload": hit.payload} for hit in response]
- return fmu
-
except Exception as e:
- print(f"[Fetcher] ā Critical Error: {e}")
- return None
-
+ print(f"ā ļø Search Warning: {e}. Returning empty history.")
+ return []
-# ==========================================
-# š¬ LOCAL ORCHESTRATION
-# ==========================================
if __name__ == "__main__":
- # 1. Instantiate Agents
- fetcher = FetchingAgent()
- historian = HistorianAgent()
-
- # 2. Run the Loop
- print("--- Starting Combined Agent Cycle ---")
-
- # Step A: Fetch & Process
- current_fmu = fetcher.run_cycle()
-
- # Step B: Consult History (if Fetch was successful)
- if current_fmu:
- historian.consult_history(current_fmu)
-\ No newline at end of file
+ agent = FetchingAgent()
+ agent.fetch_and_process()
+\ No newline at end of file
diff --git a/agent/sub_agents/water_agent.py b/agent/sub_agents/water_agent.py
@@ -0,0 +1,43 @@
+import os
+from openai import OpenAI
+
+MODEL_ID = "llama3-70b-8192"
+API_KEY = os.environ.get("GROQ_API_KEY")
+
+class WaterAgent:
+ def __init__(self):
+ self.name = "Water Agent"
+ if not API_KEY:
+ self.client = None
+ else:
+ self.client = OpenAI(base_url="https://api.groq.com/openai/v1", api_key=API_KEY)
+
+ def reason(self, current_state: dict, research_context: str) -> dict:
+ """
+ Decides on pH, EC, Water Temp changes.
+ """
+ print(f"[{self.name}] š§ Analyzing Water solution...")
+
+ prompt = (
+ f"You are the Water Chemistry System.\n"
+ f"RESEARCH GUIDELINES:\n{research_context}\n\n"
+ f"CURRENT STATE:\n{current_state}\n\n"
+ f"TASK: Output a JSON dictionary ONLY of target values for 'ph', 'ec' (dS/m), and 'water_temp' (C).\n"
+ f"Example format: {{'ph': 5.8, 'ec': 1.5, 'water_temp': 20}}\n"
+ f"Do not add markdown formatting."
+ )
+
+ if not self.client:
+ return {"ph": 6.0, "ec": 1.2}
+
+ try:
+ response = self.client.chat.completions.create(
+ model=MODEL_ID,
+ messages=[{"role": "user", "content": prompt}]
+ )
+ content = response.choices[0].message.content.replace("```json", "").replace("```", "").strip()
+ import ast
+ return ast.literal_eval(content)
+ except Exception as e:
+ print(f"[{self.name}] Error: {e}")
+ return {}
+\ No newline at end of file