Spaces:
Build error
Build error
| import gradio as gr | |
| import joblib | |
| import pandas as pd | |
| import numpy as np | |
| import re | |
| import requests | |
| from datetime import datetime | |
| from geopy.geocoders import Nominatim | |
| from geopy.distance import geodesic | |
| # Cargar modelo y dataset | |
| model = joblib.load("modelo_docks.pkl") | |
| df_stations = pd.read_csv("Informacio_Estacions_Bicing_2025.csv") | |
| geolocator = Nominatim(user_agent="bicing-agent") | |
| from groq import Groq | |
| client = Groq(api_key="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") | |
| # LLM: llama-3.3-70b-versatile | |
| def preguntar_al_usuario(pregunta): | |
| response = client.chat.completions.create( | |
| messages=[ | |
| {"role": "system", "content": "Eres un asistente de Bicing. Tu tarea es hacer una preguntal usuario y esperar su respuesta. No saludes a menos que te lo pida"}, | |
| {"role": "user", "content": f"Pregunta al usuario lo siguiente, puedes modificar el tono para hacerlo mas amigable y añadir ayudas adicionales sobre como introducir los datos: '{pregunta}'"} | |
| ], | |
| model="llama-3.3-70b-versatile", | |
| temperature=0.5, | |
| max_completion_tokens=512, | |
| top_p=1, | |
| stream=False, | |
| ) | |
| return response.choices[0].message.content.strip() | |
| # Estaciones más cercanas | |
| def get_nearest_stations(ubicacion, top_n=10): | |
| loc = geolocator.geocode(f"{ubicacion}, Barcelona, Spain") | |
| if not loc: | |
| return pd.DataFrame() | |
| user_coord = (loc.latitude, loc.longitude) | |
| df_stations["distancia"] = df_stations.apply( | |
| lambda row: geodesic(user_coord, (row["lat"], row["lon"])).meters, | |
| axis=1 | |
| ) | |
| return df_stations.nsmallest(top_n, "distancia")[["station_id", "address", "lat", "lon"]] | |
| # Tiempo (Open-Meteo) | |
| def get_weather_forecast(lat, lon, year, month, day, hour): | |
| fecha = f"{year}-{month:02d}-{day:02d}" | |
| hora_str = f"{hour:02d}:00" | |
| url = ( | |
| f"https://api.open-meteo.com/v1/forecast?" | |
| f"latitude={lat}&longitude={lon}&hourly=temperature_2m,precipitation&timezone=Europe%2FMadrid" | |
| f"&start_date={fecha}&end_date={fecha}" | |
| ) | |
| r = requests.get(url) | |
| if r.status_code != 200: | |
| return None, None | |
| data = r.json() | |
| horas = data["hourly"]["time"] | |
| temperaturas = data["hourly"]["temperature_2m"] | |
| precipitaciones = data["hourly"]["precipitation"] | |
| for i, h in enumerate(horas): | |
| if h.endswith(hora_str): | |
| return temperaturas[i], precipitaciones[i] | |
| return None, None | |
| # Predicción con el modelo | |
| def predict_disponibilidad(context): | |
| estaciones_cercanas = get_nearest_stations(context["ubicacion"]) | |
| if estaciones_cercanas.empty: | |
| return {"error": "No se encontraron estaciones cercanas."} | |
| resultados = [] | |
| for _, row in estaciones_cercanas.iterrows(): | |
| temp, precip = get_weather_forecast( | |
| row["lat"], row["lon"], 2025, | |
| context["month"], context["day"], context["hour"] | |
| ) | |
| if temp is None: | |
| continue | |
| X = np.array([[ | |
| row["station_id"], | |
| context["month"], | |
| context["day"], | |
| context["hour"], | |
| context["ctx_value"], context["ctx_value"], context["ctx_value"], context["ctx_value"], | |
| temp, | |
| precip | |
| ]]) | |
| pred = model.predict(X)[0] | |
| resultados.append({ | |
| "station_id": row["station_id"], | |
| "address": row["address"], | |
| "pred_pct": float(pred), | |
| "temperature": round(temp, 1), | |
| "precip": round(precip, 1) | |
| }) | |
| if not resultados: | |
| return {"error": "No se pudieron calcular predicciones meteorológicas."} | |
| resultados_ordenados = sorted(resultados, key=lambda x: x["pred_pct"], reverse=True) | |
| return { | |
| "target_pct": context["target_pct"], | |
| "candidatas": resultados_ordenados | |
| } | |
| # Preguntas al usuario | |
| preguntas = [ | |
| ("ubicacion", "INTRODUCE SALUDO y la pregunta ¿Dónde te gustaría coger la bici? (zona o dirección en Barcelona)"), | |
| ("month", "¿En qué mes planeas cogerla? (número 1-12)"), | |
| ("day", "¿Qué día del mes?"), | |
| ("hour", "¿A qué hora la necesitas? (0-23)?"), | |
| ("target_pct", "¿Qué porcentaje mínimo de bicicletas esperas encontrar disponibles? (0 a 100%)") | |
| ] | |
| # Flujo de conversación | |
| def chat(user_input, chat_history, current_step, user_context): | |
| key, _ = preguntas[current_step] | |
| if key in ["month", "day", "hour", "target_pct"]: | |
| match = re.search(r"\d+(\.\d+)?", user_input) | |
| if match: | |
| value = float(match.group()) | |
| user_context[key] = value / 100 if key == "target_pct" else int(value) | |
| else: | |
| chat_history.append(("user", user_input)) | |
| chat_history.append(("assistant", "Introduce un número válido.")) | |
| return chat_history, current_step, user_context | |
| else: | |
| user_context[key] = user_input.strip() | |
| chat_history.append(("user", user_input)) | |
| current_step += 1 | |
| if current_step < len(preguntas): | |
| siguiente_pregunta = preguntar_al_usuario(preguntas[current_step][1]) | |
| chat_history.append(("assistant", siguiente_pregunta)) | |
| else: | |
| resultado = predict_disponibilidad(user_context) | |
| if "error" in resultado: | |
| chat_history.append(("assistant", resultado["error"] + " Reiniciando conversación...")) | |
| user_context = { | |
| "ubicacion": None, | |
| "month": None, | |
| "day": None, | |
| "hour": None, | |
| "target_pct": None, | |
| "temperature": None, | |
| "lluvia": None | |
| } | |
| current_step = 0 | |
| chat_history.append(("assistant", preguntar_al_usuario(preguntas[0][1]))) | |
| return chat_history, current_step, user_context | |
| else: | |
| clima = resultado["candidatas"][0] | |
| # 🧾 Resumen del contexto | |
| fecha_str = f"{user_context['day']:02d}/{user_context['month']:02d}/2025" | |
| hora_str = f"{user_context['hour']:02d}:00h" | |
| resumen_contexto = ( | |
| f"📍 *Ubicación*: {user_context['ubicacion']}\n" | |
| f"🗓️ *Día*: {fecha_str}\n" | |
| f"🕒 *Hora*: {hora_str}\n" | |
| f"🎯 *Porcentaje mínimo deseado de bicis*: {int(user_context['target_pct'] * 100)}%" | |
| ) | |
| # 📈 Predicción meteorológica | |
| resumen_meteo = ( | |
| f"🌡️ *Temperatura esperada*: {clima['temperature']}°C\n" | |
| f"☔ *Precipitación esperada*: {clima['precip']} mm" | |
| ) | |
| # 🚲 Disponibilidad de estaciones | |
| candidatas = resultado["candidatas"] | |
| hay_suficientes = any(r["pred_pct"] >= resultado["target_pct"] for r in candidatas) | |
| # 🚲 Disponibilidad de estaciones | |
| msg_estaciones = "🚲 *Estaciones más cercanas ordenadas por disponibilidad:*\n" | |
| for r in candidatas: | |
| emoji = "✅" if r["pred_pct"] >= resultado["target_pct"] else "⚠️" | |
| msg_estaciones += ( | |
| f"{emoji} '{r['address']}' (ID {r['station_id']}): " | |
| f"{round(r['pred_pct']*100)}% disponibilidad\n" | |
| ) | |
| if not hay_suficientes: | |
| msg_estaciones += ( | |
| "\n⚠️ *Aviso:* ninguna estación cercana alcanza el porcentaje mínimo deseado " | |
| f"de {int(resultado['target_pct'] * 100)}%. Puedes intentar con otro horario o ubicación." | |
| ) | |
| for r in resultado["candidatas"]: | |
| emoji = "✅" if r["pred_pct"] >= resultado["target_pct"] else "⚠️" | |
| msg_estaciones += ( | |
| f"{emoji} '{r['address']}' (ID {r['station_id']}): " | |
| f"{round(r['pred_pct']*100)}% disponibilidad\n" | |
| ) | |
| # Construir mensaje completo | |
| mensaje_final = f"{resumen_contexto}\n\n{resumen_meteo}\n\n{msg_estaciones}" | |
| chat_history.append(("assistant", mensaje_final.strip())) | |
| # 🧠 Resumen generado por LLM | |
| resumen_llm = client.chat.completions.create( | |
| messages=[ | |
| { | |
| "role": "system", | |
| "content": ( | |
| "Eres un asistente experto en movilidad urbana. Resume al usuario de forma clara y amigable " | |
| "si podrá encontrar bicis disponibles. se breve." | |
| ) | |
| }, | |
| {"role": "user", "content": mensaje_final.strip()} | |
| ], | |
| model="llama-3.3-70b-versatile", | |
| temperature=0.5, | |
| max_completion_tokens=256 | |
| ).choices[0].message.content.strip() | |
| # Añadir texto fijo al final del resumen | |
| resumen_llm += "\n\nSi quieres hacer otra consulta, dime una nueva ubicación o escribe 'reiniciar'." | |
| chat_history.append(("assistant", resumen_llm)) | |
| return chat_history, current_step, user_context | |
| # Interfaz Gradio | |
| with gr.Blocks() as demo: | |
| chatbot = gr.Chatbot() | |
| txt = gr.Textbox(placeholder="Escribe tu respuesta...", label="Tu mensaje") | |
| # Nuevo input para ctx histórico | |
| ctx_selector = gr.Dropdown( | |
| choices=["alto", "medio", "bajo"], | |
| value="medio", | |
| label="Nivel de ocupación histórica (ctx)" | |
| ) | |
| state_chat = gr.State([]) | |
| state_step = gr.State(0) | |
| state_context = gr.State({ | |
| "ubicacion": None, | |
| "month": None, | |
| "day": None, | |
| "hour": None, | |
| "target_pct": None, | |
| "ctx_value": 0.5, # valor por defecto | |
| "temperature": None, | |
| "lluvia": None | |
| }) | |
| def user_submit(message, chat_history, current_step, user_context, ctx_selector_value): | |
| # Mapear valor textual a número | |
| ctx_map = {"alto": 0.9, "medio": 0.5, "bajo": 0.1} | |
| user_context["ctx_value"] = ctx_map.get(ctx_selector_value, 0.5) | |
| return chat(message, chat_history, current_step, user_context) | |
| txt.submit( | |
| user_submit, | |
| inputs=[txt, state_chat, state_step, state_context, ctx_selector], | |
| outputs=[chatbot, state_step, state_context] | |
| ) | |
| # Primer mensaje | |
| primer_pregunta = preguntar_al_usuario(preguntas[0][1]) | |
| state_chat.value = [("assistant", primer_pregunta)] | |
| chatbot.value = state_chat.value | |
| demo.launch() | |