MissawB's picture
Upload folder using huggingface_hub
26cb04a verified
Raw
History Blame Contribute Delete
18.8 kB
import datetime # noqa: E402
import logging # noqa: E402
from core.ports.eval_port import EvalResultPort # noqa: E402
from core.ports.feature_store_port import FeatureStorePort # noqa: E402
from core.ports.feedback_port import FeedbackRepositoryPort # noqa: E402
from core.ports.gold_dataset_port import GoldDatasetPort # noqa: E402
from core.ports.mlops_port import MlopsPort # noqa: E402
from core.ports.repository_port import RepositoryPort # noqa: E402
from dependency_injector.wiring import Provide, inject
from django.db import models
from django.utils.decorators import method_decorator
from django_ratelimit.decorators import ratelimit
from drf_spectacular.utils import extend_schema
from rest_framework import permissions, status, viewsets
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.views import APIView
from ..containers import Container # noqa: E402
from ..forms import ( # noqa: E402
UserIDForm,
VertexFeatureStoreForm,
VertexPipelineListForm,
VertexPipelineTriggerForm,
)
from ..models import (
AIFeedback,
AIREvalResult,
AISafetyEvent, # noqa: E402
GoldDatasetEntry,
)
from ..permissions import IsAdminOrReadOnlyExpert, IsIAPApprovedAdmin # noqa: E402
from ..serializers import ( # noqa: E402
AIFeedbackInputSerializer,
AIFeedbackSerializer,
AIREvalResultSerializer,
AISafetyEventSerializer,
DPOCurationSerializer,
GoldDatasetEntrySerializer,
XaiReportSerializer,
)
logger = logging.getLogger("animetix.mlops")
class AISafetyEventViewSet(viewsets.ReadOnlyModelViewSet):
"""API for viewing AI Safety events (Guardrail logs)."""
queryset = AISafetyEvent.objects.all().order_by("-created_at")
serializer_class = AISafetyEventSerializer
permission_classes = [permissions.IsAdminUser]
class AIREvaluationViewSet(viewsets.ReadOnlyModelViewSet):
"""API for AI Evaluation results and stats."""
queryset = AIREvalResult.objects.all().order_by("-created_at")
serializer_class = AIREvalResultSerializer
permission_classes = [permissions.IsAdminUser]
@inject
def __init__(
self,
*args,
eval_port: EvalResultPort = Provide[Container.persistence.eval_adapter],
**kwargs,
):
super().__init__(*args, **kwargs)
self.eval_port = eval_port
@action(detail=False, methods=["get"])
def stats(self, request):
stats_data = self.eval_port.get_evaluation_stats()
return Response(stats_data)
@action(detail=False, methods=["get"])
def failures(self, request):
"""Fetch evaluation logs with low scores or hallucinations."""
queryset = self.queryset.filter(
models.Q(hallucination_detected=True)
| models.Q(faithfulness__lt=0.6)
| models.Q(relevancy__lt=0.6)
)[:20]
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
class LatentSpaceAPIView(APIView):
"""API for retrieving latent space data for visualization."""
permission_classes = [permissions.IsAdminUser]
@inject
def __init__(
self,
repository: RepositoryPort = Provide[Container.persistence.repository],
**kwargs,
):
super().__init__(**kwargs)
self.repository = repository
@extend_schema(responses={200: XaiReportSerializer})
def get(self, request):
media = request.GET.get("media", "anime").lower()
vibe_type = request.GET.get("type", "thematic").lower()
data = self.repository.load_latent_space(media, vibe_type)
if data:
return Response(data)
return Response({"error": "Data not found"}, status=status.HTTP_404_NOT_FOUND)
@method_decorator(
ratelimit(key="user_or_ip", rate="5/m", method="POST", block=True), name="dispatch"
)
class AIFeedbackAPIView(APIView):
"""API for submitting and retrieving AI feedback."""
def get_permissions(self):
if self.request.method == "GET":
return [permissions.IsAuthenticated()]
return [permissions.AllowAny()]
@inject
def __init__(
self,
feedback_port: FeedbackRepositoryPort = Provide[
Container.persistence.feedback_adapter
],
**kwargs,
):
super().__init__(**kwargs)
self.feedback_port = feedback_port
def get(self, request):
"""Récupère l'historique des feedbacks de l'utilisateur connecté."""
feedbacks = (
AIFeedback.objects.filter(user=request.user)
.select_related("user")
.order_by("-created_at")[:100]
)
serializer = AIFeedbackSerializer(feedbacks, many=True)
return Response(serializer.data)
def post(self, request):
serializer = AIFeedbackInputSerializer(data=request.data)
if not serializer.is_valid():
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
data = serializer.validated_data
user_id = request.user.id if request.user.is_authenticated else None
self.feedback_port.save_feedback(
input_context=data["input_context"],
output_text=data["output_text"],
is_positive=data["is_positive"],
user_id=user_id,
feedback_type=data["type"],
)
return Response(
{"status": "success", "message": "Feedback submitted successfully"}
)
class GoldDatasetViewSet(viewsets.ModelViewSet):
"""API for Gold Dataset curation."""
queryset = GoldDatasetEntry.objects.all().order_by("-created_at")
serializer_class = GoldDatasetEntrySerializer
permission_classes = [permissions.IsAdminUser]
@inject
def __init__(
self,
*args,
gold_port: GoldDatasetPort = Provide[
Container.persistence.gold_dataset_adapter
],
**kwargs,
):
super().__init__(*args, **kwargs)
self.gold_port = gold_port
@action(detail=False, methods=["post"])
def sync_positive_feedback(self, request):
"""Syncs all uncurated positive feedback to the gold dataset."""
count = self.gold_port.sync_positive_feedback()
return Response({"status": "success", "synced_count": count})
@action(detail=True, methods=["post"])
def validate(self, request, pk=None):
success = self.gold_port.validate_entry(pk)
if success:
return Response({"status": "validated"})
return Response({"error": "Entry not found"}, status=status.HTTP_404_NOT_FOUND)
from core.domain.services.dspy_prompt_optimizer import DSPyPromptOptimizer # noqa: E402
class DPOCurationViewSet(viewsets.ViewSet):
"""API for DPO Curation (List and Post)."""
permission_classes = [permissions.IsAdminUser]
@inject
def __init__(
self,
*args,
dpo_loop=Provide[Container.core.dpo_feedback_loop],
**kwargs,
):
super().__init__(*args, **kwargs)
self.dpo_loop = dpo_loop
def list(self, request):
limit = int(request.GET.get("limit", 50))
samples = self.dpo_loop.get_rejected_for_curation(limit=limit)
return Response(samples)
def create(self, request):
serializer = DPOCurationSerializer(data=request.data)
if not serializer.is_valid():
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
success = self.dpo_loop.curate_feedback(**serializer.validated_data)
if success:
return Response(
{"status": "success", "message": "Feedback successfully curated."}
)
return Response({"error": "Curation failed"}, status=500)
class DSPyOptimizerView(APIView):
"""
Interface pour piloter l'optimisation automatique des prompts via DSPy.
Permet de muter des templates et de sélectionner le meilleur variant.
"""
permission_classes = [permissions.IsAdminUser]
@inject
def __init__(
self,
optimizer: DSPyPromptOptimizer = Provide[Container.core.dspy_prompt_optimizer],
**kwargs,
):
super().__init__(**kwargs)
self.optimizer = optimizer
def post(self, request):
template = request.data.get("template")
dataset = request.data.get("dataset", [])
if not template:
return Response({"error": "template is required"}, status=400)
if not dataset:
# Fallback dataset if empty for demo
dataset = [
{
"query": "Qui est Luffy ?",
"expected": "Luffy est le protagoniste de One Piece et capitaine des Chapeaux de Paille.",
},
{
"query": "Quel est le pouvoir de Gojo ?",
"expected": "Satoru Gojo possède l'Infini et le Sixième Œil.",
},
]
try:
best_template, best_score = self.optimizer.evaluate_and_select_best(
template, dataset
)
# On génère aussi les mutations pour l'affichage
mutations = self.optimizer.mutate_template(template, num_mutations=3)
return Response(
{
"status": "success",
"best_template": best_template,
"best_score": best_score,
"all_mutations": mutations,
}
)
except Exception:
logger.exception("Error in MLOps mutation")
return Response({"error": "Internal server error"}, status=500)
class SOTABenchmarkListView(APIView):
"""Récupère les benchmarks SOTA (State of the Art) pour les modèles IA."""
permission_classes = [permissions.AllowAny] # Public pour la transparence
@inject
def __init__(
self,
benchmark_service=Provide[Container.core.sota_benchmark_service],
**kwargs,
):
super().__init__(**kwargs)
self.benchmark_service = benchmark_service
def get(self, request):
service = self.benchmark_service
benchmarks = service.get_all_benchmarks()
best_elo = service.get_best_model("elo_score")
best_os = service.get_open_source_best()
return Response(
{
"status": "success",
"timestamp": datetime.datetime.now().isoformat(),
"benchmarks": benchmarks,
"top_model": best_elo,
"best_open_source": best_os[0] if best_os else None,
}
)
class DPOFeedbackLoopView(APIView):
"""
API for managing the DPO Feedback Loop.
Provides status, trends, and manual triggers for optimization.
"""
permission_classes = [permissions.IsAdminUser]
@inject
def __init__(
self,
dpo_loop=Provide[Container.core.dpo_feedback_loop],
**kwargs,
):
super().__init__(**kwargs)
self.dpo_loop = dpo_loop
def get(self, request, *args, **kwargs):
stats = self.dpo_loop.analyze_feedback_trends()
return Response(
{
"status": "active",
"timestamp": datetime.datetime.now().isoformat(),
"metrics": stats,
},
status=status.HTTP_200_OK,
)
def post(self, request, *args, **kwargs):
action_type = request.data.get("action")
dpo_loop = self.dpo_loop
if action_type == "export":
dpo_loop.export_preference_dataset()
return Response(
{
"status": "success",
"message": "DPO dataset exported to local storage.",
}
)
if action_type == "optimize":
prompt_key = request.data.get("prompt_key", "rag_response")
new_prompt = dpo_loop.optimize_prompt_from_feedback(prompt_key)
if new_prompt:
return Response({"status": "success", "new_system_prompt": new_prompt})
return Response(
{
"status": "error",
"message": "Optimization failed or insufficient data.",
},
status=400,
)
return Response({"error": "Invalid action"}, status=status.HTTP_400_BAD_REQUEST)
class AdaptersView(APIView):
"""
API for managing MLOps Adapters.
Allows real-time health checks and dynamic configuration.
"""
permission_classes = [permissions.IsAdminUser]
@inject
def __init__(
self,
engine=Provide[Container.inference.inference_engine],
unified_adapter=Provide[Container.inference.unified_inference_adapter],
**kwargs,
):
super().__init__(**kwargs)
self.engine = engine
self.unified_adapter = unified_adapter
def get(self, request, *args, **kwargs):
health = self.engine.health_check()
return Response(
{"engine": "FallbackInferenceAdapter", "health": health},
status=status.HTTP_200_OK,
)
def post(self, request, *args, **kwargs):
action_type = request.data.get("action")
engine = self.engine
if action_type == "set_primary":
index = int(request.data.get("index", 0))
if engine.set_primary_adapter(index):
return Response(
{
"status": "success",
"message": f"Adapter at index {index} is now primary.",
}
)
return Response({"error": "Invalid index"}, status=400)
if action_type == "set_model":
model_name = request.data.get("model_name")
if not model_name:
return Response({"error": "model_name required"}, status=400)
# Target the unified adapter specifically
self.unified_adapter.set_model_name(model_name)
return Response(
{
"status": "success",
"message": f"Unified adapter switched to model {model_name}.",
}
)
return Response({"error": "Invalid action"}, status=status.HTTP_400_BAD_REQUEST)
class VertexPipelineView(APIView):
"""Manually trigger and list Vertex AI MLOps pipelines (DPO and RAG)."""
permission_classes = [IsAdminOrReadOnlyExpert]
@inject
def __init__(
self,
mlops_port: MlopsPort = Provide[Container.agentic.mlops_adapter_factory],
**kwargs,
):
super().__init__(**kwargs)
self.mlops_port = mlops_port
def post(self, request):
form = VertexPipelineTriggerForm(request.data)
if not form.is_valid():
return Response(form.errors, status=status.HTTP_400_BAD_REQUEST)
pipeline_type = form.cleaned_data["pipeline_type"]
try:
if pipeline_type == "dpo":
min_samples = form.cleaned_data.get("min_samples") or 100
result = self.mlops_port.trigger_dpo_pipeline(min_samples=min_samples)
elif pipeline_type == "rag":
result = self.mlops_port.trigger_rag_pipeline()
elif pipeline_type == "star":
result = self.mlops_port.trigger_star_pipeline()
else:
return Response(
{"error": f"Invalid pipeline_type: {pipeline_type}"},
status=status.HTTP_400_BAD_REQUEST,
)
return Response(
{"status": "success", "pipeline_run": result},
status=status.HTTP_201_CREATED,
)
except Exception as e:
logger.error(f"Vertex pipeline trigger failed: {e}")
return Response(
{"error": "Internal server error"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
def get(self, request):
form = VertexPipelineListForm(request.GET)
if not form.is_valid():
return Response(form.errors, status=status.HTTP_400_BAD_REQUEST)
pipeline_name = form.cleaned_data.get("pipeline_name")
limit = form.cleaned_data.get("limit") or 20
try:
runs = self.mlops_port.list_pipeline_runs(
pipeline_name=pipeline_name, limit=limit
)
return Response({"status": "success", "runs": runs})
except Exception as e:
logger.error(f"Vertex pipeline list failed: {e}")
return Response(
{"error": "Internal server error"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
class VertexFeatureStoreView(APIView):
"""Query and seed user preference vectors in the Vertex AI Feature Store."""
permission_classes = [IsIAPApprovedAdmin]
@inject
def __init__(
self,
feature_store: FeatureStorePort = Provide[
Container.persistence.feature_store_adapter
],
**kwargs,
):
super().__init__(**kwargs)
self.feature_store = feature_store
def get(self, request):
form = UserIDForm(request.GET)
if not form.is_valid():
return Response(form.errors, status=status.HTTP_400_BAD_REQUEST)
user_id = form.cleaned_data["user_id"]
try:
features = self.feature_store.get_user_preferences(str(user_id))
return Response(
{"status": "success", "user_id": user_id, "features": features}
)
except Exception as e:
logger.error(f"Feature Store read failed: {e}")
return Response(
{"error": "Internal server error"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
def post(self, request):
form = VertexFeatureStoreForm(request.data)
if not form.is_valid():
return Response(form.errors, status=status.HTTP_400_BAD_REQUEST)
user_id = form.cleaned_data["user_id"]
features = form.cleaned_data["features"]
try:
self.feature_store.save_user_preferences(str(user_id), features)
return Response(
{
"status": "success",
"message": f"Updated Feature Store values for user {user_id}",
},
status=status.HTTP_201_CREATED,
)
except Exception as e:
logger.error(f"Feature Store write failed: {e}")
return Response(
{"error": "Internal server error"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
)