Skip to content

Commit 5a832dc

Browse files
committed
✨(ai) add backend for message generation
1 parent 5311086 commit 5a832dc

20 files changed

+642
-15
lines changed

docs/env.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,8 @@ The application uses a new environment file structure with `.defaults` and `.loc
276276
| `AI_API_KEY` | None| API Key used for AI features | Optional |
277277
| `AI_MODEL` | None | Default model used for AI features | Optional |
278278
| `AI_FEATURE_SUMMARY_ENABLED` | `False` | Default enabled mode for summary AI features | Required |
279+
| `AI_FEATURE_MESSAGES_GENERATION_ENABLED` | `False` | Default enabled mode for message generation AI features | Required |
280+
279281

280282
### External Services
281283

env.d/development/backend.defaults

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ AI_API_KEY=
7979
AI_MODEL=
8080

8181
AI_FEATURE_SUMMARY_ENABLED=False
82+
AI_FEATURE_MESSAGES_GENERATION_ENABLED=False
8283

8384
# Interoperability
8485
# Drive - https://github.com/suitenumerique/drive
Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
{
22
"en-us": {
3-
"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}' :"
3+
"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}' :",
4+
"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!!!",
5+
"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!!!"
46
},
57
"fr-fr": {
6-
"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}' :"
8+
"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}' :",
9+
"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 !!!",
10+
"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 !!!"
711
}
812
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
from django.conf import settings
2+
from django.utils import translation
3+
4+
from core.ai.utils import get_messages_from_thread, load_ai_prompts
5+
from core.models import Thread
6+
from core.services.ai_service import AIService
7+
8+
9+
def generate_new_message(draft: str, user_prompt: str, name: str) -> str:
10+
"""Generates a new mail using the AI model base on user prompt."""
11+
12+
# Determine the active or fallback language
13+
active_language = translation.get_language() or settings.LANGUAGE_CODE
14+
15+
# Get the prompt for the active language
16+
prompts = load_ai_prompts()
17+
prompt_template = prompts.get(active_language)
18+
prompt_query = prompt_template["new_message_generation_query"]
19+
prompt = prompt_query.format(
20+
draft=draft, language=active_language, user_prompt=user_prompt, name=name
21+
)
22+
23+
answer = AIService().call_ai_api(prompt)
24+
25+
return answer
26+
27+
28+
def generate_reply_message(draft: str, thread: Thread, user_prompt: str) -> str:
29+
"""Generates a reply message using the AI model based on the thread context and user prompt."""
30+
31+
# Determine the active or fallback language
32+
active_language = translation.get_language() or settings.LANGUAGE_CODE
33+
34+
# Extract messages from the thread
35+
messages = get_messages_from_thread(thread)
36+
messages_as_text = "\n\n".join([message.get_as_text() for message in messages])
37+
38+
# Get the prompt for the active language
39+
prompts = load_ai_prompts()
40+
prompt_template = prompts.get(active_language)
41+
prompt_query = prompt_template["reply_message_generation_query"]
42+
prompt = prompt_query.format(
43+
draft=draft,
44+
messages=messages_as_text,
45+
language=active_language,
46+
user_prompt=user_prompt,
47+
)
48+
49+
answer = AIService().call_ai_api(prompt)
50+
51+
return answer

src/backend/core/ai/thread_summarizer.py

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
1-
import json
2-
from pathlib import Path
3-
4-
from django.conf import settings
51
from django.utils import translation
62

7-
from core.ai.utils import get_messages_from_thread
3+
from core.ai.utils import get_active_language, get_messages_from_thread, load_ai_prompts
84
from core.models import Thread
95
from core.services.ai_service import AIService
106

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

1511
# Determine the active or fallback language
16-
active_language = translation.get_language() or settings.LANGUAGE_CODE
12+
active_language = get_active_language()
1713

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

22-
# Load prompt templates from ai_prompts.json
23-
prompts_path = Path(__file__).parent / "ai_prompts.json"
24-
with open(prompts_path, encoding="utf-8") as f:
25-
prompts = json.load(f)
26-
2718
# Get the prompt for the active language
19+
prompts = load_ai_prompts()
2820
prompt_template = prompts.get(active_language)
2921
prompt_query = prompt_template["summary_query"]
3022
prompt = prompt_query.format(messages=messages_as_text, language=active_language)

src/backend/core/ai/utils.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,25 @@
1+
import json
2+
from pathlib import Path
13
from typing import List
24

35
from django.conf import settings
6+
from django.utils import translation
47

58
from core.models import Message, Thread
69

710

11+
def get_active_language() -> str:
12+
"""Get the active language or fallback to the default language code."""
13+
return translation.get_language() or settings.LANGUAGE_CODE
14+
15+
16+
def load_ai_prompts() -> dict:
17+
"""Load AI prompts from the ai_prompts.json file."""
18+
prompts_path = Path(__file__).parent / "ai_prompts.json"
19+
with open(prompts_path, encoding="utf-8") as f:
20+
return json.load(f)
21+
22+
823
def get_messages_from_thread(thread: Thread) -> List[Message]:
924
"""
1025
Extract messages from a thread and return them as a list of text representations using Message.get_as_text().
@@ -32,9 +47,19 @@ def is_ai_enabled() -> bool:
3247

3348
def is_ai_summary_enabled() -> bool:
3449
"""
35-
Check if AI summary features are enabled.
50+
Check if AI summary feature is enabled.
3651
This is determined by the presence of the AI settings and if AI_FEATURE_SUMMARY_ENABLED is set to 1.
3752
"""
3853
return all(
3954
[is_ai_enabled(), getattr(settings, "AI_FEATURE_SUMMARY_ENABLED", False)]
4055
)
56+
57+
58+
def is_ai_messages_generation_enabled() -> bool:
59+
"""
60+
Check if AI messages generation feature is enabled.
61+
This is determined by the presence of the AI settings and if AI_FEATURE_MESSAGES_GENERATION_ENABLED is set to 1.
62+
"""
63+
return all(
64+
[is_ai_enabled(), getattr(settings, "AI_FEATURE_MESSAGES_GENERATION_ENABLED", False)]
65+
)

0 commit comments

Comments
 (0)