Skip to content

Commit

Permalink
Merge pull request #101 from vintasoftware/feat/custom-config-ninja-api
Browse files Browse the repository at this point in the history
Allow users to configure backend API and frontend client
  • Loading branch information
pamella authored Jun 20, 2024
2 parents 7a907a3 + d8e1381 commit a3d4dd4
Show file tree
Hide file tree
Showing 7 changed files with 68 additions and 41 deletions.
21 changes: 13 additions & 8 deletions django_ai_assistant/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -28,14 +29,18 @@ def get_openapi_operation_id(self, operation: Operation) -> str:
return (package_name + "_" + name).replace(".", "_")


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


api = app_settings.call_fn("INIT_API_FN")


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


DEFAULTS = {
"INIT_API_FN": "django_ai_assistant.api.views.init_api",
"CAN_CREATE_THREAD_FN": "django_ai_assistant.permissions.allow_all",
"CAN_VIEW_THREAD_FN": "django_ai_assistant.permissions.owns_thread",
"CAN_UPDATE_THREAD_FN": "django_ai_assistant.permissions.owns_thread",
Expand Down
50 changes: 29 additions & 21 deletions docs/tutorial.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand Down Expand Up @@ -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"
Expand All @@ -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"
Expand Down Expand Up @@ -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 = (
Expand Down Expand Up @@ -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."
Expand All @@ -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)
Expand All @@ -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")),
...
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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"
Expand All @@ -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):
...
Expand Down Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion example/assets/js/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
1 change: 1 addition & 0 deletions example/example/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
33 changes: 22 additions & 11 deletions frontend/src/config.ts
Original file line number Diff line number Diff line change
@@ -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;
}
1 change: 1 addition & 0 deletions tests/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down

0 comments on commit a3d4dd4

Please sign in to comment.