← Back to DevBytes

Property Management AI: Tenant Support Automation

Property Management AI: Tenant Support Automation

Property management AI for tenant support automation refers to the use of artificial intelligence—particularly large language models (LLMs), natural language processing (NLP), and workflow automation—to handle routine tenant inquiries, maintenance requests, lease questions, and emergency triage without requiring human intervention at every step. Instead of a property manager or leasing agent fielding hundreds of repetitive emails and calls, an AI-powered support layer can understand tenant intent, retrieve relevant information from policy documents and lease agreements, log tickets in maintenance systems, and escalate only the cases that genuinely need a human touch. This dramatically reduces response times, prevents burnout among staff, and ensures tenants receive accurate, consistent answers 24/7.

Why Tenant Support Automation Matters

In mid-size to large property portfolios, the volume of tenant communication scales faster than the support team can grow. Common pain points include:

An AI tenant support system addresses these issues by providing instant, accurate responses sourced directly from your knowledge base, lease documents, and operational playbooks. It also captures structured data from every interaction, feeding analytics that help property managers spot recurring problems, predict staffing needs, and proactively improve the tenant experience.

Core Architecture Overview

A production-ready tenant support automation system typically consists of these layers:

Below we build a working prototype step by step, using Python, FastAPI, OpenAI, and a lightweight vector store. You can adapt these patterns to any stack.

Step 1: Set Up the Project and Dependencies

Create a new project directory and install the required packages. We'll use FastAPI for the API layer, LangChain for orchestration, ChromaDB as a local vector store, and the OpenAI SDK for LLM calls.

# Create project structure
mkdir tenant-ai-support
cd tenant-ai-support
python -m venv venv
source venv/bin/activate  # or venv\Scripts\activate on Windows

# Install dependencies
pip install fastapi uvicorn langchain langchain-openai chromadb pydantic python-dotenv
pip install twilio python-multipart  # for SMS integration (optional)

Create a .env file to store API keys securely:

OPENAI_API_KEY=sk-your-key-here
TWILIO_ACCOUNT_SID=your-twilio-sid
TWILIO_AUTH_TOKEN=your-twilio-token
DATABASE_URL=sqlite:///tenant_support.db

Step 2: Define the Data Models

We need structured models for tenant messages, extracted intents, maintenance tickets, and conversation records. Create models.py:

from pydantic import BaseModel, Field
from typing import Optional, List, Dict
from datetime import datetime
from enum import Enum

class IntentCategory(str, Enum):
    MAINTENANCE_REQUEST = "maintenance_request"
    POLICY_QUESTION = "policy_question"
    EMERGENCY = "emergency"
    PAYMENT_ISSUE = "payment_issue"
    LEASE_QUESTION = "lease_question"
    GENERAL_INQUIRY = "general_inquiry"
    COMPLAINT = "complaint"

class PriorityLevel(str, Enum):
    LOW = "low"
    MEDIUM = "medium"
    HIGH = "high"
    CRITICAL = "critical"

class TenantMessage(BaseModel):
    tenant_id: str
    unit_number: str
    message_text: str
    channel: str = "web_chat"  # web_chat, sms, email, whatsapp
    timestamp: datetime = Field(default_factory=datetime.now)
    attachments: List[str] = []

class ExtractedIntent(BaseModel):
    category: IntentCategory
    confidence: float
    entities: Dict[str, str] = {}
    priority: PriorityLevel = PriorityLevel.MEDIUM
    requires_immediate_action: bool = False
    summary: str

class MaintenanceTicket(BaseModel):
    ticket_id: str
    tenant_id: str
    unit_number: str
    issue_description: str
    category: str
    priority: PriorityLevel
    status: str = "open"
    created_at: datetime = Field(default_factory=datetime.now)
    assigned_to: Optional[str] = None
    scheduled_date: Optional[datetime] = None
    resolution_notes: Optional[str] = None

