demeter
Autonomous Hydroponic Intelligencegit clone https://github.com/maydayv7/demeter
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", {})