import os, urllib.request from contextlib import asynccontextmanager from dotenv import load_dotenv from fastapi import FastAPI from pydantic import BaseModel from langchain_community.document_loaders import PyPDFLoader from langchain_text_splitters import RecursiveCharacterTextSplitter from langchain_nvidia_ai_endpoints import ChatNVIDIA, NVIDIAEmbeddings from langchain_chroma import Chroma from langchain_core.prompts import ChatPromptTemplate load_dotenv() DOCS_DIR = "/tmp/documents/" rag_state = {} def telecharger_documents(): os.makedirs(DOCS_DIR, exist_ok=True) dest = os.path.join(DOCS_DIR, "CONVENTION_SYNTEC.pdf") if os.path.exists(dest): print(f"Le fichier {dest} existe deja — telechargement ignore.") else: print("Telechargement de CONVENTION_SYNTEC.pdf...") urllib.request.urlretrieve( "https://github.com/archiducarmel/SupDeVinci_M1_MachineLearning_DeepLearning/releases/download/datas/CONVENTION_SYNTEC.pdf", dest, ) print("OK.") size_kb = os.path.getsize(dest) / 1024 print(f"\n✅ {dest} ({size_kb:.0f} Ko) pret dans ./documents/") def extract_answer(response): text = (response.content or "").strip() if not text: text = (response.additional_kwargs.get("reasoning_content", "") or "").strip() return text def format_docs(docs): return "\n\n".join( f"[{d.metadata.get('source', '?').split('/')[-1]} — page {d.metadata.get('page')}] {d.page_content}" for d in docs ) @asynccontextmanager async def lifespan(app: FastAPI): telecharger_documents() all_docs = [] for f in sorted(os.listdir(DOCS_DIR)): if f.endswith(".pdf"): all_docs.extend(PyPDFLoader(os.path.join(DOCS_DIR, f)).load()) chunks = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=100).split_documents(all_docs) embeddings = NVIDIAEmbeddings(model="nvidia/llama-nemotron-embed-1b-v2", truncate="NONE") vector_store = Chroma.from_documents(chunks, embeddings) prompt = ChatPromptTemplate.from_template( "Tu es un assistant RH expert de la convention collective Syntec. " "Réponds à la QUESTION en t'appuyant UNIQUEMENT sur le CONTEXTE ci-dessous.\n" "Si l'information n'y figure pas, réponds exactement : « Je ne sais pas ».\n" "Sois concis et cite la source (document et numéro de page).\n\n" "CONTEXTE :\n{context}\n\nQUESTION : {input}" ) llm = ChatNVIDIA(model="openai/gpt-oss-120b", temperature=0.2, max_completion_tokens=2048) rag_state["retriever"] = vector_store.as_retriever(search_kwargs={"k": 3}) rag_state["generation"] = prompt | llm | extract_answer yield rag_state.clear() app = FastAPI(title="Assistant RH — Convention Syntec — API RAG", lifespan=lifespan) class QuestionIn(BaseModel): question: str def rag_answer(question): docs = rag_state["retriever"].invoke(question) answer = rag_state["generation"].invoke({"context": format_docs(docs), "input": question}) return {"answer": answer, "context": docs} @app.get("/") def health(): return {"status": "ok"} @app.post("/ask") def ask(payload: QuestionIn): result = rag_answer(payload.question) sources = [ {"document": d.metadata.get("source", "?").split("/")[-1], "page": d.metadata.get("page")} for d in result["context"] ] return {"answer": result["answer"], "sources": sources}