Files
linear-coding-agent/utils/generate_chunk_summaries.py
David Blanc Brioir 636ad6206c feat: Add vectorized summary field and migration tools
- Add 'summary' field to Chunk collection (vectorized with text2vec)
- Migrate from Dynamic index to HNSW + RQ for both Chunk and Summary
- Add LLM summarizer module (utils/llm_summarizer.py)
- Add migration scripts (migrate_add_summary.py, restore_*.py)
- Add summary generation utilities and progress tracking
- Add testing and cleaning tools (outils_test_and_cleaning/)
- Add comprehensive documentation (ANALYSE_*.md, guides)
- Remove obsolete files (linear_config.py, old test files)
- Update .gitignore to exclude backups and temp files

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-07 22:56:03 +01:00

312 lines
9.5 KiB
Python

"""Script de génération de résumés pour les chunks existants.
Ce script:
1. Liste les œuvres disponibles dans Weaviate
2. Permet à l'utilisateur de sélectionner une œuvre
3. Récupère tous les chunks de cette œuvre
4. Génère un résumé pour chaque chunk avec Ollama (qwen2.5:7b)
5. Met à jour les chunks dans Weaviate avec les résumés générés
Usage:
python utils/generate_chunk_summaries.py
"""
import json
import logging
import sys
import time
from pathlib import Path
from typing import Any, Dict, List
import ollama
import weaviate
from tqdm import tqdm
# Configuration logging
logging.basicConfig(
level=logging.INFO,
format="[%(asctime)s] %(levelname)s - %(message)s",
handlers=[
logging.StreamHandler(sys.stdout),
logging.FileHandler("chunk_summaries.log", encoding="utf-8")
]
)
logger = logging.getLogger(__name__)
# Configuration Ollama
OLLAMA_MODEL = "qwen2.5:7b"
BATCH_SIZE = 50 # Nombre de chunks à traiter par batch
MAX_RETRIES = 3 # Nombre de tentatives en cas d'erreur
def get_available_works(client: weaviate.WeaviateClient) -> List[Dict[str, Any]]:
"""Récupère la liste des œuvres disponibles.
Args:
client: Client Weaviate connecté.
Returns:
Liste des œuvres avec titre, auteur et nombre de chunks.
"""
logger.info("Récupération des œuvres disponibles...")
works_collection = client.collections.get("Work")
works = works_collection.query.fetch_objects(limit=100)
# Pour chaque œuvre, compter les chunks
works_with_counts = []
for work in works.objects:
title = work.properties.get("title", "Sans titre")
author = work.properties.get("author", "Auteur inconnu")
# Compter les chunks de cette œuvre
chunk_collection = client.collections.get("Chunk")
result = chunk_collection.aggregate.over_all(
filters=weaviate.classes.query.Filter.by_property("work").by_property("title").equal(title)
)
chunks_count = result.total_count
works_with_counts.append({
"title": title,
"author": author,
"chunks_count": chunks_count
})
return works_with_counts
def select_work(works: List[Dict[str, Any]]) -> Dict[str, Any]:
"""Affiche les œuvres et demande à l'utilisateur de choisir.
Args:
works: Liste des œuvres disponibles.
Returns:
L'œuvre sélectionnée.
"""
print("\n" + "=" * 80)
print("ŒUVRES DISPONIBLES")
print("=" * 80)
for i, work in enumerate(works, 1):
print(f"{i}. {work['title']} - {work['author']} ({work['chunks_count']} chunks)")
print("=" * 80)
while True:
try:
choice = int(input(f"\nChoisissez une œuvre (1-{len(works)}): "))
if 1 <= choice <= len(works):
return works[choice - 1]
else:
print(f"Veuillez entrer un nombre entre 1 et {len(works)}")
except ValueError:
print("Veuillez entrer un nombre valide")
def generate_summary(chunk_text: str, work_title: str, author: str) -> str:
"""Génère un résumé dense pour un chunk avec Ollama.
Args:
chunk_text: Texte du chunk à résumer.
work_title: Titre de l'œuvre (non utilisé dans le prompt).
author: Auteur de l'œuvre (non utilisé dans le prompt).
Returns:
Résumé dense généré (100-150 mots).
"""
prompt = f"""TEXTE À RÉSUMER:
{chunk_text}
CONSIGNES STRICTES:
- Résumé direct de 100-150 mots maximum
- INTERDIT: formulations méta ("Ce passage souligne...", "L'auteur affirme...", "Peirce dit que...")
- Reformule les idées au style direct et impersonnel
- Densité conceptuelle maximale: chaque mot compte
- Conserve TOUS les concepts, termes techniques et noms propres
- Structure: thèse → arguments → implications
- Aucune perte d'information philosophique
RÉSUMÉ DENSE:"""
response = ollama.generate(
model=OLLAMA_MODEL,
prompt=prompt,
options={
"temperature": 0.2, # Très peu de créativité pour rester fidèle au texte
"num_predict": 250, # ~150 mots maximum
}
)
return response["response"].strip()
def process_work_chunks(
client: weaviate.WeaviateClient,
work: Dict[str, Any]
) -> None:
"""Traite tous les chunks d'une œuvre pour générer des résumés.
Args:
client: Client Weaviate connecté.
work: Œuvre sélectionnée.
"""
title = work["title"]
author = work["author"]
total_chunks = work["chunks_count"]
logger.info(f"Traitement de '{title}' - {total_chunks} chunks")
# Récupérer tous les chunks de l'œuvre
chunk_collection = client.collections.get("Chunk")
# Pagination pour récupérer tous les chunks
all_chunks = []
cursor = None
fetch_batch_size = 500
logger.info("Récupération des chunks...")
while True:
if cursor:
response = chunk_collection.query.fetch_objects(
limit=fetch_batch_size,
after=cursor,
filters=weaviate.classes.query.Filter.by_property("work").by_property("title").equal(title)
)
else:
response = chunk_collection.query.fetch_objects(
limit=fetch_batch_size,
filters=weaviate.classes.query.Filter.by_property("work").by_property("title").equal(title)
)
if not response.objects:
break
for obj in response.objects:
all_chunks.append({
"uuid": str(obj.uuid),
"text": obj.properties.get("text", ""),
"summary": obj.properties.get("summary", "")
})
if len(response.objects) < fetch_batch_size:
break
cursor = response.objects[-1].uuid
logger.info(f"{len(all_chunks)} chunks récupérés")
# Filtrer les chunks sans résumé
chunks_to_process = [c for c in all_chunks if not c["summary"]]
if not chunks_to_process:
logger.info("✓ Tous les chunks ont déjà un résumé !")
return
logger.info(f"{len(chunks_to_process)} chunks à traiter (résumés manquants)")
# Générer les résumés avec barre de progression
print("\nGénération des résumés...")
successful = 0
failed = 0
for chunk in tqdm(chunks_to_process, desc="Chunks"):
retry_count = 0
while retry_count < MAX_RETRIES:
try:
# Générer le résumé
summary = generate_summary(chunk["text"], title, author)
# Mettre à jour le chunk dans Weaviate
chunk_collection.data.update(
uuid=chunk["uuid"],
properties={"summary": summary}
)
successful += 1
break # Succès, sortir de la boucle de retry
except Exception as e:
retry_count += 1
logger.error(f"Erreur chunk {chunk['uuid']} (tentative {retry_count}/{MAX_RETRIES}): {e}")
if retry_count < MAX_RETRIES:
time.sleep(2) # Pause avant retry
else:
failed += 1
logger.error(f"✗ Échec définitif pour chunk {chunk['uuid']}")
# Résumé final
print("\n" + "=" * 80)
print("RÉSULTATS")
print("=" * 80)
print(f"✓ Résumés générés avec succès: {successful}")
print(f"✗ Échecs: {failed}")
print(f"Total traité: {len(chunks_to_process)}")
print("=" * 80)
def main() -> None:
"""Fonction principale."""
logger.info("=" * 80)
logger.info("GÉNÉRATION DE RÉSUMÉS POUR CHUNKS WEAVIATE")
logger.info("=" * 80)
# Vérifier que Ollama est disponible
logger.info("\n[1/4] Vérification d'Ollama...")
try:
ollama.list()
logger.info(f" ✓ Ollama disponible, modèle: {OLLAMA_MODEL}")
except Exception as e:
logger.error(f" ✗ Ollama non disponible: {e}")
logger.error(" → Vérifiez qu'Ollama est lancé (ollama serve)")
sys.exit(1)
# Connexion à Weaviate
logger.info("\n[2/4] Connexion à Weaviate...")
try:
client = weaviate.connect_to_local(
host="localhost",
port=8080,
grpc_port=50051,
)
logger.info(" ✓ Connexion établie")
except Exception as e:
logger.error(f" ✗ Erreur de connexion: {e}")
logger.error(" → Vérifiez que Weaviate est lancé (docker compose up -d)")
sys.exit(1)
try:
# Récupérer les œuvres
logger.info("\n[3/4] Récupération des œuvres disponibles...")
works = get_available_works(client)
if not works:
logger.error(" ✗ Aucune œuvre trouvée dans la base")
sys.exit(1)
logger.info(f"{len(works)} œuvres disponibles")
# Sélection de l'œuvre
selected_work = select_work(works)
logger.info(f"\n→ Œuvre sélectionnée: {selected_work['title']} ({selected_work['chunks_count']} chunks)")
# Traitement
logger.info("\n[4/4] Génération des résumés...")
process_work_chunks(client, selected_work)
logger.info("\n✓ TERMINÉ !")
finally:
client.close()
logger.info("\n✓ Connexion Weaviate fermée")
if __name__ == "__main__":
# Fix encoding pour Windows
if sys.platform == "win32" and hasattr(sys.stdout, 'reconfigure'):
sys.stdout.reconfigure(encoding='utf-8')
main()