New alert system and panel de control page

This commit is contained in:
Urtzi Alfaro
2025-11-27 15:52:40 +01:00
parent 1a2f4602f3
commit e902419b6e
178 changed files with 20982 additions and 6944 deletions

View File

@@ -40,6 +40,10 @@ from app.models.orchestration_run import (
OrchestrationRun, OrchestrationStatus
)
# Add shared utilities to path for demo dates
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent))
from shared.utils.demo_dates import BASE_REFERENCE_DATE
# Configure logging
structlog.configure(
processors=[
@@ -55,8 +59,8 @@ logger = structlog.get_logger()
DEMO_TENANT_SAN_PABLO = uuid.UUID("a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6") # Individual bakery
DEMO_TENANT_LA_ESPIGA = uuid.UUID("b2c3d4e5-f6a7-48b9-c0d1-e2f3a4b5c6d7") # Central bakery
# Base reference date for date calculations
BASE_REFERENCE_DATE = datetime(2025, 1, 15, 12, 0, 0, tzinfo=timezone.utc)
# BASE_REFERENCE_DATE now imported from shared utilities to ensure consistency
# between seeding and cloning operations
# Hardcoded orchestration run configurations
ORCHESTRATION_CONFIG = {
@@ -152,7 +156,93 @@ def generate_run_number(tenant_id: uuid.UUID, index: int, run_type: str) -> str:
"""Generate a unique run number"""
tenant_prefix = "SP" if tenant_id == DEMO_TENANT_SAN_PABLO else "LE"
type_code = run_type[0:3].upper()
return f"ORCH-{tenant_prefix}-{type_code}-{BASE_REFERENCE_DATE.year}-{index:03d}"
current_year = datetime.now(timezone.utc).year
return f"ORCH-{tenant_prefix}-{type_code}-{current_year}-{index:03d}"
def generate_reasoning_metadata(
forecasts_generated: int,
production_batches_created: int,
procurement_plans_created: int,
purchase_orders_created: int
) -> dict:
"""
Generate reasoning metadata for orchestration run that will be used by alert processor.
This creates structured reasoning data that the alert processor can use to provide
context when showing AI reasoning to users.
"""
reasoning_metadata = {
'reasoning': {
'type': 'daily_orchestration_summary',
'timestamp': datetime.now(timezone.utc).isoformat(),
'summary': 'Daily orchestration run completed successfully',
'details': {
'forecasting': {
'forecasts_created': forecasts_generated,
'method': 'automated_daily_forecast',
'reasoning': 'Generated forecasts based on historical patterns and seasonal trends'
},
'production': {
'batches_created': production_batches_created,
'method': 'demand_based_scheduling',
'reasoning': 'Scheduled production batches based on forecasted demand and inventory levels'
},
'procurement': {
'requirements_created': procurement_plans_created,
'pos_created': purchase_orders_created,
'method': 'automated_procurement',
'reasoning': 'Generated procurement plan based on production needs and inventory optimization'
}
}
},
'purchase_orders': [],
'production_batches': [],
'ai_insights': {
'generated': 0,
'posted': 0
}
}
# Add sample purchase order reasoning if any POs were created
if purchase_orders_created > 0:
for i in range(min(purchase_orders_created, 3)): # Limit to 3 sample POs
po_reasoning = {
'id': f'po-sample-{i+1}',
'status': 'created',
'reasoning': {
'type': 'inventory_optimization',
'parameters': {
'trigger': 'low_stock_prediction',
'min_depletion_days': random.randint(2, 5),
'quantity': random.randint(100, 500),
'unit': 'kg',
'supplier': 'Demo Supplier',
'financial_impact_eur': random.randint(100, 1000)
}
}
}
reasoning_metadata['purchase_orders'].append(po_reasoning)
# Add sample production batch reasoning if any batches were created
if production_batches_created > 0:
for i in range(min(production_batches_created, 3)): # Limit to 3 sample batches
batch_reasoning = {
'id': f'batch-sample-{i+1}',
'status': 'scheduled',
'reasoning': {
'type': 'demand_forecasting',
'parameters': {
'trigger': 'forecasted_demand',
'forecasted_quantity': random.randint(50, 200),
'product_name': 'Demo Product',
'financial_impact_eur': random.randint(50, 500)
}
}
}
reasoning_metadata['production_batches'].append(batch_reasoning)
return reasoning_metadata
async def generate_orchestration_for_tenant(
@@ -251,8 +341,8 @@ async def generate_orchestration_for_tenant(
# For today's run (i==0), use current datetime instead of BASE_REFERENCE_DATE
if i == 0 and today_run_created:
now = datetime.now(timezone.utc)
started_at = now - timedelta(hours=2) # Started 2 hours ago
completed_at = now - timedelta(minutes=30) # Completed 30 minutes ago
started_at = now - timedelta(minutes=90) # Started 90 minutes ago (1.5 hours)
completed_at = now - timedelta(minutes=30) # Completed 30 minutes ago, so 60-minute duration
duration_seconds = int((completed_at - started_at).total_seconds())
else:
started_at = calculate_datetime_from_offset(offset_days - 1)
@@ -261,13 +351,21 @@ async def generate_orchestration_for_tenant(
if status in ["completed", "partial_success"]:
completed_at = calculate_datetime_from_offset(offset_days)
# Calculate duration based on realistic processing times
duration_seconds = int((completed_at - started_at).total_seconds())
# Cap duration to reasonable values
if duration_seconds > 5400: # More than 1.5 hours
duration_seconds = random.randint(1800, 3600) # 30-60 minutes
elif status == "failed":
completed_at = calculate_datetime_from_offset(offset_days - 0.5)
duration_seconds = int((completed_at - started_at).total_seconds())
if duration_seconds > 3600: # More than 1 hour
duration_seconds = random.randint(300, 1800) # 5-30 minutes
elif status == "cancelled":
completed_at = calculate_datetime_from_offset(offset_days - 0.2)
duration_seconds = int((completed_at - started_at).total_seconds())
if duration_seconds > 1800: # More than 30 minutes
duration_seconds = random.randint(60, 600) # 1-10 minutes
# Generate step timing
forecasting_started_at = started_at
@@ -352,6 +450,14 @@ async def generate_orchestration_for_tenant(
metrics["quality_score"]["max"]
)))
# Generate reasoning metadata for the orchestrator context
reasoning_metadata = generate_reasoning_metadata(
forecasts_generated,
production_batches_created,
procurement_plans_created,
purchase_orders_created
)
# Create orchestration run
run = OrchestrationRun(
id=uuid.uuid4(),
@@ -388,6 +494,7 @@ async def generate_orchestration_for_tenant(
on_time_delivery_rate=on_time_delivery_rate,
cost_accuracy=cost_accuracy,
quality_score=quality_score,
run_metadata=reasoning_metadata,
created_at=calculate_datetime_from_offset(offset_days - 2),
updated_at=calculate_datetime_from_offset(offset_days),
triggered_by="scheduler" if run_type == "scheduled" else "user" if run_type == "manual" else "test-runner"