From 4f3686026241a7ab6c39e799f63c7822c083260a Mon Sep 17 00:00:00 2001 From: Filipe Ximenes Date: Fri, 23 Aug 2024 17:28:13 -0300 Subject: [PATCH] setups an endpoint that finds and lists attractions to a given coordinate --- example/demo/urls.py | 5 ++ example/demo/views.py | 22 ++++++++ example/tour_guide/ai_assistants.py | 83 +++++++++-------------------- example/tour_guide/integrations.py | 56 +++++++++++++------ 4 files changed, 90 insertions(+), 76 deletions(-) diff --git a/example/demo/urls.py b/example/demo/urls.py index fed2660..20eaedc 100644 --- a/example/demo/urls.py +++ b/example/demo/urls.py @@ -11,6 +11,11 @@ views.AIAssistantChatThreadView.as_view(), name="chat_thread", ), + path( + "tour-guide/", + views.TourGuideAssistantView.as_view(), + name="tour_guide", + ), # Catch all for react app: path("", views.react_index, {"resource": ""}), path("", views.react_index), diff --git a/example/demo/views.py b/example/demo/views.py index 053b821..1d8a9f8 100644 --- a/example/demo/views.py +++ b/example/demo/views.py @@ -1,8 +1,14 @@ +import json + from django.contrib import messages +from django.contrib.auth.models import User +from django.http import JsonResponse from django.shortcuts import get_object_or_404, redirect, render +from django.views import View from django.views.generic.base import TemplateView from pydantic import ValidationError +from tour_guide.ai_assistants import TourGuideAIAssistant from weather.ai_assistants import WeatherAIAssistant from django_ai_assistant.api.schemas import ( @@ -102,3 +108,19 @@ def post(self, request, *args, **kwargs): request=request, ) return redirect("chat_thread", thread_id=thread_id) + + +class TourGuideAssistantView(View): + def get(self, request, *args, **kwargs): + coordinates = request.GET.get("coordinate") + + if not coordinates: + return JsonResponse({}) + + thread = create_thread(name="Tour Guide Chat", user=User.objects.first()) + + a = TourGuideAIAssistant() + data = a.run(f"My coordinates are: ({coordinates})", thread.id) + print(data) + + return JsonResponse(json.loads(data)) diff --git a/example/tour_guide/ai_assistants.py b/example/tour_guide/ai_assistants.py index affd6db..51297ed 100644 --- a/example/tour_guide/ai_assistants.py +++ b/example/tour_guide/ai_assistants.py @@ -1,35 +1,9 @@ import json -import requests -from typing import Sequence - -from osmapi import OsmApi from django.utils import timezone -from langchain_community.tools.tavily_search import TavilySearchResults -from langchain_core.tools import BaseTool - from django_ai_assistant import AIAssistant, method_tool - - -# # Note this assistant is not registered, but we'll use it as a tool on the other. -# # This one shouldn't be used directly, as it does web searches and scraping. -# class OpenStreetMapsAPITool(AIAssistant): -# id = "open_street_maps_api_tool" # noqa: A003 -# instructions = ( -# "You're a tool to find the nearby attractions of a given location. " -# "Use the Open Street Maps API to find nearby attractions around the location, up to a 500m diameter from the point. " -# "Then check results and provide only the nearby attractions and their details to the user." -# ) -# name = "Open Street Maps API Tool" -# model = "gpt-4o" - -# def get_instructions(self): -# # Warning: this will use the server's timezone -# # See: https://docs.djangoproject.com/en/5.0/topics/i18n/timezones/#default-time-zone-and-current-time-zone -# # In a real application, you should use the user's timezone -# current_date_str = timezone.now().date().isoformat() -# return f"{self.instructions} Today is: {current_date_str}." +from tour_guide.integrations import fetch_points_of_interest def _tour_guide_example_json(): @@ -38,8 +12,7 @@ def _tour_guide_example_json(): "nearby_attractions": [ { "attraction_name": f"", - "attraction_description": f"", - "attraction_image_url": f"", + "attraction_description": f"", "attraction_url": f"", } for i in range(1, 6) @@ -61,14 +34,20 @@ class TourGuideAIAssistant(AIAssistant): name = "Tour Guide Assistant" instructions = ( "You are a tour guide assistant that offers information about nearby attractions. " - "The user is at a location, passed as a combination of latitude and longitude, and wants to know what to learn about nearby attractions. " + "The application will capture the user coordinates, and should provide a list of nearby attractions. " "Use the available tools to suggest nearby attractions to the user. " - "If there are no interesting attractions nearby, " - "tell the user there's nothing to see where they're at. " - "Use three sentences maximum and keep your suggestions concise." - "Your response will be integrated with a frontend web application," - "therefore it's critical to reply with only JSON output in the following structure: \n" - f"```json\n{_tour_guide_example_json()}\n```" + "You don't need to include all the found items, only include attractions that are relevant for a tourist. " + "Select the top 10 best attractions for a tourist, if there are less then 10 relevant items only return these. " + "Order items by the most relevant to the least relevant. " + "If there are no relevant attractions nearby, just keep the list empty. " + "Your response will be integrated with a frontend web application therefore it's critical that " + "it only contains a valid JSON. DON'T include '```json' in your response. " + "The JSON should be formatted according to the following structure: \n" + f"\n\n{_tour_guide_example_json()}\n\n\n" + "In the 'attraction_name' field provide the name of the attraction in english. " + "In the 'attraction_description' field generate an overview about the attraction with the most important information, " + "curiosities and interesting facts. " + "Only include a value for the 'attraction_url' field if you find a real value in the provided data otherwise keep it empty. " ) model = "gpt-4o" @@ -78,28 +57,14 @@ def get_instructions(self): # In a real application, you should use the user's timezone current_date_str = timezone.now().date().isoformat() - return "\n".join( - [ - self.instructions, - f"Today is: {current_date_str}", - f"The user's current location, considering latitude and longitude is: {self.get_user_location()}", - ] - ) - - def get_tools(self) -> Sequence[BaseTool]: - return [ - TavilySearchResults(), - # OpenStreetMapsAPITool().as_tool(description="Tool to query the Open Street Maps API for location information."), - *super().get_tools(), - ] + return f"Today is: {current_date_str}. {self.instructions}" @method_tool - def get_user_location(self) -> dict: - """Get user's current location.""" - return {"lat": "X", "lon": "Y"} # replace with actual coordinates - - @method_tool - def get_nearby_attractions_from_api(self) -> dict: - api = OsmApi() - """Get nearby attractions based on user's current location.""" - return {} + def get_nearby_attractions_from_api(self, latitude: float, longitude: float) -> dict: + """Find nearby attractions based on user's current location.""" + return fetch_points_of_interest( + latitude=latitude, + longitude=longitude, + tags=["tourism", "leisure", "place", "building"], + radius=500, + ) diff --git a/example/tour_guide/integrations.py b/example/tour_guide/integrations.py index 37cdb7a..6357b8a 100644 --- a/example/tour_guide/integrations.py +++ b/example/tour_guide/integrations.py @@ -1,23 +1,45 @@ -from requests_oauth2client import OAuth2Client -import osmapi -from django.conf import settings +from typing import List +import requests -client_id = settings.OPEN_STREET_MAPS_CLIENT_ID -client_secret = settings.OPEN_STREET_MAPS_CLIENT_SECRET -# special value for redirect_uri for non-web applications -redirect_uri = "urn:ietf:wg:oauth:2.0:oob" +def fetch_points_of_interest( + latitude: float, longitude: float, tags: List[str], radius: int = 500 +) -> dict: + """ + Fetch points of interest from OpenStreetMap using Overpass API. -authorization_base_url = "https://master.apis.dev.openstreetmap.org/oauth2/authorize" -token_url = "https://master.apis.dev.openstreetmap.org/oauth2/token" + :param latitude: Latitude of the center point. + :param longitude: Longitude of the center point. + :param radius: Radius in meters to search for POIs around the center point. + :param tags: A list of OpenStreetMap tags to filter the POIs (e.g., ["amenity", "tourism"]). + :return: A list of POIs with their details. + """ + # Base URL for the Overpass API + overpass_url = "http://overpass-api.de/api/interpreter" -oauth2client = OAuth2Client( - token_endpoint=token_url, - authorization_endpoint=authorization_base_url, - redirect_uri=redirect_uri, - auth=(client_id, client_secret), - code_challenge_method=None, -) + # Construct the Overpass QL (query language) query + pois_query = "".join( + [ + ( + f"node[{tag}](around:{radius},{latitude},{longitude});" + f"way[{tag}](around:{radius},{latitude},{longitude});" + ) + for tag in tags + ] + ) -api = osmapi.OsmApi(api="https://api06.dev.openstreetmap.org") \ No newline at end of file + query = f""" + [out:json]; + ( + {pois_query} + ); + out tags; + """ + + response = requests.get(overpass_url, params={"data": query}, timeout=10) + + response.raise_for_status() + + data = response.json() + return data["elements"]