Skip to content
Draft
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
2 changes: 2 additions & 0 deletions docs/env.md
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,8 @@ The application uses a new environment file structure with `.defaults` and `.loc
| `AI_MODEL` | None | Default model used for AI features | Optional |
| `AI_FEATURE_SUMMARY_ENABLED` | `False` | Default enabled mode for summary AI features | Required |
| `AI_FEATURE_AUTOLABELS_ENABLED` | `False` | Default enabled mode for label AI features | Required |
| `AI_FEATURE_MESSAGES_GENERATION_ENABLED` | `False` | Default enabled mode for message generation AI features | Required |


### External Services

Expand Down
1 change: 1 addition & 0 deletions env.d/development/backend.defaults
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ AI_MODEL=
# AI features
AI_FEATURE_SUMMARY_ENABLED=False
AI_FEATURE_AUTOLABELS_ENABLED=False
AI_FEATURE_MESSAGES_GENERATION_ENABLED=False

# Interoperability
# Drive - https://github.com/suitenumerique/drive
Expand Down
10 changes: 7 additions & 3 deletions src/backend/core/ai/ai_prompts.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
{
"en-us": {
"summary_query": "You are an intelligent assistant that summarizes email threads. You should summarize the content of the conversation in one or two lines maximum WITHOUT stating 'Summary:'. If important links appear in the emails, you must mention them clearly in Markdown format and integrate them into the summary. If there are no important links, the summary should not contain any information about links. \n Here is the conversation:\n\n{messages}\n\n. Summarize the above emails in Markdown in the language '{language}' :",
"autolabels_query": "Current date and time: {date_time}\nEmail or email thread: {messages}\nList of existing labels: {labels}\nFor this email or email thread, you must check for each label whether it is relevant to assign it.\nYou should assign a label only if it is truly relevant!\nRead the description of each label carefully to strictly follow the criteria provided by the user.\nIf no label is relevant, you must not assign any.\n\nYour response must be in the following format:\n[\"LABEL1\", \"LABEL2\", ...]\nNO additional text like 'Here is the list of relevant labels:'.\nIf no label is relevant, you must return an empty list: [] with no other comment !! Your reponse must always be only a list (than can be empty). No explanation or additional text should be included than the list itself."
"autolabels_query": "Current date and time: {date_time}\nEmail or email thread: {messages}\nList of existing labels: {labels}\nFor this email or email thread, you must check for each label whether it is relevant to assign it.\nYou should assign a label only if it is truly relevant!\nRead the description of each label carefully to strictly follow the criteria provided by the user.\nIf no label is relevant, you must not assign any.\n\nYour response must be in the following format:\n[\"LABEL1\", \"LABEL2\", ...]\nNO additional text like 'Here is the list of relevant labels:'.\nIf no label is relevant, you must return an empty list: [] with no other comment !! Your reponse must always be only a list (than can be empty). No explanation or additional text should be included than the list itself.",
"new_message_generation_query": "You are an assistant integrated into the email writing area of an email client. THE EXPECTED RESPONSE IS AN EMAIL BODY IN THE LANGUAGE {language}. Your response should ONLY be the body of the message (do not mention the subject or recipients) DO NOT put quotes around your response. If your response contains fields to be filled by the user, MAKE THEM ALL IN BOLD MARKDOWN AND IN BRACKETS so that the user notices them (e.g., **[Your Name]**, **[Date]**, **[Signature]**, **[Number of Users]**). Here is an example of generation: 'User prompt: 'Request an API key for Mistral'. Your response: 'Hello,\nCould you please provide me with an API key for Mistral?\nBest regards,\n[Signature]' Here is the current email draft:\n\n{draft}\n\n. Here is the user prompt : {user_prompt}\nIf the draft does not contain a signature and only in this case, then sign with {name}. NEVER CHANGE ANYTHING ELSE IN THE DRAFT OTHER THAN WHAT THE USER REQUESTS!!!",
"thread_response_generation_query": "You are an assistant integrated into the email writing area of an email client. THE EXPECTED RESPONSE IS AN EMAIL BODY IN THE LANGUAGE {language}. Your response should ONLY be the body of the message (do not mention the subject or recipients) DO NOT put quotes around your response. If your response contains fields to be filled by the user, MAKE THEM ALL IN BOLD MARKDOWN AND IN BRACKETS so that the user notices them (e.g., **[Your Name]**, **[Date]**, **[Signature]**, **[Number of Users]**). Here is an example of generation: 'User prompt: 'Request an API key for Mistral'. Previous messages: 'Hello Thomas,\nI recommend using Mistral for AI.\nBest regards,\nNicolas' User prompt: 'request the key'. Your response: Hello Nicolas,\nThank you for your response, could you please provide me with an API key for Mistral?\nBest regards,\nThomas' Your response: 'Hello,\nCould you please provide me with an API key for Mistral?\nBest regards,\n[Signature]' Here is the current email draft:\n\n{draft}\n\n. Here are the previous messages in the thread:\n\n{messages}\n\n. Here is the user prompt : {user_prompt}\nIf the draft does not contain a signature and only in this case, then sign with {name}. NEVER CHANGE ANYTHING ELSE IN THE DRAFT OTHER THAN WHAT THE USER REQUESTS!!!"
},
"fr-fr": {
"summary_query": "Tu es un assistant intelligent qui résume des boucles de mails. Tu dois résumer le contenu de la conversation en une ou deux lignes maximum SANS préciser 'Résumé:'. Si des liens importants apparaissent dans les emails, tu dois les mentionner dans le résumé de façon claire en Markdown et les intégrer au résumé. Si les liens ne sont pas importants ou qu'il n'y en a pas, le résumé ne doit pas contenir d'information à ce sujet ni même le mentionner. Voici la conversation:\n\n{messages}\n\n. Résumé des emails ci-dessus en Markdown dans la langue '{language}' :",
"autolabels_query": "Date et heure actuelle: {date_time}\nEmail ou conversation d'emails: {messages}\nListe des labels existants: {labels}\nA partir de ce mail ou de cette conversation d'emails, tu dois regarder pour chaque label s'il est pertinent de l'assigner à ce mail ou cette conversation.\nTu ne dois assigner un label que s'il est réellement pertinent!\nLis bien la description de chaque label pour bien respecter les critères renseignés par l'utilisateur.\nSi aucun label n'est pertinent, tu ne dois en assigner aucun.\n\nTa réponse doit être au format suivant:\n[\"LABEL1\", \"LABEL2\", ...]\nSANS aucun texte supplémentaire du style \"Voici la liste des labels pertinents:\".\nSi aucun label n'est pertinent, tu dois renvoyer une liste vide: [] sans aucun autre commentaire !! Ta réponse doit dans tous les cas être uniquement une liste (éventuellement vide)"
"autolabels_query": "Date et heure actuelle: {date_time}\nEmail ou conversation d'emails: {messages}\nListe des labels existants: {labels}\nA partir de ce mail ou de cette conversation d'emails, tu dois regarder pour chaque label s'il est pertinent de l'assigner à ce mail ou cette conversation.\nTu ne dois assigner un label que s'il est réellement pertinent!\nLis bien la description de chaque label pour bien respecter les critères renseignés par l'utilisateur.\nSi aucun label n'est pertinent, tu ne dois en assigner aucun.\n\nTa réponse doit être au format suivant:\n[\"LABEL1\", \"LABEL2\", ...]\nSANS aucun texte supplémentaire du style \"Voici la liste des labels pertinents:\".\nSi aucun label n'est pertinent, tu dois renvoyer une liste vide: [] sans aucun autre commentaire !! Ta réponse doit dans tous les cas être uniquement une liste (éventuellement vide)",
"new_message_generation_query": "Tu es un assistant intégré dans la zone d'écriture des mails dans une boîte mail. LA REPONSE ATTENDUE EST UN CORPS DE MAIL dans la langue {language}. Ta réponse doit être UNIQUEMENT le corps du message (ne mentionne pas l'objet ni les destinataires) NE mets PAS de guillemets autour de ta réponse. Si ta réponse contient des champs à remplir par l'utilisateur, METS LES TOUS EN GRAS MARKDOWN ET ENTRE CROCHETS pour que l'utilisateur le remarque. (ex : **[Votre nom]**, **[Date]**, **[Signature]**, **[Nombre d'utilisateurs]**). Voici un exemple de génération : 'Prompt de l'utilisateur : 'Demande une clé API pour Mistral'. Ta réponse : 'Bonjour,\nPourriez-vous me fournir une clé API pour Mistral ?\nBien à vous,\n[Signature]' Voici le brouillon de mail actuel :\n\n{draft}\n\n. Voici le prompt de l'utilisateur : {user_prompt}\nSi le brouillon ne contient pas de signature et uniquement dans ce cas, alors signe avec {name}. NE CHANGE JAMAIS RIEN D'AUTRE PAR RAPPORT AU BROUILLON QUE CE QUE L'UTILISATEUR TE DEMANDE !!!",
"thread_response_generation_query": "Tu es un assistant intégré dans la zone d'écriture des mails dans une boîte mail. LA REPONSE ATTENDUE EST UN CORPS DE MAIL dans la langue {language}. Ta réponse doit être UNIQUEMENT le corps du message (ne mentionne pas l'objet ni les destinataires) NE mets PAS de guillemets autour de ta réponse. Si ta réponse contient des champs à remplir par l'utilisateur, METS LES TOUS EN GRAS MARKDOWN ET ENTRE CROCHETS pour que l'utilisateur le remarque. (ex : **[Votre nom]**, **[Date]**, **[Signature]**, **[Nombre d'utilisateurs]**). Voici un exemple de génération : 'Prompt de l'utilisateur : 'Demande une clé API pour Mistral'. Messages précédents: 'Bonjour Thomas,\nJe vous conseille d'utiliser Mistral pour l'IA.\nBien à vous,\nNicolas' Prompt de l'utilisateur : 'demande la clé'. Ta réponse : Bonjour Nicolas,\nMerci pour votre réponse, pourriez-vous me fournir une clé API pour Mistral ?\nBien à vous,\nThomas' Ta réponse : 'Bonjour,\nPourriez-vous me fournir une clé API pour Mistral ?\nBien à vous,\n[Signature]' Voici le brouillon de mail actuel :\n\n{draft}\n\n. Voici les anciens messages du thread : \n\n{messages}\n\n. Voici le prompt de l'utilisateur : {user_prompt}\nSi le brouillon ne contient pas de signature et uniquement dans ce cas, alors signe avec {name}. NE CHANGE JAMAIS RIEN D'AUTRE PAR RAPPORT AU BROUILLON QUE CE QUE L'UTILISATEUR TE DEMANDE !!!"
}
}
}
51 changes: 51 additions & 0 deletions src/backend/core/ai/message_generator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
from django.conf import settings
from django.utils import translation

from core.ai.utils import get_messages_from_thread, load_ai_prompts
from core.models import Thread
from core.services.ai_service import AIService


def generate_new_message(draft: str, user_prompt: str, name: str) -> str:
"""Generates a new mail using the AI model base on user prompt."""

# Determine the active or fallback language
active_language = translation.get_language() or settings.LANGUAGE_CODE

# Get the prompt for the active language
prompts = load_ai_prompts()
prompt_template = prompts.get(active_language)
prompt_query = prompt_template["new_message_generation_query"]
prompt = prompt_query.format(
draft=draft, language=active_language, user_prompt=user_prompt, name=name
)

answer = AIService().call_ai_api(prompt)

return answer


def generate_reply_message(draft: str, thread: Thread, user_prompt: str) -> str:
"""Generates a reply message using the AI model based on the thread context and user prompt."""

# Determine the active or fallback language
active_language = translation.get_language() or settings.LANGUAGE_CODE

# Extract messages from the thread
messages = get_messages_from_thread(thread)
messages_as_text = "\n\n".join([message.get_as_text() for message in messages])

# Get the prompt for the active language
prompts = load_ai_prompts()
prompt_template = prompts.get(active_language)
prompt_query = prompt_template["reply_message_generation_query"]
Copy link

Copilot AI Aug 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code references 'reply_message_generation_query' but the JSON file defines 'thread_response_generation_query'. This will cause a KeyError at runtime when trying to generate reply messages.

Suggested change
prompt_query = prompt_template["reply_message_generation_query"]
prompt_query = prompt_template["thread_response_generation_query"]

Copilot uses AI. Check for mistakes.
prompt = prompt_query.format(
draft=draft,
messages=messages_as_text,
language=active_language,
user_prompt=user_prompt,
)

answer = AIService().call_ai_api(prompt)

return answer
14 changes: 3 additions & 11 deletions src/backend/core/ai/thread_summarizer.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import json
from pathlib import Path

from django.conf import settings
from django.utils import translation

from core.ai.utils import get_messages_from_thread
from core.ai.utils import get_active_language, get_messages_from_thread, load_ai_prompts
from core.models import Thread
from core.services.ai_service import AIService

Expand All @@ -13,18 +9,14 @@ def summarize_thread(thread: Thread) -> str:
"""Summarizes a thread using the OpenAI client based on the active Django language."""

# Determine the active or fallback language
active_language = translation.get_language() or settings.LANGUAGE_CODE
active_language = get_active_language()

# Extract messages from the thread
messages = get_messages_from_thread(thread)
messages_as_text = "\n\n".join([message.get_as_text() for message in messages])

# Load prompt templates from ai_prompts.json
prompts_path = Path(__file__).parent / "ai_prompts.json"
with open(prompts_path, encoding="utf-8") as f:
prompts = json.load(f)

# Get the prompt for the active language
prompts = load_ai_prompts()
prompt_template = prompts.get(active_language)
prompt_query = prompt_template["summary_query"]
prompt = prompt_query.format(messages=messages_as_text, language=active_language)
Expand Down
26 changes: 25 additions & 1 deletion src/backend/core/ai/utils.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,25 @@
import json
from pathlib import Path
from typing import List

from django.conf import settings
from django.utils import translation

from core.models import Message, Thread


def get_active_language() -> str:
"""Get the active language or fallback to the default language code."""
return translation.get_language() or settings.LANGUAGE_CODE


def load_ai_prompts() -> dict:
"""Load AI prompts from the ai_prompts.json file."""
prompts_path = Path(__file__).parent / "ai_prompts.json"
with open(prompts_path, encoding="utf-8") as f:
return json.load(f)


def get_messages_from_thread(thread: Thread) -> List[Message]:
"""
Extract messages from a thread and return them as a list of text representations using Message.get_as_text().
Expand Down Expand Up @@ -32,7 +47,7 @@ def is_ai_enabled() -> bool:

def is_ai_summary_enabled() -> bool:
"""
Check if AI summary features are enabled.
Check if AI summary feature is enabled.
This is determined by the presence of the AI settings and if AI_FEATURE_SUMMARY_ENABLED is set to 1.
"""
return all(
Expand All @@ -48,3 +63,12 @@ def is_auto_labels_enabled() -> bool:
return all(
[is_ai_enabled(), getattr(settings, "AI_FEATURE_AUTOLABELS_ENABLED", False)]
)

def is_ai_messages_generation_enabled() -> bool:
"""
Check if AI messages generation feature is enabled.
This is determined by the presence of the AI settings and if AI_FEATURE_MESSAGES_GENERATION_ENABLED is set to 1.
"""
return all(
[is_ai_enabled(), getattr(settings, "AI_FEATURE_MESSAGES_GENERATION_ENABLED", False)]
)
Loading
Loading