Skip to content

Commit

Permalink
Merge pull request #139 from vintasoftware/feat/project-mgmt-example
Browse files Browse the repository at this point in the history
Issue tracker example
  • Loading branch information
fjsj authored Jun 28, 2024
2 parents ee6c79d + 5728cbb commit d681b53
Show file tree
Hide file tree
Showing 11 changed files with 226 additions and 2 deletions.
18 changes: 16 additions & 2 deletions django_ai_assistant/__init__.py
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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",
]
13 changes: 13 additions & 0 deletions example/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<project-root>/.vscode/settings.json` to fix the Python import linting:

```json
{
// ...
"python.analysis.extraPaths": [
"example/"
]
}
```
18 changes: 18 additions & 0 deletions example/assets/js/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -111,6 +112,15 @@ const ExampleIndex = () => {
>
<Link to="/movies-chat">Movie Recommendation Chat</Link>
</List.Item>
<List.Item
icon={
<ThemeIcon color="blue" size={28} radius="xl">
<IconChecklist style={{ width: rem(18), height: rem(18) }} />
</ThemeIcon>
}
>
<Link to="/issue-tracker-chat">Issue Tracker Chat</Link>
</List.Item>
<List.Item
icon={
<ThemeIcon color="blue" size={28} radius="xl">
Expand Down Expand Up @@ -164,6 +174,14 @@ const router = createBrowserRouter([
</PageWrapper>
),
},
{
path: "/issue-tracker-chat",
element: (
<PageWrapper>
<Chat assistantId="issue_tracker_assistant" />
</PageWrapper>
),
},
{
path: "/rag-chat",
element: (
Expand Down
1 change: 1 addition & 0 deletions example/example/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"weather",
"movies",
"rag",
"issue_tracker",
]

MIDDLEWARE = [
Expand Down
Empty file.
17 changes: 17 additions & 0 deletions example/issue_tracker/admin.py
Original file line number Diff line number Diff line change
@@ -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",)
94 changes: 94 additions & 0 deletions example/issue_tracker/ai_assistants.py
Original file line number Diff line number Diff line change
@@ -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 #<id>. "
"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}"
6 changes: 6 additions & 0 deletions example/issue_tracker/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from django.apps import AppConfig


class IssueTrackerConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "issue_tracker"
33 changes: 33 additions & 0 deletions example/issue_tracker/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -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'),
},
),
]
Empty file.
28 changes: 28 additions & 0 deletions example/issue_tracker/models.py
Original file line number Diff line number Diff line change
@@ -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"<Issue {self.title} #{self.id} of {self.assignee}>"

0 comments on commit d681b53

Please sign in to comment.