Vous êtes développeur.euse Java au sein d’une startup spécialisée dans l’évaluation des modèles d’intelligence artificielle. Vous devez développer AIModelHub, une application web Spring Boot permettant de cataloguer, noter et commenter les modèles IA disponibles sur le marché (GPT-4, Llama 3, Mistral, Claude, Gemini…).
CREATE DATABASE ai_model_hub WITH ENCODING 'UTF8'; \c ai_model_hub CREATE TABLE fournisseur ( id BIGSERIAL PRIMARY KEY, nom VARCHAR(150) NOT NULL UNIQUE, description TEXT, site_web VARCHAR(255), pays VARCHAR(100), date_creation TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE modele_ia ( id BIGSERIAL PRIMARY KEY, nom VARCHAR(200) NOT NULL, version VARCHAR(50), description TEXT, type_modele VARCHAR(50) NOT NULL, statut VARCHAR(20) NOT NULL DEFAULT 'ACTIF', parametres BIGINT, contexte_max INTEGER, open_source BOOLEAN NOT NULL DEFAULT FALSE, gratuit BOOLEAN NOT NULL DEFAULT FALSE, fournisseur_id BIGINT NOT NULL REFERENCES fournisseur(id), date_sortie DATE, date_creation TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, date_modif TIMESTAMP ); CREATE TABLE tag ( id BIGSERIAL PRIMARY KEY, nom VARCHAR(50) NOT NULL UNIQUE ); CREATE TABLE modele_tag ( modele_id BIGINT NOT NULL REFERENCES modele_ia(id) ON DELETE CASCADE, tag_id BIGINT NOT NULL REFERENCES tag(id) ON DELETE CASCADE, PRIMARY KEY (modele_id, tag_id) ); CREATE TABLE evaluation ( id BIGSERIAL PRIMARY KEY, modele_id BIGINT NOT NULL REFERENCES modele_ia(id) ON DELETE CASCADE, evaluateur VARCHAR(100) NOT NULL, note INTEGER NOT NULL CHECK (note BETWEEN 1 AND 5), commentaire TEXT, cas_utilisation VARCHAR(200), date_evaluation TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); -- Données initiales INSERT INTO fournisseur (nom, description, site_web, pays) VALUES ('OpenAI', 'Créateur de GPT et DALL-E', 'https://openai.com', 'États-Unis'), ('Meta AI', 'Division IA de Meta (Facebook)', 'https://ai.meta.com', 'États-Unis'), ('Mistral AI', 'Startup française spécialisée en LLM', 'https://mistral.ai', 'France'), ('Google', 'DeepMind et Google Brain', 'https://deepmind.google', 'États-Unis'), ('Anthropic', 'Créateur de Claude', 'https://anthropic.com', 'États-Unis'); INSERT INTO tag (nom) VALUES ('NLP'),('Vision'),('Code'),('Multimodal'),('Open Source'), ('RLHF'),('RAG'),('Fine-tunable'),('API disponible'),('Temps réel');
Sur https://start.spring.io :
fr.formation
ai-model-hub
fr.formation.aimodelhub
Dépendances :
Ajouter manuellement dans pom.xml :
<!-- Layout Thymeleaf pour les templates partagés --> <dependency> <groupId>nz.net.ultraq.thymeleaf</groupId> <artifactId>thymeleaf-layout-dialect</artifactId> </dependency>
# src/main/resources/application.properties spring.datasource.url=jdbc:postgresql://localhost:5432/ai_model_hub spring.datasource.username=VOTRE_USER spring.datasource.password=VOTRE_MOT_DE_PASSE spring.jpa.hibernate.ddl-auto=validate spring.jpa.show-sql=true spring.jpa.properties.hibernate.format_sql=true logging.level.fr.formation=DEBUG
# src/test/resources/application.properties spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;MODE=PostgreSQL spring.datasource.driver-class-name=org.h2.Driver spring.datasource.username=sa spring.datasource.password= spring.jpa.hibernate.ddl-auto=create-drop
public enum TypeModele { TEXTE, IMAGE, AUDIO, MULTIMODAL, CODE, EMBEDDING } public enum StatutModele { ACTIF, DEPRECATED, EXPERIMENTAL }
Fournisseur — Table fournisseur
Fournisseur
fournisseur
ModeleIa — Table modele_ia
ModeleIa
modele_ia
Tag — Table tag
Tag
tag
Evaluation — Table evaluation
Evaluation
evaluation
Utilisez @EqualsAndHashCode(of = "id") et @ToString(exclude = {...}) sur toutes les entités ayant des associations.
@EqualsAndHashCode(of = "id")
@ToString(exclude = {...})
ModeleForm.java (Lombok @Data @NoArgsConstructor) :
ModeleForm.java
@Data @NoArgsConstructor
id Long nullable (null = création) nom String @NotBlank @Size(min=2, max=200) version String @Size(max=50) description String @NotBlank @Size(min=10) typeModele TypeModele @NotNull statut StatutModele default ACTIF parametres Long @Min(0) contexteMax Integer @Min(0) openSource boolean gratuit boolean fournisseurId Long @NotNull dateSortie LocalDate tagIds Set<Long> IDs des tags sélectionnés
EvaluationForm.java (Lombok @Data @NoArgsConstructor) :
EvaluationForm.java
evaluateur String @NotBlank @Size(min=2, max=100) note Integer @NotNull @Min(1) @Max(5) commentaire String @Size(max=2000) casUtilisation String @Size(max=200)
ModeleReponse.java (record Java 17) :
ModeleReponse.java
id, nom, version, description, typeModele, statut, parametres, contexteMax, openSource, gratuit, fournisseurNom, fournisseurPays, dateSortie, tags (Set<String>), noteMoyenne (Double), nombreEvaluations (int)
ModeleResume.java (record Java 17 — pour la liste) :
ModeleResume.java
id, nom, version, typeModele, fournisseurNom, openSource, gratuit, noteMoyenne, nombreEvaluations, statut
ModeleMapper.java (@Component) :
ModeleMapper.java
@Component
ModeleReponse versReponse(ModeleIa modele); ModeleResume versResume(ModeleIa modele); ModeleIa versEntite(ModeleForm form); void mettreAJourEntite(ModeleForm form, ModeleIa modele); ModeleForm versFormulaire(ModeleIa modele);
EvaluationMapper.java (@Component) :
EvaluationMapper.java
Evaluation versEntite(EvaluationForm form, ModeleIa modele);
Créez ModeleMapperTest avec minimum 5 tests :
ModeleMapperTest
versReponse_modeleAvecFournisseur_nomFournisseurMappe()
versReponse_modeleAvecTags_tagsMappes()
versEntite_formValide_idNull()
versResume_noteCalculee()
versFormulaire_fournisseurIdRecupere()
@Repository public interface ModeleIaRepository extends JpaRepository<ModeleIa, Long>, JpaSpecificationExecutor<ModeleIa> { // Chargement avec toutes les associations (éviter le N+1) @Query("SELECT DISTINCT m FROM ModeleIa m " + "LEFT JOIN FETCH m.fournisseur " + "LEFT JOIN FETCH m.tags " + "WHERE m.id = :id") Optional<ModeleIa> findByIdAvecDetails(@Param("id") Long id); // Pour la liste avec pagination @Query("SELECT m FROM ModeleIa m LEFT JOIN FETCH m.fournisseur ORDER BY m.nom") List<ModeleIa> findAllAvecFournisseur(); boolean existsByNomIgnoreCaseAndFournisseur(String nom, Fournisseur fournisseur); }
@Service @Transactional public class ModeleIaService { // Méthodes à implémenter : Page<ModeleIa> trouverTous(Pageable pageable); ModeleIa trouverParId(Long id); // → ResourceNotFoundException si absent ModeleIa creer(ModeleForm form); ModeleIa mettreAJour(Long id, ModeleForm form); void desactiver(Long id); // soft delete : statut = DEPRECATED List<ModeleIa> trouverParType(TypeModele type); List<ModeleIa> trouverOpenSource(); List<ModeleIa> trouverGratuits(); }
/modeles
/modeles/{id}
/modeles/nouveau
/modeles/{id}/modifier
/modeles/{id}/supprimer
templates/layout/base.html
templates/modeles/liste.html
templates/modeles/detail.html
templates/modeles/formulaire.html
La page de liste doit afficher :
Afficher :
@Service @Transactional public class EvaluationService { // Ajouter une évaluation // → Vérifier qu'un évaluateur n'a pas déjà évalué ce modèle // → Lève ValidationMetierException si doublon Evaluation evaluer(Long modeleId, EvaluationForm form); // Note moyenne d'un modèle Double calculerNoteMoyenne(Long modeleId); // Évaluations d'un modèle, triées par date desc List<Evaluation> trouverParModele(Long modeleId); }
Règle métier obligatoire : si evaluateur a déjà soumis une évaluation pour ce modèle, retourner au formulaire avec le message d’erreur : “Vous avez déjà évalué ce modèle.”
evaluateur
POST /modeles/{id}/evaluer
@Valid
public class ResourceNotFoundException extends RuntimeException { public ResourceNotFoundException(String ressource, Object id) { super(ressource + " introuvable : " + id); } } public class ValidationMetierException extends RuntimeException { private final String champ; public ValidationMetierException(String champ, String message) { ... } public void injecterDans(BindingResult result) { ... } }
@ControllerAdvice public class GlobalExceptionHandler { // Gérer : ResourceNotFoundException → 404 // Gérer : IllegalArgumentException → 400 // Gérer : IllegalStateException → 409 // Gérer : Exception (générique) → 500 }
Créez templates/error/404.html, templates/error/500.html, templates/error/erreur.html avec un design soigné.
templates/error/404.html
templates/error/500.html
templates/error/erreur.html
Test obligatoire : naviguer sur /modeles/9999 doit afficher la page 404 avec le message “Modèle IA introuvable : 9999”.
/modeles/9999
ModeleFormValidationTest
// Tests à implémenter : void modeleForm_valide_aucuneViolation() void modeleForm_nomVide_violationSurNom() void modeleForm_descriptionTropCourte_violation() void modeleForm_fournisseurIdNull_violation() void evaluationForm_note0_violation() void evaluationForm_note6_violation()
ModeleControllerTest
// Tests à implémenter : void getListe_retourneVue200() void getDetail_modeleExistant_retourne200() void getDetail_modeleInexistant_retourne404() void postNouveau_formValide_redirige() void postNouveau_nomVide_retourneFormulaireAvecErreurs() void postEvaluer_noteHorsLimites_retourneErreur()
Bonus : Recherche multicritère avec JpaSpecificationExecutor — filtrer par type, fournisseur, open source, gratuit, recherche textuelle sur le nom.
JpaSpecificationExecutor
Commencez par Mission 1 (entités) — une fois les entités correctes, le reste se construit dessus. Testez la connexion BDD dès la Mission 1 (ddl-auto=validate doit passer sans erreur). ️N’exposez JAMAIS une entité JPA directement dans un Model Thymeleaf — utilisez toujours un DTO. Pour Thymeleaf : le DTO dans Model.addAttribute("modeleForm", new ModeleForm()) doit avoir le même nom que dans @ModelAttribute("modeleForm") du contrôleur.
ddl-auto=validate
Model.addAttribute("modeleForm", new ModeleForm())
@ModelAttribute("modeleForm")
Philippe Bouget