Compare commits

...

8 Commits

Author SHA1 Message Date
Martin Guitteny ddb8387c03 ️(summary) change summary pipeline for improvements
Add number of subject approximation based on transcript
length.
Add TLDR generation with already generated summary
Change generation of parts in one prompt
2025-10-13 18:38:37 +02:00
Martin Guitteny 36b2156c7b ️(summary) change formating from prompt to response_format
Add ability to use response_format in call function in order to
have better result with albert-large model
Use reponse_format for next steps and plan generation
2025-10-13 12:07:54 +02:00
lebaudantoine ec94d613fa 🔖(minor) bump release to 0.1.40
- enhance technical documentation
- introduce external-api and service account
- fix inverted keyboard shortcuts
- allow configuring whisperX language (still wip)
- filter livekit event when sharing a single livekit instance
2025-10-12 17:13:09 +02:00
lebaudantoine 70d9d55227 🔖(helm) release chart 0.0.13
This chart exposes an external API from the backend pod.
Currently, it does not include conditional addition of the external API route.
This functionality will be added later.
2025-10-12 17:05:58 +02:00
lebaudantoine 5c74ace0d8 🐛(backend) filter LiveKit events by room name regex to exclude Tchap
Add configurable room name regex filtering to exclude Tchap events from shared
LiveKit server webhooks, preventing backend spam from unrelated application
events while maintaining UUID-based room processing for visio.

Those unrelated application events are spamming the sentry.

Acknowledges this is a pragmatic solution trading proper namespace
prefixing for immediate spam reduction with minimal refactoring impact
leaving prefix-based approach for future improvement.
2025-10-12 16:57:44 +02:00
lebaudantoine f0939b6f7c 🐛(summary) scope metadata manager signals to transcription tasks only
Restrict metadata manager signal triggers to transcription-specific Celery
tasks to prevent exceptions when new summary worker executes tasks
not designed for metadata operations, reducing false-positive Sentry errors.
2025-10-10 14:00:00 +02:00
lebaudantoine aecc48f928 🔧(summary) add configurable language settings for WhisperX transcription
Make WhisperX language detection configurable through FastAPI settings
to handle empty audio start scenarios where automatic detection fails and
incorrectly defaults to English despite 99% French usage.