class AIResponse(BaseModel):
    response_text: str
    ticket_created: Optional[MaintenanceTicket] = None
    escalation_required: bool = False
    follow_up_actions: List[str] = []
    confidence_score: float

Step 3: Build the Intent Classifier

The intent classifier uses an LLM to categorize incoming messages and extract structured entities. This is the brain of the automation system. Create classifier.py:

import json
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from models import TenantMessage, ExtractedIntent, IntentCategory, PriorityLevel

class IntentClassifier:
    def __init__(self, model_name: str = "gpt-4o-mini"):
        self.llm = ChatOpenAI(
            model=model_name,
            temperature=0.1  # Low temperature for consistent classification
        )
        self.prompt = ChatPromptTemplate.from_messages([
            ("system", """You are an expert property management AI assistant. 
Your task is to classify tenant messages and extract structured information.

Classify the intent into EXACTLY ONE of these categories:
- maintenance_request: Broken appliances, leaks, HVAC issues, electrical problems, plumbing
- policy_question: Questions about building rules, amenity hours, guest policies, noise rules
- emergency: Fire, flood, gas leak, major water damage, security threats, carbon monoxide
- payment_issue: Rent payment problems, late fees, payment portal issues, billing questions
- lease_question: Lease terms, renewal, termination, rent increase, security deposit
- general_inquiry: Non-urgent questions not fitting other categories
- complaint: Noise complaints, neighbor disputes, cleanliness issues, dissatisfaction

Determine priority:
- critical: Life-threatening emergencies (fire, gas, active flooding)
- high: Major issues requiring immediate attention (water leak damaging property, no heat in winter)
- medium: Standard maintenance, policy questions, routine inquiries
- low: Cosmetic issues, informational questions with no urgency

Extract these entities when present (use null if not found):
- appliance: The specific appliance or system (e.g., dishwasher, HVAC, toilet)
- location: Where the issue is (e.g., kitchen, bathroom, unit 3B)
- urgency_markers: Words indicating time sensitivity (e.g., "immediately", "flooding", "tonight")
- lease_topic: Specific lease section referenced (e.g., pet policy, parking, renewal)
- payment_amount: Dollar amount if mentioned
- date_reference: Any dates mentioned (e.g., "January 15th", "next week")

Return ONLY valid JSON with no markdown formatting:
{
  "category": "maintenance_request",
  "confidence": 0.95,
  "priority": "medium",
  "requires_immediate_action": false,
  "entities": {
    "appliance": "dishwasher",
    "location": "kitchen",
    "urgency_markers": null,
    "lease_topic": null,
    "payment_amount": null,
    "date_reference": null
  },
  "summary": "Tenant reports dishwasher not draining in kitchen"
}"""),
            ("human", "Tenant ID: {tenant_id}\nUnit: {unit_number}\nMessage: {message_text}\n\nClassify this message:")
        ])
    
    def classify(self, message: TenantMessage) -> ExtractedIntent:
        chain = self.prompt | self.llm
        result = chain.invoke({
            "tenant_id": message.tenant_id,
            "unit_number": message.unit_number,
            "message_text": message.message_text
        })
        
        # Parse the JSON response
        content = result.content
        if "json" in content:
            content = content.split("json")[1].split("")[0].strip()
        elif "" in content:
            content = content.split("")[1].split("")[0].strip()
        
        parsed = json.loads(content)
        return ExtractedIntent(
            category=IntentCategory(parsed["category"]),
            confidence=parsed["confidence"],
            entities=parsed.get("entities", {}),
            priority=PriorityLevel(parsed["priority"]),
            requires_immediate_action=parsed.get("requires_immediate_action", False),
            summary=parsed["summary"]
        )

Step 4: Create the Knowledge Base with RAG

Tenants ask questions that are answered in lease agreements, building policies, and FAQ documents. We embed these documents into a vector store and retrieve relevant chunks at query time. Create knowledge_base.py:

import os
from langchain_openai import OpenAIEmbeddings
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.vectorstores import Chroma
from langchain_community.document_loaders import TextLoader, PyPDFLoader
from langchain_core.documents import Document
from typing import List

