File size: 19,708 Bytes
c37fecf
 
972a04b
c37fecf
 
 
 
76d9486
c37fecf
 
972a04b
c37fecf
a4b79b1
 
c37fecf
76d9486
 
a4b79b1
76d9486
 
 
 
 
 
 
 
 
 
 
 
c37fecf
 
 
 
 
 
 
 
 
 
 
a4b79b1
c37fecf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
972a04b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c37fecf
 
 
 
 
 
 
 
 
 
 
 
 
 
972a04b
c37fecf
 
 
 
972a04b
 
c37fecf
 
 
 
 
a4b79b1
c37fecf
 
 
 
 
 
 
7671e0e
d7102d8
c37fecf
 
 
 
 
 
 
 
 
 
a4b79b1
c37fecf
16a7a52
96b5414
 
7671e0e
 
 
 
 
 
 
76d9486
a4b79b1
963adbe
 
 
 
 
 
 
 
 
 
 
77f7282
c37fecf
 
 
 
 
 
 
d7102d8
 
c37fecf
 
 
33aea0e
 
 
 
c37fecf
 
 
d2fd83d
 
 
c37fecf
d2fd83d
c37fecf
d2fd83d
 
 
 
 
c37fecf
d2fd83d
 
c37fecf
d2fd83d
 
 
 
 
c37fecf
 
 
d7102d8
c37fecf
216f293
c37fecf
 
 
 
 
 
d2fd83d
c37fecf
 
 
 
216f293
52d666b
 
c37fecf
 
 
 
16a7a52
6e63547
52d666b
 
6e63547
a7eff47
 
 
 
52d666b
 
 
 
a7eff47
 
 
 
 
 
6e63547
 
 
52d666b
 
6e63547
a7eff47
bdf5be0
c37fecf
 
 
001ce51
c37fecf
001ce51
c37fecf
 
a9e7805
 
c37fecf
7671e0e
 
c37fecf
 
 
 
 
 
d7102d8
c37fecf
52d666b
 
 
c37fecf
 
6e63547
16a7a52
6e63547
52d666b
 
6e63547
d0847ea
6e63547
a7eff47
 
 
52d666b
 
 
 
a7eff47
 
d0847ea
 
a7eff47
33aea0e
 
 
 
 
6e63547
 
52d666b
 
6e63547
d0847ea
c37fecf
bdf5be0
c37fecf
 
 
001ce51
c37fecf
001ce51
c37fecf
 
60b45c8
c37fecf
 
 
 
 
 
 
 
 
52d666b
 
c37fecf
 
d0847ea
c37fecf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6e63547
c37fecf
a7eff47
 
 
 
 
 
 
6e63547
 
 
 
a7eff47
d0847ea
c37fecf
a7eff47
c37fecf
 
 
 
 
 
 
 
16a7a52
c37fecf
 
16a7a52
972a04b
52d666b
 
16a7a52
c37fecf
 
 
16a7a52
6e63547
 
 
 
 
c37fecf
 
 
52d666b
 
16a7a52
c37fecf
 
a7eff47
 
 
16a7a52
 
 
a7eff47
 
c37fecf
a7eff47
c37fecf
16a7a52
 
c37fecf
 
 
16a7a52
c37fecf
001ce51
c37fecf
 
60b45c8
c37fecf
 
fe003ff
c37fecf
 
16a7a52
52d666b
 
fe003ff
 
 
107bf78
c37fecf
 
16a7a52
 
c37fecf
 
 
 
 
6e63547
 
a7eff47
 
 
 
 
 
 
 
6e63547
 
 
c37fecf
a7eff47
c37fecf
 
fe003ff
77f7282
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
import random
from abc import ABC, abstractmethod
from typing import get_origin, get_args
import os
# from langchain.tools import tool
import json
from pydantic import BaseModel, Field
from typing import Dict,  Union
import random
import copy
from types import UnionType

from langchain_community.vectorstores import FAISS
from langchain_community.embeddings import HuggingFaceEmbeddings

