Source code for are.simulation.apps.apartment_listing

# Copyright (c) Meta Platforms, Inc. and affiliates.
# All rights reserved.
#
# This source code is licensed under the terms described in the LICENSE file in
# the root directory of this source tree.


import json
import logging
import uuid
from dataclasses import dataclass, field
from typing import Any

from are.simulation.apps.app import App
from are.simulation.tool_utils import app_tool, data_tool, env_tool
from are.simulation.types import EventType, OperationType, event_registered
from are.simulation.utils import get_state_dict, type_check, uuid_hex

logger = logging.getLogger(__name__)


[docs] @dataclass class Apartment: name: str location: str zip_code: str price: float bedrooms: int bathrooms: int property_type: str square_footage: int furnished_status: str | None = "" floor_level: str | None = "" pet_policy: str | None = "" lease_term: str | None = "" apartment_id: str = field(default_factory=lambda: uuid.uuid4().hex) amenities: list[str] | None = field(default_factory=list) saved: bool = False def __post_init__(self): if self.apartment_id is None or len(self.apartment_id) == 0: self.apartment_id = uuid.uuid4().hex
[docs] @dataclass class ApartmentListingApp(App): """ A real estate application that manages and manipulates apartment listings. This class provides functionality for creating, reading, updating, and deleting apartment listings, as well as various utility methods for searching and filtering apartments based on multiple criteria. The app maintains apartments in a dictionary where each apartment is identified by a unique apartment_id. All price-related inputs should be in float format, and the number of bedrooms and bathrooms should be integers. Key Features: - Apartment Management: Add, update, delete, and retrieve apartment listings - Search Functionality: Filter apartments based on various criteria including location, price, size, and amenities - Favorites Management: Save and remove favorite apartments for quick access - State Management: Save and load application state to retain apartment listings and favorites Notes: - All apartment attributes are expected to adhere to specific data types (e.g., price as float, bedrooms and bathrooms as integers) - Apartment IDs are automatically generated when creating new apartments - The class supports state persistence through save/load operations from a file - Search operations are case-insensitive for string comparisons - Amenities filtering checks if all specified amenities are present in the apartment's amenities list """ name: str | None = None # state of apartments - key is apartment ID and value is a dictionary of Apartment apartments: dict[str, Apartment] = field(default_factory=dict) # user saved apartments (favorites) - set of apartment IDs saved_apartments: list[str] = field(default_factory=list) def __post_init__(self): super().__init__(self.name) if len(self.saved_apartments) > 0: for apt_id in self.saved_apartments: self.apartments[apt_id].saved = True else: self.saved_apartments = [ key for key in self.apartments if self.apartments[key].saved ]
[docs] def get_state(self) -> dict[str, Any] | None: return get_state_dict(self, ["apartments", "saved_apartments"])
[docs] def load_state(self, state_dict: dict[str, Any]): self.load_apartments_from_dict(state_dict["apartments"]) self.saved_apartments = state_dict["saved_apartments"] for apt_id in self.saved_apartments: self.apartments[apt_id].saved = True
[docs] def reset(self): super().reset() self.apartments = {} self.saved_apartments = []
[docs] def load_apartments_from_file(self, path: str): try: with open(path) as f: apts_dict = json.load(f) self.load_apartments_from_dict(apts_dict) except Exception as e: logger.exception(e)
[docs] def load_apartments_from_dict(self, apartments: dict[str, dict[str, Any]]): for apt in apartments: apartments[apt]["apartment_id"] = apt self.apartments[apt] = Apartment(**apartments[apt])
def _get_apartment(self, apartment_id: str) -> Apartment | None: """ given an apartment_id, return the apartment details """ return self.apartments.get(apartment_id)
[docs] @type_check @data_tool() @env_tool() @event_registered(operation_type=OperationType.WRITE, event_type=EventType.ENV) def add_new_apartment( self, name: str, location: str, zip_code: str, price: float, number_of_bedrooms: int, number_of_bathrooms: int, square_footage: int, property_type: str = "", furnished_status: str = "", floor_level: str = "", pet_policy: str = "", lease_term: str = "", amenities: list[str] | None = None, ) -> str: """ Adds a new apartment to the apartment listing. :param name: name of the apartment :param location: desired location :param zip_code: zip code of the apartment location :param price: price of the apartment :param number_of_bedrooms: number of bedrooms :param number_of_bathrooms: number of bathrooms :param property_type: type of property (Apartment, Condo, etc.) :param square_footage: minimum square footage :param furnished_status: Furnished, Unfurnished, or Semi-furnished :param floor_level: Ground floor, Upper floors, Penthouse, Basement :param pet_policy: Pets allowed, No pets, Cats allowed, Dogs allowed :param lease_term: Month-to-month, 6 months, 1 year, Long term lease :param amenities: list of desired amenities :returns: apartment_id of the apartment just added, if successful, otherwise raise Exception. """ if amenities is None: amenities = [] apartment_id = uuid_hex(self.rng) self.apartments[apartment_id] = Apartment( name=name, location=location, zip_code=zip_code, price=price, bedrooms=number_of_bedrooms, bathrooms=number_of_bathrooms, property_type=property_type, square_footage=square_footage, furnished_status=furnished_status, floor_level=floor_level, pet_policy=pet_policy, lease_term=lease_term, amenities=amenities, apartment_id=apartment_id, ) return apartment_id
[docs] @type_check @data_tool() @env_tool() @event_registered(operation_type=OperationType.WRITE, event_type=EventType.ENV) def update_apartment( self, apartment_id: str, new_price: float, ) -> str: """ Updates an existing apartment in the apartment listing. :param apartment_id: apartment id to update :param new_price: new price of the apartment :returns: apartment_id of the apartment just updated, if successful, otherwise raise Exception. """ if apartment_id not in self.apartments: raise ValueError("Apartment does not exist") self.apartments[apartment_id].price = new_price return apartment_id
[docs] @type_check @data_tool() @env_tool() @event_registered(operation_type=OperationType.WRITE, event_type=EventType.ENV) def delete_apartment(self, apartment_id: str) -> None: """ Deletes a specific apartment by apartment_id. :param apartment_id: apartment id to delete :returns: apartment_id of the apartment just deleted, if successful, otherwise raise Exception. """ if apartment_id in self.apartments: del self.apartments[apartment_id] else: raise ValueError("Apartment does not exist.")
[docs] @app_tool() @data_tool() @event_registered(operation_type=OperationType.READ) def list_all_apartments(self) -> dict[str, Any]: """ List all apartments in the catalog. :returns: apartment details """ return self.apartments
[docs] @type_check @app_tool() @data_tool() @event_registered(operation_type=OperationType.READ) def get_apartment_details(self, apartment_id: str) -> Apartment: """ Get apartment details for a given apartment id. :param apartment_id: apartment id to get details for :returns: apartment details """ if apartment_id not in self.apartments: raise ValueError("Apartment does not exist") return self.apartments[apartment_id]
[docs] @type_check @app_tool(llm_formatter=lambda x: "Successfully saved apartment") @data_tool() @event_registered(operation_type=OperationType.WRITE) def save_apartment(self, apartment_id: str) -> None: """ Save an apartment to favorites if not already saved, otherwise do nothing. :param apartment_id: apartment id to save """ if apartment_id not in self.apartments: raise ValueError("Apartment does not exist") if apartment_id not in self.saved_apartments: self.saved_apartments.append(apartment_id) self.apartments[apartment_id].saved = True
[docs] @type_check @app_tool(llm_formatter=lambda x: "Successfully removed apartment from favorites") @data_tool() @event_registered(operation_type=OperationType.WRITE) def remove_saved_apartment(self, apartment_id: str) -> None: """ Remove an apartment from favorites. :param apartment_id: apartment id to remove """ if apartment_id not in self.saved_apartments: raise ValueError("Apartment not in saved list") self.saved_apartments.remove(apartment_id) self.apartments[apartment_id].saved = False
[docs] @app_tool() @data_tool() @event_registered(operation_type=OperationType.READ) def list_saved_apartments(self) -> dict[str, Apartment]: """ List apartments saved to favorites. :returns: Dictionary of saved apartments: apartment_id -> apartment """ return {apt_id: self.apartments[apt_id] for apt_id in self.saved_apartments}
[docs] @type_check @app_tool() @data_tool() @event_registered(operation_type=OperationType.READ) def search_apartments( self, name: str | None = None, location: str | None = None, zip_code: str | None = None, min_price: int | float | None = None, max_price: int | float | None = None, number_of_bedrooms: int | None = None, number_of_bathrooms: int | None = None, property_type: str | None = None, square_footage: int | None = None, furnished_status: str | None = None, floor_level: str | None = None, pet_policy: str | None = None, lease_term: str | None = None, amenities: list[str] | None = None, saved_only: bool = False, ) -> dict[str, Apartment]: """ Search for apartments based on multiple filters. :param name: name of the apartment :param location: desired location :param zip_code: zip code of the location :param min_price: minimum price :param max_price: maximum price :param number_of_bedrooms: number of bedrooms :param number_of_bathrooms: number of bathrooms :param property_type: type of property (Apartment, Condo, etc.) :param square_footage: minimum square footage :param furnished_status: Furnished, Unfurnished, or Semi-furnished :param floor_level: Ground floor, Upper floors, Penthouse, Basement :param pet_policy: Pets allowed, No pets, Cats allowed, Dogs allowed :param lease_term: Month-to-month, 6 months, 1 year, Long term lease :param amenities: list of desired amenities :param saved_only: if True, search only saved apartments; if False, search all apartments (default) :returns: filtered list of apartments, dictionary of apartment_id -> apartment """ # Determine apartment set to search if saved_only: apartments_to_search = { apt_id: self.apartments[apt_id] for apt_id in self.saved_apartments } else: apartments_to_search = self.apartments # Prepare case-insensitive search terms name_lower = name.lower() if name else None location_lower = location.lower() if location else None property_type_lower = property_type.lower() if property_type else None furnished_status_lower = furnished_status.lower() if furnished_status else None pet_policy_lower = pet_policy.lower() if pet_policy else None lease_term_lower = lease_term.lower() if lease_term else None amenities_lower = [am.lower() for am in amenities] if amenities else None # Apply filters filtered_apartments = {} for apt_id, apt in apartments_to_search.items(): # Fast numeric filters first if min_price and apt.price < min_price: continue if max_price and apt.price > max_price: continue if number_of_bedrooms and apt.bedrooms != number_of_bedrooms: continue if number_of_bathrooms and apt.bathrooms != number_of_bathrooms: continue if square_footage and apt.square_footage < square_footage: continue # Exact match filters if zip_code and apt.zip_code != zip_code: continue if floor_level and apt.floor_level != floor_level: continue # String filters if name_lower and name_lower not in apt.name.lower(): continue if location_lower and location_lower not in apt.location.lower(): continue if property_type_lower and apt.property_type.lower() != property_type_lower: continue if ( furnished_status_lower and apt.furnished_status and apt.furnished_status.lower() != furnished_status_lower ): continue if ( pet_policy_lower and apt.pet_policy and apt.pet_policy.lower() != pet_policy_lower ): continue if ( lease_term_lower and apt.lease_term and apt.lease_term.lower() != lease_term_lower ): continue # Amenities filter if amenities_lower: apt_amenities_lower = [t.lower() for t in (apt.amenities or [])] if not all(am in apt_amenities_lower for am in amenities_lower): continue filtered_apartments[apt_id] = apt return filtered_apartments
[docs] @dataclass class RentAFlat(ApartmentListingApp): __doc__ = ApartmentListingApp.__doc__ name: str | None = "RentAFlat"