Improve AI logic

This commit is contained in:
Urtzi Alfaro
2025-11-05 13:34:56 +01:00
parent 5c87fbcf48
commit 394ad3aea4
218 changed files with 30627 additions and 7658 deletions

View File

@@ -24,8 +24,8 @@ logger = structlog.get_logger()
class ProcurementServiceClient(BaseServiceClient):
"""Enhanced client for communicating with the Procurement Service"""
def __init__(self, config: BaseServiceSettings):
super().__init__("procurement", config)
def __init__(self, config: BaseServiceSettings, calling_service_name: str = "unknown"):
super().__init__(calling_service_name, config)
def get_service_base_path(self) -> str:
return "/api/v1"
@@ -63,7 +63,7 @@ class ProcurementServiceClient(BaseServiceClient):
recipes_data: Optional recipes snapshot (NEW - to avoid duplicate fetching)
"""
try:
path = f"/tenants/{tenant_id}/procurement/auto-generate"
path = f"/tenants/{tenant_id}/procurement/operations/auto-generate"
payload = {
"forecast_data": forecast_data,
"production_schedule_id": production_schedule_id,
@@ -84,7 +84,9 @@ class ProcurementServiceClient(BaseServiceClient):
tenant_id=tenant_id,
has_forecast_data=bool(forecast_data))
response = await self._post(path, json=payload)
# Remove tenant_id from path since it's passed as separate parameter
endpoint = f"procurement/operations/auto-generate"
response = await self.post(endpoint, data=payload, tenant_id=tenant_id)
return response
except Exception as e:
@@ -127,7 +129,7 @@ class ProcurementServiceClient(BaseServiceClient):
- items: List of plan items with full metadata
"""
try:
path = f"/tenants/{tenant_id}/replenishment-plans/generate"
path = f"/tenants/{tenant_id}/procurement/operations/replenishment-plans/generate"
payload = {
"tenant_id": tenant_id,
"requirements": requirements,
@@ -142,7 +144,9 @@ class ProcurementServiceClient(BaseServiceClient):
tenant_id=tenant_id,
requirements_count=len(requirements))
response = await self._post(path, json=payload)
# Remove tenant_id from path since it's passed as separate parameter
endpoint = f"procurement/operations/replenishment-plans/generate"
response = await self.post(endpoint, data=payload, tenant_id=tenant_id)
return response
except Exception as e:
@@ -166,7 +170,7 @@ class ProcurementServiceClient(BaseServiceClient):
Dict with complete plan details
"""
try:
path = f"/tenants/{tenant_id}/replenishment-plans/{plan_id}"
path = f"/tenants/{tenant_id}/procurement/replenishment-plans/{plan_id}"
logger.debug("Getting replenishment plan",
tenant_id=tenant_id, plan_id=plan_id)
@@ -199,7 +203,7 @@ class ProcurementServiceClient(BaseServiceClient):
List of plan summaries
"""
try:
path = f"/tenants/{tenant_id}/replenishment-plans"
path = f"/tenants/{tenant_id}/procurement/operations/replenishment-plans"
params = {"skip": skip, "limit": limit}
if status:
params["status"] = status
@@ -250,7 +254,7 @@ class ProcurementServiceClient(BaseServiceClient):
- stockout_risk: Risk level (low/medium/high/critical)
"""
try:
path = f"/tenants/{tenant_id}/replenishment-plans/inventory-projections/project"
path = f"/tenants/{tenant_id}/procurement/operations/replenishment-plans/inventory-projections/project"
payload = {
"ingredient_id": ingredient_id,
"ingredient_name": ingredient_name,
@@ -264,7 +268,9 @@ class ProcurementServiceClient(BaseServiceClient):
logger.info("Projecting inventory",
tenant_id=tenant_id, ingredient_id=ingredient_id)
response = await self._post(path, json=payload)
# Remove tenant_id from path since it's passed as separate parameter
endpoint = f"procurement/operations/replenishment-plans/inventory-projections/project"
response = await self.post(endpoint, data=payload, tenant_id=tenant_id)
return response
except Exception as e:
@@ -296,7 +302,7 @@ class ProcurementServiceClient(BaseServiceClient):
List of inventory projections
"""
try:
path = f"/tenants/{tenant_id}/replenishment-plans/inventory-projections"
path = f"/tenants/{tenant_id}/procurement/operations/replenishment-plans/inventory-projections"
params = {
"skip": skip,
"limit": limit,
@@ -345,7 +351,7 @@ class ProcurementServiceClient(BaseServiceClient):
- reasoning: Explanation
"""
try:
path = f"/tenants/{tenant_id}/replenishment-plans/safety-stock/calculate"
path = f"/tenants/{tenant_id}/procurement/operations/replenishment-plans/safety-stock/calculate"
payload = {
"ingredient_id": ingredient_id,
"daily_demands": daily_demands,
@@ -353,7 +359,9 @@ class ProcurementServiceClient(BaseServiceClient):
"service_level": service_level
}
response = await self._post(path, json=payload)
# Remove tenant_id from path since it's passed as separate parameter
endpoint = f"procurement/operations/replenishment-plans/safety-stock/calculate"
response = await self.post(endpoint, data=payload, tenant_id=tenant_id)
return response
except Exception as e:
@@ -391,7 +399,7 @@ class ProcurementServiceClient(BaseServiceClient):
- diversification_applied: Whether diversification was applied
"""
try:
path = f"/tenants/{tenant_id}/replenishment-plans/supplier-selections/evaluate"
path = f"/tenants/{tenant_id}/procurement/operations/replenishment-plans/supplier-selections/evaluate"
payload = {
"ingredient_id": ingredient_id,
"ingredient_name": ingredient_name,
@@ -399,7 +407,9 @@ class ProcurementServiceClient(BaseServiceClient):
"supplier_options": supplier_options
}
response = await self._post(path, json=payload)
# Remove tenant_id from path since it's passed as separate parameter
endpoint = f"procurement/operations/replenishment-plans/supplier-selections/evaluate"
response = await self.post(endpoint, data=payload, tenant_id=tenant_id)
return response
except Exception as e:
@@ -429,7 +439,7 @@ class ProcurementServiceClient(BaseServiceClient):
List of supplier allocations
"""
try:
path = f"/tenants/{tenant_id}/replenishment-plans/supplier-allocations"
path = f"/tenants/{tenant_id}/procurement/operations/replenishment-plans/supplier-allocations"
params = {"skip": skip, "limit": limit}
if requirement_id:
params["requirement_id"] = requirement_id
@@ -470,7 +480,7 @@ class ProcurementServiceClient(BaseServiceClient):
- stockout_prevention_rate: Effectiveness metric
"""
try:
path = f"/tenants/{tenant_id}/replenishment-plans/analytics"
path = f"/tenants/{tenant_id}/procurement/analytics/replenishment-plans"
params = {}
if start_date:
params["start_date"] = start_date
@@ -484,3 +494,82 @@ class ProcurementServiceClient(BaseServiceClient):
logger.error("Error getting replenishment analytics",
tenant_id=tenant_id, error=str(e))
return None
# ================================================================
# ML INSIGHTS: Supplier Analysis and Price Forecasting
# ================================================================
async def trigger_supplier_analysis(
self,
tenant_id: str,
supplier_ids: Optional[List[str]] = None,
lookback_days: int = 180,
min_orders: int = 10
) -> Optional[Dict[str, Any]]:
"""
Trigger supplier performance analysis.
Args:
tenant_id: Tenant UUID
supplier_ids: Specific supplier IDs to analyze. If None, analyzes all suppliers
lookback_days: Days of historical orders to analyze (30-730)
min_orders: Minimum orders required for analysis (5-100)
Returns:
Dict with analysis results including insights posted
"""
try:
data = {
"supplier_ids": supplier_ids,
"lookback_days": lookback_days,
"min_orders": min_orders
}
result = await self.post("procurement/ml/insights/analyze-suppliers", data=data, tenant_id=tenant_id)
if result:
logger.info("Triggered supplier analysis",
suppliers_analyzed=result.get('suppliers_analyzed', 0),
insights_posted=result.get('total_insights_posted', 0),
tenant_id=tenant_id)
return result
except Exception as e:
logger.error("Error triggering supplier analysis",
error=str(e), tenant_id=tenant_id)
return None
async def trigger_price_forecasting(
self,
tenant_id: str,
ingredient_ids: Optional[List[str]] = None,
lookback_days: int = 180,
forecast_horizon_days: int = 30
) -> Optional[Dict[str, Any]]:
"""
Trigger price forecasting for procurement ingredients.
Args:
tenant_id: Tenant UUID
ingredient_ids: Specific ingredient IDs to forecast. If None, forecasts all ingredients
lookback_days: Days of historical price data to analyze (90-730)
forecast_horizon_days: Days to forecast ahead (7-90)
Returns:
Dict with forecasting results including insights posted
"""
try:
data = {
"ingredient_ids": ingredient_ids,
"lookback_days": lookback_days,
"forecast_horizon_days": forecast_horizon_days
}
result = await self.post("procurement/ml/insights/forecast-prices", data=data, tenant_id=tenant_id)
if result:
logger.info("Triggered price forecasting",
ingredients_forecasted=result.get('ingredients_forecasted', 0),
insights_posted=result.get('total_insights_posted', 0),
buy_now_recommendations=result.get('buy_now_recommendations', 0),
tenant_id=tenant_id)
return result
except Exception as e:
logger.error("Error triggering price forecasting",
error=str(e), tenant_id=tenant_id)
return None