class VectorStore:
    def __init__(self, embeddings_model, vectorstore):
        embeddings = HuggingFaceEmbeddings(model_name=embeddings_model)
        self.vectore_store = FAISS.load_local(vectorstore, embeddings, allow_dangerous_deserialization=True)
    
    def get_context(self, instruction, number_of_contexts=2):
        documentos = self.vectore_store.similarity_search_with_score(instruction, k=number_of_contexts)
        return self._beautiful_context(documentos)
    
    def _beautiful_context(self, docs):
        context = ""
        for doc in docs:
            context += doc[0].page_content + "\n"
        return context
    
def read_json(data_path: str) -> tuple[list, dict]:
    try:
        with open(data_path, 'r', encoding="utf-8") as f:
            data = [json.loads(line) for line in f.readlines()]
    except:
        with open(data_path, 'r', encoding="utf-8") as f:
            data = json.loads(f.read())
    return data

json_data = read_json("data/val_de_nuria.json")
reservations = {}

class ToolBase(BaseModel, ABC):
    @abstractmethod
    def invoke(cls, input: Dict):
        pass

    @classmethod
    def to_openai_tool(cls):
        """
        Extracts function metadata from a Pydantic class, including function name, parameters, and descriptions.
        Formats it into a structure similar to OpenAI's function metadata.
        """
        function_metadata = {
            "type": "function",
            "function": {
                "name": cls.__name__,  # Function name is same as the class name, in lowercase
                "description": cls.__doc__.strip(),
                "parameters": {
                    "type": "object",
                    "properties": {},
                    "required": [],
                },
            },
        }

        # Iterate over the fields to add them to the parameters
        for field_name, field_info in cls.model_fields.items():
            # Field properties
            field_type = "string"  # Default to string, will adjust if it's a different type
            annotation = field_info.annotation.__args__[0] if getattr(field_info.annotation, "__origin__", None) is Union else field_info.annotation
            
            has_none = False
            if get_origin(annotation) is UnionType:  # Check if it's a Union type
                args = get_args(annotation)
                if type(None) in args:
                    has_none = True
                args = [arg for arg in args if type(None) != arg]
                if len(args) > 1:
                    raise TypeError("It can be union of only a valid type (str, int, bool, etc) and None")
                elif len(args) == 0:
                    raise TypeError("There must be a valid type (str, int, bool, etc) not only None")
                else:
                    annotation = args[0]
            
            if annotation == int:
                field_type = "integer"
            elif annotation == bool:
                field_type = "boolean"
            
            # Add the field's description and type to the properties
            function_metadata["function"]["parameters"]["properties"][field_name] = {
                "type": field_type,
                "description": field_info.description,
            }

            # Determine if the field is required (not Optional or None)
            if field_info.is_required():
                function_metadata["function"]["parameters"]["required"].append(field_name)
                has_none = True

            # If there's an enum (like for `unit`), add it to the properties
            if hasattr(field_info, 'default') and field_info.default is not None and isinstance(field_info.default, list):
                function_metadata["function"]["parameters"]["properties"][field_name]["enum"] = field_info.default
                if not has_none:
                    function_metadata["function"]["parameters"]["required"].append(field_name)

        return function_metadata

tools: Dict[str, ToolBase] = {}
oitools = []
vector_store = VectorStore(embeddings_model="BAAI/bge-m3", vectorstore="data/vs")

def tool_register(cls: BaseModel):
    oaitool = cls.to_openai_tool()
    
    oitools.append(oaitool)
    tools[oaitool["function"]["name"]] = cls

# @tool_register
class hotel_description(ToolBase):
    """Retrieves basic information about the hotel, such as its name, address, contact details, and overall description."""

    @classmethod
    def invoke(cls, input: Dict) -> str: 
        return """### **Nou Vall de Núria – Brief Description**  
Nestled in the stunning **Vall de Núria** in the Pyrenees, **Nou Vall de Núria** offers a perfect blend of comfort and adventure. Guests can enjoy breathtaking mountain views, premium accommodations, and excellent facilities, including an outdoor pool, gym, and sauna.  
The hotel features **two dining options**, serving traditional Catalan cuisine and refreshing drinks. Accommodations range from **cozy standard rooms to luxurious suites and fully equipped apartments**, catering to couples, families, and groups.  
For an unforgettable stay, guests can choose from **special packages**, including family-friendly stays, romantic getaways, ski adventures, and relaxation retreats. Outdoor enthusiasts can explore **hiking trails, ski slopes, and fishing spots** in the surrounding natural beauty.  
Whether for relaxation or adventure, **Nou Vall de Núria** promises a unique and memorable experience.""" 