class PropertyKnowledgeBase:
    def __init__(self, persist_directory: str = "./chroma_db"):
        self.embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
        self.persist_directory = persist_directory
        self.vectorstore = None
        self.text_splitter = RecursiveCharacterTextSplitter(
            chunk_size=500,
            chunk_overlap=80,
            separators=["\n\n", "\n", ". ", " ", ""]
        )
    
    def load_documents(self, documents_dir: str):
        """Load all policy docs, lease templates, and FAQ files from a directory."""
        all_docs = []
        
        for filename in os.listdir(documents_dir):
            filepath = os.path.join(documents_dir, filename)
            if filename.endswith('.txt'):
                loader = TextLoader(filepath)
                docs = loader.load()
            elif filename.endswith('.pdf'):
                loader = PyPDFLoader(filepath)
                docs = loader.load()
            else:
                continue
            
            # Tag each document with its source
            for doc in docs:
                doc.metadata["source_file"] = filename
                doc.metadata["category"] = "policy" if "policy" in filename.lower() else \
                                           "lease" if "lease" in filename.lower() else \
                                           "faq" if "faq" in filename.lower() else "general"
            all_docs.extend(docs)
        
        # Split documents into chunks
        chunks = self.text_splitter.split_documents(all_docs)
        return chunks
    
    def build_index(self, documents_dir: str):
        """Build the vector index from documents."""
        chunks = self.load_documents(documents_dir)
        self.vectorstore = Chroma.from_documents(
            documents=chunks,
            embedding=self.embeddings,
            persist_directory=self.persist_directory
        )
        print(f"Indexed {len(chunks)} chunks from {documents_dir}")
    
    def load_existing_index(self):
        """Load a previously built index."""
        self.vectorstore = Chroma(
            persist_directory=self.persist_directory,
            embedding_function=self.embeddings
        )
    
    def retrieve_relevant_context(self, query: str, k: int = 4) -> List[Document]:
        """Retrieve the most relevant document chunks for a query."""
        if self.vectorstore is None:
            self.load_existing_index()
        
        results = self.vectorstore.similarity_search_with_score(query, k=k)
        
        # Filter out low-relevance results (score threshold depends on embedding model)
        relevant_docs = [doc for doc, score in results if score < 0.5]
        return relevant_docs
    
    def format_context(self, docs: List[Document]) -> str:
        """Format retrieved documents into a context string for the LLM."""
        context_parts = []
        for i, doc in enumerate(docs, 1):
            source = doc.metadata.get("source_file", "unknown")
            context_parts.append(
                f"[Document {i} | Source: {source}]\n{doc.page_content}\n"
            )
        return "\n".join(context_parts)

Prepare your documents in a documents/ folder. Example files:

# documents/lease_terms.txt
LEASE TERMS AND CONDITIONS
Section 3.2 - Pet Policy: Tenants may keep up to two domestic pets 
(cats or dogs under 50 lbs) with a non-refundable pet deposit of $300 
per pet. All pets must be registered with the management office. 
Barking or aggressive behavior that disturbs other tenants may result 
in revocation of pet privileges.

Section 4.1 - Rent Payment: Rent is due on the 1st of each month. 
A late fee of $50 applies after the 5th. Payments can be made via 
the online portal, check, or money order. Cash is not accepted.

# documents/building_policies.txt
BUILDING POLICIES
Trash and Recycling: Trash pickup occurs every Tuesday and Friday 
at 7:00 AM. Recycling is collected every Wednesday. Please place 
bins curbside the night before collection.

Pool Hours: The pool is open from 8:00 AM to 9:00 PM daily, 
May through September. Pool passes are required and can be obtained 
from the leasing office.

Quiet Hours: Quiet hours are from 10:00 PM to 7:00 AM. Please be 
respectful of neighbors. Noise complaints will result in a warning, 
followed by fines for repeat violations.

