Add pgvector quality exporter + fix missing os import
This commit is contained in:
153
exporters/latency_exporter.py
Normal file
153
exporters/latency_exporter.py
Normal file
@@ -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}")
|
||||||
94
exporters/pgvector_quality_exporter.py
Normal file
94
exporters/pgvector_quality_exporter.py
Normal file
@@ -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')
|
||||||
Reference in New Issue
Block a user