This commit is contained in:
Johan
2025-12-17 13:30:08 +01:00
parent cc50161771
commit 60b912d524
7 changed files with 39 additions and 85 deletions

View File

@@ -1,7 +1,5 @@
import strawberry import strawberry
# Note : l'énoncé demande d'importer la V1 à l'étape 3, puis la V2 à l'étape 6
# L'import ci-dessous correspond à l'étape 6.
from app.graphql.resolvers.analyze_movie_v2 import analyze_movie_by_id from app.graphql.resolvers.analyze_movie_v2 import analyze_movie_by_id
from app.graphql.types.movie_analysis import MovieAnalysis from app.graphql.types.movie_analysis import MovieAnalysis
@@ -11,13 +9,7 @@ class Query:
""" """
Point d'entrée pour toutes les requêtes GraphQL de type 'query'. Point d'entrée pour toutes les requêtes GraphQL de type 'query'.
""" """
analyze_movie: MovieAnalysis = strawberry.field(
# TODO : (Étape 3) L'énoncé vous demande d'implémenter ce champ resolver=analyze_movie_by_id,
# en important et en utilisant 'analyze_movie_v1' description="Lance une analyse par IA d'un film donné par son ID."
# (Étape 6) L'énoncé vous demande ensuite de basculer vers 'analyze_movie_v2'
# La configuration ci-dessous correspond à l'étape 6.
analyzeMovie: MovieAnalysis = strawberry.field(
resolver=analyze_movie_by_id, # 'analyze_movie_by_id' est importé depuis v2
description="Analyse un film en utilisant l'IA."
) )

View File

@@ -10,9 +10,6 @@ async def analyze_movie_by_id(
# movie_input: MovieInput, classe nécessaire seulement si on avait eu beaucoup de champs en entrée # movie_input: MovieInput, classe nécessaire seulement si on avait eu beaucoup de champs en entrée
) -> MovieAnalysis: ) -> MovieAnalysis:
# TODO : (Étape 3) Remplacer la ligne suivante par un appel au service V1 analysis_data = await analyze_movie(movie_id=movie_id)
# analysis_data = await analyze_movie(movie_id=movie_id)
raise NotImplementedError("Le resolver analyze_movie_v1.analyze_movie_by_id() n'est pas implémenté.")
# La ligne ci-dessous doit être décommentée une fois le TODO complété return MovieAnalysis(**analysis_data)
# return MovieAnalysis(**analysis_data)

View File

