SDV / app.py
UgoCodee's picture
manual push
ca4aa2d
Raw
History Blame Contribute Delete
3.48 kB
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}