feat: Implement structured reasoning_data generation for i18n support

Implemented proper reasoning data generation for purchase orders and
production batches to enable multilingual dashboard support.

Backend Strategy:
- Generate structured JSON with type codes and parameters
- Store only reasoning_data (JSONB), not hardcoded text
- Frontend will translate using i18n libraries

Changes:
1. Created shared/schemas/reasoning_types.py
   - Defined reasoning types for POs and batches
   - Created helper functions for common reasoning patterns
   - Supports multiple reasoning types (low_stock, forecast_demand, etc.)

2. Production Service (services/production/app/services/production_service.py)
   - Generate reasoning_data when creating batches from forecast
   - Include parameters: product_name, predicted_demand, current_stock, etc.
   - Structure supports frontend i18n interpolation

3. Procurement Service (services/procurement/app/services/procurement_service.py)
   - Implemented actual PO creation (was placeholder before!)
   - Groups requirements by supplier
   - Generates reasoning_data based on context (low_stock vs forecast)
   - Creates PO items automatically

Example reasoning_data:
{
  "type": "low_stock_detection",
  "parameters": {
    "supplier_name": "Harinas del Norte",
    "product_names": ["Flour Type 55", "Flour Type 45"],
    "days_until_stockout": 3,
    "current_stock": 45.5,
    "required_stock": 200
  },
  "consequence": {
    "type": "stockout_risk",
    "severity": "high",
    "impact_days": 3
  }
}

Frontend will translate:
- EN: "Low stock detected for Harinas del Norte. Stock runs out in 3 days."
- ES: "Stock bajo detectado para Harinas del Norte. Se agota en 3 días."
- CA: "Estoc baix detectat per Harinas del Norte. S'esgota en 3 dies."

Next steps:
- Remove TEXT fields (reasoning, consequence) from models
- Update dashboard service to use reasoning_data
- Create frontend i18n translation keys
- Update dashboard components to translate dynamically
This commit is contained in:
Claude
2025-11-07 18:16:44 +00:00
parent 6ee8c055ee
commit ddc4928d78
3 changed files with 446 additions and 2 deletions

View File

