Skip to content

Commit

Permalink
Merge branch 'main' into feat/support-any-type-of-id
Browse files Browse the repository at this point in the history
  • Loading branch information
amandasavluchinske committed Jun 21, 2024
2 parents 1b9390e + 178cb90 commit 699c703
Show file tree
Hide file tree
Showing 28 changed files with 619 additions and 256 deletions.
71 changes: 71 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Contributing

## Local Dev Setup

### Clone the repo

`git clone git@github.com:vintasoftware/django-ai-assistant.git`

### Set up a virtualenv, optionally set up nvm, and activate your environment(s)

You can use [pyenv](https://github.com/pyenv/pyenv), [pipenv](https://github.com/pypa/pipenv/blob/main/docs/installation.md), vanilla venvs or the tool of your choice.

For installing Node, we recommend [NVM](https://github.com/nvm-sh/nvm).

### Install dependencies

#### Backend

`poetry install`

#### Frontend

```bash
cd frontend
npm install
```

### Install pre-commit hooks

`pre-commit install`

It's critical to run the pre-commit hooks before pushing your code to follow the project's code style, and avoid linting errors.

### Developing with the example project

Run the frontend project in build:watch mode:

```bash
cd frontend
npm run build:watch
```

Then follow the instructions in the [example README](https://github.com/vintasoftware/django-ai-assistant/tree/main/example#readme).

## Tests

Run tests with:

```bash
poetry run pytest
```

The tests use `pytest-vcr` to record and replay HTTP requests to AI models.

If you're implementing a new test that needs to call a real AI model, you need to set the `OPENAI_API_KEY` environment variable on root `.env` file.
Then, you will run the tests in record mode:

```bash
poetry run pytest --record-mode=once
```

## Documentation

We use [mkdocs-material](https://squidfunk.github.io/mkdocs-material/) to generate the documentation from markdown files.
Check the files in the `docs` directory.

To run the documentation locally, you need to run:

```bash
poetry run mkdocs serve
```
92 changes: 10 additions & 82 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,97 +1,25 @@
[![Django CI](https://github.com/vintasoftware/django-ai-assistant/actions/workflows/django.yml/badge.svg)](https://github.com/vintasoftware/django-ai-assistant/actions/workflows/django.yml)
[![Node CI](https://github.com/vintasoftware/django-ai-assistant/actions/workflows/node.yml/badge.svg)](https://github.com/vintasoftware/django-ai-assistant/actions/workflows/node.yml)

# django-ai-assistant
# django-ai-assistant ![docs/images/robot-happy-outline.svg]

Django app to integrate with [OpenAI Assistants API](https://platform.openai.com/docs/assistants/overview). Supports [Function calling](https://platform.openai.com/docs/assistants/tools/function-calling).
Combine the power of LLMs with Django's productivity to build intelligent applications.
Let AI Assistants call methods from Django's side and do anything your users need!

⚠️ Under heavy development. Not launched yet!
Use AI Tool Calling and RAG with Django to easily build state of the art AI Assistants.

## Dev Setup

### Clone the repo

`git clone git@github.com:vintasoftware/django-ai-assistant.git`

### Set up a virtualenv, optionally set up nvm, and activate your environment(s)

You can use [pyenv](https://github.com/pyenv/pyenv), [pipenv](https://github.com/pypa/pipenv/blob/main/docs/installation.md), vanilla venvs or the tool of your choice.

[NVM](https://github.com/nvm-sh/nvm)

### Install vite

`npm install -g vite` or install locally if you prefer

### Create a .env file at the root of the project

`cp .env.example .env`

To gather the API keys for these tools, head to the following links:
- [OpenAI](https://platform.openai.com/api-keys)
- [Weather](https://www.weatherapi.com/)
- [Tavily](https://app.tavily.com/home)
- [Firecrawl](https://www.firecrawl.dev/)

### Build the frontend

`cd frontend`

`vite build`

### Create a .env file at the example folder

`cd example`

`cp .env.example .env`

### Install dependencies

#### Backend

`cd example`

`poetry install`

Install pre-commit hooks:

`pre-commit install`

#### Frontend

`cd example`

`npm install`

### Run the project

From the example folder:

#### Frontend

`npm run start`

#### Backend

`python manage.py migrate`

`python manage.py createsuperuser`

`python manage.py runserver`

### Log in to Django Admin

Go to /admin and log in with your superuser account.
⚠️ Under heavy development. Not publicly launched yet!

Please check the documentation: [https://vintasoftware.github.io/django-ai-assistant/](https://vintasoftware.github.io/django-ai-assistant/)

## Contributing

If you wish to contribute to this project, please first discuss the change you wish to make via an [issue](https://github.com/vintasoftware/django-react-boilerplate/issues).
You're welcome to contribute with Django AI Assistant! Please feel free to tackle existing [issues](https://github.com/vintasoftware/django-ai-assistant/issues). If you have a new idea, please open a new issue to discuss it.

Check our [contributing guide](https://github.com/vintasoftware/django-react-boilerplate/blob/main/CONTRIBUTING.md) to learn more about our development process and how you can test your changes to the boilerplate.
Check our [contributing guide](CONTRIBUTING.md) to learn more about how to develop and test the project locally, before opening a pull request.

## Commercial Support

[![alt text](https://avatars2.githubusercontent.com/u/5529080?s=80&v=4 "Vinta Logo")](https://www.vinta.com.br/)
[![alt text](https://avatars2.githubusercontent.com/u/5529080?s=80&v=4 "Vinta Logo")](https://www.vintasoftware.com/)

This project is maintained by [Vinta Software](https://www.vinta.com.br/) and is used in products of Vinta's clients. We are always looking for exciting work! If you need any commercial support, feel free to get in touch: contact@vinta.com.br
This is an open-source project maintained by [Vinta Software](https://www.vinta.com.br/) and is used in products of Vinta's clients. We are always looking for exciting work! If you need any commercial support, feel free to get in touch: contact@vinta.com.br
43 changes: 28 additions & 15 deletions django_ai_assistant/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
ThreadSchema,
ThreadSchemaIn,
)
from django_ai_assistant.exceptions import AIUserNotAllowedError
from django_ai_assistant.conf import app_settings
from django_ai_assistant.exceptions import AIAssistantNotDefinedError, AIUserNotAllowedError
from django_ai_assistant.helpers import use_cases
from django_ai_assistant.models import Message, Thread

Expand All @@ -28,14 +29,18 @@ def get_openapi_operation_id(self, operation: Operation) -> str:
return (package_name + "_" + name).replace(".", "_")


api = API(
title=package_name,
version=version,
urls_namespace="django_ai_assistant",
# Add auth to all endpoints
auth=django_auth,
csrf=True,
)
def init_api():
return API(
title=package_name,
version=version,
urls_namespace="django_ai_assistant",
# Add auth to all endpoints
auth=django_auth,
csrf=True,
)


api = app_settings.call_fn("INIT_API_FN")


@api.exception_handler(AIUserNotAllowedError)
Expand All @@ -47,6 +52,15 @@ def ai_user_not_allowed_handler(request, exc):
)


@api.exception_handler(AIAssistantNotDefinedError)
def ai_assistant_not_defined_handler(request, exc):
return api.create_response(
request,
{"message": str(exc)},
status=404,
)


@api.get("assistants/", response=List[AssistantSchema], url_name="assistants_list")
def list_assistants(request):
return list(use_cases.get_assistants_info(user=request.user, request=request))
Expand All @@ -61,7 +75,7 @@ def get_assistant(request, assistant_id: str):

@api.get("threads/", response=List[ThreadSchema], url_name="threads_list_create")
def list_threads(request):
return list(use_cases.get_threads(user=request.user, request=request))
return list(use_cases.get_threads(user=request.user))


@api.post("threads/", response=ThreadSchema, url_name="threads_list_create")
Expand All @@ -77,7 +91,7 @@ def get_thread(request, thread_id: Any):
thread_id=thread_id, user=request.user, request=request
)
except Thread.DoesNotExist:
raise Http404("No %s matches the given query." % Thread._meta.object_name) from None
raise Http404(f"No Thread with id={thread_id} found") from None
return thread


Expand All @@ -100,10 +114,9 @@ def delete_thread(request, thread_id: Any):
response=List[ThreadMessagesSchemaOut],
url_name="messages_list_create",
)
def list_thread_messages(request, thread_id: Any):
messages = use_cases.get_thread_messages(
thread_id=thread_id, user=request.user, request=request
)
def list_thread_messages(request, thread_id: str):
thread = get_object_or_404(Thread, id=thread_id)
messages = use_cases.get_thread_messages(thread=thread, user=request.user, request=request)
return [message_to_dict(m)["data"] for m in messages]


Expand Down
1 change: 1 addition & 0 deletions django_ai_assistant/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@


DEFAULTS = {
"INIT_API_FN": "django_ai_assistant.api.views.init_api",
"CAN_CREATE_THREAD_FN": "django_ai_assistant.permissions.allow_all",
"CAN_VIEW_THREAD_FN": "django_ai_assistant.permissions.owns_thread",
"CAN_UPDATE_THREAD_FN": "django_ai_assistant.permissions.owns_thread",
Expand Down
6 changes: 6 additions & 0 deletions django_ai_assistant/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
class AIAssistantMisconfiguredError(Exception):
"""Raised when the AI assistant is misconfigured, e.g. when assistant id is invalid."""

pass


class AIAssistantNotDefinedError(Exception):
"""Raised when the AI assistant is not defined when trying to get it by id."""

pass


class AIUserNotAllowedError(Exception):
"""Raised when the user has no permission to manage a Thread, Message, or AIAssistant."""

pass
35 changes: 25 additions & 10 deletions django_ai_assistant/helpers/assistants.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,11 @@

class AIAssistant(abc.ABC): # noqa: F821
"""Base class for AI Assistants. Subclasses must define at least the following attributes:
- id: str
- name: str
- instructions: str
- model: str
* id: str
* name: str
* instructions: str
* model: str
Subclasses can override the public methods to customize the behavior of the assistant.\n
Tools can be added to the assistant by decorating methods with `@method_tool`.\n
Expand Down Expand Up @@ -102,15 +103,29 @@ class AIAssistant(abc.ABC): # noqa: F821
Automatically populated by when a subclass is declared.\n
Use `get_cls_registry` and `get_cls` to access the registry."""

def __init__(self, *, user=None, request=None, view=None, **kwargs):
def __init__(self, *, user=None, request=None, view=None, **kwargs: Any):
"""Initialize the AIAssistant instance.\n
Optionally set the current user, request, and view for the assistant.\n
Those can be used in any `@method_tool` to customize behavior.\n
Args:
user (Any | None): The current user the assistant is helping. A model instance.
Defaults to `None`. Stored in `self._user`.
request (Any | None): The current Django request the assistant was initialized with.
A request instance. Defaults to `None`. Stored in `self._request`.
view (Any | None): The current Django view the assistant was initialized with.
A view instance. Defaults to `None`. Stored in `self._view`.
**kwargs: Extra keyword arguments passed to the constructor. Stored in `self._init_kwargs`.
"""

self._user = user
self._request = request
self._view = view
self._init_kwargs = kwargs

self._set_method_tools()

def __init_subclass__(cls, **kwargs):
def __init_subclass__(cls, **kwargs: Any):
"""Called when a class is subclassed from AIAssistant.
This method is automatically invoked when a new subclass of AIAssistant
Expand Down Expand Up @@ -499,7 +514,7 @@ def as_chain(self, thread_id: Any | None) -> Runnable[dict, dict]:

return agent_with_chat_history

def invoke(self, *args, thread_id: Any | None, **kwargs):
def invoke(self, *args: Any, thread_id: Any | None, **kwargs: Any) -> dict:
"""Invoke the assistant Langchain chain with the given arguments and keyword arguments.\n
This is the lower-level method to run the assistant.\n
The chain is created by the `as_chain` method.\n
Expand All @@ -518,7 +533,7 @@ def invoke(self, *args, thread_id: Any | None, **kwargs):
chain = self.as_chain(thread_id)
return chain.invoke(*args, **kwargs)

def run(self, message, thread_id: Any | None, **kwargs):
def run(self, message: str, thread_id: Any | None, **kwargs: Any) -> str:
"""Run the assistant with the given message and thread ID.\n
This is the higher-level method to run the assistant.\n
Expand All @@ -539,10 +554,10 @@ def run(self, message, thread_id: Any | None, **kwargs):
**kwargs,
)["output"]

def _run_as_tool(self, message: str, **kwargs):
def _run_as_tool(self, message: str, **kwargs: Any) -> str:
return self.run(message, thread_id=None, **kwargs)

def as_tool(self, description) -> BaseTool:
def as_tool(self, description: str) -> BaseTool:
"""Create a tool from the assistant.\n
This is useful to compose assistants.\n
Expand Down
Loading

0 comments on commit 699c703

Please sign in to comment.