khlevon commited on
Commit
dc8e24f
·
1 Parent(s): d371de6

feat: add flixbus mcp tools rename map_tools files

Browse files
src/app/main.py CHANGED
@@ -4,7 +4,7 @@ import gradio as gr
4
  from src.common.logger import get_logger
5
  from src.common.config import settings
6
  from src.tools import register_tools
7
- from src.tools.map_tool import render_map_with_places
8
 
9
  logger = get_logger(__name__)
10
 
 
4
  from src.common.logger import get_logger
5
  from src.common.config import settings
6
  from src.tools import register_tools
7
+ from src.tools.map_tools import render_map_with_places
8
 
9
  logger = get_logger(__name__)
10
 
src/common/flixbus_client.py CHANGED
@@ -210,5 +210,16 @@ class FlixBusClient:
210
  response.raise_for_status()
211
  return SearchResponse(**response.json())
212
 
 
 
 
 
 
 
 
 
 
 
 
213
  async def close(self):
214
  await self.client.aclose()
 
210
  response.raise_for_status()
211
  return SearchResponse(**response.json())
212
 
213
+ def get_shop_url_for_search_trips(
214
+ self,
215
+ from_city_id: str,
216
+ to_city_id: str,
217
+ departure_date: str, # Format: DD.MM.YYYY
218
+ adult: int = 1,
219
+ currency: str = "EUR",
220
+ locale: str = "en",
221
+ ) -> str:
222
+ return f"https://shop.global.flixbus.com/search?departureCity={from_city_id}&arrivalCity={to_city_id}&rideDate={departure_date}&adult={adult}&_locale={locale}&currency={currency}"
223
+
224
  async def close(self):
225
  await self.client.aclose()
src/tools/__init__.py CHANGED
@@ -1,5 +1,14 @@
1
  def register_tools(gr):
2
- from src.tools.map_tool import gm_walking_dir_tool, render_map_with_places_tool
 
 
 
 
 
3
 
4
  gr.api(gm_walking_dir_tool)
5
  gr.api(render_map_with_places_tool)
 
 
 
 
 
1
  def register_tools(gr):
2
+ from src.tools.map_tools import gm_walking_dir_tool, render_map_with_places_tool
3
+ from src.tools.flixbus_tools import (
4
+ get_city_info_tool,
5
+ search_cities_in_radius_tool,
6
+ get_tripe_options_list_tool,
7
+ )
8
 
9
  gr.api(gm_walking_dir_tool)
10
  gr.api(render_map_with_places_tool)
