From 4048c0d5c937784007c820fbca787aeb9f57605b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fl=C3=A1vio=20Juvenal?= Date: Fri, 28 Jun 2024 14:37:07 -0300 Subject: [PATCH 1/2] Issue tracker example --- django_ai_assistant/__init__.py | 18 +++- example/README.md | 13 +++ example/assets/js/App.tsx | 18 ++++ example/example/settings.py | 1 + example/issue_tracker/__init__.py | 0 example/issue_tracker/admin.py | 17 ++++ example/issue_tracker/ai_assistants.py | 94 +++++++++++++++++++ example/issue_tracker/apps.py | 6 ++ .../issue_tracker/migrations/0001_initial.py | 33 +++++++ example/issue_tracker/migrations/__init__.py | 0 example/issue_tracker/models.py | 28 ++++++ example/issue_tracker/tests.py | 1 + example/issue_tracker/views.py | 1 + 13 files changed, 228 insertions(+), 2 deletions(-) create mode 100644 example/issue_tracker/__init__.py create mode 100644 example/issue_tracker/admin.py create mode 100644 example/issue_tracker/ai_assistants.py create mode 100644 example/issue_tracker/apps.py create mode 100644 example/issue_tracker/migrations/0001_initial.py create mode 100644 example/issue_tracker/migrations/__init__.py create mode 100644 example/issue_tracker/models.py create mode 100644 example/issue_tracker/tests.py create mode 100644 example/issue_tracker/views.py diff --git a/django_ai_assistant/__init__.py b/django_ai_assistant/__init__.py index 8b8f883..d72e8b4 100644 --- a/django_ai_assistant/__init__.py +++ b/django_ai_assistant/__init__.py @@ -1,9 +1,9 @@ from importlib import metadata -from django_ai_assistant.helpers.assistants import ( # noqa +from django_ai_assistant.helpers.assistants import ( AIAssistant, ) -from django_ai_assistant.langchain.tools import ( # noqa +from django_ai_assistant.langchain.tools import ( BaseModel, BaseTool, Field, @@ -16,3 +16,17 @@ PACKAGE_NAME = __package__ or "django-ai-assistant" VERSION = __version__ = metadata.version(PACKAGE_NAME) + + +__all__ = [ + "AIAssistant", + "BaseModel", + "BaseTool", + "Field", + "StructuredTool", + "Tool", + "method_tool", + "tool", + "PACKAGE_NAME", + "VERSION", +] diff --git a/example/README.md b/example/README.md index fbe2118..6f7fabf 100644 --- a/example/README.md +++ b/example/README.md @@ -62,3 +62,16 @@ Access the Django admin at `http://localhost:8000/admin/` and log in with the su ## Usage Access the example project at `http://localhost:8000/`. + +## VSCode + +Fix the Python path in your `/.vscode/settings.json` to fix the Python import linting: + +```json +{ + // ... + "python.analysis.extraPaths": [ + "example/" + ] +} +``` diff --git a/example/assets/js/App.tsx b/example/assets/js/App.tsx index c66ca33..904c47e 100644 --- a/example/assets/js/App.tsx +++ b/example/assets/js/App.tsx @@ -18,6 +18,7 @@ import { IconCloud, IconXboxX, IconMovie, + IconChecklist, } from "@tabler/icons-react"; import { Chat } from "@/components"; import { createBrowserRouter, Link, RouterProvider } from "react-router-dom"; @@ -111,6 +112,15 @@ const ExampleIndex = () => { > Movie Recommendation Chat + + + + } + > + Issue Tracker Chat + @@ -164,6 +174,14 @@ const router = createBrowserRouter([ ), }, + { + path: "/issue-tracker-chat", + element: ( + + + + ), + }, { path: "/rag-chat", element: ( diff --git a/example/example/settings.py b/example/example/settings.py index eb58af8..fc08b26 100644 --- a/example/example/settings.py +++ b/example/example/settings.py @@ -33,6 +33,7 @@ "weather", "movies", "rag", + "issue_tracker", ] MIDDLEWARE = [ diff --git a/example/issue_tracker/__init__.py b/example/issue_tracker/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/example/issue_tracker/admin.py b/example/issue_tracker/admin.py new file mode 100644 index 0000000..db3400f --- /dev/null +++ b/example/issue_tracker/admin.py @@ -0,0 +1,17 @@ +from django.contrib import admin + +from issue_tracker.models import Issue + + +@admin.register(Issue) +class IssueAdmin(admin.ModelAdmin): + list_display = ( + "id", + "title", + "assignee", + "created_at", + "updated_at", + ) + search_fields = ("id", "title", "description", "assignee__username", "assignee__email") + list_filter = ("assignee", "created_at", "updated_at") + raw_id_fields = ("assignee",) diff --git a/example/issue_tracker/ai_assistants.py b/example/issue_tracker/ai_assistants.py new file mode 100644 index 0000000..c25b019 --- /dev/null +++ b/example/issue_tracker/ai_assistants.py @@ -0,0 +1,94 @@ +from typing import Sequence + +from django.contrib.auth.models import User + +from django_ai_assistant import AIAssistant, method_tool +from issue_tracker.models import Issue + + +class IssueTrackerAIAssistant(AIAssistant): + id = "issue_tracker_assistant" # noqa: A003 + name = "Issue Tracker Assistant" + instructions = ( + "You are a issue tracker assistant. " + "Help the user manage issues using the provided tools. " + "Issue IDs are unique and auto-incremented, they are represented as #. " + "Make sure to include it in your responses, " + "to know which issue you or the user are referring to. " + ) + model = "gpt-4o" + _user: User + + @method_tool + def get_current_assignee_email(self) -> str: + """Get the current user's email""" + return self._user.email + + def _format_issues(self, issues: Sequence[Issue]) -> str: + if not issues: + return "No issues found" + return "\n\n".join( + [f"- {issue.title} #{issue.id}\n{issue.description}" for issue in issues] + ) + + @method_tool + def list_issues(self) -> str: + """List all issues""" + return self._format_issues(list(Issue.objects.all())) + + @method_tool + def list_user_assigned_issues(self, assignee_email: str) -> str: + """List the issues assigned to the provided user""" + return self._format_issues(list(Issue.objects.filter(assignee__email=assignee_email))) + + @method_tool + def assign_user_to_issue(self, issue_id: int, assignee_email: str = "") -> str: + """Assign a user to an issue. When assignee_email is empty, the issue assignment is removed.""" + try: + issue = Issue.objects.get(id=issue_id) + if assignee_email: + assignee = User.objects.get(email=assignee_email) + else: + assignee = None + except Issue.DoesNotExist: + return f"ERROR: Issue {issue_id} does not exist" + except User.DoesNotExist: + return f"ERROR: User {assignee_email} does not exist" + issue.assignee = assignee + issue.save() + return f"Assigned {assignee_email} to issue {issue.title} #{issue.id}" + + @method_tool + def create_issue(self, title: str, description: str = "", assignee_email: str = "") -> str: + """Create a new issue. Title is required. Description is optional. Assignee is optional.""" + if assignee_email: + try: + assignee = User.objects.get(email=assignee_email) + except User.DoesNotExist: + return f"ERROR: User {assignee_email} does not exist" + else: + assignee = None + issue = Issue.objects.create(title=title, description=description, assignee=assignee) + return f"Created issue {issue.title} #{issue.id}" + + @method_tool + def update_issue(self, issue_id: int, title: str, description: str = "") -> str: + """Update an issue""" + try: + issue = Issue.objects.get(id=issue_id) + except Issue.DoesNotExist: + return f"ERROR: Issue {issue_id} does not exist" + issue.title = title + issue.description = description + issue.save() + return f"Updated issue {issue.title} #{issue.id}" + + @method_tool + def delete_issue(self, issue_id: int) -> str: + """Delete an issue""" + try: + issue = Issue.objects.get(id=issue_id) + except Issue.DoesNotExist: + return f"ERROR: Issue {issue_id} does not exist" + issue.delete() + return f"Deleted issue {issue.title} #{issue.id}" diff --git a/example/issue_tracker/apps.py b/example/issue_tracker/apps.py new file mode 100644 index 0000000..528cc14 --- /dev/null +++ b/example/issue_tracker/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class IssueTrackerConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "issue_tracker" diff --git a/example/issue_tracker/migrations/0001_initial.py b/example/issue_tracker/migrations/0001_initial.py new file mode 100644 index 0000000..f6d9cb7 --- /dev/null +++ b/example/issue_tracker/migrations/0001_initial.py @@ -0,0 +1,33 @@ +# Generated by Django 5.0.6 on 2024-06-28 17:31 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Issue', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=255)), + ('description', models.TextField(blank=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('assignee', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='assigned_issues', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'Issue', + 'verbose_name_plural': 'Issues', + 'ordering': ('assignee', 'created_at'), + }, + ), + ] diff --git a/example/issue_tracker/migrations/__init__.py b/example/issue_tracker/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/example/issue_tracker/models.py b/example/issue_tracker/models.py new file mode 100644 index 0000000..019fbb0 --- /dev/null +++ b/example/issue_tracker/models.py @@ -0,0 +1,28 @@ +from django.conf import settings +from django.db import models + + +class Issue(models.Model): + id: int # noqa: A003 + title = models.CharField(max_length=255) + description = models.TextField(blank=True) + assignee = models.ForeignKey( + settings.AUTH_USER_MODEL, + null=True, + blank=True, + on_delete=models.SET_NULL, + related_name="assigned_issues", + ) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + verbose_name = "Issue" + verbose_name_plural = "Issues" + ordering = ("assignee", "created_at") + + def __str__(self): + return f"{self.title} - {self.assignee}" + + def __repr__(self) -> str: + return f"" diff --git a/example/issue_tracker/tests.py b/example/issue_tracker/tests.py new file mode 100644 index 0000000..a39b155 --- /dev/null +++ b/example/issue_tracker/tests.py @@ -0,0 +1 @@ +# Create your tests here. diff --git a/example/issue_tracker/views.py b/example/issue_tracker/views.py new file mode 100644 index 0000000..60f00ef --- /dev/null +++ b/example/issue_tracker/views.py @@ -0,0 +1 @@ +# Create your views here. From 5728cbbc6b2b2ffca4ea4c202ecab3244b400195 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fl=C3=A1vio=20Juvenal?= Date: Fri, 28 Jun 2024 14:39:05 -0300 Subject: [PATCH 2/2] Remove empty files --- example/issue_tracker/tests.py | 1 - example/issue_tracker/views.py | 1 - 2 files changed, 2 deletions(-) delete mode 100644 example/issue_tracker/tests.py delete mode 100644 example/issue_tracker/views.py diff --git a/example/issue_tracker/tests.py b/example/issue_tracker/tests.py deleted file mode 100644 index a39b155..0000000 --- a/example/issue_tracker/tests.py +++ /dev/null @@ -1 +0,0 @@ -# Create your tests here. diff --git a/example/issue_tracker/views.py b/example/issue_tracker/views.py deleted file mode 100644 index 60f00ef..0000000 --- a/example/issue_tracker/views.py +++ /dev/null @@ -1 +0,0 @@ -# Create your views here.