Autonomní správa majetku s AI Agent
Tradiční správa nemovitostí je odvětví s vysokou intenzitou manuální práce: nemovitost manažer spravuje v průměru 200-300 jednotek s téměř výhradně manuálními procesy (komunikace s nájemci, objednávky údržby, vybírání nájemného, obnovování smluv). Agenti AI to mění rovnice radikálně: systémy jako IBM Maximo aplikovat na správu majetku snížit neplánované prostoje 50 % a náklady na údržbu 10–40 %.
V tomto článku vytváříme autonomní systém správy majetku s agentem AI: chatbotem vícekanálový na nájemce, systém prediktivní údržby se senzory IoT, automatizace pracovních postupů operace a dynamický systém cen nájmu založený na ML.
Co se naučíte
- Multiagentní architektura pro správu majetku: orchestrace a použití nástrojů
- Chatbot tenant s LLM: správa požadavků, údržba, platby
- Prediktivní údržba: Datový kanál IoT senzorů a detekce anomálií
- Dynamická cena: ML model pro optimalizaci pronájmu
- Automatizace workflow: schvalování, smlouvy, komunikace
- Budování digitálního dvojčete: simulace a optimalizace operací
- Integrace CMMS (Computerized Maintenance Management System).
- Sledování KPI a automatický reporting pro portfolio nemovitostí
Multiagentní architektura pro správu majetku
Autonomní systém správy majetku složený z více specializovaných agentů, kteří spolupracují. Použijme vzor orchestrátor + pracovní agenti: souřadnice centrálního agenta směrování požadavků, zatímco specializovaní agenti spravují konkrétní domény (údržba, platby, vztahy s nájemníky, dodržování předpisů).
import OpenAI from 'openai';
const openai = new OpenAI({ apiKey: process.env['OPENAI_API_KEY'] });
// Tool definitions per l'agente property manager
const propertyManagementTools = [
{
type: 'function' as const,
function: {
name: 'create_maintenance_request',
description: 'Crea un ordine di manutenzione per un\'unita',
parameters: {
type: 'object',
properties: {
unit_id: { type: 'string', description: 'ID dell\'unita' },
category: {
type: 'string',
enum: ['plumbing', 'electrical', 'hvac', 'appliance', 'structural', 'other'],
description: 'Categoria del problema'
},
priority: {
type: 'string',
enum: ['emergency', 'urgent', 'normal', 'low'],
description: 'Priorità dell\'intervento'
},
description: { type: 'string', description: 'Descrizione dettagliata del problema' },
preferred_time: { type: 'string', description: 'Fascia oraria preferita per l\'intervento' },
},
required: ['unit_id', 'category', 'priority', 'description'],
},
},
},
{
type: 'function' as const,
function: {
name: 'check_rent_status',
description: 'Verifica lo stato del pagamento affitto per un inquilino',
parameters: {
type: 'object',
properties: {
tenant_id: { type: 'string' },
},
required: ['tenant_id'],
},
},
},
{
type: 'function' as const,
function: {
name: 'get_unit_information',
description: 'Recupera informazioni sull\'unita (contratto, servizi, regole)',
parameters: {
type: 'object',
properties: {
unit_id: { type: 'string' },
info_type: {
type: 'string',
enum: ['lease', 'utilities', 'rules', 'amenities', 'neighbors_contact'],
},
},
required: ['unit_id', 'info_type'],
},
},
},
{
type: 'function' as const,
function: {
name: 'escalate_to_human',
description: 'Trasferisci la richiesta a un property manager umano',
parameters: {
type: 'object',
properties: {
reason: { type: 'string', description: 'Motivo dell\'escalation' },
urgency: { type: 'string', enum: ['high', 'medium', 'low'] },
conversation_summary: { type: 'string' },
},
required: ['reason', 'urgency'],
},
},
},
];
export class TenantAssistantAgent {
private conversations = new Map<string, OpenAI.ChatCompletionMessageParam[]>();
async handleMessage(
tenantId: string,
unitId: string,
message: string,
channel: 'whatsapp' | 'email' | 'webapp'
): Promise<string> {
const history = this.conversations.get(tenantId) ?? [];
const messages: OpenAI.ChatCompletionMessageParam[] = [
{
role: 'system',
content: `Sei l'assistente virtuale di gestione immobiliare per l'inquilino ${tenantId}
dell'unita ${unitId}. Sei amichevole, professionale e risolvi i problemi in modo efficiente.
Puoi creare richieste di manutenzione, verificare pagamenti e fornire informazioni.
Per problemi di emergenza (gas, alluvione, incendio) esegui sempre escalation immediata.
Rispondi sempre in italiano.`,
},
...history,
{ role: 'user', content: message },
];
let response = await openai.chat.completions.create({
model: 'gpt-4o',
messages,
tools: propertyManagementTools,
tool_choice: 'auto',
});
let assistantMessage = response.choices[0].message;
// Agentic loop: esegui tools finchè l'agent ha finito
while (assistantMessage.tool_calls && assistantMessage.tool_calls.length > 0) {
messages.push(assistantMessage);
const toolResults: OpenAI.ChatCompletionMessageParam[] = [];
for (const toolCall of assistantMessage.tool_calls) {
const result = await this.executeTool(
toolCall.function.name,
JSON.parse(toolCall.function.arguments)
);
toolResults.push({
role: 'tool',
tool_call_id: toolCall.id,
content: JSON.stringify(result),
});
}
messages.push(...toolResults);
response = await openai.chat.completions.create({
model: 'gpt-4o',
messages,
tools: propertyManagementTools,
});
assistantMessage = response.choices[0].message;
}
const replyText = assistantMessage.content ?? 'Mi dispiace, riprova tra poco.';
// Aggiorna history (mantieni ultimi 20 messaggi)
const updatedHistory = [...history, { role: 'user' as const, content: message }, assistantMessage];
this.conversations.set(tenantId, updatedHistory.slice(-20));
return replyText;
}
private async executeTool(name: string, args: Record<string, unknown>): Promise<unknown> {
switch (name) {
case 'create_maintenance_request':
return this.createMaintenanceRequest(args);
case 'check_rent_status':
return this.checkRentStatus(String(args['tenant_id']));
case 'get_unit_information':
return this.getUnitInformation(String(args['unit_id']), String(args['info_type']));
case 'escalate_to_human':
return this.escalateToHuman(args);
default:
return { error: 'Unknown tool' };
}
}
private async createMaintenanceRequest(args: Record<string, unknown>) {
// Integrazione con CMMS (es. ServiceChannel, Buildium, AppFolio)
const workOrder = {
id: `WO-${Date.now()}`,
unitId: args['unit_id'],
category: args['category'],
priority: args['priority'],
description: args['description'],
status: 'open',
createdAt: new Date().toISOString(),
estimatedCompletion: this.estimateCompletion(String(args['priority'])),
};
// In produzione: API call a CMMS
console.log('Creating work order:', workOrder);
return {
success: true,
workOrderId: workOrder.id,
message: `Richiesta creata con ID ${workOrder.id}. Un tecnico ti contatterà entro ${this.priorityResponse(String(args['priority']))}.`,
};
}
private priorityResponse(priority: string): string {
const responses = {
emergency: '2 ore',
urgent: '24 ore',
normal: '3-5 giorni lavorativi',
low: '7-10 giorni lavorativi',
};
return responses[priority as keyof typeof responses] ?? '5 giorni lavorativi';
}
private estimateCompletion(priority: string): string {
const days = { emergency: 0, urgent: 1, normal: 5, low: 10 }[priority] ?? 5;
const date = new Date();
date.setDate(date.getDate() + days);
return date.toISOString();
}
private async checkRentStatus(tenantId: string) {
// In produzione: query al database finanziario
return {
tenantId,
currentMonth: { status: 'paid', amount: 850, paidDate: '2026-03-01' },
balance: 0,
nextDueDate: '2026-04-01',
};
}
private async getUnitInformation(unitId: string, infoType: string) {
// In produzione: query al database immobiliare
return { unitId, type: infoType, data: '...fetched from database...' };
}
private async escalateToHuman(args: Record<string, unknown>) {
// In produzione: crea ticket in sistema CRM, notifica manager
return {
success: true,
ticketId: `ESC-${Date.now()}`,
message: 'Il tuo caso e stato assegnato a un property manager che ti contatterà presto.',
};
}
}
Prediktivní údržba s IoT a ML
Prediktivní údržba je ekonomicky nejefektivnější případ použití. IoT senzory monitorují nepřetržitě HVAC systémy, výtahy, čerpadla a elektrické systémy a modely ML předpovídají poruchy než k nim dojde, což umožňuje plánované zásahy namísto nákladných mimořádných událostí.
import { Kafka, Consumer, Producer } from 'kafkajs';
import * as tf from '@tensorflow/tfjs-node';
interface SensorReading {
sensorId: string;
buildingId: string;
equipmentId: string;
equipmentType: 'hvac' | 'elevator' | 'pump' | 'electrical_panel' | 'boiler';
timestamp: string;
metrics: {
temperature?: number; // gradi Celsius
vibration?: number; // m/s^2
current?: number; // Ampere
pressure?: number; // bar
humidity?: number; // %RH
runningHours?: number; // ore di funzionamento totali
};
}
interface MaintenancePrediction {
equipmentId: string;
failureProbability: number; // 0-1
estimatedDaysToFailure: number;
alertLevel: 'normal' | 'watch' | 'warning' | 'critical';
recommendedAction: string;
confidenceScore: number;
}
export class PredictiveMaintenanceEngine {
private model: tf.LayersModel | null = null;
private kafka: Kafka;
constructor(private readonly kafkaBrokers: string[]) {
this.kafka = new Kafka({
clientId: 'predictive-maintenance',
brokers: kafkaBrokers,
});
}
async initialize(): Promise<void> {
// Carica modello pre-addestrato (LSTM per time series)
this.model = await tf.loadLayersModel('file://./models/equipment-failure/model.json');
console.log('Predictive maintenance model loaded');
}
async startMonitoring(buildingId: string): Promise<void> {
const consumer = this.kafka.consumer({ groupId: 'maintenance-engine' });
await consumer.connect();
await consumer.subscribe({
topic: `building.${buildingId}.sensors`,
fromBeginning: false,
});
const readingBuffer = new Map<string, SensorReading[]>();
await consumer.run({
eachMessage: async ({ message }) => {
const reading: SensorReading = JSON.parse(message.value!.toString());
// Buffer rolling window (ultime 24h di dati = ~1440 letture a 1/min)
const buffer = readingBuffer.get(reading.equipmentId) ?? [];
buffer.push(reading);
if (buffer.length > 1440) buffer.shift();
readingBuffer.set(reading.equipmentId, buffer);
// Predici ogni 30 minuti (non ad ogni lettura per evitare overhead)
const minuteOfDay = new Date().getMinutes();
if (minuteOfDay % 30 === 0 && buffer.length >= 100) {
const prediction = await this.predictFailure(reading.equipmentId, buffer);
await this.handlePrediction(prediction, reading.equipmentId);
}
},
});
}
private async predictFailure(
equipmentId: string,
readings: SensorReading[]
): Promise<MaintenancePrediction> {
if (!this.model) throw new Error('Model not initialized');
// Feature engineering: estrai statistiche rolling dal buffer
const features = this.extractFeatures(readings);
const inputTensor = tf.tensor3d([features], [1, features.length, features[0].length]);
const prediction = this.model.predict(inputTensor) as tf.Tensor;
const [failureProb] = await prediction.data();
tf.dispose([inputTensor, prediction]);
const daysToFailure = Math.max(0, Math.round((1 - failureProb) * 30));
const alertLevel = this.getAlertLevel(failureProb);
return {
equipmentId,
failureProbability: failureProb,
estimatedDaysToFailure: daysToFailure,
alertLevel,
recommendedAction: this.getRecommendedAction(alertLevel, daysToFailure),
confidenceScore: 0.85, // in produzione: calibrated confidence
};
}
private extractFeatures(readings: SensorReading[]): number[][] {
// Finestre temporali: ultimi 60 min, 6h, 24h
const windows = [60, 360, 1440];
return windows.map(windowSize => {
const windowed = readings.slice(-windowSize);
const temperatures = windowed.map(r => r.metrics.temperature ?? 0);
const vibrations = windowed.map(r => r.metrics.vibration ?? 0);
const currents = windowed.map(r => r.metrics.current ?? 0);
return [
this.mean(temperatures), this.std(temperatures), this.max(temperatures),
this.mean(vibrations), this.std(vibrations), this.max(vibrations),
this.mean(currents), this.std(currents), this.max(currents),
windowed.length, // dati disponibili in finestra
];
}).flat().map(() => [0]); // Placeholder: in realta ha shape (timeSteps, features)
}
private getAlertLevel(prob: number): MaintenancePrediction['alertLevel'] {
if (prob >= 0.85) return 'critical';
if (prob >= 0.65) return 'warning';
if (prob >= 0.40) return 'watch';
return 'normal';
}
private getRecommendedAction(level: string, days: number): string {
switch (level) {
case 'critical': return `Intervento urgente entro ${days < 1 ? 'oggi' : `${days} giorni`}. Contattare tecnico ora.`;
case 'warning': return `Pianificare ispezione entro ${days} giorni. Monitoraggio intensivo attivato.`;
case 'watch': return 'Aumentare frequenza ispezioni periodiche. Monitorare trend.';
default: return 'Operativo nella norma. Continuare manutenzione ordinaria.';
}
}
private async handlePrediction(
prediction: MaintenancePrediction,
equipmentId: string
): Promise<void> {
if (prediction.alertLevel === 'normal') return;
// Invia alert via Kafka per processing downstream
const producer = this.kafka.producer();
await producer.connect();
await producer.send({
topic: 'maintenance.alerts',
messages: [{
key: equipmentId,
value: JSON.stringify({
...prediction,
timestamp: new Date().toISOString(),
source: 'predictive-maintenance-engine',
}),
}],
});
await producer.disconnect();
}
private mean(arr: number[]): number {
return arr.length ? arr.reduce((a, b) => a + b, 0) / arr.length : 0;
}
private std(arr: number[]): number {
const m = this.mean(arr);
return Math.sqrt(arr.reduce((a, b) => a + (b - m) ** 2, 0) / arr.length);
}
private max(arr: number[]): number {
return arr.length ? Math.max(...arr) : 0;
}
}
Dynamická cena: Optimalizace pronájmu s ML
Dynamická cena pronájmu využívá modely ML k maximalizaci celkových výnosů portfolia, vyrovnání obsazenosti a výnosu na jednotku. Je inspirován průmyslovými modely Revenue Management hotelový průmysl, přizpůsobený specifikům trhu s nemovitostmi (delší smlouvy, menší obrat časté).
interface RentalPricingInput {
propertyId: string;
currentRent: number;
currentLeaseEnd: string;
unitFeatures: {
squareMeters: number;
floor: number;
view: 'garden' | 'street' | 'panoramic';
lastRenovated: number; // anno ultima ristrutturazione
parkingSpots: number;
};
marketData: {
comparableRents: number[]; // affitti unita simili nella zona
avgDaysOnMarket: number; // giorni medi per affittare unita simili
vacancyRateNeighborhood: number; // % vacancy nel quartiere
demandIndex: number; // 0-1, domanda relativa
seasonalityFactor: number; // 0.8-1.2 (alta stagione = luglio/agosto)
};
tenantProfile?: {
paymentHistory: 'excellent' | 'good' | 'fair';
yearsAsReliableTenant: number;
};
}
export function calculateOptimalRent(input: RentalPricingInput): {
recommendedRent: number;
priceRange: { min: number; max: number };
occupancyForecast: number;
annualRevenueProjection: number;
reasoning: string[];
} {
const { unitFeatures, marketData, currentRent, tenantProfile } = input;
const reasoning: string[] = [];
// 1. Market baseline: mediana comparabili
const sortedComps = [...marketData.comparableRents].sort((a, b) => a - b);
const median = sortedComps[Math.floor(sortedComps.length / 2)];
let suggestedRent = median;
reasoning.push(`Baseline mercato (mediana comparabili): €${median}/mese`);
// 2. Adjustment per domanda/offerta
if (marketData.demandIndex > 0.7) {
const premium = Math.round(suggestedRent * 0.05);
suggestedRent += premium;
reasoning.push(`+${premium} per domanda elevata (indice: ${(marketData.demandIndex * 100).toFixed(0)}%)`);
} else if (marketData.vacancyRateNeighborhood > 0.08) {
const discount = Math.round(suggestedRent * 0.03);
suggestedRent -= discount;
reasoning.push(`-${discount} per alta vacancy nel quartiere (${(marketData.vacancyRateNeighborhood * 100).toFixed(0)}%)`);
}
// 3. Stagionalita
suggestedRent = Math.round(suggestedRent * marketData.seasonalityFactor);
if (marketData.seasonalityFactor !== 1) {
reasoning.push(`Fattore stagionale: x${marketData.seasonalityFactor}`);
}
// 4. Bonus ritenzione tenant di qualità
if (tenantProfile?.paymentHistory === 'excellent' && tenantProfile.yearsAsReliableTenant >= 2) {
const loyaltyDiscount = Math.round(suggestedRent * 0.02);
suggestedRent -= loyaltyDiscount;
reasoning.push(`-${loyaltyDiscount} loyalty discount per tenant eccellente (${tenantProfile.yearsAsReliableTenant} anni)`);
}
// 5. Calcola occupancy forecast basata sul pricing
const priceToMedianRatio = suggestedRent / median;
const baseOccupancy = 0.95 - (marketData.vacancyRateNeighborhood * 0.5);
const occupancyForecast = Math.max(0.5, Math.min(1.0,
baseOccupancy * (1 - (priceToMedianRatio - 1) * 0.8)
));
// 6. Revenue annuale atteso
const annualRevenue = suggestedRent * 12 * occupancyForecast;
return {
recommendedRent: suggestedRent,
priceRange: {
min: Math.round(suggestedRent * 0.95),
max: Math.round(suggestedRent * 1.05),
},
occupancyForecast,
annualRevenueProjection: Math.round(annualRevenue),
reasoning,
};
}
Portfolio Dashboard a automatické KPI
Autonomní systém správy nemovitostí musí poskytovat správcům nemovitostí a investorům přehled o výkonnosti portfolia v reálném čase.
interface PortfolioKPIs {
// Occupancy
totalUnits: number;
occupiedUnits: number;
occupancyRate: number; // %
avgDaysVacant: number;
// Revenue
grossPotentialRent: number; // se 100% occupato
effectiveGrossIncome: number; // dopo vacancy e concessions
netOperatingIncome: number; // dopo operating expenses
capRate: number; // NOI / market value
cashOnCashReturn: number; // after debt service
// Manutenzione
openWorkOrders: number;
avgResolutionDays: number;
preventiveVsReactiveRatio: number; // target >0.7 (70% preventive)
maintenanceCostPerUnit: number;
// Tenant
avgTenancyDuration: number; // mesi
turnoverRate: number; // %/anno
rentCollectionRate: number; // %
tenantSatisfactionScore: number; // 1-10
}
export async function calculatePortfolioKPIs(
db: Pool,
portfolioId: string,
periodStart: Date,
periodEnd: Date
): Promise<PortfolioKPIs> {
// Query aggregata su database
const result = await db.query(
`SELECT
COUNT(u.id) AS total_units,
COUNT(CASE WHEN l.status = 'active' THEN 1 END) AS occupied_units,
SUM(u.market_rent) AS gross_potential_rent,
SUM(CASE WHEN l.status = 'active' THEN l.monthly_rent ELSE 0 END) AS effective_gross_income,
AVG(CASE WHEN l.status = 'active' THEN
EXTRACT(MONTH FROM AGE(NOW(), l.start_date))
END) AS avg_tenancy_months
FROM units u
LEFT JOIN leases l ON u.id = l.unit_id AND l.status IN ('active', 'expired')
WHERE u.portfolio_id = $1`,
[portfolioId]
);
const row = result.rows[0];
const maintenanceResult = await db.query(
`SELECT
COUNT(*) FILTER (WHERE status = 'open') AS open_work_orders,
AVG(EXTRACT(DAY FROM (completed_at - created_at))) FILTER (WHERE status = 'closed') AS avg_resolution_days,
COUNT(*) FILTER (WHERE request_type = 'preventive') * 1.0 / NULLIF(COUNT(*), 0) AS preventive_ratio
FROM work_orders
WHERE portfolio_id = $1 AND created_at BETWEEN $2 AND $3`,
[portfolioId, periodStart, periodEnd]
);
const mRow = maintenanceResult.rows[0];
const occupancyRate = row.occupied_units / row.total_units;
const noi = row.effective_gross_income * 12 * 0.65; // approx 35% operating expenses
return {
totalUnits: parseInt(row.total_units),
occupiedUnits: parseInt(row.occupied_units),
occupancyRate,
avgDaysVacant: 21, // In produzione: calcolo reale
grossPotentialRent: parseFloat(row.gross_potential_rent),
effectiveGrossIncome: parseFloat(row.effective_gross_income),
netOperatingIncome: noi,
capRate: noi / (parseFloat(row.gross_potential_rent) * 150), // approx valuation
cashOnCashReturn: 0.08, // In produzione: calcolo reale con debt service
openWorkOrders: parseInt(mRow.open_work_orders ?? '0'),
avgResolutionDays: parseFloat(mRow.avg_resolution_days ?? '5'),
preventiveVsReactiveRatio: parseFloat(mRow.preventive_ratio ?? '0.4'),
maintenanceCostPerUnit: 200, // In produzione: calcolo reale
avgTenancyDuration: parseFloat(row.avg_tenancy_months ?? '24'),
turnoverRate: (1 / (parseFloat(row.avg_tenancy_months ?? '24') / 12)),
rentCollectionRate: 0.97, // In produzione: calcolo da transactions
tenantSatisfactionScore: 7.8, // In produzione: da survey system
};
}
ROI a metriky automatizace
| Proces | Manuál hodin/rok | Hodiny/rok AI | Úspory |
|---|---|---|---|
| Správa požadavků nájemců | 800 h (200 jednotek) | 50h (dozor) | -94 % |
| Plánování údržby | 300 h | 20h | -93 % |
| Vymáhání nájemného a následné sledování | 200h | 10h | -95 % |
| Měsíční zprávy o portfoliu | 120h | 2h | -98 % |
| Reaktivní -> prediktivní údržba | 15 000 EUR/rok | 9 000 EUR/rok | -40 % |
Lidský dohled: AI rozšiřuje, nikoli nahrazuje
I přes pokročilou automatizaci zůstává manažer lidského majetku nezbytný pro rozhodování komplexní: vystěhování, právní spory, renegociace smluv, vztahy s klíčovými dodavateli. Vždy stanovte jasné prahové hodnoty pro eskalaci a zajistěte, aby rozhodnutí, která tato rozhodnutí kontrolují, byla přezkoumána člověkem ovlivnit pohodu nájemníků. Umělá inteligence by měla posilovat schopnosti manažerů, nikoli je eliminovat.
Závěry
Autonomní správa majetku pomocí agentů AI již není sci-fi: je to realizovatelná realita dnes s vyspělými nástroji, jako je GPT-4o pro chatbota nájemce, TensorFlow pro prediktivní údržbu a Apache Kafka pro streamování dat. ROI je měřitelná a významná: portfolio 200 jednotek lze je spravovat se stejnou efektivitou jako 500 jednotek, což uvolňuje manažery na činnost vysoká hodnota (akvizice, vztahy s investory, růst portfolia).







