From f12c6ac471866b6f39afbe95a1b37d5626a5fa86 Mon Sep 17 00:00:00 2001 From: Steve Androulakis Date: Fri, 3 Jan 2025 21:11:48 -0800 Subject: [PATCH] Real events for FindEvents and real API for finding flights. TODO invoice creation --- api/main.py | 2 +- frontend/src/pages/App.jsx | 5 +- scripts/find_events_test.py | 8 + scripts/flight_api_test.py | 7 + tools/data/find_events_data.json | 252 +++++++++++++++++++++++++++++++ tools/find_events.py | 75 +++++++-- tools/search_flights.py | 171 +++++++++++++++++++++ tools/tool_registry.py | 10 +- 8 files changed, 506 insertions(+), 24 deletions(-) create mode 100644 scripts/find_events_test.py create mode 100644 scripts/flight_api_test.py create mode 100644 tools/data/find_events_data.json diff --git a/api/main.py b/api/main.py index e7eafc9..671c23a 100644 --- a/api/main.py +++ b/api/main.py @@ -78,7 +78,7 @@ async def send_prompt(prompt: str): example_conversation_history="\n ".join( [ "user: I'd like to travel to an event", - "agent: Sure! Let's start by finding an event you'd like to attend. Could you tell me which region and month you're interested in?", + "agent: Sure! Let's start by finding an event you'd like to attend. Could you tell me which city and month you're interested in?", "user: In Sao Paulo, Brazil, in February", "agent: Great! Let's find an events in Sao Paulo, Brazil in February.", "user_confirmed_tool_run: ", diff --git a/frontend/src/pages/App.jsx b/frontend/src/pages/App.jsx index 7ba5574..f68b0a1 100644 --- a/frontend/src/pages/App.jsx +++ b/frontend/src/pages/App.jsx @@ -61,11 +61,8 @@ export default function App() { const handleStartNewChat = async () => { try { - await fetch("http://127.0.0.1:8000/end-chat", { method: "POST" }); - // sleep for a bit to allow the server to process the end-chat request - await new Promise((resolve) => setTimeout(resolve, 1000)); // todo make less dodgy await fetch( - `http://127.0.0.1:8000/send-prompt?prompt=${encodeURIComponent("I'd like to travel to an event.")}`, + `http://127.0.0.1:8000/send-prompt?prompt=${encodeURIComponent("I'd like to travel for an event.")}`, { method: "POST" } ); setConversation([]); // clear local state diff --git a/scripts/find_events_test.py b/scripts/find_events_test.py new file mode 100644 index 0000000..f842be0 --- /dev/null +++ b/scripts/find_events_test.py @@ -0,0 +1,8 @@ +from tools.find_events import find_events +import json + +# Example usage +if __name__ == "__main__": + search_args = {"city": "Sydney", "month": "July"} + results = find_events(search_args) + print(json.dumps(results, indent=2)) diff --git a/scripts/flight_api_test.py b/scripts/flight_api_test.py new file mode 100644 index 0000000..208faff --- /dev/null +++ b/scripts/flight_api_test.py @@ -0,0 +1,7 @@ +from tools.search_flights import search_flights +import json + +if __name__ == "__main__": + # Suppose user typed "new" for New York, "lon" for London + flights = search_flights("London", "JFK", "2025-01-15", "2025-01-23") + print(json.dumps(flights, indent=2)) diff --git a/tools/data/find_events_data.json b/tools/data/find_events_data.json new file mode 100644 index 0000000..d7ae18f --- /dev/null +++ b/tools/data/find_events_data.json @@ -0,0 +1,252 @@ +{ + "Melbourne": [ + { + "eventName": "Australian Open", + "dateFrom": "2025-01-13", + "dateTo": "2025-01-26", + "description": "A two-week Grand Slam tennis tournament featuring the world's top players, accompanied by various entertainment options including live music and family-friendly activities." + }, + { + "eventName": "Melbourne International Comedy Festival", + "dateFrom": "2025-03-26", + "dateTo": "2025-04-20", + "description": "One of the world's largest comedy festivals, showcasing stand-up, cabaret, theatre, and street performances across numerous city venues." + }, + { + "eventName": "Melbourne International Film Festival (MIFF)", + "dateFrom": "2025-08-07", + "dateTo": "2025-08-23", + "description": "Established in 1952, MIFF presents a diverse selection of Australian and international films, including features, documentaries, and shorts." + }, + { + "eventName": "Melbourne Fringe Festival", + "dateFrom": "2025-09-17", + "dateTo": "2025-10-04", + "description": "An open-access arts festival featuring a wide array of art forms such as theatre, comedy, music, and digital art across various venues." + }, + { + "eventName": "Moomba Festival", + "dateFrom": "2025-03-07", + "dateTo": "2025-03-10", + "description": "Australia's largest free community festival, celebrated over four days during the Labour Day long weekend, including a parade, live music, fireworks, and the famous Birdman Rally along the Yarra River." + }, + { + "eventName": "White Night Melbourne", + "dateFrom": "2025-08-22", + "dateTo": "2025-08-24", + "description": "A dusk-to-dawn arts and cultural festival transforming the city with light installations, projections, music, and performances." + }, + { + "eventName": "Melbourne Food and Wine Festival", + "dateFrom": "2025-03-19", + "dateTo": "2025-03-29", + "description": "A celebration of Victoria's culinary scene, featuring food and wine events, masterclasses, and dining experiences." + } + ], + "Sydney": [ + { + "eventName": "Sydney Gay and Lesbian Mardi Gras", + "dateFrom": "2025-02-14", + "dateTo": "2025-03-01", + "description": "One of the largest LGBTQ+ festivals globally, featuring a vibrant parade, parties, and cultural events celebrating diversity and inclusion." + }, + { + "eventName": "Vivid Sydney", + "dateFrom": "2025-05-22", + "dateTo": "2025-06-13", + "description": "An annual festival of light, music, and ideas, transforming the city with mesmerizing light installations and projections." + }, + { + "eventName": "Sydney Festival", + "dateFrom": "2025-01-08", + "dateTo": "2025-01-26", + "description": "A major arts festival presenting a diverse program of theatre, dance, music, and visual arts across the city." + }, + { + "eventName": "Sculpture by the Sea, Bondi", + "dateFrom": "2025-10-23", + "dateTo": "2025-11-09", + "description": "An outdoor sculpture exhibition along the Bondi to Tamarama coastal walk, showcasing works by Australian and international artists." + }, + { + "eventName": "Sydney Writers' Festival", + "dateFrom": "2025-04-27", + "dateTo": "2025-05-03", + "description": "An annual literary festival featuring talks, panel discussions, and workshops with acclaimed authors and thinkers." + }, + { + "eventName": "Sydney Film Festival", + "dateFrom": "2025-06-04", + "dateTo": "2025-06-15", + "description": "One of the longest-running film festivals in the world, showcasing a diverse selection of local and international films." + } + ], + "Auckland": [ + { + "eventName": "Pasifika Festival", + "dateFrom": "2025-03-08", + "dateTo": "2025-03-09", + "description": "The largest Pacific Islands-themed festival globally, celebrating the diverse cultures of the Pacific with traditional cuisine, performances, and arts." + }, + { + "eventName": "Auckland Arts Festival", + "dateFrom": "2025-03-11", + "dateTo": "2025-03-29", + "description": "A biennial multi-arts festival showcasing local and international artists in theatre, dance, music, and visual arts." + }, + { + "eventName": "Auckland Writers Festival", + "dateFrom": "2025-05-13", + "dateTo": "2025-05-18", + "description": "An annual event bringing together international and local writers for discussions, readings, and workshops." + }, + { + "eventName": "Auckland Diwali Festival", + "dateFrom": "2025-10-26", + "dateTo": "2025-10-27", + "description": "A vibrant celebration of Indian culture and the Hindu festival of Diwali, featuring performances, food stalls, and traditional activities." + } + ], + "Brisbane": [ + { + "eventName": "Brisbane Festival", + "dateFrom": "2025-09-05", + "dateTo": "2025-09-26", + "description": "A major international arts festival featuring theatre, music, dance, and visual arts, culminating in the Riverfire fireworks display." + }, + { + "eventName": "NRL Magic Round", + "dateFrom": "2025-05-02", + "dateTo": "2025-05-04", + "description": "A rugby league extravaganza where all NRL matches for the round are played at Suncorp Stadium, attracting fans nationwide." + }, + { + "eventName": "Brisbane International Film Festival", + "dateFrom": "2025-10-01", + "dateTo": "2025-10-11", + "description": "Showcasing a curated selection of films from around the world, including premieres and special events." + }, + { + "eventName": "Brisbane Comedy Festival", + "dateFrom": "2025-02-22", + "dateTo": "2025-03-24", + "description": "A month-long comedy festival featuring local and international comedians in stand-up, sketch, and improv performances." + }, + { + "eventName": "Brisbane Writers Festival", + "dateFrom": "2025-09-05", + "dateTo": "2025-09-08", + "description": "An annual literary festival celebrating books, writing, and ideas with author talks, panel discussions, and workshops." + }, + { + "eventName": "Brisbane Asia Pacific Film Festival", + "dateFrom": "2025-11-29", + "dateTo": "2025-12-08", + "description": "Showcasing the best cinema from the Asia Pacific region, including features, documentaries, and short films." + } + ], + "Perth": [ + { + "eventName": "Perth Festival", + "dateFrom": "2025-02-07", + "dateTo": "2025-03-01", + "description": "Australia's longest-running cultural festival, offering a diverse program of music, theatre, dance, literature, and visual arts." + }, + { + "eventName": "Fringe World Festival", + "dateFrom": "2025-01-16", + "dateTo": "2025-02-15", + "description": "One of the largest fringe festivals globally, featuring a vast array of performances including comedy, cabaret, theatre, and street arts." + }, + { + "eventName": "Sculpture by the Sea", + "dateFrom": "2025-03-06", + "dateTo": "2025-03-23", + "description": "An annual outdoor sculpture exhibition along Cottesloe Beach, showcasing works from Australian and international artists." + }, + { + "eventName": "Revelation Perth International Film Festival", + "dateFrom": "2025-07-03", + "dateTo": "2025-07-13", + "description": "A showcase of independent cinema, featuring a diverse selection of films, documentaries, and short films." + }, + { + "eventName": "Perth Comedy Festival", + "dateFrom": "2025-04-22", + "dateTo": "2025-05-19", + "description": "A month-long comedy festival featuring local and international comedians in stand-up, sketch, and improv performances." + } + ], + "Adelaide": [ + { + "eventName": "Adelaide Festival", + "dateFrom": "2025-02-28", + "dateTo": "2025-03-15", + "description": "A premier arts festival offering a rich program of theatre, music, dance, and visual arts from renowned international and local artists." + }, + { + "eventName": "Adelaide Fringe", + "dateFrom": "2025-02-14", + "dateTo": "2025-03-15", + "description": "The largest open-access arts festival in the Southern Hemisphere, featuring thousands of performances across various genres and venues." + }, + { + "eventName": "SALA Festival", + "dateFrom": "2025-08-01", + "dateTo": "2025-08-31", + "description": "South Australia's largest visual arts festival, showcasing the work of local artists in exhibitions, workshops, and events." + }, + { + "eventName": "OzAsia Festival", + "dateFrom": "2025-09-25", + "dateTo": "2025-10-11", + "description": "A celebration of Asian arts and culture, featuring performances, exhibitions, and events from across the region." + }, + { + "eventName": "Adelaide Film Festival", + "dateFrom": "2025-10-16", + "dateTo": "2025-10-26", + "description": "Showcasing a diverse selection of Australian and international films, including features, documentaries, and shorts." + }, + { + "eventName": "Adelaide Writers' Week", + "dateFrom": "2025-03-01", + "dateTo": "2025-03-06", + "description": "An annual literary festival featuring talks, panel discussions, and readings by acclaimed authors and thinkers." + } + ], + "Wellington": [ + { + "eventName": "New Zealand Festival of the Arts", + "dateFrom": "2025-02-21", + "dateTo": "2025-03-15", + "description": "The nation's largest celebration of contemporary arts and culture, featuring a diverse range of performances and exhibitions across various venues in Wellington.", + "url": "https://www.festival.nz/" + }, + { + "eventName": "Wellington Jazz Festival", + "dateFrom": "2025-06-05", + "dateTo": "2025-06-09", + "description": "A five-day festival showcasing local and international jazz musicians in concerts, workshops, and community events.", + "url": "https://www.jazzfestival.co.nz/" + }, + { + "eventName": "Wellington on a Plate", + "dateFrom": "2025-08-01", + "dateTo": "2025-08-16", + "description": "A culinary festival celebrating the city's food and beverage industry with special menus, events, and culinary experiences." + }, + { + "eventName": "CubaDupa", + "dateFrom": "2025-03-28", + "dateTo": "2025-03-29", + "description": "A vibrant street festival in Wellington's Cuba Street, featuring music, dance, street performers, and food stalls." + }, + { + "eventName": "Wellington Pasifika Festival", + "dateFrom": "2025-01-18", + "dateTo": "2025-01-19", + "description": "A celebration of Pacific Island culture with traditional performances, food stalls, and arts and crafts." + } + ] +} \ No newline at end of file diff --git a/tools/find_events.py b/tools/find_events.py index b28d936..364afbc 100644 --- a/tools/find_events.py +++ b/tools/find_events.py @@ -1,17 +1,64 @@ -def find_events(args: dict) -> dict: - # Example: continent="Oceania", month="April" - region = args.get("region") - month = args.get("month") - print(f"[FindEvents] Searching events in {region} for {month} ...") +from datetime import datetime +from pathlib import Path +import json - # Stub result + +def find_events(args: dict) -> dict: + search_city = args.get("city", "").lower() + search_month = args.get("month", "").capitalize() + + file_path = Path(__file__).resolve().parent / "data" / "find_events_data.json" + if not file_path.exists(): + return {"error": "Data file not found."} + + try: + month_number = datetime.strptime(search_month, "%B").month + except ValueError: + return {"error": "Invalid month provided."} + + # Helper to wrap months into [1..12] + def get_adjacent_months(m): + prev_m = 12 if m == 1 else (m - 1) + next_m = 1 if m == 12 else (m + 1) + return [prev_m, m, next_m] + + valid_months = get_adjacent_months(month_number) + + matching_events = [] + for city_name, events in json.load(open(file_path)).items(): + if search_city and search_city not in city_name.lower(): + continue + + for event in events: + date_from = datetime.strptime(event["dateFrom"], "%Y-%m-%d") + date_to = datetime.strptime(event["dateTo"], "%Y-%m-%d") + + # If the event's start or end month is in our valid months + if date_from.month in valid_months or date_to.month in valid_months: + # Add metadata explaining how it matches + if date_from.month == month_number or date_to.month == month_number: + month_context = "requested month" + elif ( + date_from.month == valid_months[0] + or date_to.month == valid_months[0] + ): + month_context = "previous month" + else: + month_context = "next month" + + matching_events.append( + { + "city": city_name, + "eventName": event["eventName"], + "dateFrom": event["dateFrom"], + "dateTo": event["dateTo"], + "description": event["description"], + "monthContext": month_context, + } + ) + + # Add top-level metadata if you wish return { - "events": [ - { - "city": "Melbourne", - "eventName": "Melbourne International Comedy Festival", - "dateFrom": "2025-03-26", - "dateTo": "2025-04-20", - }, - ] + "note": f"Returning events from {search_month} plus one month either side (i.e., {', '.join(datetime(2025, m, 1).strftime('%B') for m in valid_months)}).", + "events": matching_events, } diff --git a/tools/search_flights.py b/tools/search_flights.py index b765ca8..46ae148 100644 --- a/tools/search_flights.py +++ b/tools/search_flights.py @@ -1,4 +1,175 @@ +import os +import json +import http.client +from dotenv import load_dotenv +import urllib.parse + + +def search_airport(query: str) -> list: + """ + Returns a list of matching airports/cities from sky-scrapper's searchAirport endpoint. + """ + load_dotenv() + api_key = os.getenv("RAPIDAPI_KEY", "YOUR_DEFAULT_KEY") + api_host = os.getenv("RAPIDAPI_HOST", "sky-scrapper.p.rapidapi.com") + + conn = http.client.HTTPSConnection(api_host) + headers = { + "x-rapidapi-key": api_key, + "x-rapidapi-host": api_host, + } + + # Sanitize the query to ensure it is URL-safe + print(f"Searching for: {query}") + encoded_query = urllib.parse.quote(query) + path = f"/api/v1/flights/searchAirport?query={encoded_query}&locale=en-US" + + conn.request("GET", path, headers=headers) + res = conn.getresponse() + if res.status != 200: + print(f"Error: API responded with status code {res.status}") + print(f"Response: {res.read().decode('utf-8')}") + return [] + + data = res.read() + conn.close() + + try: + return json.loads(data).get("data", []) + except json.JSONDecodeError: + return [] + + def search_flights(args: dict) -> dict: + """ + 1) Looks up airport/city codes via search_airport. + 2) Finds the first matching skyId/entityId for both origin & destination. + 3) Calls the flight search endpoint with those codes. + """ + date_depart = args.get("dateDepart") + date_return = args.get("dateReturn") + origin_query = args.get("origin") + dest_query = args.get("destination") + + # Step 1: Resolve skyIds + origin_candidates = search_airport(origin_query) + destination_candidates = search_airport(dest_query) + + if not origin_candidates or not destination_candidates: + return {"error": "No matches found for origin/destination"} + + origin_params = origin_candidates[0]["navigation"]["relevantFlightParams"] + dest_params = destination_candidates[0]["navigation"]["relevantFlightParams"] + + origin_sky_id = origin_params["skyId"] # e.g. "LOND" + origin_entity_id = origin_params["entityId"] # e.g. "27544008" + dest_sky_id = dest_params["skyId"] # e.g. "NYCA" + dest_entity_id = dest_params["entityId"] # e.g. "27537542" + + # Step 2: Call flight search with resolved codes + load_dotenv() + api_key = os.getenv("RAPIDAPI_KEY", "YOUR_DEFAULT_KEY") + api_host = os.getenv("RAPIDAPI_HOST", "sky-scrapper.p.rapidapi.com") + + conn = http.client.HTTPSConnection(api_host) + headers = { + "x-rapidapi-key": api_key, + "x-rapidapi-host": api_host, + } + + path = ( + "/api/v2/flights/searchFlights?" + f"originSkyId={origin_sky_id}" + f"&destinationSkyId={dest_sky_id}" + f"&originEntityId={origin_entity_id}" + f"&destinationEntityId={dest_entity_id}" + f"&date={date_depart}" + f"&returnDate={date_return}" + f"&cabinClass=economy&adults=1&sortBy=best¤cy=USD" + f"&market=en-US&countryCode=US" + ) + + conn.request("GET", path, headers=headers) + res = conn.getresponse() + data = res.read() + conn.close() + + try: + json_data = json.loads(data) + except json.JSONDecodeError: + return {"error": "Invalid JSON response"} + + itineraries = json_data.get("data", {}).get("itineraries", []) + if not itineraries: + return json_data # Return raw response for debugging if itineraries are empty + + formatted_results = [] + seen_carriers = set() + + for itinerary in itineraries: + legs = itinerary.get("legs", []) + if len(legs) >= 2: + # Extract outbound and return flight details + outbound_leg = legs[0] + return_leg = legs[1] + + # Get the first segment for flight details + outbound_flight = outbound_leg.get("segments", [{}])[0] + return_flight = return_leg.get("segments", [{}])[0] + + # Extract flight details + outbound_carrier = outbound_flight.get("operatingCarrier", {}).get( + "name", "N/A" + ) + outbound_carrier_code = outbound_flight.get("operatingCarrier", {}).get( + "alternateId", "" + ) + outbound_flight_number = outbound_flight.get("flightNumber", "N/A") + outbound_flight_code = ( + f"{outbound_carrier_code}{outbound_flight_number}" + if outbound_carrier_code + else outbound_flight_number + ) + + return_carrier = return_flight.get("operatingCarrier", {}).get( + "name", "N/A" + ) + return_carrier_code = return_flight.get("operatingCarrier", {}).get( + "alternateId", "" + ) + return_flight_number = return_flight.get("flightNumber", "N/A") + return_flight_code = ( + f"{return_carrier_code}{return_flight_number}" + if return_carrier_code + else return_flight_number + ) + + # Check if carrier is unique + if outbound_carrier not in seen_carriers: + seen_carriers.add(outbound_carrier) # Add to seen carriers + formatted_results.append( + { + "outbound_flight_code": outbound_flight_code, + "operating_carrier": outbound_carrier, + "return_flight_code": return_flight_code, + "return_operating_carrier": return_carrier, + "price": itinerary.get("price", {}).get("raw", 0.0), + } + ) + + # Stop after finding 3 unique carriers + if len(formatted_results) >= 3: + break + + return { + "origin": origin_query, + "destination": dest_query, + "currency": "USD", + "results": formatted_results, + } + + +def search_flights_example(args: dict) -> dict: """ Example function for searching flights. Currently just prints/returns the passed args, diff --git a/tools/tool_registry.py b/tools/tool_registry.py index 1bd4971..18649c8 100644 --- a/tools/tool_registry.py +++ b/tools/tool_registry.py @@ -2,12 +2,12 @@ from models.tool_definitions import ToolDefinition, ToolArgument find_events_tool = ToolDefinition( name="FindEvents", - description="Find upcoming events to travel to given a location or region (e.g., 'Oceania') and a date or month", + description="Find upcoming events to travel to given a location or city (e.g., 'Oceania') and a date or month", arguments=[ ToolArgument( - name="region", + name="city", type="string", - description="Which region to search for events", + description="Which city to search for events", ), ToolArgument( name="month", @@ -25,12 +25,12 @@ search_flights_tool = ToolDefinition( ToolArgument( name="origin", type="string", - description="Airport or city (infer airport code from city)", + description="Airport or city (infer airport code from city and store)", ), ToolArgument( name="destination", type="string", - description="Airport or city code for arrival (infer airport code from city)", + description="Airport or city code for arrival (infer airport code from city and store)", ), ToolArgument( name="dateDepart",