@@ -5,18 +5,13 @@ from app.graphql.resolvers.helper import is_field_requested
from app.graphql.types.movie_analysis import MovieAnalysis from app.graphql.types.movie_analysis import MovieAnalysis
from app.services.movie_analyzer_v2 import analyze_movie from app.services.movie_analyzer_v2 import analyze_movie
async def analyze_movie_by_id( async def analyze_movie_by_id(
movie_id: strawberry.ID, movie_id: strawberry.ID,
info: Info, info: Info,
) -> MovieAnalysis: ) -> MovieAnalysis:
# TODO : (Étape 6) Remplacer la ligne suivante par la récupération
# du LLM depuis 'info.context' (ex: llm = info.context["llm"])
raise NotImplementedError("Le resolver V2 n'a pas encore récupéré le LLM du contexte.")
# llm = ... # <== Code à écrire llm = info.context["llm"]
# Le 'llm=llm' ci-dessous fonctionnera une fois le TODO complété
analysis_data = await analyze_movie( analysis_data = await analyze_movie(
movie_id=movie_id, movie_id=movie_id,
ai_summary=is_field_requested(info, "aiSummary"), ai_summary=is_field_requested(info, "aiSummary"),

View File

@@ -7,10 +7,6 @@ from app.repositories._base_client import api_client
class GenreRepository: class GenreRepository:
async def list(self) -> List[Genre]: async def list(self) -> List[Genre]:
response = await api_client._request("GET", "/genres/") response = await api_client._request("GET", "/genres/")
# TODO : (Étape 2) Remplacer la ligne suivante par un appel à
# response = await api_client._request("GET", "/genres/")
# La ligne ci-dessous doit être décommentée une fois le TODO complété
return [Genre.model_validate(g) for g in response.json()] return [Genre.model_validate(g) for g in response.json()]
genre_repository = GenreRepository() genre_repository = GenreRepository()

View File

@@ -1,28 +1,24 @@
from typing import List, Optional from typing import List, Optional
import httpx
from app.core.exceptions import DALException from app.core.exceptions import DALException
from app.models.movie import Movie from app.models.movie import Movie
from app.repositories._base_client import api_client from app.repositories._base_client import api_client
class MovieRepository: class MovieRepository:
async def list(self, skip: int = 0, limit: int = 100) -> List[Movie]: async def list(self, skip: int = 0, limit: int = 100) -> List[Movie]:
# TODO : (Étape 2) Remplacer la ligne suivante par un appel à response = await api_client._request(
# response = await api_client._request("GET", "/movies/", params={"skip": skip, "limit": limit}) "GET", "/movies/", params={"skip": skip, "limit": limit}
# La ligne ci-dessous doit être décommentée une fois le TODO complété )
response = await api_client._request("GET", "/movies/", params={"skip": skip, "limit": limit})
return [Movie.model_validate(m) for m in response.json()] return [Movie.model_validate(m) for m in response.json()]
async def find_by_id(self, movie_id: int) -> Optional[Movie]: async def find_by_id(self, movie_id: int) -> Optional[Movie]:
try: try:
response = await api_client._request("GET", f"/movies/{movie_id}") response = await api_client._request("GET", f"/movies/{movie_id}")
# TODO : (Étape 2) Remplacer la ligne suivante par un appel à
# response = await api_client._request("GET", f"/movies/{movie_id}")
# La ligne ci-dessous doit être décommentée une fois le TODO complété
return Movie.model_validate(response.json()) return Movie.model_validate(response.json())
except DALException as e: except DALException as e:
if e.status_code == 404: if e.status_code == 404:
return None return None
raise raise
movie_repository = MovieRepository() movie_repository = MovieRepository()

View File

@@ -1,17 +1,9 @@
async def analyze_movie(movie_id: str) -> dict: async def analyze_movie(movie_id: str) -> dict:
# TODO : retourner un dictionnaire python statique (chaînes de caractères en dur) avec comme attributs:
# id (correspondant à movie_id)
# aiSummary (chaîne de caractères arbitraire)
# aiOpinionSummary (chaîne de caractères arbitraire)
# aiBestGenre (chaîne de caractères arbitraire)
# aiTags (TABLEAU de chaînes de caractères arbitraire)
raise NotImplementedError("Le service movie_analyzer_v1.analyze_movie() n'est pas implémenté.")
return { return {
"id": movie_id, "id": movie_id,
"aiSummary" : "C'est l'histoire de...",
"aiOpinionSummary": "Le film est une aventure épique...",
"aiBestGenre": "Fantastique",
"aiTags": ["Épique", "Quête", "Magie"]
} }

View File

@@ -41,22 +41,12 @@ async def get_ai_best_genre(llm, synopsis, all_genres):
genres_list = ", ".join([genre.label for genre in all_genres]) genres_list = ", ".join([genre.label for genre in all_genres])
# Prompt pour choisir le genre le plus pertinent # Prompt pour choisir le genre le plus pertinent
# TODO : compléter les instructions du prompt
prompt = f""" prompt = f"""
# TODO : Écrire les instructions pour le LLM. Français uniquement.
# Objectif : Choisir le *seul* genre le plus pertinent pour le film. Parmi la liste suivante de genres cinématographiques, choisis le genre le plus pertinent pour le synopsis donné.
# Contraintes : Liste des genres : {genres_list}
# 1. Le LLM DOIT répondre en français. Ne retourne que le nom du genre, sans explication, sans phrase d'introduction.
# 2. Le LLM DOIT choisir son genre EXCLUSIVEMENT parmi la liste fournie. Synopsis : {synopsis}
# 3. Le LLM NE DOIT retourner QUE le nom du genre (ex: "Drame"), sans aucune autre phrase.
Voici le synopsis :
{synopsis}
Voici la liste des genres autorisés :
{genres_list}
Genre le plus pertinent :
""" """
# Appel asynchrone au modèle de langage # Appel asynchrone au modèle de langage
@@ -68,21 +58,15 @@ async def get_ai_tags(llm, title, synopsis):
if not title or not synopsis: if not title or not synopsis:
return None return None
# TODO : définir le prompt approprié
prompt = f""" prompt = f"""
# TODO : Écrire les instructions pour le LLM. Français uniquement.
# Objectif : Générer 5 tags (mots-clés) pertinents pour le film. Liste 5 à 8 mots-clés (tags) pertinents pour le film suivant.
# Contraintes : Réponds uniquement avec les mots-clés séparés par des virgules, sans numérotation, sans explication, sans phrase d'introduction.
# 1. Le LLM DOIT répondre en français. Par exemple : "intelligence artificielle, prophétie, pirate informatique, réalité virtuelle".
# 2. Le LLM DOIT retourner une liste de tags séparés par des virgules.
# 3. Le LLM NE DOIT PAS inclure de phrase d'introduction (ex: "Voici les tags :").
Titre du film : {title} Titre : {title}
Synopsis : {synopsis} Synopsis : {synopsis}
Génère 5 tags pertinents, séparés par des virgules :
""" """
response = await llm.ainvoke(prompt) response = await llm.ainvoke(prompt)
tags = [tag.strip() for tag in response.content.split(',') if tag.strip()] tags = [tag.strip() for tag in response.content.split(',') if tag.strip()]
return tags return tags
@@ -113,12 +97,14 @@ async def analyze_movie(
if ai_summary: if ai_summary:
tasks["aiSummary"] = get_ai_summary(llm, movie_data.synopsis) tasks["aiSummary"] = get_ai_summary(llm, movie_data.synopsis)
# TODO : (Étape 5) compléter la logique d'ajout des tâches avec : if ai_opinion_summary:
# appeler 'get_ai_opinion_summary', 'get_ai_best_genre', 'get_ai_tags' tasks["aiOpinionSummary"] = get_ai_opinion_summary(llm, movie_data.title, movie_data.opinions)
# mettre le résultat respectivement dans la clé "aiOpinionSummary", "aiBestGenre", "aiTags" (ATTENTION : il faut respecter la casse pour ces clés!) du tableau associatif (dictionnaire) "tasks"
# respectivement en fonction des booléens 'ai_opinion_summary', 'ai_best_genre', 'ai_tags' if ai_best_genre:
if ai_opinion_summary or ai_best_genre or ai_tags: tasks["aiBestGenre"] = get_ai_best_genre(llm, movie_data.synopsis, all_genres)
raise NotImplementedError("La logique d'ajout de tâches (opinion, genre, tags) n'est pas implémentée.")
if ai_tags:
tasks["aiTags"] = get_ai_tags(llm, movie_data.title, movie_data.synopsis)
if tasks: if tasks:
# On récupère les coroutines (les fonctions async prêtes à être lancées) # On récupère les coroutines (les fonctions async prêtes à être lancées)