demeter

Autonomous Hydroponic Intelligence
commit 0cd13e439dbf10b4885a5462d9b2b70548ae206c
parent 1bf0c5f020ef202e5c9a1e074715e497dd52c29e
Author: Arnav Gupta <66205884+arnav0103@users.noreply.github.com>
Date:   Sat, 21 Mar 2026 18:57:12 +0530

Merge pull request #11 from arnav0103/main

debxmayday
Diffstat:
Magent/Marl/bandit.py | 4+++-
Magent/sub_agents/Doctor.py | 157++++++++++++++++++++++++++++++++++++++++---------------------------------------
Magent/sub_agents/Explainer.py | 22++++++++++++----------
Magent/sub_agents/Supervisor.py | 36++++++++++++++++++++++++++++--------
Magent/sub_agents/fetching_agent.py | 68++++++++++++++++++++++++--------------------------------------------
Magent/sub_agents/judge_agent.py | 128++++++++++++++++++++++++++++++++++++++++++++-----------------------------------
Magent/sub_agents/water_and_atmospheric_dependencies/physics_engine.py | 10+++-------
7 files changed, 221 insertions(+), 204 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): @@ -190,6 +196,20 @@ class SupervisorAgent: 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