File size: 18,162 Bytes
4b9d59b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
"""
Data Consistency Insights for AI Debug System

Analyzes data consistency across distributed components and detects:
- Data flow across distributed nodes
- State divergence (same data, different states)
- Replication completion verification
- Data synchronization issues

Example insights:
- "Data sent to 5 nodes, 4 confirmed, 1 pending"
- "Data X saved on Node 1 but not Node 2"
- "Replication lag detected across 3 nodes"
"""

import asyncio
from collections import defaultdict
from datetime import datetime, timedelta
from typing import Any, Dict, List, Optional, Set, Tuple

from sqlalchemy.orm import Session
from sqlalchemy import and_, or_

from core.models import (
    DebugEvent,
    DebugInsight,
    DebugStateSnapshot,
    DebugInsightType,
    DebugInsightSeverity,
)
from core.structured_logger import StructuredLogger


class ConsistencyInsightGenerator:
    """
    Generates insights about data consistency across distributed components.

    Tracks data flow through the system and detects inconsistencies,
    replication delays, and synchronization issues.
    """

    def __init__(self, db_session: Session):
        """
        Initialize consistency insight generator.

        Args:
            db_session: SQLAlchemy database session
        """
        self.logger = StructuredLogger(__name__)
        self.db = db_session

    async def analyze_data_flow(
        self,
        operation_id: str,
        component_ids: List[str],
        component_type: str = "agent",
    ) -> Optional[DebugInsight]:
        """
        Analyze data flow across distributed components.

        Tracks how data propagates through the system and identifies
        any components that haven't received or acknowledged the data.

        Args:
            operation_id: Operation to analyze
            component_ids: Expected components in the flow
            component_type: Type of components

        Returns:
            Consistency insight or None
        """
        try:
            # Find all state snapshots for this operation
            snapshots = (
                self.db.query(DebugStateSnapshot)
                .filter(
                    and_(
                        DebugStateSnapshot.operation_id == operation_id,
                        DebugStateSnapshot.component_type == component_type,
                    )
                )
                .order_by(DebugStateSnapshot.captured_at)
                .all()
            )

            if not snapshots:
                return None

            # Track which components have snapshots
            components_with_snapshots = set(s.component_id for s in snapshots)

            # Find missing components
            missing_components = set(component_ids) - components_with_snapshots

            if missing_components:
                # Data hasn't reached all components yet
                return DebugInsight(
                    insight_type=DebugInsightType.CONSISTENCY.value,
                    severity=DebugInsightSeverity.WARNING.value,
                    title="Incomplete data propagation",
                    description=f"Data sent to {len(component_ids)} nodes but only "
                    f"{len(components_with_snapshots)} confirmed receipt",
                    summary=f"Data sent to {len(component_ids)} nodes, {len(missing_components)} pending",
                    evidence={
                        "operation_id": operation_id,
                        "expected_components": component_ids,
                        "confirmed_components": list(components_with_snapshots),
                        "missing_components": list(missing_components),
                        "propagation_rate": len(components_with_snapshots) / len(component_ids),
                    },
                    confidence_score=0.95,
                    suggestions=[
                        f"Check connectivity to missing nodes: {', '.join(missing_components)}",
                        "Verify message queues are processing",
                        "Check for network partitions",
                    ],
                    scope="distributed",
                    affected_components=[
                        {"type": component_type, "id": comp_id} for comp_id in component_ids
                    ],
                    generated_at=datetime.utcnow(),
                )

            # All components have data - check timing consistency
            capture_times = [s.captured_at for s in snapshots if s.captured_at]
            if len(capture_times) > 1:
                min_time = min(capture_times)
                max_time = max(capture_times)
                replication_lag = (max_time - min_time).total_seconds()

                if replication_lag > 5.0:  # 5 second threshold
                    return DebugInsight(
                        insight_type=DebugInsightType.CONSISTENCY.value,
                        severity=DebugInsightSeverity.WARNING.value,
                        title="Replication lag detected",
                        description=f"Data replicated to all {len(component_ids)} nodes but with "
                        f"{replication_lag:.1f}s delay between first and last",
                        summary=f"Replication lag of {replication_lag:.1f}s across {len(component_ids)} nodes",
                        evidence={
                            "operation_id": operation_id,
                            "replication_lag_seconds": replication_lag,
                            "first_confirmation": min_time.isoformat() if min_time else None,
                            "last_confirmation": max_time.isoformat() if max_time else None,
                        },
                        confidence_score=0.90,
                        suggestions=[
                            "Investigate slow components",
                            "Check network latency between nodes",
                            "Review resource utilization",
                        ],
                        scope="distributed",
                        affected_components=[
                            {"type": component_type, "id": comp_id} for comp_id in component_ids
                        ],
                        generated_at=datetime.utcnow(),
                    )

            # All good - data consistent
            return DebugInsight(
                insight_type=DebugInsightType.CONSISTENCY.value,
                severity=DebugInsightSeverity.INFO.value,
                title="Data consistent across all nodes",
                description=f"Data successfully propagated to all {len(component_ids)} components",
                summary=f"Data sent to {len(component_ids)} nodes, all confirmed",
                evidence={
                    "operation_id": operation_id,
                    "component_count": len(component_ids),
                    "replication_complete": True,
                },
                confidence_score=1.0,
                scope="distributed",
                affected_components=[
                    {"type": component_type, "id": comp_id} for comp_id in component_ids
                ],
                generated_at=datetime.utcnow(),
            )

        except Exception as e:
            self.logger.error(
                "Failed to analyze data flow",
                operation_id=operation_id,
                error=str(e),
            )
            return None

    async def detect_state_divergence(
        self,
        operation_id: str,
        component_type: str = "agent",
    ) -> Optional[DebugInsight]:
        """
        Detect state divergence across components.

        Identifies when the same data has different states on different nodes,
        indicating a synchronization issue.

        Args:
            operation_id: Operation to analyze
            component_type: Type of components

        Returns:
            Consistency insight or None
        """
        try:
            # Get all state snapshots for this operation
            snapshots = (
                self.db.query(DebugStateSnapshot)
                .filter(
                    and_(
                        DebugStateSnapshot.operation_id == operation_id,
                        DebugStateSnapshot.component_type == component_type,
                    )
                )
                .order_by(DebugStateSnapshot.captured_at.desc())
                .all()
            )

            if len(snapshots) < 2:
                return None  # Need at least 2 components to compare

            # Get latest snapshot for each component
            latest_snapshots = {}
            for snapshot in snapshots:
                if snapshot.component_id not in latest_snapshots:
                    latest_snapshots[snapshot.component_id] = snapshot

            # Compare states across components
            inconsistencies = self._compare_states(latest_snapshots)

            if inconsistencies:
                component_ids = list(latest_snapshots.keys())
                return DebugInsight(
                    insight_type=DebugInsightType.CONSISTENCY.value,
                    severity=DebugInsightSeverity.CRITICAL.value,
                    title="State divergence detected",
                    description=f"Found {len(inconsistencies)} state inconsistencies across "
                    f"{len(component_ids)} components",
                    summary=f"Data differs across {len(component_ids)} components for {len(inconsistencies)} keys",
                    evidence={
                        "operation_id": operation_id,
                        "inconsistencies": inconsistencies,
                        "affected_keys": list(inconsistencies.keys()),
                    },
                    confidence_score=0.92,
                    suggestions=[
                        "Review synchronization logic",
                        "Check for concurrent modifications",
                        "Verify conflict resolution mechanisms",
                        "Investigate data partitioning issues",
                    ],
                    scope="distributed",
                    affected_components=[
                        {"type": component_type, "id": comp_id} for comp_id in component_ids
                    ],
                    generated_at=datetime.utcnow(),
                )

            return None

        except Exception as e:
            self.logger.error(
                "Failed to detect state divergence",
                operation_id=operation_id,
                error=str(e),
            )
            return None

    async def verify_replication_completion(
        self,
        operation_id: str,
        expected_replicas: int,
        component_type: str = "agent",
    ) -> Optional[DebugInsight]:
        """
        Verify that data has been replicated to all expected nodes.

        Args:
            operation_id: Operation to verify
            expected_replicas: Number of expected replicas
            component_type: Type of components

        Returns:
            Consistency insight or None
        """
        try:
            # Count unique components that have data
            component_count = (
                self.db.query(DebugStateSnapshot)
                .filter(
                    and_(
                        DebugStateSnapshot.operation_id == operation_id,
                        DebugStateSnapshot.component_type == component_type,
                    )
                )
                .distinct(DebugStateSnapshot.component_id)
                .count()
            )

            if component_count < expected_replicas:
                return DebugInsight(
                    insight_type=DebugInsightType.CONSISTENCY.value,
                    severity=DebugInsightSeverity.WARNING.value,
                    title="Incomplete replication",
                    description=f"Expected {expected_replicas} replicas but only {component_count} confirmed",
                    summary=f"{expected_replicas - component_count} replicas missing",
                    evidence={
                        "operation_id": operation_id,
                        "expected_replicas": expected_replicas,
                        "actual_replicas": component_count,
                        "completion_rate": component_count / expected_replicas,
                    },
                    confidence_score=0.98,
                    suggestions=[
                        "Check replication logs",
                        "Verify network connectivity",
                        "Review replication configuration",
                    ],
                    scope="distributed",
                    affected_components=[{"type": component_type}],
                    generated_at=datetime.utcnow(),
                )

            # Replication complete
            return DebugInsight(
                insight_type=DebugInsightType.CONSISTENCY.value,
                severity=DebugInsightSeverity.INFO.value,
                title="Replication complete",
                description=f"Data successfully replicated to all {expected_replicas} nodes",
                summary=f"All {expected_replicas} replicas confirmed",
                evidence={
                    "operation_id": operation_id,
                    "replica_count": component_count,
                },
                confidence_score=1.0,
                scope="distributed",
                affected_components=[{"type": component_type}],
                generated_at=datetime.utcnow(),
            )

        except Exception as e:
            self.logger.error(
                "Failed to verify replication completion",
                operation_id=operation_id,
                error=str(e),
            )
            return None

    async def analyze_sync_patterns(
        self,
        time_range: str = "last_1h",
    ) -> List[DebugInsight]:
        """
        Analyze synchronization patterns across all operations.

        Identifies systemic issues with data synchronization.

        Args:
            time_range: Time range to analyze

        Returns:
            List of consistency insights
        """
        try:
            insights = []
            time_filter = self._parse_time_range(time_range)

            # Find operations with incomplete replication
            operations = (
                self.db.query(DebugStateSnapshot.operation_id)
                .filter(DebugStateSnapshot.captured_at >= time_filter)
                .group_by(DebugStateSnapshot.operation_id)
                .all()
            )

            for (operation_id,) in operations:
                # Count components per operation
                component_count = (
                    self.db.query(DebugStateSnapshot)
                    .filter(
                        and_(
                            DebugStateSnapshot.operation_id == operation_id,
                            DebugStateSnapshot.captured_at >= time_filter,
                        )
                    )
                    .distinct(DebugStateSnapshot.component_id)
                    .count()
                )

                # If an operation has only 1 component, it might not be replicating
                if component_count == 1:
                    insights.append(
                        DebugInsight(
                            insight_type=DebugInsightType.CONSISTENCY.value,
                            severity=DebugInsightSeverity.INFO.value,
                            title="Single-component operation detected",
                            description=f"Operation {operation_id} has data on only 1 component",
                            summary=f"Operation may not be configured for replication",
                            evidence={"operation_id": operation_id},
                            confidence_score=0.70,
                            suggestions=[
                                "Verify replication is configured",
                                "Check if this is a local-only operation",
                            ],
                            scope="distributed",
                            affected_components=[],
                            generated_at=datetime.utcnow(),
                        )
                    )

            return insights

        except Exception as e:
            self.logger.error("Failed to analyze sync patterns", error=str(e))
            return []

    def _compare_states(
        self,
        snapshots: Dict[str, DebugStateSnapshot],
    ) -> Dict[str, Any]:
        """
        Compare states across multiple components.

        Args:
            snapshots: Component ID -> Snapshot mapping

        Returns:
            Dictionary of inconsistent keys and their values
        """
        inconsistencies = {}

        # Get all keys from the first snapshot
        first_snapshot = list(snapshots.values())[0]
        all_keys = set(first_snapshot.state_data.keys()) if first_snapshot.state_data else set()

        # For each key, compare values across all snapshots
        for key in all_keys:
            values = {}
            for comp_id, snapshot in snapshots.items():
                if snapshot.state_data and key in snapshot.state_data:
                    values[comp_id] = snapshot.state_data[key]

            # Check if all values are the same
            unique_values = set(str(v) for v in values.values())
            if len(unique_values) > 1:
                inconsistencies[key] = {
                    "values": values,
                    "divergence_detected": True,
                }

        return inconsistencies

    def _parse_time_range(self, time_range: str) -> datetime:
        """Parse time range string to datetime."""
        now = datetime.utcnow()

        if time_range == "last_1h":
            return now - timedelta(hours=1)
        elif time_range == "last_24h":
            return now - timedelta(hours=24)
        elif time_range == "last_7d":
            return now - timedelta(days=7)
        else:
            return now - timedelta(hours=1)