From c5303833b35fd1552f6c409f7cd2374c1a4a773f Mon Sep 17 00:00:00 2001 From: amandasavluchinske Date: Wed, 3 Jul 2024 09:24:52 -0300 Subject: [PATCH 1/9] Pushes partial work related to JSON tour guide --- example/assets/js/App.tsx | 9 +- .../js/components/ResultsPage/ResultsPage.tsx | 36 ++++++ example/assets/js/components/index.ts | 1 + example/example/settings.py | 1 + example/tour_guide/__init__.py | 0 example/tour_guide/ai_assistants.py | 105 ++++++++++++++++++ example/tour_guide/apps.py | 6 + example/tour_guide/integrations.py | 23 ++++ example/tour_guide/migrations/__init__.py | 0 poetry.lock | 16 ++- pyproject.toml | 1 + 11 files changed, 196 insertions(+), 2 deletions(-) create mode 100644 example/assets/js/components/ResultsPage/ResultsPage.tsx create mode 100644 example/tour_guide/__init__.py create mode 100644 example/tour_guide/ai_assistants.py create mode 100644 example/tour_guide/apps.py create mode 100644 example/tour_guide/integrations.py create mode 100644 example/tour_guide/migrations/__init__.py diff --git a/example/assets/js/App.tsx b/example/assets/js/App.tsx index 904c47e..03983df 100644 --- a/example/assets/js/App.tsx +++ b/example/assets/js/App.tsx @@ -20,7 +20,7 @@ import { IconMovie, IconChecklist, } from "@tabler/icons-react"; -import { Chat } from "@/components"; +import { Chat, ResultsPage } from "@/components"; import { createBrowserRouter, Link, RouterProvider } from "react-router-dom"; import { ApiError, @@ -139,6 +139,9 @@ const ExampleIndex = () => { > HTMX demo (no React) + + Tour Guide Assistant + ); @@ -198,6 +201,10 @@ const router = createBrowserRouter([ ), }, + { + path: "/tour-guide", + element: (), + }, { path: "/admin", element: ( diff --git a/example/assets/js/components/ResultsPage/ResultsPage.tsx b/example/assets/js/components/ResultsPage/ResultsPage.tsx new file mode 100644 index 0000000..0d2e345 --- /dev/null +++ b/example/assets/js/components/ResultsPage/ResultsPage.tsx @@ -0,0 +1,36 @@ +import "@mantine/core/styles.css"; + +import { + Container, + createTheme, + List, + MantineProvider, + Title, +} from "@mantine/core"; +import { useState } from "react"; + +const theme = createTheme({}); + +export function ResultsPage({ assistantId }: { assistantId: string }) { + const [lat, setLat] = useState(0); + const [lon, setLon] = useState(0); + const successCallback = (position) => { + console.log(position); + setLat(position.coords.latitude); + setLon(position.coords.longitude); + }; + + const errorCallback = (error) => { + console.log(error); + }; + + navigator.geolocation.getCurrentPosition(successCallback, errorCallback); + return ( + + hi + lat: {lat} + lon: {lon} + + ); +}; + diff --git a/example/assets/js/components/index.ts b/example/assets/js/components/index.ts index 86bd4fa..c386cf4 100644 --- a/example/assets/js/components/index.ts +++ b/example/assets/js/components/index.ts @@ -1,2 +1,3 @@ export { ThreadsNav } from "./ThreadsNav/ThreadsNav"; export { Chat } from "./Chat/Chat"; +export { ResultsPage } from "./ResultsPage/ResultsPage"; diff --git a/example/example/settings.py b/example/example/settings.py index fc08b26..dadb501 100644 --- a/example/example/settings.py +++ b/example/example/settings.py @@ -34,6 +34,7 @@ "movies", "rag", "issue_tracker", + "tour_guide", ] MIDDLEWARE = [ diff --git a/example/tour_guide/__init__.py b/example/tour_guide/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/example/tour_guide/ai_assistants.py b/example/tour_guide/ai_assistants.py new file mode 100644 index 0000000..affd6db --- /dev/null +++ b/example/tour_guide/ai_assistants.py @@ -0,0 +1,105 @@ +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}." + + +def _tour_guide_example_json(): + return json.dumps( + { + "nearby_attractions": [ + { + "attraction_name": f"", + "attraction_description": f"", + "attraction_image_url": f"", + "attraction_url": f"", + } + for i in range(1, 6) + ] + }, + indent=2, + ).translate( # Necessary due to ChatPromptTemplate + str.maketrans( + { + "{": "{{", + "}": "}}", + } + ) + ) + + +class TourGuideAIAssistant(AIAssistant): + id = "tour_guide_assistant" # noqa: A003 + 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. " + "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```" + ) + 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 "\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(), + ] + + @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 {} diff --git a/example/tour_guide/apps.py b/example/tour_guide/apps.py new file mode 100644 index 0000000..60a8f92 --- /dev/null +++ b/example/tour_guide/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class TourGuideConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "tour_guide" diff --git a/example/tour_guide/integrations.py b/example/tour_guide/integrations.py new file mode 100644 index 0000000..37cdb7a --- /dev/null +++ b/example/tour_guide/integrations.py @@ -0,0 +1,23 @@ +from requests_oauth2client import OAuth2Client +import osmapi +from django.conf import settings + + +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" + +authorization_base_url = "https://master.apis.dev.openstreetmap.org/oauth2/authorize" +token_url = "https://master.apis.dev.openstreetmap.org/oauth2/token" + +oauth2client = OAuth2Client( + token_endpoint=token_url, + authorization_endpoint=authorization_base_url, + redirect_uri=redirect_uri, + auth=(client_id, client_secret), + code_challenge_method=None, +) + +api = osmapi.OsmApi(api="https://api06.dev.openstreetmap.org") \ No newline at end of file diff --git a/example/tour_guide/migrations/__init__.py b/example/tour_guide/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/poetry.lock b/poetry.lock index 0d40f22..0e18c95 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2297,6 +2297,20 @@ files = [ {file = "orjson-3.10.5.tar.gz", hash = "sha256:7a5baef8a4284405d96c90c7c62b755e9ef1ada84c2406c24a9ebec86b89f46d"}, ] +[[package]] +name = "osmapi" +version = "4.1.0" +description = "Python wrapper for the OSM API" +optional = false +python-versions = ">=3.8" +files = [ + {file = "osmapi-4.1.0-py3-none-any.whl", hash = "sha256:3d50f1e469a23d6a7855edc604c233b7019c89762111b9f8f92a1757aa500720"}, + {file = "osmapi-4.1.0.tar.gz", hash = "sha256:7b51804f8d0da953376cdfb6f9cfcb79b501312e535b31c654d213d9b46f0270"}, +] + +[package.dependencies] +requests = "*" + [[package]] name = "packaging" version = "24.1" @@ -3897,4 +3911,4 @@ test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", [metadata] lock-version = "2.0" python-versions = ">=3.10,<3.13" -content-hash = "e5a78c3a532295d2a099fda693801334694416c8748f7274d64a5dfa779f5518" +content-hash = "343656c8ce163fd6c8092db61cbfced093550345c61d6a399cb8107d732cc18e" diff --git a/pyproject.toml b/pyproject.toml index 08e93f5..b4194eb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,6 +51,7 @@ pydantic = "^2.7.1" django-ninja = "^1.1.0" langchain = "^0.2.1" langchain-openai = "^0.1.8" +osmapi = "^4.1.0" [tool.poetry.group.dev.dependencies] coverage = "^7.2.7" From 4f3686026241a7ab6c39e799f63c7822c083260a Mon Sep 17 00:00:00 2001 From: Filipe Ximenes Date: Fri, 23 Aug 2024 17:28:13 -0300 Subject: [PATCH 2/9] 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"] From 078132abfb0ee882643c50acc193faa3b1a413b8 Mon Sep 17 00:00:00 2001 From: Filipe Ximenes Date: Fri, 23 Aug 2024 18:27:39 -0300 Subject: [PATCH 3/9] adds a frontend to use the tour guide assistant --- example/assets/js/App.tsx | 4 +- example/assets/js/components/Chat/Chat.tsx | 1 - .../js/components/ResultsPage/ResultsPage.tsx | 36 ------------- .../js/components/TourGuide/TourGuide.tsx | 53 +++++++++++++++++++ example/assets/js/components/index.ts | 2 +- example/demo/views.py | 1 - example/package.json | 1 + example/pnpm-lock.yaml | 11 ++-- 8 files changed, 64 insertions(+), 45 deletions(-) delete mode 100644 example/assets/js/components/ResultsPage/ResultsPage.tsx create mode 100644 example/assets/js/components/TourGuide/TourGuide.tsx diff --git a/example/assets/js/App.tsx b/example/assets/js/App.tsx index 03983df..6afe750 100644 --- a/example/assets/js/App.tsx +++ b/example/assets/js/App.tsx @@ -20,7 +20,7 @@ import { IconMovie, IconChecklist, } from "@tabler/icons-react"; -import { Chat, ResultsPage } from "@/components"; +import { Chat, TourGuide } from "@/components"; import { createBrowserRouter, Link, RouterProvider } from "react-router-dom"; import { ApiError, @@ -203,7 +203,7 @@ const router = createBrowserRouter([ }, { path: "/tour-guide", - element: (), + element: (), }, { path: "/admin", diff --git a/example/assets/js/components/Chat/Chat.tsx b/example/assets/js/components/Chat/Chat.tsx index a871912..1ab333f 100644 --- a/example/assets/js/components/Chat/Chat.tsx +++ b/example/assets/js/components/Chat/Chat.tsx @@ -1,7 +1,6 @@ import { ActionIcon, Avatar, - Box, Button, Container, Group, diff --git a/example/assets/js/components/ResultsPage/ResultsPage.tsx b/example/assets/js/components/ResultsPage/ResultsPage.tsx deleted file mode 100644 index 0d2e345..0000000 --- a/example/assets/js/components/ResultsPage/ResultsPage.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import "@mantine/core/styles.css"; - -import { - Container, - createTheme, - List, - MantineProvider, - Title, -} from "@mantine/core"; -import { useState } from "react"; - -const theme = createTheme({}); - -export function ResultsPage({ assistantId }: { assistantId: string }) { - const [lat, setLat] = useState(0); - const [lon, setLon] = useState(0); - const successCallback = (position) => { - console.log(position); - setLat(position.coords.latitude); - setLon(position.coords.longitude); - }; - - const errorCallback = (error) => { - console.log(error); - }; - - navigator.geolocation.getCurrentPosition(successCallback, errorCallback); - return ( - - hi - lat: {lat} - lon: {lon} - - ); -}; - diff --git a/example/assets/js/components/TourGuide/TourGuide.tsx b/example/assets/js/components/TourGuide/TourGuide.tsx new file mode 100644 index 0000000..a5bef1d --- /dev/null +++ b/example/assets/js/components/TourGuide/TourGuide.tsx @@ -0,0 +1,53 @@ +import "@mantine/core/styles.css"; +import { Container } from "@mantine/core"; +import axios from 'axios'; +import { useEffect, useState } from "react"; + + +export function TourGuide() { + const [latitude, setLatitude] = useState(""); + const [longitude, setLongitude] = useState(""); + const [attractions, setAttractions] = useState([]); + const [loading, setLoading] = useState(false); + + navigator.geolocation.getCurrentPosition((position: any) => { + if (latitude && longitude) { + return; + } + setLatitude(position.coords.latitude); + setLongitude(position.coords.longitude); + }, (error) => console.log(error)); + + function findAttractions() { + if (!latitude || !longitude) { + return; + } + + setLoading(true) + axios.get(`/tour-guide/?coordinate=${latitude},${longitude}`) + .then((response: any) => { + setAttractions(response.data.nearby_attractions) + }).finally(() => setLoading(false)) + } + + console.log(attractions) + + return ( + + Latitude: setLatitude(e.target.value)} /> + Longitude: setLongitude(e.target.value)} /> + + {loading ?

Loading

: null} +
+ {attractions.map((item, i) => +
+

{item.attraction_url ? {item.attraction_name} : item.attraction_name }

+ {item.attraction_description} + +
+ )} +
+
+ ); +}; + diff --git a/example/assets/js/components/index.ts b/example/assets/js/components/index.ts index c386cf4..c50b84e 100644 --- a/example/assets/js/components/index.ts +++ b/example/assets/js/components/index.ts @@ -1,3 +1,3 @@ export { ThreadsNav } from "./ThreadsNav/ThreadsNav"; export { Chat } from "./Chat/Chat"; -export { ResultsPage } from "./ResultsPage/ResultsPage"; +export { TourGuide } from "./TourGuide/TourGuide"; diff --git a/example/demo/views.py b/example/demo/views.py index 1d8a9f8..adb5417 100644 --- a/example/demo/views.py +++ b/example/demo/views.py @@ -121,6 +121,5 @@ def get(self, request, *args, **kwargs): a = TourGuideAIAssistant() data = a.run(f"My coordinates are: ({coordinates})", thread.id) - print(data) return JsonResponse(json.loads(data)) diff --git a/example/package.json b/example/package.json index 381fa9e..484c709 100644 --- a/example/package.json +++ b/example/package.json @@ -43,6 +43,7 @@ "@mantine/hooks": "^7.11.0", "@mantine/notifications": "^7.11.0", "@tabler/icons-react": "^3.7.0", + "axios": "^1.7.5", "cookie": "^0.6.0", "django-ai-assistant-client": "0.0.1", "modern-normalize": "^2.0.0", diff --git a/example/pnpm-lock.yaml b/example/pnpm-lock.yaml index d390fd4..57ad579 100644 --- a/example/pnpm-lock.yaml +++ b/example/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: '@tabler/icons-react': specifier: ^3.7.0 version: 3.7.0(react@18.3.1) + axios: + specifier: ^1.7.5 + version: 1.7.5 cookie: specifier: ^0.6.0 version: 0.6.0 @@ -1361,8 +1364,8 @@ packages: peerDependencies: postcss: ^8.1.0 - axios@1.7.2: - resolution: {integrity: sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==} + axios@1.7.5: + resolution: {integrity: sha512-fZu86yCo+svH3uqJ/yTdQ0QHpQu5oL+/QE+QPSv6BZSkDAoky9vytxp7u5qk83OJFS3kEBcesWni9WTZAv3tSw==} babel-loader@9.1.3: resolution: {integrity: sha512-xG3ST4DglodGf8qSwv0MdeWLhrDsw/32QMdTO5T1ZIp9gQur0HkCyFs7Awskr10JKXFXwpAhiCuYX5oGXnRGbw==} @@ -4944,7 +4947,7 @@ snapshots: postcss: 8.4.38 postcss-value-parser: 4.2.0 - axios@1.7.2: + axios@1.7.5: dependencies: follow-redirects: 1.15.6 form-data: 4.0.0 @@ -5253,7 +5256,7 @@ snapshots: django-ai-assistant-client@0.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - axios: 1.7.2 + axios: 1.7.5 cookie: 0.6.0 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) From eab9d96a9c577ea48296cbc59df81821d0e6eafd Mon Sep 17 00:00:00 2001 From: Filipe Ximenes Date: Tue, 27 Aug 2024 11:28:20 -0300 Subject: [PATCH 4/9] adds link to google maps to tour guide search results --- .../js/components/TourGuide/TourGuide.module.css | 6 ++++++ example/assets/js/components/TourGuide/TourGuide.tsx | 11 +++++++---- 2 files changed, 13 insertions(+), 4 deletions(-) create mode 100644 example/assets/js/components/TourGuide/TourGuide.module.css diff --git a/example/assets/js/components/TourGuide/TourGuide.module.css b/example/assets/js/components/TourGuide/TourGuide.module.css new file mode 100644 index 0000000..7a44745 --- /dev/null +++ b/example/assets/js/components/TourGuide/TourGuide.module.css @@ -0,0 +1,6 @@ +.searchBar { + padding: 10px 0 5px 0; + .searchItem { + padding-right: 5px; + } +} diff --git a/example/assets/js/components/TourGuide/TourGuide.tsx b/example/assets/js/components/TourGuide/TourGuide.tsx index a5bef1d..3c08e96 100644 --- a/example/assets/js/components/TourGuide/TourGuide.tsx +++ b/example/assets/js/components/TourGuide/TourGuide.tsx @@ -2,6 +2,7 @@ import "@mantine/core/styles.css"; import { Container } from "@mantine/core"; import axios from 'axios'; import { useEffect, useState } from "react"; +import classes from "./TourGuide.module.css"; export function TourGuide() { @@ -34,16 +35,18 @@ export function TourGuide() { return ( - Latitude: setLatitude(e.target.value)} /> - Longitude: setLongitude(e.target.value)} /> - +
+ Latitude: setLatitude(e.target.value)} /> + Longitude: setLongitude(e.target.value)} /> + +
{loading ?

Loading

: null}
{attractions.map((item, i) =>

{item.attraction_url ? {item.attraction_name} : item.attraction_name }

{item.attraction_description} - +
)}
From 90f3a9ce811c87182e979f2430eacc7aa69178d3 Mon Sep 17 00:00:00 2001 From: Filipe Ximenes Date: Tue, 27 Aug 2024 11:34:25 -0300 Subject: [PATCH 5/9] removes unecessary osmap dependency --- frontend/openapi_schema.json | 2 +- pyproject.toml | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/frontend/openapi_schema.json b/frontend/openapi_schema.json index ee16121..1af3897 100644 --- a/frontend/openapi_schema.json +++ b/frontend/openapi_schema.json @@ -2,7 +2,7 @@ "openapi": "3.1.0", "info": { "title": "django_ai_assistant", - "version": "0.0.2", + "version": "0.0.4", "description": "" }, "paths": { diff --git a/pyproject.toml b/pyproject.toml index 4f79304..47cbaba 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,7 +49,6 @@ pydantic = "^2.7.1" django-ninja = "^1.1.0" langchain = "^0.2.1" langchain-openai = "^0.1.8" -osmapi = "^4.1.0" [tool.poetry.group.dev.dependencies] coverage = "^7.2.7" From 6ccecb775f312e6ac194304a0ac66e10cb4344bd Mon Sep 17 00:00:00 2001 From: Filipe Ximenes Date: Tue, 27 Aug 2024 13:59:07 -0300 Subject: [PATCH 6/9] fixes PR comments for the tour guide app --- example/assets/js/App.tsx | 15 +++- .../components/TourGuide/TourGuide.module.css | 9 +- .../js/components/TourGuide/TourGuide.tsx | 89 +++++++++++++------ example/demo/views.py | 6 +- example/package.json | 1 - example/pnpm-lock.yaml | 3 - 6 files changed, 85 insertions(+), 38 deletions(-) diff --git a/example/assets/js/App.tsx b/example/assets/js/App.tsx index 6afe750..b879996 100644 --- a/example/assets/js/App.tsx +++ b/example/assets/js/App.tsx @@ -19,6 +19,7 @@ import { IconXboxX, IconMovie, IconChecklist, + IconPlane, } from "@tabler/icons-react"; import { Chat, TourGuide } from "@/components"; import { createBrowserRouter, Link, RouterProvider } from "react-router-dom"; @@ -139,7 +140,13 @@ const ExampleIndex = () => { > HTMX demo (no React) - + + + + } + > Tour Guide Assistant @@ -203,7 +210,11 @@ const router = createBrowserRouter([ }, { path: "/tour-guide", - element: (), + element: ( + + + + ), }, { path: "/admin", diff --git a/example/assets/js/components/TourGuide/TourGuide.module.css b/example/assets/js/components/TourGuide/TourGuide.module.css index 7a44745..ea4ef86 100644 --- a/example/assets/js/components/TourGuide/TourGuide.module.css +++ b/example/assets/js/components/TourGuide/TourGuide.module.css @@ -1,6 +1,9 @@ .searchBar { - padding: 10px 0 5px 0; - .searchItem { - padding-right: 5px; + .inputBlock{ + display: inline-block; + margin-right: 5px; + } + .coordinateInput { + width: 300px; } } diff --git a/example/assets/js/components/TourGuide/TourGuide.tsx b/example/assets/js/components/TourGuide/TourGuide.tsx index 3c08e96..fcdcfb2 100644 --- a/example/assets/js/components/TourGuide/TourGuide.tsx +++ b/example/assets/js/components/TourGuide/TourGuide.tsx @@ -1,56 +1,91 @@ import "@mantine/core/styles.css"; -import { Container } from "@mantine/core"; -import axios from 'axios'; -import { useEffect, useState } from "react"; +import { Container, TextInput, Button } from "@mantine/core"; +import { useState } from "react"; import classes from "./TourGuide.module.css"; - export function TourGuide() { const [latitude, setLatitude] = useState(""); const [longitude, setLongitude] = useState(""); const [attractions, setAttractions] = useState([]); const [loading, setLoading] = useState(false); - navigator.geolocation.getCurrentPosition((position: any) => { - if (latitude && longitude) { - return; - } - setLatitude(position.coords.latitude); - setLongitude(position.coords.longitude); - }, (error) => console.log(error)); + navigator.geolocation.getCurrentPosition( + (position: any) => { + if (latitude && longitude) { + return; + } + setLatitude(position.coords.latitude); + setLongitude(position.coords.longitude); + }, + (error) => console.log(error) + ); function findAttractions() { if (!latitude || !longitude) { return; } - setLoading(true) - axios.get(`/tour-guide/?coordinate=${latitude},${longitude}`) - .then((response: any) => { - setAttractions(response.data.nearby_attractions) - }).finally(() => setLoading(false)) - } + setLoading(true); + fetch(`/tour-guide/?coordinate=${latitude},${longitude}`) + .then((response) => response.json()) + .then((data: any) => { + console.log(data); - console.log(attractions) + setAttractions(data.nearby_attractions); + }) + .finally(() => setLoading(false)); + } return (
- Latitude: setLatitude(e.target.value)} /> - Longitude: setLongitude(e.target.value)} /> - + + Latitude: + setLatitude(e.target.value)} + className={classes.coordinateInput} + /> + + + Longitude: + setLongitude(e.target.value)} + className={classes.coordinateInput} + /> + +
{loading ?

Loading

: null}
- {attractions.map((item, i) => + {attractions.map((item, i) => (
-

{item.attraction_url ? {item.attraction_name} : item.attraction_name }

+

+ {item.attraction_url ? ( + + {item.attraction_name} + + ) : ( + item.attraction_name + )} +

{item.attraction_description} - +
- )} + ))}
); -}; - +} diff --git a/example/demo/views.py b/example/demo/views.py index adb5417..467b768 100644 --- a/example/demo/views.py +++ b/example/demo/views.py @@ -1,9 +1,9 @@ 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.utils import timezone from django.views import View from django.views.generic.base import TemplateView @@ -117,7 +117,9 @@ def get(self, request, *args, **kwargs): if not coordinates: return JsonResponse({}) - thread = create_thread(name="Tour Guide Chat", user=User.objects.first()) + thread = create_thread( + name=f"{timezone.now().isoformat()} - Tour Guide Chat", user=request.user + ) a = TourGuideAIAssistant() data = a.run(f"My coordinates are: ({coordinates})", thread.id) diff --git a/example/package.json b/example/package.json index 484c709..381fa9e 100644 --- a/example/package.json +++ b/example/package.json @@ -43,7 +43,6 @@ "@mantine/hooks": "^7.11.0", "@mantine/notifications": "^7.11.0", "@tabler/icons-react": "^3.7.0", - "axios": "^1.7.5", "cookie": "^0.6.0", "django-ai-assistant-client": "0.0.1", "modern-normalize": "^2.0.0", diff --git a/example/pnpm-lock.yaml b/example/pnpm-lock.yaml index 57ad579..c5518f5 100644 --- a/example/pnpm-lock.yaml +++ b/example/pnpm-lock.yaml @@ -20,9 +20,6 @@ importers: '@tabler/icons-react': specifier: ^3.7.0 version: 3.7.0(react@18.3.1) - axios: - specifier: ^1.7.5 - version: 1.7.5 cookie: specifier: ^0.6.0 version: 0.6.0 From a080ec2902484384abed1dc4a8a58987a764e302 Mon Sep 17 00:00:00 2001 From: Filipe Ximenes Date: Tue, 27 Aug 2024 14:10:00 -0300 Subject: [PATCH 7/9] fixes issue with manually inputing coordinates on tour guide app --- example/assets/js/App.tsx | 1 - .../js/components/TourGuide/TourGuide.tsx | 23 ++++++++----------- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/example/assets/js/App.tsx b/example/assets/js/App.tsx index b879996..6fa300e 100644 --- a/example/assets/js/App.tsx +++ b/example/assets/js/App.tsx @@ -3,7 +3,6 @@ import "@mantine/notifications/styles.css"; import React, { useEffect, useState } from "react"; import { - Button, Container, createTheme, List, diff --git a/example/assets/js/components/TourGuide/TourGuide.tsx b/example/assets/js/components/TourGuide/TourGuide.tsx index fcdcfb2..607d011 100644 --- a/example/assets/js/components/TourGuide/TourGuide.tsx +++ b/example/assets/js/components/TourGuide/TourGuide.tsx @@ -1,6 +1,6 @@ import "@mantine/core/styles.css"; import { Container, TextInput, Button } from "@mantine/core"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import classes from "./TourGuide.module.css"; export function TourGuide() { @@ -9,16 +9,15 @@ export function TourGuide() { const [attractions, setAttractions] = useState([]); const [loading, setLoading] = useState(false); - navigator.geolocation.getCurrentPosition( - (position: any) => { - if (latitude && longitude) { - return; - } - setLatitude(position.coords.latitude); - setLongitude(position.coords.longitude); - }, - (error) => console.log(error) - ); + useEffect(() => { + navigator.geolocation.getCurrentPosition( + (position: any) => { + setLatitude(position.coords.latitude); + setLongitude(position.coords.longitude); + }, + (error) => console.log(error) + ); + }, []); function findAttractions() { if (!latitude || !longitude) { @@ -42,7 +41,6 @@ export function TourGuide() { Latitude: setLatitude(e.target.value)} className={classes.coordinateInput} @@ -51,7 +49,6 @@ export function TourGuide() { Longitude: setLongitude(e.target.value)} className={classes.coordinateInput} From e67f0aa200d1a8f57ae617d76557405aa19abc57 Mon Sep 17 00:00:00 2001 From: Filipe Ximenes Date: Tue, 27 Aug 2024 16:29:09 -0300 Subject: [PATCH 8/9] improves usage of mantine components in the tour guide demo --- .../components/TourGuide/TourGuide.module.css | 9 ---- .../js/components/TourGuide/TourGuide.tsx | 46 +++++++++---------- 2 files changed, 23 insertions(+), 32 deletions(-) delete mode 100644 example/assets/js/components/TourGuide/TourGuide.module.css diff --git a/example/assets/js/components/TourGuide/TourGuide.module.css b/example/assets/js/components/TourGuide/TourGuide.module.css deleted file mode 100644 index ea4ef86..0000000 --- a/example/assets/js/components/TourGuide/TourGuide.module.css +++ /dev/null @@ -1,9 +0,0 @@ -.searchBar { - .inputBlock{ - display: inline-block; - margin-right: 5px; - } - .coordinateInput { - width: 300px; - } -} diff --git a/example/assets/js/components/TourGuide/TourGuide.tsx b/example/assets/js/components/TourGuide/TourGuide.tsx index 607d011..aa5a1c0 100644 --- a/example/assets/js/components/TourGuide/TourGuide.tsx +++ b/example/assets/js/components/TourGuide/TourGuide.tsx @@ -1,7 +1,12 @@ import "@mantine/core/styles.css"; -import { Container, TextInput, Button } from "@mantine/core"; +import { + Container, + TextInput, + Button, + LoadingOverlay, + Group, +} from "@mantine/core"; import { useEffect, useState } from "react"; -import classes from "./TourGuide.module.css"; export function TourGuide() { const [latitude, setLatitude] = useState(""); @@ -37,27 +42,22 @@ export function TourGuide() { return ( -
- - Latitude: - setLatitude(e.target.value)} - className={classes.coordinateInput} - /> - - - Longitude: - setLongitude(e.target.value)} - className={classes.coordinateInput} - /> - - -
+ + + setLatitude(e.target.value)} + /> + setLongitude(e.target.value)} + /> + + {loading ?

Loading

: null}
{attractions.map((item, i) => ( From e19becd1748217e0c33fb3c8a71dbe8e2d160b14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fl=C3=A1vio=20Juvenal?= Date: Wed, 28 Aug 2024 10:51:30 -0300 Subject: [PATCH 9/9] Simplify and add login required to TourGuideAssistantView --- example/assets/js/App.tsx | 2 +- example/assets/js/components/Chat/Chat.tsx | 12 +++++- .../js/components/TourGuide/TourGuide.tsx | 42 +++++++++++++++---- example/demo/views.py | 10 ++--- 4 files changed, 48 insertions(+), 18 deletions(-) diff --git a/example/assets/js/App.tsx b/example/assets/js/App.tsx index 6fa300e..7bd7044 100644 --- a/example/assets/js/App.tsx +++ b/example/assets/js/App.tsx @@ -75,7 +75,7 @@ const ExampleIndex = () => { message: ( <> You must be logged in to engage with the examples. Please{" "} - + log in {" "} to continue. diff --git a/example/assets/js/components/Chat/Chat.tsx b/example/assets/js/components/Chat/Chat.tsx index 1ab333f..be6fa6c 100644 --- a/example/assets/js/components/Chat/Chat.tsx +++ b/example/assets/js/components/Chat/Chat.tsx @@ -30,6 +30,7 @@ import { useMessageList, useThreadList, } from "django-ai-assistant-client"; +import { Link } from "react-router-dom"; function ChatMessage({ message, @@ -174,8 +175,15 @@ export function Chat({ assistantId }: { assistantId: string }) { notifications.show({ title: "Login Required", - message: - "You must be logged in to engage with the examples. Please log in to continue.", + message: ( + <> + You must be logged in to engage with the examples. Please{" "} + + log in + {" "} + to continue. + + ), color: "red", autoClose: 5000, withCloseButton: true, diff --git a/example/assets/js/components/TourGuide/TourGuide.tsx b/example/assets/js/components/TourGuide/TourGuide.tsx index aa5a1c0..1baecc1 100644 --- a/example/assets/js/components/TourGuide/TourGuide.tsx +++ b/example/assets/js/components/TourGuide/TourGuide.tsx @@ -7,8 +7,12 @@ import { Group, } from "@mantine/core"; import { useEffect, useState } from "react"; +import { notifications } from "@mantine/notifications"; +import { Link } from "react-router-dom"; export function TourGuide() { + const [showLoginNotification, setShowLoginNotification] = + useState(false); const [latitude, setLatitude] = useState(""); const [longitude, setLongitude] = useState(""); const [attractions, setAttractions] = useState([]); @@ -24,22 +28,42 @@ export function TourGuide() { ); }, []); - function findAttractions() { + async function findAttractions() { if (!latitude || !longitude) { return; } setLoading(true); - fetch(`/tour-guide/?coordinate=${latitude},${longitude}`) - .then((response) => response.json()) - .then((data: any) => { - console.log(data); - - setAttractions(data.nearby_attractions); - }) - .finally(() => setLoading(false)); + const response = await fetch(`/tour-guide/?coordinate=${latitude},${longitude}`); + const data = await response.json(); + if (data.error) { + setShowLoginNotification(true); + } else { + setAttractions(data.nearby_attractions); + } + setLoading(false) } + useEffect(() => { + if (!showLoginNotification) return; + + notifications.show({ + title: "Login Required", + message: ( + <> + You must be logged in to engage with the examples. Please{" "} + + log in + {" "} + to continue. + + ), + color: "red", + autoClose: 5000, + withCloseButton: true, + }); + }, [showLoginNotification]); + return ( diff --git a/example/demo/views.py b/example/demo/views.py index 467b768..74c2a1c 100644 --- a/example/demo/views.py +++ b/example/demo/views.py @@ -3,7 +3,6 @@ from django.contrib import messages from django.http import JsonResponse from django.shortcuts import get_object_or_404, redirect, render -from django.utils import timezone from django.views import View from django.views.generic.base import TemplateView @@ -112,16 +111,15 @@ def post(self, request, *args, **kwargs): class TourGuideAssistantView(View): def get(self, request, *args, **kwargs): + if not request.user.is_authenticated: + return JsonResponse({"error": "You must be logged in to use this feature."}, status=401) + coordinates = request.GET.get("coordinate") if not coordinates: return JsonResponse({}) - thread = create_thread( - name=f"{timezone.now().isoformat()} - Tour Guide Chat", user=request.user - ) - a = TourGuideAIAssistant() - data = a.run(f"My coordinates are: ({coordinates})", thread.id) + data = a.run(f"My coordinates are: ({coordinates})") return JsonResponse(json.loads(data))