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:
- Repetitive questions — "What day is trash pickup?", "How do I reset my garbage disposal?", "Is the pool open today?" consume hours of staff time each week.
- After-hours emergencies — A pipe burst at 2 AM needs immediate triage to determine whether to dispatch an on-call plumber or advise the tenant to shut off the valve and wait until morning.
- Ticket overload — Maintenance requests pile up without proper categorization, priority assignment, or automated follow-up.
- Lease and policy questions — Tenants frequently ask about pet policies, parking rules, or rent payment deadlines, all of which are documented but buried in PDFs that tenants rarely read thoroughly.
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:
- Ingestion gateway — Accepts tenant messages via SMS, email, web chat widget, or messaging apps (WhatsApp, Slack, etc.)
- Intent classification & entity extraction — Determines what the tenant needs (maintenance, policy question, emergency, payment issue) and extracts key details (unit number, appliance type, urgency cues)
- Retrieval-augmented generation (RAG) pipeline — Searches a vector database of property-specific documents to ground responses in factual, approved content
- Action engine — Creates maintenance tickets, sends notifications to on-call staff, updates tenant portals, or triggers downstream workflows
- Escalation & human-in-the-loop — Routes ambiguous or high-stakes cases to human agents with full conversation context
- Analytics dashboard — Tracks response times, resolution rates, common issue clusters, and tenant satisfaction scores
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