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)