""" Response Compiler - Stage 3 Compiles MCP results with focus on ALERTING INFORMATION ONLY """ from typing import Dict, Any from openai import OpenAI import httpx import os class ResponseCompiler: """ Compiles MCP server results into alert-focused responses. KEY PRINCIPLE: All MCPs are queried, but the compiler extracts ONLY the alerting, concerning, or actionable information. Normal/good status is minimized or omitted entirely. """ def __init__(self): """Initialize compiler with OpenAI client""" api_key = os.getenv("OPENAI_API_KEY") if not api_key: raise ValueError("OPENAI_API_KEY environment variable not set") # Create custom httpx client without proxies parameter (compatibility fix) http_client = httpx.Client( timeout=httpx.Timeout(60.0, connect=10.0), limits=httpx.Limits(max_keepalive_connections=10, max_connections=20) ) self.client = OpenAI(api_key=api_key, http_client=http_client) def compile_alert_summary( self, mcp_results: Dict[str, Any], location: Dict[str, float], location_name: str = "" ) -> str: """ Compile MCP results into alert summary focusing ONLY on concerning information. This is where the intelligence lives - not in routing. All MCP servers are queried, but we extract only what farmers need to act on. Args: mcp_results: Results from all MCP servers location: Dict with 'latitude' and 'longitude' keys location_name: Optional human-readable location name Returns: Alert summary highlighting only actionable concerns """ # Build comprehensive context from all MCP data context_parts = [] for server_name, result in mcp_results.items(): if result["status"] == "success" and result["data"]: context_parts.append(f"=== {server_name.upper()} DATA ===\n{result['data']}") if not context_parts: return "Unable to generate alert summary - no data available from MCP servers." full_context = "\n\n".join(context_parts) location_str = f"{location_name} ({location['latitude']:.4f}°N, {location['longitude']:.4f}°E)" # THE KEY PROMPT: Extract only alerting information prompt = f"""You are an agricultural alert analyst. Your task is to analyze comprehensive agricultural data and extract ONLY the alerting, concerning, or time-sensitive information. LOCATION: {location_str} COMPREHENSIVE DATA FROM ALL MONITORING SYSTEMS: {full_context} YOUR TASK: Generate a concise ALERT SUMMARY that includes ONLY: 1. **CRITICAL ALERTS** - Immediate threats requiring urgent action: - Extreme weather conditions (heat waves, storms, frost) - Active pest/disease outbreaks - Severe water scarcity or excess - Soil contamination or extreme deficiencies 2. **IMPORTANT WARNINGS** - Developing issues requiring attention: - Concerning trends (declining water table, degrading soil) - Moderate pest pressure building up - Suboptimal weather patterns affecting crops - Nutrient imbalances needing correction 3. **ACTIONABLE RECOMMENDATIONS** - What farmers should do: - Specific actions with timing - Preventive measures - Mitigation strategies CRITICAL RULES: - OMIT all normal/good status information unless it provides important context - If weather is normal → DON'T mention it or say "Weather: Normal" briefly - If soil is healthy → SKIP or say "Soil: No concerns" briefly - If no pest activity → SKIP or say "Pests: No threats detected" briefly - FOCUS on deviations from normal, risks, and time-sensitive items - Use specific numbers/dates only when they convey urgency - Maximum 400 words total - If everything is fine, say so clearly upfront then provide brief context Structure: 1. Status Line: "CRITICAL ALERTS DETECTED" or "NO CRITICAL ALERTS - FAVORABLE CONDITIONS" 2. Critical Alerts section (if any) 3. Important Warnings section (if any) 4. Recommended Actions (always include if alerts/warnings exist) 5. Add raw API output in JSON format at end for reference. Be direct. Skip pleasantries. Farmers need to know what matters.""" try: response = self.client.chat.completions.create( model="gpt-4o", messages=[ { "role": "system", "content": "You are an expert agricultural alert analyst. Extract ONLY concerning, alerting, or actionable information. Omit normal status unless contextually necessary." }, {"role": "user", "content": prompt} ], temperature=0.2, max_tokens=1000 ) return response.choices[0].message.content.strip() except Exception as e: print(f"⚠️ Compilation error: {e}") return self._create_fallback_summary(mcp_results, location_str) def compile_response( self, query: str, mcp_results: Dict[str, Any], location: Dict[str, float] ) -> str: """ Compile MCP results into a response for a specific query. Args: query: User's original query mcp_results: Results from MCP servers location: Dict with 'latitude' and 'longitude' keys Returns: Compiled response text focusing on query-relevant information """ # Format MCP results for context context_parts = [] for server_name, result in mcp_results.items(): if result["status"] == "success" and result["data"]: context_parts.append(f"{server_name.upper()}: {result['data']}") context = "\n\n".join(context_parts) prompt = f"""Answer this farmer's question using the provided data, focusing on actionable insights. QUESTION: {query} LOCATION: {location['latitude']:.4f}°N, {location['longitude']:.4f}°E AVAILABLE DATA: {context} Provide a focused answer that: 1. Directly addresses the question 2. Highlights any concerning information relevant to the query 3. Gives specific recommendations 4. Keeps explanations brief and practical 5. Omits irrelevant normal/good status information 6. Add Raw API Output from all MCP Servers at the end for reference. Be conversational but professional. Skip unnecessary background unless it aids understanding.""" try: response = self.client.chat.completions.create( model="gpt-4o", messages=[ {"role": "system", "content": "You are a knowledgeable agricultural advisor providing practical guidance to farmers."}, {"role": "user", "content": prompt} ], temperature=0.5, max_tokens=800 ) return response.choices[0].message.content.strip() except Exception as e: print(f"⚠️ Compilation error: {e}") return f"Error compiling response: {str(e)}" def _create_fallback_summary(self, mcp_results: Dict[str, Any], location_str: str) -> str: """Create basic fallback summary if LLM compilation fails""" summary_parts = [f"Alert Summary for {location_str}\n\n"] for server_name, result in mcp_results.items(): if result["status"] == "success" and result["data"]: summary_parts.append(f"{server_name.upper()}:") summary_parts.append(str(result["data"])[:300] + "...\n") return "\n".join(summary_parts)