Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JSON tour guide #147

Merged
merged 10 commits into from
Aug 28, 2024
21 changes: 19 additions & 2 deletions example/assets/js/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import "@mantine/notifications/styles.css";

import React, { useEffect, useState } from "react";
import {
Button,
Container,
createTheme,
List,
Expand All @@ -19,8 +18,9 @@ import {
IconXboxX,
IconMovie,
IconChecklist,
IconPlane,
} from "@tabler/icons-react";
import { Chat } from "@/components";
import { Chat, TourGuide } from "@/components";
import { createBrowserRouter, Link, RouterProvider } from "react-router-dom";
import {
ApiError,
Expand Down Expand Up @@ -139,6 +139,15 @@ const ExampleIndex = () => {
>
<Link to="/htmx">HTMX demo (no React)</Link>
</List.Item>
<List.Item
icon={
<ThemeIcon color="blue" size={28} radius="xl">
<IconPlane style={{ width: rem(18), height: rem(18) }} />
</ThemeIcon>
}
>
<Link to="/tour-guide">Tour Guide Assistant</Link>
</List.Item>
</List>
</Container>
);
Expand Down Expand Up @@ -198,6 +207,14 @@ const router = createBrowserRouter([
</PageWrapper>
),
},
{
path: "/tour-guide",
element: (
<PageWrapper>
<TourGuide assistantId="tour_guide_assistant" />
</PageWrapper>
),
},
{
path: "/admin",
element: (
Expand Down
1 change: 0 additions & 1 deletion example/assets/js/components/Chat/Chat.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import {
ActionIcon,
Avatar,
Box,
Button,
Container,
Group,
Expand Down
9 changes: 9 additions & 0 deletions example/assets/js/components/TourGuide/TourGuide.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.searchBar {
.inputBlock{
display: inline-block;
margin-right: 5px;
}
.coordinateInput {
width: 300px;
}
}
88 changes: 88 additions & 0 deletions example/assets/js/components/TourGuide/TourGuide.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import "@mantine/core/styles.css";
import { Container, TextInput, Button } from "@mantine/core";
import { useEffect, 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);

useEffect(() => {
navigator.geolocation.getCurrentPosition(
(position: any) => {
setLatitude(position.coords.latitude);
setLongitude(position.coords.longitude);
},
(error) => console.log(error)
);
}, []);

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));
}

return (
<Container>
<div className={classes.searchBar}>
filipeximenes marked this conversation as resolved.
Show resolved Hide resolved
<span className={classes.inputBlock}>
Latitude:
<TextInput
value={latitude}
onChange={(e) => setLatitude(e.target.value)}
className={classes.coordinateInput}
/>
</span>
<span className={classes.inputBlock}>
Longitude:
<TextInput
value={longitude}
onChange={(e) => setLongitude(e.target.value)}
className={classes.coordinateInput}
/>
</span>
<Button className={classes.inputBlock} onClick={findAttractions}>
Guide Me!
</Button>
</div>
{loading ? <h3>Loading</h3> : null}
pamella marked this conversation as resolved.
Show resolved Hide resolved
<div>
{attractions.map((item, i) => (
<div key={i}>
<h2>
{item.attraction_url ? (
<a href={item.attraction_url} target="_blank">
{item.attraction_name}
</a>
) : (
item.attraction_name
)}
</h2>
<span>{item.attraction_description}</span>
<div>
<a
href={`https://www.google.com/maps?q=${item.attraction_name}`}
target="_blank"
>
Open in Google Maps
</a>
</div>
</div>
))}
</div>
</Container>
);
}
1 change: 1 addition & 0 deletions example/assets/js/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { ThreadsNav } from "./ThreadsNav/ThreadsNav";
export { Chat } from "./Chat/Chat";
export { TourGuide } from "./TourGuide/TourGuide";
5 changes: 5 additions & 0 deletions example/demo/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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("<path:resource>", views.react_index),
Expand Down
23 changes: 23 additions & 0 deletions example/demo/views.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import json

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

from pydantic import ValidationError
from tour_guide.ai_assistants import TourGuideAIAssistant
from weather.ai_assistants import WeatherAIAssistant

from django_ai_assistant.api.schemas import (
Expand Down Expand Up @@ -102,3 +108,20 @@ 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=f"{timezone.now().isoformat()} - Tour Guide Chat", user=request.user
)

a = TourGuideAIAssistant()
data = a.run(f"My coordinates are: ({coordinates})", thread.id)

return JsonResponse(json.loads(data))
1 change: 1 addition & 0 deletions example/example/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"movies",
"rag",
"issue_tracker",
"tour_guide",
]

MIDDLEWARE = [
Expand Down
8 changes: 4 additions & 4 deletions example/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Empty file added example/tour_guide/__init__.py
Empty file.
70 changes: 70 additions & 0 deletions example/tour_guide/ai_assistants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import json

from django.utils import timezone

from django_ai_assistant import AIAssistant, method_tool
from tour_guide.integrations import fetch_points_of_interest


def _tour_guide_example_json():
return json.dumps(
{
"nearby_attractions": [
{
"attraction_name": f"<attraction-{i}-name-here>",
"attraction_description": f"<attraction-{i}-description-here>",
"attraction_url": f"<attraction-{i}-imdb-page-url-here>",
}
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 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. "
"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"

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"Today is: {current_date_str}. {self.instructions}"

@method_tool
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,
)
6 changes: 6 additions & 0 deletions example/tour_guide/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from django.apps import AppConfig


class TourGuideConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "tour_guide"
45 changes: 45 additions & 0 deletions example/tour_guide/integrations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from typing import List

import requests


def fetch_points_of_interest(
pamella marked this conversation as resolved.
Show resolved Hide resolved
latitude: float, longitude: float, tags: List[str], radius: int = 500
) -> dict:
"""
Fetch points of interest from OpenStreetMap using Overpass API.

: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"

# 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
]
)

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"]
Empty file.