update
This commit is contained in:
Binary file not shown.
@@ -8,8 +8,8 @@ from pathlib import Path
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||
from src.config.database import Base
|
||||
from src.config.settings import settings
|
||||
from src.shared.config.database import Base
|
||||
from src.shared.config.settings import settings
|
||||
from src.models import *
|
||||
config = context.config
|
||||
if config.config_file_name is not None:
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,6 +1,6 @@
|
||||
import uvicorn
|
||||
from src.config.settings import settings
|
||||
from src.config.logging_config import setup_logging, get_logger
|
||||
from src.shared.config.settings import settings
|
||||
from src.shared.config.logging_config import setup_logging, get_logger
|
||||
setup_logging()
|
||||
logger = get_logger(__name__)
|
||||
if __name__ == '__main__':
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Script to run integration tests for the Hotel Booking API
|
||||
|
||||
echo "Running integration tests for Hotel Booking API..."
|
||||
echo "=================================================="
|
||||
|
||||
# Change to Backend directory
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
# Run pytest with integration marker
|
||||
pytest src/tests/ -v -m integration --tb=short
|
||||
|
||||
# Exit with pytest's exit code
|
||||
exit $?
|
||||
|
||||
@@ -11,7 +11,7 @@ from pathlib import Path
|
||||
backend_dir = Path(__file__).parent
|
||||
sys.path.insert(0, str(backend_dir))
|
||||
|
||||
from src.config.database import SessionLocal
|
||||
from src.shared.config.database import SessionLocal
|
||||
from src.models.role import Role
|
||||
|
||||
def add_accountant_role():
|
||||
|
||||
@@ -6,7 +6,7 @@ from pathlib import Path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
from src.config.database import SessionLocal
|
||||
from src.shared.config.database import SessionLocal
|
||||
from src.models.blog import BlogPost
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import json
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
from src.config.database import SessionLocal
|
||||
from src.shared.config.database import SessionLocal
|
||||
from src.models.page_content import PageContent, PageType
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import os
|
||||
from datetime import datetime, timedelta
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
|
||||
from sqlalchemy.orm import Session
|
||||
from src.config.database import SessionLocal
|
||||
from src.shared.config.database import SessionLocal
|
||||
from src.models.banner import Banner
|
||||
from src.models.system_settings import SystemSettings
|
||||
from src.models.user import User
|
||||
|
||||
@@ -7,7 +7,7 @@ import os
|
||||
from pathlib import Path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
from sqlalchemy.orm import Session
|
||||
from src.config.database import SessionLocal
|
||||
from src.shared.config.database import SessionLocal
|
||||
from src.models.blog import BlogPost
|
||||
from src.models.user import User
|
||||
from src.models.role import Role
|
||||
|
||||
@@ -3,7 +3,7 @@ import os
|
||||
import json
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
|
||||
from sqlalchemy.orm import Session
|
||||
from src.config.database import SessionLocal
|
||||
from src.shared.config.database import SessionLocal
|
||||
from src.models.page_content import PageContent, PageType
|
||||
|
||||
def seed_homepage_content(db: Session):
|
||||
|
||||
@@ -3,7 +3,7 @@ import os
|
||||
from pathlib import Path
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
from sqlalchemy.orm import Session
|
||||
from src.config.database import SessionLocal
|
||||
from src.shared.config.database import SessionLocal
|
||||
from src.models.role import Role
|
||||
from src.models.room_type import RoomType
|
||||
from src.models.user import User
|
||||
|
||||
@@ -3,7 +3,7 @@ import os
|
||||
from pathlib import Path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
from sqlalchemy.orm import Session
|
||||
from src.config.database import SessionLocal
|
||||
from src.shared.config.database import SessionLocal
|
||||
from src.models.loyalty_reward import LoyaltyReward, RewardType
|
||||
from src.models.loyalty_tier import LoyaltyTier, TierLevel
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
@@ -3,7 +3,7 @@ import os
|
||||
import json
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
|
||||
from sqlalchemy.orm import Session
|
||||
from src.config.database import SessionLocal, engine
|
||||
from src.shared.config.database import SessionLocal, engine
|
||||
from src.models.page_content import PageContent
|
||||
from src.models.user import User
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import json
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
from src.config.database import SessionLocal
|
||||
from src.shared.config.database import SessionLocal
|
||||
from src.models.page_content import PageContent, PageType
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import os
|
||||
from pathlib import Path
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
from sqlalchemy.orm import Session
|
||||
from src.config.database import SessionLocal, engine
|
||||
from src.shared.config.database import SessionLocal, engine
|
||||
from src.models.room import Room, RoomStatus
|
||||
from src.models.room_type import RoomType
|
||||
from datetime import datetime
|
||||
|
||||
@@ -3,7 +3,7 @@ import os
|
||||
from pathlib import Path
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
from sqlalchemy.orm import Session
|
||||
from src.config.database import SessionLocal
|
||||
from src.shared.config.database import SessionLocal
|
||||
from src.models.role import Role
|
||||
from src.models.user import User
|
||||
import bcrypt
|
||||
|
||||
Binary file not shown.
Binary file not shown.
0
Backend/src/ai/__init__.py
Normal file
0
Backend/src/ai/__init__.py
Normal file
0
Backend/src/ai/models/__init__.py
Normal file
0
Backend/src/ai/models/__init__.py
Normal file
138
Backend/src/ai/models/ai_conversation.py
Normal file
138
Backend/src/ai/models/ai_conversation.py
Normal file
@@ -0,0 +1,138 @@
|
||||
"""
|
||||
AI Conversation History and Learning Models
|
||||
Stores conversation history and learned patterns for self-training
|
||||
"""
|
||||
|
||||
from sqlalchemy import Column, Integer, String, Text, ForeignKey, DateTime, Boolean, Numeric, JSON, Index
|
||||
from sqlalchemy.orm import relationship
|
||||
from datetime import datetime
|
||||
from ...shared.config.database import Base
|
||||
|
||||
|
||||
class AIConversation(Base):
|
||||
"""Stores AI assistant conversation history"""
|
||||
__tablename__ = 'ai_conversations'
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
||||
user_id = Column(Integer, ForeignKey('users.id'), nullable=False, index=True)
|
||||
session_id = Column(String(100), nullable=True, index=True) # For grouping related conversations
|
||||
user_query = Column(Text, nullable=False)
|
||||
ai_response = Column(Text, nullable=False)
|
||||
intent = Column(String(100), nullable=True, index=True)
|
||||
context_used = Column(JSON, nullable=True) # Store context data used
|
||||
user_role = Column(String(50), nullable=True, index=True)
|
||||
response_time_ms = Column(Integer, nullable=True) # Response time in milliseconds
|
||||
is_helpful = Column(Boolean, nullable=True) # User feedback on helpfulness
|
||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False, index=True)
|
||||
|
||||
# Relationships
|
||||
user = relationship('User', foreign_keys=[user_id])
|
||||
feedbacks = relationship('AIConversationFeedback', back_populates='conversation', cascade='all, delete-orphan')
|
||||
learned_patterns = relationship('AILearnedPattern', back_populates='source_conversation')
|
||||
|
||||
__table_args__ = (
|
||||
Index('idx_ai_conv_user_date', 'user_id', 'created_at'),
|
||||
Index('idx_ai_conv_intent', 'intent'),
|
||||
Index('idx_ai_conv_session', 'session_id'),
|
||||
)
|
||||
|
||||
|
||||
class AIConversationFeedback(Base):
|
||||
"""User feedback on AI responses for learning"""
|
||||
__tablename__ = 'ai_conversation_feedbacks'
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
||||
conversation_id = Column(Integer, ForeignKey('ai_conversations.id'), nullable=False, index=True)
|
||||
user_id = Column(Integer, ForeignKey('users.id'), nullable=False)
|
||||
rating = Column(Integer, nullable=True) # 1-5 rating
|
||||
is_helpful = Column(Boolean, nullable=True)
|
||||
is_correct = Column(Boolean, nullable=True)
|
||||
feedback_text = Column(Text, nullable=True) # User's text feedback
|
||||
correction = Column(Text, nullable=True) # If user provides corrected answer
|
||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||
|
||||
# Relationships
|
||||
conversation = relationship('AIConversation', back_populates='feedbacks')
|
||||
user = relationship('User', foreign_keys=[user_id])
|
||||
|
||||
|
||||
class AILearnedPattern(Base):
|
||||
"""Learned patterns from conversations for pattern matching"""
|
||||
__tablename__ = 'ai_learned_patterns'
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
||||
pattern_keywords = Column(Text, nullable=False) # Keywords extracted from query
|
||||
query_pattern = Column(Text, nullable=False) # Pattern template
|
||||
intent = Column(String(100), nullable=False, index=True)
|
||||
response_template = Column(Text, nullable=True) # Response template
|
||||
context_keys = Column(JSON, nullable=True) # Which context keys to use
|
||||
confidence_score = Column(Numeric(5, 2), nullable=False, default=0.0) # Confidence 0-100
|
||||
usage_count = Column(Integer, nullable=False, default=0) # How many times used
|
||||
success_count = Column(Integer, nullable=False, default=0) # How many successful matches
|
||||
source_conversation_id = Column(Integer, ForeignKey('ai_conversations.id'), nullable=True)
|
||||
user_role = Column(String(50), nullable=True, index=True) # Role-specific patterns
|
||||
is_active = Column(Boolean, nullable=False, default=True)
|
||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
||||
last_used_at = Column(DateTime, nullable=True)
|
||||
|
||||
# Relationships
|
||||
source_conversation = relationship('AIConversation', back_populates='learned_patterns')
|
||||
|
||||
__table_args__ = (
|
||||
Index('idx_ai_pattern_intent_role', 'intent', 'user_role'),
|
||||
Index('idx_ai_pattern_active', 'is_active', 'confidence_score'),
|
||||
)
|
||||
|
||||
|
||||
class AIKnowledgeEntry(Base):
|
||||
"""Dynamic knowledge base entries learned from interactions"""
|
||||
__tablename__ = 'ai_knowledge_entries'
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
||||
topic = Column(String(200), nullable=False, index=True)
|
||||
question = Column(Text, nullable=False) # Common question format
|
||||
answer = Column(Text, nullable=False) # Best answer
|
||||
keywords = Column(JSON, nullable=True) # Keywords for matching
|
||||
related_intent = Column(String(100), nullable=True, index=True)
|
||||
source = Column(String(100), nullable=True) # 'user_feedback', 'admin_added', 'learned'
|
||||
confidence = Column(Numeric(5, 2), nullable=False, default=50.0) # 0-100
|
||||
usage_count = Column(Integer, nullable=False, default=0)
|
||||
success_count = Column(Integer, nullable=False, default=0)
|
||||
user_role = Column(String(50), nullable=True, index=True) # Role-specific knowledge
|
||||
is_verified = Column(Boolean, nullable=False, default=False) # Verified by admin
|
||||
created_by_user_id = Column(Integer, ForeignKey('users.id'), nullable=True)
|
||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
||||
last_used_at = Column(DateTime, nullable=True)
|
||||
|
||||
# Relationships
|
||||
created_by = relationship('User', foreign_keys=[created_by_user_id])
|
||||
|
||||
__table_args__ = (
|
||||
Index('idx_ai_knowledge_topic_role', 'topic', 'user_role'),
|
||||
Index('idx_ai_knowledge_active', 'is_verified', 'confidence'),
|
||||
)
|
||||
|
||||
|
||||
class AITrainingMetrics(Base):
|
||||
"""Metrics for tracking AI training and improvement"""
|
||||
__tablename__ = 'ai_training_metrics'
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
||||
metric_date = Column(DateTime, nullable=False, index=True) # Date of metrics
|
||||
total_conversations = Column(Integer, nullable=False, default=0)
|
||||
total_patterns_learned = Column(Integer, nullable=False, default=0)
|
||||
total_knowledge_entries = Column(Integer, nullable=False, default=0)
|
||||
average_response_time_ms = Column(Integer, nullable=True)
|
||||
average_rating = Column(Numeric(3, 2), nullable=True) # Average user rating
|
||||
helpful_rate = Column(Numeric(5, 2), nullable=True) # Percentage of helpful responses
|
||||
correct_rate = Column(Numeric(5, 2), nullable=True) # Percentage of correct responses
|
||||
pattern_match_success_rate = Column(Numeric(5, 2), nullable=True)
|
||||
knowledge_usage_rate = Column(Numeric(5, 2), nullable=True)
|
||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||
|
||||
__table_args__ = (
|
||||
Index('idx_ai_metrics_date', 'metric_date'),
|
||||
)
|
||||
|
||||
@@ -2,7 +2,7 @@ from sqlalchemy import Column, Integer, String, Text, ForeignKey, DateTime, Enum
|
||||
from sqlalchemy.orm import relationship
|
||||
from datetime import datetime
|
||||
import enum
|
||||
from ..config.database import Base
|
||||
from ...shared.config.database import Base
|
||||
|
||||
class ChatStatus(str, enum.Enum):
|
||||
pending = 'pending'
|
||||
0
Backend/src/ai/routes/__init__.py
Normal file
0
Backend/src/ai/routes/__init__.py
Normal file
295
Backend/src/ai/routes/ai_assistant_routes.py
Normal file
295
Backend/src/ai/routes/ai_assistant_routes.py
Normal file
@@ -0,0 +1,295 @@
|
||||
"""
|
||||
AI Assistant API Routes
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel
|
||||
|
||||
from ...shared.config.database import get_db
|
||||
from ...security.middleware.auth import get_current_user
|
||||
from ...auth.models.user import User
|
||||
from ...auth.models.role import Role
|
||||
from ..services.ai_chat_service import AIChatService
|
||||
from ..services.ai_assistant_service import AIAssistantService
|
||||
from ..services.ai_learning_service import AILearningService
|
||||
from ..services.ai_training_scheduler import get_training_scheduler
|
||||
from ...shared.config.logging_config import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/ai-assistant", tags=["AI Assistant"])
|
||||
|
||||
|
||||
class ChatMessageRequest(BaseModel):
|
||||
message: str
|
||||
context: Optional[dict] = None
|
||||
|
||||
|
||||
class ChatMessageResponse(BaseModel):
|
||||
response: str
|
||||
intent: str
|
||||
data_used: dict
|
||||
timestamp: str
|
||||
|
||||
|
||||
@router.post("/chat")
|
||||
async def chat_with_ai(
|
||||
request: ChatMessageRequest,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Chat with AI assistant
|
||||
Accessible to all authenticated users (admin, staff, accountant, customer)
|
||||
Responses are filtered based on user role
|
||||
"""
|
||||
try:
|
||||
# Load user role
|
||||
user_role = db.query(Role).filter(Role.id == current_user.role_id).first()
|
||||
if not user_role:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="User role not found"
|
||||
)
|
||||
|
||||
# Initialize AI chat service with current user for role-based responses
|
||||
ai_service = AIChatService(db, current_user)
|
||||
|
||||
# Generate response with role awareness
|
||||
result = ai_service.generate_response(
|
||||
user_query=request.message,
|
||||
context=request.context
|
||||
)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"data": {
|
||||
"response": result["response"],
|
||||
"intent": result.get("intent", "unknown"),
|
||||
"data_used": result.get("data_used", {}),
|
||||
"timestamp": result.get("timestamp", ""),
|
||||
"user_role": user_role.name
|
||||
}
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error in AI chat: {str(e)}", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Error processing AI request: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/status")
|
||||
async def get_ai_status(
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Get comprehensive system status for AI assistant
|
||||
Status is filtered based on user role
|
||||
"""
|
||||
try:
|
||||
# Load user role
|
||||
user_role = db.query(Role).filter(Role.id == current_user.role_id).first()
|
||||
if not user_role:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="User role not found"
|
||||
)
|
||||
|
||||
# Initialize AI assistant with current user for role-based context
|
||||
ai_assistant = AIAssistantService(db, current_user)
|
||||
context = ai_assistant.generate_context_for_ai()
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"data": context
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting AI status: {str(e)}", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=str(e)
|
||||
)
|
||||
|
||||
|
||||
@router.get("/rooms/occupied")
|
||||
async def get_occupied_rooms(
|
||||
room_number: Optional[str] = None,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get list of occupied rooms - Admin and Staff only"""
|
||||
try:
|
||||
user_role = db.query(Role).filter(Role.id == current_user.role_id).first()
|
||||
if not user_role or user_role.name not in ['staff', 'admin']:
|
||||
raise HTTPException(status_code=403, detail="Only staff and admin can access this information")
|
||||
|
||||
ai_assistant = AIAssistantService(db, current_user)
|
||||
rooms = ai_assistant.search_occupied_rooms(room_number)
|
||||
|
||||
return {"status": "success", "data": {"rooms": rooms}}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting occupied rooms: {str(e)}", exc_info=True)
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/rooms/problems")
|
||||
async def get_room_problems(
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get rooms with problems - Admin and Staff only"""
|
||||
try:
|
||||
user_role = db.query(Role).filter(Role.id == current_user.role_id).first()
|
||||
if not user_role or user_role.name not in ['staff', 'admin']:
|
||||
raise HTTPException(status_code=403, detail="Only staff and admin can access this information")
|
||||
|
||||
ai_assistant = AIAssistantService(db, current_user)
|
||||
problems = ai_assistant.get_room_problems()
|
||||
|
||||
return {"status": "success", "data": {"problems": problems}}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting room problems: {str(e)}", exc_info=True)
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/chats/unanswered")
|
||||
async def get_unanswered_chats(
|
||||
hours: int = 24,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get unanswered chat messages - Admin and Staff only"""
|
||||
try:
|
||||
user_role = db.query(Role).filter(Role.id == current_user.role_id).first()
|
||||
if not user_role or user_role.name not in ['staff', 'admin']:
|
||||
raise HTTPException(status_code=403, detail="Only staff and admin can access this information")
|
||||
|
||||
ai_assistant = AIAssistantService(db, current_user)
|
||||
chats = ai_assistant.get_unanswered_chats(hours)
|
||||
|
||||
return {"status": "success", "data": {"chats": chats}}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting unanswered chats: {str(e)}", exc_info=True)
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/training/trigger")
|
||||
async def trigger_manual_training(
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Manually trigger AI training - Admin only"""
|
||||
try:
|
||||
user_role = db.query(Role).filter(Role.id == current_user.role_id).first()
|
||||
if not user_role or user_role.name != 'admin':
|
||||
raise HTTPException(status_code=403, detail="Only admin can trigger manual training")
|
||||
|
||||
learning_service = AILearningService(db)
|
||||
result = learning_service.auto_train_from_all_conversations()
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "Training completed",
|
||||
"data": result
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error triggering training: {str(e)}", exc_info=True)
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/training/analyze")
|
||||
async def trigger_manual_analysis(
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Manually trigger AI analysis and improvement - Admin only"""
|
||||
try:
|
||||
user_role = db.query(Role).filter(Role.id == current_user.role_id).first()
|
||||
if not user_role or user_role.name != 'admin':
|
||||
raise HTTPException(status_code=403, detail="Only admin can trigger manual analysis")
|
||||
|
||||
learning_service = AILearningService(db)
|
||||
result = learning_service.auto_analyze_and_improve()
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "Analysis completed",
|
||||
"data": result
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error triggering analysis: {str(e)}", exc_info=True)
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/training/status")
|
||||
async def get_training_status(
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get AI training status and metrics - Admin only"""
|
||||
try:
|
||||
user_role = db.query(Role).filter(Role.id == current_user.role_id).first()
|
||||
if not user_role or user_role.name != 'admin':
|
||||
raise HTTPException(status_code=403, detail="Only admin can view training status")
|
||||
|
||||
from ..models.ai_conversation import AIConversation, AILearnedPattern, AIKnowledgeEntry, AITrainingMetrics
|
||||
from sqlalchemy import func
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
# Get statistics
|
||||
total_conversations = db.query(AIConversation).count()
|
||||
total_patterns = db.query(AILearnedPattern).filter(AILearnedPattern.is_active == True).count()
|
||||
total_knowledge = db.query(AIKnowledgeEntry).count()
|
||||
|
||||
# Get recent metrics
|
||||
recent_metrics = db.query(AITrainingMetrics).order_by(
|
||||
AITrainingMetrics.metric_date.desc()
|
||||
).limit(1).first()
|
||||
|
||||
# Get scheduler status
|
||||
scheduler = get_training_scheduler()
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"data": {
|
||||
"scheduler_running": scheduler.running,
|
||||
"last_training": scheduler.last_training_time.isoformat() if scheduler.last_training_time else None,
|
||||
"last_analysis": scheduler.last_analysis_time.isoformat() if scheduler.last_analysis_time else None,
|
||||
"statistics": {
|
||||
"total_conversations": total_conversations,
|
||||
"total_patterns": total_patterns,
|
||||
"total_knowledge_entries": total_knowledge,
|
||||
},
|
||||
"recent_metrics": {
|
||||
"average_rating": float(recent_metrics.average_rating) if recent_metrics and recent_metrics.average_rating else None,
|
||||
"helpful_rate": float(recent_metrics.helpful_rate) if recent_metrics and recent_metrics.helpful_rate else None,
|
||||
"average_response_time_ms": recent_metrics.average_response_time_ms if recent_metrics else None,
|
||||
} if recent_metrics else None
|
||||
}
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting training status: {str(e)}", exc_info=True)
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
0
Backend/src/ai/schemas/__init__.py
Normal file
0
Backend/src/ai/schemas/__init__.py
Normal file
0
Backend/src/ai/services/__init__.py
Normal file
0
Backend/src/ai/services/__init__.py
Normal file
434
Backend/src/ai/services/ai_assistant_service.py
Normal file
434
Backend/src/ai/services/ai_assistant_service.py
Normal file
@@ -0,0 +1,434 @@
|
||||
"""
|
||||
Enterprise AI Assistant Service
|
||||
Monitors rooms, bookings, invoices, payments, and chat messages
|
||||
Provides intelligent responses to queries about hotel operations
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Optional, Any
|
||||
from datetime import datetime, timedelta
|
||||
from sqlalchemy.orm import Session, joinedload
|
||||
from sqlalchemy import and_, or_, func
|
||||
import json
|
||||
import logging
|
||||
|
||||
from ...rooms.models.room import Room, RoomStatus
|
||||
from ...bookings.models.booking import Booking, BookingStatus
|
||||
from ...payments.models.invoice import Invoice
|
||||
from ...payments.models.payment import Payment, PaymentStatus
|
||||
from ..models.chat import Chat, ChatMessage, ChatStatus
|
||||
from ...auth.models.user import User
|
||||
from ...shared.config.logging_config import get_logger
|
||||
from .ai_knowledge_base import AIKnowledgeBase
|
||||
from .ai_role_access_service import AIRoleAccessService
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class AIAssistantService:
|
||||
"""Enterprise AI Assistant for Hotel Management"""
|
||||
|
||||
def __init__(self, db: Session, current_user: Optional[User] = None):
|
||||
self.db = db
|
||||
self.current_user = current_user
|
||||
self.role_access_service = AIRoleAccessService(db, current_user) if current_user else None
|
||||
|
||||
def get_room_status_summary(self) -> Dict[str, Any]:
|
||||
"""Get comprehensive room status summary"""
|
||||
try:
|
||||
total_rooms = self.db.query(Room).count()
|
||||
available_rooms = self.db.query(Room).filter(Room.status == RoomStatus.available).count()
|
||||
occupied_rooms = self.db.query(Room).filter(Room.status == RoomStatus.occupied).count()
|
||||
maintenance_rooms = self.db.query(Room).filter(Room.status == RoomStatus.maintenance).count()
|
||||
cleaning_rooms = self.db.query(Room).filter(Room.status == RoomStatus.cleaning).count()
|
||||
|
||||
# Get rooms with bookings today
|
||||
today = datetime.utcnow().date()
|
||||
rooms_with_bookings = self.db.query(Booking).filter(
|
||||
and_(
|
||||
Booking.status != BookingStatus.cancelled,
|
||||
Booking.check_in_date <= today,
|
||||
Booking.check_out_date > today
|
||||
)
|
||||
).count()
|
||||
|
||||
return {
|
||||
"total_rooms": total_rooms,
|
||||
"available": available_rooms,
|
||||
"occupied": occupied_rooms,
|
||||
"maintenance": maintenance_rooms,
|
||||
"cleaning": cleaning_rooms,
|
||||
"rooms_with_active_bookings": rooms_with_bookings,
|
||||
"occupancy_rate": round((occupied_rooms / total_rooms * 100) if total_rooms > 0 else 0, 2)
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting room status: {str(e)}", exc_info=True)
|
||||
return {}
|
||||
|
||||
def get_booking_summary(self, days: int = 7) -> Dict[str, Any]:
|
||||
"""Get booking summary for specified days"""
|
||||
try:
|
||||
today = datetime.utcnow().date()
|
||||
end_date = today + timedelta(days=days)
|
||||
|
||||
# Upcoming check-ins
|
||||
upcoming_checkins = self.db.query(Booking).filter(
|
||||
and_(
|
||||
Booking.status == BookingStatus.confirmed,
|
||||
Booking.check_in_date >= today,
|
||||
Booking.check_in_date <= end_date
|
||||
)
|
||||
).count()
|
||||
|
||||
# Upcoming check-outs
|
||||
upcoming_checkouts = self.db.query(Booking).filter(
|
||||
and_(
|
||||
Booking.status == BookingStatus.confirmed,
|
||||
Booking.check_out_date >= today,
|
||||
Booking.check_out_date <= end_date
|
||||
)
|
||||
).count()
|
||||
|
||||
# Active bookings
|
||||
active_bookings = self.db.query(Booking).filter(
|
||||
and_(
|
||||
Booking.status == BookingStatus.confirmed,
|
||||
Booking.check_in_date <= today,
|
||||
Booking.check_out_date > today
|
||||
)
|
||||
).count()
|
||||
|
||||
# Pending bookings
|
||||
pending_bookings = self.db.query(Booking).filter(
|
||||
Booking.status == BookingStatus.pending
|
||||
).count()
|
||||
|
||||
return {
|
||||
"upcoming_checkins": upcoming_checkins,
|
||||
"upcoming_checkouts": upcoming_checkouts,
|
||||
"active_bookings": active_bookings,
|
||||
"pending_bookings": pending_bookings,
|
||||
"period_days": days
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting booking summary: {str(e)}", exc_info=True)
|
||||
return {}
|
||||
|
||||
def get_payment_summary(self, days: int = 30) -> Dict[str, Any]:
|
||||
"""Get payment summary"""
|
||||
try:
|
||||
today = datetime.utcnow().date()
|
||||
start_date = today - timedelta(days=days)
|
||||
|
||||
# Total payments in period
|
||||
total_payments = self.db.query(func.sum(Payment.amount)).filter(
|
||||
and_(
|
||||
Payment.payment_status == PaymentStatus.completed,
|
||||
func.date(Payment.payment_date) >= start_date,
|
||||
func.date(Payment.payment_date) <= today
|
||||
)
|
||||
).scalar() or 0
|
||||
|
||||
# Pending payments
|
||||
pending_payments = self.db.query(func.sum(Payment.amount)).filter(
|
||||
Payment.payment_status == PaymentStatus.pending
|
||||
).scalar() or 0
|
||||
|
||||
# Payment count
|
||||
completed_count = self.db.query(Payment).filter(
|
||||
and_(
|
||||
Payment.payment_status == PaymentStatus.completed,
|
||||
func.date(Payment.payment_date) >= start_date
|
||||
)
|
||||
).count()
|
||||
|
||||
return {
|
||||
"total_revenue": float(total_payments),
|
||||
"pending_amount": float(pending_payments),
|
||||
"completed_payments": completed_count,
|
||||
"period_days": days
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting payment summary: {str(e)}", exc_info=True)
|
||||
return {}
|
||||
|
||||
def get_invoice_summary(self) -> Dict[str, Any]:
|
||||
"""Get invoice summary"""
|
||||
try:
|
||||
total_invoices = self.db.query(Invoice).count()
|
||||
|
||||
# Group by status
|
||||
status_counts = {}
|
||||
invoices = self.db.query(Invoice).all()
|
||||
for invoice in invoices:
|
||||
status = invoice.status if hasattr(invoice, 'status') else 'unknown'
|
||||
status_counts[status] = status_counts.get(status, 0) + 1
|
||||
|
||||
# Overdue invoices
|
||||
today = datetime.utcnow().date()
|
||||
overdue_count = self.db.query(Invoice).filter(
|
||||
and_(
|
||||
Invoice.due_date < today,
|
||||
Invoice.status != 'paid'
|
||||
)
|
||||
).count() if hasattr(Invoice, 'due_date') else 0
|
||||
|
||||
return {
|
||||
"total_invoices": total_invoices,
|
||||
"status_breakdown": status_counts,
|
||||
"overdue_invoices": overdue_count
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting invoice summary: {str(e)}", exc_info=True)
|
||||
return {}
|
||||
|
||||
def get_unanswered_chats(self, hours: int = 24) -> List[Dict[str, Any]]:
|
||||
"""Get unanswered chat messages from after working hours"""
|
||||
try:
|
||||
cutoff_time = datetime.utcnow() - timedelta(hours=hours)
|
||||
|
||||
# Get open chats with unread messages
|
||||
chats = self.db.query(Chat).filter(
|
||||
and_(
|
||||
Chat.status == ChatStatus.open,
|
||||
Chat.updated_at >= cutoff_time
|
||||
)
|
||||
).all()
|
||||
|
||||
unanswered = []
|
||||
for chat in chats:
|
||||
# Get last message
|
||||
last_message = self.db.query(ChatMessage).filter(
|
||||
ChatMessage.chat_id == chat.id
|
||||
).order_by(ChatMessage.created_at.desc()).first()
|
||||
|
||||
if last_message and last_message.sender_type == 'visitor' and not last_message.is_read:
|
||||
unanswered.append({
|
||||
"chat_id": chat.id,
|
||||
"visitor_name": chat.visitor_name,
|
||||
"visitor_email": chat.visitor_email,
|
||||
"last_message": last_message.message[:100],
|
||||
"last_message_time": last_message.created_at.isoformat(),
|
||||
"waiting_hours": (datetime.utcnow() - last_message.created_at).total_seconds() / 3600
|
||||
})
|
||||
|
||||
return unanswered
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting unanswered chats: {str(e)}", exc_info=True)
|
||||
return []
|
||||
|
||||
def search_occupied_rooms(self, room_number: Optional[str] = None) -> List[Dict[str, Any]]:
|
||||
"""Search for occupied rooms"""
|
||||
try:
|
||||
today = datetime.utcnow().date()
|
||||
|
||||
# Get rooms with active bookings
|
||||
query = self.db.query(Room).options(
|
||||
joinedload(Room.room_type)
|
||||
).join(Booking).filter(
|
||||
and_(
|
||||
Room.status == RoomStatus.occupied,
|
||||
Booking.status == BookingStatus.confirmed,
|
||||
Booking.check_in_date <= today,
|
||||
Booking.check_out_date > today
|
||||
)
|
||||
)
|
||||
|
||||
if room_number:
|
||||
query = query.filter(Room.room_number.ilike(f"%{room_number}%"))
|
||||
|
||||
rooms = query.all()
|
||||
|
||||
result = []
|
||||
for room in rooms:
|
||||
# Get the active booking for this room
|
||||
booking = self.db.query(Booking).options(
|
||||
joinedload(Booking.user)
|
||||
).filter(
|
||||
and_(
|
||||
Booking.room_id == room.id,
|
||||
Booking.status == BookingStatus.confirmed,
|
||||
Booking.check_in_date <= today,
|
||||
Booking.check_out_date > today
|
||||
)
|
||||
).first()
|
||||
|
||||
result.append({
|
||||
"room_number": room.room_number,
|
||||
"room_type": room.room_type.name if room.room_type else "Unknown",
|
||||
"floor": room.floor,
|
||||
"booking_id": booking.id if booking else None,
|
||||
"guest_name": booking.user.full_name if booking and booking.user else "Unknown",
|
||||
"check_in": booking.check_in_date.isoformat() if booking else None,
|
||||
"check_out": booking.check_out_date.isoformat() if booking else None
|
||||
})
|
||||
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f"Error searching occupied rooms: {str(e)}", exc_info=True)
|
||||
return []
|
||||
|
||||
def get_room_problems(self) -> List[Dict[str, Any]]:
|
||||
"""Get rooms with problems (maintenance, issues)"""
|
||||
try:
|
||||
# Rooms in maintenance
|
||||
maintenance_rooms = self.db.query(Room).filter(
|
||||
Room.status == RoomStatus.maintenance
|
||||
).all()
|
||||
|
||||
problems = []
|
||||
for room in maintenance_rooms:
|
||||
problems.append({
|
||||
"room_number": room.room_number,
|
||||
"issue_type": "maintenance",
|
||||
"status": room.status.value,
|
||||
"description": f"Room {room.room_number} is under maintenance"
|
||||
})
|
||||
|
||||
# Check for maintenance records
|
||||
try:
|
||||
from ...rooms.models.room_maintenance import RoomMaintenance, MaintenanceStatus
|
||||
active_maintenance = self.db.query(RoomMaintenance).filter(
|
||||
MaintenanceStatus.in_progress == MaintenanceStatus.in_progress
|
||||
).all()
|
||||
|
||||
for maint in active_maintenance:
|
||||
room = self.db.query(Room).filter(Room.id == maint.room_id).first()
|
||||
if room:
|
||||
problems.append({
|
||||
"room_number": room.room_number,
|
||||
"issue_type": "maintenance",
|
||||
"status": maint.status.value if hasattr(maint.status, 'value') else str(maint.status),
|
||||
"description": maint.description or f"Maintenance issue in room {room.room_number}",
|
||||
"scheduled_start": maint.scheduled_start.isoformat() if maint.scheduled_start else None
|
||||
})
|
||||
except ImportError:
|
||||
pass # Maintenance model might not exist
|
||||
|
||||
return problems
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting room problems: {str(e)}", exc_info=True)
|
||||
return []
|
||||
|
||||
def get_booking_by_number(self, booking_number: str) -> Optional[Dict[str, Any]]:
|
||||
"""Get booking details by booking number"""
|
||||
try:
|
||||
booking = self.db.query(Booking).filter(
|
||||
Booking.booking_number == booking_number
|
||||
).first()
|
||||
|
||||
if not booking:
|
||||
return None
|
||||
|
||||
return {
|
||||
"booking_number": booking.booking_number,
|
||||
"status": booking.status.value if hasattr(booking.status, 'value') else str(booking.status),
|
||||
"guest_name": booking.user.full_name if booking.user else "Unknown",
|
||||
"room_number": booking.room.room_number if booking.room else "Unknown",
|
||||
"check_in": booking.check_in_date.isoformat(),
|
||||
"check_out": booking.check_out_date.isoformat(),
|
||||
"total_price": float(booking.total_price) if booking.total_price else 0,
|
||||
"payment_status": self._get_booking_payment_status(booking.id)
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting booking: {str(e)}", exc_info=True)
|
||||
return None
|
||||
|
||||
def get_invoice_by_number(self, invoice_number: str) -> Optional[Dict[str, Any]]:
|
||||
"""Get invoice details by invoice number"""
|
||||
try:
|
||||
invoice = self.db.query(Invoice).filter(
|
||||
Invoice.invoice_number == invoice_number
|
||||
).first()
|
||||
|
||||
if not invoice:
|
||||
return None
|
||||
|
||||
return {
|
||||
"invoice_number": invoice.invoice_number,
|
||||
"status": invoice.status if hasattr(invoice, 'status') else "unknown",
|
||||
"amount": float(invoice.total_amount) if hasattr(invoice, 'total_amount') else 0,
|
||||
"due_date": invoice.due_date.isoformat() if hasattr(invoice, 'due_date') and invoice.due_date else None,
|
||||
"booking_number": invoice.booking.booking_number if invoice.booking else None,
|
||||
"created_at": invoice.created_at.isoformat() if hasattr(invoice, 'created_at') else None
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting invoice: {str(e)}", exc_info=True)
|
||||
return None
|
||||
|
||||
def _get_booking_payment_status(self, booking_id: int) -> str:
|
||||
"""Get payment status for a booking"""
|
||||
try:
|
||||
payments = self.db.query(Payment).filter(
|
||||
Payment.booking_id == booking_id
|
||||
).all()
|
||||
|
||||
if not payments:
|
||||
return "unpaid"
|
||||
|
||||
completed = [p for p in payments if p.payment_status == PaymentStatus.completed]
|
||||
if completed:
|
||||
total_paid = sum(float(p.amount) for p in completed)
|
||||
booking = self.db.query(Booking).filter(Booking.id == booking_id).first()
|
||||
if booking and booking.total_price:
|
||||
if total_paid >= float(booking.total_price):
|
||||
return "paid"
|
||||
else:
|
||||
return "partially_paid"
|
||||
|
||||
return "pending"
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting payment status: {str(e)}", exc_info=True)
|
||||
return "unknown"
|
||||
|
||||
def generate_context_for_ai(self) -> Dict[str, Any]:
|
||||
"""Generate comprehensive context for AI assistant with role-based filtering"""
|
||||
context = {
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
}
|
||||
|
||||
# Use role-based access service if user is provided
|
||||
if self.role_access_service:
|
||||
role_name = self.role_access_service.role_name
|
||||
context["user_role"] = role_name
|
||||
context["role_capabilities"] = AIKnowledgeBase.get_role_info(role_name)
|
||||
|
||||
# Get role-appropriate data
|
||||
context["room_status"] = self.role_access_service.get_room_status_summary()
|
||||
|
||||
# Get user-specific bookings
|
||||
context["user_bookings"] = self.role_access_service.get_user_bookings(limit=5)
|
||||
|
||||
# Financial data only for authorized roles
|
||||
if self.role_access_service.can_access_feature("payment_summary"):
|
||||
payment_summary = self.role_access_service.get_payment_summary()
|
||||
if payment_summary:
|
||||
context["payment_summary"] = payment_summary
|
||||
|
||||
invoice_summary = self.role_access_service.get_invoice_summary()
|
||||
if invoice_summary:
|
||||
context["invoice_summary"] = invoice_summary
|
||||
|
||||
# Operational data for staff and admin
|
||||
if role_name in ["admin", "staff"]:
|
||||
context["booking_summary"] = self.get_booking_summary()
|
||||
context["unanswered_chats"] = self.get_unanswered_chats()
|
||||
context["room_problems"] = self.get_room_problems()
|
||||
|
||||
# Add knowledge base information
|
||||
context["application_knowledge"] = {
|
||||
"features": list(AIKnowledgeBase.FEATURES.keys()),
|
||||
"role_info": AIKnowledgeBase.get_role_info(role_name)
|
||||
}
|
||||
else:
|
||||
# Default context without user (shouldn't happen in production)
|
||||
context.update({
|
||||
"room_status": self.get_room_status_summary(),
|
||||
"booking_summary": self.get_booking_summary(),
|
||||
"payment_summary": self.get_payment_summary(),
|
||||
"invoice_summary": self.get_invoice_summary(),
|
||||
"unanswered_chats": self.get_unanswered_chats(),
|
||||
"room_problems": self.get_room_problems(),
|
||||
})
|
||||
|
||||
return context
|
||||
|
||||
975
Backend/src/ai/services/ai_chat_service.py
Normal file
975
Backend/src/ai/services/ai_chat_service.py
Normal file
@@ -0,0 +1,975 @@
|
||||
"""
|
||||
AI Chat Service - Handles AI-powered chat responses
|
||||
Integrates with OpenAI, Anthropic, or other LLM providers
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Optional, Any
|
||||
from datetime import datetime
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
|
||||
from ..services.ai_assistant_service import AIAssistantService
|
||||
from ..services.ai_knowledge_base import AIKnowledgeBase
|
||||
from ..services.ai_role_access_service import AIRoleAccessService
|
||||
from ..services.ai_learning_service import AILearningService
|
||||
from ...auth.models.user import User
|
||||
from ..models.ai_conversation import AILearnedPattern
|
||||
from ...shared.config.logging_config import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class AIChatService:
|
||||
"""AI Chat Service for generating intelligent responses"""
|
||||
|
||||
def __init__(self, db_session, current_user: Optional[User] = None):
|
||||
self.db = db_session
|
||||
self.current_user = current_user
|
||||
self.ai_assistant = AIAssistantService(db_session, current_user)
|
||||
self.knowledge_base = AIKnowledgeBase()
|
||||
self.role_access_service = AIRoleAccessService(db_session, current_user) if current_user else None
|
||||
self.learning_service = AILearningService(db_session)
|
||||
# Configure AI provider (OpenAI, Anthropic, etc.)
|
||||
self.provider = os.getenv("AI_PROVIDER", "openai")
|
||||
self.api_key = os.getenv("AI_API_KEY", "")
|
||||
self.model = os.getenv("AI_MODEL", "gpt-4")
|
||||
|
||||
def generate_response(self, user_query: str, context: Optional[Dict] = None, session_id: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Generate AI response based on user query with self-learning
|
||||
"""
|
||||
start_time = datetime.utcnow()
|
||||
|
||||
try:
|
||||
# Check learned patterns first
|
||||
role_name = self.role_access_service.role_name if self.role_access_service else None
|
||||
learned_pattern = self.learning_service.get_learned_pattern(user_query, role_name)
|
||||
knowledge_entry = self.learning_service.get_knowledge_entry(user_query, role_name)
|
||||
|
||||
# Check for similar past queries
|
||||
similar_queries = self.learning_service.find_similar_queries(user_query, limit=3)
|
||||
|
||||
# Get current system context
|
||||
system_context = self.ai_assistant.generate_context_for_ai()
|
||||
|
||||
# Merge with provided context
|
||||
if context:
|
||||
system_context.update(context)
|
||||
|
||||
# Add learned knowledge to context
|
||||
if learned_pattern:
|
||||
system_context["learned_pattern"] = {
|
||||
"pattern": learned_pattern.query_pattern,
|
||||
"intent": learned_pattern.intent,
|
||||
"confidence": float(learned_pattern.confidence_score)
|
||||
}
|
||||
|
||||
if knowledge_entry:
|
||||
system_context["knowledge_entry"] = knowledge_entry
|
||||
|
||||
if similar_queries:
|
||||
system_context["similar_past_queries"] = similar_queries
|
||||
|
||||
# Analyze query intent (or use learned intent)
|
||||
intent = learned_pattern.intent if learned_pattern else self._analyze_intent(user_query)
|
||||
|
||||
# Get relevant data based on intent
|
||||
relevant_data = self._get_relevant_data(intent, user_query)
|
||||
|
||||
# Status-related intents should always use rule-based formatted responses
|
||||
# These provide consistent, well-formatted output with emojis and structure
|
||||
status_intents = [
|
||||
"general_status", "room_status", "room_occupied", "room_available",
|
||||
"room_problems", "booking_status", "payment_status", "invoice_status",
|
||||
"chat_status", "app_info"
|
||||
]
|
||||
|
||||
response = None
|
||||
|
||||
# For status queries, prioritize rule-based formatted responses
|
||||
if intent in status_intents:
|
||||
# Always use rule-based response for status queries to ensure proper formatting
|
||||
if self.api_key and self.provider == "openai":
|
||||
# Even with LLM, prefer rule-based for status to maintain format consistency
|
||||
response = self._generate_rule_based_response(intent, user_query, relevant_data)
|
||||
else:
|
||||
response = self._generate_rule_based_response(intent, user_query, relevant_data)
|
||||
logger.info(f"Using rule-based formatted response for status intent: {intent}")
|
||||
else:
|
||||
# For non-status queries, use learned patterns/knowledge if available
|
||||
if learned_pattern and learned_pattern.confidence_score >= 70.0 and learned_pattern.response_template:
|
||||
# Try to use learned pattern response
|
||||
try:
|
||||
response = self._apply_learned_pattern(learned_pattern, relevant_data, user_query)
|
||||
if response:
|
||||
logger.info(f"Using learned pattern {learned_pattern.id} for query")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to apply learned pattern: {str(e)}")
|
||||
|
||||
# Use knowledge entry if available
|
||||
if not response and knowledge_entry and knowledge_entry.get("confidence", 0) >= 70.0:
|
||||
response = knowledge_entry.get("answer")
|
||||
logger.info(f"Using knowledge entry for query")
|
||||
|
||||
# Generate response using LLM or rule-based system if no learned response
|
||||
if not response:
|
||||
if self.api_key and self.provider == "openai":
|
||||
response = self._generate_llm_response(user_query, system_context, relevant_data)
|
||||
else:
|
||||
# Fallback to rule-based responses
|
||||
response = self._generate_rule_based_response(intent, user_query, relevant_data)
|
||||
|
||||
# Calculate response time
|
||||
response_time = int((datetime.utcnow() - start_time).total_seconds() * 1000)
|
||||
|
||||
# Save conversation for learning (async, don't block response)
|
||||
if self.current_user:
|
||||
try:
|
||||
self.learning_service.save_conversation(
|
||||
user_id=self.current_user.id,
|
||||
user_query=user_query,
|
||||
ai_response=response,
|
||||
intent=intent,
|
||||
context_used=relevant_data,
|
||||
user_role=role_name,
|
||||
session_id=session_id,
|
||||
response_time_ms=response_time
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to save conversation for learning: {str(e)}")
|
||||
|
||||
return {
|
||||
"response": response,
|
||||
"intent": intent,
|
||||
"data_used": relevant_data,
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
"used_learned_pattern": learned_pattern.id if learned_pattern else None,
|
||||
"used_knowledge_entry": knowledge_entry.get("topic") if knowledge_entry else None
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating AI response: {str(e)}", exc_info=True)
|
||||
return {
|
||||
"response": "I apologize, but I'm having trouble processing your request right now. Please try again later.",
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
def _analyze_intent(self, query: str) -> str:
|
||||
"""Analyze user query intent with comprehensive pattern matching"""
|
||||
query_lower = query.lower()
|
||||
|
||||
# Room-related queries - expanded patterns
|
||||
room_keywords = ['room', 'rooms', 'occupied', 'available', 'free', 'vacant', 'vacancy',
|
||||
'occupancy', 'suite', 'suites', 'accommodation']
|
||||
if any(word in query_lower for word in room_keywords):
|
||||
if any(word in query_lower for word in ['occupied', 'booked', 'taken', 'in use', 'guest']):
|
||||
return "room_occupied"
|
||||
elif any(word in query_lower for word in ['available', 'free', 'vacant', 'empty', 'ready']):
|
||||
return "room_available"
|
||||
elif any(word in query_lower for word in ['problem', 'issue', 'maintenance', 'broken', 'repair', 'fix']):
|
||||
return "room_problems"
|
||||
elif any(word in query_lower for word in ['how many', 'total', 'count', 'number of', 'how much', 'quantity']):
|
||||
return "room_status"
|
||||
elif any(word in query_lower for word in ['status', 'state', 'condition', 'overview']):
|
||||
return "room_status"
|
||||
return "room_status"
|
||||
|
||||
# Booking-related queries - expanded patterns
|
||||
booking_keywords = ['booking', 'bookings', 'reservation', 'reservations', 'check-in', 'check-out',
|
||||
'checkin', 'checkout', 'arrival', 'departure', 'guest', 'guests']
|
||||
if any(word in query_lower for word in booking_keywords):
|
||||
if any(word in query_lower for word in ['number', 'id', 'code', 'reference']):
|
||||
return "booking_by_number"
|
||||
elif any(word in query_lower for word in ['upcoming', 'coming', 'next', 'future', 'scheduled']):
|
||||
return "booking_status"
|
||||
elif any(word in query_lower for word in ['active', 'current', 'today', 'now']):
|
||||
return "booking_status"
|
||||
return "booking_status"
|
||||
|
||||
# Invoice-related queries - expanded patterns
|
||||
invoice_keywords = ['invoice', 'invoices', 'bill', 'bills', 'receipt', 'receipts', 'statement', 'statements']
|
||||
if any(word in query_lower for word in invoice_keywords):
|
||||
if any(word in query_lower for word in ['number', 'id', 'code', 'reference']):
|
||||
return "invoice_by_number"
|
||||
elif any(word in query_lower for word in ['overdue', 'unpaid', 'pending', 'due']):
|
||||
return "invoice_status"
|
||||
return "invoice_status"
|
||||
|
||||
# Payment-related queries - expanded patterns
|
||||
payment_keywords = ['payment', 'payments', 'paid', 'unpaid', 'revenue', 'money', 'income',
|
||||
'earnings', 'sales', 'financial', 'cash', 'transaction', 'transactions']
|
||||
if any(word in query_lower for word in payment_keywords):
|
||||
if any(word in query_lower for word in ['all', 'list', 'show all', 'every', 'entire']):
|
||||
return "all_payments"
|
||||
return "payment_status"
|
||||
|
||||
# Chat-related queries - expanded patterns
|
||||
chat_keywords = ['chat', 'chats', 'message', 'messages', 'unanswered', 'customer', 'customers',
|
||||
'inquiry', 'inquiries', 'support', 'help', 'contact']
|
||||
if any(word in query_lower for word in chat_keywords):
|
||||
return "chat_status"
|
||||
|
||||
# App/Platform information queries - for admin only
|
||||
app_keywords = ['app', 'application', 'platform', 'system', 'software', 'what is this',
|
||||
'about the app', 'about the platform', 'about the system', 'features',
|
||||
'capabilities', 'what can', 'how does the app', 'tell me about the app',
|
||||
'what does this app do', 'app features', 'system features', 'platform features',
|
||||
'what features', 'list features', 'show features', 'tell me about features',
|
||||
'what modules', 'what models', 'system overview', 'platform overview']
|
||||
if any(word in query_lower for word in app_keywords):
|
||||
return "app_info"
|
||||
|
||||
# General status queries - expanded patterns
|
||||
status_keywords = ['status', 'summary', 'overview', 'dashboard', 'report', 'reports',
|
||||
'statistics', 'stats', 'info', 'information', 'details', 'what', 'show me', 'tell me']
|
||||
if any(word in query_lower for word in status_keywords):
|
||||
return "general_status"
|
||||
|
||||
# Questions about "how many", "what", "show", "tell" - default to general status
|
||||
question_words = ['how many', 'how much', 'what', 'show', 'tell', 'give me', 'list', 'display']
|
||||
if any(word in query_lower for word in question_words):
|
||||
return "general_status"
|
||||
|
||||
return "general_query"
|
||||
|
||||
def _get_relevant_data(self, intent: str, query: str) -> Dict[str, Any]:
|
||||
"""Get relevant data based on intent with role-based filtering"""
|
||||
data = {}
|
||||
role_name = self.role_access_service.role_name if self.role_access_service else "customer"
|
||||
|
||||
try:
|
||||
if intent == "room_occupied":
|
||||
# Only staff/admin can see occupied room details
|
||||
if role_name in ["admin", "staff"]:
|
||||
room_number = self._extract_room_number(query)
|
||||
data["occupied_rooms"] = self.ai_assistant.search_occupied_rooms(room_number)
|
||||
data["room_summary"] = self.ai_assistant.get_room_status_summary()
|
||||
|
||||
elif intent == "room_available":
|
||||
data["room_summary"] = self.ai_assistant.get_room_status_summary()
|
||||
|
||||
elif intent == "room_status":
|
||||
data["room_summary"] = self.ai_assistant.get_room_status_summary()
|
||||
|
||||
elif intent == "room_problems":
|
||||
# Only staff/admin can see room problems
|
||||
if role_name in ["admin", "staff"]:
|
||||
data["problems"] = self.ai_assistant.get_room_problems()
|
||||
|
||||
elif intent == "booking_by_number":
|
||||
booking_number = self._extract_booking_number(query)
|
||||
if booking_number:
|
||||
booking_data = self.ai_assistant.get_booking_by_number(booking_number)
|
||||
if booking_data:
|
||||
# Filter based on role
|
||||
if role_name == "customer":
|
||||
# Customers can only see their own bookings
|
||||
user_bookings = self.role_access_service.get_user_bookings(limit=100)
|
||||
if any(b.get("booking_number") == booking_number for b in user_bookings):
|
||||
data["booking"] = booking_data
|
||||
else:
|
||||
data["booking"] = booking_data
|
||||
|
||||
elif intent == "booking_status":
|
||||
if role_name in ["admin", "staff"]:
|
||||
data["booking_summary"] = self.ai_assistant.get_booking_summary()
|
||||
elif role_name == "customer":
|
||||
# Customers see their own bookings
|
||||
data["user_bookings"] = self.role_access_service.get_user_bookings(limit=10)
|
||||
|
||||
elif intent == "invoice_by_number":
|
||||
invoice_number = self._extract_invoice_number(query)
|
||||
if invoice_number:
|
||||
invoice_data = self.ai_assistant.get_invoice_by_number(invoice_number)
|
||||
if invoice_data:
|
||||
# Filter based on role
|
||||
if role_name == "customer":
|
||||
user_invoices = self.role_access_service.get_user_invoices(limit=100)
|
||||
if any(i.get("invoice_number") == invoice_number for i in user_invoices):
|
||||
data["invoice"] = invoice_data
|
||||
else:
|
||||
data["invoice"] = invoice_data
|
||||
|
||||
elif intent == "invoice_status":
|
||||
if role_name in ["admin", "accountant", "staff"]:
|
||||
invoice_summary = self.role_access_service.get_invoice_summary()
|
||||
if invoice_summary:
|
||||
data["invoice_summary"] = invoice_summary
|
||||
elif role_name == "customer":
|
||||
# Customers see their own invoices
|
||||
data["user_invoices"] = self.role_access_service.get_user_invoices(limit=10)
|
||||
|
||||
elif intent == "all_payments":
|
||||
# Get all payments for admin/accountant
|
||||
if role_name in ["admin", "accountant"]:
|
||||
data["all_payments"] = self.role_access_service.get_all_payments()
|
||||
elif role_name == "staff":
|
||||
data["all_payments"] = self.role_access_service.get_user_payments(limit=50)
|
||||
elif role_name == "customer":
|
||||
data["all_payments"] = self.role_access_service.get_user_payments(limit=None)
|
||||
|
||||
elif intent == "payment_status":
|
||||
if role_name in ["admin", "accountant", "staff"]:
|
||||
payment_summary = self.role_access_service.get_payment_summary()
|
||||
if payment_summary:
|
||||
data["payment_summary"] = payment_summary
|
||||
elif role_name == "customer":
|
||||
# Customers see their own payments
|
||||
data["user_payments"] = self.role_access_service.get_user_payments(limit=10)
|
||||
|
||||
elif intent == "chat_status":
|
||||
# Only staff/admin can see unanswered chats
|
||||
if role_name in ["admin", "staff"]:
|
||||
data["unanswered_chats"] = self.ai_assistant.get_unanswered_chats()
|
||||
|
||||
elif intent == "general_status":
|
||||
data["room_summary"] = self.ai_assistant.get_room_status_summary()
|
||||
|
||||
if role_name in ["admin", "staff"]:
|
||||
data["booking_summary"] = self.ai_assistant.get_booking_summary()
|
||||
data["unanswered_chats"] = self.ai_assistant.get_unanswered_chats()
|
||||
data["room_problems"] = self.ai_assistant.get_room_problems()
|
||||
elif role_name == "customer":
|
||||
data["user_bookings"] = self.role_access_service.get_user_bookings(limit=5)
|
||||
data["user_invoices"] = self.role_access_service.get_user_invoices(limit=5)
|
||||
data["user_payments"] = self.role_access_service.get_user_payments(limit=5)
|
||||
|
||||
# Financial summaries only for authorized roles
|
||||
if role_name in ["admin", "accountant", "staff"]:
|
||||
payment_summary = self.role_access_service.get_payment_summary()
|
||||
if payment_summary:
|
||||
data["payment_summary"] = payment_summary
|
||||
|
||||
invoice_summary = self.role_access_service.get_invoice_summary()
|
||||
if invoice_summary:
|
||||
data["invoice_summary"] = invoice_summary
|
||||
|
||||
elif intent == "app_info":
|
||||
# App information - only for admin
|
||||
if role_name == "admin":
|
||||
# Get comprehensive application knowledge
|
||||
app_knowledge = self.knowledge_base.get_application_knowledge()
|
||||
data["app_knowledge"] = app_knowledge
|
||||
else:
|
||||
# Non-admin users get limited information
|
||||
data["app_knowledge"] = {
|
||||
"overview": self.knowledge_base.APPLICATION_OVERVIEW,
|
||||
"role_info": self.knowledge_base.get_role_info(role_name)
|
||||
}
|
||||
|
||||
elif intent == "general_query":
|
||||
# For general queries, get role-appropriate data
|
||||
data["room_summary"] = self.ai_assistant.get_room_status_summary()
|
||||
|
||||
if role_name in ["admin", "staff"]:
|
||||
data["booking_summary"] = self.ai_assistant.get_booking_summary()
|
||||
elif role_name == "customer":
|
||||
data["user_bookings"] = self.role_access_service.get_user_bookings(limit=5)
|
||||
|
||||
if role_name in ["admin", "accountant"]:
|
||||
payment_summary = self.role_access_service.get_payment_summary()
|
||||
if payment_summary:
|
||||
data["payment_summary"] = payment_summary
|
||||
|
||||
invoice_summary = self.role_access_service.get_invoice_summary()
|
||||
if invoice_summary:
|
||||
data["invoice_summary"] = invoice_summary
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting relevant data: {str(e)}", exc_info=True)
|
||||
|
||||
return data
|
||||
|
||||
def _extract_room_number(self, query: str) -> Optional[str]:
|
||||
"""Extract room number from query"""
|
||||
import re
|
||||
# Look for patterns like "room 101", "room101", "#101"
|
||||
patterns = [
|
||||
r'room\s*#?\s*(\d+)',
|
||||
r'#\s*(\d+)',
|
||||
r'room\s+(\d{3,4})',
|
||||
]
|
||||
for pattern in patterns:
|
||||
match = re.search(pattern, query, re.IGNORECASE)
|
||||
if match:
|
||||
return match.group(1)
|
||||
return None
|
||||
|
||||
def _extract_booking_number(self, query: str) -> Optional[str]:
|
||||
"""Extract booking number from query"""
|
||||
import re
|
||||
# Look for booking number patterns
|
||||
patterns = [
|
||||
r'booking\s*#?\s*([A-Z0-9-]+)',
|
||||
r'BK[A-Z0-9-]+',
|
||||
]
|
||||
for pattern in patterns:
|
||||
match = re.search(pattern, query, re.IGNORECASE)
|
||||
if match:
|
||||
return match.group(1) if match.groups() else match.group(0)
|
||||
return None
|
||||
|
||||
def _extract_invoice_number(self, query: str) -> Optional[str]:
|
||||
"""Extract invoice number from query"""
|
||||
import re
|
||||
patterns = [
|
||||
r'invoice\s*#?\s*([A-Z0-9-]+)',
|
||||
r'INV[A-Z0-9-]+',
|
||||
]
|
||||
for pattern in patterns:
|
||||
match = re.search(pattern, query, re.IGNORECASE)
|
||||
if match:
|
||||
return match.group(1) if match.groups() else match.group(0)
|
||||
return None
|
||||
|
||||
def _generate_llm_response(self, query: str, context: Dict, data: Dict) -> str:
|
||||
"""Generate response using LLM (OpenAI, Anthropic, etc.)"""
|
||||
try:
|
||||
if self.provider == "openai":
|
||||
import openai
|
||||
openai.api_key = self.api_key
|
||||
|
||||
# Build system prompt
|
||||
system_prompt = self._build_system_prompt(context, data)
|
||||
|
||||
# Call OpenAI API
|
||||
response = openai.ChatCompletion.create(
|
||||
model=self.model,
|
||||
messages=[
|
||||
{"role": "system", "content": system_prompt},
|
||||
{"role": "user", "content": query}
|
||||
],
|
||||
temperature=0.7,
|
||||
max_tokens=500
|
||||
)
|
||||
|
||||
return response.choices[0].message.content.strip()
|
||||
|
||||
except ImportError:
|
||||
logger.warning("OpenAI library not installed, falling back to rule-based responses")
|
||||
return self._generate_rule_based_response(self._analyze_intent(query), query, data)
|
||||
except Exception as e:
|
||||
logger.error(f"Error calling LLM: {str(e)}", exc_info=True)
|
||||
return self._generate_rule_based_response(self._analyze_intent(query), query, data)
|
||||
|
||||
def _build_system_prompt(self, context: Dict, data: Dict) -> str:
|
||||
"""Build enterprise-level system prompt for LLM with role awareness"""
|
||||
role_name = context.get("user_role", "customer")
|
||||
role_info = self.knowledge_base.get_role_info(role_name)
|
||||
|
||||
prompt = f"""You are an enterprise AI assistant for a comprehensive Hotel Booking and Management System (HBMS).
|
||||
You provide intelligent, role-aware assistance based on the user's permissions and access level.
|
||||
|
||||
USER ROLE: {role_name.upper()}
|
||||
ROLE DESCRIPTION: {role_info.get('description', 'Standard user access')}
|
||||
|
||||
YOUR CAPABILITIES:
|
||||
"""
|
||||
|
||||
# Add role-specific capabilities
|
||||
can_access = role_info.get("can_access", [])
|
||||
for capability in can_access[:15]: # Limit to avoid token overflow
|
||||
prompt += f"- {capability}\n"
|
||||
|
||||
if role_info.get("cannot_access"):
|
||||
prompt += "\nRESTRICTIONS:\n"
|
||||
for restriction in role_info.get("cannot_access", [])[:5]:
|
||||
prompt += f"- Cannot access: {restriction}\n"
|
||||
|
||||
prompt += """
|
||||
\nSYSTEM FEATURES AVAILABLE:
|
||||
- Booking Management (create, view, modify, cancel bookings)
|
||||
- Room Management (availability, status, types, amenities)
|
||||
- Payment Processing (multiple payment methods, status tracking)
|
||||
- Invoice Management (generation, tracking, payment reconciliation)
|
||||
- Loyalty Program (points, tiers, rewards, referrals)
|
||||
- Service Management (additional hotel services)
|
||||
- Guest Management (profiles, preferences, CRM)
|
||||
- Chat Support System
|
||||
- Check-in/Check-out Operations
|
||||
- Maintenance and Housekeeping
|
||||
|
||||
IMPORTANT SECURITY RULES:
|
||||
1. NEVER provide information about other users' bookings, payments, or personal data unless user is Admin
|
||||
2. For customers: Only show information about their own bookings, invoices, and payments
|
||||
3. For staff: Provide operational data but not financial summaries
|
||||
4. For accountant: Provide financial data but not operational details
|
||||
5. For admin: Full access to all information
|
||||
|
||||
Current System Status:
|
||||
"""
|
||||
prompt += json.dumps(context, indent=2, default=str)
|
||||
|
||||
if data:
|
||||
prompt += "\n\nRelevant Data for Current Query:\n"
|
||||
prompt += json.dumps(data, indent=2, default=str)
|
||||
|
||||
prompt += """
|
||||
\nINSTRUCTIONS:
|
||||
- Provide clear, concise, and helpful responses
|
||||
- Use the data provided to answer questions accurately
|
||||
- Respect role-based access restrictions
|
||||
- If you don't have specific information or the user doesn't have access, explain clearly
|
||||
- Be professional and friendly
|
||||
- For customers, always refer to data as "your bookings", "your invoices", etc.
|
||||
- For staff/admin, provide comprehensive operational insights
|
||||
- For accountants, focus on financial data and reports"""
|
||||
|
||||
return prompt
|
||||
|
||||
def _generate_rule_based_response(self, intent: str, query: str, data: Dict) -> str:
|
||||
"""Generate rule-based response when LLM is not available - with role awareness"""
|
||||
role_name = self.role_access_service.role_name if self.role_access_service else "customer"
|
||||
is_customer = role_name == "customer"
|
||||
|
||||
if intent == "room_occupied":
|
||||
if data.get("occupied_rooms"):
|
||||
rooms = data["occupied_rooms"]
|
||||
if len(rooms) == 1:
|
||||
room = rooms[0]
|
||||
return f"Room {room['room_number']} is currently occupied by {room['guest_name']}. Check-in: {room['check_in']}, Check-out: {room['check_out']}."
|
||||
else:
|
||||
return f"There are {len(rooms)} occupied rooms. Here are the details: {', '.join([r['room_number'] for r in rooms[:5]])}"
|
||||
else:
|
||||
summary = data.get("room_summary", {})
|
||||
return f"Currently, {summary.get('occupied', 0)} rooms are occupied out of {summary.get('total_rooms', 0)} total rooms."
|
||||
|
||||
elif intent == "room_available":
|
||||
summary = data.get("room_summary", {})
|
||||
return f"There are {summary.get('available', 0)} available rooms out of {summary.get('total_rooms', 0)} total rooms. {summary.get('cleaning', 0)} rooms are being cleaned."
|
||||
|
||||
elif intent == "room_status":
|
||||
summary = data.get("room_summary", {})
|
||||
total = summary.get('total_rooms', 0)
|
||||
available = summary.get('available', 0)
|
||||
occupied = summary.get('occupied', 0)
|
||||
maintenance = summary.get('maintenance', 0)
|
||||
cleaning = summary.get('cleaning', 0)
|
||||
occupancy_rate = summary.get('occupancy_rate', 0)
|
||||
|
||||
response = f"We have {total} total rooms. "
|
||||
response += f"Currently: {available} available, {occupied} occupied, {cleaning} being cleaned, and {maintenance} under maintenance. "
|
||||
response += f"Occupancy rate: {occupancy_rate}%."
|
||||
return response
|
||||
|
||||
elif intent == "room_problems":
|
||||
problems = data.get("problems", [])
|
||||
if problems:
|
||||
response = f"⚠️ Room Issues: There are {len(problems)} rooms with problems:\n\n"
|
||||
for i, problem in enumerate(problems[:10], 1):
|
||||
response += f"{i}. Room {problem['room_number']}\n"
|
||||
response += f" Issue: {problem.get('issue_type', 'Unknown')}\n"
|
||||
if problem.get('description'):
|
||||
response += f" Details: {problem['description']}\n"
|
||||
if problem.get('status'):
|
||||
response += f" Status: {problem['status']}\n"
|
||||
response += "\n"
|
||||
if len(problems) > 10:
|
||||
response += f"... and {len(problems) - 10} more rooms with issues."
|
||||
return response
|
||||
else:
|
||||
return "✅ No room problems reported at this time. All rooms are in good condition!"
|
||||
|
||||
elif intent == "booking_by_number":
|
||||
booking = data.get("booking")
|
||||
if booking:
|
||||
response = f"Booking {booking['booking_number']}:\n"
|
||||
response += f"• Status: {booking['status']}\n"
|
||||
response += f"• Guest: {booking['guest_name']}\n"
|
||||
response += f"• Room: {booking['room_number']}\n"
|
||||
response += f"• Check-in: {booking['check_in']}\n"
|
||||
response += f"• Check-out: {booking['check_out']}\n"
|
||||
response += f"• Payment Status: {booking['payment_status']}\n"
|
||||
if booking.get('total_price'):
|
||||
response += f"• Total Price: ${float(booking['total_price']):,.2f}"
|
||||
return response
|
||||
else:
|
||||
return "I couldn't find that booking. Please check the booking number and try again."
|
||||
|
||||
elif intent == "booking_status":
|
||||
# Role-aware booking status
|
||||
if is_customer:
|
||||
user_bookings = data.get("user_bookings", [])
|
||||
if user_bookings:
|
||||
response = f"You have {len(user_bookings)} booking(s):\n\n"
|
||||
for booking in user_bookings[:5]:
|
||||
response += f"• {booking.get('booking_number', 'N/A')}: {booking.get('status', 'Unknown')}\n"
|
||||
response += f" Check-in: {booking.get('check_in', 'N/A')}, Check-out: {booking.get('check_out', 'N/A')}\n"
|
||||
if len(user_bookings) > 5:
|
||||
response += f"\n... and {len(user_bookings) - 5} more bookings."
|
||||
return response
|
||||
else:
|
||||
return "You don't have any bookings at this time."
|
||||
else:
|
||||
summary = data.get("booking_summary", {})
|
||||
if summary:
|
||||
response = "Booking Status Summary:\n"
|
||||
response += f"• Active Bookings: {summary.get('active_bookings', 0)}\n"
|
||||
response += f"• Upcoming Check-ins: {summary.get('upcoming_checkins', 0)}\n"
|
||||
response += f"• Upcoming Check-outs: {summary.get('upcoming_checkouts', 0)}\n"
|
||||
response += f"• Pending Bookings: {summary.get('pending_bookings', 0)}"
|
||||
return response
|
||||
return "No booking data available at this time."
|
||||
|
||||
elif intent == "invoice_by_number":
|
||||
invoice = data.get("invoice")
|
||||
if invoice:
|
||||
response = f"Invoice {invoice['invoice_number']}:\n"
|
||||
response += f"• Status: {invoice['status']}\n"
|
||||
response += f"• Amount: ${invoice['amount']:,.2f}\n"
|
||||
if invoice.get('due_date'):
|
||||
response += f"• Due Date: {invoice['due_date']}\n"
|
||||
if invoice.get('booking_number'):
|
||||
response += f"• Booking: {invoice['booking_number']}"
|
||||
return response
|
||||
else:
|
||||
return "I couldn't find that invoice. Please check the invoice number."
|
||||
|
||||
elif intent == "invoice_status":
|
||||
# Role-aware invoice status
|
||||
if is_customer:
|
||||
user_invoices = data.get("user_invoices", [])
|
||||
if user_invoices:
|
||||
response = f"You have {len(user_invoices)} invoice(s):\n\n"
|
||||
for invoice in user_invoices[:5]:
|
||||
status_emoji = "✅" if invoice.get('status') == 'paid' else "⏳" if invoice.get('status') == 'sent' else "⚠️"
|
||||
response += f"{status_emoji} Invoice {invoice.get('invoice_number', 'N/A')}: {invoice.get('status', 'Unknown')}\n"
|
||||
response += f" Amount: ${invoice.get('total_amount', 0):,.2f}, Due: {invoice.get('due_date', 'N/A')}\n"
|
||||
if len(user_invoices) > 5:
|
||||
response += f"\n... and {len(user_invoices) - 5} more invoices."
|
||||
return response
|
||||
else:
|
||||
return "You don't have any invoices at this time."
|
||||
else:
|
||||
summary = data.get("invoice_summary", {})
|
||||
if summary:
|
||||
response = "Invoice Status Summary:\n"
|
||||
response += f"• Total Invoices: {summary.get('total_invoices', 0)}\n"
|
||||
response += f"• Overdue Invoices: {summary.get('overdue_invoices', 0)}"
|
||||
if summary.get('status_breakdown'):
|
||||
response += f"\n• Status Breakdown: {summary['status_breakdown']}"
|
||||
return response
|
||||
return "No invoice data available at this time."
|
||||
|
||||
elif intent == "all_payments":
|
||||
# Display all payments in table format
|
||||
all_payments = data.get("all_payments", [])
|
||||
if all_payments:
|
||||
response = f"📋 ALL PAYMENTS ({len(all_payments)} total):\n\n"
|
||||
response += "┌──────────────┬──────────────────────────────┬──────────────────┬──────────┬──────────────────┬──────────┬────────────┬──────────────┐\n"
|
||||
response += "│ Transaction │ Booking Number │ Customer │ Method │ Type │ Status │ Amount │ Payment Date │\n"
|
||||
response += "├──────────────┼──────────────────────────────┼──────────────────┼──────────┼──────────────────┼──────────┼────────────┼──────────────┤\n"
|
||||
|
||||
for payment in all_payments:
|
||||
trans_id = (payment.get('transaction_id') or f"PAY-{payment.get('id', 'N/A')}")[:12]
|
||||
booking = (payment.get('booking_number') or 'N/A')[:28]
|
||||
customer = (payment.get('customer_name') or 'N/A')[:16]
|
||||
method = str(payment.get('payment_method', 'N/A')).replace('_', ' ').title()[:8]
|
||||
ptype = payment.get('payment_type_display', payment.get('payment_type', 'N/A'))[:16]
|
||||
status = str(payment.get('payment_status', 'N/A')).title()
|
||||
|
||||
status_emoji = "✅" if status.lower() == 'completed' else "⏳" if status.lower() == 'pending' else "❌" if status.lower() == 'failed' else "🔄" if status.lower() == 'refunded' else "❓"
|
||||
status_display = f"{status_emoji} {status}"[:10]
|
||||
|
||||
amount = payment.get('amount', 0)
|
||||
amount_str = f"{amount:,.2f} €"[:12]
|
||||
|
||||
pdate = payment.get('payment_date', None)
|
||||
if pdate and pdate != 'Invalid Date':
|
||||
try:
|
||||
from datetime import datetime
|
||||
if isinstance(pdate, str):
|
||||
if 'T' in pdate:
|
||||
dt = datetime.fromisoformat(pdate.replace('Z', '+00:00'))
|
||||
pdate = dt.strftime('%Y-%m-%d')
|
||||
else:
|
||||
pdate = pdate[:10]
|
||||
else:
|
||||
pdate = pdate.strftime('%Y-%m-%d') if hasattr(pdate, 'strftime') else str(pdate)[:10]
|
||||
except Exception:
|
||||
pdate = "Invalid Date"
|
||||
else:
|
||||
pdate = "Invalid Date"
|
||||
|
||||
response += f"│ {trans_id:<12} │ {booking:<28} │ {customer:<16} │ {method:<8} │ {ptype:<16} │ {status_display:<10} │ {amount_str:<12} │ {pdate:<14} │\n"
|
||||
|
||||
response += "└──────────────┴──────────────────────────────┴──────────────────┴──────────┴──────────────────┴──────────┴────────────┴──────────────┘\n"
|
||||
|
||||
# Add summary statistics
|
||||
completed = sum(1 for p in all_payments if p.get('payment_status', '').lower() == 'completed')
|
||||
pending = sum(1 for p in all_payments if p.get('payment_status', '').lower() == 'pending')
|
||||
failed = sum(1 for p in all_payments if p.get('payment_status', '').lower() == 'failed')
|
||||
refunded = sum(1 for p in all_payments if p.get('payment_status', '').lower() == 'refunded')
|
||||
total_amount = sum(float(p.get('amount', 0)) for p in all_payments)
|
||||
completed_amount = sum(float(p.get('amount', 0)) for p in all_payments if p.get('payment_status', '').lower() == 'completed')
|
||||
pending_amount = sum(float(p.get('amount', 0)) for p in all_payments if p.get('payment_status', '').lower() == 'pending')
|
||||
|
||||
response += f"\n📊 Summary:\n"
|
||||
response += f"• Total Payments: {len(all_payments)}\n"
|
||||
response += f"• ✅ Completed: {completed} ({completed_amount:,.2f} €)\n"
|
||||
response += f"• ⏳ Pending: {pending} ({pending_amount:,.2f} €)\n"
|
||||
response += f"• ❌ Failed: {failed}\n"
|
||||
if refunded > 0:
|
||||
response += f"• 🔄 Refunded: {refunded}\n"
|
||||
response += f"• Total Amount: {total_amount:,.2f} €"
|
||||
|
||||
return response
|
||||
else:
|
||||
return "No payments found."
|
||||
|
||||
elif intent == "payment_status":
|
||||
# Role-aware payment status
|
||||
if is_customer:
|
||||
user_payments = data.get("user_payments", [])
|
||||
if user_payments:
|
||||
response = f"Your recent payments ({len(user_payments)} total):\n\n"
|
||||
for payment in user_payments[:5]:
|
||||
status_emoji = "✅" if payment.get('payment_status') == 'completed' else "⏳" if payment.get('payment_status') == 'pending' else "❌"
|
||||
response += f"{status_emoji} ${payment.get('amount', 0):,.2f} - {payment.get('payment_method', 'N/A')}\n"
|
||||
response += f" Status: {payment.get('payment_status', 'Unknown')}, Date: {payment.get('payment_date', 'N/A')}\n"
|
||||
if len(user_payments) > 5:
|
||||
response += f"\n... and {len(user_payments) - 5} more payments."
|
||||
return response
|
||||
else:
|
||||
return "You don't have any payment records at this time."
|
||||
else:
|
||||
summary = data.get("payment_summary", {})
|
||||
if summary:
|
||||
response = "Payment Status Summary:\n"
|
||||
response += f"• Total Revenue (last 30 days): ${summary.get('total_revenue', 0):,.2f}\n"
|
||||
response += f"• Pending Payments: ${summary.get('pending_amount', 0):,.2f}\n"
|
||||
response += f"• Completed Payments: {summary.get('completed_payments', 0)}"
|
||||
return response
|
||||
return "No payment data available at this time."
|
||||
|
||||
elif intent == "chat_status":
|
||||
chats = data.get("unanswered_chats", [])
|
||||
if chats:
|
||||
response = f"Chat Status: There are {len(chats)} unanswered chat messages.\n\n"
|
||||
response += "Oldest waiting messages:\n"
|
||||
for i, chat in enumerate(chats[:3], 1):
|
||||
response += f"{i}. {chat['visitor_name']} - Waiting {chat['waiting_hours']:.1f} hours\n"
|
||||
response += f" Last message: {chat['last_message'][:50]}...\n"
|
||||
return response
|
||||
else:
|
||||
return "All chat messages have been answered. Great job! ✅"
|
||||
|
||||
elif intent == "general_status":
|
||||
# Comprehensive status overview - always show all sections for consistency
|
||||
room_summary = data.get("room_summary", {})
|
||||
booking_summary = data.get("booking_summary", {})
|
||||
payment_summary = data.get("payment_summary", {})
|
||||
invoice_summary = data.get("invoice_summary", {})
|
||||
unanswered_chats = data.get("unanswered_chats", [])
|
||||
problems = data.get("room_problems", [])
|
||||
|
||||
response = "📊 Hotel Status Overview:\n\n"
|
||||
|
||||
# Rooms - always show
|
||||
response += "🏨 ROOMS:\n"
|
||||
occupancy_rate = room_summary.get('occupancy_rate', 0)
|
||||
# Format occupancy rate to show one decimal place
|
||||
if isinstance(occupancy_rate, (int, float)):
|
||||
occupancy_rate = f"{occupancy_rate:.1f}"
|
||||
response += f"• Total: {room_summary.get('total_rooms', 0)}\n"
|
||||
response += f"• Available: {room_summary.get('available', 0)}\n"
|
||||
response += f"• Occupied: {room_summary.get('occupied', 0)}\n"
|
||||
response += f"• Cleaning: {room_summary.get('cleaning', 0)}\n"
|
||||
response += f"• Maintenance: {room_summary.get('maintenance', 0)}\n"
|
||||
response += f"• Occupancy Rate: {occupancy_rate}%\n\n"
|
||||
|
||||
# Bookings - always show
|
||||
response += "📅 BOOKINGS:\n"
|
||||
response += f"• Active: {booking_summary.get('active_bookings', 0)}\n"
|
||||
response += f"• Upcoming Check-ins: {booking_summary.get('upcoming_checkins', 0)}\n"
|
||||
response += f"• Upcoming Check-outs: {booking_summary.get('upcoming_checkouts', 0)}\n"
|
||||
response += f"• Pending: {booking_summary.get('pending_bookings', 0)}\n\n"
|
||||
|
||||
# Payments - always show
|
||||
response += "💰 PAYMENTS:\n"
|
||||
total_revenue = payment_summary.get('total_revenue', 0) or 0
|
||||
pending_amount = payment_summary.get('pending_amount', 0) or 0
|
||||
response += f"• Revenue (30 days): ${total_revenue:,.2f}\n"
|
||||
response += f"• Pending: ${pending_amount:,.2f}\n"
|
||||
response += f"• Completed: {payment_summary.get('completed_payments', 0)}\n\n"
|
||||
|
||||
# Invoices - always show
|
||||
response += "🧾 INVOICES:\n"
|
||||
response += f"• Total: {invoice_summary.get('total_invoices', 0)}\n"
|
||||
response += f"• Overdue: {invoice_summary.get('overdue_invoices', 0)}\n\n"
|
||||
|
||||
# Chats - always show
|
||||
response += "💬 CHATS:\n"
|
||||
response += f"• Unanswered: {len(unanswered_chats)}\n\n"
|
||||
|
||||
# Problems - show if any, otherwise skip
|
||||
if problems:
|
||||
response += "⚠️ ISSUES:\n"
|
||||
response += f"• Rooms with problems: {len(problems)}\n"
|
||||
for problem in problems[:3]:
|
||||
response += f" - Room {problem['room_number']}: {problem.get('description', 'Issue reported')}\n"
|
||||
|
||||
return response
|
||||
|
||||
elif intent == "app_info":
|
||||
# Comprehensive application information - formatted for admin
|
||||
app_knowledge = data.get("app_knowledge", {})
|
||||
role_name = self.role_access_service.role_name if self.role_access_service else "customer"
|
||||
|
||||
if role_name != "admin":
|
||||
response = "📱 Application Overview:\n\n"
|
||||
response += app_knowledge.get("overview", "This is a Hotel Booking and Management System.")
|
||||
response += "\n\n"
|
||||
role_info = app_knowledge.get("role_info", {})
|
||||
if role_info:
|
||||
response += f"Your Role: {role_name.upper()}\n"
|
||||
response += f"Description: {role_info.get('description', 'Standard user access')}\n"
|
||||
return response
|
||||
|
||||
# Admin gets full comprehensive information
|
||||
response = "📱 Hotel Booking & Management System (HBMS) - Complete Overview\n\n"
|
||||
response += "=" * 60 + "\n\n"
|
||||
|
||||
# Overview
|
||||
response += "📋 SYSTEM OVERVIEW:\n"
|
||||
response += app_knowledge.get("overview", "") + "\n\n"
|
||||
|
||||
# Core Features
|
||||
features = app_knowledge.get("features", {})
|
||||
if features:
|
||||
response += "🔧 CORE FEATURES & MODULES:\n\n"
|
||||
feature_count = 0
|
||||
for feature_name, feature_info in features.items():
|
||||
feature_count += 1
|
||||
feature_title = feature_name.replace("_", " ").title()
|
||||
response += f"{feature_count}. {feature_title}\n"
|
||||
response += f" {feature_info.get('description', 'N/A')}\n"
|
||||
|
||||
if feature_info.get('models'):
|
||||
models = feature_info.get('models', [])
|
||||
if models:
|
||||
response += f" Models: {', '.join(models[:5])}"
|
||||
if len(models) > 5:
|
||||
response += f" (+{len(models) - 5} more)"
|
||||
response += "\n"
|
||||
|
||||
if feature_info.get('statuses'):
|
||||
statuses = feature_info.get('statuses', [])
|
||||
response += f" Statuses: {', '.join(statuses)}\n"
|
||||
|
||||
response += "\n"
|
||||
|
||||
# User Roles
|
||||
role_capabilities = app_knowledge.get("role_capabilities", {})
|
||||
if role_capabilities:
|
||||
response += "👥 USER ROLES & PERMISSIONS:\n\n"
|
||||
for role_name_key, role_info in role_capabilities.items():
|
||||
response += f"• {role_name_key.upper()}\n"
|
||||
response += f" {role_info.get('description', '')}\n"
|
||||
|
||||
can_access = role_info.get('can_access', [])
|
||||
if can_access:
|
||||
response += f" Can Access: {len(can_access)} features\n"
|
||||
# Show first 5 capabilities
|
||||
for cap in can_access[:5]:
|
||||
response += f" - {cap}\n"
|
||||
if len(can_access) > 5:
|
||||
response += f" ... and {len(can_access) - 5} more\n"
|
||||
|
||||
cannot_access = role_info.get('cannot_access', [])
|
||||
if cannot_access:
|
||||
response += f" Restrictions: {len(cannot_access)} items\n"
|
||||
|
||||
response += "\n"
|
||||
|
||||
# Technology Stack
|
||||
response += "💻 TECHNOLOGY STACK:\n\n"
|
||||
response += "• Backend: FastAPI (Python)\n"
|
||||
response += "• Frontend: React/TypeScript\n"
|
||||
response += "• Database: SQLAlchemy ORM\n"
|
||||
response += "• AI: Self-learning assistant with pattern recognition\n"
|
||||
response += "• Authentication: JWT with MFA support\n"
|
||||
response += "• Security: Role-based access control, audit logging\n\n"
|
||||
|
||||
# Key Capabilities
|
||||
response += "✨ KEY CAPABILITIES:\n\n"
|
||||
response += "• Complete booking lifecycle management\n"
|
||||
response += "• Multi-role access control (Admin, Staff, Accountant, Customer)\n"
|
||||
response += "• Payment processing with multiple methods\n"
|
||||
response += "• Invoice generation and tracking\n"
|
||||
response += "• Loyalty program with points and rewards\n"
|
||||
response += "• Real-time chat support system\n"
|
||||
response += "• Room maintenance and housekeeping management\n"
|
||||
response += "• Guest profile and CRM features\n"
|
||||
response += "• Analytics and reporting\n"
|
||||
response += "• Content management (blog, pages, banners)\n"
|
||||
response += "• Email campaigns and marketing automation\n"
|
||||
response += "• Group booking management\n"
|
||||
response += "• Dynamic pricing and rate plans\n"
|
||||
response += "• Package deals (room + services)\n"
|
||||
response += "• Review and rating system\n"
|
||||
response += "• Audit logging and security compliance\n\n"
|
||||
|
||||
# Common Queries
|
||||
common_queries = app_knowledge.get("common_queries", {})
|
||||
if common_queries:
|
||||
response += "💬 COMMON QUERY CATEGORIES:\n\n"
|
||||
for category, queries in common_queries.items():
|
||||
response += f"• {category.title()}: {', '.join(queries[:3])}"
|
||||
if len(queries) > 3:
|
||||
response += f" (+{len(queries) - 3} more)"
|
||||
response += "\n"
|
||||
response += "\n"
|
||||
|
||||
response += "=" * 60 + "\n"
|
||||
response += "\n💡 TIP: Ask me about any specific feature, model, or capability for detailed information!"
|
||||
|
||||
return response
|
||||
|
||||
elif intent == "general_query":
|
||||
# Try to provide helpful response based on available data
|
||||
room_summary = data.get("room_summary", {})
|
||||
booking_summary = data.get("booking_summary", {})
|
||||
payment_summary = data.get("payment_summary", {})
|
||||
|
||||
response = "I can help you with information about:\n\n"
|
||||
response += "🏨 Rooms - availability, occupancy, status, problems\n"
|
||||
response += "📅 Bookings - active, upcoming, pending reservations\n"
|
||||
response += "💰 Payments - revenue, pending payments, transactions\n"
|
||||
response += "🧾 Invoices - status, overdue invoices\n"
|
||||
response += "💬 Chats - unanswered customer messages\n\n"
|
||||
|
||||
if room_summary:
|
||||
response += f"Quick Stats: {room_summary.get('total_rooms', 0)} total rooms, "
|
||||
response += f"{room_summary.get('occupied', 0)} occupied, "
|
||||
response += f"{room_summary.get('available', 0)} available.\n\n"
|
||||
|
||||
response += "Try asking:\n"
|
||||
response += "• 'How many rooms do we have?'\n"
|
||||
response += "• 'Show me occupied rooms'\n"
|
||||
response += "• 'What's our revenue?'\n"
|
||||
response += "• 'Are there any unanswered chats?'"
|
||||
|
||||
return response
|
||||
|
||||
else:
|
||||
return "I can help you with questions about rooms, bookings, invoices, payments, and chat messages. What would you like to know?"
|
||||
|
||||
def _apply_learned_pattern(self, pattern: AILearnedPattern, data: Dict, query: str) -> Optional[str]:
|
||||
"""Apply learned pattern to generate response"""
|
||||
try:
|
||||
response_template = pattern.response_template or ""
|
||||
|
||||
# Replace placeholders with actual data
|
||||
if "{data}" in response_template and data:
|
||||
# Inject relevant data into response
|
||||
data_str = json.dumps(data, indent=2, default=str)[:500]
|
||||
response = response_template.replace("{data}", data_str)
|
||||
else:
|
||||
response = response_template
|
||||
|
||||
return response[:2000] # Limit response length
|
||||
except Exception as e:
|
||||
logger.error(f"Error applying learned pattern: {str(e)}", exc_info=True)
|
||||
return None
|
||||
|
||||
382
Backend/src/ai/services/ai_knowledge_base.py
Normal file
382
Backend/src/ai/services/ai_knowledge_base.py
Normal file
@@ -0,0 +1,382 @@
|
||||
"""
|
||||
Enterprise AI Knowledge Base
|
||||
Comprehensive documentation of all application features, models, and capabilities
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Any
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AIKnowledgeBase:
|
||||
"""Enterprise Knowledge Base for AI Assistant - Documents entire application"""
|
||||
|
||||
APPLICATION_OVERVIEW = """
|
||||
This is a comprehensive Hotel Booking and Management System (HBMS) - an enterprise-grade
|
||||
solution for managing hotel operations, reservations, payments, guest services, and business analytics.
|
||||
|
||||
The system supports multiple user roles: Admin, Staff, Accountant, and Customer, each with
|
||||
different access levels and permissions.
|
||||
"""
|
||||
|
||||
# Core Models and Features
|
||||
FEATURES = {
|
||||
"booking_management": {
|
||||
"description": "Complete booking lifecycle management including creation, confirmation, check-in, check-out, and cancellation",
|
||||
"models": ["Booking", "BookingStatus"],
|
||||
"key_fields": ["booking_number", "user_id", "room_id", "check_in_date", "check_out_date",
|
||||
"num_guests", "total_price", "status", "special_requests"],
|
||||
"statuses": ["pending", "confirmed", "checked_in", "checked_out", "cancelled"],
|
||||
"relationships": ["User", "Room", "Payment", "Invoice", "ServiceUsage", "CheckInCheckOut"]
|
||||
},
|
||||
|
||||
"room_management": {
|
||||
"description": "Room inventory, availability, status tracking, and maintenance",
|
||||
"models": ["Room", "RoomType", "RoomStatus"],
|
||||
"key_fields": ["room_number", "floor", "status", "price", "capacity", "amenities", "room_size", "view"],
|
||||
"statuses": ["available", "occupied", "maintenance", "cleaning"],
|
||||
"relationships": ["RoomType", "Booking", "Review", "RoomMaintenance", "HousekeepingTask"]
|
||||
},
|
||||
|
||||
"payment_processing": {
|
||||
"description": "Payment handling for bookings with multiple payment methods and status tracking",
|
||||
"models": ["Payment", "PaymentMethod", "PaymentStatus", "PaymentType"],
|
||||
"key_fields": ["amount", "payment_method", "payment_type", "payment_status", "transaction_id", "payment_date"],
|
||||
"payment_methods": ["cash", "credit_card", "debit_card", "bank_transfer", "e_wallet", "stripe", "paypal", "borica"],
|
||||
"statuses": ["pending", "completed", "failed", "refunded"],
|
||||
"payment_types": ["full", "deposit", "remaining"],
|
||||
"relationships": ["Booking"]
|
||||
},
|
||||
|
||||
"invoice_management": {
|
||||
"description": "Invoice generation, tracking, and payment reconciliation",
|
||||
"models": ["Invoice", "InvoiceItem", "InvoiceStatus"],
|
||||
"key_fields": ["invoice_number", "total_amount", "subtotal", "tax_amount", "discount_amount",
|
||||
"balance_due", "status", "due_date", "paid_date"],
|
||||
"statuses": ["draft", "sent", "paid", "overdue", "cancelled"],
|
||||
"relationships": ["Booking", "User", "InvoiceItem"]
|
||||
},
|
||||
|
||||
"user_management": {
|
||||
"description": "User accounts, roles, authentication, and guest profiles",
|
||||
"models": ["User", "Role", "RefreshToken", "PasswordResetToken"],
|
||||
"key_fields": ["email", "full_name", "phone", "role_id", "is_active", "currency"],
|
||||
"roles": ["admin", "staff", "customer", "accountant"],
|
||||
"guest_fields": ["is_vip", "lifetime_value", "satisfaction_score", "total_visits", "last_visit_date"],
|
||||
"relationships": ["Role", "Booking", "Payment", "Invoice", "Review", "Favorite"]
|
||||
},
|
||||
|
||||
"loyalty_program": {
|
||||
"description": "Customer loyalty program with tiers, points, rewards, and referrals",
|
||||
"models": ["LoyaltyTier", "UserLoyalty", "LoyaltyPointTransaction", "LoyaltyReward",
|
||||
"RewardRedemption", "Referral"],
|
||||
"key_fields": ["tier_level", "points_balance", "lifetime_points", "referral_code"],
|
||||
"relationships": ["User", "Booking"]
|
||||
},
|
||||
|
||||
"service_management": {
|
||||
"description": "Additional hotel services like spa, restaurant, room service",
|
||||
"models": ["Service", "ServiceUsage", "ServiceBooking", "ServiceBookingItem", "ServicePayment"],
|
||||
"key_fields": ["service_name", "price", "category", "description"],
|
||||
"relationships": ["Booking"]
|
||||
},
|
||||
|
||||
"promotion_system": {
|
||||
"description": "Discount codes, promotional campaigns, and special offers",
|
||||
"models": ["Promotion"],
|
||||
"key_fields": ["code", "discount_type", "discount_value", "start_date", "end_date", "is_active"],
|
||||
"relationships": ["Booking"]
|
||||
},
|
||||
|
||||
"reviews_and_ratings": {
|
||||
"description": "Guest reviews and ratings for rooms and services",
|
||||
"models": ["Review"],
|
||||
"key_fields": ["rating", "comment", "is_verified"],
|
||||
"relationships": ["User", "Room"]
|
||||
},
|
||||
|
||||
"chat_system": {
|
||||
"description": "Real-time chat support between guests and staff",
|
||||
"models": ["Chat", "ChatMessage", "ChatStatus"],
|
||||
"key_fields": ["visitor_name", "visitor_email", "status", "message", "sender_type", "is_read"],
|
||||
"statuses": ["open", "closed"],
|
||||
"relationships": ["User"]
|
||||
},
|
||||
|
||||
"check_in_check_out": {
|
||||
"description": "Check-in and check-out process management",
|
||||
"models": ["CheckInCheckOut"],
|
||||
"key_fields": ["checked_in_at", "checked_out_at", "checked_in_by", "checked_out_by", "notes"],
|
||||
"relationships": ["Booking", "User"]
|
||||
},
|
||||
|
||||
"maintenance_tracking": {
|
||||
"description": "Room maintenance scheduling and tracking",
|
||||
"models": ["RoomMaintenance", "RoomInspection"],
|
||||
"key_fields": ["scheduled_start", "scheduled_end", "status", "description", "priority"],
|
||||
"relationships": ["Room"]
|
||||
},
|
||||
|
||||
"housekeeping": {
|
||||
"description": "Housekeeping task management and scheduling",
|
||||
"models": ["HousekeepingTask"],
|
||||
"key_fields": ["task_type", "status", "scheduled_time", "completed_at"],
|
||||
"relationships": ["Room"]
|
||||
},
|
||||
|
||||
"guest_management": {
|
||||
"description": "Comprehensive guest profile management and CRM features",
|
||||
"models": ["GuestNote", "GuestPreference", "GuestTag", "GuestSegment", "GuestCommunication"],
|
||||
"key_fields": ["notes", "preferences", "tags", "communication_history"],
|
||||
"relationships": ["User"]
|
||||
},
|
||||
|
||||
"group_bookings": {
|
||||
"description": "Group booking management for multiple rooms",
|
||||
"models": ["GroupBooking"],
|
||||
"key_fields": ["group_name", "contact_person", "total_rooms", "group_discount"],
|
||||
"relationships": ["Booking"]
|
||||
},
|
||||
|
||||
"rate_plans": {
|
||||
"description": "Dynamic pricing and rate plan management",
|
||||
"models": ["RatePlan"],
|
||||
"key_fields": ["name", "base_price", "season_multiplier", "valid_from", "valid_to"],
|
||||
"relationships": ["Booking"]
|
||||
},
|
||||
|
||||
"packages": {
|
||||
"description": "Pre-configured booking packages (room + services)",
|
||||
"models": ["Package"],
|
||||
"key_fields": ["name", "price", "includes", "valid_period"],
|
||||
"relationships": ["RoomType", "Service"]
|
||||
},
|
||||
|
||||
"blog_and_content": {
|
||||
"description": "Content management for hotel blog and marketing",
|
||||
"models": ["BlogPost", "Banner", "PageContent"],
|
||||
"key_fields": ["title", "content", "is_published", "published_at"],
|
||||
"relationships": []
|
||||
},
|
||||
|
||||
"analytics_and_reporting": {
|
||||
"description": "Business intelligence, analytics, and financial reporting",
|
||||
"features": ["Revenue reports", "Occupancy analytics", "Guest analytics", "Payment reports",
|
||||
"Invoice reports", "Booking trends", "Customer lifetime value"],
|
||||
"models": ["AuditLog"]
|
||||
},
|
||||
|
||||
"system_settings": {
|
||||
"description": "System-wide configuration and settings",
|
||||
"models": ["SystemSettings"],
|
||||
"key_fields": ["key", "value", "category"],
|
||||
"categories": ["general", "payment", "email", "company_info"]
|
||||
},
|
||||
|
||||
"security_and_compliance": {
|
||||
"description": "Security, audit logging, and GDPR compliance",
|
||||
"models": ["SecurityEvent", "AuditLog", "GDPRCompliance"],
|
||||
"features": ["Action audit trails", "Security event logging", "GDPR data requests",
|
||||
"Cookie consent management"]
|
||||
},
|
||||
|
||||
"notifications": {
|
||||
"description": "System notifications and alerts",
|
||||
"models": ["Notification"],
|
||||
"key_fields": ["type", "message", "is_read", "priority"],
|
||||
"relationships": ["User"]
|
||||
},
|
||||
|
||||
"workflows": {
|
||||
"description": "Automated workflow management for business processes",
|
||||
"models": ["Workflow"],
|
||||
"key_fields": ["name", "trigger", "actions", "conditions"],
|
||||
"relationships": []
|
||||
},
|
||||
|
||||
"email_campaigns": {
|
||||
"description": "Marketing email campaigns and automation",
|
||||
"models": ["EmailCampaign"],
|
||||
"key_fields": ["subject", "content", "recipient_segment", "scheduled_time"],
|
||||
"relationships": ["GuestSegment"]
|
||||
}
|
||||
}
|
||||
|
||||
ROLE_CAPABILITIES = {
|
||||
"admin": {
|
||||
"description": "Full system access with all administrative privileges",
|
||||
"can_access": [
|
||||
"All bookings (all customers)",
|
||||
"All rooms and room management",
|
||||
"All payments and financial data",
|
||||
"All invoices",
|
||||
"All users and user management",
|
||||
"System settings and configuration",
|
||||
"Analytics and reports",
|
||||
"Audit logs and security events",
|
||||
"Content management (blog, banners, pages)",
|
||||
"Promotion management",
|
||||
"Loyalty program administration",
|
||||
"Service management",
|
||||
"Maintenance and housekeeping",
|
||||
"Check-in/check-out operations",
|
||||
"Chat management",
|
||||
"Email campaigns",
|
||||
"Workflow management",
|
||||
"Group bookings",
|
||||
"Rate plans and packages"
|
||||
],
|
||||
"cannot_access": [],
|
||||
"restrictions": "None - full access"
|
||||
},
|
||||
|
||||
"accountant": {
|
||||
"description": "Financial data access for accounting and reporting",
|
||||
"can_access": [
|
||||
"All payments and payment history",
|
||||
"All invoices and invoice management",
|
||||
"Financial reports and analytics",
|
||||
"Revenue summaries",
|
||||
"Payment reconciliation",
|
||||
"Invoice generation and editing",
|
||||
"Booking financial details (for invoicing)",
|
||||
"Customer payment history",
|
||||
"Refund processing",
|
||||
"Financial dashboard statistics"
|
||||
],
|
||||
"cannot_access": [
|
||||
"User personal information (except payment-related)",
|
||||
"System settings",
|
||||
"Content management",
|
||||
"Room operational details (unless related to billing)",
|
||||
"Security and audit logs (financial audit access only)"
|
||||
],
|
||||
"restrictions": "Financial and payment data only"
|
||||
},
|
||||
|
||||
"staff": {
|
||||
"description": "Operational staff with access to daily hotel operations",
|
||||
"can_access": [
|
||||
"All bookings and booking management",
|
||||
"Room status and availability",
|
||||
"Check-in and check-out operations",
|
||||
"Guest information (for operations)",
|
||||
"Room maintenance and housekeeping tasks",
|
||||
"Service bookings and service usage",
|
||||
"Chat system for customer support",
|
||||
"Basic booking reports",
|
||||
"Occupancy status",
|
||||
"Group bookings",
|
||||
"Guest preferences and notes (operational)"
|
||||
],
|
||||
"cannot_access": [
|
||||
"Financial reports and analytics",
|
||||
"System settings",
|
||||
"User management (except guest profile updates)",
|
||||
"Content management",
|
||||
"Email campaigns",
|
||||
"Workflow management",
|
||||
"Full payment details (can view payment status only)",
|
||||
"Invoice editing (view only)"
|
||||
],
|
||||
"restrictions": "Operational data only, no financial or system administration"
|
||||
},
|
||||
|
||||
"customer": {
|
||||
"description": "Hotel guests with access to their own booking and account information",
|
||||
"can_access": [
|
||||
"Own bookings (view, cancel)",
|
||||
"Own booking details",
|
||||
"Own invoices (view only)",
|
||||
"Own payment history",
|
||||
"Own profile information",
|
||||
"Room browsing and availability (public)",
|
||||
"Service browsing",
|
||||
"Booking creation",
|
||||
"Review submission",
|
||||
"Favorite rooms",
|
||||
"Loyalty points and rewards",
|
||||
"Referral code usage",
|
||||
"Chat with support"
|
||||
],
|
||||
"cannot_access": [
|
||||
"Other customers' bookings or information",
|
||||
"All bookings overview",
|
||||
"Financial reports",
|
||||
"System settings",
|
||||
"Room management",
|
||||
"Staff operations",
|
||||
"Admin features",
|
||||
"Other users' data"
|
||||
],
|
||||
"restrictions": "Only own data and customer-facing features"
|
||||
}
|
||||
}
|
||||
|
||||
COMMON_QUERIES = {
|
||||
"booking": [
|
||||
"booking status", "check-in date", "check-out date", "booking number",
|
||||
"cancel booking", "modify booking", "upcoming bookings", "past bookings"
|
||||
],
|
||||
"room": [
|
||||
"available rooms", "room types", "room amenities", "room prices",
|
||||
"room availability", "occupied rooms", "room status"
|
||||
],
|
||||
"payment": [
|
||||
"payment status", "payment history", "payment methods", "refund",
|
||||
"outstanding balance", "payment due", "transaction"
|
||||
],
|
||||
"invoice": [
|
||||
"invoice status", "invoice number", "invoice amount", "due date",
|
||||
"paid invoice", "outstanding invoice", "invoice download"
|
||||
],
|
||||
"loyalty": [
|
||||
"loyalty points", "loyalty tier", "rewards", "referral code",
|
||||
"points balance", "points history"
|
||||
],
|
||||
"general": [
|
||||
"hotel information", "contact", "policies", "services", "amenities",
|
||||
"location", "FAQ", "help"
|
||||
]
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_application_knowledge(cls) -> Dict[str, Any]:
|
||||
"""Get comprehensive application knowledge for AI context"""
|
||||
return {
|
||||
"overview": cls.APPLICATION_OVERVIEW,
|
||||
"features": cls.FEATURES,
|
||||
"role_capabilities": cls.ROLE_CAPABILITIES,
|
||||
"common_queries": cls.COMMON_QUERIES
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_role_info(cls, role_name: str) -> Dict[str, Any]:
|
||||
"""Get information about a specific role"""
|
||||
return cls.ROLE_CAPABILITIES.get(role_name.lower(), {})
|
||||
|
||||
@classmethod
|
||||
def get_feature_info(cls, feature_name: str) -> Dict[str, Any]:
|
||||
"""Get information about a specific feature"""
|
||||
return cls.FEATURES.get(feature_name.lower(), {})
|
||||
|
||||
@classmethod
|
||||
def can_role_access_feature(cls, role_name: str, feature: str) -> bool:
|
||||
"""Check if a role can access a specific feature"""
|
||||
role_info = cls.ROLE_CAPABILITIES.get(role_name.lower(), {})
|
||||
can_access = role_info.get("can_access", [])
|
||||
|
||||
# Admin can access everything
|
||||
if role_name.lower() == "admin":
|
||||
return True
|
||||
|
||||
# Check if feature is in can_access list
|
||||
feature_keywords = feature.lower().split("_")
|
||||
for accessible in can_access:
|
||||
accessible_lower = accessible.lower()
|
||||
if any(keyword in accessible_lower for keyword in feature_keywords):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
818
Backend/src/ai/services/ai_learning_service.py
Normal file
818
Backend/src/ai/services/ai_learning_service.py
Normal file
@@ -0,0 +1,818 @@
|
||||
"""
|
||||
AI Learning Service - Self-learning and pattern recognition system
|
||||
Learns from conversations, stores patterns, and improves responses over time
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Optional, Any, Tuple
|
||||
from datetime import datetime, timedelta
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import and_, or_, func, desc
|
||||
import json
|
||||
import re
|
||||
import logging
|
||||
from collections import Counter
|
||||
|
||||
from ..models.ai_conversation import (
|
||||
AIConversation, AIConversationFeedback, AILearnedPattern,
|
||||
AIKnowledgeEntry, AITrainingMetrics
|
||||
)
|
||||
from ...shared.config.logging_config import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class AILearningService:
|
||||
"""Service for AI self-learning and pattern recognition"""
|
||||
|
||||
def __init__(self, db: Session):
|
||||
self.db = db
|
||||
|
||||
def save_conversation(
|
||||
self,
|
||||
user_id: int,
|
||||
user_query: str,
|
||||
ai_response: str,
|
||||
intent: str,
|
||||
context_used: Optional[Dict] = None,
|
||||
user_role: Optional[str] = None,
|
||||
session_id: Optional[str] = None,
|
||||
response_time_ms: Optional[int] = None
|
||||
) -> AIConversation:
|
||||
"""Save conversation for learning"""
|
||||
try:
|
||||
conversation = AIConversation(
|
||||
user_id=user_id,
|
||||
session_id=session_id,
|
||||
user_query=user_query,
|
||||
ai_response=ai_response,
|
||||
intent=intent,
|
||||
context_used=context_used or {},
|
||||
user_role=user_role,
|
||||
response_time_ms=response_time_ms
|
||||
)
|
||||
self.db.add(conversation)
|
||||
self.db.commit()
|
||||
self.db.refresh(conversation)
|
||||
|
||||
# Trigger learning process in background
|
||||
self._learn_from_conversation(conversation)
|
||||
|
||||
return conversation
|
||||
except Exception as e:
|
||||
logger.error(f"Error saving conversation: {str(e)}", exc_info=True)
|
||||
self.db.rollback()
|
||||
raise
|
||||
|
||||
def save_feedback(
|
||||
self,
|
||||
conversation_id: int,
|
||||
user_id: int,
|
||||
rating: Optional[int] = None,
|
||||
is_helpful: Optional[bool] = None,
|
||||
is_correct: Optional[bool] = None,
|
||||
feedback_text: Optional[str] = None,
|
||||
correction: Optional[str] = None
|
||||
) -> AIConversationFeedback:
|
||||
"""Save user feedback for learning"""
|
||||
try:
|
||||
feedback = AIConversationFeedback(
|
||||
conversation_id=conversation_id,
|
||||
user_id=user_id,
|
||||
rating=rating,
|
||||
is_helpful=is_helpful,
|
||||
is_correct=is_correct,
|
||||
feedback_text=feedback_text,
|
||||
correction=correction
|
||||
)
|
||||
self.db.add(feedback)
|
||||
|
||||
# Update conversation helpfulness
|
||||
conversation = self.db.query(AIConversation).filter(
|
||||
AIConversation.id == conversation_id
|
||||
).first()
|
||||
if conversation:
|
||||
conversation.is_helpful = is_helpful
|
||||
|
||||
self.db.commit()
|
||||
self.db.refresh(feedback)
|
||||
|
||||
# Trigger learning from feedback
|
||||
if feedback.is_correct == False or correction:
|
||||
self._learn_from_feedback(conversation, feedback)
|
||||
|
||||
return feedback
|
||||
except Exception as e:
|
||||
logger.error(f"Error saving feedback: {str(e)}", exc_info=True)
|
||||
self.db.rollback()
|
||||
raise
|
||||
|
||||
def find_similar_queries(self, query: str, intent: Optional[str] = None, limit: int = 5) -> List[Dict[str, Any]]:
|
||||
"""Find similar past queries to learn from"""
|
||||
try:
|
||||
# Extract keywords from query
|
||||
keywords = self._extract_keywords(query)
|
||||
|
||||
# Search for similar queries
|
||||
query_obj = self.db.query(AIConversation).filter(
|
||||
AIConversation.user_query.ilike(f"%{query[:50]}%")
|
||||
)
|
||||
|
||||
if intent:
|
||||
query_obj = query_obj.filter(AIConversation.intent == intent)
|
||||
|
||||
similar = query_obj.order_by(desc(AIConversation.created_at)).limit(limit).all()
|
||||
|
||||
results = []
|
||||
for conv in similar:
|
||||
results.append({
|
||||
"query": conv.user_query,
|
||||
"response": conv.ai_response,
|
||||
"intent": conv.intent,
|
||||
"is_helpful": conv.is_helpful,
|
||||
"created_at": conv.created_at.isoformat()
|
||||
})
|
||||
|
||||
return results
|
||||
except Exception as e:
|
||||
logger.error(f"Error finding similar queries: {str(e)}", exc_info=True)
|
||||
return []
|
||||
|
||||
def get_learned_pattern(self, query: str, user_role: Optional[str] = None) -> Optional[AILearnedPattern]:
|
||||
"""Get learned pattern that matches query"""
|
||||
try:
|
||||
keywords = self._extract_keywords(query)
|
||||
|
||||
# Search patterns by keywords
|
||||
patterns = self.db.query(AILearnedPattern).filter(
|
||||
and_(
|
||||
AILearnedPattern.is_active == True,
|
||||
or_(*[AILearnedPattern.pattern_keywords.contains(kw) for kw in keywords[:5]])
|
||||
)
|
||||
)
|
||||
|
||||
if user_role:
|
||||
patterns = patterns.filter(
|
||||
or_(
|
||||
AILearnedPattern.user_role == user_role,
|
||||
AILearnedPattern.user_role.is_(None)
|
||||
)
|
||||
)
|
||||
|
||||
# Order by confidence and usage
|
||||
patterns = patterns.order_by(
|
||||
desc(AILearnedPattern.confidence_score),
|
||||
desc(AILearnedPattern.usage_count)
|
||||
).limit(1).all()
|
||||
|
||||
if patterns:
|
||||
pattern = patterns[0]
|
||||
# Update usage stats
|
||||
pattern.usage_count += 1
|
||||
pattern.last_used_at = datetime.utcnow()
|
||||
self.db.commit()
|
||||
return pattern
|
||||
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting learned pattern: {str(e)}", exc_info=True)
|
||||
return None
|
||||
|
||||
def get_knowledge_entry(self, query: str, user_role: Optional[str] = None) -> Optional[Dict[str, Any]]:
|
||||
"""Get knowledge base entry that matches query"""
|
||||
try:
|
||||
keywords = self._extract_keywords(query)
|
||||
query_lower = query.lower()
|
||||
|
||||
# Search knowledge entries
|
||||
entries = self.db.query(AIKnowledgeEntry).filter(
|
||||
and_(
|
||||
or_(
|
||||
AIKnowledgeEntry.question.ilike(f"%{query[:50]}%"),
|
||||
*[AIKnowledgeEntry.keywords.contains([kw]) for kw in keywords[:3]]
|
||||
),
|
||||
or_(
|
||||
AIKnowledgeEntry.is_verified == True,
|
||||
AIKnowledgeEntry.confidence >= 70.0
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
if user_role:
|
||||
entries = entries.filter(
|
||||
or_(
|
||||
AIKnowledgeEntry.user_role == user_role,
|
||||
AIKnowledgeEntry.user_role.is_(None)
|
||||
)
|
||||
)
|
||||
|
||||
entries = entries.order_by(
|
||||
desc(AIKnowledgeEntry.confidence),
|
||||
desc(AIKnowledgeEntry.success_count)
|
||||
).limit(1).all()
|
||||
|
||||
if entries:
|
||||
entry = entries[0]
|
||||
# Update usage stats
|
||||
entry.usage_count += 1
|
||||
entry.last_used_at = datetime.utcnow()
|
||||
self.db.commit()
|
||||
|
||||
return {
|
||||
"topic": entry.topic,
|
||||
"question": entry.question,
|
||||
"answer": entry.answer,
|
||||
"confidence": float(entry.confidence)
|
||||
}
|
||||
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting knowledge entry: {str(e)}", exc_info=True)
|
||||
return None
|
||||
|
||||
def _learn_from_conversation(self, conversation: AIConversation):
|
||||
"""Learn patterns from a conversation - automatically learns from all conversations"""
|
||||
try:
|
||||
# Always extract keywords and patterns (learn from all conversations)
|
||||
keywords = self._extract_keywords(conversation.user_query)
|
||||
|
||||
# Check if similar pattern exists
|
||||
existing_pattern = self.get_learned_pattern(
|
||||
conversation.user_query,
|
||||
conversation.user_role
|
||||
)
|
||||
|
||||
if existing_pattern:
|
||||
# Update existing pattern confidence
|
||||
existing_pattern.success_count += 1
|
||||
existing_pattern.usage_count += 1
|
||||
|
||||
# Adjust confidence based on helpfulness
|
||||
if conversation.is_helpful is True:
|
||||
existing_pattern.confidence_score = min(
|
||||
100.0,
|
||||
existing_pattern.confidence_score + 1.0
|
||||
)
|
||||
elif conversation.is_helpful is False:
|
||||
existing_pattern.confidence_score = max(
|
||||
0.0,
|
||||
existing_pattern.confidence_score - 2.0
|
||||
)
|
||||
else:
|
||||
# Neutral - slight increase
|
||||
existing_pattern.confidence_score = min(
|
||||
100.0,
|
||||
existing_pattern.confidence_score + 0.3
|
||||
)
|
||||
|
||||
existing_pattern.last_used_at = datetime.utcnow()
|
||||
else:
|
||||
# Create new pattern - learn from all conversations
|
||||
initial_confidence = 60.0 if conversation.is_helpful is True else 50.0
|
||||
if conversation.is_helpful is False:
|
||||
initial_confidence = 30.0 # Lower confidence for unhelpful patterns
|
||||
|
||||
pattern = AILearnedPattern(
|
||||
pattern_keywords=json.dumps(keywords),
|
||||
query_pattern=conversation.user_query[:200],
|
||||
intent=conversation.intent or "general_query",
|
||||
response_template=conversation.ai_response[:500],
|
||||
context_keys=conversation.context_used,
|
||||
confidence_score=initial_confidence,
|
||||
usage_count=1,
|
||||
success_count=1 if conversation.is_helpful is not False else 0,
|
||||
source_conversation_id=conversation.id,
|
||||
user_role=conversation.user_role,
|
||||
last_used_at=datetime.utcnow()
|
||||
)
|
||||
self.db.add(pattern)
|
||||
|
||||
# Automatically extract knowledge from every conversation
|
||||
# This enables the AI to build its knowledge base autonomously
|
||||
self._auto_extract_knowledge(conversation)
|
||||
|
||||
self.db.commit()
|
||||
except Exception as e:
|
||||
logger.error(f"Error learning from conversation: {str(e)}", exc_info=True)
|
||||
self.db.rollback()
|
||||
|
||||
def _learn_from_feedback(self, conversation: AIConversation, feedback: AIConversationFeedback):
|
||||
"""Learn from user feedback and corrections"""
|
||||
try:
|
||||
# If user provided correction, create knowledge entry
|
||||
if feedback.correction:
|
||||
knowledge = AIKnowledgeEntry(
|
||||
topic=self._extract_topic(conversation.user_query),
|
||||
question=conversation.user_query,
|
||||
answer=feedback.correction,
|
||||
keywords=self._extract_keywords(conversation.user_query),
|
||||
related_intent=conversation.intent,
|
||||
source='user_feedback',
|
||||
confidence=75.0,
|
||||
user_role=conversation.user_role,
|
||||
created_by_user_id=feedback.user_id
|
||||
)
|
||||
self.db.add(knowledge)
|
||||
|
||||
# Update pattern confidence based on feedback
|
||||
patterns = self.db.query(AILearnedPattern).filter(
|
||||
AILearnedPattern.source_conversation_id == conversation.id
|
||||
).all()
|
||||
|
||||
for pattern in patterns:
|
||||
if feedback.is_helpful:
|
||||
pattern.confidence_score = min(100.0, pattern.confidence_score + 2.0)
|
||||
pattern.success_count += 1
|
||||
elif feedback.is_helpful == False:
|
||||
pattern.confidence_score = max(0.0, pattern.confidence_score - 5.0)
|
||||
|
||||
if feedback.is_correct == False:
|
||||
pattern.is_active = False # Deactivate incorrect patterns
|
||||
|
||||
self.db.commit()
|
||||
except Exception as e:
|
||||
logger.error(f"Error learning from feedback: {str(e)}", exc_info=True)
|
||||
self.db.rollback()
|
||||
|
||||
def _extract_keywords(self, text: str) -> List[str]:
|
||||
"""Extract keywords from text"""
|
||||
# Remove common stop words
|
||||
stop_words = {
|
||||
'the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for',
|
||||
'of', 'with', 'by', 'from', 'as', 'is', 'are', 'was', 'were', 'be',
|
||||
'been', 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would',
|
||||
'should', 'could', 'may', 'might', 'can', 'this', 'that', 'these',
|
||||
'those', 'i', 'you', 'he', 'she', 'it', 'we', 'they', 'me', 'him',
|
||||
'her', 'us', 'them', 'my', 'your', 'his', 'her', 'its', 'our', 'their',
|
||||
'what', 'which', 'who', 'whom', 'whose', 'where', 'when', 'why', 'how',
|
||||
'show', 'tell', 'give', 'get', 'find', 'search', 'list', 'all'
|
||||
}
|
||||
|
||||
# Extract words
|
||||
words = re.findall(r'\b[a-z]+\b', text.lower())
|
||||
keywords = [w for w in words if w not in stop_words and len(w) > 2]
|
||||
|
||||
# Count frequency and return top keywords
|
||||
word_freq = Counter(keywords)
|
||||
return [word for word, _ in word_freq.most_common(10)]
|
||||
|
||||
def _extract_topic(self, query: str) -> str:
|
||||
"""Extract topic from query"""
|
||||
# Simple topic extraction - can be enhanced
|
||||
keywords = self._extract_keywords(query)
|
||||
if keywords:
|
||||
return keywords[0].title()
|
||||
return "General"
|
||||
|
||||
def train_from_history(self, days: int = 7) -> Dict[str, Any]:
|
||||
"""Train AI from recent conversation history"""
|
||||
try:
|
||||
cutoff_date = datetime.utcnow() - timedelta(days=days)
|
||||
|
||||
# Get conversations from last N days
|
||||
conversations = self.db.query(AIConversation).filter(
|
||||
AIConversation.created_at >= cutoff_date
|
||||
).all()
|
||||
|
||||
trained_patterns = 0
|
||||
trained_knowledge = 0
|
||||
improved_patterns = 0
|
||||
|
||||
for conv in conversations:
|
||||
# Learn from helpful conversations
|
||||
if conv.is_helpful is True:
|
||||
self._learn_from_conversation(conv)
|
||||
trained_patterns += 1
|
||||
|
||||
# Get feedback for this conversation
|
||||
feedbacks = self.db.query(AIConversationFeedback).filter(
|
||||
AIConversationFeedback.conversation_id == conv.id
|
||||
).all()
|
||||
|
||||
for feedback in feedbacks:
|
||||
if feedback.correction:
|
||||
# Create knowledge entry from correction
|
||||
knowledge = AIKnowledgeEntry(
|
||||
topic=self._extract_topic(conv.user_query),
|
||||
question=conv.user_query,
|
||||
answer=feedback.correction,
|
||||
keywords=self._extract_keywords(conv.user_query),
|
||||
related_intent=conv.intent,
|
||||
source='training',
|
||||
confidence=80.0,
|
||||
user_role=conv.user_role
|
||||
)
|
||||
self.db.add(knowledge)
|
||||
trained_knowledge += 1
|
||||
|
||||
self.db.commit()
|
||||
|
||||
# Update training metrics
|
||||
self._update_training_metrics()
|
||||
|
||||
return {
|
||||
"conversations_analyzed": len(conversations),
|
||||
"patterns_trained": trained_patterns,
|
||||
"knowledge_entries_created": trained_knowledge,
|
||||
"improved_patterns": improved_patterns
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error in training: {str(e)}", exc_info=True)
|
||||
self.db.rollback()
|
||||
return {"error": str(e)}
|
||||
|
||||
def _update_training_metrics(self):
|
||||
"""Update training metrics"""
|
||||
try:
|
||||
today = datetime.utcnow().date()
|
||||
|
||||
# Calculate metrics
|
||||
total_conversations = self.db.query(AIConversation).count()
|
||||
total_patterns = self.db.query(AILearnedPattern).filter(
|
||||
AILearnedPattern.is_active == True
|
||||
).count()
|
||||
total_knowledge = self.db.query(AIKnowledgeEntry).filter(
|
||||
AIKnowledgeEntry.is_verified == True
|
||||
).count()
|
||||
|
||||
# Average response time
|
||||
avg_response_time = self.db.query(
|
||||
func.avg(AIConversation.response_time_ms)
|
||||
).filter(
|
||||
AIConversation.response_time_ms.isnot(None)
|
||||
).scalar()
|
||||
|
||||
# Average rating
|
||||
avg_rating = self.db.query(
|
||||
func.avg(AIConversationFeedback.rating)
|
||||
).filter(
|
||||
AIConversationFeedback.rating.isnot(None)
|
||||
).scalar()
|
||||
|
||||
# Helpful rate
|
||||
total_with_feedback = self.db.query(AIConversation).filter(
|
||||
AIConversation.is_helpful.isnot(None)
|
||||
).count()
|
||||
helpful_count = self.db.query(AIConversation).filter(
|
||||
AIConversation.is_helpful == True
|
||||
).count()
|
||||
helpful_rate = (helpful_count / total_with_feedback * 100) if total_with_feedback > 0 else None
|
||||
|
||||
# Create or update metrics
|
||||
metrics = self.db.query(AITrainingMetrics).filter(
|
||||
func.date(AITrainingMetrics.metric_date) == today
|
||||
).first()
|
||||
|
||||
if not metrics:
|
||||
metrics = AITrainingMetrics(metric_date=datetime.utcnow())
|
||||
self.db.add(metrics)
|
||||
|
||||
metrics.total_conversations = total_conversations
|
||||
metrics.total_patterns_learned = total_patterns
|
||||
metrics.total_knowledge_entries = total_knowledge
|
||||
metrics.average_response_time_ms = int(avg_response_time) if avg_response_time else None
|
||||
metrics.average_rating = float(avg_rating) if avg_rating else None
|
||||
metrics.helpful_rate = float(helpful_rate) if helpful_rate else None
|
||||
|
||||
self.db.commit()
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating training metrics: {str(e)}", exc_info=True)
|
||||
self.db.rollback()
|
||||
|
||||
def auto_train_from_all_conversations(self) -> Dict[str, Any]:
|
||||
"""Automatically train from all conversations - self-learning method"""
|
||||
try:
|
||||
logger.info("Starting automatic self-training from all conversations...")
|
||||
|
||||
# Get all conversations that haven't been processed for learning
|
||||
# Process conversations from last 30 days by default
|
||||
cutoff_date = datetime.utcnow() - timedelta(days=30)
|
||||
conversations = self.db.query(AIConversation).filter(
|
||||
AIConversation.created_at >= cutoff_date
|
||||
).order_by(desc(AIConversation.created_at)).all()
|
||||
|
||||
trained_patterns = 0
|
||||
created_knowledge = 0
|
||||
improved_patterns = 0
|
||||
analyzed_conversations = 0
|
||||
|
||||
for conv in conversations:
|
||||
analyzed_conversations += 1
|
||||
|
||||
# Always learn from conversations (even without explicit feedback)
|
||||
# The AI learns from patterns in successful responses
|
||||
if conv.is_helpful is not False: # Learn from helpful or neutral conversations
|
||||
# Extract and save knowledge automatically
|
||||
self._auto_extract_knowledge(conv)
|
||||
created_knowledge += 1
|
||||
|
||||
# Learn patterns from all conversations
|
||||
self._learn_from_conversation(conv)
|
||||
trained_patterns += 1
|
||||
|
||||
# Auto-optimize patterns based on usage and success
|
||||
improved_patterns = self._auto_optimize_patterns()
|
||||
|
||||
# Auto-merge similar patterns
|
||||
merged = self._auto_merge_similar_patterns()
|
||||
|
||||
# Update metrics
|
||||
self._update_training_metrics()
|
||||
|
||||
logger.info(f"Auto-training completed: {analyzed_conversations} conversations analyzed, "
|
||||
f"{trained_patterns} patterns trained, {created_knowledge} knowledge entries created, "
|
||||
f"{improved_patterns} patterns improved, {merged} patterns merged")
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"conversations_analyzed": analyzed_conversations,
|
||||
"patterns_trained": trained_patterns,
|
||||
"knowledge_entries_created": created_knowledge,
|
||||
"patterns_improved": improved_patterns,
|
||||
"patterns_merged": merged,
|
||||
"timestamp": datetime.utcnow().isoformat()
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error in auto-training: {str(e)}", exc_info=True)
|
||||
self.db.rollback()
|
||||
return {"status": "error", "error": str(e)}
|
||||
|
||||
def _auto_extract_knowledge(self, conversation: AIConversation):
|
||||
"""Automatically extract knowledge from conversations"""
|
||||
try:
|
||||
# Check if similar knowledge already exists
|
||||
keywords = self._extract_keywords(conversation.user_query)
|
||||
existing = self.db.query(AIKnowledgeEntry).filter(
|
||||
and_(
|
||||
AIKnowledgeEntry.question.ilike(f"%{conversation.user_query[:50]}%"),
|
||||
or_(
|
||||
AIKnowledgeEntry.user_role == conversation.user_role,
|
||||
AIKnowledgeEntry.user_role.is_(None)
|
||||
)
|
||||
)
|
||||
).first()
|
||||
|
||||
if not existing:
|
||||
# Create new knowledge entry (only for helpful or neutral conversations)
|
||||
# Don't learn from explicitly unhelpful responses
|
||||
if conversation.is_helpful is not False:
|
||||
knowledge = AIKnowledgeEntry(
|
||||
topic=self._extract_topic(conversation.user_query),
|
||||
question=conversation.user_query,
|
||||
answer=conversation.ai_response,
|
||||
keywords=keywords,
|
||||
related_intent=conversation.intent,
|
||||
source='auto_learned',
|
||||
confidence=60.0 if conversation.is_helpful is True else 50.0,
|
||||
user_role=conversation.user_role,
|
||||
is_verified=False # Auto-learned entries need verification
|
||||
)
|
||||
self.db.add(knowledge)
|
||||
else:
|
||||
# Update existing knowledge if this response is better
|
||||
# Increase confidence if response is marked as helpful
|
||||
existing.usage_count += 1
|
||||
if conversation.is_helpful is True:
|
||||
existing.confidence = min(100.0, existing.confidence + 5.0)
|
||||
existing.success_count += 1
|
||||
# Update answer if confidence is high enough
|
||||
if existing.confidence >= 80.0:
|
||||
existing.answer = conversation.ai_response
|
||||
existing.updated_at = datetime.utcnow()
|
||||
elif conversation.is_helpful is False:
|
||||
existing.confidence = max(20.0, existing.confidence - 3.0)
|
||||
else:
|
||||
# Neutral - slight increase
|
||||
existing.confidence = min(100.0, existing.confidence + 1.0)
|
||||
existing.success_count += 1
|
||||
|
||||
existing.last_used_at = datetime.utcnow()
|
||||
|
||||
# Don't commit here - let the caller commit
|
||||
except Exception as e:
|
||||
logger.error(f"Error auto-extracting knowledge: {str(e)}", exc_info=True)
|
||||
# Don't rollback here - let the caller handle it
|
||||
|
||||
def _auto_optimize_patterns(self) -> int:
|
||||
"""Automatically optimize patterns based on performance"""
|
||||
try:
|
||||
improved = 0
|
||||
|
||||
# Get all active patterns
|
||||
patterns = self.db.query(AILearnedPattern).filter(
|
||||
AILearnedPattern.is_active == True
|
||||
).all()
|
||||
|
||||
for pattern in patterns:
|
||||
# Calculate success rate
|
||||
if pattern.usage_count > 0:
|
||||
success_rate = (pattern.success_count / pattern.usage_count) * 100
|
||||
|
||||
# Adjust confidence based on success rate
|
||||
if success_rate >= 80:
|
||||
# High success rate - increase confidence
|
||||
pattern.confidence_score = min(100.0, pattern.confidence_score + 1.0)
|
||||
improved += 1
|
||||
elif success_rate < 50 and pattern.usage_count >= 5:
|
||||
# Low success rate - decrease confidence or deactivate
|
||||
pattern.confidence_score = max(0.0, pattern.confidence_score - 2.0)
|
||||
if pattern.confidence_score < 30:
|
||||
pattern.is_active = False
|
||||
logger.info(f"Deactivated low-performing pattern {pattern.id}")
|
||||
improved += 1
|
||||
|
||||
# Update last used timestamp
|
||||
pattern.updated_at = datetime.utcnow()
|
||||
|
||||
self.db.commit()
|
||||
return improved
|
||||
except Exception as e:
|
||||
logger.error(f"Error optimizing patterns: {str(e)}", exc_info=True)
|
||||
self.db.rollback()
|
||||
return 0
|
||||
|
||||
def _auto_merge_similar_patterns(self) -> int:
|
||||
"""Automatically merge similar patterns to reduce redundancy"""
|
||||
try:
|
||||
merged = 0
|
||||
|
||||
# Get patterns with same intent and role
|
||||
patterns_by_intent = {}
|
||||
all_patterns = self.db.query(AILearnedPattern).filter(
|
||||
AILearnedPattern.is_active == True
|
||||
).all()
|
||||
|
||||
for pattern in all_patterns:
|
||||
key = (pattern.intent, pattern.user_role)
|
||||
if key not in patterns_by_intent:
|
||||
patterns_by_intent[key] = []
|
||||
patterns_by_intent[key].append(pattern)
|
||||
|
||||
# Merge similar patterns within each intent group
|
||||
for (intent, role), patterns in patterns_by_intent.items():
|
||||
if len(patterns) < 2:
|
||||
continue
|
||||
|
||||
# Sort by confidence and usage
|
||||
patterns.sort(key=lambda p: (p.confidence_score, p.usage_count), reverse=True)
|
||||
|
||||
# Keep the best pattern and merge others into it
|
||||
best_pattern = patterns[0]
|
||||
for pattern in patterns[1:]:
|
||||
# Check if patterns are similar (same keywords or similar query pattern)
|
||||
best_keywords = set(json.loads(best_pattern.pattern_keywords) if best_pattern.pattern_keywords else [])
|
||||
pattern_keywords = set(json.loads(pattern.pattern_keywords) if pattern.pattern_keywords else [])
|
||||
|
||||
# If 70% keywords overlap, merge them
|
||||
if best_keywords and pattern_keywords:
|
||||
overlap = len(best_keywords & pattern_keywords) / len(best_keywords | pattern_keywords)
|
||||
if overlap >= 0.7:
|
||||
# Merge: combine usage counts and update confidence
|
||||
best_pattern.usage_count += pattern.usage_count
|
||||
best_pattern.success_count += pattern.success_count
|
||||
best_pattern.confidence_score = (
|
||||
(best_pattern.confidence_score * best_pattern.usage_count +
|
||||
pattern.confidence_score * pattern.usage_count) /
|
||||
(best_pattern.usage_count + pattern.usage_count)
|
||||
)
|
||||
|
||||
# Deactivate the merged pattern
|
||||
pattern.is_active = False
|
||||
merged += 1
|
||||
logger.info(f"Merged pattern {pattern.id} into {best_pattern.id}")
|
||||
|
||||
self.db.commit()
|
||||
return merged
|
||||
except Exception as e:
|
||||
logger.error(f"Error merging patterns: {str(e)}", exc_info=True)
|
||||
self.db.rollback()
|
||||
return 0
|
||||
|
||||
def auto_analyze_and_improve(self) -> Dict[str, Any]:
|
||||
"""Automatically analyze performance and improve - self-improvement method"""
|
||||
try:
|
||||
logger.info("Starting automatic self-analysis and improvement...")
|
||||
|
||||
# Analyze conversation quality
|
||||
recent_conversations = self.db.query(AIConversation).filter(
|
||||
AIConversation.created_at >= datetime.utcnow() - timedelta(days=7)
|
||||
).all()
|
||||
|
||||
# Find patterns in successful vs unsuccessful conversations
|
||||
successful_queries = []
|
||||
unsuccessful_queries = []
|
||||
|
||||
for conv in recent_conversations:
|
||||
if conv.is_helpful is True:
|
||||
successful_queries.append(conv.user_query)
|
||||
elif conv.is_helpful is False:
|
||||
unsuccessful_queries.append(conv.user_query)
|
||||
|
||||
# Extract common patterns from successful queries
|
||||
if successful_queries:
|
||||
common_success_keywords = self._find_common_keywords(successful_queries)
|
||||
logger.info(f"Common keywords in successful queries: {common_success_keywords[:10]}")
|
||||
|
||||
# Identify problematic patterns
|
||||
if unsuccessful_queries:
|
||||
common_failure_keywords = self._find_common_keywords(unsuccessful_queries)
|
||||
logger.info(f"Common keywords in unsuccessful queries: {common_failure_keywords[:10]}")
|
||||
|
||||
# Improve knowledge entries based on usage
|
||||
improved_knowledge = self._improve_knowledge_entries()
|
||||
|
||||
# Clean up old inactive patterns
|
||||
cleaned = self._cleanup_old_patterns()
|
||||
|
||||
# Update metrics
|
||||
self._update_training_metrics()
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"knowledge_entries_improved": improved_knowledge,
|
||||
"old_patterns_cleaned": cleaned,
|
||||
"timestamp": datetime.utcnow().isoformat()
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error in auto-analysis: {str(e)}", exc_info=True)
|
||||
return {"status": "error", "error": str(e)}
|
||||
|
||||
def _find_common_keywords(self, queries: List[str]) -> List[str]:
|
||||
"""Find common keywords across multiple queries"""
|
||||
all_keywords = []
|
||||
for query in queries:
|
||||
keywords = self._extract_keywords(query)
|
||||
all_keywords.extend(keywords)
|
||||
|
||||
# Count frequency
|
||||
keyword_freq = Counter(all_keywords)
|
||||
return [word for word, _ in keyword_freq.most_common(20)]
|
||||
|
||||
def _improve_knowledge_entries(self) -> int:
|
||||
"""Improve knowledge entries based on usage statistics"""
|
||||
try:
|
||||
improved = 0
|
||||
|
||||
# Get knowledge entries with usage data
|
||||
entries = self.db.query(AIKnowledgeEntry).filter(
|
||||
AIKnowledgeEntry.usage_count > 0
|
||||
).all()
|
||||
|
||||
for entry in entries:
|
||||
# Calculate success rate
|
||||
if entry.usage_count > 0:
|
||||
success_rate = (entry.success_count / entry.usage_count) * 100
|
||||
|
||||
# Auto-verify entries with high success rate
|
||||
if success_rate >= 85 and entry.usage_count >= 5 and not entry.is_verified:
|
||||
entry.is_verified = True
|
||||
entry.confidence = min(100.0, entry.confidence + 10.0)
|
||||
improved += 1
|
||||
logger.info(f"Auto-verified knowledge entry {entry.id} (success rate: {success_rate:.1f}%)")
|
||||
|
||||
# Increase confidence for successful entries
|
||||
elif success_rate >= 70:
|
||||
entry.confidence = min(100.0, entry.confidence + 2.0)
|
||||
improved += 1
|
||||
|
||||
# Decrease confidence for unsuccessful entries
|
||||
elif success_rate < 40 and entry.usage_count >= 3:
|
||||
entry.confidence = max(20.0, entry.confidence - 5.0)
|
||||
improved += 1
|
||||
|
||||
self.db.commit()
|
||||
return improved
|
||||
except Exception as e:
|
||||
logger.error(f"Error improving knowledge entries: {str(e)}", exc_info=True)
|
||||
self.db.rollback()
|
||||
return 0
|
||||
|
||||
def _cleanup_old_patterns(self) -> int:
|
||||
"""Clean up old inactive or low-performing patterns"""
|
||||
try:
|
||||
cutoff_date = datetime.utcnow() - timedelta(days=90)
|
||||
|
||||
# Find patterns that haven't been used in 90 days and have low confidence
|
||||
old_patterns = self.db.query(AILearnedPattern).filter(
|
||||
and_(
|
||||
or_(
|
||||
AILearnedPattern.last_used_at < cutoff_date,
|
||||
AILearnedPattern.last_used_at.is_(None)
|
||||
),
|
||||
AILearnedPattern.confidence_score < 40.0,
|
||||
AILearnedPattern.is_active == True
|
||||
)
|
||||
).all()
|
||||
|
||||
cleaned = 0
|
||||
for pattern in old_patterns:
|
||||
pattern.is_active = False
|
||||
cleaned += 1
|
||||
|
||||
self.db.commit()
|
||||
logger.info(f"Cleaned up {cleaned} old inactive patterns")
|
||||
return cleaned
|
||||
except Exception as e:
|
||||
logger.error(f"Error cleaning up patterns: {str(e)}", exc_info=True)
|
||||
self.db.rollback()
|
||||
return 0
|
||||
|
||||
383
Backend/src/ai/services/ai_role_access_service.py
Normal file
383
Backend/src/ai/services/ai_role_access_service.py
Normal file
@@ -0,0 +1,383 @@
|
||||
"""
|
||||
Enterprise Role-Based Access Control Service for AI Assistant
|
||||
Filters and controls data access based on user roles
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Optional, Any
|
||||
from datetime import datetime, timedelta
|
||||
from sqlalchemy.orm import Session, joinedload
|
||||
from sqlalchemy import and_, or_, func
|
||||
import logging
|
||||
|
||||
from ...auth.models.user import User
|
||||
from ...auth.models.role import Role
|
||||
from ...bookings.models.booking import Booking, BookingStatus
|
||||
from ...payments.models.invoice import Invoice
|
||||
from ...payments.models.payment import Payment, PaymentStatus
|
||||
from ...rooms.models.room import Room, RoomStatus
|
||||
from ...shared.config.logging_config import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class AIRoleAccessService:
|
||||
"""Role-based data access control for AI Assistant"""
|
||||
|
||||
def __init__(self, db: Session, current_user: User):
|
||||
self.db = db
|
||||
self.current_user = current_user
|
||||
|
||||
# Load user role - try from relationship first, then query
|
||||
self.user_role = None
|
||||
if hasattr(current_user, 'role') and current_user.role:
|
||||
self.user_role = current_user.role
|
||||
|
||||
if not self.user_role:
|
||||
self.user_role = db.query(Role).filter(Role.id == current_user.role_id).first()
|
||||
|
||||
if not self.user_role:
|
||||
# Fallback to customer role if not found
|
||||
logger.warning(f"Role not found for user {current_user.id}, defaulting to customer")
|
||||
self.user_role = db.query(Role).filter(Role.name == 'customer').first()
|
||||
|
||||
self.role_name = self.user_role.name.lower() if self.user_role else "customer"
|
||||
|
||||
def get_user_bookings(self, limit: int = 10) -> List[Dict[str, Any]]:
|
||||
"""Get bookings based on role permissions"""
|
||||
if self.role_name in ["admin", "staff"]:
|
||||
# Admin and staff can see all bookings
|
||||
bookings = self.db.query(Booking).options(
|
||||
joinedload(Booking.user),
|
||||
joinedload(Booking.room).joinedload(Room.room_type)
|
||||
).order_by(Booking.created_at.desc()).limit(limit).all()
|
||||
elif self.role_name == "accountant":
|
||||
# Accountant can see all bookings (for invoicing purposes)
|
||||
bookings = self.db.query(Booking).options(
|
||||
joinedload(Booking.user),
|
||||
joinedload(Booking.room).joinedload(Room.room_type)
|
||||
).order_by(Booking.created_at.desc()).limit(limit).all()
|
||||
else:
|
||||
# Customers can only see their own bookings
|
||||
bookings = self.db.query(Booking).options(
|
||||
joinedload(Booking.user),
|
||||
joinedload(Booking.room).joinedload(Room.room_type)
|
||||
).filter(Booking.user_id == self.current_user.id).order_by(
|
||||
Booking.created_at.desc()
|
||||
).limit(limit).all()
|
||||
|
||||
result = []
|
||||
for booking in bookings:
|
||||
booking_dict = {
|
||||
"booking_number": booking.booking_number,
|
||||
"status": booking.status.value if hasattr(booking.status, 'value') else str(booking.status),
|
||||
"check_in": booking.check_in_date.isoformat(),
|
||||
"check_out": booking.check_out_date.isoformat(),
|
||||
"num_guests": booking.num_guests,
|
||||
"total_price": float(booking.total_price) if booking.total_price else 0,
|
||||
}
|
||||
|
||||
# Add user info based on role
|
||||
if self.role_name in ["admin", "staff", "accountant"]:
|
||||
booking_dict["guest_name"] = booking.user.full_name if booking.user else "Unknown"
|
||||
booking_dict["guest_email"] = booking.user.email if booking.user else None
|
||||
else:
|
||||
booking_dict["guest_name"] = "Your booking"
|
||||
|
||||
# Add room info
|
||||
if booking.room:
|
||||
booking_dict["room_number"] = booking.room.room_number
|
||||
if booking.room.room_type:
|
||||
booking_dict["room_type"] = booking.room.room_type.name
|
||||
|
||||
result.append(booking_dict)
|
||||
|
||||
return result
|
||||
|
||||
def get_user_invoices(self, limit: int = 10) -> List[Dict[str, Any]]:
|
||||
"""Get invoices based on role permissions"""
|
||||
if self.role_name in ["admin", "accountant"]:
|
||||
# Admin and accountant can see all invoices
|
||||
invoices = self.db.query(Invoice).options(
|
||||
joinedload(Invoice.user),
|
||||
joinedload(Invoice.booking)
|
||||
).order_by(Invoice.created_at.desc()).limit(limit).all()
|
||||
elif self.role_name == "staff":
|
||||
# Staff can view invoices but not edit
|
||||
invoices = self.db.query(Invoice).options(
|
||||
joinedload(Invoice.user),
|
||||
joinedload(Invoice.booking)
|
||||
).order_by(Invoice.created_at.desc()).limit(limit).all()
|
||||
else:
|
||||
# Customers can only see their own invoices
|
||||
invoices = self.db.query(Invoice).options(
|
||||
joinedload(Invoice.user),
|
||||
joinedload(Invoice.booking)
|
||||
).filter(Invoice.user_id == self.current_user.id).order_by(
|
||||
Invoice.created_at.desc()
|
||||
).limit(limit).all()
|
||||
|
||||
result = []
|
||||
for invoice in invoices:
|
||||
invoice_dict = {
|
||||
"invoice_number": invoice.invoice_number,
|
||||
"status": invoice.status.value if hasattr(invoice.status, 'value') else str(invoice.status),
|
||||
"total_amount": float(invoice.total_amount) if invoice.total_amount else 0,
|
||||
"balance_due": float(invoice.balance_due) if invoice.balance_due else 0,
|
||||
"due_date": invoice.due_date.isoformat() if invoice.due_date else None,
|
||||
"paid_date": invoice.paid_date.isoformat() if invoice.paid_date else None,
|
||||
}
|
||||
|
||||
# Add user info based on role
|
||||
if self.role_name in ["admin", "staff", "accountant"]:
|
||||
invoice_dict["customer_name"] = invoice.customer_name if hasattr(invoice, 'customer_name') else (
|
||||
invoice.user.full_name if invoice.user else "Unknown"
|
||||
)
|
||||
invoice_dict["customer_email"] = invoice.customer_email if hasattr(invoice, 'customer_email') else (
|
||||
invoice.user.email if invoice.user else None
|
||||
)
|
||||
else:
|
||||
invoice_dict["customer_name"] = "Your invoice"
|
||||
|
||||
if invoice.booking:
|
||||
invoice_dict["booking_number"] = invoice.booking.booking_number
|
||||
|
||||
result.append(invoice_dict)
|
||||
|
||||
return result
|
||||
|
||||
def get_user_payments(self, limit: Optional[int] = 10) -> List[Dict[str, Any]]:
|
||||
"""Get payments based on role permissions"""
|
||||
if self.role_name in ["admin", "accountant"]:
|
||||
# Admin and accountant can see all payments
|
||||
query = self.db.query(Payment).options(
|
||||
joinedload(Payment.booking).joinedload(Booking.user)
|
||||
).order_by(Payment.created_at.desc())
|
||||
if limit:
|
||||
payments = query.limit(limit).all()
|
||||
else:
|
||||
payments = query.all()
|
||||
elif self.role_name == "staff":
|
||||
# Staff can see payment status but limited details
|
||||
query = self.db.query(Payment).options(
|
||||
joinedload(Payment.booking).joinedload(Booking.user)
|
||||
).order_by(Payment.created_at.desc())
|
||||
if limit:
|
||||
payments = query.limit(limit).all()
|
||||
else:
|
||||
payments = query.all()
|
||||
else:
|
||||
# Customers can only see their own payments
|
||||
query = self.db.query(Payment).options(
|
||||
joinedload(Payment.booking)
|
||||
).join(Booking).filter(Booking.user_id == self.current_user.id).order_by(
|
||||
Payment.created_at.desc()
|
||||
)
|
||||
if limit:
|
||||
payments = query.limit(limit).all()
|
||||
else:
|
||||
payments = query.all()
|
||||
|
||||
result = []
|
||||
for payment in payments:
|
||||
payment_type = payment.payment_type.value if hasattr(payment.payment_type, 'value') else str(payment.payment_type)
|
||||
|
||||
# Format payment type with deposit percentage if available
|
||||
if payment_type == 'deposit' and payment.deposit_percentage:
|
||||
payment_type_display = f"Deposit ({payment.deposit_percentage}%)"
|
||||
elif payment_type == 'deposit':
|
||||
payment_type_display = "Deposit (20%)" # Default
|
||||
elif payment_type == 'full':
|
||||
payment_type_display = "Full Payment"
|
||||
elif payment_type == 'remaining':
|
||||
payment_type_display = "Remaining"
|
||||
else:
|
||||
payment_type_display = payment_type
|
||||
|
||||
payment_dict = {
|
||||
"id": payment.id,
|
||||
"amount": float(payment.amount) if payment.amount else 0,
|
||||
"payment_method": payment.payment_method.value if hasattr(payment.payment_method, 'value') else str(payment.payment_method),
|
||||
"payment_type": payment_type,
|
||||
"payment_type_display": payment_type_display,
|
||||
"payment_status": payment.payment_status.value if hasattr(payment.payment_status, 'value') else str(payment.payment_status),
|
||||
"payment_date": payment.payment_date.isoformat() if payment.payment_date else None,
|
||||
"transaction_id": payment.transaction_id or f"PAY-{payment.id}",
|
||||
"deposit_percentage": payment.deposit_percentage,
|
||||
}
|
||||
|
||||
if payment.booking:
|
||||
payment_dict["booking_number"] = payment.booking.booking_number
|
||||
|
||||
# Add user info based on role
|
||||
if self.role_name in ["admin", "staff", "accountant"] and payment.booking.user:
|
||||
payment_dict["customer_name"] = payment.booking.user.full_name
|
||||
payment_dict["customer_email"] = payment.booking.user.email
|
||||
else:
|
||||
payment_dict["customer_name"] = "Your payment"
|
||||
|
||||
result.append(payment_dict)
|
||||
|
||||
return result
|
||||
|
||||
def get_all_payments(self) -> List[Dict[str, Any]]:
|
||||
"""Get all payments without limit - for admin and accountant only"""
|
||||
if self.role_name not in ["admin", "accountant"]:
|
||||
return []
|
||||
return self.get_user_payments(limit=None)
|
||||
|
||||
def get_room_status_summary(self) -> Dict[str, Any]:
|
||||
"""Get room status summary based on role permissions"""
|
||||
total_rooms = self.db.query(Room).count()
|
||||
available_rooms = self.db.query(Room).filter(Room.status == RoomStatus.available).count()
|
||||
occupied_rooms = self.db.query(Room).filter(Room.status == RoomStatus.occupied).count()
|
||||
|
||||
result = {
|
||||
"total_rooms": total_rooms,
|
||||
"available": available_rooms,
|
||||
"occupied": occupied_rooms,
|
||||
}
|
||||
|
||||
# Additional details for admin/staff
|
||||
if self.role_name in ["admin", "staff"]:
|
||||
maintenance_rooms = self.db.query(Room).filter(Room.status == RoomStatus.maintenance).count()
|
||||
cleaning_rooms = self.db.query(Room).filter(Room.status == RoomStatus.cleaning).count()
|
||||
|
||||
today = datetime.utcnow().date()
|
||||
rooms_with_bookings = self.db.query(Booking).filter(
|
||||
and_(
|
||||
Booking.status != BookingStatus.cancelled,
|
||||
Booking.check_in_date <= today,
|
||||
Booking.check_out_date > today
|
||||
)
|
||||
).count()
|
||||
|
||||
result.update({
|
||||
"maintenance": maintenance_rooms,
|
||||
"cleaning": cleaning_rooms,
|
||||
"rooms_with_active_bookings": rooms_with_bookings,
|
||||
"occupancy_rate": round((occupied_rooms / total_rooms * 100) if total_rooms > 0 else 0, 2)
|
||||
})
|
||||
else:
|
||||
# Customers get simplified room availability info
|
||||
result["occupancy_rate"] = round((occupied_rooms / total_rooms * 100) if total_rooms > 0 else 0, 2)
|
||||
|
||||
return result
|
||||
|
||||
def get_payment_summary(self, days: int = 30) -> Optional[Dict[str, Any]]:
|
||||
"""Get payment summary - only for admin, accountant, and staff"""
|
||||
if self.role_name not in ["admin", "accountant", "staff"]:
|
||||
return None # Customers cannot access payment summaries
|
||||
|
||||
today = datetime.utcnow().date()
|
||||
start_date = today - timedelta(days=days)
|
||||
|
||||
# Total payments in period
|
||||
total_payments = self.db.query(func.sum(Payment.amount)).filter(
|
||||
and_(
|
||||
Payment.payment_status == PaymentStatus.completed,
|
||||
func.date(Payment.payment_date) >= start_date,
|
||||
func.date(Payment.payment_date) <= today
|
||||
)
|
||||
).scalar() or 0
|
||||
|
||||
# Pending payments
|
||||
pending_payments = self.db.query(func.sum(Payment.amount)).filter(
|
||||
Payment.payment_status == PaymentStatus.pending
|
||||
).scalar() or 0
|
||||
|
||||
# Payment count
|
||||
completed_count = self.db.query(Payment).filter(
|
||||
and_(
|
||||
Payment.payment_status == PaymentStatus.completed,
|
||||
func.date(Payment.payment_date) >= start_date
|
||||
)
|
||||
).count()
|
||||
|
||||
result = {
|
||||
"total_revenue": float(total_payments),
|
||||
"pending_amount": float(pending_payments),
|
||||
"completed_payments": completed_count,
|
||||
"period_days": days
|
||||
}
|
||||
|
||||
# Additional details for admin and accountant
|
||||
if self.role_name in ["admin", "accountant"]:
|
||||
failed_count = self.db.query(Payment).filter(
|
||||
Payment.payment_status == PaymentStatus.failed
|
||||
).count()
|
||||
result["failed_payments"] = failed_count
|
||||
|
||||
return result
|
||||
|
||||
def get_invoice_summary(self) -> Optional[Dict[str, Any]]:
|
||||
"""Get invoice summary - only for admin, accountant, and staff"""
|
||||
if self.role_name not in ["admin", "accountant", "staff"]:
|
||||
return None # Customers cannot access invoice summaries
|
||||
|
||||
total_invoices = self.db.query(Invoice).count()
|
||||
|
||||
# Group by status
|
||||
status_counts = {}
|
||||
invoices = self.db.query(Invoice).all()
|
||||
for invoice in invoices:
|
||||
status = invoice.status.value if hasattr(invoice.status, 'value') else str(invoice.status)
|
||||
status_counts[status] = status_counts.get(status, 0) + 1
|
||||
|
||||
# Overdue invoices
|
||||
today = datetime.utcnow().date()
|
||||
overdue_count = self.db.query(Invoice).filter(
|
||||
and_(
|
||||
Invoice.due_date < today,
|
||||
Invoice.status != 'paid'
|
||||
)
|
||||
).count()
|
||||
|
||||
result = {
|
||||
"total_invoices": total_invoices,
|
||||
"status_breakdown": status_counts,
|
||||
"overdue_invoices": overdue_count
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
def can_access_feature(self, feature: str) -> bool:
|
||||
"""Check if current user can access a feature"""
|
||||
if self.role_name == "admin":
|
||||
return True
|
||||
|
||||
feature_lower = feature.lower()
|
||||
|
||||
# Define feature access rules
|
||||
if feature_lower in ["payment_summary", "invoice_summary", "financial_reports"]:
|
||||
return self.role_name in ["admin", "accountant", "staff"]
|
||||
|
||||
if feature_lower in ["all_bookings", "all_users", "system_settings"]:
|
||||
return self.role_name == "admin"
|
||||
|
||||
if feature_lower in ["operational_reports", "room_management", "maintenance"]:
|
||||
return self.role_name in ["admin", "staff"]
|
||||
|
||||
# Default: allow if it's user's own data
|
||||
return True
|
||||
|
||||
def filter_sensitive_data(self, data: Dict[str, Any], data_type: str) -> Dict[str, Any]:
|
||||
"""Filter sensitive data based on role"""
|
||||
if self.role_name == "admin":
|
||||
return data # Admin sees everything
|
||||
|
||||
filtered = data.copy()
|
||||
|
||||
# Remove sensitive financial data for staff
|
||||
if self.role_name == "staff" and data_type in ["payment", "invoice"]:
|
||||
if "transaction_id" in filtered:
|
||||
filtered["transaction_id"] = "***" if filtered.get("transaction_id") else None
|
||||
if "payment_method_details" in filtered:
|
||||
del filtered["payment_method_details"]
|
||||
|
||||
# Remove other users' personal info for non-admin roles
|
||||
if self.role_name != "admin" and data_type in ["booking", "invoice", "payment"]:
|
||||
# Only show own data, not other users' details
|
||||
pass # This is handled at query level
|
||||
|
||||
return filtered
|
||||
|
||||
171
Backend/src/ai/services/ai_training_scheduler.py
Normal file
171
Backend/src/ai/services/ai_training_scheduler.py
Normal file
@@ -0,0 +1,171 @@
|
||||
"""
|
||||
AI Training Scheduler - Automatic background training for AI self-learning
|
||||
Runs periodic training tasks to improve AI performance autonomously
|
||||
"""
|
||||
|
||||
import threading
|
||||
import time
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional
|
||||
from sqlalchemy.orm import Session
|
||||
import logging
|
||||
|
||||
from ...shared.config.database import get_db
|
||||
from ...shared.config.logging_config import get_logger
|
||||
from .ai_learning_service import AILearningService
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class AITrainingScheduler:
|
||||
"""Background scheduler for automatic AI training"""
|
||||
|
||||
def __init__(self):
|
||||
self.running = False
|
||||
self.thread: Optional[threading.Thread] = None
|
||||
self.training_interval_hours = 6 # Train every 6 hours
|
||||
self.analysis_interval_hours = 24 # Analyze every 24 hours
|
||||
self.last_training_time: Optional[datetime] = None
|
||||
self.last_analysis_time: Optional[datetime] = None
|
||||
|
||||
def start(self):
|
||||
"""Start the background training scheduler"""
|
||||
if self.running:
|
||||
logger.warning("AI Training Scheduler is already running")
|
||||
return
|
||||
|
||||
self.running = True
|
||||
self.thread = threading.Thread(target=self._scheduler_loop, daemon=True)
|
||||
self.thread.start()
|
||||
logger.info("AI Training Scheduler started - automatic self-learning enabled")
|
||||
|
||||
def stop(self):
|
||||
"""Stop the background training scheduler"""
|
||||
if not self.running:
|
||||
return
|
||||
|
||||
self.running = False
|
||||
if self.thread:
|
||||
self.thread.join(timeout=5.0)
|
||||
logger.info("AI Training Scheduler stopped")
|
||||
|
||||
def _scheduler_loop(self):
|
||||
"""Main scheduler loop that runs training tasks periodically"""
|
||||
logger.info("AI Training Scheduler loop started")
|
||||
|
||||
# Run initial training after 1 minute (to let app fully start)
|
||||
time.sleep(60)
|
||||
|
||||
while self.running:
|
||||
try:
|
||||
current_time = datetime.utcnow()
|
||||
|
||||
# Check if it's time for training
|
||||
should_train = False
|
||||
if self.last_training_time is None:
|
||||
should_train = True
|
||||
else:
|
||||
time_since_training = current_time - self.last_training_time
|
||||
if time_since_training >= timedelta(hours=self.training_interval_hours):
|
||||
should_train = True
|
||||
|
||||
if should_train:
|
||||
logger.info("Starting scheduled automatic training...")
|
||||
self._run_training()
|
||||
self.last_training_time = current_time
|
||||
|
||||
# Check if it's time for analysis
|
||||
should_analyze = False
|
||||
if self.last_analysis_time is None:
|
||||
should_analyze = True
|
||||
else:
|
||||
time_since_analysis = current_time - self.last_analysis_time
|
||||
if time_since_analysis >= timedelta(hours=self.analysis_interval_hours):
|
||||
should_analyze = True
|
||||
|
||||
if should_analyze:
|
||||
logger.info("Starting scheduled automatic analysis...")
|
||||
self._run_analysis()
|
||||
self.last_analysis_time = current_time
|
||||
|
||||
# Sleep for 1 hour before checking again
|
||||
time.sleep(3600)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in scheduler loop: {str(e)}", exc_info=True)
|
||||
# Sleep for 10 minutes before retrying on error
|
||||
time.sleep(600)
|
||||
|
||||
def _run_training(self):
|
||||
"""Run automatic training from conversations"""
|
||||
try:
|
||||
# Get database session
|
||||
db_gen = get_db()
|
||||
db = next(db_gen)
|
||||
|
||||
try:
|
||||
learning_service = AILearningService(db)
|
||||
result = learning_service.auto_train_from_all_conversations()
|
||||
|
||||
if result.get("status") == "success":
|
||||
logger.info(
|
||||
f"Automatic training completed: "
|
||||
f"{result.get('conversations_analyzed', 0)} conversations analyzed, "
|
||||
f"{result.get('patterns_trained', 0)} patterns trained, "
|
||||
f"{result.get('knowledge_entries_created', 0)} knowledge entries created"
|
||||
)
|
||||
else:
|
||||
logger.warning(f"Training completed with errors: {result.get('error', 'Unknown error')}")
|
||||
finally:
|
||||
db.close()
|
||||
except Exception as e:
|
||||
logger.error(f"Error running automatic training: {str(e)}", exc_info=True)
|
||||
|
||||
def _run_analysis(self):
|
||||
"""Run automatic analysis and improvement"""
|
||||
try:
|
||||
# Get database session
|
||||
db_gen = get_db()
|
||||
db = next(db_gen)
|
||||
|
||||
try:
|
||||
learning_service = AILearningService(db)
|
||||
result = learning_service.auto_analyze_and_improve()
|
||||
|
||||
if result.get("status") == "success":
|
||||
logger.info(
|
||||
f"Automatic analysis completed: "
|
||||
f"{result.get('knowledge_entries_improved', 0)} knowledge entries improved, "
|
||||
f"{result.get('old_patterns_cleaned', 0)} old patterns cleaned"
|
||||
)
|
||||
else:
|
||||
logger.warning(f"Analysis completed with errors: {result.get('error', 'Unknown error')}")
|
||||
finally:
|
||||
db.close()
|
||||
except Exception as e:
|
||||
logger.error(f"Error running automatic analysis: {str(e)}", exc_info=True)
|
||||
|
||||
def trigger_manual_training(self):
|
||||
"""Manually trigger training (for testing or on-demand training)"""
|
||||
logger.info("Manual training triggered")
|
||||
self._run_training()
|
||||
self.last_training_time = datetime.utcnow()
|
||||
|
||||
def trigger_manual_analysis(self):
|
||||
"""Manually trigger analysis (for testing or on-demand analysis)"""
|
||||
logger.info("Manual analysis triggered")
|
||||
self._run_analysis()
|
||||
self.last_analysis_time = datetime.utcnow()
|
||||
|
||||
|
||||
# Global scheduler instance
|
||||
_global_scheduler: Optional[AITrainingScheduler] = None
|
||||
|
||||
|
||||
def get_training_scheduler() -> AITrainingScheduler:
|
||||
"""Get or create the global training scheduler instance"""
|
||||
global _global_scheduler
|
||||
if _global_scheduler is None:
|
||||
_global_scheduler = AITrainingScheduler()
|
||||
return _global_scheduler
|
||||
|
||||
0
Backend/src/analytics/__init__.py
Normal file
0
Backend/src/analytics/__init__.py
Normal file
0
Backend/src/analytics/models/__init__.py
Normal file
0
Backend/src/analytics/models/__init__.py
Normal file
@@ -1,7 +1,7 @@
|
||||
from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, JSON
|
||||
from sqlalchemy.orm import relationship
|
||||
from datetime import datetime
|
||||
from ..config.database import Base
|
||||
from ...shared.config.database import Base
|
||||
|
||||
class AuditLog(Base):
|
||||
__tablename__ = 'audit_logs'
|
||||
0
Backend/src/analytics/routes/__init__.py
Normal file
0
Backend/src/analytics/routes/__init__.py
Normal file
@@ -1,9 +1,9 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import Optional
|
||||
from ..config.database import get_db
|
||||
from ..middleware.auth import authorize_roles, get_current_user
|
||||
from ..models.user import User
|
||||
from ...shared.config.database import get_db
|
||||
from ...security.middleware.auth import authorize_roles, get_current_user
|
||||
from ...auth.models.user import User
|
||||
from ..services.analytics_service import AnalyticsService
|
||||
|
||||
router = APIRouter(prefix='/analytics', tags=['analytics'])
|
||||
@@ -3,9 +3,9 @@ from sqlalchemy.orm import Session
|
||||
from sqlalchemy import desc, or_, func
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
from ..config.database import get_db
|
||||
from ..middleware.auth import get_current_user, authorize_roles
|
||||
from ..models.user import User
|
||||
from ...shared.config.database import get_db
|
||||
from ...security.middleware.auth import get_current_user, authorize_roles
|
||||
from ...auth.models.user import User
|
||||
from ..models.audit_log import AuditLog
|
||||
router = APIRouter(prefix='/audit-logs', tags=['audit-logs'])
|
||||
|
||||
@@ -3,15 +3,15 @@ from sqlalchemy.orm import Session, load_only, joinedload
|
||||
from sqlalchemy import func, and_
|
||||
from typing import Optional
|
||||
from datetime import datetime, timedelta
|
||||
from ..config.database import get_db
|
||||
from ..middleware.auth import get_current_user, authorize_roles
|
||||
from ..models.user import User
|
||||
from ..models.booking import Booking, BookingStatus
|
||||
from ..models.payment import Payment, PaymentStatus
|
||||
from ..models.room import Room, RoomStatus
|
||||
from ..models.service_usage import ServiceUsage
|
||||
from ..models.service import Service
|
||||
from ..utils.response_helpers import success_response
|
||||
from ...shared.config.database import get_db
|
||||
from ...security.middleware.auth import get_current_user, authorize_roles
|
||||
from ...auth.models.user import User
|
||||
from ...bookings.models.booking import Booking, BookingStatus
|
||||
from ...payments.models.payment import Payment, PaymentStatus
|
||||
from ...rooms.models.room import Room, RoomStatus
|
||||
from ...hotel_services.models.service_usage import ServiceUsage
|
||||
from ...hotel_services.models.service import Service
|
||||
from ...shared.utils.response_helpers import success_response
|
||||
router = APIRouter(prefix='/reports', tags=['reports'])
|
||||
|
||||
@router.get('')
|
||||
0
Backend/src/analytics/schemas/__init__.py
Normal file
0
Backend/src/analytics/schemas/__init__.py
Normal file
0
Backend/src/analytics/services/__init__.py
Normal file
0
Backend/src/analytics/services/__init__.py
Normal file
@@ -2,15 +2,15 @@ from sqlalchemy.orm import Session, load_only
|
||||
from sqlalchemy import func, and_, or_, case, extract, distinct
|
||||
from typing import Optional, Dict, List, Any
|
||||
from datetime import datetime, timedelta, date
|
||||
from ..models.booking import Booking, BookingStatus
|
||||
from ..models.payment import Payment, PaymentStatus, PaymentMethod
|
||||
from ..models.room import Room, RoomStatus
|
||||
from ..models.room_type import RoomType
|
||||
from ..models.user import User
|
||||
from ..models.service_usage import ServiceUsage
|
||||
from ..models.service import Service
|
||||
from ..models.review import Review, ReviewStatus
|
||||
from ..models.invoice import Invoice
|
||||
from ...bookings.models.booking import Booking, BookingStatus
|
||||
from ...payments.models.payment import Payment, PaymentStatus, PaymentMethod
|
||||
from ...rooms.models.room import Room, RoomStatus
|
||||
from ...rooms.models.room_type import RoomType
|
||||
from ...auth.models.user import User
|
||||
from ...hotel_services.models.service_usage import ServiceUsage
|
||||
from ...hotel_services.models.service import Service
|
||||
from ...reviews.models.review import Review, ReviewStatus
|
||||
from ...payments.models.invoice import Invoice
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -290,7 +290,7 @@ class AnalyticsService:
|
||||
start_date = end_date - timedelta(days=30)
|
||||
|
||||
# Get staff users
|
||||
from ..models.role import Role
|
||||
from ...auth.models.role import Role
|
||||
staff_users = db.query(User).join(Role, User.role_id == Role.id).filter(
|
||||
or_(Role.name == 'staff', Role.name == 'admin')
|
||||
).all()
|
||||
@@ -2,7 +2,7 @@ from sqlalchemy.orm import Session
|
||||
from typing import Optional, Dict, Any
|
||||
from datetime import datetime
|
||||
from ..models.audit_log import AuditLog
|
||||
from ..config.logging_config import get_logger
|
||||
from ...shared.config.logging_config import get_logger
|
||||
logger = get_logger(__name__)
|
||||
|
||||
class AuditService:
|
||||
0
Backend/src/auth/__init__.py
Normal file
0
Backend/src/auth/__init__.py
Normal file
0
Backend/src/auth/models/__init__.py
Normal file
0
Backend/src/auth/models/__init__.py
Normal file
@@ -1,7 +1,7 @@
|
||||
from sqlalchemy import Column, Integer, String, DateTime, Boolean, ForeignKey
|
||||
from sqlalchemy.orm import relationship
|
||||
from datetime import datetime
|
||||
from ..config.database import Base
|
||||
from ...shared.config.database import Base
|
||||
|
||||
class PasswordResetToken(Base):
|
||||
__tablename__ = 'password_reset_tokens'
|
||||
@@ -1,7 +1,7 @@
|
||||
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey
|
||||
from sqlalchemy.orm import relationship
|
||||
from datetime import datetime
|
||||
from ..config.database import Base
|
||||
from ...shared.config.database import Base
|
||||
|
||||
class RefreshToken(Base):
|
||||
__tablename__ = 'refresh_tokens'
|
||||
@@ -1,7 +1,7 @@
|
||||
from sqlalchemy import Column, Integer, String, DateTime
|
||||
from sqlalchemy.orm import relationship
|
||||
from datetime import datetime
|
||||
from ..config.database import Base
|
||||
from ...shared.config.database import Base
|
||||
|
||||
class Role(Base):
|
||||
__tablename__ = 'roles'
|
||||
@@ -1,7 +1,7 @@
|
||||
from sqlalchemy import Column, Integer, String, Boolean, Text, ForeignKey, DateTime, Numeric
|
||||
from sqlalchemy.orm import relationship
|
||||
from datetime import datetime
|
||||
from ..config.database import Base
|
||||
from ...shared.config.database import Base
|
||||
|
||||
class User(Base):
|
||||
__tablename__ = 'users'
|
||||
0
Backend/src/auth/routes/__init__.py
Normal file
0
Backend/src/auth/routes/__init__.py
Normal file
@@ -5,12 +5,12 @@ from pathlib import Path
|
||||
import aiofiles
|
||||
import uuid
|
||||
import os
|
||||
from ..config.database import get_db
|
||||
from ...shared.config.database import get_db
|
||||
from ..services.auth_service import auth_service
|
||||
from ..schemas.auth import RegisterRequest, LoginRequest, RefreshTokenRequest, ForgotPasswordRequest, ResetPasswordRequest, AuthResponse, TokenResponse, MessageResponse, MFAInitResponse, EnableMFARequest, VerifyMFARequest, MFAStatusResponse, UpdateProfileRequest
|
||||
from ..middleware.auth import get_current_user
|
||||
from ...security.middleware.auth import get_current_user
|
||||
from ..models.user import User
|
||||
from ..services.audit_service import audit_service
|
||||
from ...analytics.services.audit_service import audit_service
|
||||
from slowapi import Limiter, _rate_limit_exceeded_handler
|
||||
from slowapi.util import get_remote_address
|
||||
from slowapi.errors import RateLimitExceeded
|
||||
@@ -58,7 +58,7 @@ async def register(
|
||||
|
||||
try:
|
||||
result = await auth_service.register(db=db, name=register_request.name, email=register_request.email, password=register_request.password, phone=register_request.phone)
|
||||
from ..config.settings import settings
|
||||
from ...shared.config.settings import settings
|
||||
max_age = 7 * 24 * 60 * 60 # 7 days for registration
|
||||
# Use secure cookies in production (HTTPS required)
|
||||
# Set access token in httpOnly cookie for security
|
||||
@@ -144,7 +144,7 @@ async def login(
|
||||
status='success'
|
||||
)
|
||||
return {'status': 'success', 'requires_mfa': True, 'user_id': result['user_id']}
|
||||
from ..config.settings import settings
|
||||
from ...shared.config.settings import settings
|
||||
max_age = 7 * 24 * 60 * 60 if login_request.rememberMe else 1 * 24 * 60 * 60
|
||||
# Use secure cookies in production (HTTPS required)
|
||||
# Set access token in httpOnly cookie for security
|
||||
@@ -215,7 +215,7 @@ async def refresh_token(
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail='Refresh token not found')
|
||||
try:
|
||||
result = await auth_service.refresh_access_token(db, refreshToken)
|
||||
from ..config.settings import settings
|
||||
from ...shared.config.settings import settings
|
||||
# Set new access token in httpOnly cookie
|
||||
# Use 'lax' in development for cross-origin support, 'strict' in production
|
||||
samesite_value = 'strict' if settings.is_production else 'lax'
|
||||
@@ -250,7 +250,7 @@ async def logout(
|
||||
await auth_service.logout(db, refreshToken)
|
||||
|
||||
# Delete both access and refresh token cookies
|
||||
from ..config.settings import settings
|
||||
from ...shared.config.settings import settings
|
||||
# Use 'lax' in development for cross-origin support, 'strict' in production
|
||||
samesite_value = 'strict' if settings.is_production else 'lax'
|
||||
response.delete_cookie(key='refreshToken', path='/', secure=settings.is_production, samesite=samesite_value)
|
||||
@@ -320,7 +320,7 @@ async def reset_password(request: ResetPasswordRequest, db: Session=Depends(get_
|
||||
status_code = status.HTTP_404_NOT_FOUND
|
||||
raise HTTPException(status_code=status_code, detail=str(e))
|
||||
from ..services.mfa_service import mfa_service
|
||||
from ..config.settings import settings
|
||||
from ...shared.config.settings import settings
|
||||
|
||||
@router.get('/mfa/init')
|
||||
async def init_mfa(current_user: User=Depends(get_current_user), db: Session=Depends(get_db)):
|
||||
@@ -380,7 +380,7 @@ async def regenerate_backup_codes(current_user: User=Depends(get_current_user),
|
||||
async def upload_avatar(request: Request, image: UploadFile=File(...), current_user: User=Depends(get_current_user), db: Session=Depends(get_db)):
|
||||
try:
|
||||
# Use comprehensive file validation (magic bytes + size)
|
||||
from ..utils.file_validation import validate_uploaded_image
|
||||
from ...shared.utils.file_validation import validate_uploaded_image
|
||||
max_avatar_size = 2 * 1024 * 1024 # 2MB for avatars
|
||||
|
||||
# Validate file completely (MIME type, size, magic bytes, integrity)
|
||||
@@ -3,14 +3,14 @@ from sqlalchemy.orm import Session
|
||||
from sqlalchemy import or_
|
||||
from typing import Optional
|
||||
import bcrypt
|
||||
from ..config.database import get_db
|
||||
from ..middleware.auth import get_current_user, authorize_roles
|
||||
from ...shared.config.database import get_db
|
||||
from ...security.middleware.auth import get_current_user, authorize_roles
|
||||
from ..models.user import User
|
||||
from ..models.role import Role
|
||||
from ..models.booking import Booking, BookingStatus
|
||||
from ..utils.role_helpers import can_manage_users
|
||||
from ..utils.response_helpers import success_response
|
||||
from ..services.audit_service import audit_service
|
||||
from ...bookings.models.booking import Booking, BookingStatus
|
||||
from ...shared.utils.role_helpers import can_manage_users
|
||||
from ...shared.utils.response_helpers import success_response
|
||||
from ...analytics.services.audit_service import audit_service
|
||||
from ..schemas.user import CreateUserRequest, UpdateUserRequest
|
||||
router = APIRouter(prefix='/users', tags=['users'])
|
||||
|
||||
0
Backend/src/auth/schemas/__init__.py
Normal file
0
Backend/src/auth/schemas/__init__.py
Normal file
0
Backend/src/auth/services/__init__.py
Normal file
0
Backend/src/auth/services/__init__.py
Normal file
@@ -11,13 +11,13 @@ from ..models.user import User
|
||||
from ..models.refresh_token import RefreshToken
|
||||
from ..models.password_reset_token import PasswordResetToken
|
||||
from ..models.role import Role
|
||||
from ..utils.mailer import send_email
|
||||
from ..utils.email_templates import (
|
||||
from ...shared.utils.mailer import send_email
|
||||
from ...shared.utils.email_templates import (
|
||||
welcome_email_template,
|
||||
password_reset_email_template,
|
||||
password_changed_email_template
|
||||
)
|
||||
from ..config.settings import settings
|
||||
from ...shared.config.settings import settings
|
||||
import os
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -116,7 +116,7 @@ class AuthService:
|
||||
|
||||
async def register(self, db: Session, name: str, email: str, password: str, phone: Optional[str] = None) -> dict:
|
||||
# Validate password strength
|
||||
from ..utils.password_validation import validate_password_strength
|
||||
from ...shared.utils.password_validation import validate_password_strength
|
||||
is_valid, errors = validate_password_strength(password)
|
||||
if not is_valid:
|
||||
error_message = 'Password does not meet requirements: ' + '; '.join(errors)
|
||||
@@ -361,7 +361,7 @@ class AuthService:
|
||||
raise ValueError("Current password is incorrect")
|
||||
|
||||
# Validate new password strength
|
||||
from ..utils.password_validation import validate_password_strength
|
||||
from ...shared.utils.password_validation import validate_password_strength
|
||||
is_valid, errors = validate_password_strength(password)
|
||||
if not is_valid:
|
||||
error_message = 'New password does not meet requirements: ' + '; '.join(errors)
|
||||
@@ -6,9 +6,9 @@ import secrets
|
||||
from urllib.parse import urlencode
|
||||
import logging
|
||||
|
||||
from ..models.security_event import OAuthProvider, OAuthToken
|
||||
from ...security.models.security_event import OAuthProvider, OAuthToken
|
||||
from ..models.user import User
|
||||
from ..config.logging_config import get_logger
|
||||
from ...shared.config.logging_config import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
0
Backend/src/bookings/__init__.py
Normal file
0
Backend/src/bookings/__init__.py
Normal file
0
Backend/src/bookings/models/__init__.py
Normal file
0
Backend/src/bookings/models/__init__.py
Normal file
@@ -2,7 +2,7 @@ from sqlalchemy import Column, Integer, String, DateTime, Numeric, Boolean, Text
|
||||
from sqlalchemy.orm import relationship
|
||||
from datetime import datetime
|
||||
import enum
|
||||
from ..config.database import Base
|
||||
from ...shared.config.database import Base
|
||||
|
||||
class BookingStatus(str, enum.Enum):
|
||||
pending = 'pending'
|
||||
@@ -1,7 +1,7 @@
|
||||
from sqlalchemy import Column, Integer, DateTime, Numeric, Text, ForeignKey
|
||||
from sqlalchemy.orm import relationship
|
||||
from datetime import datetime
|
||||
from ..config.database import Base
|
||||
from ...shared.config.database import Base
|
||||
|
||||
class CheckInCheckOut(Base):
|
||||
__tablename__ = 'checkin_checkout'
|
||||
@@ -2,7 +2,7 @@ from sqlalchemy import Column, Integer, String, DateTime, Numeric, Boolean, Text
|
||||
from sqlalchemy.orm import relationship
|
||||
from datetime import datetime
|
||||
import enum
|
||||
from ..config.database import Base
|
||||
from ...shared.config.database import Base
|
||||
|
||||
class GroupBookingStatus(str, enum.Enum):
|
||||
draft = 'draft'
|
||||
0
Backend/src/bookings/routes/__init__.py
Normal file
0
Backend/src/bookings/routes/__init__.py
Normal file
@@ -5,25 +5,25 @@ from typing import Optional
|
||||
from datetime import datetime
|
||||
import random
|
||||
import os
|
||||
from ..config.database import get_db
|
||||
from ..config.settings import settings
|
||||
from ..middleware.auth import get_current_user, authorize_roles
|
||||
from ..models.user import User
|
||||
from ..models.role import Role
|
||||
from ...shared.config.database import get_db
|
||||
from ...shared.config.settings import settings
|
||||
from ...security.middleware.auth import get_current_user, authorize_roles
|
||||
from ...auth.models.user import User
|
||||
from ...auth.models.role import Role
|
||||
from ..models.booking import Booking, BookingStatus
|
||||
from ..models.room import Room, RoomStatus
|
||||
from ..models.room_type import RoomType
|
||||
from ..models.payment import Payment, PaymentMethod, PaymentType, PaymentStatus
|
||||
from ..models.service_usage import ServiceUsage
|
||||
from ..models.user_loyalty import UserLoyalty
|
||||
from ..models.referral import Referral, ReferralStatus
|
||||
from ..services.room_service import normalize_images, get_base_url
|
||||
from ...rooms.models.room import Room, RoomStatus
|
||||
from ...rooms.models.room_type import RoomType
|
||||
from ...payments.models.payment import Payment, PaymentMethod, PaymentType, PaymentStatus
|
||||
from ...hotel_services.models.service_usage import ServiceUsage
|
||||
from ...loyalty.models.user_loyalty import UserLoyalty
|
||||
from ...loyalty.models.referral import Referral, ReferralStatus
|
||||
from ...rooms.services.room_service import normalize_images, get_base_url
|
||||
from fastapi import Request
|
||||
from ..utils.mailer import send_email
|
||||
from ..utils.email_templates import booking_confirmation_email_template, booking_status_changed_email_template
|
||||
from ..services.loyalty_service import LoyaltyService
|
||||
from ..utils.currency_helpers import get_currency_symbol
|
||||
from ..utils.response_helpers import success_response
|
||||
from ...shared.utils.mailer import send_email
|
||||
from ...shared.utils.email_templates import booking_confirmation_email_template, booking_status_changed_email_template
|
||||
from ...loyalty.services.loyalty_service import LoyaltyService
|
||||
from ...shared.utils.currency_helpers import get_currency_symbol
|
||||
from ...shared.utils.response_helpers import success_response
|
||||
from ..schemas.booking import CreateBookingRequest, UpdateBookingRequest
|
||||
router = APIRouter(prefix='/bookings', tags=['bookings'])
|
||||
|
||||
@@ -216,7 +216,7 @@ async def create_booking(booking_data: CreateBookingRequest, current_user: User=
|
||||
raise HTTPException(status_code=409, detail='Room already booked for the selected dates')
|
||||
|
||||
# Check for maintenance blocks
|
||||
from ..models.room_maintenance import RoomMaintenance, MaintenanceStatus
|
||||
from ...rooms.models.room_maintenance import RoomMaintenance, MaintenanceStatus
|
||||
maintenance_block = db.query(RoomMaintenance).filter(
|
||||
and_(
|
||||
RoomMaintenance.room_id == room_id,
|
||||
@@ -251,7 +251,7 @@ async def create_booking(booking_data: CreateBookingRequest, current_user: User=
|
||||
# Calculate services total if any (using Pydantic model)
|
||||
services_total = 0.0
|
||||
if services:
|
||||
from ..models.service import Service
|
||||
from ...hotel_services.models.service import Service
|
||||
for service_item in services:
|
||||
service_id = service_item.service_id
|
||||
quantity = service_item.quantity
|
||||
@@ -314,8 +314,8 @@ async def create_booking(booking_data: CreateBookingRequest, current_user: User=
|
||||
# Process referral code if provided
|
||||
if referral_code:
|
||||
try:
|
||||
from ..services.loyalty_service import LoyaltyService
|
||||
from ..models.system_settings import SystemSettings
|
||||
from ...loyalty.services.loyalty_service import LoyaltyService
|
||||
from ...system.models.system_settings import SystemSettings
|
||||
|
||||
# Check if loyalty program is enabled
|
||||
setting = db.query(SystemSettings).filter(
|
||||
@@ -338,7 +338,7 @@ async def create_booking(booking_data: CreateBookingRequest, current_user: User=
|
||||
logger.warning(f"Failed to process referral code {referral_code}: {referral_error}")
|
||||
# Don't fail the booking if referral processing fails
|
||||
if payment_method in ['stripe', 'paypal']:
|
||||
from ..models.payment import Payment, PaymentMethod, PaymentStatus, PaymentType
|
||||
from ...payments.models.payment import Payment, PaymentMethod, PaymentStatus, PaymentType
|
||||
if payment_method == 'stripe':
|
||||
payment_method_enum = PaymentMethod.stripe
|
||||
elif payment_method == 'paypal':
|
||||
@@ -352,14 +352,14 @@ async def create_booking(booking_data: CreateBookingRequest, current_user: User=
|
||||
db.flush()
|
||||
logger.info(f'Payment created: ID={payment.id}, method={(payment.payment_method.value if hasattr(payment.payment_method, 'value') else payment.payment_method)}')
|
||||
if requires_deposit and deposit_amount > 0:
|
||||
from ..models.payment import Payment, PaymentMethod, PaymentStatus, PaymentType
|
||||
from ...payments.models.payment import Payment, PaymentMethod, PaymentStatus, PaymentType
|
||||
deposit_payment = Payment(booking_id=booking.id, amount=deposit_amount, payment_method=PaymentMethod.stripe, payment_type=PaymentType.deposit, deposit_percentage=deposit_percentage, payment_status=PaymentStatus.pending, payment_date=None)
|
||||
db.add(deposit_payment)
|
||||
db.flush()
|
||||
logger.info(f'Deposit payment created: ID={deposit_payment.id}, amount={deposit_amount}, percentage={deposit_percentage}%')
|
||||
# Add service usages (services already extracted from Pydantic model)
|
||||
if services:
|
||||
from ..models.service import Service
|
||||
from ...hotel_services.models.service import Service
|
||||
for service_item in services:
|
||||
service_id = service_item.service_id
|
||||
quantity = service_item.quantity
|
||||
@@ -375,7 +375,7 @@ async def create_booking(booking_data: CreateBookingRequest, current_user: User=
|
||||
|
||||
# Send booking confirmation notification
|
||||
try:
|
||||
from ..services.notification_service import NotificationService
|
||||
from ...notifications.services.notification_service import NotificationService
|
||||
if booking.status == BookingStatus.confirmed:
|
||||
NotificationService.send_booking_confirmation(db, booking)
|
||||
except Exception as e:
|
||||
@@ -384,11 +384,11 @@ async def create_booking(booking_data: CreateBookingRequest, current_user: User=
|
||||
logger.warning(f'Failed to send booking confirmation notification: {e}')
|
||||
|
||||
try:
|
||||
from ..services.invoice_service import InvoiceService
|
||||
from ..utils.mailer import send_email
|
||||
from ...payments.services.invoice_service import InvoiceService
|
||||
from ...shared.utils.mailer import send_email
|
||||
from sqlalchemy.orm import joinedload, selectinload
|
||||
booking = db.query(Booking).options(selectinload(Booking.service_usages).selectinload(ServiceUsage.service)).filter(Booking.id == booking.id).first()
|
||||
from ..models.system_settings import SystemSettings
|
||||
from ...system.models.system_settings import SystemSettings
|
||||
company_settings = {}
|
||||
for key in ['company_name', 'company_address', 'company_phone', 'company_email', 'company_tax_id', 'company_logo_url']:
|
||||
setting = db.query(SystemSettings).filter(SystemSettings.key == key).first()
|
||||
@@ -503,7 +503,7 @@ async def get_booking_by_id(id: int, request: Request, current_user: User=Depend
|
||||
booking = db.query(Booking).options(selectinload(Booking.payments), selectinload(Booking.service_usages).selectinload(ServiceUsage.service), joinedload(Booking.user), joinedload(Booking.room).joinedload(Room.room_type)).filter(Booking.id == id).first()
|
||||
if not booking:
|
||||
raise HTTPException(status_code=404, detail='Booking not found')
|
||||
from ..utils.role_helpers import is_admin
|
||||
from ...shared.utils.role_helpers import is_admin
|
||||
if not is_admin(current_user, db) and booking.user_id != current_user.id:
|
||||
raise HTTPException(status_code=403, detail='Forbidden')
|
||||
import logging
|
||||
@@ -612,7 +612,7 @@ async def cancel_booking(id: int, current_user: User=Depends(get_current_user),
|
||||
|
||||
if not active_booking:
|
||||
# Check for maintenance
|
||||
from ..models.room_maintenance import RoomMaintenance, MaintenanceStatus
|
||||
from ...rooms.models.room_maintenance import RoomMaintenance, MaintenanceStatus
|
||||
active_maintenance = db.query(RoomMaintenance).filter(
|
||||
and_(
|
||||
RoomMaintenance.room_id == booking.room_id,
|
||||
@@ -630,7 +630,7 @@ async def cancel_booking(id: int, current_user: User=Depends(get_current_user),
|
||||
db.flush()
|
||||
db.commit()
|
||||
try:
|
||||
from ..models.system_settings import SystemSettings
|
||||
from ...system.models.system_settings import SystemSettings
|
||||
client_url_setting = db.query(SystemSettings).filter(SystemSettings.key == 'client_url').first()
|
||||
client_url = client_url_setting.value if client_url_setting and client_url_setting.value else settings.CLIENT_URL or os.getenv('CLIENT_URL', 'http://localhost:5173')
|
||||
email_html = booking_status_changed_email_template(booking_number=booking.booking_number, guest_name=booking.user.full_name if booking.user else 'Guest', status='cancelled', client_url=client_url)
|
||||
@@ -673,7 +673,7 @@ async def update_booking(id: int, booking_data: UpdateBookingRequest, current_us
|
||||
# Set room to cleaning when checked out (housekeeping needed)
|
||||
if room:
|
||||
# Check if there's active maintenance
|
||||
from ..models.room_maintenance import RoomMaintenance, MaintenanceStatus
|
||||
from ...rooms.models.room_maintenance import RoomMaintenance, MaintenanceStatus
|
||||
active_maintenance = db.query(RoomMaintenance).filter(
|
||||
and_(
|
||||
RoomMaintenance.room_id == room.id,
|
||||
@@ -710,7 +710,7 @@ async def update_booking(id: int, booking_data: UpdateBookingRequest, current_us
|
||||
|
||||
if not active_booking:
|
||||
# Check for maintenance
|
||||
from ..models.room_maintenance import RoomMaintenance, MaintenanceStatus
|
||||
from ...rooms.models.room_maintenance import RoomMaintenance, MaintenanceStatus
|
||||
active_maintenance = db.query(RoomMaintenance).filter(
|
||||
and_(
|
||||
RoomMaintenance.room_id == room.id,
|
||||
@@ -753,7 +753,7 @@ async def update_booking(id: int, booking_data: UpdateBookingRequest, current_us
|
||||
# Send booking confirmation notification if status changed to confirmed
|
||||
if new_status == BookingStatus.confirmed:
|
||||
try:
|
||||
from ..services.notification_service import NotificationService
|
||||
from ...notifications.services.notification_service import NotificationService
|
||||
NotificationService.send_booking_confirmation(db, booking)
|
||||
except Exception as e:
|
||||
import logging
|
||||
@@ -778,7 +778,7 @@ async def update_booking(id: int, booking_data: UpdateBookingRequest, current_us
|
||||
if status_value and old_status != booking.status:
|
||||
if booking.status in [BookingStatus.confirmed, BookingStatus.cancelled]:
|
||||
try:
|
||||
from ..models.system_settings import SystemSettings
|
||||
from ...system.models.system_settings import SystemSettings
|
||||
client_url_setting = db.query(SystemSettings).filter(SystemSettings.key == 'client_url').first()
|
||||
client_url = client_url_setting.value if client_url_setting and client_url_setting.value else settings.CLIENT_URL or os.getenv('CLIENT_URL', 'http://localhost:5173')
|
||||
if booking.status == BookingStatus.confirmed:
|
||||
@@ -797,7 +797,7 @@ async def update_booking(id: int, booking_data: UpdateBookingRequest, current_us
|
||||
if booking.user:
|
||||
try:
|
||||
# Check if booking already earned points
|
||||
from ..models.loyalty_point_transaction import LoyaltyPointTransaction, TransactionSource
|
||||
from ...loyalty.models.loyalty_point_transaction import LoyaltyPointTransaction, TransactionSource
|
||||
existing_points = db.query(LoyaltyPointTransaction).filter(
|
||||
LoyaltyPointTransaction.booking_id == booking.id,
|
||||
LoyaltyPointTransaction.source == TransactionSource.booking
|
||||
@@ -815,7 +815,7 @@ async def update_booking(id: int, booking_data: UpdateBookingRequest, current_us
|
||||
user_loyalty = db.query(UserLoyalty).filter(UserLoyalty.user_id == booking.user_id).first()
|
||||
if user_loyalty and user_loyalty.referral_code:
|
||||
# Check if there's a referral for this user that hasn't been rewarded yet
|
||||
from ..models.referral import Referral
|
||||
from ...loyalty.models.referral import Referral
|
||||
referral = db.query(Referral).filter(
|
||||
Referral.referred_user_id == booking.user_id,
|
||||
Referral.booking_id == booking.id,
|
||||
@@ -999,7 +999,7 @@ async def admin_create_booking(booking_data: dict, current_user: User=Depends(au
|
||||
services = booking_data.get('services', [])
|
||||
services_total = 0.0
|
||||
if services:
|
||||
from ..models.service import Service
|
||||
from ...hotel_services.models.service import Service
|
||||
for service_item in services:
|
||||
service_id = service_item.get('service_id')
|
||||
quantity = service_item.get('quantity', 1)
|
||||
@@ -1078,7 +1078,7 @@ async def admin_create_booking(booking_data: dict, current_user: User=Depends(au
|
||||
db.flush()
|
||||
|
||||
# Create payment records based on payment_status
|
||||
from ..models.payment import Payment, PaymentMethod, PaymentStatus, PaymentType
|
||||
from ...payments.models.payment import Payment, PaymentMethod, PaymentStatus, PaymentType
|
||||
from datetime import datetime as dt
|
||||
|
||||
# Determine payment method enum
|
||||
@@ -1179,7 +1179,7 @@ async def admin_create_booking(booking_data: dict, current_user: User=Depends(au
|
||||
|
||||
# Add service usages if any
|
||||
if services:
|
||||
from ..models.service import Service
|
||||
from ...hotel_services.models.service import Service
|
||||
for service_item in services:
|
||||
service_id = service_item.get('service_id')
|
||||
quantity = service_item.get('quantity', 1)
|
||||
@@ -4,18 +4,18 @@ from typing import Optional, List
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
|
||||
from ..config.database import get_db
|
||||
from ..middleware.auth import get_current_user, authorize_roles
|
||||
from ..models.user import User
|
||||
from ..models.role import Role
|
||||
from ...shared.config.database import get_db
|
||||
from ...security.middleware.auth import get_current_user, authorize_roles
|
||||
from ...auth.models.user import User
|
||||
from ...auth.models.role import Role
|
||||
from ..models.group_booking import (
|
||||
GroupBooking, GroupBookingMember, GroupRoomBlock, GroupPayment,
|
||||
GroupBookingStatus, PaymentOption
|
||||
)
|
||||
from ..models.room import Room
|
||||
from ..models.room_type import RoomType
|
||||
from ...rooms.models.room import Room
|
||||
from ...rooms.models.room_type import RoomType
|
||||
from ..services.group_booking_service import GroupBookingService
|
||||
from ..services.room_service import get_base_url
|
||||
from ...rooms.services.room_service import get_base_url
|
||||
from fastapi import Request
|
||||
|
||||
router = APIRouter(prefix='/group-bookings', tags=['group-bookings'])
|
||||
0
Backend/src/bookings/schemas/__init__.py
Normal file
0
Backend/src/bookings/schemas/__init__.py
Normal file
0
Backend/src/bookings/services/__init__.py
Normal file
0
Backend/src/bookings/services/__init__.py
Normal file
@@ -9,10 +9,10 @@ from ..models.group_booking import (
|
||||
GroupBookingStatus, PaymentOption
|
||||
)
|
||||
from ..models.booking import Booking, BookingStatus
|
||||
from ..models.room import Room, RoomStatus
|
||||
from ..models.room_type import RoomType
|
||||
from ..models.user import User
|
||||
from ..models.payment import Payment, PaymentStatus, PaymentMethod
|
||||
from ...rooms.models.room import Room, RoomStatus
|
||||
from ...rooms.models.room_type import RoomType
|
||||
from ...auth.models.user import User
|
||||
from ...payments.models.payment import Payment, PaymentStatus, PaymentMethod
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user