# documents/maintenance_faq.txt
MAINTENANCE FAQ
Garbage Disposal Reset: Locate the reset button on the bottom of 
the unit under the sink. Press firmly until you hear a click. 
If it doesn't reset, check the breaker panel in your unit's hallway closet.

HVAC Filter Change: Filters should be changed every 3 months. 
Replacement filters are available at the leasing office for $8 each 
or you may purchase compatible 16x25x1 filters at any hardware store.

Water Shutoff: In case of a plumbing emergency, the main water 
shutoff valve for each unit is located in the utility closet 
near the water heater. Turn the red lever clockwise to shut off water.

Step 5: Build the Response Generator

The response generator combines the classified intent, retrieved knowledge, and LLM reasoning to produce a helpful, safe response. Create response_generator.py:

from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from models import TenantMessage, ExtractedIntent, AIResponse, MaintenanceTicket, PriorityLevel
from knowledge_base import PropertyKnowledgeBase
import uuid
from datetime import datetime

class ResponseGenerator:
    def __init__(self, knowledge_base: PropertyKnowledgeBase):
        self.knowledge_base = knowledge_base
        self.llm = ChatOpenAI(model="gpt-4o", temperature=0.3)
        
        self.response_prompt = ChatPromptTemplate.from_messages([
            ("system", """You are a helpful, professional property management AI assistant for 
{property_name}. Your name is "Maple Assistant".

CRITICAL RULES:
1. NEVER make up policies, lease terms, or procedures. Only use information from the 
   provided context documents. If the context doesn't contain the answer, say:
   "I don't have that specific information in my records. Let me connect you with 
   a property manager who can help. I'll flag this for follow-up."
2. For maintenance requests, be empathetic and provide clear next steps.
3. For emergencies (fire, flood, gas leak, active major water damage), IMMEDIATELY 
   instruct the tenant to call 911 if life-threatening, then provide the emergency 
   maintenance number: (555) 123-4567.
4. Always confirm the tenant's unit number when creating tickets.
5. Be concise but warm. Use a friendly, professional tone.
6. If you need to escalate, clearly state that a human team member will follow up.

Provided context from property documents:
{context}

Tenant message details:
- Tenant ID: {tenant_id}
- Unit: {unit_number}
- Intent category: {intent_category}
- Priority: {priority}
- Extracted entities: {entities}

Generate a response that:
- Addresses the tenant by name if known, otherwise by unit
- Directly answers the question or addresses the concern
- Cites specific policy details when applicable (with section references)
- Provides clear action steps for maintenance issues
- States whether a ticket has been created and what to expect next"""),
            ("human", "Tenant message: {message_text}\n\nGenerate the appropriate response:")
        ])
    
    def generate(self, message: TenantMessage, intent: ExtractedIntent) -> AIResponse:
        # Retrieve relevant context from knowledge base
        relevant_docs = self.knowledge_base.retrieve_relevant_context(
            query=f"{intent.category} {intent.summary} {message.message_text}",
            k=5
        )
        context = self.knowledge_base.format_context(relevant_docs)
        
        # Determine if escalation is needed
        escalation_needed = (
            intent.category == "emergency" or
            intent.priority == PriorityLevel.CRITICAL or
            (intent.confidence < 0.7) or
            ("legal" in message.message_text.lower()) or
            ("lawsuit" in message.message_text.lower()) or
            ("attorney" in message.message_text.lower())
        )
        
        chain = self.response_prompt | self.llm
        result = chain.invoke({
            "property_name": "Maple Ridge Apartments",
            "context": context if context else "No specific documents matched this query.",
            "tenant_id": message.tenant_id,
            "unit_number": message.unit_number,
            "intent_category": intent.category.value,
            "priority": intent.priority.value,
            "entities": str(intent.entities),
            "message_text": message.message_text
        })
        
        response_text = result.content
        
        # Create maintenance ticket if applicable
        ticket = None
        if intent.category in ["maintenance_request", "emergency"]:
            ticket = MaintenanceTicket(
                ticket_id=f"MT-{uuid.uuid4().hex[:8].upper()}",
                tenant_id=message.tenant_id,
                unit_number=message.unit_number,
                issue_description=intent.summary,
                category=intent.category.value,
                priority=intent.priority,
                status="open",
                created_at=datetime.now()
            )
        
        return AIResponse(
            response_text=response_text,
            ticket_created=ticket,
            escalation_required=escalation_needed,
            follow_up_actions=["Monitor ticket status"] if ticket else [],
            confidence_score=intent.confidence
        )

