Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
SUPERDOCS_API_KEY="sk_your_api_key_here"
39 changes: 39 additions & 0 deletions use-cases/Sheikh-JamirAlam/comparative-market-analysis/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
.env

# Python-generated files
__pycache__/
*.py[oc]
build/
dist/
wheels/
*.egg-info
.uv-cache/
.pytest_cache/

# Virtual environments
.venv

# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
110 changes: 110 additions & 0 deletions use-cases/Sheikh-JamirAlam/comparative-market-analysis/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# Comparative Market Analysis Generator

This project generates a polished, branded Comparative Market Analysis (CMA) document for real estate agents. An agent provides a subject property, four or more comparable properties, and their branding. The application calculates a suggested price range, builds the document from an HTML template, and uses SuperDocs to produce a styled PDF, DOCX, or HTML document.

## Problem statement

Real estate agents often need to turn scattered property information into a clear, homeowner-friendly listing presentation. Spreadsheet-style data dumps are difficult for sellers to understand and do not communicate the agent's recommendation effectively. This application provides a repeatable workflow for turning property data into a branded CMA with a subject-property summary, photos, comparable-sales table, pricing rationale, and suggested price range.

The property data in this repository is synthetic fixture data. No MLS integration is required.

## Prerequisites

- Python 3.14 or newer
- [`uv`](https://docs.astral.sh/uv/)
- Node.js and Bun for the frontend
- A SuperDocs account and API key

Create a local environment file by copying `.env.example` to `.env` and add your key:

```text
SUPERDOCS_API_KEY="sk_your_api_key_here"
```

Keep the real key out of source control.

## Run a sample from a fixture

From the repository root:

```powershell
cd backend
uv sync
uv run python scripts/run_sample.py --fixture suburban_single_family.json
```

The generated file is saved in `backend/output/`. Other available fixtures (find them in `backend/fixtures/`) are:

```text
condo.json
luxury.json
```

You can choose `pdf`, `docx`, or `html`:

```powershell
uv run python scripts/run_sample.py --fixture condo.json --format docx
```

The script loads the selected JSON fixture (these are all generated data), applies the saved HTML template, sends the document workflow to SuperDocs, and exports the result.

## Use your own dataset

Use two terminals.

In terminal 1, start the FastAPI backend:

```powershell
cd backend
uv sync
uv run uvicorn cma.api:app --reload --port 8000
```

The API is available at `http://localhost:8000`.

In terminal 2, install and start the frontend:

```powershell
cd frontend
bun install
bun run dev
```

Open the local URL printed by Vite, usually `http://localhost:5173`. Enter your branding, subject property, and at least four comparable properties, then submit the form.

You can add your own property data directly in the web form. The frontend sends it to the backend, which creates the CMA using the same SuperDocs workflow as the sample script.

Document generation may take a while — large or complex requests can take from several seconds to several minutes depending on SuperDocs response time. When the request completes, view the generated document in [use.superdocs.app](https://use.superdocs.app).

## Run tests

From `backend/`:

```powershell
uv run pytest -v
```

The tests currently cover the price-range heuristic, including normal and edge cases. If `uv` reports a local cache-permission error, run pytest directly through the existing virtual environment:

```powershell
.\.venv\Scripts\python.exe -m pytest -v
```

## Project structure

```text
backend/
cma/ Domain models, pricing, pipeline, API, and SuperDocs client
fixtures/ Synthetic sample property datasets
scripts/ Command-line sample runner
templates/ Saved HTML document template
tests/ Backend tests
output/ Generated documents
frontend/ React/Vite data-entry form
planning.md Project planning and implementation notes
superdocs.txt SuperDocs API and integration reference
```

## Pricing heuristic and limitations

The suggested price range is based on comparable-property price-per-square-foot values and is intended as a simple demonstration heuristic. It is not an appraisal, valuation, or substitute for local market expertise. The application uses synthetic data and public placeholder photo URLs; it does not connect to MLS data.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.14
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Core domain package for the CMA document generator."""
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import os
from pathlib import Path
from typing import Any
from uuid import uuid4

from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, Field

from .pipeline import branding_from_dict, generate_cma_from_saved_template, property_from_dict


class CMARequest(BaseModel):
branding: dict[str, Any]
subject: dict[str, Any]
comps: list[dict[str, Any]] = Field(min_length=4)
export_format: str = "pdf"


app = FastAPI(title="Comparative Market Analysis API")
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:5173", "http://127.0.0.1:5173"],
allow_credentials=True,
allow_methods=["POST", "GET"],
allow_headers=["*"],
)


@app.get("/health")
def health() -> dict[str, str]:
return {"status": "ok"}


@app.post("/api/generate-cma")
def generate_cma_endpoint(payload: CMARequest) -> dict[str, str]:
if payload.export_format not in {"pdf", "docx", "html"}:
raise HTTPException(
status_code=422, detail="Unsupported export format")

try:
subject = property_from_dict(payload.subject)
comps = [property_from_dict(comp) for comp in payload.comps]
branding = branding_from_dict(payload.branding)
output_dir = Path(__file__).resolve().parents[1] / "output"
output_path = output_dir / f"cma_{uuid4().hex}.{payload.export_format}"
template_path = Path(__file__).resolve(
).parents[1] / "templates" / "cma_saved_template.html"
result = generate_cma_from_saved_template(
subject,
comps,
branding,
output_path,
template_path=template_path,
export_format=payload.export_format,
api_key=os.getenv("SUPERDOCS_API_KEY"),
)
except (TypeError, ValueError, KeyError) as exc:
raise HTTPException(
status_code=422, detail=f"Invalid CMA data: {exc}") from exc
except Exception as exc:
raise HTTPException(
status_code=502, detail=f"Document generation failed: {exc}") from exc

return {
"message": "Document created successfully",
"filename": result.name,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""Data shapes used by the CMA pipeline"""
from dataclasses import dataclass


@dataclass(frozen=True)
class Property:
address: str
city: str
state: str
beds: int
baths: float
sqft: int
lot_size_sqft: int
year_built: int
list_price: int
sale_price: int | None
sale_date: str | None
photo_url: str
distance_miles: float | None = None
days_on_market: int | None = None


@dataclass(frozen=True)
class PriceRange:
low: int
high: int
rationale: str


@dataclass(frozen=True)
class Branding:
agent_name: str
brokerage: str
phone: str
email: str
logo_url: str | None = None
primary_color: str = "#1F4E5F"
accent_color: str = "#D6A756"
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
from datetime import date
import json
from pathlib import Path

from .models import Branding, Property
from .pricing import estimate_price_range
from .superdocs_client import (
export_document,
list_user_templates,
send_chat_instruction,
upload_user_template,
)


def generate_cma_from_saved_template(
subject: Property,
comps: list[Property],
branding: Branding,
out_path: str | Path,
*,
template_path: str | Path,
template_name: str = "cma_saved_template.html",
export_format: str = "pdf",
api_key: str | None = None,
) -> Path:
templates = list_user_templates(api_key=api_key)
if not any(template.get("name") == template_name for template in templates):
upload_user_template(template_path, api_key=api_key)

price_range = estimate_price_range(subject, comps)
placeholder_values = {
"[AGENT NAME]": branding.agent_name,
"[BROKERAGE NAME]": branding.brokerage,
"[AGENT PHONE]": branding.phone,
"[AGENT EMAIL]": branding.email,
"[SUBJECT PROPERTY ADDRESS]": subject.address,
"[SUBJECT CITY]": subject.city,
"[SUBJECT STATE]": subject.state,
"[SUBJECT BEDS]": subject.beds,
"[SUBJECT BATHS]": subject.baths,
"[SUBJECT SQFT]": subject.sqft,
"[SUBJECT LOT SIZE]": subject.lot_size_sqft,
"[SUBJECT YEAR BUILT]": subject.year_built,
"[SUBJECT PHOTO URL]": subject.photo_url,
"[DATE]": date.today().strftime("%B %d, %Y").replace(" 0", " "),
"[PRICE_RANGE_PLACEHOLDER]": f"${price_range.low:,.0f} - ${price_range.high:,.0f}",
"[PRICE_RANGE_BASIS_PLACEHOLDER]": price_range.rationale,
}
comparables = [
{
"address": comp.address,
"city": comp.city,
"state": comp.state,
"sale_price": comp.sale_price or comp.list_price,
"sqft": comp.sqft,
"price_per_sqft": round((comp.sale_price or comp.list_price) / comp.sqft, 2),
"beds": comp.beds,
"baths": comp.baths,
"sale_date": comp.sale_date,
"photo_url": comp.photo_url,
}
for comp in comps
if comp.sqft > 0
]
session_id = str(__import__("uuid").uuid4())
send_chat_instruction(
session_id,
"Create a new CMA using my saved " + template_name + " template. Replace every exact placeholder using this mapping::\n" +
json.dumps(placeholder_values, indent=2) + "\n"
"Replace placeholders in visible text and in HTML attributes, especially image src and alt attributes. "
"Do not leave any bracketed placeholders anywhere in the final document. "
"Replace [COMPARABLES_TABLE_PLACEHOLDER] with a properly formatted table including each comparable's photo URL, address, sale price, square feet, price per square foot, beds/baths, and sale date. Replace the price-range placeholders with the supplied range and rationale. Use this comparable data to build the table, without inventing facts:\n" +
json.dumps(comparables, indent=2) +
"\nPreserve the template's branding and layout.",
api_key=api_key,
)
exported = export_document(session_id, export_format, api_key=api_key)
destination = Path(out_path)
destination.parent.mkdir(parents=True, exist_ok=True)
destination.write_bytes(exported)
return destination


def property_from_dict(data: dict[str, object]) -> Property:
return Property(**data)


def branding_from_dict(data: dict[str, object]) -> Branding:
return Branding(**data)
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from statistics import median

from .models import PriceRange, Property


# Use the closed price when available, otherwise the asking price
def comparable_price(comp: Property) -> int:
price = comp.sale_price or comp.list_price
if price <= 0:
raise ValueError(f"Comparable {comp.address!r} has no usable price")
return price


# Estimate value from the median comparable price per square foot
def estimate_price_range(subject: Property, comps: list[Property]) -> PriceRange:
if subject.sqft <= 0:
raise ValueError(
"Subject property must have a positive square footage")
if not comps:
raise ValueError("At least one comparable property is required")

price_per_sqft = [
comparable_price(comp) / comp.sqft
for comp in comps
if comp.sqft > 0
]
if not price_per_sqft:
raise ValueError(
"At least one comparable must have positive square footage")

median_price_per_sqft = median(price_per_sqft)
midpoint = round(subject.sqft * median_price_per_sqft)
low = round(midpoint * 0.95 / 1000) * 1000
high = round(midpoint * 1.05 / 1000) * 1000
rationale = (
f"Based on the median comparable price of "
f"${median_price_per_sqft:,.0f} per square foot across "
f"{len(price_per_sqft)} comparable(s), adjusted to the subject's "
f"{subject.sqft:,} square feet."
)
return PriceRange(low=low, high=high, rationale=rationale)
Loading