diff --git a/django_ai_assistant/api/views.py b/django_ai_assistant/api/views.py index b5a19bb..0d52443 100644 --- a/django_ai_assistant/api/views.py +++ b/django_ai_assistant/api/views.py @@ -16,6 +16,7 @@ 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 @@ -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) 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..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")), ... @@ -243,6 +235,28 @@ 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 @@ -270,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, @@ -284,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, @@ -304,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" @@ -329,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): ... @@ -369,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" 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/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/frontend/src/config.ts b/frontend/src/config.ts index a82dc96..d1aded3 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) { 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; } 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"