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
9 changes: 8 additions & 1 deletion example/assets/js/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -139,6 +139,9 @@ const ExampleIndex = () => {
>
<Link to="/htmx">HTMX demo (no React)</Link>
</List.Item>
<List.Item>
<Link to="/tour-guide">Tour Guide Assistant</Link>
</List.Item>
</List>
</Container>
);
Expand Down Expand Up @@ -198,6 +201,10 @@ const router = createBrowserRouter([
</PageWrapper>
),
},
{
path: "/tour-guide",
element: (<PageWrapper><ResultsPage assistantId="tour_guide_assistant" /></PageWrapper>),
},
{
path: "/admin",
element: (
Expand Down
36 changes: 36 additions & 0 deletions example/assets/js/components/ResultsPage/ResultsPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import "@mantine/core/styles.css";
filipeximenes marked this conversation as resolved.
Show resolved Hide resolved

import {
Container,
createTheme,
List,
MantineProvider,
Title,
} from "@mantine/core";
import { useState } from "react";

const theme = createTheme({});
filipeximenes marked this conversation as resolved.
Show resolved Hide resolved

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 (
<Container>
hi
lat: {lat}
lon: {lon}
</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 { ResultsPage } from "./ResultsPage/ResultsPage";
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
Empty file added example/tour_guide/__init__.py
Empty file.
105 changes: 105 additions & 0 deletions example/tour_guide/ai_assistants.py
Original file line number Diff line number Diff line change
@@ -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-{i}-name-here>",
"attraction_description": f"<attraction-{i}-short-description-here>",
"attraction_image_url": f"<attraction-{i}-image-url-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 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 {}
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"
23 changes: 23 additions & 0 deletions example/tour_guide/integrations.py
Original file line number Diff line number Diff line change
@@ -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")
Empty file.
16 changes: 15 additions & 1 deletion poetry.lock

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

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading