Skip to content

Commit

Permalink
Merge pull request #147 from vintasoftware/feat/json-tour-guide
Browse files Browse the repository at this point in the history
JSON tour guide
  • Loading branch information
filipeximenes authored Aug 28, 2024
2 parents bcb92af + e19becd commit b9f6ec0
Show file tree
Hide file tree
Showing 13 changed files with 295 additions and 10 deletions.
23 changes: 20 additions & 3 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 @@ -75,7 +75,7 @@ const ExampleIndex = () => {
message: (
<>
You must be logged in to engage with the examples. Please{" "}
<Link to="admin/" target="_blank">
<Link to="/admin/" target="_blank">
log in
</Link>{" "}
to continue.
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
13 changes: 10 additions & 3 deletions 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 Expand Up @@ -31,6 +30,7 @@ import {
useMessageList,
useThreadList,
} from "django-ai-assistant-client";
import { Link } from "react-router-dom";

function ChatMessage({
message,
Expand Down Expand Up @@ -175,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{" "}
<Link to="/admin/" target="_blank">
log in
</Link>{" "}
to continue.
</>
),
color: "red",
autoClose: 5000,
withCloseButton: true,
Expand Down
112 changes: 112 additions & 0 deletions example/assets/js/components/TourGuide/TourGuide.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import "@mantine/core/styles.css";
import {
Container,
TextInput,
Button,
LoadingOverlay,
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<boolean>(false);
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)
);
}, []);

async function findAttractions() {
if (!latitude || !longitude) {
return;
}

setLoading(true);
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{" "}
<Link to="/admin/" target="_blank">
log in
</Link>{" "}
to continue.
</>
),
color: "red",
autoClose: 5000,
withCloseButton: true,
});
}, [showLoginNotification]);

return (
<Container>
<LoadingOverlay visible={loading} />
<Group justify="left" align="flex-end">
<TextInput
required
label="Latitude"
value={latitude}
onChange={(e) => setLatitude(e.target.value)}
/>
<TextInput
required
label="Longitude"
value={longitude}
onChange={(e) => setLongitude(e.target.value)}
/>
<Button onClick={findAttractions}>Guide Me!</Button>
</Group>
{loading ? <h3>Loading</h3> : null}
<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
21 changes: 21 additions & 0 deletions example/demo/views.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import json

from django.contrib import messages
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 (
Expand Down Expand Up @@ -102,3 +107,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):
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({})

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

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

0 comments on commit b9f6ec0

Please sign in to comment.