11
+
12
+ gr.api(get_city_info_tool)
13
+ gr.api(search_cities_in_radius_tool)
14
+ gr.api(get_tripe_options_list_tool)
src/tools/flixbus_tools.py ADDED
@@ -0,0 +1,229 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import json
3
+ from typing import Optional
4
+ from src.common.logger import get_logger
5
+ from src.common.flixbus_client import FlixBusClient
6
+ from src.common.utils import format_docstring
7
+ from src.tools.schemas.flixbus_tools_schema import (
8
+ GetCityInfoResilt,
9
+ GetTripeOptionsListResult,
10
+ SearchCitiesInRadiusResult,
11
+ TripeOption,
12
+ )
13
+
14
+
15
+ logger = get_logger(__name__)
16
+
17
+ ##### get_city_info #####
18
+
19
+
20
+ async def get_city_info(city_name: str) -> Optional[GetCityInfoResilt]:
21
+ """
22
+ Resolves a city name to its Flixbus UUID.
23
+ """
24
+ client = FlixBusClient()
25
+ try:
26
+ suggestions = await client.get_city_suggestions(q=city_name)
27
+ if not suggestions:
28
+ return None
29
+
30
+ city = suggestions[0]
31
+ return GetCityInfoResilt(
32
+ id=city.id,
33
+ name=city.name,
34
+ country=city.country,
35
+ latitude=city.location.lat,
36
+ longitude=city.location.lon,
37
+ )
38
+
39
+ except Exception as e:
40
+ logger.error(f"Error resolving city ID: {e}")
41
+ return None
42
+ finally:
43
+ await client.close()
44
+
45
+
46
+ @format_docstring(template_args={"return_schema": GetCityInfoResilt._model_json_schema})
47
+ @gr.mcp.tool(
48
+ name="get_city_info_tool",
49
+ )
50
+ async def get_city_info_tool(
51
+ city_name: str,
52
+ ) -> str:
53
+ """
54
+ Resolves a city name to its Flixbus UUID.
55
+
56
+ Args:
57
+ city_name: The name of the city to resolve.
58
+
59
+ Returns:
60
+ {return_schema}
61
+ """
62
+ city_info = await get_city_info(city_name)
63
+
64
+ if city_info is None:
65
+ return json.dumps(
66
+ {"error": f"City '{city_name}' not found in Flixbus database."}
67
+ )
68
+
69
+ return city_info.model_dump_json()
70
+
71
+
72
+ ##### search_cities_in_radius #####
73
+
74
+
75
+ async def search_cities_in_radius(
76
+ lat: float, lon: float, radius_km: int = 300, limit: int = 20
77
+ ) -> SearchCitiesInRadiusResult:
78
+ """
79
+ Finds Flixbus cities within a bounding box roughly corresponding to the radius.
80
+ Note: This is an approximation using a bounding box.
81
+ """
82
+ client = FlixBusClient()
83
+ try:
84
+ # 1 degree of latitude is approx 111km
85
+ lat_delta = radius_km / 111
86
+ # 1 degree of longitude is approx 111km * cos(lat) - simplifying to 111km for rough box
87
+ lon_delta = radius_km / 111
88
+
89
+ top_left_lat = lat + lat_delta
90
+ top_left_lon = lon - lon_delta
91
+ bottom_right_lat = lat - lat_delta
92
+ bottom_right_lon = lon + lon_delta
93
+
94
+ response = await client.get_cms_cities(
95
+ top_left_lat=top_left_lat,
96
+ top_left_lon=top_left_lon,
97
+ bottom_right_lat=bottom_right_lat,
98
+ bottom_right_lon=bottom_right_lon,
99
+ limit=limit,
100
+ )
101
+
102
+ cities: list[GetCityInfoResilt] = []
103
+ for city in response.result:
104
+ cities.append(
105
+ GetCityInfoResilt(
106
+ name=city.name,
107
+ id=city.uuid, # Using UUID as ID
108
+ latitude=city.location.lat,
109
+ longitude=city.location.lon,
110
+ country=city.country,
111
+ )
112
+ )
113
+
114
+ return SearchCitiesInRadiusResult(cities=cities)
115
+ finally:
116
+ await client.close()
117
+
118
+
119
+ @format_docstring(
120
+ template_args={"return_schema": SearchCitiesInRadiusResult._model_json_schema}
121
+ )
122
+ @gr.mcp.tool(
123
+ name="search_cities_in_radius_tool",
124
+ )
125
+ async def search_cities_in_radius_tool(
126
+ lat: float, lon: float, radius_km: int = 300, limit: int = 20
127
+ ) -> str:
128
+ """
129
+ Finds Flixbus cities within a bounding box roughly corresponding to the radius.
130
+ Note: This is an approximation using a bounding box.
131
+
132
+ Args:
133
+ lat: Latitude of the center point.
134
+ lon: Longitude of the center point.
135
+ radius_km: Search radius in kilometers, default is 300km.
136
+ limit: Maximum number of cities to return, default is 20.
137
+
138
+ Returns:
139
+ {return_schema}
140
+ """
141
+ result = await search_cities_in_radius(lat, lon, radius_km, limit)
142
+ return result.model_dump_json()
143
+
144
+
145
+ ##### get_trip_options_list #####
146
+
147
+
148
+ async def get_tripe_options_list(
149
+ from_city_id: str, to_city_id: str, date: str
150
+ ) -> GetTripeOptionsListResult:
151
+ """
152
+ Searches for the trips between two cities.
153
+ """
154
+ client = FlixBusClient()
155
+
156
+ shop_url = client.get_shop_url_for_search_trips(
157
+ from_city_id=from_city_id, to_city_id=to_city_id, departure_date=date
158
+ )
159
+
160
+ empty_result = GetTripeOptionsListResult(
161
+ options=[], best_option=None, shop_url=shop_url
162
+ )
163
+
164
+ try:
165
+ response = await client.search_trips(
166
+ from_city_id=from_city_id, to_city_id=to_city_id, departure_date=date
167
+ )
168
+
169
+ if not response.trips:
170
+ return empty_result
171
+
172
+ options: list[TripeOption] = []
173
+ best_option: Optional[TripeOption] = None
174
+
175
+ tripe_options = response.trips[0].results
176
+
177
+ for _, trip_option in tripe_options.items():
178
+ if trip_option.status != "available":
179
+ continue
180
+
181
+ option = TripeOption(
182
+ price=trip_option.price.total,
183
+ departure_station_id=trip_option.departure.station_id,
184
+ arrival_station_id=trip_option.arrival.station_id,
185
+ departure_time=trip_option.departure.date.strftime(
186
+ "%Y-%m-%dT%H:%M:%S%z"
187
+ ),
188
+ arrival_time=trip_option.arrival.date.strftime("%Y-%m-%dT%H:%M:%S%z"),
189
+ duration_hours=trip_option.duration.hours,
190
+ available_seats=trip_option.available.seats,
191
+ )
192
+
193
+ options.append(option)
194
+
195
+ if best_option is None or option.price < best_option.price:
196
+ best_option = option
197
+
198
+ return GetTripeOptionsListResult(
199
+ options=options, best_option=best_option, shop_url=shop_url
200
+ )
201
+
202
+ except Exception as e:
203
+ logger.error(f"Error finding trip: {e}")
204
+ return empty_result
205
+ finally:
206
+ await client.close()
207
+
208
+
209
+ @format_docstring(
210
+ template_args={"return_schema": GetTripeOptionsListResult._model_json_schema}
211
+ )
212
+ @gr.mcp.tool(
213
+ name="get_tripe_options_list_tool",
214
+ )
215
+ async def get_tripe_options_list_tool(
216
+ from_city_id: str, to_city_id: str, date: str
217
+ ) -> str:
218
+ """
219
+ Searches for the trips between two cities.
220
+
221
+ Args:
222
+ from_city_id: Flixbus UUID of the departure city.
223
+ to_city_id: Flixbus UUID of the destination city.
224
+ date: Departure date in format DD.MM.YYYY.
225
+ Returns:
226
+ {return_schema}
227
+ """
228
+ result = await get_tripe_options_list(from_city_id, to_city_id, date)
229
+ return result.model_dump_json()
src/tools/{map_tool.py → map_tools.py} RENAMED
@@ -1,10 +1,9 @@
1
- from unittest import result
2
  from urllib.parse import urlencode, quote_plus