Step 6: Build the FastAPI Application

Now we wire everything together with a REST API. Create main.py:

from fastapi import FastAPI, HTTPException, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from pydantic import BaseModel
from typing import Optional
import logging

from models import TenantMessage, AIResponse
from classifier import IntentClassifier
from knowledge_base import PropertyKnowledgeBase
from response_generator import ResponseGenerator

# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

app = FastAPI(title="Tenant Support AI", version="1.0.0")

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_methods=["*"],
    allow_headers=["*"],
)

# Initialize components
knowledge_base = PropertyKnowledgeBase()
classifier = IntentClassifier()
response_generator = ResponseGenerator(knowledge_base)

# Build index on startup (or load existing)
@app.on_event("startup")
async def startup_event():
    try:
        knowledge_base.load_existing_index()
        logger.info("Loaded existing knowledge base index")
    except Exception:
        logger.info("Building new knowledge base index...")
        knowledge_base.build_index("./documents")

class ChatRequest(BaseModel):
    tenant_id: str
    unit_number: str
    message: str
    channel: Optional[str] = "web_chat"

class ChatResponse(BaseModel):
    reply: str
    ticket_id: Optional[str] = None
    escalation: bool
    intent_category: str
    confidence: float

@app.post("/api/chat", response_model=ChatResponse)
async def handle_chat(request: ChatRequest):
    """Main endpoint for tenant chat interactions."""
    try:
        # Create tenant message model
        tenant_msg = TenantMessage(
            tenant_id=request.tenant_id,
            unit_number=request.unit_number,
            message_text=request.message,
            channel=request.channel
        )
        
        logger.info(f"Processing message from tenant {request.tenant_id} in unit {request.unit_number}")
        
        # Step 1: Classify intent
        intent = classifier.classify(tenant_msg)
        logger.info(f"Classified intent: {intent.category.value} (confidence: {intent.confidence})")
        
        # Step 2: Generate response with RAG
        ai_response = response_generator.generate(tenant_msg, intent)
        
        # Step 3: If ticket created, store it and notify on-call if emergency
        if ai_response.ticket_created:
            ticket = ai_response.ticket_created
            logger.info(f"Created ticket {ticket.ticket_id} for {intent.category.value}")
            
            # In production: save to database, trigger notifications
            # await db.save_ticket(ticket)
            # if intent.priority == "critical":
            #     await notification_service.alert_oncall(ticket)
        
        return ChatResponse(
            reply=ai_response.response_text,
            ticket_id=ai_response.ticket_created.ticket_id if ai_response.ticket_created else None,
            escalation=ai_response.escalation_required,
            intent_category=intent.category.value,
            confidence=intent.confidence
        )
    
    except Exception as e:
        logger.error(f"Error processing chat: {str(e)}")
        raise HTTPException(status_code=500, detail="Internal processing error")

@app.get("/api/health")
async def health_check():
    return {"status": "healthy", "service": "Tenant Support AI"}

