- Multi-modèles : 4 rôles LLM (coding=gemma3:27b-cloud, cpam=gemma3:27b-cloud, validation=deepseek-v3.2:cloud, qc=gemma3:12b) avec get_model(role) - Prompts externalisés : 7 templates dans src/prompts/templates.py - Cache Ollama : modèle stocké par entrée (migration auto ancien format) - call_ollama() : paramètre role= (priorité: model > role > global) - Quality engine : veto_engine + decision_engine + rules_router (YAML) - Benchmark qualité : scripts/benchmark_quality.py (A/B, métriques CIM-10) - Fix biologie : valeurs qualitatives (troponine négative) non filtrées - Fix CPAM : gemma3:27b-cloud au lieu de deepseek (JSON tronqué par thinking) - CPAM max_tokens 4000→6000, viewer admin multi-modèles - Benchmark 10 dossiers : 100% DAS valides, 10/10 CPAM, 243s/dossier Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
169 lines
6.4 KiB
Python
169 lines
6.4 KiB
Python
"""Tests unitaires pour le cache Ollama persistant."""
|
|
|
|
import json
|
|
import threading
|
|
|
|
import pytest
|
|
|
|
from src.medical.ollama_cache import OllamaCache
|
|
|
|
|
|
class TestOllamaCache:
|
|
def test_get_miss(self, tmp_path):
|
|
cache = OllamaCache(tmp_path / "cache.json", "gemma3:12b")
|
|
assert cache.get("HTA", "das") is None
|
|
|
|
def test_put_and_get(self, tmp_path):
|
|
cache = OllamaCache(tmp_path / "cache.json", "gemma3:12b")
|
|
result = {"code": "I10", "confidence": "high", "justification": "HTA essentielle"}
|
|
cache.put("HTA", "das", result)
|
|
assert cache.get("HTA", "das") == result
|
|
|
|
def test_key_normalization(self, tmp_path):
|
|
cache = OllamaCache(tmp_path / "cache.json", "gemma3:12b")
|
|
result = {"code": "I10", "confidence": "high"}
|
|
cache.put(" HTA ", "das", result)
|
|
assert cache.get("hta", "das") == result
|
|
|
|
def test_different_types_different_keys(self, tmp_path):
|
|
cache = OllamaCache(tmp_path / "cache.json", "gemma3:12b")
|
|
cache.put("Diabète", "dp", {"code": "E11.9"})
|
|
cache.put("Diabète", "das", {"code": "E11.8"})
|
|
assert cache.get("Diabète", "dp")["code"] == "E11.9"
|
|
assert cache.get("Diabète", "das")["code"] == "E11.8"
|
|
|
|
def test_save_and_reload(self, tmp_path):
|
|
path = tmp_path / "cache.json"
|
|
cache = OllamaCache(path, "gemma3:12b")
|
|
cache.put("HTA", "das", {"code": "I10"})
|
|
cache.save()
|
|
|
|
assert path.exists()
|
|
|
|
cache2 = OllamaCache(path, "gemma3:12b")
|
|
assert cache2.get("HTA", "das") == {"code": "I10"}
|
|
|
|
def test_save_no_write_if_clean(self, tmp_path):
|
|
path = tmp_path / "cache.json"
|
|
cache = OllamaCache(path, "gemma3:12b")
|
|
cache.save()
|
|
assert not path.exists()
|
|
|
|
def test_model_change_returns_none(self, tmp_path):
|
|
"""Entrées d'un autre modèle retournent None (pas d'invalidation globale)."""
|
|
cache = OllamaCache(tmp_path / "cache.json", "gemma3:12b")
|
|
cache.put("HTA", "das", {"code": "I10"})
|
|
# Même cache, modèle différent → miss
|
|
assert cache.get("HTA", "das", model="llama3:8b") is None
|
|
# Modèle original → hit
|
|
assert cache.get("HTA", "das") == {"code": "I10"}
|
|
|
|
def test_corrupted_file(self, tmp_path):
|
|
path = tmp_path / "cache.json"
|
|
path.write_text("not valid json", encoding="utf-8")
|
|
|
|
cache = OllamaCache(path, "gemma3:12b")
|
|
assert len(cache) == 0
|
|
assert cache.get("HTA", "das") is None
|
|
|
|
def test_len(self, tmp_path):
|
|
cache = OllamaCache(tmp_path / "cache.json", "gemma3:12b")
|
|
assert len(cache) == 0
|
|
cache.put("HTA", "das", {"code": "I10"})
|
|
assert len(cache) == 1
|
|
cache.put("Diabète", "dp", {"code": "E11.9"})
|
|
assert len(cache) == 2
|
|
|
|
def test_thread_safety(self, tmp_path):
|
|
"""Écriture concurrente depuis plusieurs threads."""
|
|
cache = OllamaCache(tmp_path / "cache.json", "gemma3:12b")
|
|
errors = []
|
|
|
|
def writer(i):
|
|
try:
|
|
cache.put(f"diag_{i}", "das", {"code": f"X{i:02d}"})
|
|
except Exception as e:
|
|
errors.append(e)
|
|
|
|
threads = [threading.Thread(target=writer, args=(i,)) for i in range(20)]
|
|
for t in threads:
|
|
t.start()
|
|
for t in threads:
|
|
t.join()
|
|
|
|
assert not errors
|
|
assert len(cache) == 20
|
|
|
|
def test_json_format_new(self, tmp_path):
|
|
"""Le nouveau format stocke le modèle PAR ENTRÉE (pas global)."""
|
|
path = tmp_path / "cache.json"
|
|
cache = OllamaCache(path, "gemma3:12b")
|
|
cache.put("HTA", "das", {"code": "I10"})
|
|
cache.save()
|
|
|
|
raw = json.loads(path.read_text(encoding="utf-8"))
|
|
assert "entries" in raw
|
|
assert "model" not in raw # plus de model global
|
|
# Chaque entrée contient model + result
|
|
entry = list(raw["entries"].values())[0]
|
|
assert entry["model"] == "gemma3:12b"
|
|
assert entry["result"] == {"code": "I10"}
|
|
|
|
def test_migration_old_format(self, tmp_path):
|
|
"""Ancien format (model global) migré automatiquement."""
|
|
path = tmp_path / "cache.json"
|
|
# Écrire un cache ancien format
|
|
old_data = {
|
|
"model": "gemma3:12b",
|
|
"entries": {
|
|
"das::hta": {"code": "I10", "confidence": "high"},
|
|
},
|
|
}
|
|
path.write_text(json.dumps(old_data), encoding="utf-8")
|
|
|
|
cache = OllamaCache(path, "gemma3:12b")
|
|
# L'entrée doit être accessible
|
|
assert cache.get("HTA", "das") == {"code": "I10", "confidence": "high"}
|
|
assert len(cache) == 1
|
|
|
|
# Sauvegarder et vérifier le nouveau format
|
|
cache.save()
|
|
raw = json.loads(path.read_text(encoding="utf-8"))
|
|
assert "model" not in raw
|
|
entry = raw["entries"]["das::hta"]
|
|
assert entry["model"] == "gemma3:12b"
|
|
assert entry["result"]["code"] == "I10"
|
|
|
|
def test_migration_old_format_different_model(self, tmp_path):
|
|
"""Migration ancien format : les entrées sont bien taggées avec l'ancien modèle."""
|
|
path = tmp_path / "cache.json"
|
|
old_data = {
|
|
"model": "old-model",
|
|
"entries": {
|
|
"das::hta": {"code": "I10"},
|
|
},
|
|
}
|
|
path.write_text(json.dumps(old_data), encoding="utf-8")
|
|
|
|
# Charger avec un modèle différent
|
|
cache = OllamaCache(path, "new-model")
|
|
# L'entrée est taggée "old-model" → miss avec "new-model"
|
|
assert cache.get("HTA", "das") is None
|
|
# Mais accessible avec l'ancien modèle
|
|
assert cache.get("HTA", "das", model="old-model") == {"code": "I10"}
|
|
|
|
def test_put_with_explicit_model(self, tmp_path):
|
|
"""put() avec model= explicite stocke ce modèle."""
|
|
cache = OllamaCache(tmp_path / "cache.json", "default-model")
|
|
cache.put("HTA", "das", {"code": "I10"}, model="explicit-model")
|
|
# get sans model → utilise default → miss
|
|
assert cache.get("HTA", "das") is None
|
|
# get avec le bon modèle → hit
|
|
assert cache.get("HTA", "das", model="explicit-model") == {"code": "I10"}
|
|
|
|
def test_get_returns_none_if_model_mismatch(self, tmp_path):
|
|
"""get() retourne None si le modèle stocké ≠ modèle demandé."""
|
|
cache = OllamaCache(tmp_path / "cache.json", "gemma3:12b")
|
|
cache.put("HTA", "das", {"code": "I10"})
|
|
assert cache.get("HTA", "das", model="llama3:8b") is None
|