Quick fix acknowledging long-term solution should allow dynamic
per-recording language selection configured by users through web
interface rather than global server settings.
2025-10-10 13:55:53 +02:00
lebaudantoine 4353db4a5f 🐛(frontend) fix inverted keyboard shortcuts for video and microphone
Correct accidentally swapped keyboard shortcuts between video and
microphone toggle controls introduced during device component
refactoring, restoring expected shortcut behavior reported by users.
2025-10-09 22:44:14 +02:00
19 changed files with 352 additions and 70 deletions
+2 -1
View File
@@ -11,10 +11,11 @@ AWS_S3_SECRET_ACCESS_KEY="password"
WHISPERX_BASE_URL="https://configure-your-url.com"
WHISPERX_ASR_MODEL="large-v2"
WHISPERX_API_KEY="your-secret-key"
WHISPERX_DEFAULT_LANGUAGE="fr"
LLM_BASE_URL="https://configure-your-url.com"
LLM_API_KEY="dev-apikey"
LLM_MODEL="Qwen/Qwen2.5-Coder-32B-Instruct-AWQ"
LLM_MODEL="albert-large"
WEBHOOK_API_TOKEN="secret"
WEBHOOK_URL="https://configure-your-url.com"
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "agents"
version = "0.1.39"
version = "0.1.40"
requires-python = ">=3.12"
dependencies = [
"livekit-agents==1.2.6",
@@ -2,6 +2,7 @@
# pylint: disable=no-member
import re
import uuid
from enum import Enum
from logging import getLogger
@@ -92,6 +93,17 @@ class LiveKitEventsService:
self.telephony_service = TelephonyService()
self.recording_events = RecordingEventsService()
self._filter_regex = None
if settings.LIVEKIT_WEBHOOK_EVENTS_FILTER_REGEX:
try:
self._filter_regex = re.compile(
settings.LIVEKIT_WEBHOOK_EVENTS_FILTER_REGEX
)
except re.error:
logger.exception(
"Invalid LIVEKIT_WEBHOOK_EVENTS_FILTER_REGEX. Webhook filtering disabled."
)
def receive(self, request):
"""Process webhook and route to appropriate handler."""
@@ -106,6 +118,10 @@ class LiveKitEventsService:
except Exception as e:
raise InvalidPayloadError("Invalid webhook payload") from e
if self._filter_regex and not self._filter_regex.search(data.room.name):
logger.info("Filtered webhook event for room '%s'", data.room.name)
return
try:
webhook_type = LiveKitWebhookEventType(data.event)
except ValueError as e:
@@ -343,3 +343,99 @@ def test_receive_unsupported_event(mock_receive, service):
UnsupportedEventTypeError, match="Unknown webhook type: unsupported_event"
):
service.receive(mock_request)
@mock.patch.object(api.WebhookReceiver, "receive")
@mock.patch.object(LiveKitEventsService, "_handle_room_started")
def test_receive_no_filter_processes_all_events(
mock_handle_room_started, mock_receive, mock_livekit_config, settings
):
"""Should process all events when filter regex is not configured."""
settings.LIVEKIT_WEBHOOK_EVENTS_FILTER_REGEX = None
mock_request = mock.MagicMock()
mock_request.headers = {"Authorization": "test_token"}
mock_request.body = b"{}"
mock_data = mock.MagicMock()
mock_data.room.name = "!JIfCxVLcKKkWrmVBOb:your-domain.com"
mock_data.event = "room_started"
mock_receive.return_value = mock_data
service = LiveKitEventsService()
service.receive(mock_request)
mock_handle_room_started.assert_called_once()
@mock.patch.object(api.WebhookReceiver, "receive")
@mock.patch.object(LiveKitEventsService, "_handle_room_started")
def test_receive_invalid_filter_regex_processes_all_events(
mock_handle_room_started, mock_receive, mock_livekit_config, settings
):
"""Should process all events when filter regex is invalid (fail-safe)."""
settings.LIVEKIT_WEBHOOK_EVENTS_FILTER_REGEX = "(abc"
mock_request = mock.MagicMock()
mock_request.headers = {"Authorization": "test_token"}
mock_request.body = b"{}"
mock_data = mock.MagicMock()
mock_data.room.name = "!JIfCxVLcKKkWrmVBOb:your-domain.com"
mock_data.event = "room_started"
mock_receive.return_value = mock_data
service = LiveKitEventsService()
service.receive(mock_request)
mock_handle_room_started.assert_called_once()
@mock.patch.object(api.WebhookReceiver, "receive")
@mock.patch.object(LiveKitEventsService, "_handle_room_started")
def test_receive_filter_drops_non_matching_events(
mock_handle_room_started, mock_receive, mock_livekit_config, settings
):
"""Should drop events when room name does not match filter regex."""
settings.LIVEKIT_WEBHOOK_EVENTS_FILTER_REGEX = (
r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"
)
mock_request = mock.MagicMock()
mock_request.headers = {"Authorization": "test_token"}
mock_request.body = b"{}"
mock_data = mock.MagicMock()
mock_data.room.name = "!JIfCxVLcKKkWrmVBOb:your-domain.com"
mock_data.event = "room_started"
mock_receive.return_value = mock_data
service = LiveKitEventsService()
service.receive(mock_request)
mock_handle_room_started.assert_not_called()
@mock.patch.object(api.WebhookReceiver, "receive")
@mock.patch.object(LiveKitEventsService, "_handle_room_started")
def test_receive_filter_processes_matching_events(
mock_handle_room_started, mock_receive, mock_livekit_config, settings
):
"""Should process events when room name matches filter regex."""
settings.LIVEKIT_WEBHOOK_EVENTS_FILTER_REGEX = (
r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"
)
mock_request = mock.MagicMock()
mock_request.headers = {"Authorization": "test_token"}
mock_request.body = b"{}"
mock_data = mock.MagicMock()
mock_data.room.name = str(uuid.uuid4())
mock_data.event = "room_started"
mock_receive.return_value = mock_data
service = LiveKitEventsService()
service.receive(mock_request)
mock_handle_room_started.assert_called_once()
+4
View File
@@ -517,6 +517,10 @@ class Base(Configuration):
LIVEKIT_VERIFY_SSL = values.BooleanValue(
True, environ_name="LIVEKIT_VERIFY_SSL", environ_prefix=None
)
# Regex to filter webhook events by room name. Only matching events are processed.
LIVEKIT_WEBHOOK_EVENTS_FILTER_REGEX = values.Value(
None, environ_name="LIVEKIT_WEBHOOK_EVENTS_FILTER_REGEX", environ_prefix=None
)
RESOURCE_DEFAULT_ACCESS_LEVEL = values.Value(
"public", environ_name="RESOURCE_DEFAULT_ACCESS_LEVEL", environ_prefix=None
)
+1 -1
View File
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "meet"
version = "0.1.39"
version = "0.1.40"
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
classifiers = [
"Development Status :: 5 - Production/Stable",
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "meet",
"version": "0.1.39",
"version": "0.1.40",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "meet",
"version": "0.1.39",
"version": "0.1.40",
"dependencies": {
"@livekit/components-react": "2.9.13",
"@livekit/components-styles": "1.1.6",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "meet",
"private": true,
"version": "0.1.39",
"version": "0.1.40",
"type": "module",
"scripts": {
"dev": "panda codegen && vite",
@@ -6,12 +6,12 @@ export const useDeviceShortcut = (kind: MediaDeviceKind) => {
switch (kind) {
case 'audioinput':
return {
key: 'e',
key: 'd',
ctrlKey: true,
}
case 'videoinput':
return {
key: 'd',
key: 'e',
ctrlKey: true,
}
default:
@@ -155,6 +155,7 @@ summary:
WHISPERX_API_KEY: your-secret-value
WHISPERX_BASE_URL: https://configure-your-url.com
WHISPERX_ASR_MODEL: large-v2
WHISPERX_DEFAULT_LANGUAGE: fr
LLM_BASE_URL: https://configure-your-url.com
LLM_API_KEY: your-secret-value
LLM_MODEL: meta-llama/Llama-3.1-8B-Instruct
@@ -191,6 +192,7 @@ celeryTranscribe:
WHISPERX_API_KEY: your-secret-value
WHISPERX_BASE_URL: https://configure-your-url.com
WHISPERX_ASR_MODEL: large-v2
WHISPERX_DEFAULT_LANGUAGE: fr
LLM_BASE_URL: https://configure-your-url.com
LLM_API_KEY: your-secret-value
LLM_MODEL: meta-llama/Llama-3.1-8B-Instruct
@@ -228,6 +230,7 @@ celerySummarize:
WHISPERX_API_KEY: your-secret-value
WHISPERX_BASE_URL: https://configure-your-url.com
WHISPERX_ASR_MODEL: large-v2
WHISPERX_DEFAULT_LANGUAGE: fr
LLM_BASE_URL: https://configure-your-url.com
LLM_API_KEY: your-secret-value
LLM_MODEL: meta-llama/Llama-3.1-8B-Instruct
+1 -1
View File
@@ -1,4 +1,4 @@
apiVersion: v2
type: application
name: meet
version: 0.0.13-beta.1
version: 0.0.13
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "mail_mjml",
"version": "0.1.39",
"version": "0.1.40",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "mail_mjml",
"version": "0.1.39",
"version": "0.1.40",
"license": "MIT",
"dependencies": {
"@html-to/text-cli": "0.5.4",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "mail_mjml",
"version": "0.1.39",
"version": "0.1.40",
"description": "An util to generate html and text django's templates from mjml templates",
"type": "module",
"dependencies": {
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "sdk",
"version": "0.1.39",
"version": "0.1.40",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "sdk",
"version": "0.1.39",
"version": "0.1.40",
"license": "ISC",
"workspaces": [
"./library",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "sdk",
"version": "0.1.39",
"version": "0.1.40",
"author": "",
"license": "ISC",
"description": "",
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "summary"
version = "0.1.39"
version = "0.1.40"
dependencies = [
"fastapi[standard]>=0.105.0",
"uvicorn>=0.24.0",
+99 -40
View File
@@ -7,7 +7,8 @@ import os
import tempfile
import time
from pathlib import Path
from typing import Optional
from typing import Any, Mapping, Optional
import math
import openai
import sentry_sdk
@@ -22,6 +23,8 @@ from urllib3.util import Retry
from summary.core.analytics import MetadataManager, get_analytics
from summary.core.config import get_settings
from summary.core.prompt import (
FORMAT_NEXT_STEPS,
FORMAT_PLAN,
PROMPT_SYSTEM_CLEANING,
PROMPT_SYSTEM_NEXT_STEP,
PROMPT_SYSTEM_PART,
@@ -101,6 +104,12 @@ def create_retry_session():
session.mount("https://", HTTPAdapter(max_retries=retries))
return session
def add_line_numbers(transcript: str):
"""Add line numbers to a text file."""
lines = transcript.splitlines()
lines = [line for line in lines if line.strip()]
numbered_lines = [f"{i + 1}: {line}" for i, line in enumerate(lines)]
return "\n".join(numbered_lines)
class LLMException(Exception):
"""LLM call failed."""
@@ -115,24 +124,53 @@ class LLMService:
base_url=settings.llm_base_url, api_key=settings.llm_api_key
)
def call(self, system_prompt: str, user_prompt: str):
def call(
self,
system_prompt: str,
user_prompt: str,
response_format: Optional[Mapping[str, Any]] = None,
):
"""Call the LLM service.
Takes a system prompt and a user prompt, and returns the LLM's response
Returns None if the call fails.
"""
try:
response = self._client.chat.completions.create(
model=settings.llm_model,
messages=[
params: dict[str, Any] = {
"model": settings.llm_model,
"messages": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt},
],
)
}
if response_format is not None:
params["response_format"] = response_format
response = self._client.chat.completions.create(**params)
return response.choices[0].message.content
except Exception as e:
logger.error("LLM call failed: %s", e)
raise LLMException("LLM call failed.") from e
logger.exception("LLM call failed: %s", e)
raise LLMException("LLM call failed: {e}") from e
def format_actions(llm_output: dict) -> str:
"""Format the actions from the LLM output into a markdown list.
fomat:
- [ ] Action title Assignée à : assignee1, assignee2, Échéance : due_date
"""
lines = []
for action in llm_output.get("actions", []):
title = action.get("title", "").strip()
assignees = ", ".join(action.get("assignees", [])) or "-"
due_date = action.get("due_date") or "-"
line = f"- [ ] {title} Assignée à : {assignees}, Échéance : {due_date}"
lines.append(line)
if lines:
return "### Prochaines étapes\n\n" + "\n".join(lines)
return ""
def format_segments(transcription_data):
@@ -177,25 +215,6 @@ def post_with_retries(url, data):
session.close()
@signals.task_prerun.connect
def task_started(task_id=None, task=None, args=None, **kwargs):
"""Signal handler called before task execution begins."""
task_args = args or []
metadata_manager.create(task_id, task_args)
@signals.task_retry.connect
def task_retry_handler(request=None, reason=None, einfo=None, **kwargs):
"""Signal handler called when task execution retries."""
metadata_manager.retry(request.id)
@signals.task_failure.connect
def task_failure_handler(task_id, exception=None, **kwargs):
"""Signal handler called when task execution fails permanently."""
metadata_manager.capture(task_id, settings.posthog_event_failure)
@celery.task(
bind=True,
autoretry_for=[exceptions.HTTPError],
@@ -270,7 +289,9 @@ def process_audio_transcribe_summarize_v2(
transcription_start_time = time.time()
with open(temp_file_path, "rb") as audio_file:
transcription = whisperx_client.audio.transcriptions.create(
model=settings.whisperx_asr_model, file=audio_file
model=settings.whisperx_asr_model,
file=audio_file,
language=settings.whisperx_default_language,
)
metadata_manager.track(
task_id,
@@ -333,6 +354,25 @@ def process_audio_transcribe_summarize_v2(
logger.info("Summary generation not enabled for this user.")
@signals.task_prerun.connect(sender=process_audio_transcribe_summarize_v2)
def task_started(task_id=None, task=None, args=None, **kwargs):
"""Signal handler called before task execution begins."""
task_args = args or []
metadata_manager.create(task_id, task_args)
@signals.task_retry.connect(sender=process_audio_transcribe_summarize_v2)
def task_retry_handler(request=None, reason=None, einfo=None, **kwargs):
"""Signal handler called when task execution retries."""
metadata_manager.retry(request.id)
@signals.task_failure.connect(sender=process_audio_transcribe_summarize_v2)
def task_failure_handler(task_id, exception=None, **kwargs):
"""Signal handler called when task execution fails permanently."""
metadata_manager.capture(task_id, settings.posthog_event_failure)
@celery.task(
bind=True,
autoretry_for=[LLMException, Exception],
@@ -353,34 +393,53 @@ def summarize_transcription(self, transcript: str, email: str, sub: str, title:
llm_service = LLMService()
tldr = llm_service.call(PROMPT_SYSTEM_TLDR, transcript)
token_estimate = len(transcript) // 3.6
t = round(0.5 + 2.3 * math.log1p((token_estimate / 3800) ** 2))
nb_parts = max(1, t)
logger.info("TLDR generated")
parts = llm_service.call(PROMPT_SYSTEM_PLAN, transcript)
transcript = add_line_numbers(transcript)
logger.info("Transcription with line numbers: \n%s", transcript)
parts = llm_service.call(
PROMPT_SYSTEM_PLAN.format(nb_parts_min=nb_parts-2, nb_parts_max=nb_parts+2), "Transcript : \n" + transcript, response_format=FORMAT_PLAN
)
logger.info("Plan generated")
parts = parts.split("\n")
parts = [x for x in parts if x.strip() != ""]
logger.info("Empty parts removed")
res = json.loads(parts)
parts = res.get("parts", [])
parts_summarized = []
logger.info("Parts to summarize: %s", parts)
parts_titles = ""
for part in parts:
prompt_user_part = PROMPT_USER_PART.format(part=part, transcript=transcript)
logger.info("Summarizing part: %s", part)
parts_summarized.append(llm_service.call(PROMPT_SYSTEM_PART, prompt_user_part))
parts_titles += f"- {part['title']}, lignes {part['plages_lignes']}\n"
raw_summary = llm_service.call(
PROMPT_SYSTEM_PART,
f"Plan:\n{parts_titles}\n\nTranscript:\n{transcript}",
)
logger.info("Parts summarized")
raw_summary = "\n\n".join(parts_summarized)
next_steps = llm_service.call(
PROMPT_SYSTEM_NEXT_STEP, transcript, response_format=FORMAT_NEXT_STEPS
)
next_steps = format_actions(json.loads(next_steps))
next_steps = llm_service.call(PROMPT_SYSTEM_NEXT_STEP, transcript)
logger.info("Next steps generated")
cleaned_summary = llm_service.call(PROMPT_SYSTEM_CLEANING, raw_summary)
logger.info("Summary cleaned")
summary = tldr + "\n\n" + cleaned_summary + "\n\n" + next_steps
tldr = llm_service.call(
PROMPT_SYSTEM_TLDR,
"transcript :\n" + transcript + "\n\nsummary :\n" + cleaned_summary,
)
summary = tldr + "\n\n" + raw_summary + "\n\n" + next_steps
data = {
"title": settings.summary_title_template.format(
+2
View File
@@ -39,6 +39,8 @@ class Settings(BaseSettings):
whisperx_base_url: str = "https://api.openai.com/v1"
whisperx_asr_model: str = "whisper-1"
whisperx_max_retries: int = 0
# ISO 639-1 language code (e.g., "en", "fr", "es")
whisperx_default_language: Optional[str] = None
llm_base_url: str
llm_api_key: str
llm_model: str
+115 -14
View File
@@ -4,18 +4,50 @@ PROMPT_SYSTEM_TLDR = """Tu es un agent dont le rôle est de créer un TL;DR (ré
### Résumé TL;DR
[Résumé concis et structuré]"""
PROMPT_SYSTEM_PLAN = """Ta tâche est de diviser le contenu du transcript en sujets concrets correspondant aux grands axes discutés durant la réunion. Ne crée pas de catégories génériques. Les titres doivent être courts, précis et représentatifs des échanges. Veille à ce que chaque sujet soit distinct et quaucun thème ne soit répété. Tu te limiteras à 5 ou 6 sujets maximum.
L'introduction, ordre du jour, conclusion, etc. seront rajoutés a posteriori. Tu répondras dans le format suivant sans rien ajouter d'autre:
"Titre du sujet 1
Titre du sujet 2
Titre du sujet 3
..."
"""
PROMPT_SYSTEM_PLAN = """Ta tâche consiste à identifier les sujets abordés dans un transcript, cest-à-dire les thèmes précis et concrets correspondant aux grands axes réellement discutés durant la réunion.
- Ninclus pas de catégories génériques.
- Les titres doivent être courts, précis et représentatifs des échanges.
- Chaque sujet doit être distinct, sans répétition de thèmes.
- Tu fera entre {nb_parts_min} et {nb_parts_max} sujets.
Pour chaque sujet identifié :
- Donne le titre du sujet
- Indique la ou les plages de lignes (numérotées) du transcript correspondant à ce sujet (une ou plusieurs par sujet, si nécessaire : [[début, fin], ...]).
Lintroduction, lordre du jour, la conclusion, etc., seront ajoutés ultérieurement.
Si aucun sujet clair nest repéré, réponds exactement : "Général"."""
PROMPT_SYSTEM_PART = """Tu es un agent dont le rôle est de créer une partie du résumé d'un compte rendu de réunion. Tu utiliseras un style synthétique, administratif, à la troisième personne, sans affect. Tu recevras en entrée le transcript, et le titre du sujet correspondant. Ta tâche est de rédiger un résumé concis de cette partie et uniquement cette partie, en te concentrant uniquement sur les informations essentielles et pertinentes. Le résumé de chaque partie doit tenir en 4 à 6 phrases maximum, sans entrer dans les détails mineurs. Tu répondras dans le format suivant :
### Titre du sujet [Traduire ce titre selon la langue du transcript]
[Résumé concis et structuré de la partie du transcript]
"""
PROMPT_SYSTEM_PART = """
Tu es un agent dont le rôle est de rédiger le résumé complet dun compte rendu de réunion.
Tu utiliseras un style synthétique, administratif, à la troisième personne, sans affect.
En entrée, tu recevras :
- Lintégralité du transcript de la réunion,
- Un plan indiquant pour chaque sujet : son titre et la plage de lignes correspondante.
- Les notes prises par un utilisateur durant la réunion contenant des points important à aborder dans le transcript.
Consignes :
- Générer un résumé pour chacune des parties définies dans le plan.
- Pour chaque partie :
- Résumer les lignes indiquées et en lien avec le sujet.,
- Utiliser exclusivement des bullet points clairs et concis,
- Ne pas dépasser 4 ou 5 bullet points par partie,
- Se limiter aux informations essentielles et pertinentes,
- Éviter tout détail secondaire ou interprétation.
- mettre en avant les points écrits dans les notes de l'utilisateur.
- mettre en gras les noms propres (personnes, entreprises, produits, etc.)
Format de sortie attendu : un enchaînement de résumés, un par sujet, par exemple :
### [Titre du sujet 1]
- [Point essentiel 1]
- [Point essentiel 2]
- [Point essentiel 3]
### [Titre du sujet 2]
- [Point essentiel 1]
- [Point essentiel 2]
(...)
"""
PROMPT_USER_PART = """Titre de la partie à résumer : {part}
Transcript complet :
@@ -23,6 +55,75 @@ Transcript complet :
PROMPT_SYSTEM_CLEANING = """Tu es un agent dont le rôle est de nettoyer un résumé de compte rendu de réunion. Tu recevras en entrée le résumé brut, potentiellement avec des erreurs de formatage, des incohérences ou des redondances. Ta tâche est de corriger les erreurs de formatage, d'améliorer la clarté et la cohérence du texte, et de t'assurer que le résumé est bien structuré et facile à lire. Ton but principal est de retirer les redondances et les répétitions. Assure la cohérence entre les titres et homogénéise le style d’écriture entre les parties. Supprime les doublons dinformations entre les parties si présents. Si certaines parties sont plus secondaires, tu peux les fusionner ou les réduire en 1 à 2 phrases. Mets en avant les points centraux qui ont fait lobjet de décisions ou dactions. Tu répondras uniquement avec le résumé sans rien ajouter d'autre"""
PROMPT_SYSTEM_NEXT_STEP = """Tu es un agent dont le rôle est d'extraire les prochaines étapes d'un transcript de réunion. Tu utiliseras un style synthétique, administratif, à la troisième personne, sans affect. Tu recevras en entrée le transcript. Ta tâche est d'identifier et de lister toutes les actions à entreprendre, en indiquant la ou les personnes assignées et en précisant les échéances si elles sont mentionnées. Ne retiens que les actions concrètes et à venir. Ignore les remarques générales ou les constats sans suite. Les actions doivent suivre ce format strict :
### Prochaines étapes
- [ ] [Action à effectuer] Assignée à : [Nom], Échéance : [Date si mentionnée]"""
PROMPT_SYSTEM_NEXT_STEP = """Tu es un agent dont le rôle est d'extraire les prochaines étapes d'un transcript de réunion. Tu utiliseras un style synthétique, administratif, à la troisième personne, sans affect. Tu recevras en entrée le transcript. Ta tâche est d'identifier et de lister toutes les actions à entreprendre, en indiquant la ou les personnes assignées et en précisant les échéances si elles sont mentionnées. Ne retiens que les actions concrètes et à venir. Ignore les remarques générales ou les constats sans suite."""
FORMAT_NEXT_STEPS = {
"type": "json_schema",
"json_schema": {
"name": "actions",
"schema": {
"type": "object",
"properties": {
"actions": {
"type": "array",
"items": {
"type": "object",
"properties": {
"title": {"type": "string"},
"assignees": {
"type": "array",
"items": {"type": "string"},
"description": "Noms des personnes assignées",
},
"due_date": {
"type": "string",
"description": "Date d'échéance si mentionnée (si l'année nest pas précisée, ne pas l'ajouter)",
},
},
"required": ["title", "assignees"],
"additionalProperties": False,
},
}
},
"required": ["actions"],
"additionalProperties": False,
},
"strict": True,
},
}
FORMAT_PLAN = {
"type": "json_schema",
"json_schema": {
"name": "PlanWithLineRanges",
"strict": True,
"schema": {
"type": "object",
"additionalProperties": False,
"required": ["parts"],
"properties": {
"parts": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": False,
"required": ["title", "plages_lignes"],
"properties": {
"title": { "type": "string" },
"plages_lignes": {
"type": "array",
"minItems": 1,
"items": {
"type": "array",
"items": { "type": "integer", "minimum": 1 },
"minItems": 2,
"maxItems": 2
}
}
}
}
}
}
}
}
}