import os import pandas as pd from smolagents import Tool from typing import List, Dict, Any, Union, Tuple from meteofrance_api import MeteoFranceClient from src.skitour_api import get_topos, get_refuges, get_details_topo, get_massifs, get_recent_outings from src.meteo_france_api import get_massif_conditions from src.utils import geocode_location, assign_location_to_clusters, haversine, llm_summarizer class RefugeTool(Tool): name = "refuge_recherche" description = "Recherche d'un refuge dans un massif donné" inputs = { "massif_id": { "description": "[Optional, default: None] Id du massif souhaité ", "type": "string", } } output_type = "string" def forward(self, massif_id) -> List[Dict]: return get_refuges(massif_id) class GetRoutesTool(Tool): name = "list_routes" description = """ Looking for a list of ski touring routes in a given list of mountain ranges. Returns a list containing the information of the topos found. Use `topo_details` immediately after this tool to get the details of a specific topo. """ inputs = { "mountain_range_ids": { "description": "List of mountain range ids", "type": "string", } } output_type = "any" def forward(self, mountain_range_ids: str) -> List[Dict]: topos = get_topos(mountain_range_ids) return topos class DescribeRouteTool(Tool): name = "describe_route" description = """ Searches for key information about a specific ski touring route, including weather forecasts and associated avalanche risks. Always use this tool after using the `list_routes` tool. This tool returns a dictionary containing the route's information, the avalanche risk estimation bulletin, and the weather forecast for the coming days of the route. """ inputs = { "id_route": { "description": "id of the route", "type": "string", }, "id_range": { "description": "mountain range id of the route", "type": "string"} } output_type = "any" def __init__(self, skitour2meteofrance: dict, llm_engine: Any): super().__init__() self.massifs_infos = skitour2meteofrance self.weather_client = MeteoFranceClient(access_token=os.getenv("METEO_FRANCE_API_KEY")) self.llm_engine = llm_engine def forward(self, id_route: str, id_range: str) -> dict: topo_info = get_details_topo(str(id_route)) avalanche_conditions = get_massif_conditions( self.massifs_infos[str(id_range)]['meteofrance_id'] ) lat, lon = topo_info["depart"]["latlon"] weather_forecast = self.weather_client.get_forecast(float(lat), float(lon)) daily_forecast = weather_forecast.forecast[:24] for day_forecast in daily_forecast: day_forecast["dt"] = weather_forecast.timestamp_to_locale_time(day_forecast["dt"]).isoformat() forecast_summary = llm_summarizer(str(daily_forecast), self.llm_engine) avalanche_summary = llm_summarizer(str(avalanche_conditions), self.llm_engine) return { "route_info": topo_info, "avalanche_conditions": avalanche_summary, "daily_weather_forecast": forecast_summary, "route_link": f"https://skitour.fr/topos/{id_route}" } class RecentOutingsTool(Tool): name = "recent_outings" description = """ Searches for recent outings in a given mountain range. Returns a list of the most recent outings in the given range. """ inputs = { "id_range": { "description": "id of the mountain range", "type": "string", } } output_type = "any" def forward(self, id_range: str) -> List[Dict]: return get_recent_outings(id_range) class MountainRangesTool(Tool): name = "list_mountain_ranges" description = """ Searches for the ID(s) of the mountain ranges closest to a given location. If the location is too far from known ranges, the search returns None. Should return a string with the massif IDs separated by commas. """ inputs = { "location": { "description": "Location to search for", "type": "string", }, "num_ranges": { "description": "[Optional, default: 3] Number of closest mountain ranges to return", "type": "number", } } output_type = "string" def __init__(self, clusters: Dict[str, List[Tuple[float, float]]]): super().__init__() self.clusters = clusters def forward(self, location: str, num_ranges: int) -> Union[str, None]: coord_location = geocode_location(location) if not location: return None matched_ranges = assign_location_to_clusters(coord_location, self.clusters, k=num_ranges) list_ranges = [range[0] for range in matched_ranges if range[1] < 100] if not list_ranges: return '' massifs= get_massifs() massif_ids = [_massif['id'] for _massif in massifs if _massif['nom'] in list_ranges] return ", ".join(massif_ids) class ForecastTool(Tool): name = "forecast" description = """Searches for the weather forecast for a given location as well as the current avalanche risk estimation bulletin. Unnecessary if the user is inquiring about a route, as `describe_route` already provides this information.""" inputs = { "location": { "description": "Location to search for", "type": "string", }, } output_type = "any" def __init__(self, llm_engine, clusters: Dict[str, List[Tuple[float, float]]], skitour2meteofrance: dict): super().__init__() self.clusters = clusters self.massifs_infos = skitour2meteofrance self.llm_engine = llm_engine def forward(self, location: str) -> Union[Dict[str, Any], None]: coord_location = geocode_location(location) if not location: return None # Get the closest mountain range to the location to get the avalanche conditions matched_ranges = assign_location_to_clusters(coord_location, self.clusters, k=1) list_ranges = [range[0] for range in matched_ranges if range[1] < 100] if not list_ranges: return None massifs= get_massifs() massif_id = [_massif['id'] for _massif in massifs if _massif['nom'] in list_ranges] avalanche_conditions = get_massif_conditions( self.massifs_infos[str(massif_id[0])]['meteofrance_id'] ) weather_client = MeteoFranceClient(access_token=os.getenv("METEO_FRANCE_API_KEY")) forecast = weather_client.get_forecast(*coord_location) daily_forecast = forecast.forecast[:24] for day_forecast in daily_forecast: day_forecast["dt"] = forecast.timestamp_to_locale_time(day_forecast["dt"]).isoformat() forecast_summary = llm_summarizer(str(daily_forecast), self.llm_engine) avalanche_summary = llm_summarizer(str(avalanche_conditions), self.llm_engine) return {"forecast": forecast_summary, "avalanche_conditions": avalanche_summary}