🔧(backend) make frontend ai bot configurable

We make the AI bot configurable with settings.
We will be able to have different AI bot name
per instance.
This commit is contained in:
Anthony LC
2026-01-27 17:53:18 +01:00
parent 08b04dea90
commit 91aa9d6acb
9 changed files with 51 additions and 15 deletions
+1
View File
@@ -11,6 +11,7 @@ These are the environment variables you can set for the `impress-backend` contai
| AI_ALLOW_REACH_FROM | Users that can use AI must be this level. options are "public", "authenticated", "restricted" | authenticated |
| AI_API_KEY | AI key to be used for AI Base url | |
| AI_BASE_URL | OpenAI compatible AI base url | |
| AI_BOT | Information to give to the frontend about the AI bot | { "name": "Docs AI", "color": "#8bc6ff" }
| AI_FEATURE_ENABLED | Enable AI options | false |
| AI_MODEL | AI Model to use | |
| ALLOW_LOGOUT_GET_METHOD | Allow get logout method | true |
+2
View File
@@ -2349,7 +2349,9 @@ class ConfigView(drf.views.APIView):
Return a dictionary of public settings.
"""
array_settings = [
"AI_BOT",
"AI_FEATURE_ENABLED",
"AI_MODEL",
"COLLABORATION_WS_URL",
"COLLABORATION_WS_NOT_CONNECTED_READY_ONLY",
"CONVERSION_FILE_EXTENSIONS_ALLOWED",
@@ -19,7 +19,9 @@ pytestmark = pytest.mark.django_db
@override_settings(
AI_BOT={"name": "Test Bot", "color": "#000000"},
AI_FEATURE_ENABLED=False,
AI_MODEL="test-model",
COLLABORATION_WS_URL="http://testcollab/",
COLLABORATION_WS_NOT_CONNECTED_READY_ONLY=True,
CRISP_WEBSITE_ID="123",
@@ -43,6 +45,9 @@ def test_api_config(is_authenticated):
response = client.get("/api/v1.0/config/")
assert response.status_code == HTTP_200_OK
assert response.json() == {
"AI_BOT": {"name": "Test Bot", "color": "#000000"},
"AI_FEATURE_ENABLED": False,
"AI_MODEL": "test-model",
"AI_FEATURE_ENABLED": False,
"COLLABORATION_WS_URL": "http://testcollab/",
"COLLABORATION_WS_NOT_CONNECTED_READY_ONLY": True,
+15 -7
View File
@@ -670,24 +670,32 @@ class Base(Configuration):
default=True, environ_name="ALLOW_LOGOUT_GET_METHOD", environ_prefix=None
)
# AI service
AI_FEATURE_ENABLED = values.BooleanValue(
default=False, environ_name="AI_FEATURE_ENABLED", environ_prefix=None
)
AI_API_KEY = SecretFileValue(None, environ_name="AI_API_KEY", environ_prefix=None)
AI_BASE_URL = values.Value(None, environ_name="AI_BASE_URL", environ_prefix=None)
AI_MODEL = values.Value(None, environ_name="AI_MODEL", environ_prefix=None)
# AI settings
AI_ALLOW_REACH_FROM = values.Value(
choices=("public", "authenticated", "restricted"),
default="authenticated",
environ_name="AI_ALLOW_REACH_FROM",
environ_prefix=None,
)
AI_API_KEY = SecretFileValue(None, environ_name="AI_API_KEY", environ_prefix=None)
AI_BASE_URL = values.Value(None, environ_name="AI_BASE_URL", environ_prefix=None)
AI_BOT = values.DictValue(
default={
"name": _("Docs AI"),
"color": "#8bc6ff",
},
environ_name="AI_BOT",
environ_prefix=None,
)
AI_DOCUMENT_RATE_THROTTLE_RATES = {
"minute": 5,
"hour": 100,
"day": 500,
}
AI_FEATURE_ENABLED = values.BooleanValue(
default=False, environ_name="AI_FEATURE_ENABLED", environ_prefix=None
)
AI_MODEL = values.Value(None, environ_name="AI_MODEL", environ_prefix=None)
AI_USER_RATE_THROTTLE_RATES = {
"minute": 3,
"hour": 50,
@@ -390,6 +390,15 @@ test.describe('Doc Editor', () => {
});
test('it checks the AI feature', async ({ page, browserName }) => {
await overrideConfig(page, {
AI_BOT: {
name: 'Albert AI',
color: '#8bc6ff',
},
});
await page.goto('/');
await page.route(/.*\/ai-proxy\//, async (route) => {
const request = route.request();
if (request.method().includes('POST')) {
@@ -474,7 +483,7 @@ test.describe('Doc Editor', () => {
await page.getByRole('option', { name: 'Translate' }).click();
await page.getByPlaceholder('Ask AI anything…').fill('French');
await page.getByPlaceholder('Ask AI anything…').press('Enter');
await expect(editor.getByText('Docs AI')).toBeVisible();
await expect(editor.getByText('Albert AI')).toBeVisible();
await page
.locator('p.bn-mt-suggestion-menu-item-title')
.getByText('Accept')
@@ -4,7 +4,12 @@ export type BrowserName = 'chromium' | 'firefox' | 'webkit';
export const BROWSERS: BrowserName[] = ['chromium', 'webkit', 'firefox'];
export const CONFIG = {
AI_BOT: {
name: 'Docs AI',
color: '#8bc6ff',
},
AI_FEATURE_ENABLED: true,
AI_MODEL: 'llama',
CRISP_WEBSITE_ID: null,
COLLABORATION_WS_URL: 'ws://localhost:4444/collaboration/ws/',
COLLABORATION_WS_NOT_CONNECTED_READY_ONLY: true,
@@ -15,7 +15,9 @@ interface ThemeCustomization {
}
export interface ConfigResponse {
AI_BOT: { name: string; color: string };
AI_FEATURE_ENABLED?: boolean;
AI_MODEL?: string;
COLLABORATION_WS_URL?: string;
COLLABORATION_WS_NOT_CONNECTED_READY_ONLY?: boolean;
CONVERSION_FILE_EXTENSIONS_ALLOWED: string[];
@@ -3,6 +3,7 @@ import { createAIExtension, createBlockNoteAIClient } from '@blocknote/xl-ai';
import { useMemo } from 'react';
import { fetchAPI } from '@/api';
import { useConfig } from '@/core';
import { Doc } from '@/docs/doc-management';
const client = createBlockNoteAIClient({
@@ -17,7 +18,13 @@ const client = createBlockNoteAIClient({
* Custom prompts can be invoked using the pattern !promptName in the AI input field.
*/
export const useAI = (docId: Doc['id']) => {
const conf = useConfig().data;
return useMemo(() => {
if (!conf?.AI_MODEL) {
return null;
}
const openai = createOpenAI({
...client.getProviderSettings('openai'),
fetch: (input, init) => {
@@ -31,17 +38,14 @@ export const useAI = (docId: Doc['id']) => {
});
},
});
const model = openai.chat('neuralmagic/Meta-Llama-3.1-70B-Instruct-FP8');
const model = openai.chat(conf.AI_MODEL);
const extension = createAIExtension({
stream: false,
model,
agentCursor: {
name: 'Albert',
color: '#8bc6ff',
},
agentCursor: conf?.AI_BOT,
});
return extension;
}, [docId]);
}, [docId, conf?.AI_BOT, conf?.AI_MODEL]);
};
@@ -252,7 +252,7 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
comments={showComments}
aria-label={t('Document editor')}
>
<AIMenuController aiMenu={AIMenu} />
{aiExtension && <AIMenuController aiMenu={AIMenu} />}
<BlockNoteSuggestionMenu />
<BlockNoteToolbar />
</BlockNoteView>