+
+ );
+}
diff --git a/example/assets/js/components/index.ts b/example/assets/js/components/index.ts
index 86bd4fa..c50b84e 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 { TourGuide } from "./TourGuide/TourGuide";
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..74c2a1c 100644
--- a/example/demo/views.py
+++ b/example/demo/views.py
@@ -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 (
@@ -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))
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/pnpm-lock.yaml b/example/pnpm-lock.yaml
index d390fd4..c5518f5 100644
--- a/example/pnpm-lock.yaml
+++ b/example/pnpm-lock.yaml
@@ -1361,8 +1361,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 +4944,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 +5253,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)
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..51297ed
--- /dev/null
+++ b/example/tour_guide/ai_assistants.py
@@ -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_description": 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 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,
+ )
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..6357b8a
--- /dev/null
+++ b/example/tour_guide/integrations.py
@@ -0,0 +1,45 @@
+from typing import List
+
+import requests
+
+
+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.
+
+ :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"]
diff --git a/example/tour_guide/migrations/__init__.py b/example/tour_guide/migrations/__init__.py
new file mode 100644
index 0000000..e69de29