@tool_register
class get_hotal_general_information(ToolBase):
    """    Retrieves general information about a hotel, including its background, amenities, location, and nearby attractions based on the query.    """
    query: str = Field(description="User's question or query for retrieving the information.")

    @classmethod
    def invoke(cls, input: Dict) -> str:
        query = input.get("query", None)
        if not query:
            return "Missing required argument: query."
        
        # return "We are currently working on it. You can't use this tool right now—please try again later. Thank you for your patience!"
        return vector_store.get_context(query)


@tool_register
class packs(ToolBase):
    """Provides a list of available activity pack at the hotel."""

    @classmethod
    def invoke(cls, input: Dict) -> str: 
        return json_data["packs"]

@tool_register
class hotel_facilities(ToolBase):
    """Provides a list of available general facilities at the hotel, which could include amenities like a spa, pool, gym, conference rooms, etc."""

    @classmethod
    def invoke(cls, input: Dict) -> str: 
        return json_data["general_facilities"]

@tool_register
class restaurants_details(ToolBase):
    """Provides a list of available restaurants with their details."""

    @classmethod
    def invoke(cls, input: Dict) -> str: 
        """
        Play a playlist by its name, starting with the first or a random song.
        """

        return json_data["restaurants"]
    

@tool_register
class restaurant_details(ToolBase):
    """Retrieves detailed information about a specific restaurant in the hotel, including its menu, ambiance, operating hours, and special features."""

    name: str = Field(default=[res["name"] for res in json_data["restaurants"]], description="Name of the resaturant")
    
    @classmethod
    def invoke(cls, input: Dict) -> str: 
        """
        Play a playlist by its name, starting with the first or a random song.
        """

        instance = cls(**input)
        name = instance.name

        restaurante = [res for res in json_data["restaurants"] if res["name"] == name]
        if restaurante:
            return restaurante
        else:
            return f"We don't have any restaurante with the name: {name}"


@tool_register
class rooms_information(ToolBase):
    """
    Provides a list of available hotel rooms, including brief descriptions of each room type.
    """
    @classmethod
    def invoke(cls, input: Dict) -> str: 
        return json_data["room_types"]


@tool_register
class check_room_availability(ToolBase):
    """
    Checks if a specified room type is available between the provided check-in and check-out dates for a given number of guests.
    """
    room_type: str = Field(default=list(json_data["room_types"].keys()), description="The type of room to check for availability.")
    reservation_start_date: str = Field(description="The check-in date for the reservation, formatted as 'YYYY-MM-DD' (e.g., '2025-04-01')")
    reservation_end_date: str = Field(description="The check-out date for the reservation, formatted as 'YYYY-MM-DD' (e.g., '2025-04-05')")
    guests: int = Field(description="The number of guests that will occupy the room.")
    
    @classmethod
    def invoke(cls, input: Dict) -> str: 
        input["room_type"] = input.get("room_type", None)
        room_type = input.get("room_type", None)
        reservation_start_date = input.get("reservation_start_date", None)
        reservation_end_date = input.get("reservation_end_date", None)
        guests = input.get("guests", None)

        missing = []
        if not room_type:
            missing.append("room_type")
        if not reservation_start_date:
            missing.append("reservation_start_date")
        if not reservation_end_date:
            missing.append("reservation_end_date")
        if not guests:
            missing.append("guests")

        if len(missing):
            value = ", ".join(missing)
            return f"Unable to check the room availability. The following required arguments are missing:{value}." 

        instance = cls(**input)
        room_type = instance.room_type
        reservation_start_date = instance.reservation_start_date
        reservation_end_date = instance.reservation_end_date
        guests = instance.guests
        
        rooms = [room for room in json_data["accomodations"]["rooms"] if room_type in room["type"]]
        if len(rooms) == 0:
            return f"There is no room exists with room type {room_type}"
        
        rooms2 = [room for room in rooms if guests <= room["number_of_guests"]]
        if len(rooms2) == 0:
            max_guests = json_data["room_types"][room_type]["number_of_guests"]
            return f"The number of guest is superior then the availibilty, maximum is {max_guests}"

        return rooms2




