Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/docker-hub.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ on:
push:
branches:
- 'main'
- 'refacto/blocknote-ai'
tags:
- 'v*'
pull_request:
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ and this project adheres to
- ✨ Import of documents #1609
- 🚨(CI) gives warning if theme not updated #1811
- 🔧(project) add DJANGO_EMAIL_URL_APP environment variable #1825
- ✨(frontend) integrate new Blocknote AI feature #1016

### Changed

Expand Down
1 change: 1 addition & 0 deletions docs/env.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
52 changes: 28 additions & 24 deletions src/backend/core/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@

from core import choices, enums, models, utils, validators
from core.services import mime_types
from core.services.ai_services import AI_ACTIONS
from core.services.converter_services import (
ConversionError,
Converter,
Expand Down Expand Up @@ -792,33 +791,38 @@ class VersionFilterSerializer(serializers.Serializer):
)


class AITransformSerializer(serializers.Serializer):
"""Serializer for AI transform requests."""
class AIProxySerializer(serializers.Serializer):
"""Serializer for AI proxy requests."""

action = serializers.ChoiceField(choices=AI_ACTIONS, required=True)
text = serializers.CharField(required=True)

def validate_text(self, value):
"""Ensure the text field is not empty."""

if len(value.strip()) == 0:
raise serializers.ValidationError("Text field cannot be empty.")
return value


class AITranslateSerializer(serializers.Serializer):
"""Serializer for AI translate requests."""