3
  import folium
4
  import gradio as gr
5
  from src.common.logger import get_logger
6
  from src.common.utils import format_docstring
7
- from src.tools.schemas.map_tool_schema import (
8
  MapRenderingResult,
9
  MapWalkingDirectionResult,
10
  )
 
 
1
  from urllib.parse import urlencode, quote_plus
2
  import folium
3
  import gradio as gr
4
  from src.common.logger import get_logger
5
  from src.common.utils import format_docstring
6
+ from src.tools.schemas.map_tools_schema import (
7
  MapRenderingResult,
8
  MapWalkingDirectionResult,
9
  )
src/tools/schemas/flixbus_tools_schema.py ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Optional
2
+ from pydantic import BaseModel, Field
3
+
4
+
5
+ ###################
6
+
7
+
8
+ class GetCityInfoResilt(BaseModel):
9
+ """
10
+ Contains information about a city
11
+ """
12
+
13
+ id: str = Field(..., description="Flixbus UUID of the city")
14
+ name: str = Field(..., description="Name of the city")
15
+ country: str = Field(..., description="Country of the city")
16
+ latitude: float = Field(..., description="Latitude of the city")
17
+ longitude: float = Field(..., description="Longitude of the city")
18
+
19
+
20
+ GetCityInfoResilt._model_example = GetCityInfoResilt(
21
+ id="string", name="string", country="string", latitude=0.0, longitude=0.0
22
+ )
23
+
24
+ GetCityInfoResilt._model_json_schema = (
25
+ GetCityInfoResilt._model_example.model_dump_json()
26
+ )
27
+
28
+ ###################
29
+
30
+
31
+ class SearchCitiesInRadiusResult(BaseModel):
32
+ """
33
+ Contains a list of cities within a specified radius
34
+ """
35
+
36
+ cities: list[GetCityInfoResilt] = Field(
37
+ ..., description="List of cities within the specified radius"
38
+ )
39
+
40
+
41
+ SearchCitiesInRadiusResult._model_example = SearchCitiesInRadiusResult(
42
+ cities=[GetCityInfoResilt._model_example]
43
+ )
44
+
45
+ SearchCitiesInRadiusResult._model_json_schema = (
46
+ SearchCitiesInRadiusResult._model_example.model_dump_json()
47
+ )
48
+
49
+
50
+ ###################
51
+
52
+
53
+ class TripeOption(BaseModel):
54
+ """
55
+ Contains information about a trip option
56
+ """
57
+
58
+ departure_station_id: Optional[str] = Field(
59
+ None, description="ID of the departure station"
60
+ )
61
+ arrival_station_id: Optional[str] = Field(
62
+ None, description="ID of the arrival station"
63
+ )
64
+
65
+ departure_time: Optional[str] = Field(
66
+ None,
67
+ description="Departure time of the trip option, e.g., '2025-11-23T13:10:00+01:00'",
68
+ )
69
+ arrival_time: Optional[str] = Field(
70
+ None,
71
+ description="Arrival time of the trip option, e.g., '2025-11-23T17:30:00+01:00'",
72
+ )
73
+
74
+ duration_hours: Optional[float] = Field(
75
+ None, description="Duration of the trip option in hours"
76
+ )
77
+ available_seats: Optional[int] = Field(
78
+ None, description="Number of available seats for the trip option"
79
+ )
80
+
81
+ price: float = Field(..., description="Price of the trip option")
82
+
83
+
84
+ TripeOption._model_example = TripeOption(
85
+ departure_station_id="string",
86
+ arrival_station_id="string",
87
+ departure_time="2025-11-23T13:10:00+01:00",
88
+ arrival_time="2025-11-23T17:30:00+01:00",
89
+ duration_hours=4,
90
+ available_seats=10,
91
+ price=19.99,
92
+ )
93
+
94
+ TripeOption._model_json_schema = TripeOption._model_example.model_dump_json()
95
+
96
+
97
+ class GetTripeOptionsListResult(BaseModel):
98
+ """
99
+ Contains a list of trip prices between two cities
100
+ """
101
+
102
+ options: list[TripeOption] = Field(
103
+ ..., description="List of trip options between two cities"
104
+ )
105
+
106
+ best_option: Optional[TripeOption] = Field(
107
+ None, description="Best trip option between two cities"
108
+ )
109
+
110
+ shop_url: Optional[str] = Field(
111
+ None, description="URL to shop for the best trip option"
112
+ )
113
+
114
+
115
+ GetTripeOptionsListResult._model_example = GetTripeOptionsListResult(
116
+ options=[TripeOption._model_example],
117
+ best_option=TripeOption._model_example,
118
+ shop_url="string",
119
+ )
120
+
121
+ GetTripeOptionsListResult._model_json_schema = (
122
+ GetTripeOptionsListResult._model_example.model_dump_json()
123
+ )
src/tools/schemas/{map_tool_schema.py → map_tools_schema.py} RENAMED
@@ -1,6 +1,9 @@
1
  from pydantic import BaseModel, Field
2
 
3
 
 
 
 
4
  class MapWalkingDirectionResult(BaseModel):
5
  """
6
  Contains url of the map with walking direction route
 
1
  from pydantic import BaseModel, Field
2
 
3
 
4
+ ###################
5
+
6
+
7
  class MapWalkingDirectionResult(BaseModel):
8
  """
9
  Contains url of the map with walking direction route