From 0dd9beb91a49c6cc291a1a2fd77d0394edcadae3 Mon Sep 17 00:00:00 2001 From: Pamella Bezerra Date: Wed, 19 Jun 2024 14:46:33 -0300 Subject: [PATCH 1/5] Add support for INIT_API_FN --- django_ai_assistant/api/views.py | 30 +++++++++++++++++------------- django_ai_assistant/conf.py | 1 + docs/tutorial.md | 25 +++++++++++++++++++++++++ example/example/settings.py | 1 + tests/settings.py | 1 + 5 files changed, 45 insertions(+), 13 deletions(-) diff --git a/django_ai_assistant/api/views.py b/django_ai_assistant/api/views.py index b5a19bb..8cdb874 100644 --- a/django_ai_assistant/api/views.py +++ b/django_ai_assistant/api/views.py @@ -16,26 +16,30 @@ ThreadSchema, ThreadSchemaIn, ) +from django_ai_assistant.conf import app_settings from django_ai_assistant.exceptions import AIUserNotAllowedError from django_ai_assistant.helpers import use_cases from django_ai_assistant.models import Message, Thread -class API(NinjaAPI): - # Force "operationId" to be like "django_ai_assistant_delete_thread" - def get_openapi_operation_id(self, operation: Operation) -> str: - name = operation.view_func.__name__ - return (package_name + "_" + name).replace(".", "_") +def init_api(): + class API(NinjaAPI): + # Force "operationId" to be like "django_ai_assistant_delete_thread" + def get_openapi_operation_id(self, operation: Operation) -> str: + name = operation.view_func.__name__ + return (package_name + "_" + name).replace(".", "_") + + return API( + title=package_name, + version=version, + urls_namespace="django_ai_assistant", + # Add auth to all endpoints + auth=django_auth, + csrf=True, + ) -api = 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) diff --git a/django_ai_assistant/conf.py b/django_ai_assistant/conf.py index 6d25e77..7ebe069 100644 --- a/django_ai_assistant/conf.py +++ b/django_ai_assistant/conf.py @@ -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", diff --git a/docs/tutorial.md b/docs/tutorial.md index e1ab309..2e15669 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -243,6 +243,31 @@ urlpatterns = [ 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 the API + +The built-in API is implemented using [Django Ninja](https://django-ninja.dev/reference/api/). By default, it is initialized with the following setting: + +```python title="myproject/settings.py" +AI_ASSISTANT_INIT_API_FN = "django_ai_assistant.api.views.init_api" +``` + +You can override this setting in your Django project's `settings.py` to customize the API, such as using a different authentication method or modifying other configurations. + +The method signature for `AI_ASSISTANT_INIT_API_FN` is as follows: + +```python +from ninja import NinjaAPI + + +def init_api(): + return NinjaAPI( + ... + ) +``` + +By providing your own implementation of init_api, you can tailor the API setup to better fit your project's requirements. + ### Configuring permissions The API uses the helpers from the `django_ai_assistant.use_cases` module, which have permission checks diff --git a/example/example/settings.py b/example/example/settings.py index 5116f69..eb58af8 100644 --- a/example/example/settings.py +++ b/example/example/settings.py @@ -157,6 +157,7 @@ # django-ai-assistant +AI_ASSISTANT_INIT_API_FN = "django_ai_assistant.api.views.init_api" 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/tests/settings.py b/tests/settings.py index cef44a6..47aa14c 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -107,6 +107,7 @@ # django-ai-assistant # NOTE: set a OPENAI_API_KEY on .env.tests file at root when updating the VCRs. +AI_ASSISTANT_INIT_API_FN = "django_ai_assistant.api.views.init_api" 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 1943544625d530819961f7b7a0db10baaf7e86cc Mon Sep 17 00:00:00 2001 From: Pamella Bezerra Date: Wed, 19 Jun 2024 15:28:03 -0300 Subject: [PATCH 2/5] Refactor configAIAssistant to allow config overrides --- example/assets/js/App.tsx | 2 +- frontend/src/config.ts | 35 +++++++++++++++++++++++------------ 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/example/assets/js/App.tsx b/example/assets/js/App.tsx index d151019..7adfbd5 100644 --- a/example/assets/js/App.tsx +++ b/example/assets/js/App.tsx @@ -16,7 +16,7 @@ const theme = createTheme({}); // Relates to path("ai-assistant/", include("django_ai_assistant.urls")) // which can be found at example/demo/urls.py) -configAIAssistant({ baseURL: "ai-assistant" }); +configAIAssistant({ BASE: "ai-assistant" }); const ExampleIndex = () => { return ( diff --git a/frontend/src/config.ts b/frontend/src/config.ts index a82dc96..6b891ad 100644 --- a/frontend/src/config.ts +++ b/frontend/src/config.ts @@ -1,27 +1,38 @@ import cookie from "cookie"; -import { OpenAPI } from "./client"; +import { OpenAPI, OpenAPIConfig } 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 AI Assistant client, such as setting the base URL (which is + * associated with the Django path include) and request interceptors. * - * Configures the Axios request to include the CSRF token if it exists. + * By default, this function will add a request interceptor to include the CSRF token + * in the request headers if it exists. You can override the default request interceptor + * by providing your own request interceptor function in the configuration object. * - * @param baseURL Base URL of the AI Assistant API. + * NOTE: This function must be called in the root of your application before any + * requests are made to the AI Assistant API. + * + * @param props An `OpenAPIConfig` object containing configuration options for the OpenAPI client. * * @example - * configAIAssistant({ baseURL: "ai-assistant" }); + * configAIAssistant({ BASE: "ai-assistant" }); */ -export function configAIAssistant({ baseURL }: { baseURL: string }) { - OpenAPI.BASE = baseURL; - - OpenAPI.interceptors.request.use((request: AxiosRequestConfig) => { +export function configAIAssistant(props: OpenAPIConfig): OpenAPIConfig { + function defaultRequestInterceptor(request: AxiosRequestConfig) { const { csrftoken } = cookie.parse(document.cookie); - if (request.headers && csrftoken) { + if (csrftoken && request.headers) { request.headers["X-CSRFTOKEN"] = csrftoken; } return request; - }); + } + + OpenAPI.interceptors.request.use(defaultRequestInterceptor); + + // Apply the configuration options to the OpenAPI client, and allow the user + // to override the default request interceptor. + Object.assign(OpenAPI, props); + + return OpenAPI; } From a3365de7a949b191861021ae15ad158491e8a627 Mon Sep 17 00:00:00 2001 From: Pamella Bezerra Date: Wed, 19 Jun 2024 15:32:08 -0300 Subject: [PATCH 3/5] Undo unnecessary change --- frontend/src/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/config.ts b/frontend/src/config.ts index 6b891ad..d1aded3 100644 --- a/frontend/src/config.ts +++ b/frontend/src/config.ts @@ -22,7 +22,7 @@ import { AxiosRequestConfig } from "axios"; export function configAIAssistant(props: OpenAPIConfig): OpenAPIConfig { function defaultRequestInterceptor(request: AxiosRequestConfig) { const { csrftoken } = cookie.parse(document.cookie); - if (csrftoken && request.headers) { + if (request.headers && csrftoken) { request.headers["X-CSRFTOKEN"] = csrftoken; } return request; From 89fcf860ebc76aa6d8b0a2cb3257e847802a3ca1 Mon Sep 17 00:00:00 2001 From: Pamella Bezerra Date: Thu, 20 Jun 2024 09:54:33 -0300 Subject: [PATCH 4/5] Update docs/tutorial.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Flávio Juvenal --- docs/tutorial.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/tutorial.md b/docs/tutorial.md index 2e15669..3d71aa7 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -261,12 +261,10 @@ from ninja import NinjaAPI def init_api(): - return NinjaAPI( - ... - ) + return NinjaAPI(...) ``` -By providing your own implementation of init_api, you can tailor the API setup to better fit your project's requirements. +By providing your own implementation of `init_api`, you can tailor the API setup to better fit your project's requirements. ### Configuring permissions From d5d5ff12edf8a0a45e3f0b18d3d1cbf8dbfbc1de Mon Sep 17 00:00:00 2001 From: Pamella Bezerra Date: Thu, 20 Jun 2024 10:00:35 -0300 Subject: [PATCH 5/5] Address code review notes --- django_ai_assistant/api/views.py | 13 +++++++------ docs/tutorial.md | 29 +++++++---------------------- 2 files changed, 14 insertions(+), 28 deletions(-) diff --git a/django_ai_assistant/api/views.py b/django_ai_assistant/api/views.py index 8cdb874..0d52443 100644 --- a/django_ai_assistant/api/views.py +++ b/django_ai_assistant/api/views.py @@ -22,13 +22,14 @@ from django_ai_assistant.models import Message, Thread -def init_api(): - class API(NinjaAPI): - # Force "operationId" to be like "django_ai_assistant_delete_thread" - def get_openapi_operation_id(self, operation: Operation) -> str: - name = operation.view_func.__name__ - return (package_name + "_" + name).replace(".", "_") +class API(NinjaAPI): + # Force "operationId" to be like "django_ai_assistant_delete_thread" + def get_openapi_operation_id(self, operation: Operation) -> str: + name = operation.view_func.__name__ + return (package_name + "_" + name).replace(".", "_") + +def init_api(): return API( title=package_name, version=version, diff --git a/docs/tutorial.md b/docs/tutorial.md index 3d71aa7..f749ab7 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -35,7 +35,6 @@ To create an AI Assistant, you need to: ```python title="myapp/ai_assistants.py" from django_ai_assistant import AIAssistant - class WeatherAIAssistant(AIAssistant): id = "weather_assistant" name = "Weather Assistant" @@ -50,12 +49,11 @@ such as getting the current date and finding the current weather by calling an A Use the `@method_tool` decorator to define a tool method in the AI Assistant: -```{.python title="myapp/ai_assistants.py" hl_lines="15-22"} +```{.python title="myapp/ai_assistants.py" hl_lines="14-21"} from django.utils import timezone from django_ai_assistant import AIAssistant, method_tool import json - class WeatherAIAssistant(AIAssistant): id = "weather_assistant" name = "Weather Assistant" @@ -94,10 +92,9 @@ AI: The weather in NYC is sunny with a temperature of 25°C. You have access to the current request user in tools: -```{.python title="myapp/ai_assistants.py" hl_lines=13} +```{.python title="myapp/ai_assistants.py" hl_lines=12} from django_ai_assistant import AIAssistant, method_tool - class PersonalAIAssistant(AIAssistant): id = "personal_assistant" name = "Personal Assistant" @@ -112,11 +109,10 @@ class PersonalAIAssistant(AIAssistant): You can also add any Django logic to tools, such as querying the database: -```{.python title="myapp/ai_assistants.py" hl_lines=14-16} +```{.python title="myapp/ai_assistants.py" hl_lines=13-15} from django_ai_assistant import AIAssistant, method_tool import json - class IssueManagementAIAssistant(AIAssistant): id = "issue_mgmt_assistant" name = "Issue Management Assistant" @@ -153,11 +149,10 @@ Then, set the `TAVILY_API_KEY` environment variable. You'll need to sign up at [ 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"} +```{.python title="myapp/ai_assistants.py" hl_lines="2 19"} from django_ai_assistant import AIAssistant from langchain_community.tools.tavily_search import TavilySearchResults - class MovieSearchAIAssistant(AIAssistant): id = "movie_search_assistant" # noqa: A003 instructions = ( @@ -191,7 +186,6 @@ 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." @@ -210,11 +204,10 @@ and automatically retrieved then passed to the LLM when calling the AI Assistant To create a `Thread`, you can use a helper from the `django_ai_assistant.use_cases` module. For example: -```{.python hl_lines="5 9"} +```{.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) @@ -233,7 +226,6 @@ such as a React application or a mobile app. Add the following to your Django pr ```python title="myproject/urls.py" from django.urls import include, path - urlpatterns = [ path("ai-assistant/", include("django_ai_assistant.urls")), ... @@ -259,7 +251,6 @@ The method signature for `AI_ASSISTANT_INIT_API_FN` is as follows: ```python from ninja import NinjaAPI - def init_api(): return NinjaAPI(...) ``` @@ -293,7 +284,6 @@ Thread permission signatures look like this: from django_ai_assistant.models import Thread from django.http import HttpRequest - def check_custom_thread_permission( thread: Thread, user: Any, @@ -307,7 +297,6 @@ While Message permission signatures look like this: from django_ai_assistant.models import Thread, Message from django.http import HttpRequest - def check_custom_message_permission( message: Message, thread: Thread, @@ -327,7 +316,6 @@ but you can use [any chat model from Langchain that supports Tool Calling](https from django_ai_assistant import AIAssistant from langchain_anthropic import ChatAnthropic - class WeatherAIAssistant(AIAssistant): id = "weather_assistant" name = "Weather Assistant" @@ -352,15 +340,13 @@ class WeatherAIAssistant(AIAssistant): 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="14 16"} +```{.python title="myapp/ai_assistants.py" hl_lines="12 14"} class SimpleAssistant(AIAssistant): ... - class AnotherSimpleAssistant(AIAssistant): ... - class ComplexAssistant(AIAssistant): ... @@ -392,10 +378,9 @@ 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 18"} +```{.python title="myapp/ai_assistants.py" hl_lines="11 15 17"} from django_ai_assistant import AIAssistant - class DocsAssistant(AIAssistant): id = "docs_assistant" # noqa: A003 name = "Docs Assistant"