demeter

Autonomous Hydroponic Intelligence

agent/sub_agents/atmospheric_agent.py (6179B)


  1import os
  2from langchain_openai import AzureChatOpenAI
  3from langgraph.graph import StateGraph, END
  4
  5# Graph State & Nodes
  6from agent.sub_agents.water_and_atmospheric_dependencies.state import AgentState
  7from agent.sub_agents.water_and_atmospheric_dependencies.nodes import decide_node, simulate_node, finalize_node, execute_tools_node
  8
  9# Tools
 10from agent.sub_agents.water_and_atmospheric_dependencies.retrieval import ask_historian, ask_rag, diagnose_plant, ask_memory
 11from agent.sub_agents.water_and_atmospheric_dependencies.tools import calculate_vpd, web_search
 12
 13# 🛡️ GUARDRAILS
 14from agent.guardrails.validation import sanitize_input, validate_plan, create_validation_report
 15
 16# Configuration
 17API_KEY = os.environ.get("AZURE_OPENAI_API_KEY")
 18ENDPOINT = os.environ.get("AZURE_OPENAI_ENDPOINT")
 19DEPLOYMENT_NAME = os.environ.get("AZURE_OPENAI_DEPLOYMENT_NAME", "gpt-4.1")
 20API_VERSION = os.environ.get("AZURE_OPENAI_API_VERSION", "2024-12-01-preview")
 21
 22ATMOS_PROMPT = """
 23 === ATMOSPHERIC SPECIALIST (FARM-ONLY MODE) ===
 24
 25YOUR ROLE:
 26You are an AI specialist controlling ONLY the atmospheric conditions of a hydroponic farm.
 27Your SOLE purpose is to optimize plant growth through air temperature, humidity, CO₂, and light.
 28
 29 HARD CONSTRAINTS (NON-NEGOTIABLE - YOU WILL FAIL IF YOU VIOLATE THESE):
 301. Air Temperature: MUST be between 10°C and 35°C (failure if outside range)
 312. Humidity: MUST be between 30% and 90% (failure if outside range)
 323. CO₂ Level: MUST be between 300 and 1500 ppm (failure if outside range)
 334. Light Intensity: MUST be between 0% and 100% (failure if outside range)
 34
 35 OPTIMIZATION TARGETS (aim for these if possible, but NEVER violate hard constraints):
 36- VPD: 0.8-1.2 kPa (Vegetative), 1.2-1.6 kPa (Flowering)
 37- Humidity: 60-80% (avoid >80% mold risk, avoid <30% stress)
 38- CO₂: 1000-1500 ppm only if light is at 80%+
 39- Temperature: Crop-specific (see strategy)
 40
 41 CRITICAL SITUATION HANDLING:
 42- If plant is in critical condition (health < 50%), STAY SAFE within hard constraints
 43- Do NOT attempt aggressive corrections that violate bounds
 44- Conservative stable values within bounds are BETTER than aggressive out-of-bounds values
 45- The system will gradually improve through multiple safe cycles
 46
 47 CURRENT STATE:
 48Sensors: {sensors}
 49Strategy: {strategy}
 50Research: {research}
 51History: {history}
 52Simulation Feedback: {critique}
 53
 54 OUTPUT REQUIREMENTS:
 55- Return ONLY valid JSON with exactly these keys: 'air_temp', 'humidity', 'co2', 'light_intensity'
 56- All values must be NUMBERS within the hard constraints above
 57- NO markdown, NO code blocks, NO explanations, NO text outside JSON
 58- EVERY violation of hard constraints will cause a RETRY - your plan will be rejected
 59
 60 FORBIDDEN:
 61- Do NOT attempt to control water, nutrients, or pH
 62- Do NOT make suggestions unrelated to the farm
 63- Do NOT return anything except the JSON object
 64- Do NOT violate hard constraints under ANY circumstance
 65"""
 66
 67class AtmosphericAgent:
 68    def __init__(self):
 69        self.name = "Atmospheric Agent"
 70        
 71        if not API_KEY or not ENDPOINT:
 72            print(f"[{self.name}] ⚠️ No Azure OpenAI credentials found.")
 73            self.model = None
 74        else:
 75            llm = AzureChatOpenAI(
 76                azure_endpoint=ENDPOINT,
 77                api_key=API_KEY,
 78                api_version=API_VERSION,
 79                deployment_name=DEPLOYMENT_NAME,
 80                temperature=0.2,
 81                model_kwargs={"tool_choice": "auto", "parallel_tool_calls": False}
 82            )
 83
 84            self.model_with_tools = llm.bind_tools([
 85              #  ask_historian, 
 86                ask_rag, 
 87                web_search,
 88                calculate_vpd,
 89                diagnose_plant,
 90             #   ask_memory
 91            ])
 92
 93        self.app = self._build_graph()
 94
 95    def _build_graph(self):
 96        workflow = StateGraph(AgentState)
 97
 98        workflow.add_node("decide", lambda state: decide_node(state, self.model_with_tools, ATMOS_PROMPT))
 99  
100        workflow.add_node("tools", execute_tools_node)
101
102        workflow.add_node("simulate", simulate_node)
103
104        workflow.add_node("finalize", finalize_node)
105        
106        workflow.add_node("skip_unsafe", lambda state: {"final_action": {"air_temp": 25, "humidity": 65, "co2": 800, "light_intensity": 50}})
107
108        workflow.set_entry_point("decide")
109
110        def check_decision_output(state):
111            if state.get("next_step") == "tools":
112                return "tools"
113            return "simulate"
114
115        workflow.add_conditional_edges(
116            "decide", 
117            check_decision_output, 
118            {"tools": "tools", "simulate": "simulate"}
119        )
120        
121        workflow.add_edge("tools", "decide")
122
123        def check_simulation_result(state):
124            if state["simulation_result"]["passed"]:
125                return "finalize"
126            elif state["retry_count"] > 3:
127                print(f"[{self.name}] ⚠️ Max retries reached. Skipping execution (no-op).")
128                return "skip_unsafe"
129            else:
130                # Loop back to fix the mistake
131                return "decide"
132
133        workflow.add_conditional_edges(
134            "simulate", 
135            check_simulation_result, 
136            {"finalize": "finalize", "decide": "decide", "skip_unsafe": "skip_unsafe"}
137        )
138        
139        workflow.add_edge("finalize", END)
140        workflow.add_edge("skip_unsafe", END)
141        final_plan = workflow.compile()
142        print("final_plan(Atmos): ", final_plan)
143        return final_plan
144
145    def reason(self, sensors, research, strategy, history="None", image_b64=None):
146        """Entry point called by main_agent.py"""
147        
148        initial_state = {
149            "sensors": sensors,
150            "research_context": research,
151            "strategy": strategy,
152            "history": history,
153            "image_b64": image_b64, # 🟢 Stored in state, waiting to be injected
154            "retry_count": 0,
155            "critique": None,
156            "messages": [] 
157        }
158        
159        result = self.app.invoke(initial_state)
160
161   #     print(f"\n[{self.name}] Final Result: {result}")
162        return result.get("final_action", {})