From f8105b0775b0e00cff28ee538412a881dba75168 Mon Sep 17 00:00:00 2001 From: Pamella Bezerra Date: Mon, 17 Jun 2024 17:40:16 -0300 Subject: [PATCH 01/14] Add django_auth to ninja API --- django_ai_assistant/api/views.py | 9 ++- example/assets/js/components/Chat/Chat.tsx | 7 +- frontend/openapi_schema.json | 77 +++++++++++++++++++--- 3 files changed, 76 insertions(+), 17 deletions(-) diff --git a/django_ai_assistant/api/views.py b/django_ai_assistant/api/views.py index 10eb0d6..5db9a52 100644 --- a/django_ai_assistant/api/views.py +++ b/django_ai_assistant/api/views.py @@ -5,6 +5,7 @@ from langchain_core.messages import message_to_dict from ninja import NinjaAPI from ninja.operation import Operation +from ninja.security import django_auth from django_ai_assistant import package_name, version from django_ai_assistant.api.schemas import ( @@ -26,7 +27,13 @@ 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") +api = API( + title=package_name, + version=version, + urls_namespace="django_ai_assistant", + # Add auth to all endpoints + auth=django_auth, +) @api.exception_handler(AIUserNotAllowedError) diff --git a/example/assets/js/components/Chat/Chat.tsx b/example/assets/js/components/Chat/Chat.tsx index 60c30c7..2606c90 100644 --- a/example/assets/js/components/Chat/Chat.tsx +++ b/example/assets/js/components/Chat/Chat.tsx @@ -93,12 +93,7 @@ function ChatMessageList({ deleteMessage, }: { messages: ThreadMessagesSchemaOut[]; - deleteMessage: ({ - threadId, - messageId, - }: { - messageId: string; - }) => Promise; + deleteMessage: ({ messageId }: { messageId: string }) => Promise; }) { if (messages.length === 0) { return No messages.; diff --git a/frontend/openapi_schema.json b/frontend/openapi_schema.json index 75f5898..1adc83d 100644 --- a/frontend/openapi_schema.json +++ b/frontend/openapi_schema.json @@ -26,7 +26,12 @@ } } } - } + }, + "security": [ + { + "SessionAuth": [] + } + ] } }, "/assistants/{assistant_id}/": { @@ -55,7 +60,12 @@ } } } - } + }, + "security": [ + { + "SessionAuth": [] + } + ] } }, "/threads/": { @@ -78,7 +88,12 @@ } } } - } + }, + "security": [ + { + "SessionAuth": [] + } + ] }, "post": { "operationId": "django_ai_assistant_create_thread", @@ -105,7 +120,12 @@ } }, "required": true - } + }, + "security": [ + { + "SessionAuth": [] + } + ] } }, "/threads/{thread_id}/": { @@ -134,7 +154,12 @@ } } } - } + }, + "security": [ + { + "SessionAuth": [] + } + ] }, "patch": { "operationId": "django_ai_assistant_update_thread", @@ -171,7 +196,12 @@ } }, "required": true - } + }, + "security": [ + { + "SessionAuth": [] + } + ] }, "delete": { "operationId": "django_ai_assistant_delete_thread", @@ -191,7 +221,12 @@ "204": { "description": "No Content" } - } + }, + "security": [ + { + "SessionAuth": [] + } + ] } }, "/threads/{thread_id}/messages/": { @@ -224,7 +259,12 @@ } } } - } + }, + "security": [ + { + "SessionAuth": [] + } + ] }, "post": { "operationId": "django_ai_assistant_create_thread_message", @@ -254,7 +294,12 @@ } }, "required": true - } + }, + "security": [ + { + "SessionAuth": [] + } + ] } }, "/threads/{thread_id}/messages/{message_id}/": { @@ -285,7 +330,12 @@ "204": { "description": "No Content" } - } + }, + "security": [ + { + "SessionAuth": [] + } + ] } } }, @@ -414,6 +464,13 @@ "title": "ThreadMessagesSchemaIn", "type": "object" } + }, + "securitySchemes": { + "SessionAuth": { + "type": "apiKey", + "in": "cookie", + "name": "sessionid" + } } }, "servers": [] From e20cc78773064013354694985d2a32377b139bb0 Mon Sep 17 00:00:00 2001 From: Pamella Bezerra Date: Mon, 17 Jun 2024 17:43:50 -0300 Subject: [PATCH 02/14] Update tests to consider django auth --- tests/test_views.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/tests/test_views.py b/tests/test_views.py index 7a58763..282c292 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -51,8 +51,9 @@ def authenticated_client(client): # Assistant Views -def test_list_assistants_with_results(client): - response = client.get("/assistants/") +@pytest.mark.django_db() +def test_list_assistants_with_results(authenticated_client): + response = authenticated_client.get("/assistants/") assert response.status_code == HTTPStatus.OK assert response.json() == [{"id": "temperature_assistant", "name": "Temperature Assistant"}] @@ -63,16 +64,18 @@ def test_does_not_list_assistants_if_unauthorized(): pass -def test_get_assistant_that_exists(client): - response = client.get("/assistants/temperature_assistant/") +@pytest.mark.django_db() +def test_get_assistant_that_exists(authenticated_client): + response = authenticated_client.get("/assistants/temperature_assistant/") assert response.status_code == HTTPStatus.OK assert response.json() == {"id": "temperature_assistant", "name": "Temperature Assistant"} -def test_get_assistant_that_does_not_exist(client): +@pytest.mark.django_db() +def test_get_assistant_that_does_not_exist(authenticated_client): with pytest.raises(AIAssistantNotDefinedError): - client.get("/assistants/fake_assistant/") + authenticated_client.get("/assistants/fake_assistant/") def test_does_not_return_assistant_if_unauthorized(): From ac163572585e8696df4807327f08960cc4569777 Mon Sep 17 00:00:00 2001 From: Pamella Bezerra Date: Mon, 17 Jun 2024 18:13:16 -0300 Subject: [PATCH 03/14] Config csrf token --- django_ai_assistant/api/views.py | 1 + frontend/package-lock.json | 18 +++++++++++++++++- frontend/package.json | 4 +++- frontend/src/config.ts | 13 +++++++++++++ 4 files changed, 34 insertions(+), 2 deletions(-) diff --git a/django_ai_assistant/api/views.py b/django_ai_assistant/api/views.py index 5db9a52..876f27f 100644 --- a/django_ai_assistant/api/views.py +++ b/django_ai_assistant/api/views.py @@ -33,6 +33,7 @@ def get_openapi_operation_id(self, operation: Operation) -> str: urls_namespace="django_ai_assistant", # Add auth to all endpoints auth=django_auth, + csrf=True, ) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index c0d20ec..156a272 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -9,12 +9,14 @@ "version": "0.0.1", "license": "MIT", "dependencies": { - "axios": "^1.7.2" + "axios": "^1.7.2", + "cookie": "^0.6.0" }, "devDependencies": { "@hey-api/openapi-ts": "^0.46.3", "@testing-library/dom": "^10.1.0", "@testing-library/react": "^16.0.0", + "@types/cookie": "^0.6.0", "@types/jest": "^29.5.12", "@types/node": "^20.14.1", "@types/react": "^18.3.3", @@ -2813,6 +2815,12 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "dev": true + }, "node_modules/@types/estree": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", @@ -4166,6 +4174,14 @@ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true }, + "node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index 321b182..81d9036 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -42,7 +42,8 @@ "generate-client": "openapi-ts" }, "dependencies": { - "axios": "^1.7.2" + "axios": "^1.7.2", + "cookie": "^0.6.0" }, "peerDependencies": { "react": "^18.3.1", @@ -52,6 +53,7 @@ "@hey-api/openapi-ts": "^0.46.3", "@testing-library/dom": "^10.1.0", "@testing-library/react": "^16.0.0", + "@types/cookie": "^0.6.0", "@types/jest": "^29.5.12", "@types/node": "^20.14.1", "@types/react": "^18.3.3", diff --git a/frontend/src/config.ts b/frontend/src/config.ts index 0437273..a82dc96 100644 --- a/frontend/src/config.ts +++ b/frontend/src/config.ts @@ -1,9 +1,14 @@ +import cookie from "cookie"; + import { OpenAPI } from "./client"; +import { AxiosRequestConfig } from "axios"; /** * Configures the base URL for the AI Assistant API which is path associated with * the Django include. * + * Configures the Axios request to include the CSRF token if it exists. + * * @param baseURL Base URL of the AI Assistant API. * * @example @@ -11,4 +16,12 @@ import { OpenAPI } from "./client"; */ export function configAIAssistant({ baseURL }: { baseURL: string }) { OpenAPI.BASE = baseURL; + + OpenAPI.interceptors.request.use((request: AxiosRequestConfig) => { + const { csrftoken } = cookie.parse(document.cookie); + if (request.headers && csrftoken) { + request.headers["X-CSRFTOKEN"] = csrftoken; + } + return request; + }); } From c0d247cca7a432e934b6af47e5a553af9b752873 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fl=C3=A1vio=20Juvenal?= Date: Tue, 18 Jun 2024 16:33:09 -0300 Subject: [PATCH 04/14] Basic docs with tutorial --- .editorconfig | 1 + django_ai_assistant/api/views.py | 8 +- django_ai_assistant/helpers/assistants.py | 11 + django_ai_assistant/helpers/use_cases.py | 8 +- django_ai_assistant/permissions.py | 14 + docs/get-started.md | 32 ++ docs/index.md | 24 ++ docs/tutorial.md | 396 ++++++++++++++++++++++ example/example/settings.py | 1 - example/weather/ai_assistants.py | 14 +- mkdocs.yml | 48 +++ poetry.lock | 356 ++++++++++++++++++- pyproject.toml | 5 + tests/settings.py | 1 - 14 files changed, 907 insertions(+), 12 deletions(-) create mode 100644 docs/get-started.md create mode 100644 docs/index.md create mode 100644 docs/tutorial.md create mode 100644 mkdocs.yml diff --git a/.editorconfig b/.editorconfig index 26f53fd..c27a17b 100644 --- a/.editorconfig +++ b/.editorconfig @@ -15,6 +15,7 @@ indent_size = 2 [*.md] trim_trailing_whitespace = false +indent_size = 4 [Makefile] indent_style = tab diff --git a/django_ai_assistant/api/views.py b/django_ai_assistant/api/views.py index 10eb0d6..f7a823a 100644 --- a/django_ai_assistant/api/views.py +++ b/django_ai_assistant/api/views.py @@ -1,5 +1,6 @@ from typing import List +from django.http import Http404 from django.shortcuts import get_object_or_404 from langchain_core.messages import message_to_dict @@ -63,7 +64,12 @@ def create_thread(request, payload: ThreadSchemaIn): @api.get("threads/{thread_id}/", response=ThreadSchema, url_name="thread_detail_update_delete") def get_thread(request, thread_id: str): - thread = use_cases.get_single_thread(thread_id=thread_id, user=request.user, request=request) + try: + thread = use_cases.get_single_thread( + 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 return thread diff --git a/django_ai_assistant/helpers/assistants.py b/django_ai_assistant/helpers/assistants.py index 7eace1d..42b7059 100644 --- a/django_ai_assistant/helpers/assistants.py +++ b/django_ai_assistant/helpers/assistants.py @@ -192,7 +192,9 @@ def get_contextualize_prompt(self) -> ChatPromptTemplate: return ChatPromptTemplate.from_messages( [ ("system", contextualize_q_system_prompt), + # TODO: make history key confirgurable? MessagesPlaceholder("history"), + # TODO: make input key confirgurable? ("human", "{input}"), ] ) @@ -284,6 +286,15 @@ def invoke(self, *args, thread_id: int | None, **kwargs): chain = self.as_chain(thread_id) return chain.invoke(*args, **kwargs) + def run(self, message, thread_id: int | None, **kwargs): + return self.invoke( + { + "input": message, + }, + thread_id=thread_id, + **kwargs, + )["output"] + def run_as_tool(self, message: str, **kwargs): chain = self.as_chain(thread_id=None) output = chain.invoke({"input": message}, **kwargs) diff --git a/django_ai_assistant/helpers/use_cases.py b/django_ai_assistant/helpers/use_cases.py index 595a424..5fb2405 100644 --- a/django_ai_assistant/helpers/use_cases.py +++ b/django_ai_assistant/helpers/use_cases.py @@ -17,6 +17,7 @@ can_delete_message, can_delete_thread, can_run_assistant, + can_view_thread, ) @@ -98,7 +99,12 @@ def get_single_thread( user: Any, request: HttpRequest | None = None, ): - return Thread.objects.filter(created_by=user).get(id=thread_id) + thread = Thread.objects.get(id=thread_id) + + if not can_view_thread(thread=thread, user=user, request=request): + raise AIUserNotAllowedError("User is not allowed to view this thread") + + return thread def get_threads( diff --git a/django_ai_assistant/permissions.py b/django_ai_assistant/permissions.py index 015330b..5b912c7 100644 --- a/django_ai_assistant/permissions.py +++ b/django_ai_assistant/permissions.py @@ -25,6 +25,20 @@ def can_create_thread( ) +def can_view_thread( + thread: Thread, + user: Any, + request: HttpRequest | None = None, + **kwargs, +) -> bool: + return app_settings.call_fn( + "CAN_VIEW_THREAD_FN", + **_get_default_kwargs(user, request), + thread=thread, + **kwargs, + ) + + def can_update_thread( thread: Thread, user: Any, diff --git a/docs/get-started.md b/docs/get-started.md new file mode 100644 index 0000000..50f98ee --- /dev/null +++ b/docs/get-started.md @@ -0,0 +1,32 @@ +# Get started + +## Prerequisites + +- Python: Supported Python versions +- Django >= 3.2 + +## How to install + +Install Django AI Assistant package: + +```bash +pip install django-ai-assistant +``` + +Add Django AI Assistant to your Django project's `INSTALLED_APPS`: + +```python title="myproject/settings.py" +INSTALLED_APPS = [ + ... + 'django_ai_assistant', + ... +] +``` + +Run the migrations: + +```bash +python manage.py migrate +``` + +Learn how to use the package in the [Tutorial](tutorial.md) section. diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..3bbf138 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,24 @@ +# Django AI Assistant + +Implement powerful AI Assistants using Django. +Combine the power of Large Language Models with Django's productivity. + +Regardless of the feasibility of AGI, AI assistants are (already!) a new paradigm for computation. +AI agents and assistants allow devs to easily build applications with smart decision logic +that would otherwise be too expensive to build and maintain. + +The latest LLMs from major AI providers have a "killer feature" called Tool Calling, +which enable AI models to call provided methods from Django's side, and essentially +do anything a Django view can, such as accessing DB, checking permissions, sending emails, +downloading and uploading media files, etc. + +While users commonly interact with LLMs via conversations, AI Assistants can do a lot with any kind of string input, including JSON. +You can abstract from your application's end user that a LLM is doing the heavy-lifting behind the scenes! +Some ideas for innovative AI assistants: + +- A movie recommender chatbot that helps users manage their movie backlogs +- An autofill button for certain forms of you application +- Personalized email reminders that consider user's written preferences and application's recent notifications +- A real-time audio guide for tourists that recommends attractions given the user's current location + +We have an open-source example with some of those applications. But it's best to start with the [Get Started](get-started.md) guide. diff --git a/docs/tutorial.md b/docs/tutorial.md new file mode 100644 index 0000000..da45409 --- /dev/null +++ b/docs/tutorial.md @@ -0,0 +1,396 @@ +# Tutorial + +In this tutorial, you will learn how to use Django AI Assistant to supercharge your Django project with LLM capabilities. + +## Prerequisites + +Make sure you properly configured Django AI Assistant as described in the [Get Started](get-started.md) guide. + +## Setting up API keys + +The tutorial below uses OpenAI's gpt-4o model, so make sure you have `OPENAI_API_KEY` set as an environment variable for your Django project. +You can also use other models, keep reading to learn more. Just make sure their keys are properly set. + +!!! note + An easy way to set environment variables is to use a `.env` file in your project's root directory and use `python-dotenv` to load them. + Our [example project](https://github.com/vintasoftware/django-ai-assistant/tree/main/example) uses this approach. + +## What AI Assistants can do + +AI Assistants are LLMs that can answer to user queries as ChatGPT does, i.e. inputting and outputting strings. +But when integrated with Django, they can also do anything a Django view can, such as accessing the database, +checking permissions, sending emails, downloading and uploading media files, etc. +This is possible by defining "tools" the AI can use. These tools are methods in an AI Assistant class on the Django side. + +## Defining an AI Assistant + +### Registering + +To create an AI Assistant, you need to: + +1. Create a `ai_assistants.py` file; +2. Define a class that inherits from `AIAssistant` with the decorator `@register_assistant` over it; +3. Provide an `id`, a `name`, some `instructions` for the LLM (a system prompt), and a `model` name: + +```python title="myapp/ai_assistants.py" +from django_ai_assistant import AIAssistant, register_assistant + +@register_assistant +class WeatherAIAssistant(AIAssistant): + id = "weather_assistant" + name = "Weather Assistant" + instructions = "You are a weather bot." + model = "gpt-4o" +``` + +### Defining tools + +Useful tools give abilities the LLM doesn't have out-of-the-box, +such as getting the current date and finding the current weather by calling some API. + +Use the `@method_tool` decorator to define a tool method in the AI Assistant: + +```{.python title="myapp/ai_assistants.py" hl_lines="15-22"} +from django.utils import timezone +from django_ai_assistant import AIAssistant, method_tool, register_assistant +import json + +@register_assistant +class WeatherAIAssistant(AIAssistant): + id = "weather_assistant" + name = "Weather Assistant" + instructions = "You are a weather bot." + model = "gpt-4o" + + def get_instructions(self): + return f"{self.instructions} Today is {timezone.now().isoformat()}." + + @method_tool + def get_weather(self, location: str) -> str: + """Fetch the current weather data for a location""" + return json.dumps({ + "location": location, + "temperature": "25°C", + "weather": "sunny" + }) # imagine some weather API here, this is just a placeholder +``` + +The `get_weather` method is a tool that the AI Assistant can use to get the current weather for a location, when the user asks for it. +The tool method must be fully type-hinted (all parameters and return value), and it must include a descriptive docstring. +This is necessary for the LLM model to understand the tool's purpose. + +A conversation with this Weather Assistant looks like this: + +```txt +User: What's the weather in New York City? +AI: The weather in NYC is sunny with a temperature of 25°C. +``` + +!!! note + State of the art models such as gpt-4o can process JSON well. + You can return a `json.dumps(api_output)` from a tool method and the model will be able to process it before responding the user. + +### Using Django logic in tools + +You have access to the current request user in tools: + +```{.python title="myapp/ai_assistants.py" hl_lines=13} +from django_ai_assistant import AIAssistant, method_tool, register_assistant + +@register_assistant +class PersonalAIAssistant(AIAssistant): + id = "personal_assistant" + name = "Personal Assistant" + instructions = "You are a personal assistant." + model = "gpt-4o" + + @method_tool + def get_current_user_username(self) -> str: + """Get the username of the current user""" + return self._user.username +``` + +You can also add any Django logic to tools, such as querying the database: + +```{.python title="myapp/ai_assistants.py" hl_lines=14-16} +from django_ai_assistant import AIAssistant, method_tool, register_assistant +import json + +@register_assistant +class IssueManagementAIAssistant(AIAssistant): + id = "issue_mgmt_assistant" + name = "Issue Management Assistant" + instructions = "You are an issue management bot." + model = "gpt-4o" + + @method_tool + def get_current_user_assigned_issues(self) -> str: + """Get the issues assigned to the current user""" + return json.dumps({ + "issues": list(Issue.objects.filter(assignee=self._user).values()) + }) +``` + +!!! warning + Make sure you only return to the LLM what the user can see, considering permissions and privacy. + Code the tools as if they were Django views. + +### Using pre-implemented tools + +Django AI Assistant works with [any LangChain-compatible tool](https://python.langchain.com/v0.2/docs/integrations/tools/). +Just override the `get_tools` method in your AI Assistant class to include the tools you want to use. + +For example, you can use the `TavilySearch` tool to provide your AI Assistant with the ability to search the web +for information about upcoming movies. + +First install dependencies: + +```bash +pip install -U langchain-community tavily-python +``` + +Then, set the `TAVILY_API_KEY` environment variable. You'll need to sign up at [Tavily](https://tavily.com/). + +Finally, add the tool to your AI Assistant class by overriding the `get_tools` method: + +```{.python title="myapp/ai_assistants.py" hl_lines="2 20"} +from django_ai_assistant import AIAssistant, register_assistant +from langchain_community.tools.tavily_search import TavilySearchResults + +@register_assistant +class MovieSearchAIAssistant(AIAssistant): + id = "movie_search_assistant" # noqa: A003 + instructions = ( + "You're a helpful movie search assistant. " + "Help the user find more information about movies. " + "Use the provided tools to search the web for upcoming movies. " + ) + name = "Movie Search Assistant" + model = "gpt-4o" + + def get_instructions(self): + return f"{self.instructions} Today is {timezone.now().isoformat()}." + + def get_tools(self): + return [ + TavilySearchResults(), + *super().get_tools(), + ] +``` + +!!! note + As of now, Django AI Assistant is powered by [LangChain](https://python.langchain.com/v0.2/docs/introduction/), + but previous knowledge on LangChain is NOT necessary to use this library, at least for the main use cases. + +## Using an AI Assistant + +### Manually calling an AI Assistant + +You can manually call an AI Assistant from anywhere in your Django application: + +```python +from myapp.ai_assistants import WeatherAIAssistant + +assistant = WeatherAIAssistant() +output = assistant.run("What's the weather in New York City?") +assert output == "The weather in NYC is sunny with a temperature of 25°C." +``` + +The constructor of `AIAssistant` receives `user`, `request`, `view` as optional parameters, +which can be used in the tools with `self._user`, `self._request`, `self._view`. +Also, any extra parameters passed in constructor are stored at `self._init_kwargs`. + +### Threads of Messages + +The django-ai-assistant app provides two models `Thread` and `Message` to store and retrieve conversations with AI Assistants. +LLMs are stateless by design, meaning they don't hold any context between calls. All they know is the current input. +But by using the `AIAssistant` class, the conversation state is stored in the database as as `Message`s of a `Thread`, +and automatically retrieved then passed to the LLM when calling the AI Assistant. + +To create of `Thread`s and `Message`s, you can use the helpers from the `django_ai_assistant.use_cases` module. For example: + +```{.python hl_lines="4 8"} +from django_ai_assistant.use_cases import create_thread, get_thread_messages +from myapp.ai_assistants import WeatherAIAssistant + +thread = create_thread(name="Weather Chat", user=some_user) +assistant = WeatherAIAssistant() +assistant.run("What's the weather in New York City?", thread_id=thread.id) + +messages = get_thread_messages(thread) # returns both user and AI messages +``` + +More CRUD helpers are available at `django_ai_assistant.use_cases` module. Check the API Reference for more information. + + +### Using built-in API views + +You can use the built-in API views to interact with AI Assistants via HTTP requests from any frontend, +such as a React application or a mobile app. Add the following to your Django project's `urls.py`: + +```python title="myproject/urls.py" +from django.urls import include, path + +urlpatterns = [ + path("ai-assistant/", include("django_ai_assistant.urls")), + ... +] +``` + +The built-in API supports retrieval of Assistants info, as well as CRUD for Threads and Messages. +It has a OpenAPI schema that you can explore at `ai-assistant/docs/`. + +### Configuring permissions + +The API uses the helpers from the `django_ai_assistant.use_cases` module, which have permission checks +to ensure the user has can use a certain AI Assistant or do CRUD on Threads and Messages. + +By default, any authenticated user can use any AI Assistant, and create a thread. +Users can manage both their own threads and the messages on them. Therefore, the default permissions are: + +```python title="myproject/settings.py" +AI_ASSISTANT_CAN_CREATE_THREAD_FN = "django_ai_assistant.permissions.allow_all" +AI_ASSISTANT_CAN_VIEW_THREAD_FN = "django_ai_assistant.permissions.owns_thread" +AI_ASSISTANT_CAN_UPDATE_THREAD_FN = "django_ai_assistant.permissions.owns_thread" +AI_ASSISTANT_CAN_DELETE_THREAD_FN = "django_ai_assistant.permissions.owns_thread" +AI_ASSISTANT_CAN_CREATE_MESSAGE_FN = "django_ai_assistant.permissions.owns_thread" +AI_ASSISTANT_CAN_UPDATE_MESSAGE_FN = "django_ai_assistant.permissions.owns_thread" +AI_ASSISTANT_CAN_DELETE_MESSAGE_FN = "django_ai_assistant.permissions.owns_thread" +AI_ASSISTANT_CAN_RUN_ASSISTANT = "django_ai_assistant.permissions.allow_all" +``` + +You can override these settings in your Django project's `settings.py` to customize the permissions. + +Thread permission signatures look like this: + +```python +from django_ai_assistant.models import Thread +from django.http import HttpRequest + +def check_custom_thread_permission( + thread: Thread, + user: Any, + request: HttpRequest | None = None) -> bool: + return ... +``` + +While Message permission signatures look like this: + +```python +from django_ai_assistant.models import Thread, Message +from django.http import HttpRequest + +def check_custom_message_permission( + message: Message, + thread: Thread, + user: Any, + request: HttpRequest | None = None) -> bool: + return ... +``` + +## Advanced usage + +### Using other AI models + +By default the supported models are OpenAI ones, +but you can use [any chat model from Langchain that supports Tool Calling](https://python.langchain.com/v0.2/docs/integrations/chat/#advanced-features) by overriding `get_llm`: + +```python title="myapp/ai_assistants.py" +from django_ai_assistant import AIAssistant, register_assistant +from langchain_anthropic import ChatAnthropic + +@register_assistant +class WeatherAIAssistant(AIAssistant): + id = "weather_assistant" + name = "Weather Assistant" + instructions = "You are a weather bot." + model = "claude-3-opus-20240229" + + def get_llm(self): + model = self.get_model() + temperature = self.get_temperature() + model_kwargs = self.get_model_kwargs() + return ChatAnthropic( + model_name=model, + temperature=temperature, + model_kwargs=model_kwargs, + timeout=None, + max_retries=2, + ) +``` + +### Composing AI Assistants + +One AI Assistant can call another AI Assistant as a tool. This is useful for composing complex AI Assistants. +Use the `as_tool` method for that: + +```{.python title="myapp/ai_assistants.py" hl_lines="15 17"} +@register_assistant +class SimpleAssistant(AIAssistant): + ... + +@register_assistant +class AnotherSimpleAssistant(AIAssistant): + ... + +@register_assistant +class ComplexAssistant(AIAssistant): + ... + + def get_tools(self) -> Sequence[BaseTool]: + return [ + SimpleAssistant().as_tool( + description="Tool to <...add a meaningful description here...>"), + AnotherSimpleAssistant().as_tool( + description="Tool to <...add a meaningful description here...>"), + *super().get_tools(), + ] +``` + +The `movies/ai_assistants.py` file in the [example project](https://github.com/vintasoftware/django-ai-assistant/tree/main/example) +shows an example of a composed AI Assistant that's able to recommend movies and manage the user's movie backlog. + +### Retrieval Augmented Generation (RAG) + +You can use RAG in your AI Assistants. RAG means using a retriever to fetch chunks of textual data from a pre-existing DB to give +context to the LLM. This context goes into the `{context}` placeholder in the `instructions` string, namely the system prompt. +This means the LLM will have access to a context your retriever logic provides when generating the response, +thereby improving the quality of the response by avoiding generic or off-topic answers. + +For this to work, your must do the following in your AI Assistant: + +1. Add a `{context}` placeholder in the `instructions` string; +2. Add `has_rag = True` as a class attribute; +3. Override the `get_retriever` method to return a [Langchain Retriever](https://python.langchain.com/v0.2/docs/how_to/#retrievers). + +For example: + +```{.python title="myapp/ai_assistants.py" hl_lines="12 16"} +from django_ai_assistant import AIAssistant, register_assistant + +@register_assistant +class DocsAssistant(AIAssistant): + id = "docs_assistant" # noqa: A003 + name = "Docs Assistant" + instructions = ( + "You are an assistant for answering questions related to the provided context. " + "Use the following pieces of retrieved context to answer the user's question. " + "\n\n" + "---START OF CONTEXT---\n" + "{context}" + "---END OF CONTEXT---\n" + ) + model = "gpt-4o" + has_rag = True + + def get_retriever(self) -> BaseRetriever: + return ... # use a Langchain Retriever here +``` + +The `rag/ai_assistants.py` file in the [example project](https://github.com/vintasoftware/django-ai-assistant/tree/main/example) +shows an example of a RAG-powered AI Assistant that's able to answer questions about Django using the Django Documentation as context. + +### Further configuration of AI Assistants + +You can further configure the `AIAssistant` subclasses by overriding its public methods. Check the API Reference for more information. + diff --git a/example/example/settings.py b/example/example/settings.py index b30acb7..5116f69 100644 --- a/example/example/settings.py +++ b/example/example/settings.py @@ -157,7 +157,6 @@ # django-ai-assistant -OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") AI_ASSISTANT_CAN_CREATE_THREAD_FN = "django_ai_assistant.permissions.allow_all" AI_ASSISTANT_CAN_VIEW_THREAD_FN = "django_ai_assistant.permissions.owns_thread" AI_ASSISTANT_CAN_UPDATE_THREAD_FN = "django_ai_assistant.permissions.owns_thread" diff --git a/example/weather/ai_assistants.py b/example/weather/ai_assistants.py index 847afbf..20bb626 100644 --- a/example/weather/ai_assistants.py +++ b/example/weather/ai_assistants.py @@ -16,6 +16,13 @@ class WeatherAIAssistant(AIAssistant): name = "Weather Assistant" 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"You are a weather bot. Use the provided functions to answer questions. Today is: {current_date_str}." + @method_tool def fetch_current_weather(self, location: str) -> dict: """Fetch the current weather data for a location""" @@ -57,10 +64,3 @@ def who_am_i(self) -> str: return self._user.username else: return "Anonymous" - - 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"You are a weather bot. Use the provided functions to answer questions. Today is: {current_date_str}." diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..7be349e --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,48 @@ +site_name: django-ai-assistant +site_description: Implement powerful AI assistants using Django + +repo_name: vintasoftware/django-ai-assistant +repo_url: https://github.com/vintasoftware/django-ai-assistant/ +edit_uri: blob/main/docs/ + +theme: + name: material + palette: + # Palette toggle for automatic mode + - media: "(prefers-color-scheme)" + toggle: + icon: material/brightness-auto + name: Switch to light mode + + # Palette toggle for light mode + - media: "(prefers-color-scheme: light)" + scheme: default + toggle: + icon: material/brightness-7 + name: Switch to dark mode + + # Palette toggle for dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + toggle: + icon: material/brightness-4 + name: Switch to system preference + +copyright: Vinta Software + +markdown_extensions: + - admonition + - pymdownx.highlight: + use_pygments: true + - pymdownx.inlinehilite + - pymdownx.superfences + - pymdownx.snippets: + check_paths: true + - toc: + permalink: true + - attr_list + +nav: + - Home: index.md + - Get Started: get-started.md + - Tutorial: tutorial.md diff --git a/poetry.lock b/poetry.lock index 8c30480..9359848 100644 --- a/poetry.lock +++ b/poetry.lock @@ -191,6 +191,20 @@ tests = ["attrs[tests-no-zope]", "zope-interface"] tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"] tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"] +[[package]] +name = "babel" +version = "2.15.0" +description = "Internationalization utilities" +optional = false +python-versions = ">=3.8" +files = [ + {file = "Babel-2.15.0-py3-none-any.whl", hash = "sha256:08706bdad8d0a3413266ab61bd6c34d0c28d6e1e7badf40a2cebe67644e2e1fb"}, + {file = "babel-2.15.0.tar.gz", hash = "sha256:8daf0e265d05768bc6c7a314cf1321e9a123afc328cc635c18622a2f30a04413"}, +] + +[package.extras] +dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] + [[package]] name = "beautifulsoup4" version = "4.12.3" @@ -333,6 +347,20 @@ files = [ {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, ] +[[package]] +name = "click" +version = "8.1.7" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + [[package]] name = "colorama" version = "0.4.6" @@ -668,6 +696,23 @@ files = [ {file = "frozenlist-1.4.1.tar.gz", hash = "sha256:c037a86e8513059a2613aaba4d817bb90b9d9b6b69aace3ce9c877e8c8ed402b"}, ] +[[package]] +name = "ghp-import" +version = "2.1.0" +description = "Copy your docs directly to the gh-pages branch." +optional = false +python-versions = "*" +files = [ + {file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"}, + {file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"}, +] + +[package.dependencies] +python-dateutil = ">=2.8.1" + +[package.extras] +dev = ["flake8", "markdown", "twine", "wheel"] + [[package]] name = "gitdb" version = "4.0.11" @@ -918,6 +963,23 @@ docs = ["Jinja2 (==2.11.3)", "MarkupSafe (==1.1.1)", "Pygments (==2.8.1)", "alab qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] testing = ["Django", "attrs", "colorama", "docopt", "pytest (<7.0.0)"] +[[package]] +name = "jinja2" +version = "3.1.4" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +files = [ + {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, + {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + [[package]] name = "joblib" version = "1.4.2" @@ -1226,6 +1288,90 @@ html5 = ["html5lib"] htmlsoup = ["BeautifulSoup4"] source = ["Cython (>=3.0.10)"] +[[package]] +name = "markdown" +version = "3.6" +description = "Python implementation of John Gruber's Markdown." +optional = false +python-versions = ">=3.8" +files = [ + {file = "Markdown-3.6-py3-none-any.whl", hash = "sha256:48f276f4d8cfb8ce6527c8f79e2ee29708508bf4d40aa410fbc3b4ee832c850f"}, + {file = "Markdown-3.6.tar.gz", hash = "sha256:ed4f41f6daecbeeb96e576ce414c41d2d876daa9a16cb35fa8ed8c2ddfad0224"}, +] + +[package.extras] +docs = ["mdx-gh-links (>=0.2)", "mkdocs (>=1.5)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python]"] +testing = ["coverage", "pyyaml"] + +[[package]] +name = "markupsafe" +version = "2.1.5" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.7" +files = [ + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"}, + {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, +] + [[package]] name = "marshmallow" version = "3.21.3" @@ -1259,6 +1405,103 @@ files = [ [package.dependencies] traitlets = "*" +[[package]] +name = "mergedeep" +version = "1.3.4" +description = "A deep merge function for 🐍." +optional = false +python-versions = ">=3.6" +files = [ + {file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"}, + {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"}, +] + +[[package]] +name = "mkdocs" +version = "1.6.0" +description = "Project documentation with Markdown." +optional = false +python-versions = ">=3.8" +files = [ + {file = "mkdocs-1.6.0-py3-none-any.whl", hash = "sha256:1eb5cb7676b7d89323e62b56235010216319217d4af5ddc543a91beb8d125ea7"}, + {file = "mkdocs-1.6.0.tar.gz", hash = "sha256:a73f735824ef83a4f3bcb7a231dcab23f5a838f88b7efc54a0eef5fbdbc3c512"}, +] + +[package.dependencies] +click = ">=7.0" +colorama = {version = ">=0.4", markers = "platform_system == \"Windows\""} +ghp-import = ">=1.0" +jinja2 = ">=2.11.1" +markdown = ">=3.3.6" +markupsafe = ">=2.0.1" +mergedeep = ">=1.3.4" +mkdocs-get-deps = ">=0.2.0" +packaging = ">=20.5" +pathspec = ">=0.11.1" +pyyaml = ">=5.1" +pyyaml-env-tag = ">=0.1" +watchdog = ">=2.0" + +[package.extras] +i18n = ["babel (>=2.9.0)"] +min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4)", "ghp-import (==1.0)", "importlib-metadata (==4.4)", "jinja2 (==2.11.1)", "markdown (==3.3.6)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "mkdocs-get-deps (==0.2.0)", "packaging (==20.5)", "pathspec (==0.11.1)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "watchdog (==2.0)"] + +[[package]] +name = "mkdocs-get-deps" +version = "0.2.0" +description = "MkDocs extension that lists all dependencies according to a mkdocs.yml file" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134"}, + {file = "mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c"}, +] + +[package.dependencies] +mergedeep = ">=1.3.4" +platformdirs = ">=2.2.0" +pyyaml = ">=5.1" + +[[package]] +name = "mkdocs-material" +version = "9.5.27" +description = "Documentation that simply works" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mkdocs_material-9.5.27-py3-none-any.whl", hash = "sha256:af8cc263fafa98bb79e9e15a8c966204abf15164987569bd1175fd66a7705182"}, + {file = "mkdocs_material-9.5.27.tar.gz", hash = "sha256:a7d4a35f6d4a62b0c43a0cfe7e987da0980c13587b5bc3c26e690ad494427ec0"}, +] + +[package.dependencies] +babel = ">=2.10,<3.0" +colorama = ">=0.4,<1.0" +jinja2 = ">=3.0,<4.0" +markdown = ">=3.2,<4.0" +mkdocs = ">=1.6,<2.0" +mkdocs-material-extensions = ">=1.3,<2.0" +paginate = ">=0.5,<1.0" +pygments = ">=2.16,<3.0" +pymdown-extensions = ">=10.2,<11.0" +regex = ">=2022.4" +requests = ">=2.26,<3.0" + +[package.extras] +git = ["mkdocs-git-committers-plugin-2 (>=1.1,<2.0)", "mkdocs-git-revision-date-localized-plugin (>=1.2.4,<2.0)"] +imaging = ["cairosvg (>=2.6,<3.0)", "pillow (>=10.2,<11.0)"] +recommended = ["mkdocs-minify-plugin (>=0.7,<1.0)", "mkdocs-redirects (>=1.2,<2.0)", "mkdocs-rss-plugin (>=1.6,<2.0)"] + +[[package]] +name = "mkdocs-material-extensions" +version = "1.3.1" +description = "Extension pack for Python Markdown and MkDocs Material." +optional = false +python-versions = ">=3.8" +files = [ + {file = "mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31"}, + {file = "mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443"}, +] + [[package]] name = "multidict" version = "6.0.5" @@ -1514,6 +1757,16 @@ files = [ {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, ] +[[package]] +name = "paginate" +version = "0.5.6" +description = "Divides large result sets into pages for easier browsing" +optional = false +python-versions = "*" +files = [ + {file = "paginate-0.5.6.tar.gz", hash = "sha256:5e6007b6a9398177a7e1648d04fdd9f8c9766a1a945bceac82f1929e8c78af2d"}, +] + [[package]] name = "parso" version = "0.8.4" @@ -1529,6 +1782,17 @@ files = [ qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] testing = ["docopt", "pytest"] +[[package]] +name = "pathspec" +version = "0.12.1" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, +] + [[package]] name = "pexpect" version = "4.9.0" @@ -1755,6 +2019,24 @@ files = [ [package.extras] windows-terminal = ["colorama (>=0.4.6)"] +[[package]] +name = "pymdown-extensions" +version = "10.8.1" +description = "Extension pack for Python Markdown." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pymdown_extensions-10.8.1-py3-none-any.whl", hash = "sha256:f938326115884f48c6059c67377c46cf631c733ef3629b6eed1349989d1b30cb"}, + {file = "pymdown_extensions-10.8.1.tar.gz", hash = "sha256:3ab1db5c9e21728dabf75192d71471f8e50f216627e9a1fa9535ecb0231b9940"}, +] + +[package.dependencies] +markdown = ">=3.6" +pyyaml = "*" + +[package.extras] +extra = ["pygments (>=2.12)"] + [[package]] name = "pytest" version = "8.2.2" @@ -1830,6 +2112,20 @@ vcrpy = ">=2.0.1" dev = ["pytest-recording[tests]"] tests = ["pytest-httpbin", "pytest-mock", "requests", "werkzeug (==3.0.1)"] +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + +[package.dependencies] +six = ">=1.5" + [[package]] name = "python-dotenv" version = "1.0.1" @@ -1903,6 +2199,20 @@ files = [ {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, ] +[[package]] +name = "pyyaml-env-tag" +version = "0.1" +description = "A custom YAML tag for referencing environment variables in YAML files. " +optional = false +python-versions = ">=3.6" +files = [ + {file = "pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069"}, + {file = "pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb"}, +] + +[package.dependencies] +pyyaml = "*" + [[package]] name = "regex" version = "2024.5.15" @@ -2495,6 +2805,50 @@ platformdirs = ">=3.9.1,<5" docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] +[[package]] +name = "watchdog" +version = "4.0.1" +description = "Filesystem events monitoring" +optional = false +python-versions = ">=3.8" +files = [ + {file = "watchdog-4.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:da2dfdaa8006eb6a71051795856bedd97e5b03e57da96f98e375682c48850645"}, + {file = "watchdog-4.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e93f451f2dfa433d97765ca2634628b789b49ba8b504fdde5837cdcf25fdb53b"}, + {file = "watchdog-4.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ef0107bbb6a55f5be727cfc2ef945d5676b97bffb8425650dadbb184be9f9a2b"}, + {file = "watchdog-4.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:17e32f147d8bf9657e0922c0940bcde863b894cd871dbb694beb6704cfbd2fb5"}, + {file = "watchdog-4.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:03e70d2df2258fb6cb0e95bbdbe06c16e608af94a3ffbd2b90c3f1e83eb10767"}, + {file = "watchdog-4.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:123587af84260c991dc5f62a6e7ef3d1c57dfddc99faacee508c71d287248459"}, + {file = "watchdog-4.0.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:093b23e6906a8b97051191a4a0c73a77ecc958121d42346274c6af6520dec175"}, + {file = "watchdog-4.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:611be3904f9843f0529c35a3ff3fd617449463cb4b73b1633950b3d97fa4bfb7"}, + {file = "watchdog-4.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:62c613ad689ddcb11707f030e722fa929f322ef7e4f18f5335d2b73c61a85c28"}, + {file = "watchdog-4.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:d4925e4bf7b9bddd1c3de13c9b8a2cdb89a468f640e66fbfabaf735bd85b3e35"}, + {file = "watchdog-4.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cad0bbd66cd59fc474b4a4376bc5ac3fc698723510cbb64091c2a793b18654db"}, + {file = "watchdog-4.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a3c2c317a8fb53e5b3d25790553796105501a235343f5d2bf23bb8649c2c8709"}, + {file = "watchdog-4.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c9904904b6564d4ee8a1ed820db76185a3c96e05560c776c79a6ce5ab71888ba"}, + {file = "watchdog-4.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:667f3c579e813fcbad1b784db7a1aaa96524bed53437e119f6a2f5de4db04235"}, + {file = "watchdog-4.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d10a681c9a1d5a77e75c48a3b8e1a9f2ae2928eda463e8d33660437705659682"}, + {file = "watchdog-4.0.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0144c0ea9997b92615af1d94afc0c217e07ce2c14912c7b1a5731776329fcfc7"}, + {file = "watchdog-4.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:998d2be6976a0ee3a81fb8e2777900c28641fb5bfbd0c84717d89bca0addcdc5"}, + {file = "watchdog-4.0.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:e7921319fe4430b11278d924ef66d4daa469fafb1da679a2e48c935fa27af193"}, + {file = "watchdog-4.0.1-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:f0de0f284248ab40188f23380b03b59126d1479cd59940f2a34f8852db710625"}, + {file = "watchdog-4.0.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:bca36be5707e81b9e6ce3208d92d95540d4ca244c006b61511753583c81c70dd"}, + {file = "watchdog-4.0.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:ab998f567ebdf6b1da7dc1e5accfaa7c6992244629c0fdaef062f43249bd8dee"}, + {file = "watchdog-4.0.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:dddba7ca1c807045323b6af4ff80f5ddc4d654c8bce8317dde1bd96b128ed253"}, + {file = "watchdog-4.0.1-py3-none-manylinux2014_armv7l.whl", hash = "sha256:4513ec234c68b14d4161440e07f995f231be21a09329051e67a2118a7a612d2d"}, + {file = "watchdog-4.0.1-py3-none-manylinux2014_i686.whl", hash = "sha256:4107ac5ab936a63952dea2a46a734a23230aa2f6f9db1291bf171dac3ebd53c6"}, + {file = "watchdog-4.0.1-py3-none-manylinux2014_ppc64.whl", hash = "sha256:6e8c70d2cd745daec2a08734d9f63092b793ad97612470a0ee4cbb8f5f705c57"}, + {file = "watchdog-4.0.1-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:f27279d060e2ab24c0aa98363ff906d2386aa6c4dc2f1a374655d4e02a6c5e5e"}, + {file = "watchdog-4.0.1-py3-none-manylinux2014_s390x.whl", hash = "sha256:f8affdf3c0f0466e69f5b3917cdd042f89c8c63aebdb9f7c078996f607cdb0f5"}, + {file = "watchdog-4.0.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:ac7041b385f04c047fcc2951dc001671dee1b7e0615cde772e84b01fbf68ee84"}, + {file = "watchdog-4.0.1-py3-none-win32.whl", hash = "sha256:206afc3d964f9a233e6ad34618ec60b9837d0582b500b63687e34011e15bb429"}, + {file = "watchdog-4.0.1-py3-none-win_amd64.whl", hash = "sha256:7577b3c43e5909623149f76b099ac49a1a01ca4e167d1785c76eb52fa585745a"}, + {file = "watchdog-4.0.1-py3-none-win_ia64.whl", hash = "sha256:d7b9f5f3299e8dd230880b6c55504a1f69cf1e4316275d1b215ebdd8187ec88d"}, + {file = "watchdog-4.0.1.tar.gz", hash = "sha256:eebaacf674fa25511e8867028d281e602ee6500045b57f43b08778082f7f8b44"}, +] + +[package.extras] +watchmedo = ["PyYAML (>=3.10)"] + [[package]] name = "wcwidth" version = "0.2.13" @@ -2705,4 +3059,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = ">=3.12,<3.13" -content-hash = "ccd01a998eb526dd7a4b6b4a401f34462d16a6bf5eb703c10b5b0a7175e74ef7" +content-hash = "e8ca2bf50a46ca3c51b9b56cead1ea6aefa51827aa6312669629216801cd38fe" diff --git a/pyproject.toml b/pyproject.toml index 32485db..0a6c40f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,6 +26,11 @@ ipython = "^8.24.0" pytest-asyncio = "^0.23.7" pytest-recording = "^0.13.1" coveralls = "^4.0.1" +mkdocs = "^1.6.0" +mkdocs-material = "^9.5.27" +pymdown-extensions = "^10.8.1" +markdown = "^3.6" +pygments = "^2.18.0" [tool.poetry.group.example.dependencies] django-webpack-loader = "^3.1.0" diff --git a/tests/settings.py b/tests/settings.py index 82ddef4..e394f0a 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -107,7 +107,6 @@ # django-ai-assistant # Comment the OPENAI_API_KEY below and set one on .env.tests file at root when updating the VCRs: -OPENAI_API_KEY = "sk-fake-test-key-123" AI_ASSISTANT_CAN_CREATE_THREAD_FN = "django_ai_assistant.permissions.allow_all" AI_ASSISTANT_CAN_VIEW_THREAD_FN = "django_ai_assistant.permissions.owns_thread" AI_ASSISTANT_CAN_UPDATE_THREAD_FN = "django_ai_assistant.permissions.owns_thread" From bc61cdf8aebea9ece9b481527b14b10dd6f64c81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fl=C3=A1vio=20Juvenal?= Date: Tue, 18 Jun 2024 16:40:29 -0300 Subject: [PATCH 05/14] Fix OPENAI_API_KEY comment on tests/settings.py --- tests/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/settings.py b/tests/settings.py index e394f0a..cef44a6 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -106,7 +106,7 @@ # django-ai-assistant -# Comment the OPENAI_API_KEY below and set one on .env.tests file at root when updating the VCRs: +# NOTE: set a OPENAI_API_KEY on .env.tests file at root when updating the VCRs. AI_ASSISTANT_CAN_CREATE_THREAD_FN = "django_ai_assistant.permissions.allow_all" AI_ASSISTANT_CAN_VIEW_THREAD_FN = "django_ai_assistant.permissions.owns_thread" AI_ASSISTANT_CAN_UPDATE_THREAD_FN = "django_ai_assistant.permissions.owns_thread" From b3a1a7c8a5c7c06119679bfe7d49033983cf931c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fl=C3=A1vio=20Juvenal?= Date: Wed, 19 Jun 2024 10:02:20 -0300 Subject: [PATCH 06/14] Update Django badge Co-authored-by: Pamella Bezerra --- docs/get-started.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/get-started.md b/docs/get-started.md index 50f98ee..b7bd3fe 100644 --- a/docs/get-started.md +++ b/docs/get-started.md @@ -3,7 +3,7 @@ ## Prerequisites - Python: Supported Python versions -- Django >= 3.2 +- Django: Supported Django versions ## How to install From 05a07bf9949178fb738f74ccd0e5e3f740f4ee4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fl=C3=A1vio=20Juvenal?= Date: Wed, 19 Jun 2024 10:04:49 -0300 Subject: [PATCH 07/14] Fix plurals --- docs/tutorial.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/tutorial.md b/docs/tutorial.md index da45409..6fd10e1 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -204,10 +204,10 @@ Also, any extra parameters passed in constructor are stored at `self._init_kwarg The django-ai-assistant app provides two models `Thread` and `Message` to store and retrieve conversations with AI Assistants. LLMs are stateless by design, meaning they don't hold any context between calls. All they know is the current input. -But by using the `AIAssistant` class, the conversation state is stored in the database as as `Message`s of a `Thread`, +But by using the `AIAssistant` class, the conversation state is stored in the database as multiple `Message` of a `Thread`, and automatically retrieved then passed to the LLM when calling the AI Assistant. -To create of `Thread`s and `Message`s, you can use the helpers from the `django_ai_assistant.use_cases` module. For example: +To create a `Thread`, you can use a helper from the `django_ai_assistant.use_cases` module. For example: ```{.python hl_lines="4 8"} from django_ai_assistant.use_cases import create_thread, get_thread_messages From baf72cf208c1368a8a2f845a180941c515fc2d09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fl=C3=A1vio=20Juvenal?= Date: Wed, 19 Jun 2024 10:06:08 -0300 Subject: [PATCH 08/14] Fix typos --- docs/tutorial.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/tutorial.md b/docs/tutorial.md index 6fd10e1..2c4f69a 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -243,7 +243,7 @@ It has a OpenAPI schema that you can explore at `ai-assistant/docs/`. ### Configuring permissions The API uses the helpers from the `django_ai_assistant.use_cases` module, which have permission checks -to ensure the user has can use a certain AI Assistant or do CRUD on Threads and Messages. +to ensure the user can use a certain AI Assistant or do CRUD on Threads and Messages. By default, any authenticated user can use any AI Assistant, and create a thread. Users can manage both their own threads and the messages on them. Therefore, the default permissions are: @@ -365,7 +365,7 @@ For this to work, your must do the following in your AI Assistant: For example: -```{.python title="myapp/ai_assistants.py" hl_lines="12 16"} +```{.python title="myapp/ai_assistants.py" hl_lines="12 16 18"} from django_ai_assistant import AIAssistant, register_assistant @register_assistant From 1d25cb02967589b6d40764f1f1c9e8820cde97a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fl=C3=A1vio=20Juvenal?= Date: Wed, 19 Jun 2024 10:06:35 -0300 Subject: [PATCH 09/14] Fix Vinta URL Co-authored-by: Pamella Bezerra --- mkdocs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mkdocs.yml b/mkdocs.yml index 7be349e..84f413e 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -28,7 +28,7 @@ theme: icon: material/brightness-4 name: Switch to system preference -copyright: Vinta Software +copyright: Vinta Software markdown_extensions: - admonition From 73261412a763a1ccdd1e89f624e8534692b6ad8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fl=C3=A1vio=20Juvenal?= Date: Wed, 19 Jun 2024 10:07:10 -0300 Subject: [PATCH 10/14] Fix tense Co-authored-by: Amanda Savluchinske --- docs/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index 3bbf138..9231f06 100644 --- a/docs/index.md +++ b/docs/index.md @@ -8,7 +8,7 @@ AI agents and assistants allow devs to easily build applications with smart deci that would otherwise be too expensive to build and maintain. The latest LLMs from major AI providers have a "killer feature" called Tool Calling, -which enable AI models to call provided methods from Django's side, and essentially +which enables AI models to call provided methods from Django's side, and essentially do anything a Django view can, such as accessing DB, checking permissions, sending emails, downloading and uploading media files, etc. From dec1db9f347d4081bcd9078775f03f2f06975952 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fl=C3=A1vio=20Juvenal?= Date: Wed, 19 Jun 2024 10:07:27 -0300 Subject: [PATCH 11/14] Fix plural Co-authored-by: Amanda Savluchinske --- docs/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index 9231f06..6d09069 100644 --- a/docs/index.md +++ b/docs/index.md @@ -9,7 +9,7 @@ that would otherwise be too expensive to build and maintain. The latest LLMs from major AI providers have a "killer feature" called Tool Calling, which enables AI models to call provided methods from Django's side, and essentially -do anything a Django view can, such as accessing DB, checking permissions, sending emails, +do anything a Django view can, such as accessing DBs, checking permissions, sending emails, downloading and uploading media files, etc. While users commonly interact with LLMs via conversations, AI Assistants can do a lot with any kind of string input, including JSON. From dc9df3bd8c6699131bd9b061ec50cf99b7ae4508 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fl=C3=A1vio=20Juvenal?= Date: Wed, 19 Jun 2024 10:07:47 -0300 Subject: [PATCH 12/14] Simplify text Co-authored-by: Amanda Savluchinske --- docs/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index 6d09069..c8b3a07 100644 --- a/docs/index.md +++ b/docs/index.md @@ -13,7 +13,7 @@ do anything a Django view can, such as accessing DBs, checking permissions, send downloading and uploading media files, etc. While users commonly interact with LLMs via conversations, AI Assistants can do a lot with any kind of string input, including JSON. -You can abstract from your application's end user that a LLM is doing the heavy-lifting behind the scenes! +Your application's end users won't even realize that a LLM is doing the heavy-lifting behind the scenes! Some ideas for innovative AI assistants: - A movie recommender chatbot that helps users manage their movie backlogs From ae68e3d525a3e93190bf982ca9596b44780ef683 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fl=C3=A1vio=20Juvenal?= Date: Wed, 19 Jun 2024 10:08:26 -0300 Subject: [PATCH 13/14] Improve ideas Co-authored-by: Amanda Savluchinske --- docs/index.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/index.md b/docs/index.md index c8b3a07..31c431b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -17,8 +17,8 @@ Your application's end users won't even realize that a LLM is doing the heavy-li Some ideas for innovative AI assistants: - A movie recommender chatbot that helps users manage their movie backlogs -- An autofill button for certain forms of you application -- Personalized email reminders that consider user's written preferences and application's recent notifications +- An autofill button for certain forms of your application +- Personalized email reminders that consider users' written preferences and the application's recent notifications - A real-time audio guide for tourists that recommends attractions given the user's current location We have an open-source example with some of those applications. But it's best to start with the [Get Started](get-started.md) guide. From a3df8f6769edba5fc5a5027eb1ad279efe209788 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fl=C3=A1vio=20Juvenal?= Date: Wed, 19 Jun 2024 10:09:04 -0300 Subject: [PATCH 14/14] Apply suggestions from code review Co-authored-by: Amanda Savluchinske --- docs/index.md | 2 +- docs/tutorial.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/index.md b/docs/index.md index 31c431b..8e3464f 100644 --- a/docs/index.md +++ b/docs/index.md @@ -21,4 +21,4 @@ Some ideas for innovative AI assistants: - Personalized email reminders that consider users' written preferences and the application's recent notifications - A real-time audio guide for tourists that recommends attractions given the user's current location -We have an open-source example with some of those applications. But it's best to start with the [Get Started](get-started.md) guide. +We have an open-source example with some of those applications, but it's best to start with the [Get Started](get-started.md) guide. diff --git a/docs/tutorial.md b/docs/tutorial.md index 2c4f69a..49a2a4a 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -28,7 +28,7 @@ This is possible by defining "tools" the AI can use. These tools are methods in To create an AI Assistant, you need to: -1. Create a `ai_assistants.py` file; +1. Create an `ai_assistants.py` file; 2. Define a class that inherits from `AIAssistant` with the decorator `@register_assistant` over it; 3. Provide an `id`, a `name`, some `instructions` for the LLM (a system prompt), and a `model` name: @@ -46,7 +46,7 @@ class WeatherAIAssistant(AIAssistant): ### Defining tools Useful tools give abilities the LLM doesn't have out-of-the-box, -such as getting the current date and finding the current weather by calling some API. +such as getting the current date and finding the current weather by calling an API. Use the `@method_tool` decorator to define a tool method in the AI Assistant: