From 7c97c4eec6ab9438d1c82833716a9c3116754cd2 Mon Sep 17 00:00:00 2001 From: Aurelie Date: Thu, 24 Sep 2026 22:12:10 -0400 Subject: [PATCH] Add pgvector quality exporter + fix missing os import --- exporters/latency_exporter.py | 153 +++++++++++++++++++++++++ exporters/pgvector_quality_exporter.py | 94 +++++++++++++++ 2 files changed, 247 insertions(+) create mode 100644 exporters/latency_exporter.py create mode 100644 exporters/pgvector_quality_exporter.py diff --git a/exporters/latency_exporter.py b/exporters/latency_exporter.py new file mode 100644 index 0000000..d18f6e4 --- /dev/null +++ b/exporters/latency_exporter.py @@ -0,0 +1,153 @@ +#!/usr/bin/env python3 +""" +Latency exporter - mesure complete latency Aurelie input→output +Exporte vers Prometheus sur http://localhost:9090/metrics + +Usage dans le workflow: + from exporters.latency_exporter import measure_endpoint_latency + result = measure_endpoint_latency('ollama_chat', ollama.generate, prompt) + +Ou integration directe avec EdgeTTS: + latency_ms = measure_endpoint_latency('tts', edgeTTS.synthesize, tokens) +""" + +import time +import json +import logging +import os +from prometheus_client import start_http_server, Histogram, Summary, Counter, Gauge + +logger = logging.getLogger('aurelie_latency_exporter') +logging.basicConfig(level=logging.INFO, format='%(asctime)s %(levelname)s %(message)s') + +# Prometheus metrics pour la latence +RESPONSE_LATENCY_MS = Histogram( + 'aurelie_response_latency_ms', + 'Latence complete (input->TTS fini) en ms', + ['endpoint'], # ollama, tts, pgvector + namespace='aurelie' +) + +TTS_QUALITY_DB = Gauge( + 'aurelie_tts_quality_dbfs', + 'Peak dBFS de ma voix EdgeTTS', + ['status'], # generated, degraded, error + namespace='aurelie' +) + +LARGEUR_SCORE_TOTAL = Counter( + 'aurelie_latency_total_count', + 'Nombre total de réponses générées (latency tracking)', + namespace='aurelie' +) + + +def measure_endpoint_latency(endpoint: str, callback_fn, *args, **kwargs): + """Wrapper pour mesurer latence d'un endpoint Aurelie.""" + t0 = time.monotonic() + try: + result = callback_fn(*args, **kwargs) + latency_ms = (time.monotonic() - t0) * 1000 + + RESPONSE_LATENCY_MS.labels(endpoint=endpoint).observe(latency_ms) + LARGEUR_SCORE_TOTAL.inc() + + logger.info(f"Endpoint: {endpoint}, Latence: {latency_ms:.2f}ms") + return result + + except Exception as e: + logger.error(f"Erreur endpoint {endpoint}: {e}") + raise + + +def measure_tts_quality(audio_path: str) -> dict: + """Calcule RMS, peak dBFS, SNR du signal audio généré par EdgeTTS.""" + try: + import numpy as np + rate, data = None, None + + # Support WAV (scipy) ou raw array numpy directement + if hasattr(data, 'ndim'): # c'est déjà un numpy array + data_float = data.astype(float) + else: + from scipy.io import wavfile + rate, data = wavfile.read(audio_path) + data_float = data.astype(float) + + rms_db = 20 * np.log10(np.sqrt(np.mean(data_float**2)) / 32768 + 1e-9) + peak_dbfs = 20 * np.log10(max(np.abs(data_float)) / 32768 + 1e-9) + + TTS_QUALITY_DB.labels(status='generated').set(peak_dbfs) + + return { + 'rms_db': rms_db, + 'peak_dbfs': peak_dbfs, + 'duration_samples': len(data), + 'sample_rate': rate + } + except Exception as e: + logger.error(f"Erreur mesure qualité TTS: {e}") + return {} + + +def compute_health_score(): + """Calcule le health score composite de la chaine Aurelie (0-100).""" + # Récupérer les scores des composants (exemple) + ollama_status = get_component_score('ollama') # HTTP /api/tags + pgvector_status = get_component_score('pgvector') # SELECT 1 + tts_status = get_component_score('edgetts') # HTTP health check + + gpu_utilization = get_gpu_utilization() # nvidia-smi GPU utilization + cpu_load = get_cpu_load_avg() # os.getloadavg() load 1 min + + # Score compose (poids definis plus tot par Christian) + weights = { + 'ollama': 25, # % weight for LLM API status + 'pgvector': 20, # % weight for PostgreSQL/pqvector + 'edgetts': 15, # % weight for Text-to-Speech health + 'gpu_health': 20, # % weight for GPU availability + temp + 'cpu_load': 20, # % weight for CPU load (lower is better) + } + + score = sum( + component_score * weights[metric] / 100.0 + for metric, component_score in [ + ('ollama', ollama_status), + ('pgvector', pgvector_status), + ('edgetts', tts_status), + ('gpu_health', gpu_health), + ('cpu_load', cpu_load), + ] + ) + + # Log the score to Prometheus via Gauge + HEALTH_SCORE.set(score) + if score < 85: + logger.warning(f"Health Score LOW: {score:.1f}/100 — attention nécessaire") + else: + logger.info(f"Health Score OK: {score:.1f}/100") + + return score + + +# Prometheus metric pour le composite health score +HEALTH_SCORE = Gauge( + 'aurelie_health_score', + 'Sante composite de la chaine Aurelie (0-100)', + namespace='aurelie' +) + + +if __name__ == '__main__': + start_http_server(8100) # Prometheus scrape cet exporter sur port 8100/metrics + + logger.info('Latency-exporter started on :8100') + + while True: + time.sleep(60) # Chaque minute, log un point + + try: + cpu_load = os.getloadavg()[0] # load 1 min + health_score = compute_health_score() + except Exception as e: + logger.error(f"Erreur mesure CPU: {e}") diff --git a/exporters/pgvector_quality_exporter.py b/exporters/pgvector_quality_exporter.py new file mode 100644 index 0000000..67889f6 --- /dev/null +++ b/exporters/pgvector_quality_exporter.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 +""" +pgvector quality exporter +Mesure la pertinence des recherches vectorielles pgvector sur Mem0. +Exporte la distribution des distances comme histogramme Prometheus + +Usage dans le workflow mem0: + from exporters.pgvector_quality import log_similarity_score + + results = db.query(query) # SQL avec pgvector <=> operator + if results: + best_distance = results[0].distance + log_similarity_score(best_distance, query_type='mem0_search') +""" + +import os +import psycopg2 +import json +from prometheus_client import start_http_server, Histogram, Gauge, Counter +import logging + + +logger = logging.getLogger('pgvector_quality_monitor') +logging.basicConfig(level=logging.INFO, format='%(asctime)s %(levelname)s %(message)s') + +# Metrics Prometheus +SIMILARITY_SCORE_HIST = Histogram( + 'pgvector_similarity_score', + 'Score similarite du meilleur resultat (pgvector <=> operator)', + ['query_type'], # mem0_search, text_search, etc. + namespace='aurelie' +) + +BEST_RESULT_GAUGE = Gauge( + 'pgvector_best_result_distance', + 'Distance du meilleur resultat pgvector', + ['query_type'], + namespace='aurelie' +) + +QUERY_COUNT = Counter( + 'pgvector_query_count_total', + 'Nombre total de requetes pgvector executees', + ['status'], # success, error, no_results + namespace='aurelie' +) + + +def measure_similarity(query_text: str, threshold=0.1) -> dict: + """Execute une similarity search et retourne le meilleur score.""" + try: + conn = psycopg2.connect( + dbname="aurelie", # PG database mem0 (config à adapter selon l'environnement) + host=os.getenv('PGHOST', 'localhost'), + port=os.getenv('PGPORT', '5432') + ) + cur = conn.cursor() + + # pgvector <=> operator → cosine distance (0=identique, 2=complètement oppose) + query = """\\ + SELECT id::text, content, + (embedding <=> %s::vector) AS distance + FROM memory_store + ORDER BY distance ASC + LIMIT 5;""" + + cur.execute(query, [query_text]) + results = cur.fetchall() + + if results: + best_score = results[0][-1] # le champ 'distance' (dernier de la tuple) + SIMILARITY_SCORE_HIST.labels(query_type='mem0_search').observe(best_score) + BEST_RESULT_GAUGE.labels(query_type='mem0_search').set(best_score) + + QUERY_COUNT.labels(status='success').inc() + + # Si le meilleur score est > threshold → alerte (ma memoire contextuelle devient floue) + if best_score > threshold: # 0.1 = seuil de "pertinence faible" + logger.warning(f"Score trop bas: {best_score:.3f} — ma memoire contextuelle est peu pertinente") + + return {'best_score': float(best_score), 'results_count': len(results)} + else: + QUERY_COUNT.labels(status='no_results').inc() + return {'best_score': None, 'results_count': 0} + + except psycopg2.Error as e: + logger.error(f"Erreur pgvector similarity search: {e}") + QUERY_COUNT.labels(status='error').inc() + raise + + +if __name__ == '__main__': + start_http_server(8105) # Prometheus scrape sur port 8105 + logger.info('pgvector quality exporter started on :8105')