@@ -0,0 +1,267 @@
"""
Reasoning Types for JTBD Dashboard
Defines standard reasoning data structures for Purchase Orders and Production Batches.
Backend generates structured data, frontend translates using i18n.
IMPORTANT: All text displayed to users is translated in the frontend.
Backend only stores type codes and parameters.
"""
from enum import Enum
from typing import Dict, Any, Optional
from pydantic import BaseModel, Field
# ============================================================
# Reasoning Types
# ============================================================
class PurchaseOrderReasoningType(str, Enum):
"""Types of reasoning for why a purchase order was created"""
LOW_STOCK_DETECTION = "low_stock_detection"
FORECAST_DEMAND = "forecast_demand"
SAFETY_STOCK_REPLENISHMENT = "safety_stock_replenishment"
SUPPLIER_CONTRACT = "supplier_contract"
SEASONAL_DEMAND = "seasonal_demand"
PRODUCTION_REQUIREMENT = "production_requirement"
MANUAL_REQUEST = "manual_request"
class ProductionBatchReasoningType(str, Enum):
"""Types of reasoning for why a production batch was created"""
FORECAST_DEMAND = "forecast_demand"
CUSTOMER_ORDER = "customer_order"
STOCK_REPLENISHMENT = "stock_replenishment"
SEASONAL_PREPARATION = "seasonal_preparation"
PROMOTION_EVENT = "promotion_event"
URGENT_ORDER = "urgent_order"
REGULAR_SCHEDULE = "regular_schedule"
class ConsequenceSeverity(str, Enum):
"""Severity level of consequences if action is not taken"""
CRITICAL = "critical" # Immediate business impact
HIGH = "high" # Significant impact within 48h
MEDIUM = "medium" # Moderate impact within week
LOW = "low" # Minor impact
# ============================================================
# Reasoning Data Models
# ============================================================
class PurchaseOrderReasoningData(BaseModel):
"""
Structured reasoning data for purchase orders
Example:
{
"type": "low_stock_detection",
"parameters": {
"supplier_name": "Harinas del Norte",
"product_names": ["Flour Type 55", "Flour Type 45"],
"current_stock_kg": 45.5,
"required_stock_kg": 200,
"days_until_stockout": 3,
"threshold_percentage": 20
},
"consequence": {
"type": "stockout_risk",
"severity": "high",
"impact_days": 3,
"affected_products": ["Baguette", "Croissant"],
"estimated_lost_orders": 15
},
"metadata": {
"trigger_source": "orchestrator_auto",
"forecast_confidence": 0.85,
"ai_assisted": true
}
}
"""
type: PurchaseOrderReasoningType = Field(..., description="Type of reasoning")
parameters: Dict[str, Any] = Field(..., description="Parameters for i18n interpolation")
consequence: Optional[Dict[str, Any]] = Field(None, description="What happens if not approved")
metadata: Optional[Dict[str, Any]] = Field(None, description="Additional context")
class ProductionBatchReasoningData(BaseModel):
"""
Structured reasoning data for production batches
Example:
{
"type": "forecast_demand",
"parameters": {
"product_name": "Croissant",
"predicted_demand": 500,
"current_stock": 120,
"production_needed": 380,
"target_date": "2025-11-08",
"confidence_score": 0.87
},
"urgency": {
"level": "normal",
"ready_by_time": "08:00",
"customer_commitment": false
},
"metadata": {
"trigger_source": "orchestrator_auto",
"forecast_id": "uuid-here",
"ai_assisted": true
}
}
"""
type: ProductionBatchReasoningType = Field(..., description="Type of reasoning")
parameters: Dict[str, Any] = Field(..., description="Parameters for i18n interpolation")
urgency: Optional[Dict[str, Any]] = Field(None, description="Urgency information")
metadata: Optional[Dict[str, Any]] = Field(None, description="Additional context")
# ============================================================
# Helper Functions
# ============================================================
def create_po_reasoning_low_stock(
supplier_name: str,
product_names: list,
current_stock: float,
required_stock: float,
days_until_stockout: int,
threshold_percentage: int = 20,
affected_products: Optional[list] = None,
estimated_lost_orders: Optional[int] = None
) -> Dict[str, Any]:
"""
Create reasoning data for low stock detection
Args:
supplier_name: Name of the supplier
product_names: List of product names in the order
current_stock: Current stock level
required_stock: Required stock level
days_until_stockout: Days until stock runs out
threshold_percentage: Stock threshold percentage
affected_products: Products that will be affected
estimated_lost_orders: Estimated number of lost orders
Returns:
Reasoning data dictionary
"""
return {
"type": PurchaseOrderReasoningType.LOW_STOCK_DETECTION.value,
"parameters": {
"supplier_name": supplier_name,
"product_names": product_names,
"product_count": len(product_names),
"current_stock": current_stock,
"required_stock": required_stock,
"days_until_stockout": days_until_stockout,
"threshold_percentage": threshold_percentage,
"stock_percentage": round((current_stock / required_stock * 100), 1) if required_stock > 0 else 0
},
"consequence": {
"type": "stockout_risk",
"severity": ConsequenceSeverity.HIGH.value if days_until_stockout <= 2 else ConsequenceSeverity.MEDIUM.value,
"impact_days": days_until_stockout,
"affected_products": affected_products or [],
"estimated_lost_orders": estimated_lost_orders or 0
},
"metadata": {
"trigger_source": "orchestrator_auto",
"ai_assisted": True
}
}
def create_po_reasoning_forecast_demand(
supplier_name: str,
product_names: list,
forecast_period_days: int,
total_demand: float,
forecast_confidence: float = 0.85
) -> Dict[str, Any]:
"""Create reasoning data for forecast-based demand"""
return {
"type": PurchaseOrderReasoningType.FORECAST_DEMAND.value,
"parameters": {
"supplier_name": supplier_name,
"product_names": product_names,
"product_count": len(product_names),
"forecast_period_days": forecast_period_days,
"total_demand": total_demand,
"forecast_confidence": round(forecast_confidence * 100, 1)
},
"consequence": {
"type": "insufficient_supply",
"severity": ConsequenceSeverity.MEDIUM.value,
"impact_days": forecast_period_days
},
"metadata": {
"trigger_source": "orchestrator_auto",
"forecast_confidence": forecast_confidence,
"ai_assisted": True
}
}
def create_batch_reasoning_forecast_demand(
product_name: str,
predicted_demand: float,
current_stock: float,
production_needed: float,
target_date: str,
confidence_score: float = 0.85
) -> Dict[str, Any]:
"""Create reasoning data for forecast-based production"""
return {
"type": ProductionBatchReasoningType.FORECAST_DEMAND.value,
"parameters": {
"product_name": product_name,
"predicted_demand": round(predicted_demand, 1),
"current_stock": round(current_stock, 1),
"production_needed": round(production_needed, 1),
"target_date": target_date,
"confidence_score": round(confidence_score * 100, 1)
},
"urgency": {
"level": "normal",
"ready_by_time": "08:00",
"customer_commitment": False
},
"metadata": {
"trigger_source": "orchestrator_auto",
"confidence_score": confidence_score,
"ai_assisted": True
}
}
def create_batch_reasoning_customer_order(
product_name: str,
customer_name: str,
order_quantity: float,
delivery_date: str,
order_number: str
) -> Dict[str, Any]:
"""Create reasoning data for customer order fulfillment"""
return {
"type": ProductionBatchReasoningType.CUSTOMER_ORDER.value,
"parameters": {
"product_name": product_name,
"customer_name": customer_name,
"order_quantity": round(order_quantity, 1),
"delivery_date": delivery_date,
"order_number": order_number
},
"urgency": {
"level": "high",
"ready_by_time": "07:00",
"customer_commitment": True
},
"metadata": {
"trigger_source": "customer_order",
"ai_assisted": False
}
}