@tool_register
class make_reservation(ToolBase):
    """
    Creates a new reservation for the hotel by booking a room of the specified type for the desired dates, and associating the booking with a user.
    """

    user_name: str = Field(description="The name of user who is doing the reservation.")
    room_type: str = Field(default=list(json_data["room_types"].keys()), description="The type of room being reserved.")
    reservation_start_date: str = Field(description="The check-in date for the reservation, formatted as 'YYYY-MM-DD' (e.g., '2025-04-01')")
    reservation_end_date: str = Field(description="The check-out date for the reservation, formatted as 'YYYY-MM-DD' (e.g., '2025-04-05')")
    guests: int = Field(description="The total number of guests for the reservation. Must be a positive integer.")
    
    @classmethod
    def invoke(cls, input: Dict) -> str:
        input["room_type"] = input.get("room_type", None)
        room_type = input.get("room_type", None)
        reservation_start_date = input.get("reservation_start_date", None)
        reservation_end_date = input.get("reservation_end_date", None)
        guests = input.get("guests", None)
        user_name = input.get("user_name", None)
        
        missing = []
        if not room_type:
            missing.append("room_type")
        if not reservation_start_date:
            missing.append("reservation_start_date")
        if not reservation_end_date:
            missing.append("reservation_end_date")
        if not guests:
            missing.append("guests")
        if not user_name:
            missing.append("user_name")

        if len(missing):
            value = ", ".join(missing)
            return f"Unable to complete the reservation. The following required arguments are missing:{value}."   
    

        instance = cls(**input)
        room_type = instance.room_type
        reservation_start_date = instance.reservation_start_date
        reservation_end_date = instance.reservation_end_date
        guests = instance.guests
        user_name = instance.user_name.lower()

        rooms = [room for room in json_data["accomodations"]["rooms"] if room_type in room["type"]]
        if len(rooms) == 0:
            return f"There is no room exists with room type {room_type}"
        
        rooms2 = [room for room in rooms if guests <= room["number_of_guests"]]
        if len(rooms2) == 0:
            max_guests = json_data["room_types"][room_type]["number_of_guests"]
            return f"The number of guest is superior then the availibilty, maximum is {max_guests}"

        room = rooms2[random.randint(0, len(rooms2) - 1)]

        rand = int(random.randint(0,10000000))
        while rand in reservations:
            rand = int(random.randint(0,10000000))
        
        tmp_data = {
            "status": "Reserved",
            "room_number": room["room_number"],
            "room_type": room_type,
            "reservation_start_date": reservation_start_date,
            "reservation_end_date": reservation_end_date,
            "guests": guests,
            "reservation_id": rand,
            "user_name": user_name,
        }

        reservations[rand] = tmp_data

        return json.dumps(tmp_data)

@tool_register
class cancel_reservation(ToolBase):
    """Playing a specific playlist by its name."""

    reservation_id: int = Field(description="The unique identifier of the reservation to be canceled.")
    
    @classmethod
    def invoke(cls, input: Dict) -> str: 

        reservation_id = input.get("reservation_id", None)

        missing = []
        if not reservation_id:
            missing.append("reservation_id")

        if len(missing):
            value = ", ".join(missing)
            return f"Unable to cancel the reservation. The following required arguments are missing:{value}."     
        
        instance = cls(**input)
        reservation_id = instance.reservation_id


        
        if reservation_id not in reservations:
            return f"There is no reservations with the id: {reservation_id}"
        
        reservations.pop(reservation_id)
        return f"The reservation {reservation_id} is cancled correctly"

