diff --git a/backend/app/ai_models/california.py b/backend/app/ai_models/california.py new file mode 100644 index 0000000..4a9cef0 --- /dev/null +++ b/backend/app/ai_models/california.py @@ -0,0 +1,33 @@ +from sklearn.datasets import fetch_california_housing +from sklearn.linear_model import LinearRegression +from sklearn.model_selection import train_test_split +import joblib +import pandas as pd + +# Load dataset +data = fetch_california_housing() +df = pd.DataFrame(data.data, columns=data.feature_names) +df['target'] = data.target # in 100k USD + +# Engineer features +df['square_feet'] = df['AveRooms'] * 350 +df['bedrooms'] = df['AveBedrms'] +df['bathrooms'] = df['AveRooms'] * 0.2 + +# Clean bathrooms +df['bathrooms'] = df['bathrooms'].clip(lower=1) + +X = df[['square_feet', 'bedrooms', 'bathrooms']] +y = df['target'] + +# Train/test split +X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42) + +# Train model +model = LinearRegression() +model.fit(X_train, y_train) + +# Need to be tested of course..: ) + +# Save model +joblib.dump(model, 'price_predictor.pkl') diff --git a/backend/app/ai_models/price_predictor.pkl b/backend/app/ai_models/price_predictor.pkl new file mode 100644 index 0000000..86918a7 Binary files /dev/null and b/backend/app/ai_models/price_predictor.pkl differ diff --git a/backend/app/dtos/house/house_create_request.py b/backend/app/dtos/house/house_create_request.py new file mode 100644 index 0000000..ca07085 --- /dev/null +++ b/backend/app/dtos/house/house_create_request.py @@ -0,0 +1,34 @@ +from pydantic import BaseModel, Field + + +class HouseCreateRequest(BaseModel): + address: str = Field( + ..., + min_length=1, + max_length=255, + description="House address", + examples=["123 Main St"], + ) + city: str = Field( + ..., description="City where the house is located", examples=["Springfield"] + ) + country: str = Field( + ..., description="Country where the house is located", examples=["USA"] + ) + price: float = Field(..., description="Price of the house", examples=[250000.00]) + description: str = Field( + ..., + description="Description of the house", + examples=["A beautiful 3-bedroom house"], + ) + square_feet: float = Field( + ..., + description="Square footage of the house", + examples=[1500.00], + ) + bedrooms: int = Field( + ..., description="Number of bedrooms in the house", examples=[3] + ) + bathrooms: float = Field( + ..., description="Number of bathrooms in the house", examples=[2.5] + ) diff --git a/backend/app/dtos/house/house_create_response.py b/backend/app/dtos/house/house_create_response.py new file mode 100644 index 0000000..5f74154 --- /dev/null +++ b/backend/app/dtos/house/house_create_response.py @@ -0,0 +1,5 @@ +from pydantic import BaseModel + + +class HouseCreateResponse(BaseModel): + id: str diff --git a/backend/app/dtos/house/house_features.py b/backend/app/dtos/house/house_features.py new file mode 100644 index 0000000..271ec25 --- /dev/null +++ b/backend/app/dtos/house/house_features.py @@ -0,0 +1,8 @@ +from pydantic import BaseModel, Field +from typing import Optional + +class HouseFeatures(BaseModel): + square_feet: float = Field(..., description="Total square feet of the house") + bedrooms: int = Field(..., description="Number of bedrooms") + bathrooms: float = Field(..., description="Number of bathrooms") + number_of_floors: Optional[int] = Field(default=None, description="Number of floors") \ No newline at end of file diff --git a/backend/app/dtos/house/house_predict_request.py b/backend/app/dtos/house/house_predict_request.py new file mode 100644 index 0000000..68c216e --- /dev/null +++ b/backend/app/dtos/house/house_predict_request.py @@ -0,0 +1,7 @@ +from pydantic import BaseModel + + +class HousePricePredictionRequest(BaseModel): + square_feet: float + bedrooms: int + bathrooms: float diff --git a/backend/app/dtos/house/house_predict_response.py b/backend/app/dtos/house/house_predict_response.py new file mode 100644 index 0000000..0e1e5ab --- /dev/null +++ b/backend/app/dtos/house/house_predict_response.py @@ -0,0 +1,5 @@ +from pydantic import BaseModel + + +class HousePricePredictionResponse(BaseModel): + predicted_price: float diff --git a/backend/app/dtos/house/house_response.py b/backend/app/dtos/house/house_response.py new file mode 100644 index 0000000..b1026a2 --- /dev/null +++ b/backend/app/dtos/house/house_response.py @@ -0,0 +1,10 @@ +from pydantic import BaseModel + + +class HouseResponse(BaseModel): + id: str + description: str + address: str + city: str + country: str + price: float \ No newline at end of file diff --git a/backend/app/dtos/house/houses_list_response.py b/backend/app/dtos/house/houses_list_response.py new file mode 100644 index 0000000..47f5429 --- /dev/null +++ b/backend/app/dtos/house/houses_list_response.py @@ -0,0 +1,6 @@ +from pydantic import BaseModel + +from backend.app.dtos.house.house_response import HouseResponse + +class HousesListResponse(BaseModel): + houses: list[HouseResponse] diff --git a/backend/app/dtos/owner/owner_detail_response.py b/backend/app/dtos/owner/owner_detail_response.py new file mode 100644 index 0000000..b928c01 --- /dev/null +++ b/backend/app/dtos/owner/owner_detail_response.py @@ -0,0 +1,7 @@ +from pydantic import BaseModel + + +class OwnerDetailResponse(BaseModel): + id: str + user_id: str + email: str diff --git a/backend/app/dtos/owner/owner_list_response.py b/backend/app/dtos/owner/owner_list_response.py new file mode 100644 index 0000000..03d1aba --- /dev/null +++ b/backend/app/dtos/owner/owner_list_response.py @@ -0,0 +1,10 @@ +from pydantic import BaseModel + + +class OwnerResponse(BaseModel): + id: str + user_id: str + + +class OwnerListResponse(BaseModel): + owners: list[OwnerResponse] diff --git a/backend/app/dtos/user/user_list_response.py b/backend/app/dtos/user/user_list_response.py new file mode 100644 index 0000000..3c5b9c1 --- /dev/null +++ b/backend/app/dtos/user/user_list_response.py @@ -0,0 +1,6 @@ +from pydantic import BaseModel + +from backend.app.dtos.user.user_response import UserResponse + +class UserListResponse(BaseModel): + users: list[UserResponse] \ No newline at end of file diff --git a/backend/app/dtos/user/user_response.py b/backend/app/dtos/user/user_response.py new file mode 100644 index 0000000..fe3f880 --- /dev/null +++ b/backend/app/dtos/user/user_response.py @@ -0,0 +1,6 @@ +from pydantic import BaseModel + + +class UserResponse(BaseModel): + id: str + email: str \ No newline at end of file diff --git a/backend/app/models/house.py b/backend/app/models/house.py index 84ce0bf..e39200f 100644 --- a/backend/app/models/house.py +++ b/backend/app/models/house.py @@ -1,6 +1,11 @@ +from typing import Optional, TYPE_CHECKING from uuid import UUID, uuid4 -from sqlmodel import Field, SQLModel +from sqlmodel import Field, Relationship, SQLModel + +if TYPE_CHECKING: + from backend.app.models.owner import Owner + class House(SQLModel, table=True): @@ -14,3 +19,4 @@ class House(SQLModel, table=True): square_feet: float = Field() bedrooms: int = Field() bathrooms: float = Field() + owner: Optional["Owner"] = Relationship(back_populates="houses") diff --git a/backend/app/models/owner.py b/backend/app/models/owner.py index a2200b1..d3aa0ae 100644 --- a/backend/app/models/owner.py +++ b/backend/app/models/owner.py @@ -1,8 +1,17 @@ +from typing import Optional, TYPE_CHECKING from uuid import UUID, uuid4 -from sqlmodel import Field, SQLModel +from sqlmodel import Field, Relationship, SQLModel + +if TYPE_CHECKING: + from backend.app.models.house import House + from backend.app.models.user import User class Owner(SQLModel, table=True): id: UUID = Field(default_factory=uuid4, primary_key=True) user_id: UUID = Field(foreign_key="user.id", unique=True) + + # Relationship + houses: list["House"] = Relationship(back_populates="owner") + user: Optional["User"] = Relationship(back_populates="owner") \ No newline at end of file diff --git a/backend/app/models/user.py b/backend/app/models/user.py index 8692ddd..5190df4 100644 --- a/backend/app/models/user.py +++ b/backend/app/models/user.py @@ -1,9 +1,16 @@ +from typing import Optional, TYPE_CHECKING from uuid import UUID, uuid4 -from sqlmodel import Field, SQLModel +from sqlmodel import Field, Relationship, SQLModel + +if TYPE_CHECKING: + from backend.app.models.owner import Owner class User(SQLModel, table=True): id: UUID = Field(default_factory=lambda: uuid4(), primary_key=True) email: str = Field(unique=True, nullable=False) password_hash: str = Field(nullable=False) + + # Relationships + owner: Optional["Owner"] = Relationship(back_populates="user") diff --git a/backend/app/repositories/house_repository.py b/backend/app/repositories/house_repository.py index 21c2338..682504e 100644 --- a/backend/app/repositories/house_repository.py +++ b/backend/app/repositories/house_repository.py @@ -8,7 +8,6 @@ from sqlmodel import asc, desc, select from ..models.house import House from ..providers.db_provider import get_session - class HouseRepository: def __init__(self, session: Annotated[AsyncSession, Depends(get_session)]) -> None: self.session = session @@ -34,6 +33,11 @@ class HouseRepository: result = await self.session.execute(statement) return result.scalar_one_or_none() + async def get_by_user_id(self, user_id: UUID): + statement = select(House).where(House.owner_user_id == user_id) + result = await self.session.execute(statement) + return result.scalars().all() + async def save(self, house: House) -> None: """ Save a house to the database. If a house with that ID already exists, do an upsert. diff --git a/backend/app/repositories/owner_repository.py b/backend/app/repositories/owner_repository.py index d3151e1..b415356 100644 --- a/backend/app/repositories/owner_repository.py +++ b/backend/app/repositories/owner_repository.py @@ -5,6 +5,9 @@ from fastapi import Depends from sqlalchemy.ext.asyncio.session import AsyncSession from sqlmodel import select +from backend.app.models.house import House +from backend.app.models.user import User + from ..models.owner import Owner from ..providers.db_provider import get_session @@ -27,7 +30,26 @@ class OwnerRepository: statement = select(Owner).where(Owner.user_id == user_id) result = await self.session.execute(statement) return result.scalar_one_or_none() + + async def get_details_by_house_id(self, house_id: UUID): + statement = ( + select(Owner, User) + .join(User, Owner.user_id == User.id) + .join(House, House.owner_user_id == Owner.user_id) + .where(House.id == house_id) + ) + result = await self.session.execute(statement) + row = result.first() + + if row: + owner, user = row + return { + "owner": owner, + "user": user + } + return None + async def save(self, owner: Owner) -> None: """ Save a owner to the database. If an owner with that ID already exists, do an upsert. diff --git a/backend/app/routers/houses.py b/backend/app/routers/houses.py index a9bc95c..71997d7 100644 --- a/backend/app/routers/houses.py +++ b/backend/app/routers/houses.py @@ -2,15 +2,19 @@ from typing import Annotated, Literal from fastapi import APIRouter, Depends -from ..dtos.house_create_request import HouseCreateRequest -from ..dtos.house_create_response import HouseCreateResponse -from ..dtos.houses_list_response import HouseResponse, HousesListResponse +from backend.app.dtos.house.house_create_request import HouseCreateRequest +from backend.app.dtos.house.house_create_response import HouseCreateResponse +from backend.app.dtos.house.house_features import HouseFeatures +from backend.app.dtos.house.house_predict_request import HousePricePredictionRequest +from backend.app.dtos.house.house_predict_response import HousePricePredictionResponse +from backend.app.dtos.house.house_response import HouseResponse +from backend.app.dtos.house.houses_list_response import HousesListResponse from ..models.house import House from ..models.owner import Owner from ..providers.auth_provider import AuthContext from ..repositories.house_repository import HouseRepository from ..repositories.owner_repository import OwnerRepository - +from ..services.house_price_predictor import HousePricePredictor router = APIRouter() @@ -69,4 +73,22 @@ async def get_all_houses( for house in all_houses ] + return HousesListResponse(houses=house_responses) + +@router.post("/predict-price") +async def predict_house_price( + body: HouseFeatures, + price_predictor: Annotated[HousePricePredictor, Depends()], + ) -> HousePricePredictionResponse: + """ + Predict the price of a house based on its features. + """ + predicted_price = await price_predictor.predict_california( + square_feet=body.square_feet, + bedrooms=body.bedrooms, + bathrooms=body.bathrooms + ) + + return HousePricePredictionResponse(predicted_price=predicted_price) + diff --git a/backend/app/routers/owners.py b/backend/app/routers/owners.py index 9eb9b98..9b5c465 100644 --- a/backend/app/routers/owners.py +++ b/backend/app/routers/owners.py @@ -1,15 +1,14 @@ from typing import Annotated -from fastapi import APIRouter, Depends +from fastapi import APIRouter, Depends, HTTPException -from ..dtos.owner_detail_response import OwnerDetailResponse -from ..dtos.owner_list_response import OwnerListResponse, OwnerResponse +from ..dtos.owner.owner_detail_response import OwnerDetailResponse +from ..dtos.owner.owner_list_response import OwnerListResponse, OwnerResponse from ..repositories.owner_repository import OwnerRepository from ..repositories.user_repository import UserRepository router = APIRouter() - @router.get("") async def get_owners( owner_repository: Annotated[OwnerRepository, Depends()], @@ -22,7 +21,6 @@ async def get_owners( return OwnerListResponse(owners=owners_response) - @router.get("/{id}") async def get_owner( id: str, @@ -35,3 +33,22 @@ async def get_owner( return OwnerDetailResponse( id=str(owner.id), user_id=str(owner.user_id), email=user.email ) + +@router.get("/byhouse/{house_id}") +async def get_owner_by_house_id( + house_id: str, + owner_repository: Annotated[OwnerRepository, Depends()], +) -> OwnerDetailResponse: + result = await owner_repository.get_details_by_house_id(house_id) + + if result is None: + raise HTTPException(status_code=404, detail="House or owner not found") + + owner = result["owner"] + user = result["user"] + + return OwnerDetailResponse( + id=str(owner.id), + user_id=str(owner.user_id), + email=str(user.email) + ) diff --git a/backend/app/services/house_price_predictor.py b/backend/app/services/house_price_predictor.py index 6243ff8..146fde6 100644 --- a/backend/app/services/house_price_predictor.py +++ b/backend/app/services/house_price_predictor.py @@ -1,16 +1,17 @@ +import os +import joblib +import numpy as np + +from backend.app.dtos.house.house_features import HouseFeatures + class HousePricePredictor: """ Mock ML model that predicts house prices. In a real scenario, this would load a trained model. """ - - async def predict( - self, square_feet: float, bedrooms: int, bathrooms: float - ) -> float: - base_price = square_feet * 200 - bedroom_value = bedrooms * 25000 - bathroom_value = bathrooms * 15000 - - predicted_price = base_price + bedroom_value + bathroom_value - - return predicted_price + def __init__(self): + self.model = joblib.load("backend/app/ai_models/price_predictor.pkl") + + def predict(self, features: HouseFeatures) -> float: + X = np.array([[features.square_feet, features.bedrooms, features.bathrooms]]) + return self.model.predict(X)[0] * 100000 \ No newline at end of file diff --git a/backend/requirements.txt b/backend/requirements.txt index b137471..9104de0 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -8,3 +8,5 @@ python-dotenv pg8000 asyncpg greenlet +botbuilder-core +openai