imporve features
This commit is contained in:
123
services/external/app/api/calendar_operations.py
vendored
123
services/external/app/api/calendar_operations.py
vendored
@@ -213,17 +213,17 @@ async def check_is_school_holiday(
|
||||
response_model=TenantLocationContextResponse
|
||||
)
|
||||
async def get_tenant_location_context(
|
||||
tenant_id: UUID = Depends(get_current_user_dep),
|
||||
tenant_id: str = Path(..., description="Tenant ID"),
|
||||
current_user: dict = Depends(get_current_user_dep),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Get location context for a tenant including school calendar assignment (cached)"""
|
||||
try:
|
||||
tenant_id_str = str(tenant_id)
|
||||
|
||||
# Check cache first
|
||||
cached = await cache.get_cached_tenant_context(tenant_id_str)
|
||||
cached = await cache.get_cached_tenant_context(tenant_id)
|
||||
if cached:
|
||||
logger.debug("Returning cached tenant context", tenant_id=tenant_id_str)
|
||||
logger.debug("Returning cached tenant context", tenant_id=tenant_id)
|
||||
return TenantLocationContextResponse(**cached)
|
||||
|
||||
# Cache miss - fetch from database
|
||||
@@ -261,11 +261,16 @@ async def get_tenant_location_context(
|
||||
)
|
||||
async def create_or_update_tenant_location_context(
|
||||
request: TenantLocationContextCreateRequest,
|
||||
tenant_id: UUID = Depends(get_current_user_dep),
|
||||
tenant_id: str = Path(..., description="Tenant ID"),
|
||||
current_user: dict = Depends(get_current_user_dep),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Create or update tenant location context"""
|
||||
try:
|
||||
|
||||
# Convert to UUID for use with repository
|
||||
tenant_uuid = UUID(tenant_id)
|
||||
|
||||
repo = CalendarRepository(db)
|
||||
|
||||
# Validate calendar_id if provided
|
||||
@@ -279,7 +284,7 @@ async def create_or_update_tenant_location_context(
|
||||
|
||||
# Create or update context
|
||||
context_obj = await repo.create_or_update_tenant_location_context(
|
||||
tenant_id=tenant_id,
|
||||
tenant_id=tenant_uuid,
|
||||
city_id=request.city_id,
|
||||
school_calendar_id=request.school_calendar_id,
|
||||
neighborhood=request.neighborhood,
|
||||
@@ -288,13 +293,13 @@ async def create_or_update_tenant_location_context(
|
||||
)
|
||||
|
||||
# Invalidate cache since context was updated
|
||||
await cache.invalidate_tenant_context(str(tenant_id))
|
||||
await cache.invalidate_tenant_context(tenant_id)
|
||||
|
||||
# Get full context with calendar details
|
||||
context = await repo.get_tenant_with_calendar(tenant_id)
|
||||
context = await repo.get_tenant_with_calendar(tenant_uuid)
|
||||
|
||||
# Cache the new context
|
||||
await cache.set_cached_tenant_context(str(tenant_id), context)
|
||||
await cache.set_cached_tenant_context(tenant_id, context)
|
||||
|
||||
return TenantLocationContextResponse(**context)
|
||||
|
||||
@@ -317,13 +322,18 @@ async def create_or_update_tenant_location_context(
|
||||
status_code=204
|
||||
)
|
||||
async def delete_tenant_location_context(
|
||||
tenant_id: UUID = Depends(get_current_user_dep),
|
||||
tenant_id: str = Path(..., description="Tenant ID"),
|
||||
current_user: dict = Depends(get_current_user_dep),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""Delete tenant location context"""
|
||||
try:
|
||||
|
||||
# Convert to UUID for use with repository
|
||||
tenant_uuid = UUID(tenant_id)
|
||||
|
||||
repo = CalendarRepository(db)
|
||||
deleted = await repo.delete_tenant_location_context(tenant_id)
|
||||
deleted = await repo.delete_tenant_location_context(tenant_uuid)
|
||||
|
||||
if not deleted:
|
||||
raise HTTPException(
|
||||
@@ -347,6 +357,97 @@ async def delete_tenant_location_context(
|
||||
)
|
||||
|
||||
|
||||
# ===== Calendar Suggestion Endpoint =====
|
||||
|
||||
@router.post(
|
||||
route_builder.build_base_route("location-context/suggest-calendar")
|
||||
)
|
||||
async def suggest_calendar_for_tenant(
|
||||
tenant_id: str = Path(..., description="Tenant ID"),
|
||||
current_user: dict = Depends(get_current_user_dep),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Suggest an appropriate school calendar for a tenant based on location and POI data.
|
||||
|
||||
This endpoint analyzes:
|
||||
- Tenant's city location
|
||||
- Detected schools nearby (from POI detection)
|
||||
- Available calendars for the city
|
||||
- Bakery-specific heuristics (primary schools = stronger morning rush)
|
||||
|
||||
Returns a suggestion with confidence score and reasoning.
|
||||
Does NOT automatically assign - requires admin approval.
|
||||
"""
|
||||
try:
|
||||
from app.utils.calendar_suggester import CalendarSuggester
|
||||
from app.repositories.poi_context_repository import POIContextRepository
|
||||
|
||||
tenant_uuid = UUID(tenant_id)
|
||||
|
||||
# Get tenant's location context
|
||||
calendar_repo = CalendarRepository(db)
|
||||
location_context = await calendar_repo.get_tenant_location_context(tenant_uuid)
|
||||
|
||||
if not location_context:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Location context not found. Create location context first."
|
||||
)
|
||||
|
||||
city_id = location_context.city_id
|
||||
|
||||
# Get available calendars for city
|
||||
calendars_result = await calendar_repo.get_calendars_by_city(city_id, enabled_only=True)
|
||||
calendars = calendars_result.get("calendars", []) if calendars_result else []
|
||||
|
||||
# Get POI context if available
|
||||
poi_repo = POIContextRepository(db)
|
||||
poi_context = await poi_repo.get_by_tenant_id(tenant_uuid)
|
||||
poi_data = poi_context.to_dict() if poi_context else None
|
||||
|
||||
# Generate suggestion
|
||||
suggester = CalendarSuggester()
|
||||
suggestion = suggester.suggest_calendar_for_tenant(
|
||||
city_id=city_id,
|
||||
available_calendars=calendars,
|
||||
poi_context=poi_data,
|
||||
tenant_data=None # Could include tenant info if needed
|
||||
)
|
||||
|
||||
# Format for admin display
|
||||
admin_message = suggester.format_suggestion_for_admin(suggestion)
|
||||
|
||||
logger.info(
|
||||
"Calendar suggestion generated",
|
||||
tenant_id=tenant_id,
|
||||
city_id=city_id,
|
||||
suggested_calendar=suggestion.get("suggested_calendar_id"),
|
||||
confidence=suggestion.get("confidence")
|
||||
)
|
||||
|
||||
return {
|
||||
**suggestion,
|
||||
"admin_message": admin_message,
|
||||
"tenant_id": tenant_id,
|
||||
"current_calendar_id": str(location_context.school_calendar_id) if location_context.school_calendar_id else None
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Error generating calendar suggestion",
|
||||
tenant_id=tenant_id,
|
||||
error=str(e),
|
||||
exc_info=True
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Error generating calendar suggestion: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
# ===== Helper Endpoints =====
|
||||
|
||||
@router.get(
|
||||
|
||||
82
services/external/app/api/poi_context.py
vendored
82
services/external/app/api/poi_context.py
vendored
@@ -21,10 +21,10 @@ from app.core.redis_client import get_redis_client
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
router = APIRouter(prefix="/poi-context", tags=["POI Context"])
|
||||
router = APIRouter(prefix="/tenants", tags=["POI Context"])
|
||||
|
||||
|
||||
@router.post("/{tenant_id}/detect")
|
||||
@router.post("/{tenant_id}/poi-context/detect")
|
||||
async def detect_pois_for_tenant(
|
||||
tenant_id: str,
|
||||
latitude: float = Query(..., description="Bakery latitude"),
|
||||
@@ -209,13 +209,79 @@ async def detect_pois_for_tenant(
|
||||
relevant_categories=len(feature_selection.get("relevant_categories", []))
|
||||
)
|
||||
|
||||
# Phase 3: Auto-trigger calendar suggestion after POI detection
|
||||
# This helps admins by providing intelligent calendar recommendations
|
||||
calendar_suggestion = None
|
||||
try:
|
||||
from app.utils.calendar_suggester import CalendarSuggester
|
||||
from app.repositories.calendar_repository import CalendarRepository
|
||||
|
||||
# Get tenant's location context
|
||||
calendar_repo = CalendarRepository(db)
|
||||
location_context = await calendar_repo.get_tenant_location_context(tenant_uuid)
|
||||
|
||||
if location_context and location_context.school_calendar_id is None:
|
||||
# Only suggest if no calendar assigned yet
|
||||
city_id = location_context.city_id
|
||||
|
||||
# Get available calendars for city
|
||||
calendars_result = await calendar_repo.get_calendars_by_city(city_id, enabled_only=True)
|
||||
calendars = calendars_result.get("calendars", []) if calendars_result else []
|
||||
|
||||
if calendars:
|
||||
# Generate suggestion using POI data
|
||||
suggester = CalendarSuggester()
|
||||
calendar_suggestion = suggester.suggest_calendar_for_tenant(
|
||||
city_id=city_id,
|
||||
available_calendars=calendars,
|
||||
poi_context=poi_context.to_dict(),
|
||||
tenant_data=None
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"Calendar suggestion auto-generated after POI detection",
|
||||
tenant_id=tenant_id,
|
||||
suggested_calendar=calendar_suggestion.get("calendar_name"),
|
||||
confidence=calendar_suggestion.get("confidence_percentage"),
|
||||
should_auto_assign=calendar_suggestion.get("should_auto_assign")
|
||||
)
|
||||
|
||||
# TODO: Send notification to admin about available suggestion
|
||||
# This will be implemented when notification service is integrated
|
||||
else:
|
||||
logger.info(
|
||||
"No calendars available for city, skipping suggestion",
|
||||
tenant_id=tenant_id,
|
||||
city_id=city_id
|
||||
)
|
||||
elif location_context and location_context.school_calendar_id:
|
||||
logger.info(
|
||||
"Calendar already assigned, skipping suggestion",
|
||||
tenant_id=tenant_id,
|
||||
calendar_id=str(location_context.school_calendar_id)
|
||||
)
|
||||
else:
|
||||
logger.warning(
|
||||
"No location context found, skipping calendar suggestion",
|
||||
tenant_id=tenant_id
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
# Non-blocking: POI detection should succeed even if suggestion fails
|
||||
logger.warning(
|
||||
"Failed to auto-generate calendar suggestion (non-blocking)",
|
||||
tenant_id=tenant_id,
|
||||
error=str(e)
|
||||
)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"source": "detection",
|
||||
"poi_context": poi_context.to_dict(),
|
||||
"feature_selection": feature_selection,
|
||||
"competitor_analysis": competitor_analysis,
|
||||
"competitive_insights": competitive_insights
|
||||
"competitive_insights": competitive_insights,
|
||||
"calendar_suggestion": calendar_suggestion # Include suggestion in response
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
@@ -231,7 +297,7 @@ async def detect_pois_for_tenant(
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{tenant_id}")
|
||||
@router.get("/{tenant_id}/poi-context")
|
||||
async def get_poi_context(
|
||||
tenant_id: str,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
@@ -265,7 +331,7 @@ async def get_poi_context(
|
||||
}
|
||||
|
||||
|
||||
@router.post("/{tenant_id}/refresh")
|
||||
@router.post("/{tenant_id}/poi-context/refresh")
|
||||
async def refresh_poi_context(
|
||||
tenant_id: str,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
@@ -299,7 +365,7 @@ async def refresh_poi_context(
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/{tenant_id}")
|
||||
@router.delete("/{tenant_id}/poi-context")
|
||||
async def delete_poi_context(
|
||||
tenant_id: str,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
@@ -327,7 +393,7 @@ async def delete_poi_context(
|
||||
}
|
||||
|
||||
|
||||
@router.get("/{tenant_id}/feature-importance")
|
||||
@router.get("/{tenant_id}/poi-context/feature-importance")
|
||||
async def get_feature_importance(
|
||||
tenant_id: str,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
@@ -364,7 +430,7 @@ async def get_feature_importance(
|
||||
}
|
||||
|
||||
|
||||
@router.get("/{tenant_id}/competitor-analysis")
|
||||
@router.get("/{tenant_id}/poi-context/competitor-analysis")
|
||||
async def get_competitor_analysis(
|
||||
tenant_id: str,
|
||||
db: AsyncSession = Depends(get_db)
|
||||
|
||||
342
services/external/app/utils/calendar_suggester.py
vendored
Normal file
342
services/external/app/utils/calendar_suggester.py
vendored
Normal file
@@ -0,0 +1,342 @@
|
||||
"""
|
||||
Calendar Suggester Utility
|
||||
|
||||
Provides intelligent school calendar suggestions based on POI detection data,
|
||||
tenant location, and heuristics optimized for bakery demand forecasting.
|
||||
"""
|
||||
|
||||
from typing import Optional, Dict, List, Any, Tuple
|
||||
from datetime import datetime, date, timezone
|
||||
import structlog
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
|
||||
class CalendarSuggester:
|
||||
"""
|
||||
Suggests appropriate school calendars for tenants based on location context.
|
||||
|
||||
Uses POI detection data, proximity analysis, and bakery-specific heuristics
|
||||
to provide intelligent calendar recommendations with confidence scores.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.logger = logger
|
||||
|
||||
def suggest_calendar_for_tenant(
|
||||
self,
|
||||
city_id: str,
|
||||
available_calendars: List[Dict[str, Any]],
|
||||
poi_context: Optional[Dict[str, Any]] = None,
|
||||
tenant_data: Optional[Dict[str, Any]] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Suggest the most appropriate calendar for a tenant.
|
||||
|
||||
Args:
|
||||
city_id: Normalized city ID (e.g., "madrid")
|
||||
available_calendars: List of available school calendars for the city
|
||||
poi_context: Optional POI detection results including school data
|
||||
tenant_data: Optional tenant information (location, etc.)
|
||||
|
||||
Returns:
|
||||
Dict with:
|
||||
- suggested_calendar_id: UUID of suggested calendar or None
|
||||
- calendar_name: Name of suggested calendar
|
||||
- confidence: Float 0.0-1.0 confidence score
|
||||
- reasoning: List of reasoning steps
|
||||
- fallback_calendars: Alternative suggestions
|
||||
- should_assign: Boolean recommendation to auto-assign
|
||||
"""
|
||||
if not available_calendars:
|
||||
return self._no_calendars_available(city_id)
|
||||
|
||||
# Get current academic year
|
||||
academic_year = self._get_current_academic_year()
|
||||
|
||||
# Filter calendars for current academic year
|
||||
current_year_calendars = [
|
||||
cal for cal in available_calendars
|
||||
if cal.get("academic_year") == academic_year
|
||||
]
|
||||
|
||||
if not current_year_calendars:
|
||||
# Fallback to any calendar if current year not available
|
||||
current_year_calendars = available_calendars
|
||||
self.logger.warning(
|
||||
"No calendars for current academic year, using all available",
|
||||
city_id=city_id,
|
||||
academic_year=academic_year
|
||||
)
|
||||
|
||||
# Analyze POI context if available
|
||||
school_analysis = self._analyze_schools_from_poi(poi_context) if poi_context else None
|
||||
|
||||
# Apply bakery-specific heuristics
|
||||
suggestion = self._apply_suggestion_heuristics(
|
||||
current_year_calendars,
|
||||
school_analysis,
|
||||
city_id
|
||||
)
|
||||
|
||||
return suggestion
|
||||
|
||||
def _get_current_academic_year(self) -> str:
|
||||
"""
|
||||
Determine current academic year based on date.
|
||||
|
||||
Academic year runs September to June (Spain):
|
||||
- Jan-Aug: Previous year (e.g., 2024-2025)
|
||||
- Sep-Dec: Current year (e.g., 2025-2026)
|
||||
|
||||
Returns:
|
||||
Academic year string (e.g., "2024-2025")
|
||||
"""
|
||||
today = date.today()
|
||||
year = today.year
|
||||
|
||||
# Academic year starts in September
|
||||
if today.month >= 9: # September onwards
|
||||
return f"{year}-{year + 1}"
|
||||
else: # January-August
|
||||
return f"{year - 1}-{year}"
|
||||
|
||||
def _analyze_schools_from_poi(
|
||||
self,
|
||||
poi_context: Dict[str, Any]
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Analyze school POIs to infer school type preferences.
|
||||
|
||||
Args:
|
||||
poi_context: POI detection results
|
||||
|
||||
Returns:
|
||||
Dict with:
|
||||
- has_schools_nearby: Boolean
|
||||
- school_count: Int count of schools
|
||||
- nearest_distance: Float distance to nearest school (meters)
|
||||
- proximity_score: Float proximity score
|
||||
- school_names: List of detected school names
|
||||
"""
|
||||
try:
|
||||
poi_results = poi_context.get("poi_detection_results", {})
|
||||
schools_data = poi_results.get("schools", {})
|
||||
|
||||
if not schools_data:
|
||||
return None
|
||||
|
||||
school_pois = schools_data.get("pois", [])
|
||||
school_count = len(school_pois)
|
||||
|
||||
if school_count == 0:
|
||||
return None
|
||||
|
||||
# Extract school details
|
||||
school_names = [
|
||||
poi.get("name", "Unknown School")
|
||||
for poi in school_pois
|
||||
if poi.get("name")
|
||||
]
|
||||
|
||||
# Get proximity metrics
|
||||
features = schools_data.get("features", {})
|
||||
proximity_score = features.get("proximity_score", 0.0)
|
||||
|
||||
# Calculate nearest distance (approximate from POI data)
|
||||
nearest_distance = None
|
||||
if school_pois:
|
||||
# If we have POIs, estimate nearest distance
|
||||
# This is approximate - exact calculation would require tenant coords
|
||||
nearest_distance = 100.0 # Default assumption if schools detected
|
||||
|
||||
return {
|
||||
"has_schools_nearby": True,
|
||||
"school_count": school_count,
|
||||
"nearest_distance": nearest_distance,
|
||||
"proximity_score": proximity_score,
|
||||
"school_names": school_names
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
self.logger.warning(
|
||||
"Failed to analyze schools from POI",
|
||||
error=str(e)
|
||||
)
|
||||
return None
|
||||
|
||||
def _apply_suggestion_heuristics(
|
||||
self,
|
||||
calendars: List[Dict[str, Any]],
|
||||
school_analysis: Optional[Dict[str, Any]],
|
||||
city_id: str
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Apply heuristics to suggest best calendar.
|
||||
|
||||
Bakery-specific heuristics:
|
||||
1. If schools detected nearby -> Prefer primary (stronger morning rush)
|
||||
2. If no schools detected -> Still suggest primary (more common, safer default)
|
||||
3. Primary schools have stronger impact on bakery traffic
|
||||
|
||||
Args:
|
||||
calendars: List of available calendars
|
||||
school_analysis: Analysis of nearby schools
|
||||
city_id: City identifier
|
||||
|
||||
Returns:
|
||||
Suggestion dict with confidence and reasoning
|
||||
"""
|
||||
reasoning = []
|
||||
confidence = 0.0
|
||||
|
||||
# Separate calendars by type
|
||||
primary_calendars = [c for c in calendars if c.get("school_type") == "primary"]
|
||||
secondary_calendars = [c for c in calendars if c.get("school_type") == "secondary"]
|
||||
other_calendars = [c for c in calendars if c.get("school_type") not in ["primary", "secondary"]]
|
||||
|
||||
# Heuristic 1: Schools detected nearby
|
||||
if school_analysis and school_analysis.get("has_schools_nearby"):
|
||||
school_count = school_analysis.get("school_count", 0)
|
||||
proximity_score = school_analysis.get("proximity_score", 0.0)
|
||||
|
||||
reasoning.append(f"Detected {school_count} schools nearby (proximity score: {proximity_score:.2f})")
|
||||
|
||||
if primary_calendars:
|
||||
suggested = primary_calendars[0]
|
||||
confidence = min(0.85, 0.65 + (proximity_score * 0.1)) # 65-85% confidence
|
||||
reasoning.append("Primary schools create strong morning rush (7:30-9am drop-off)")
|
||||
reasoning.append("Primary calendars recommended for bakeries near schools")
|
||||
elif secondary_calendars:
|
||||
suggested = secondary_calendars[0]
|
||||
confidence = 0.70
|
||||
reasoning.append("Secondary school calendars available (later morning start)")
|
||||
else:
|
||||
suggested = calendars[0]
|
||||
confidence = 0.50
|
||||
reasoning.append("Using available calendar (school type not specified)")
|
||||
|
||||
# Heuristic 2: No schools detected
|
||||
else:
|
||||
reasoning.append("No schools detected within 500m radius")
|
||||
|
||||
if primary_calendars:
|
||||
suggested = primary_calendars[0]
|
||||
confidence = 0.60 # Lower confidence without detected schools
|
||||
reasoning.append("Defaulting to primary calendar (more common, safer choice)")
|
||||
reasoning.append("Primary school holidays still affect general foot traffic")
|
||||
elif secondary_calendars:
|
||||
suggested = secondary_calendars[0]
|
||||
confidence = 0.55
|
||||
reasoning.append("Secondary calendar available as default")
|
||||
elif other_calendars:
|
||||
suggested = other_calendars[0]
|
||||
confidence = 0.50
|
||||
reasoning.append("Using available calendar")
|
||||
else:
|
||||
suggested = calendars[0]
|
||||
confidence = 0.45
|
||||
reasoning.append("No preferred calendar type available")
|
||||
|
||||
# Confidence adjustment based on school analysis quality
|
||||
if school_analysis:
|
||||
if school_analysis.get("school_count", 0) >= 3:
|
||||
confidence = min(1.0, confidence + 0.05) # Boost for multiple schools
|
||||
reasoning.append("High confidence: Multiple schools detected")
|
||||
|
||||
proximity = school_analysis.get("proximity_score", 0.0)
|
||||
if proximity > 2.0:
|
||||
confidence = min(1.0, confidence + 0.05) # Boost for close proximity
|
||||
reasoning.append("High confidence: Schools very close to bakery")
|
||||
|
||||
# Determine if we should auto-assign
|
||||
# Only auto-assign if confidence >= 75% AND schools detected
|
||||
should_auto_assign = (
|
||||
confidence >= 0.75 and
|
||||
school_analysis is not None and
|
||||
school_analysis.get("has_schools_nearby", False)
|
||||
)
|
||||
|
||||
# Build fallback suggestions
|
||||
fallback_calendars = []
|
||||
for cal in calendars:
|
||||
if cal.get("id") != suggested.get("id"):
|
||||
fallback_calendars.append({
|
||||
"calendar_id": str(cal.get("id")),
|
||||
"calendar_name": cal.get("name"),
|
||||
"school_type": cal.get("school_type"),
|
||||
"academic_year": cal.get("academic_year")
|
||||
})
|
||||
|
||||
return {
|
||||
"suggested_calendar_id": str(suggested.get("id")),
|
||||
"calendar_name": suggested.get("name"),
|
||||
"school_type": suggested.get("school_type"),
|
||||
"academic_year": suggested.get("academic_year"),
|
||||
"confidence": round(confidence, 2),
|
||||
"confidence_percentage": round(confidence * 100, 1),
|
||||
"reasoning": reasoning,
|
||||
"fallback_calendars": fallback_calendars[:2], # Top 2 alternatives
|
||||
"should_auto_assign": should_auto_assign,
|
||||
"school_analysis": school_analysis,
|
||||
"city_id": city_id
|
||||
}
|
||||
|
||||
def _no_calendars_available(self, city_id: str) -> Dict[str, Any]:
|
||||
"""Return response when no calendars available for city."""
|
||||
return {
|
||||
"suggested_calendar_id": None,
|
||||
"calendar_name": None,
|
||||
"school_type": None,
|
||||
"academic_year": None,
|
||||
"confidence": 0.0,
|
||||
"confidence_percentage": 0.0,
|
||||
"reasoning": [
|
||||
f"No school calendars configured for city: {city_id}",
|
||||
"Calendar assignment not possible at this time",
|
||||
"Location context created without calendar (can be added later)"
|
||||
],
|
||||
"fallback_calendars": [],
|
||||
"should_auto_assign": False,
|
||||
"school_analysis": None,
|
||||
"city_id": city_id
|
||||
}
|
||||
|
||||
def format_suggestion_for_admin(self, suggestion: Dict[str, Any]) -> str:
|
||||
"""
|
||||
Format suggestion as human-readable text for admin UI.
|
||||
|
||||
Args:
|
||||
suggestion: Suggestion dict from suggest_calendar_for_tenant
|
||||
|
||||
Returns:
|
||||
Formatted string for display
|
||||
"""
|
||||
if not suggestion.get("suggested_calendar_id"):
|
||||
return f"⚠️ No calendars available for {suggestion.get('city_id', 'this city')}"
|
||||
|
||||
confidence_pct = suggestion.get("confidence_percentage", 0)
|
||||
calendar_name = suggestion.get("calendar_name", "Unknown")
|
||||
school_type = suggestion.get("school_type", "").capitalize()
|
||||
|
||||
# Confidence emoji
|
||||
if confidence_pct >= 80:
|
||||
emoji = "✅"
|
||||
elif confidence_pct >= 60:
|
||||
emoji = "📊"
|
||||
else:
|
||||
emoji = "💡"
|
||||
|
||||
text = f"{emoji} **Suggested**: {calendar_name}\n"
|
||||
text += f"**Type**: {school_type} | **Confidence**: {confidence_pct}%\n\n"
|
||||
text += "**Reasoning**:\n"
|
||||
|
||||
for reason in suggestion.get("reasoning", []):
|
||||
text += f"• {reason}\n"
|
||||
|
||||
if suggestion.get("fallback_calendars"):
|
||||
text += "\n**Alternatives**:\n"
|
||||
for alt in suggestion.get("fallback_calendars", [])[:2]:
|
||||
text += f"• {alt.get('calendar_name')} ({alt.get('school_type')})\n"
|
||||
|
||||
return text
|
||||
Reference in New Issue
Block a user