language = serializers.ChoiceField(
choices=tuple(enums.ALL_LANGUAGES.items()), required=True
messages = serializers.ListField(
required=True,
child=serializers.DictField(
child=serializers.CharField(required=True),
),
allow_empty=False,
)
text = serializers.CharField(required=True)
model = serializers.CharField(required=True)

def validate_messages(self, messages):
"""Validate messages structure."""
# Ensure each message has the required fields
for message in messages:
if (
not isinstance(message, dict)
or "role" not in message
or "content" not in message
):
raise serializers.ValidationError(
"Each message must have 'role' and 'content' fields"
)

return messages

def validate_text(self, value):
"""Ensure the text field is not empty."""
def validate_model(self, value):
"""Validate model value is the same than settings.AI_MODEL"""
if value != settings.AI_MODEL:
raise serializers.ValidationError(f"{value} is not a valid model")

if len(value.strip()) == 0:
raise serializers.ValidationError("Text field cannot be empty.")
return value


Expand Down
87 changes: 30 additions & 57 deletions src/backend/core/api/viewsets.py
Original file line number Diff line number Diff line change
Expand Up @@ -338,21 +338,8 @@ class DocumentViewSet(
9. **Media Auth**: Authorize access to document media.
Example: GET /documents/media-auth/

10. **AI Transform**: Apply a transformation action on a piece of text with AI.
Example: POST /documents/{id}/ai-transform/
Expected data:
- text (str): The input text.
- action (str): The transformation type, one of [prompt, correct, rephrase, summarize].
Returns: JSON response with the processed text.
Throttled by: AIDocumentRateThrottle, AIUserRateThrottle.

11. **AI Translate**: Translate a piece of text with AI.
Example: POST /documents/{id}/ai-translate/
Expected data:
- text (str): The input text.
- language (str): The target language, chosen from settings.LANGUAGES.
Returns: JSON response with the translated text.
Throttled by: AIDocumentRateThrottle, AIUserRateThrottle.
10. **AI Proxy**: Proxy an AI request to an external AI service.
Example: POST /api/v1.0/documents/<resource_id>/ai-proxy

### Ordering: created_at, updated_at, is_favorite, title

Expand Down Expand Up @@ -391,7 +378,6 @@ class DocumentViewSet(
throttle_scope = "document"
queryset = models.Document.objects.select_related("creator").all()
serializer_class = serializers.DocumentSerializer
ai_translate_serializer_class = serializers.AITranslateSerializer
all_serializer_class = serializers.ListDocumentSerializer
children_serializer_class = serializers.ListDocumentSerializer
descendants_serializer_class = serializers.ListDocumentSerializer
Expand Down Expand Up @@ -1645,58 +1631,42 @@ def media_check(self, request, *args, **kwargs):
@drf.decorators.action(
detail=True,
methods=["post"],
name="Apply a transformation action on a piece of text with AI",
url_path="ai-transform",
throttle_classes=[utils.AIDocumentRateThrottle, utils.AIUserRateThrottle],
name="Proxy AI requests to the AI provider",
url_path="ai-proxy",
# throttle_classes=[utils.AIDocumentRateThrottle, utils.AIUserRateThrottle],
)
def ai_transform(self, request, *args, **kwargs):
def ai_proxy(self, request, *args, **kwargs):
"""
POST /api/v1.0/documents/<resource_id>/ai-transform
with expected data:
- text: str
- action: str [prompt, correct, rephrase, summarize]
Return JSON response with the processed text.
POST /api/v1.0/documents/<resource_id>/ai-proxy
Proxy AI requests to the configured AI provider.
This endpoint forwards requests to the AI provider and returns the complete response.
"""
# Check permissions first
self.get_object()

serializer = serializers.AITransformSerializer(data=request.data)
serializer.is_valid(raise_exception=True)

text = serializer.validated_data["text"]
action = serializer.validated_data["action"]
if not settings.AI_FEATURE_ENABLED:
raise ValidationError("AI feature is not enabled.")

response = AIService().transform(text, action)
ai_service = AIService()

return drf.response.Response(response, status=drf.status.HTTP_200_OK)

@drf.decorators.action(
detail=True,
methods=["post"],
name="Translate a piece of text with AI",
url_path="ai-translate",
throttle_classes=[utils.AIDocumentRateThrottle, utils.AIUserRateThrottle],
)
def ai_translate(self, request, *args, **kwargs):
"""
POST /api/v1.0/documents/<resource_id>/ai-translate
with expected data:
- text: str
- language: str [settings.LANGUAGES]
Return JSON response with the translated text.
"""
# Check permissions first
self.get_object()

serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
if settings.AI_STREAM:
stream_gen = ai_service.stream_proxy(
url=settings.AI_BASE_URL.rstrip("/") + "/chat/completions",
method="POST",
headers={"Content-Type": "application/json"},
body=json.dumps(request.data, ensure_ascii=False).encode("utf-8"),
)

text = serializer.validated_data["text"]
language = serializer.validated_data["language"]
resp = StreamingHttpResponse(
streaming_content=stream_gen,
content_type="text/event-stream",
status=200,
)
resp["X-Accel-Buffering"] = "no"
resp["Cache-Control"] = "no-cache"
return resp

response = AIService().translate(text, language)

return drf.response.Response(response, status=drf.status.HTTP_200_OK)

def _reject_invalid_ips(self, ips):
"""
Expand Down Expand Up @@ -2337,7 +2307,10 @@ def get(self, request):
Return a dictionary of public settings.
"""
array_settings = [
"AI_BOT",
"AI_FEATURE_ENABLED",
"AI_MODEL",
"AI_STREAM",
"COLLABORATION_WS_URL",
"COLLABORATION_WS_NOT_CONNECTED_READY_ONLY",
"CONVERSION_FILE_EXTENSIONS_ALLOWED",
Expand Down
3 changes: 1 addition & 2 deletions src/backend/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -783,8 +783,7 @@ def get_abilities(self, user):
return {
"accesses_manage": is_owner_or_admin,
"accesses_view": has_access_role,
"ai_transform": ai_access,
"ai_translate": ai_access,
"ai_proxy": ai_access,
"attachment_upload": can_update,
"media_check": can_get,
"can_edit": can_update,
Expand Down
Loading
Loading