File size: 19,402 Bytes
75882f6 25b1650 99e9df2 75882f6 ca22b18 25b1650 75882f6 ca22b18 25b1650 75882f6 99e9df2 ca22b18 0c3c847 14a6786 84fc742 14a6786 84fc742 0fbf626 3a96c31 84fc742 25b1650 75882f6 99e9df2 25b1650 99e9df2 25b1650 99e9df2 25b1650 99e9df2 25b1650 ca22b18 25b1650 99e9df2 ca22b18 25b1650 99e9df2 25b1650 99e9df2 ca22b18 25b1650 22018f9 ca22b18 25b1650 ca22b18 25b1650 99e9df2 ca22b18 75882f6 25b1650 99e9df2 25b1650 99e9df2 25b1650 99e9df2 25b1650 99e9df2 25b1650 99e9df2 25b1650 99e9df2 25b1650 99e9df2 25b1650 99e9df2 25b1650 99e9df2 25b1650 99e9df2 25b1650 99e9df2 25b1650 99e9df2 25b1650 99e9df2 25b1650 99e9df2 25b1650 99e9df2 25b1650 99e9df2 25b1650 99e9df2 25b1650 99e9df2 25b1650 99e9df2 25b1650 99e9df2 25b1650 99e9df2 25b1650 99e9df2 25b1650 99e9df2 25b1650 99e9df2 25b1650 99e9df2 25b1650 99e9df2 25b1650 99e9df2 25b1650 99e9df2 25b1650 99e9df2 25b1650 99e9df2 25b1650 99e9df2 25b1650 99e9df2 25b1650 99e9df2 25b1650 99e9df2 25b1650 99e9df2 25b1650 99e9df2 25b1650 99e9df2 25b1650 99e9df2 25b1650 99e9df2 25b1650 99e9df2 25b1650 99e9df2 25b1650 99e9df2 25b1650 99e9df2 25b1650 99e9df2 25b1650 99e9df2 25b1650 99e9df2 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 | """
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) |