@tool_register
class modify_reservation(ToolBase):
    """
    Allows a user to modify an existing reservation by updating the check-in/check-out dates or changing the room type, subject to availability.
    One of these parameter is mandatory: new_room_type, new_reservation_start_date, new_reservation_end_date, guests.
    """

    reservation_id: int = Field(description="The unique identifier of the reservation to be modified.")
    new_room_type: str | None = Field(default=list(json_data["room_types"].keys()), description=f"The type of new room to be modified, if {None} same room will be modified.")
    new_reservation_start_date: str = Field(default=None, description="New check out date in format DD/MM/YYYY")
    new_reservation_end_date: str = Field(default=None, description="New check out date in format DD/MM/YYYY")
    new_guests: int = Field(default=None, description="New number of guests for the reservation.")

    @classmethod
    def invoke(cls, input: Dict) -> str: 
        input["new_room_type"] = input.get("new_room_type", None)
        reservation_id = input.get("reservation_id", None)

        missing = []
        if not reservation_id:
            missing.append("reservation_id")

        instance = cls(**input)
        new_room_type = instance.new_room_type
        new_reservation_start_date = instance.new_reservation_start_date
        new_reservation_end_date = instance.new_reservation_end_date
        new_guests = instance.new_guests
        reservation_id = instance.reservation_id

        if len(missing):
            value = ", ".join(missing)
            return f"Unable to modify the reservation. The following required arguments are missing:{value}."     
    
        if not (new_room_type or new_reservation_start_date or new_reservation_end_date or new_guests):
            return "Unable to modify the reservation. One of the following arguments must be passed: new_room_type, new_reservation_start_date, new_reservation_end_date, new_guests."     


        if reservation_id not in reservations:
            return f"There is no reservations with the id: {reservation_id}"
        
        if new_room_type or new_guests:
            rooms = [room for room in json_data["accomodations"]["rooms"] if new_room_type in room["type"]]
            if len(rooms) == 0:
                return f"There is no room exists with room type {new_room_type}"
            
            rooms = [room for room in rooms if new_guests <= room["number_of_guests"]]
            if len(rooms) == 0:
                max_guests = json_data["room_types"][new_room_type]["number_of_guests"]
                return f"The number of guest is superior then the availibilty, maximum is {max_guests}"
            
            room = rooms[random.randint(0, len(rooms) - 1)]
            room_number = room["room_number"]
        else:
            room_number = reservations[reservation_id]["room_number"]

        
        reservations[reservation_id]["guests"] = new_guests if new_guests else reservations[reservation_id]["guests"]
        reservations[reservation_id]["reservation_start_date"] = new_reservation_start_date if new_reservation_start_date else reservations[reservation_id]["reservation_start_date"]
        reservations[reservation_id]["reservation_end_date"] = new_reservation_end_date if new_reservation_end_date else reservations[reservation_id]["reservation_end_date"]
        reservations[reservation_id]["room_type"] = new_room_type if new_room_type else reservations[reservation_id]["room_type"]
        reservations[reservation_id]["room_number"] = room_number
        tmp_data = reservations[reservation_id]
        return f"The reservation {reservation_id} is modified correctly: {json.dumps(tmp_data)}"

@tool_register
class get_reservation_detail(ToolBase):
    """Retrieve comprehensive details of a specific room reservation by providing its unique reservation ID."""

    reservation_id: int = Field(description="Id of the reservation")

    @classmethod
    def invoke(cls, input: Dict) -> str: 
        reservation_id = input.get("reservation_id", None)
    
        missing = []
        if not reservation_id:
            missing.append("reservation_id")

        if len(missing):
            value = ", ".join(missing)
            return f"Unable to get the details. The following required arguments are missing:{value}."     

        instance = cls(**input)
        reservation_id = instance.reservation_id

        if reservation_id not in reservations:
            return f"There is no reservations with the id: {reservation_id}"
        
        
        tmp_data = copy.deepcopy(reservations[reservation_id])
        return json.dumps(tmp_data)