Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ddb8387c03 | |||
| 36b2156c7b |
@@ -15,7 +15,7 @@ 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"
|
||||
|
||||
@@ -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):
|
||||
@@ -355,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(
|
||||
|
||||
@@ -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 qu’aucun 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, c’est-à-dire les thèmes précis et concrets correspondant aux grands axes réellement discutés durant la réunion.
|
||||
- N’inclus 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], ...]).
|
||||
L’introduction, l’ordre du jour, la conclusion, etc., seront ajoutés ultérieurement.
|
||||
Si aucun sujet clair n’est 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 d’un compte rendu de réunion.
|
||||
Tu utiliseras un style synthétique, administratif, à la troisième personne, sans affect.
|
||||
|
||||
En entrée, tu recevras :
|
||||
- L’inté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 d’informations 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 l’objet de décisions ou d’actions. 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user