| """ |
| 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 |
| """ |
| |
| |
| self.llm = LLM( |
| model="gpt-3.5-turbo", |
| temperature=0.3, |
| api_key=os.getenv("OPENAI_API_KEY") |
| ) |
|
|
| |
| self.portfolio_config = { |
| "total_value": 42_000_000_000, |
| "programs": 3, |
| "active_contracts": 87, |
| "active_suppliers": 45, |
| "portfolio_name": "UK National Infrastructure Portfolio" |
| } |
| |
| |
| 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 |
| """ |
| |
| |
| tier2_results = context.get('tier2_zone_summaries', {}) if context else {} |
| tier3_results = context.get('tier3_supporting_results', {}) if context else {} |
| |
| |
| agent_data_summary = self._build_agent_data_summary(tier2_results, tier3_results) |
| |
| |
| synthesis_prompt = self._create_synthesis_prompt(query, agent_data_summary) |
| |
| |
| 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 = [] |
| |
| |
| summary_lines.append("=" * 80) |
| summary_lines.append("AGENT INTELLIGENCE SUMMARY") |
| summary_lines.append("=" * 80) |
| summary_lines.append("") |
| |
| |
| 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("") |
| |
| |
| 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("") |
| |
| |
| 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("") |
| |
| |
| 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() |
| |
| |
| if key_lower in ['tier', 'feeds_into_zone', 'agent_name', 'zone_name', 'zone', 'tier3_inputs_received']: |
| return None |
| |
| |
| 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" |
| |
| |
| if 'pct' in key_lower or 'percent' in key_lower or 'ratio' in key_lower: |
| if isinstance(value, (int, float)): |
| return f"{value:.1f}%" |
| |
| |
| if 'health' in key_lower or 'score' in key_lower: |
| if isinstance(value, (int, float)): |
| return f"{value:.1f}/100" |
| |
| |
| if 'month' in key_lower: |
| if isinstance(value, (int, float)): |
| return f"{value:.1f} months" |
| |
| |
| if 'day' in key_lower and 'days' not in str(value): |
| if isinstance(value, (int, float)): |
| return f"{value:.1f} days" |
| |
| |
| 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" |
| |
| |
| if isinstance(value, dict): |
| return f"{len(value)} entries" |
| |
| |
| if isinstance(value, (int, float)): |
| return f"{value:.1f}" |
| |
| |
| 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')} |
| """ |
|
|
|
|
| |
| def create_executive_intelligence_agent(llm=None): |
| return ExecutiveIntelligenceAgent(llm=llm) |