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:
267
shared/schemas/reasoning_types.py
Normal file
267
shared/schemas/reasoning_types.py
Normal 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user