""" Mother Orchestrator - Executive Intelligence Synthesis Receives zone results from Master Orchestrator, uses LLM to synthesize intelligence and format executive-ready responses dynamically """ import os from crewai import Agent, Task, Crew, LLM from typing import Dict, Any, List from datetime import datetime import json import re class ExecutiveIntelligenceAgent: """ MOTHER ORCHESTRATOR - Executive Intelligence Synthesis Fully LLM-driven synthesis: 1. Receives ALL agent/zone data from Master Orchestrator 2. Passes data to LLM with instructions 3. LLM analyzes data and generates executive-ready response 4. NO hardcoded logic - LLM makes all decisions based on data 5. DEMO MODE: LLM can estimate missing data intelligently """ def __init__(self, llm=None): """ Initialize Mother Orchestrator Args: llm: Ignored - Mother always creates its own CrewAI LLM instance """ # ALWAYS create a proper CrewAI LLM (ignore the callable passed from Master) # Master Orchestrator passes a function, but we need a CrewAI LLM object self.llm = LLM( model="gpt-3.5-turbo", temperature=0.3, api_key=os.getenv("OPENAI_API_KEY") ) # Portfolio Configuration (for context) self.portfolio_config = { "total_value": 42_000_000_000, # £42B "programs": 3, "active_contracts": 87, "active_suppliers": 45, "portfolio_name": "UK National Infrastructure Portfolio" } # Portfolio Context (targets/benchmarks to guide LLM) self.portfolio_context = { "target_cash_runway_months": 6, "minimum_safe_runway_months": 3, "target_float_days": 15, "minimum_float_days": 7, "target_cpi": 0.95, "minimum_cpi": 0.90, "target_supplier_health": 60, "target_on_time_delivery": 90 } self.agent = self._create_agent() def _create_agent(self): return Agent( role='Mother Orchestrator - Executive Intelligence Synthesis', goal='Synthesize zone intelligence into executive-ready responses that directly answer questions using data from specialized agents', backstory=( "You are the Mother Orchestrator for a £42B UK infrastructure portfolio. " "You receive intelligence from multiple specialized zone agents and synthesize it into clear, " "executive-ready responses. Your audience is C-suite executives who need: " "\n\n" "1) DIRECT answers to their questions (YES/NO/PARTIAL with RAG status) \n" "2) CLEAR explanation of which agents you consulted and what data they provided \n" "3) SPECIFIC numbers from the agent data (or reasonable estimates if data missing) \n" "4) ACTIONABLE recommendations with costs, timelines, and owners \n" "5) FOCUSED responses without unnecessary information \n" "\n\n" "You analyze the data provided by agents and determine the answer based on evidence, " "not on hardcoded rules. You explain your reasoning clearly. For demo purposes, you can " "make reasonable estimates for missing data based on portfolio context and industry standards." ), verbose=False, llm=self.llm, allow_delegation=False ) def analyze(self, query: str, context: Dict[str, Any] = None) -> str: """ Main entry point - receives zone results from Master Orchestrator and returns executive-ready text response Args: query: User's question context: Contains tier2_zone_summaries and tier3_supporting_results from Master Returns: Executive-ready text response generated by LLM """ # Extract zone results from context (passed by Master Orchestrator) tier2_results = context.get('tier2_zone_summaries', {}) if context else {} tier3_results = context.get('tier3_supporting_results', {}) if context else {} # Build comprehensive agent data summary for LLM agent_data_summary = self._build_agent_data_summary(tier2_results, tier3_results) # Create synthesis prompt for LLM synthesis_prompt = self._create_synthesis_prompt(query, agent_data_summary) # Let LLM analyze and synthesize response try: task = Task( description=synthesis_prompt, agent=self.agent, expected_output="Executive-ready response that directly answers the question using agent data" ) crew = Crew( agents=[self.agent], tasks=[task], verbose=False ) result = crew.kickoff() return str(result) except Exception as e: print(f"⚠️ Error in Mother Orchestrator synthesis: {e}") return self._get_fallback_response(query, agent_data_summary, str(e)) def _build_agent_data_summary(self, tier2_results: Dict, tier3_results: Dict) -> str: """Build comprehensive summary of all agent data for the LLM""" summary_lines = [] # Add header summary_lines.append("=" * 80) summary_lines.append("AGENT INTELLIGENCE SUMMARY") summary_lines.append("=" * 80) summary_lines.append("") # Tier 3 Supporting Agents if tier3_results: summary_lines.append("TIER 3 SUPPORTING AGENTS:") summary_lines.append("-" * 80) summary_lines.append("") for agent_name, agent_data in tier3_results.items(): if isinstance(agent_data, dict) and 'error' not in agent_data: summary_lines.append(f"**{agent_name.upper().replace('_', ' ')} AGENT**") summary_lines.append(f"Purpose: {self._get_agent_description(agent_name)}") summary_lines.append("") # Extract and format key metrics for key, value in agent_data.items(): if key not in ['tier', 'feeds_into_zone', 'agent_name', 'zone']: formatted_value = self._format_metric_value(key, value) if formatted_value: summary_lines.append(f" • {self._format_key_name(key)}: {formatted_value}") summary_lines.append("") # Tier 2 Zone Orchestrators if tier2_results: summary_lines.append("TIER 2 ZONE ORCHESTRATORS:") summary_lines.append("-" * 80) summary_lines.append("") for zone_key, zone_data in tier2_results.items(): if isinstance(zone_data, dict) and 'error' not in zone_data: zone_name = zone_data.get('zone_name', zone_key) summary_lines.append(f"**{zone_name.upper()}**") summary_lines.append("") # Extract and format key metrics for key, value in zone_data.items(): if key not in ['tier', 'zone_name', 'tier3_inputs_received', 'agent_name']: formatted_value = self._format_metric_value(key, value) if formatted_value: summary_lines.append(f" • {self._format_key_name(key)}: {formatted_value}") summary_lines.append("") if not tier2_results and not tier3_results: summary_lines.append("No agent data available - using portfolio context and benchmarks for analysis.") summary_lines.append("=" * 80) return "\n".join(summary_lines) def _create_synthesis_prompt(self, query: str, agent_data_summary: str) -> str: """Create the synthesis prompt for the LLM""" return f"""You are the Mother Orchestrator synthesizing intelligence from multiple specialized agents to answer an executive's question. **USER QUESTION:** {query} **PORTFOLIO CONTEXT:** - Portfolio: {self.portfolio_config['portfolio_name']} - Total Value: £{self.portfolio_config['total_value'] / 1_000_000_000:.1f}B - Programs: {self.portfolio_config['programs']} - Active Contracts: {self.portfolio_config['active_contracts']} - Active Suppliers: {self.portfolio_config['active_suppliers']} **TARGET BENCHMARKS:** - Cash Runway Target: {self.portfolio_context['target_cash_runway_months']}+ months (minimum safe: {self.portfolio_context['minimum_safe_runway_months']} months) - Schedule Float Target: {self.portfolio_context['target_float_days']}+ days (minimum: {self.portfolio_context['minimum_float_days']} days) - Cost Performance Index (CPI) Target: ≥{self.portfolio_context['target_cpi']} (minimum: {self.portfolio_context['minimum_cpi']}) - Supplier Health Target: ≥{self.portfolio_context['target_supplier_health']}/100 - On-Time Delivery Target: ≥{self.portfolio_context['target_on_time_delivery']}% **AGENT INTELLIGENCE RECEIVED:** {agent_data_summary} **YOUR TASK:** Analyze the agent data and create an executive-ready response with this EXACT structure: **Executive Summary** **Question:** {query} **Direct Answer:** [emoji] **[YES/NO/PARTIAL]** - [One clear sentence explaining the answer] ([RAG emoji] [RED/AMBER/GREEN]) --- **How We Got Here - Agent Intelligence:** [Explain in 3-4 paragraphs which agents you consulted and what data they provided. Be conversational and specific. Example: "To answer your question, I consulted the Cashflow Agent who analyzed bank balances and identified £X.XM in uncommitted cash..." Make this section tell the story of how you reached your conclusion.] --- **Current Position:** [Show 5-8 key metrics from the agent data in bullet points. Use the ACTUAL numbers from the agents. Format financial figures as £X.XM, percentages as X.X%, and include target benchmarks in parentheses. Example: "• Cash Runway: 2.8 months (Target: 6+ months)"] **What This Means:** [2-3 sentences in plain English explaining the practical implications of these numbers. Connect the metrics to the answer you gave.] --- **Key Risks:** [List 3-5 specific risks based on the agent data. Each risk should include specific numbers and explain the potential impact. Use "•" for bullet points. Prioritize by severity.] --- **Recommended Actions:** [Provide 2-4 prioritized actions with this format: **[URGENCY LEVEL] ([timeframe]):** 1. **[Action Title]** - Amount: £X.XM (if applicable) - Owner: [Role] - Timeline: [Specific timeframe] - Expected Impact: [What this achieves] Use urgency levels: URGENT (48-72 hours), PRIORITY (7-14 days), or MAINTAIN POSITION] --- **Data Sources:** - [List which zones/agents provided data - be specific] - Data as of: {datetime.now().strftime('%d-%b-%Y')} **CRITICAL RULES:** 1. **Prioritize agent data** - Use the actual numbers provided by agents whenever available 2. **DEMO MODE - Fill gaps intelligently:** If specific data is missing but you have related information, make reasonable estimates based on: - Industry standards for £42B infrastructure portfolios - Related metrics from other agents (e.g., if burn rate missing, estimate from budget data) - Portfolio context and benchmarks provided above - Typical values for UK infrastructure programmes of this scale Example: If cash runway not provided but you have burn rate, calculate it. If supplier health missing but delivery performance is low, infer supplier issues. 3. **Determine YES/NO/PARTIAL by analyzing the data** against the target benchmarks provided 4. **Use RAG status logic:** - 🟢 GREEN: Metrics at or above targets, no critical risks - 🟡 AMBER: Metrics below targets but above minimums, manageable risks - 🔴 RED: Metrics below minimums, critical risks requiring urgent action 5. **Be specific with numbers** - Always include actual values (either from agents or reasonable estimates) 6. **Explain your reasoning** in "How We Got Here" - show your analytical thinking. If you estimated something, you can briefly mention it naturally: "Based on typical burn patterns for this portfolio size..." 7. **Make recommendations proportional** to severity - more urgent = more detailed actions with specific costs and timelines 8. **Use clear, executive-appropriate language** - avoid jargon unless necessary 9. **Maintain realism** - Estimates should be plausible for a £42B portfolio. Don't make extreme numbers. **REASONABLE ESTIMATION GUIDELINES FOR £42B UK INFRASTRUCTURE PORTFOLIO:** Typical ranges for context: - Monthly burn rate: £10-20M - Cash runway: 2-8 months (varies by funding phase) - Contractor payables (30 days): £15-35M - Schedule float: 5-20 days (lower is concerning) - CPI: 0.88-1.05 (below 0.90 is poor, above 1.0 is excellent) - Supplier on-time delivery: 75-95% - Supplier health scores: 40-80/100 - CE/variation exposure: £5-15M - Design maturity: 70-90% - Approval processing time: 20-45 days **EXAMPLES OF GOOD "How We Got Here" SECTIONS:** Example 1 (Cashflow - with full data): "To answer your question, I consulted three intelligence sources. The Cashflow Agent analyzed our bank position and identified £42.5M in uncommitted cash reserves, with a monthly burn rate of £15.2M based on recent spend patterns. This gives us a runway of 2.8 months. The Commercial Zone reviewed our payment obligations and found £28.3M due to contractors in the next 30 days, plus £9.6M in pending compensation events. The Cost Performance Zone validated these numbers against our approved budget. While we have enough cash to cover immediate contractor payments (coverage ratio of 1.5x), our runway of 2.8 months falls critically below our 6-month safety threshold and even below the 3-month minimum safe level. This means we're operating with inadequate financial buffer." Example 2 (Schedule - with partial data and estimation): "I analyzed schedule intelligence from two zones. The Schedule Zone examined our critical path and found only 8.5 days of float remaining—well below our 15-day target and concerning given we aim for healthy margins. They also identified 18 'brittle' activities with less than 5 days of float, meaning any small delay could impact the critical path. The Supplier Zone assessed our construction partners' delivery performance and found 85% on-time delivery, which is below our 90% target. This combination tells us partners are delivering but with eroding margins. The low float means we've lost our schedule buffer, making the programme vulnerable to any disruption." Example 3 (Using estimation when data sparse): "To assess budget performance, I consulted the Cost Performance Zone which reported a CPI of 0.93, indicating we're spending £1.08 for every £1.00 budgeted. The Commercial Zone identified £8.2M in pending variations. While specific contractor payment data wasn't available from agents, based on our programme scale and burn rate patterns, typical monthly contractor obligations would be around £18-25M for a portfolio of this size. The cost overrun, combined with variation exposure, suggests we're experiencing budget pressure that requires attention." Now synthesize the agent data (and fill any necessary gaps intelligently) into your response following this exact structure. """ def _format_key_name(self, key: str) -> str: """Convert snake_case key to readable name""" return key.replace('_', ' ').title() def _format_metric_value(self, key: str, value: Any) -> str: """Format metric value based on type and key name""" if value is None: return None key_lower = key.lower() # Skip certain keys if key_lower in ['tier', 'feeds_into_zone', 'agent_name', 'zone_name', 'zone', 'tier3_inputs_received']: return None # Financial values if 'gbp' in key_lower or 'cost' in key_lower or 'value' in key_lower or 'exposure' in key_lower or 'payable' in key_lower: if isinstance(value, (int, float)): return f"£{value/1_000_000:.1f}M" # Percentages if 'pct' in key_lower or 'percent' in key_lower or 'ratio' in key_lower: if isinstance(value, (int, float)): return f"{value:.1f}%" # Health scores if 'health' in key_lower or 'score' in key_lower: if isinstance(value, (int, float)): return f"{value:.1f}/100" # Months if 'month' in key_lower: if isinstance(value, (int, float)): return f"{value:.1f} months" # Days if 'day' in key_lower and 'days' not in str(value): if isinstance(value, (int, float)): return f"{value:.1f} days" # Lists (truncate if too long) if isinstance(value, list): if len(value) == 0: return None if len(value) <= 3: return ', '.join(str(v) for v in value) else: return f"{len(value)} items" # Dictionaries (count) if isinstance(value, dict): return f"{len(value)} entries" # Regular numbers if isinstance(value, (int, float)): return f"{value:.1f}" # Strings if isinstance(value, str) and len(value) < 100: return value return None def _get_agent_description(self, agent_name: str) -> str: """Get friendly description of agent purpose""" descriptions = { "cashflow": "Analyzes cash position, burn rate, and runway projections", "contract360": "Provides contract-level aggregation and commercial intelligence", "design_guard": "Monitors design compliance and maturity", "commissioning": "Tracks commissioning readiness and handover status", "risk": "Cross-zone risk intelligence and materialization tracking" } return descriptions.get(agent_name, "Specialized intelligence agent") def _get_fallback_response(self, query: str, agent_data: str, error: str) -> str: """Provide fallback response if LLM synthesis fails""" return f"""**Executive Summary** **Question:** {query} **Status:** ⚠️ Analysis Error An error occurred while synthesizing the intelligence from our agents: ``` {error} ``` **Available Agent Data:** {agent_data[:500]}... **Action Required:** Please try rephrasing your question or contact technical support if the issue persists. **Timestamp:** {datetime.now().strftime('%d-%b-%Y %H:%M')} """ # Factory function for Master Orchestrator to use def create_executive_intelligence_agent(llm=None): return ExecutiveIntelligenceAgent(llm=llm)