# Webhook endpoint for SMS via Twilio
@app.post("/api/sms/webhook")
async def sms_webhook(request: Request):
    """Handle incoming SMS messages from Twilio."""
    form_data = await request.form()
    from_number = form_data.get("From", "")
    message_body = form_data.get("Body", "")
    
    # In production: look up tenant by phone number
    # Here we extract a demo tenant ID from the message or use a default
    tenant_id = "T-1001"  # Would be looked up from phone number mapping
    unit_number = "3B"
    
    tenant_msg = TenantMessage(
        tenant_id=tenant_id,
        unit_number=unit_number,
        message_text=message_body,
        channel="sms"
    )
    
    intent = classifier.classify(tenant_msg)
    ai_response = response_generator.generate(tenant_msg, intent)
    
    # Return TwiML response
    from twilio.twiml.messaging_response import MessagingResponse
    twiml = MessagingResponse()
    twiml.message(ai_response.response_text)
    
    return JSONResponse(
        content=str(twiml),
        media_type="application/xml"
    )

Run the application with:

uvicorn main:app --reload --host 0.0.0.0 --port 8000

Step 7: Add a Web Chat Widget (Frontend)

Here's a lightweight chat widget you can embed on a tenant portal. Create widget.html:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Tenant Support Chat</title>
  <style>
    * { box-sizing: border-box; margin: 0; padding: 0; }
    body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }
    
    .chat-container {
      max-width: 600px;
      margin: 40px auto;
      border: 1px solid #e0e0e0;
      border-radius: 12px;
      overflow: hidden;
      box-shadow: 0 4px 20px rgba(0,0,0,0.08);
      display: flex;
      flex-direction: column;
      height: 70vh;
    }
    
    .chat-header {
      background: linear-gradient(135deg, #2c3e50, #3498db);
      color: white;
      padding: 20px;
      font-weight: 600;
      font-size: 18px;
    }
    
    .chat-header span {
      font-size: 12px;
      opacity: 0.8;
      display: block;
    }
    
    .chat-messages {
      flex: 1;
      overflow-y: auto;
      padding: 20px;
      background: #f8f9fa;
    }
    
    .message {
      margin-bottom: 16px;
      display: flex;
      flex-direction: column;
    }
    
    .message.user { align-items: flex-end; }
    .message.ai { align-items: flex-start; }
    
    .message-bubble {
      max-width: 80%;
      padding: 12px 16px;
      border-radius: 18px;
      line-height: 1.5;
      font-size: 14px;
    }
    
    .message.user .message-bubble {
      background: #3498db;
      color: white;
      border-bottom-right-radius: 4px;
    }
    
    .message.ai .message-bubble {
      background: white;
      color: #2c3e50;
      border: 1px solid #e0e0e0;
      border-bottom-left-radius: 4px;
    }
    
    .message-meta {
      font-size: 11px;
      color: #95a5a6;
      margin-top: 4px;
      padding: 0 4px;
    }
    
    .chat-input-area {
      padding: 16px;
      background: white;
      border-top: 1px solid #e0e0e0;
      display: flex;
      gap: 8px;
    }
    
    .chat-input-area input {
      flex: 1;
      padding: 12px 16px;
      border: 1px solid #ddd;
      border-radius: 24px;
      font-size: 14px;
      outline: none;
      transition: border-color 0.2s;
    }
    
    .chat-input-area input:focus {
      border-color: #3498db;
    }
    
    .chat-input-area button {
      padding: 12px 24px;
      background: #3498db;
      color: white;
      border: none;
      border-radius: 24px;
      font-weight: 600;
      cursor: pointer;
      transition: background 0.2s;
    }
    
    .chat-input-area button:hover { background: #2980b9; }
    .chat-input-area button:disabled { background: #bdc3c7; cursor: not-allowed; }
    
    .typing-indicator {
      color: #95a5a6;
      font-style: italic;
      font-size: 13px;
      padding: 8px 16px;
    }
    
    .ticket-badge {
      background: #27ae60;
      color: white;
      font-size: 11px;
      padding: 4px 10px;
      border-radius: 12px;
      display: inline-block;
      margin-top: 6px;
    }
  </style>
</head>
<body>
  <div class="chat-container">
    <div class="chat-header">
      🏢 Maple Ridge Apartments - Tenant Support
      <span>AI-powered assistance 24/7 • Escalates to human team when needed</span>
    </div>
    
    <div class="chat-messages" id="chatMessages">
      <div class="message ai">
        <div class="message-bubble">
          👋 Hello! I'm your property management assistant. How can I help you today? 
          You can ask about maintenance, policies, lease terms, or report issues in your unit.
        </div>
        <div class="message-meta">Maple Assistant • Just now</div>
      </div>
    </div>
    
    <div class="chat-input-area">
      <input 
        type="text" 
        id="chatInput" 
        placeholder="Type your question or describe an issue..." 
        autocomplete="off"
      >
      <button id="sendButton">Send</button>
    </div>
  </div>

  <script>
    const chatMessages = document.getElementById('chatMessages');
    const chatInput = document.getElementById('chatInput');
    const sendButton = document.getElementById('sendButton');
    
    // Demo tenant - in production this comes from authentication
    const TENANT_ID = 'T-1001';
    const UNIT_NUMBER = '3B';
    
    let isProcessing = false;
    
    function addMessage(text, sender, ticketId = null) {
      const messageDiv = document.createElement('div');
      messageDiv.className = `message ${sender}`;
      
      const bubble = document.createElement('div');
      bubble.className = 'message-bubble';
      bubble.textContent = text;
      messageDiv.appendChild(bubble);
      
      if (ticketId) {
        const badge = document.createElement('div');
        badge.className = 'ticket-badge';
        badge.textContent = `🎫 Ticket ${ticketId} created`;
        messageDiv.appendChild(badge);
      }
      
      const meta = document.createElement('div');
      meta.className = 'message-meta';
      meta.textContent = sender === 'user' ? 'You • Just now' : 'Maple Assistant • Just now';
      messageDiv.appendChild(meta);
      
      chatMessages.appendChild(messageDiv);
      chatMessages.scrollTop = chatMessages.scrollHeight;
    }
    
    function showTypingIndicator() {
      const indicator = document.createElement('div');
      indicator.className = 'typing-indicator';
      indicator.id = 'typingIndicator';
      indicator.textContent = 'Maple Assistant is typing...';
      chatMessages.appendChild(indicator);
      chatMessages.scrollTop = chatMessages.scrollHeight;
    }
    
    function hideTypingIndicator() {
      const indicator = document.getElementById('typingIndicator');
      if (indicator) indicator.remove();
    }
    
    async function sendMessage() {
      const message = chatInput.value.trim();
      if (!message || isProcessing) return;
      
      isProcessing = true;
      sendButton.disabled = true;
      chatInput.value = '';
      
      addMessage(message, 'user');
      showTypingIndicator();
      
      try {
        const response = await fetch('/api/chat', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({
            tenant_id: TENANT_ID,
            unit_number: UNIT_NUMBER,
            message: message,
            channel: 'web_chat'
          })
        });
        
        const data = await response.json();
        hideTypingIndicator();
        
        if (data.reply) {
          addMessage(data.reply, 'ai', data.ticket_id);
        }
        
        if (data.escalation) {
          setTimeout(() => {
            addMessage(
              '📞 A property manager has been notified and will follow up with you shortly.',
              'ai'
            );
          }, 2000);
        }
        
      } catch (error) {
        hideTypingIndicator();
        addMessage(
          'I apologize, but I encountered a technical issue. Please try again or call (555) 123-4567 for immediate assistance.',
          'ai'
        );
        console.error('Chat error:', error);
      } finally {
        isProcessing = false;
        sendButton.disabled = false;
        chatInput.focus();
      }
    }
    
    sendButton.addEventListener('click', sendMessage);
    chatInput.addEventListener('keypress', (e) => {
      if (e.key === 'Enter' && !e.shiftKey) {
        e.preventDefault();
        sendMessage();
      }
    });
    
    // Focus input on load
    chatInput.focus();
  </script>
</body>
</html>

Step 8: Add Emergency Detection and Escalation Logic

Emergencies require special handling. Create emergency_handler.py to detect critical situations and trigger alerts:

from models import TenantMessage, Ext

— Ad —

Google AdSense will appear here after approval

← Back to all articles