New alert system and panel de control page
This commit is contained in:
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user