Download app.py from nbacchi/exercise3: direct link, hf CLI and curl.
- Browser
- Download file 7.16 kB
-
https://huggingface.co/spaces/nbacchi/exercise3/resolve/main/app.py
- Command line
-
hf download hf://spaces/nbacchi/exercise3/app.py
-
curl -L -o app.py https://huggingface.co/spaces/nbacchi/exercise3/resolve/main/app.py
7.16 kB
| 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() | |