exercise3 / app.py
nbacchi's picture
Upload app.py with huggingface_hub
791b742 verified
import json
import os
import pickle
import gradio as gr
import pandas as pd
from openai import OpenAI
MODEL_PATH = "random_forest_regression.pkl"
DATA_PATH = "bfs_municipality_and_tax_data.csv"
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "")
OPENAI_MODEL = os.getenv("OPENAI_MODEL", "gpt-4o-mini")
with open(MODEL_PATH, "rb") as f:
model = pickle.load(f)
df_bfs_data = pd.read_csv(DATA_PATH, sep=",", encoding="utf-8")
df_bfs_data["tax_income"] = (
df_bfs_data["tax_income"].astype(str).str.replace("'", "", regex=False).astype(float)
)
town_to_row = {
str(row["bfs_name"]).lower(): row
for _, row in df_bfs_data.iterrows()
}
valid_towns = list(df_bfs_data["bfs_name"].sort_values().unique())
def match_town(user_town: str):
if not user_town or not user_town.strip():
return None
key = user_town.strip().lower()
if key in town_to_row:
return str(town_to_row[key]["bfs_name"])
for canonical_lower, row in town_to_row.items():
if key in canonical_lower or canonical_lower in key:
return str(row["bfs_name"])
return None
def call_llm_json(system_prompt: str, user_prompt: str) -> str:
if not OPENAI_API_KEY:
raise ValueError("OPENAI_API_KEY ist nicht gesetzt.")
if not OPENAI_MODEL:
raise ValueError("OPENAI_MODEL ist nicht gesetzt.")
client = OpenAI(api_key=OPENAI_API_KEY)
response = client.chat.completions.create(
model=OPENAI_MODEL,
max_tokens=512,
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt},
],
)
return response.choices[0].message.content
def parse_json_response(raw: str, required_keys: tuple[str, ...]) -> dict:
cleaned = (raw or "").strip()
if not cleaned:
raise ValueError("LLM hat eine leere Antwort zurückgegeben.")
if cleaned.startswith("```"):
lines = cleaned.splitlines()
inner = [line for line in lines[1:] if line.strip() != "```"]
cleaned = "\n".join(inner)
try:
parsed = json.loads(cleaned)
except json.JSONDecodeError as exc:
raise ValueError(f"Kein gültiges JSON erhalten: {cleaned[:300]}") from exc
missing = [k for k in required_keys if k not in parsed]
if missing:
raise ValueError(f"JSON fehlen Pflichtfelder: {', '.join(missing)}.")
return parsed
def extract_preferences(user_text: str) -> dict:
system_prompt = (
"Du bist ein Datenextraktionssystem für Schweizer Wohnungsanfragen. "
"Extrahiere aus dem Benutzertext genau drei Werte und gib sie als JSON zurück:\n"
'- "rooms": Anzahl Zimmer als Dezimalzahl (z.B. 3.5)\n'
'- "area_m2": Wohnfläche in m² als Zahl\n'
'- "town": Name der Gemeinde oder Stadt als String\n'
"Gib NUR gültiges JSON zurück, kein Markdown, keine Erklärung.\n"
'Beispiel: {"rooms": 3.5, "area_m2": 85, "town": "Winterthur"}\n'
"Falls ein Wert fehlt oder unklar ist, setze ihn auf null."
)
raw = call_llm_json(system_prompt=system_prompt, user_prompt=user_text)
return parse_json_response(raw, required_keys=("rooms", "area_m2", "town"))
def predict_apartment_price(rooms: float, area_m2: float, town: str) -> float:
canonical = match_town(town)
if canonical is None:
raise ValueError(
f"Gemeinde '{town}' nicht gefunden. "
"Bitte einen gültigen Ort im Kanton Zürich angeben."
)
row = town_to_row[canonical.lower()]
features = pd.DataFrame([{
"rooms": rooms,
"area_m2": area_m2,
"pop": float(row["pop"]),
"pop_dens": float(row["pop_dens"]),
"frg_pct": float(row["frg_pct"]),
"emp": float(row["emp"]),
"tax_income": float(row["tax_income"]),
}])
return float(model.predict(features)[0])
def generate_explanation(preferences: dict, prediction: float) -> str:
system_prompt = (
"Du bist ein freundlicher Schweizer Wohnungsberater. "
"Erkläre die Mietpreisschätzung auf Deutsch in 2–3 Sätzen. "
"Erwähne die Wohnungsmerkmale (Zimmer, Fläche, Ort), den geschätzten Mietpreis "
"und einen Hinweis auf Unsicherheit (z.B. Zustand, Mikrolage). "
"Berechne keinen neuen Preis selbst – verwende nur den angegebenen Modellwert. "
'Gib NUR gültiges JSON zurück im Format: {"answer": "Deine Erklärung"}'
)
user_prompt = (
f"Wohnungswunsch: {preferences.get('rooms')} Zimmer, "
f"{preferences.get('area_m2')} m², {preferences.get('town')}. "
f"Modellschätzung: CHF {prediction:,.0f} pro Monat."
)
raw = call_llm_json(system_prompt=system_prompt, user_prompt=user_prompt)
result = parse_json_response(raw, required_keys=("answer",))
return result["answer"]
def run_pipeline(user_text: str):
if not user_text or not user_text.strip():
return {}, 0.0, "Bitte beschreibe deinen Wohnungswunsch auf Deutsch."
try:
preferences = extract_preferences(user_text)
except Exception as e:
return {}, 0.0, f"Fehler bei der Texterkennung: {e}"
rooms = preferences.get("rooms")
area_m2 = preferences.get("area_m2")
town = preferences.get("town")
if rooms is None or area_m2 is None or town is None:
missing = [k for k, v in {"Zimmer": rooms, "Fläche": area_m2, "Ort": town}.items() if v is None]
return preferences, 0.0, (
f"Fehlende Angaben: {', '.join(missing)}. "
"Bitte Anfrage präzisieren, z.B.: «3.5 Zimmer, 85 m², Winterthur»."
)
try:
prediction = predict_apartment_price(float(rooms), float(area_m2), str(town))
except Exception as e:
return preferences, 0.0, f"Fehler bei der Preisschätzung: {e}"
try:
explanation = generate_explanation(preferences, prediction)
except Exception as e:
explanation = f"Geschätzte Monatsmiete: CHF {prediction:,.0f}. (Erklärung nicht verfügbar: {e})"
return preferences, prediction, explanation
with gr.Blocks(title="Wohnungspreisschätzer – Kanton Zürich") as demo:
gr.Markdown(
"""
# Wohnungspreisschätzer – Kanton Zürich
Beschreibe deinen Wohnungswunsch auf **Deutsch** und erhalte eine Mietpreisschätzung.
"""
)
user_input = gr.Textbox(
label="Wohnungswunsch",
lines=3,
placeholder="Ich suche eine 3.5-Zimmer-Wohnung mit etwa 85 m² in Winterthur.",
)
btn = gr.Button("Schätzen", variant="primary")
extracted = gr.JSON(label="Extrahierte Eingaben")
price = gr.Number(label="Geschätzte Monatsmiete (CHF)")
answer = gr.Textbox(label="Antwort", lines=5)
btn.click(fn=run_pipeline, inputs=[user_input], outputs=[extracted, price, answer])
gr.Examples(
examples=[
["Ich suche eine 3.5-Zimmer-Wohnung mit etwa 85 m² in Winterthur."],
["Haben Sie etwas für mich? 4 Zimmer, 100 m², am liebsten in Zürich."],
["Suche kleine 2-Zimmer-Wohnung mit 50 m² in Uster."],
],
inputs=[user_input],
)
demo.launch()