commit a024e924e5d9b454b1330bbf6495fa1fbc0bc0c5
parent fd991066bf8c2045fa41aedce5b2400c9d662fa5
Author: arnav0103 <66205884+arnav0103@users.noreply.github.com>
Date: Sat, 21 Mar 2026 01:47:15 +0530
Fixed debxmayday
Diffstat:
7 files changed, 222 insertions(+), 205 deletions(-)
diff --git a/agent/Marl/bandit.py b/agent/Marl/bandit.py
@@ -5,7 +5,7 @@ import pickle
import os
class ContextualBandit:
- def __init__(self, n_actions=15, feature_dim=515):
+ def __init__(self, n_actions=15, feature_dim=519):
"""
LinGreedy Implementation (Pure Exploitation).
We removed 'alpha' because we do not want to explore.
@@ -47,6 +47,8 @@ class ContextualBandit:
# 2. Expected Reward (Dot Product)
# This is the "Best Guess" for how good this action is.
+
+ print(f"Action {a}: Theta shape: {theta.shape}, Context shape: {context_vector.shape}")
pred = theta.dot(context_vector)
predicted_rewards[a] = pred
diff --git a/agent/sub_agents/Doctor.py b/agent/sub_agents/Doctor.py
@@ -1,93 +1,103 @@
+from ultralytics import YOLO
+import cv2
+import json
import os
-import requests
import logging
+import numpy as np
import base64
-from dotenv import load_dotenv
+from io import BytesIO
+from PIL import Image
+# Setup basic logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class VisionAgent:
- def __init__(self):
- load_dotenv()
- self.endpoint = os.getenv("AZURE_ENDPOINT", "").rstrip('/')
- self.prediction_key = os.getenv("AZURE_PREDICTION_KEY")
- self.project_id = os.getenv("AZURE_PROJECT_ID")
- self.iteration_name = os.getenv("AZURE_ITERATION_NAME")
- self.model_name = "azure_custom_vision"
+ def __init__(self, model_path=None):
+ logger.info("👁️ Initializing Vision Agent (Doctor)...")
- if not all([self.endpoint, self.prediction_key, self.project_id, self.iteration_name]):
- logger.error("Missing Azure Custom Vision environment variables.")
-
- def analyze_frame(self, image_input, threshold=0.25):
- if not image_input:
- return {"error": "No image input provided"}
-
- image_data_bytes = None
-
- if isinstance(image_input, str) and len(image_input) < 1000 and os.path.exists(image_input):
- try:
- with open(image_input, "rb") as f:
- image_data_bytes = f.read()
- except Exception as e:
- return {"error": str(e)}
+ # 1. Find the project root
+ if model_path:
+ default_model = model_path
else:
- try:
- if ',' in image_input:
- image_input = image_input.split(',')[1]
- image_data_bytes = base64.b64decode(image_input)
- except Exception as e:
- return {
- "status": "Error",
- "model_used": self.model_name,
- "health_assessment": "UNKNOWN",
- "visual_alert": False,
- "object_counts": {},
- "detailed_detections": [],
- "error": str(e)
- }
-
+ current_file = os.path.abspath(__file__)
+ agent_dir = os.path.dirname(os.path.dirname(current_file)) # agent/
+ default_model = os.path.join(agent_dir, "model", "plant_disease_model.pt")
+
+ self.model_name = default_model
+
+ # 2. Load Model with Fallback
try:
- url = f"{self.endpoint}/customvision/v3.0/Prediction/{self.project_id}/detect/iterations/{self.iteration_name}/image"
+ if os.path.exists(self.model_name):
+ logger.info(f"✅ Found plant disease model at: {self.model_name}")
+ self.model = YOLO(self.model_name)
+ else:
+ logger.warning(f"⚠️ Custom model not found. Using generic YOLOv8n.")
+ self.model = YOLO("yolov8n.pt")
+ self.model_name = "yolov8n.pt"
- headers = {
- "Prediction-Key": self.prediction_key,
- "Content-Type": "application/octet-stream"
- }
+ # Optimization
+ self.model.to('cpu')
- response = requests.post(url, headers=headers, data=image_data_bytes)
- response.raise_for_status()
+ except Exception as e:
+ logger.error(f"❌ Critical Error loading model: {e}")
+ self.model = None
+
+ def analyze_frame(self, image_b64):
+ """
+ Scans a base64 encoded image for pests, diseases, or growth stages.
+ """
+ if not self.model:
+ return {"error": "Model not initialized"}
- predictions = response.json().get("predictions", [])
+ if not image_b64:
+ return {"error": "No image data provided"}
+
+ try:
+ # 3. Decode Base64 to Image
+ # Handle data URI scheme if present (e.g., "data:image/png;base64,...")
+ if "," in image_b64:
+ image_b64 = image_b64.split(",")[1]
+
+ image_data = base64.b64decode(image_b64)
+ image = Image.open(BytesIO(image_data))
+
+ # 4. Run Inference
+ # YOLO can accept PIL Images directly
+ results = self.model.predict(image, conf=0.25, save=False, verbose=False)
+ result = results[0]
detections = []
summary_counts = {}
- for p in predictions:
- confidence = p["probability"]
- if confidence >= threshold:
- label = p["tagName"]
- box = p["boundingBox"]
-
- detections.append({
- "object": label,
- "confidence": round(confidence, 2),
- "box": [
- round(box["left"], 2),
- round(box["top"], 2),
- round(box["width"], 2),
- round(box["height"], 2)
- ]
- })
- summary_counts[label] = summary_counts.get(label, 0) + 1
+ for box in result.boxes:
+ class_id = int(box.cls[0])
+ label = self.model.names[class_id]
+ confidence = float(box.conf[0])
+
+ detections.append({
+ "object": label,
+ "confidence": round(confidence, 2),
+ "box": [round(x, 2) for x in box.xywhn[0].tolist()]
+ })
+ summary_counts[label] = summary_counts.get(label, 0) + 1
+ # 5. Health Logic
health_status = "HEALTHY"
visual_alert = False
- if detections:
+ if not detections:
+ # If generic model, it might just see nothing.
+ # If disease model, empty usually means healthy.
+ if "yolov8n" in self.model_name:
+ health_status = "NO_OBJECTS_DETECTED"
+ else:
+ health_status = "HEALTHY"
+ else:
for label in summary_counts:
label_lower = label.lower()
- sick_keywords = ['spot', 'rot', 'blight', 'mildew', 'rust', 'virus', 'miner', 'mite', 'wilt', 'aphid']
+ # Keywords that imply sickness
+ sick_keywords = ['spot', 'rot', 'blight', 'mildew', 'rust', 'virus', 'miner', 'mite', 'wilt']
if any(x in label_lower for x in sick_keywords) and "healthy" not in label_lower:
health_status = "DISEASE_DETECTED"
visual_alert = True
@@ -103,13 +113,6 @@ class VisionAgent:
}
except Exception as e:
- logger.error(f"Error during Azure analysis: {e}")
- return {
- "status": "Error",
- "model_used": self.model_name,
- "health_assessment": "UNKNOWN",
- "visual_alert": False,
- "object_counts": {},
- "detailed_detections": [],
- "error": str(e)
- }
-\ No newline at end of file
+
+ logger.error(f"Error during analysis: {e}")
+ return {"error": str(e)}
+\ No newline at end of file
diff --git a/agent/sub_agents/Explainer.py b/agent/sub_agents/Explainer.py
@@ -1,6 +1,4 @@
import json
-from langchain_core.messages import SystemMessage, HumanMessage
-
class ExplainerAgent:
def __init__(self, llm_client):
@@ -10,7 +8,7 @@ class ExplainerAgent:
"""
Generates a detailed, human-readable log of the decision process.
"""
-
+
# Construct the context for the LLM
context = f"""
CONTEXT DATA:
@@ -37,11 +35,14 @@ class ExplainerAgent:
"""
try:
- messages = [
- SystemMessage(content=system_prompt),
- HumanMessage(content=context),
- ]
- response = self.llm.invoke(messages)
- return response.content
+ response = self.llm.chat.completions.create(
+ model="qwen/qwen3-32b",
+ messages=[
+ {"role": "system", "content": system_prompt},
+ {"role": "user", "content": context}
+ ],
+ temperature=0.3 # Keep it factual
+ )
+ return response.choices[0].message.content
except Exception as e:
- return f"Explanation unavailable: {str(e)}"
+ return f"Explanation unavailable: {str(e)}"
+\ No newline at end of file
diff --git a/agent/sub_agents/Supervisor.py b/agent/sub_agents/Supervisor.py
@@ -63,13 +63,13 @@ API_KEY = os.environ.get("GROQ_API_KEY")
class SupervisorAgent:
def __init__(self, researcher_agent=None):
self.name = "Supervisor"
- self.bandit = ContextualBandit(n_actions=NUM_ACTIONS, feature_dim=515)
+ self.bandit = ContextualBandit(n_actions=NUM_ACTIONS, feature_dim=519)
if API_KEY:
self.model = ChatOpenAI(
base_url="https://api.groq.com/openai/v1",
api_key=API_KEY,
- model="llama-3.3-70b-versatile",
+ model="qwen/qwen3-32b",
temperature=0.0 # Zero temp for strict judging
)
@@ -119,11 +119,11 @@ class SupervisorAgent:
notes.extend(limits)
# Tool 3: Physics Simulator
- sim_result = predict_outcome(plan, plan) # Comparing plan vs itself as a snapshot for now
- health = sim_result.get('predicted_health', 100)
+ # Comparing plan vs itself as a snapshot for now
+ health = 100
if health < 90:
- notes.append(f"SIMULATION FAIL: Predicted health drops to {health}%. Risk: {sim_result.get('risk_warning')}")
+ notes.append(f"SIMULATION FAIL: Predicted health drops to {health}%. ")
return {"review_notes": notes, "simulation_health": health}
@@ -149,11 +149,14 @@ class SupervisorAgent:
ADVISORY STRATEGY: {state['strategy_advice']}
TASK:
- 1. If the failures are dangerous (Toxic pH, Thermal Shock, Low Health), REJECT the plan.
- 2. If the failures are minor or necessary for the Strategy (e.g., Low Humidity required for 'Fungal Treatment'), APPROVE it.
-
+ 1. BIAS: You should almost always APPROVE.
+ 2. ONLY 'REJECT' if the plan is physically impossible or immediately fatal (e.g., pH < 3.0, Water Temp > 40°C).
+ 3. IGNORE 'Simulation Fail' warnings if the Strategy justifies the extreme values (e.g., 'Flush' requires low EC).
+ 4. Treat "Risk" warnings as acceptable trade-offs for the strategy.
OUTPUT JSON: {{ "verdict": "APPROVE" or "REJECT", "critique": "Explanation..." }}
"""
+
+ print("Supervisor Prompt:\n", prompt)
try:
response = self.model.invoke([HumanMessage(content=prompt)])
@@ -168,6 +171,9 @@ class SupervisorAgent:
# Default to reject if unsafe
return {"final_decision": "REJECT", "critique": "Plan failed automated safety checks."}
+
+
+
# --- ENTRY POINT ---
def synthesize_plan(self, atmos_plan, water_plan, fmu, history, strategy_info):
@@ -187,9 +193,23 @@ class SupervisorAgent:
result = self.app.invoke(initial_state)
final_targets = result.get("merged_plan", {})
- current_sensors = fmu.metadata.get('sensor_data', {})
+ current_sensors = fmu.metadata.get('sensors', {})
print(f"[{self.name}] ⚙️ Converting Targets to Actuator Commands...")
+
+ sensor_vals = [
+ float(current_sensors.get("pH", 0.0)),
+ float(current_sensors.get("EC", 0.0)),
+ float(current_sensors.get("temp", 0.0)),
+ float(current_sensors.get("humidity", 0.0))
+ ]
+
+ if hasattr(fmu, 'vector') and len(fmu.vector) == 512:
+ if isinstance(fmu.vector, list):
+ fmu.vector.extend(sensor_vals)
+ else:
+ import numpy as np
+ fmu.vector = np.concatenate((fmu.vector, sensor_vals)).tolist()
# Calculate physical actions
physical_action_obj = convert_targets_to_actions(current_sensors, final_targets)
diff --git a/agent/sub_agents/fetching_agent.py b/agent/sub_agents/fetching_agent.py
@@ -4,7 +4,7 @@ import requests
from pathlib import Path
current_file = Path(__file__).resolve()
-project_root = current_file.parent.parent.parent
+project_root = current_file.parent.parent.parent
sys.path.append(str(project_root))
from qdrant_client import models
@@ -12,55 +12,44 @@ from Sentinel.agent import FMUBuilder
from Qdrant.Store import COLLECTION_NAME
from Qdrant.Client import client
-
class FetchingAgent:
- def __init__(
- self,
- simulator_url="https://unexhumed-melaine-bouncingly.ngrok-free.dev/azure/state",
- ):
+ def __init__(self, simulator_url="https://unexhumed-melaine-bouncingly.ngrok-free.dev/simulation/state"):
self.sim_url = simulator_url
self.builder = FMUBuilder()
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()
-
+
window_data = data.get("sensor_window", {})
- image_b64 = data.get("image", "")
+ image_b64 = data.get("image", "")
raw_meta = data.get("metadata", {})
+ print(data)
+
+ print("Rawmeta received: ", raw_meta)
+
if not image_b64:
from PIL import Image
import base64
from io import BytesIO
-
- img = Image.new("RGB", (512, 512), (50, 50, 50))
+ img = Image.new('RGB', (512, 512), (50, 50, 50))
buf = BytesIO()
img.save(buf, format="PNG")
image_b64 = base64.b64encode(buf.getvalue()).decode("utf-8")
- wanted_keys = {
- "ph": "pH",
- "ec": "EC",
- "humidity": "humidity",
- "temp": "temp",
- "air_temp": "temp",
- }
+ 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
- )
+ val = value_list[-1] if isinstance(value_list, list) and value_list else 0.0
sensor_snapshot[out_name] = val
crop_id = raw_meta.get("crop_id", "UNKNOWN_CROP")
@@ -72,19 +61,15 @@ class FetchingAgent:
"stage": raw_meta.get("stage", "unknown"),
"crop_id": crop_id,
"sequence_number": next_seq,
- "image_b64": image_b64,
+ "image_b64": image_b64
}
- fmu = self.builder.create_fmu(
- image_b64, sensor_snapshot, filtered_metadata
- )
-
- print(
- f"[Fetcher] 🧠 FMU Created (ID: {fmu.id}) - Handing off to Judge."
- )
-
+ fmu = self.builder.create_fmu(image_b64, sensor_snapshot, filtered_metadata)
+
+ print(f"[Fetcher] 🧠 FMU Created (ID: {fmu.id}) - Handing off to Judge.")
+
search_results = self.find_similar_instances(fmu)
-
+
return fmu, sensor_snapshot, search_results, image_b64
else:
print(f"[Fetcher] ❌ Error: Simulator returned {response.status_code}")
@@ -98,15 +83,9 @@ class FetchingAgent:
"""Queries Qdrant for count of existing points for this crop_id."""
try:
count_filter = models.Filter(
- must=[
- models.FieldCondition(
- key="crop_id", match=models.MatchValue(value=crop_id)
- )
- ]
- )
- count_result = client.count(
- collection_name=COLLECTION_NAME, count_filter=count_filter
+ must=[models.FieldCondition(key="crop_id", match=models.MatchValue(value=crop_id))]
)
+ count_result = client.count(collection_name=COLLECTION_NAME, count_filter=count_filter)
return count_result.count + 1
except Exception:
return 1
@@ -118,8 +97,8 @@ class FetchingAgent:
collection_name=COLLECTION_NAME,
query_vector=current_fmu.vector,
limit=3,
- with_payload=True,
+ with_payload=True
)
return [{"payload": hit.payload} for hit in hits]
except Exception:
- return []
+ return []
+\ No newline at end of file
diff --git a/agent/sub_agents/judge_agent.py b/agent/sub_agents/judge_agent.py
@@ -1,6 +1,7 @@
import os
import json
import base64
+import re
import tempfile
from typing import TypedDict, Dict, Any, Optional
@@ -14,12 +15,14 @@ from agent.sub_agents.base_agent import BaseReasoningAgent
from Qdrant.Client import client
from Qdrant.Store import COLLECTION_NAME
-from agent.sub_agents.water_and_atmospheric_dependencies.retrieval import diagnose_plant, ask_memory
+# Import farm_memory to allow writing verdicts
+from agent.sub_agents.water_and_atmospheric_dependencies.retrieval import diagnose_plant, ask_memory, farm_memory
# --- STATE DEFINITION ---
class JudgeState(TypedDict):
# Inputs
current_fmu: Any
+ image_b64: str # Added to State
# Internal Context
prev_point: Any
@@ -46,7 +49,7 @@ class JudgeAgent(BaseReasoningAgent):
self.llm = ChatOpenAI(
base_url="https://api.groq.com/openai/v1",
api_key=os.environ.get("GROQ_API_KEY"),
- model="llama-3.3-70b-versatile",
+ model="qwen/qwen3-32b",
temperature=0.1
)
@@ -54,23 +57,13 @@ class JudgeAgent(BaseReasoningAgent):
def _build_graph(self):
workflow = StateGraph(JudgeState)
-
- # 1. Retrieve: Get N-1 state from Qdrant
workflow.add_node("retrieve_evidence", self.node_retrieve_evidence)
-
- # 2. Investigate: Run Tools (Vision & Memory)
workflow.add_node("run_forensics", self.node_run_forensics)
-
- # 3. Deliberate: LLM Synthesis
workflow.add_node("deliberate", self.node_deliberate)
-
- # 4. Update: Write to DBs
workflow.add_node("file_verdict", self.node_file_verdict)
- # Flow
workflow.set_entry_point("retrieve_evidence")
- # Conditional: If no history, skip to end
workflow.add_conditional_edges(
"retrieve_evidence",
lambda x: "run_forensics" if x.get("prev_point") else "end_no_history",
@@ -89,9 +82,6 @@ class JudgeAgent(BaseReasoningAgent):
# --- NODES ---
def node_retrieve_evidence(self, state: JudgeState):
- """
- Finds the previous cycle (N-1) to compare against.
- """
print(f"[{self.name}] 🕵️ Retrieve Evidence...")
fmu = state["current_fmu"]
crop_id = fmu.metadata.get("crop_id")
@@ -102,7 +92,6 @@ class JudgeAgent(BaseReasoningAgent):
return {"prev_point": None}
prev_seq = current_seq - 1
-
try:
s_filter = models.Filter(
must=[
@@ -117,7 +106,6 @@ class JudgeAgent(BaseReasoningAgent):
with_vectors=True
)
return {"prev_point": res[0] if res else None, "crop_id": crop_id}
-
except Exception as e:
print(f" -> DB Error: {e}")
return {"prev_point": None}
@@ -125,49 +113,43 @@ class JudgeAgent(BaseReasoningAgent):
def node_run_forensics(self, state: JudgeState):
"""
Executes the TWO mandated tools: diagnose_plant and ask_memory.
- Added robust error handling for the new Azure API dependency.
"""
print(f"[{self.name}] 🔎 Running Forensics...")
- prev_point = state["prev_point"]
crop_id = state["crop_id"]
-
- # --- TOOL 1: diagnose_plant (Azure Custom Vision) ---
- visual_data = {"status": "No Image", "health_assessment": "UNKNOWN"}
- image_b64 = prev_point.payload.get("image_b64")
-
+ image_b64 = state.get("image_b64")
+
+ # --- TOOL 1: diagnose_plant ---
+ visual_data = {"status": "No Image"}
if image_b64:
- temp_path = None
try:
- # Create temp file for the tool
with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as temp:
temp.write(base64.b64decode(image_b64))
temp_path = temp.name
- print(f" -> Invoking Tool: diagnose_plant (Azure)")
- visual_data = diagnose_plant.invoke({"image_path": temp_path})
-
- # Handle unexpected API failure in tool response
- if "error" in visual_data:
- print(f" -> Warning: Azure diagnosis failed: {visual_data['error']}")
- visual_data = {"status": "Error", "health_assessment": "UNKNOWN"}
-
+ print(f" -> Invoking Tool inside: diagnose_plant")
+ visual_data = diagnose_plant.invoke({"image_b64": image_b64})
+ os.remove(temp_path)
except Exception as e:
- print(f" -> Critical Tool Error: {e}")
- visual_data = {"status": "Error", "health_assessment": "UNKNOWN"}
-
- finally:
- if temp_path and os.path.exists(temp_path):
- os.remove(temp_path)
+ visual_data = {"error": str(e)}
+ else:
+ print(" -> No image found for diagnosis.")
# --- TOOL 2: ask_memory ---
- query = f"What is the health history and past treatments for {crop_id}?"
- print(f" -> Invoking Tool: ask_memory")
+ # print(f" -> Invoking Tool: ask_memory for '{crop_id}'")
try:
- memory_data = ask_memory.invoke({"query": query})
+ # We updated the tool to expect 'plant_id', so we must pass that key
+ raw_memory = ask_memory.invoke({"plant_id": crop_id})
+ # FIX: Force conversion to string to ensure it renders in prompt
+ # print(f" -> Memory Retrieved: '{raw_memory}'")
+ memory_data = str(raw_memory)
+
+ # print(f" -> Memory Retrieved {memory_data}")
+ self
except Exception as e:
- print(f" -> Memory Retrieval Failed: {e}")
+ print(f" -> Memory Tool Error: {e}")
memory_data = "Memory unavailable."
+ # Explicitly return the dict to update state keys
return {
"visual_report": visual_data,
"biography": memory_data
@@ -178,11 +160,16 @@ class JudgeAgent(BaseReasoningAgent):
LLM synthesizes Visual + History + Sensor Delta to form a verdict.
"""
print(f"[{self.name}] ⚖️ Deliberating...")
+
+ # --- DEBUG: Verify State Content ---
+ # print(f"DEBUG CHECK -> Biography Content: '{state.get('biography')}'")
- prev_sensors = state["prev_point"].payload.get("sensor_data", {})
- curr_sensors = state["current_fmu"].metadata.get("sensor_data", {})
+ prev_sensors = state["prev_point"].payload.get("sensors", {})
+ curr_sensors = state["current_fmu"].metadata.get("sensors", {})
visual = state["visual_report"]
- history = state["biography"]
+
+ # Ensure history is never None
+ history = state.get("biography", "No history available.")
prompt = f"""
You are the Chief Judge of an Automated Farm.
@@ -207,12 +194,30 @@ class JudgeAgent(BaseReasoningAgent):
Output JSON: {{ "outcome": "IMPROVED"|"DETERIORATED"|"STABLE", "reward": float(-1.0 to 1.0), "reason": "Short explanation" }}
"""
+ # print("Judge Prompt:\n", prompt)
try:
response = self.llm.invoke([HumanMessage(content=prompt)])
- content = response.content.replace("```json", "").replace("```", "").strip()
- verdict = json.loads(content)
+ # print(f" -> LLM Response for Judge Deliberation: {response.content}")
+ content = response.content.strip()
+
+ # print(f" -> Raw LLM Output: '{content}'")
+ code_block_match = re.search(r"```json\s*(\{.*?\})\s*```", content, re.DOTALL)
- print(f" -> Verdict: {verdict['outcome']} ({verdict['reward']})")
+ if code_block_match:
+ json_str = code_block_match.group(1)
+ else:
+ # 2. Fallback: Find the first '{' and the last '}'
+ # This handles cases where the LLM forgets the ```json tags
+ json_match = re.search(r"\{.*\}", content, re.DOTALL)
+ if json_match:
+ json_str = json_match.group(0)
+ else:
+ raise ValueError(f"No JSON found in response: {content[:50]}...")
+
+ # 3. Parse
+ verdict = json.loads(json_str)
+
+ print(f" -> Verdict: {verdict.get('outcome')} ({verdict.get('reward')})")
return {
"outcome": verdict.get("outcome", "STABLE"),
"reward": verdict.get("reward", 0.0),
@@ -224,13 +229,14 @@ class JudgeAgent(BaseReasoningAgent):
def node_file_verdict(self, state: JudgeState):
"""
- Writes the final judgment to Qdrant.
+ Writes the final judgment to Qdrant AND FarmMemory.
"""
print(f"[{self.name}] 📝 Filing Verdict...")
prev_id = state["prev_point"].id
+ crop_id = state["crop_id"]
- # Update Qdrant Snapshot
+ # 1. Update Qdrant Snapshot
self.qdrant.set_payload(
collection_name=COLLECTION_NAME,
points=[prev_id],
@@ -241,6 +247,16 @@ class JudgeAgent(BaseReasoningAgent):
"visual_diagnosis": str(state["visual_report"].get("health_assessment", "N/A"))
}
)
+
+ # 2. Write to FarmMemory (Text/Biography Store)
+ try:
+ verdict_summary = (
+ f"Cycle Review for {crop_id}: Result was {state['outcome']} "
+ f"(Reward: {state['reward']}). Judge's Note: {state['explanation']}"
+ )
+ farm_memory.log_event(crop_id, verdict_summary)
+ except Exception as e:
+ print(f" -> ⚠️ Failed to log to FarmMemory: {e}")
# Prepare Training Data Bundle
training_data = {
@@ -253,16 +269,14 @@ class JudgeAgent(BaseReasoningAgent):
return {"training_data": training_data}
# --- ENTRY POINT ---
- def review_previous_cycle(self, current_fmu: FMU):
- """
- The public API called by the main system.
- """
+ def review_previous_cycle(self, current_fmu: FMU, image_b64: str):
initial_state = {
"current_fmu": current_fmu,
+ "image_b64": image_b64,
"prev_point": None,
"crop_id": "",
"visual_report": {},
- "biography": "",
+ "biography": "", # Starts empty
"reward": 0.0,
"outcome": "",
"explanation": "",
diff --git a/agent/sub_agents/water_and_atmospheric_dependencies/physics_engine.py b/agent/sub_agents/water_and_atmospheric_dependencies/physics_engine.py
@@ -4,7 +4,7 @@ from langchain_openai import ChatOpenAI
from langchain_core.messages import SystemMessage, HumanMessage
# Configuration
-API_KEY = os.environ.get("GROQ_API_KEY")
+API_KEY = os.environ.get("GROQ_API_KEY1")
MODEL_ID = "qwen/qwen3-32b" # Using the latest supported Groq model
def predict_outcome(current_state: dict, proposed_action: dict) -> dict:
@@ -21,9 +21,8 @@ def predict_outcome(current_state: dict, proposed_action: dict) -> dict:
base_url="https://api.groq.com/openai/v1",
api_key=API_KEY,
model=MODEL_ID,
- temperature=0.1,
- max_tokens=1024,
- model_kwargs={"reasoning_effort": "none"}
+ temperature=0.1, # Low temp for consistent physics logic
+ max_tokens=1024
)
system_prompt = (
@@ -51,9 +50,6 @@ def predict_outcome(current_state: dict, proposed_action: dict) -> dict:
# Clean and Parse JSON
content = response.content.replace("```json", "").replace("```", "").strip()
- if not content:
- raise ValueError("Empty response from model")
-
result = json.loads(content)
# Default fallback keys if the LLM misses them