File size: 4,627 Bytes
0fd380e
 
d11e1fe
 
bec7021
dbd9820
5f710b6
1737ef1
0fd380e
5f710b6
037a839
11c5d58
 
5f710b6
ec7f6a1
 
 
 
 
5f710b6
d11e1fe
 
5f710b6
7f617c9
ec7f6a1
 
 
 
 
5f710b6
ec7f6a1
7f617c9
ec7f6a1
 
 
5f710b6
ec7f6a1
 
 
5f710b6
ec7f6a1
 
5f710b6
d11e1fe
 
0fd380e
 
2583cf2
 
 
 
 
d11e1fe
 
ec7f6a1
0fd380e
2c6bd00
5f710b6
ec7f6a1
ecd203a
ec7f6a1
 
 
 
 
 
63ed81d
5f710b6
ecd203a
5f710b6
ec7f6a1
 
 
037a839
5f710b6
 
1737ef1
 
d11e1fe
5f710b6
359c625
5f710b6
359c625
 
5f710b6
 
 
359c625
 
0fd380e
 
5f710b6
 
359c625
5f710b6
 
 
 
 
0fd380e
 
2c6bd00
5f710b6
bec7021
 
 
 
 
 
ec7f6a1
5f710b6
bec7021
5ac0022
d11e1fe
0fd380e
d11e1fe
d00f6f0
5f710b6
d00f6f0
 
d5e5243
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
# ✅ API FastAPI de chunking sémantique intelligent avec fallback automatique

from fastapi import FastAPI
from pydantic import BaseModel
from typing import Optional

# ✅ LlamaIndex (version >= 0.10.0)
from llama_index.core import Document
from llama_index.core.settings import Settings
from llama_index.core.node_parser import SemanticSplitterNodeParser, RecursiveCharacterTextSplitter
from llama_index.llms.llama_cpp import LlamaCPP
from llama_index.core.base.llms.base import BaseLLM

# ✅ Embedding local (basé sur transformers + torch)
from transformers import AutoTokenizer, AutoModel
import torch
import torch.nn.functional as F
import os

# ✅ Initialisation de l'application FastAPI
app = FastAPI()

# ✅ Configuration du cache local de Hugging Face pour économiser l'espace dans le container
CACHE_DIR = "/app/cache"
os.environ["HF_HOME"] = CACHE_DIR
os.environ["TRANSFORMERS_CACHE"] = CACHE_DIR
os.environ["HF_MODULES_CACHE"] = CACHE_DIR
os.environ["HF_HUB_CACHE"] = CACHE_DIR

# ✅ Modèle d'embedding local utilisé pour vectoriser les textes
MODEL_NAME = "BAAI/bge-small-en-v1.5"
tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME, cache_dir=CACHE_DIR)
model = AutoModel.from_pretrained(MODEL_NAME, cache_dir=CACHE_DIR)

def get_embedding(text: str):
    """Fonction pour générer un embedding dense normalisé à partir d’un texte."""
    with torch.no_grad():
        inputs = tokenizer(text, return_tensors="pt", truncation=True, padding=True)
        outputs = model(**inputs)
        embeddings = outputs.last_hidden_state[:, 0]  # On prend le vecteur [CLS]
        return F.normalize(embeddings, p=2, dim=1).squeeze().tolist()

# ✅ Schéma des données attendues dans le POST
class ChunkRequest(BaseModel):
    text: str
    max_tokens: Optional[int] = 1000
    overlap: Optional[int] = 350
    source_id: Optional[str] = None
    titre: Optional[str] = None
    source: Optional[str] = None
    type: Optional[str] = None

@app.post("/chunk")
async def chunk_text(data: ChunkRequest):
    try:
        print(f"\n✅ Texte reçu ({len(data.text)} caractères) : {data.text[:200]}...", flush=True)

        # ✅ Chargement du modèle LLM CodeLlama quantifié (GGUF) via URL Hugging Face
        llm = LlamaCPP(
            model_url="https://huggingface.co/TheBloke/CodeLlama-7B-Instruct-GGUF/resolve/main/codellama-7b-instruct.Q4_K_M.gguf",
            temperature=0.1,
            max_new_tokens=512,
            context_window=2048,
            generate_kwargs={"top_p": 0.95},
            model_kwargs={"n_gpu_layers": 1},
        )

        print("✅ Modèle CodeLlama-7B chargé avec succès !")

        # ✅ Embedding local pour LlamaIndex
        class SimpleEmbedding:
            def get_text_embedding(self, text: str):
                return get_embedding(text)

        # ✅ Configuration du moteur dans LlamaIndex
        assert isinstance(llm, BaseLLM), "❌ Le LLM n'est pas compatible avec Settings.llm"
        Settings.llm = llm
        Settings.embed_model = SimpleEmbedding()

        print("✅ Configuration du LLM et de l'embedding terminée.")

        # ✅ Document à découper
        doc = Document(text=data.text)

        # ✅ Split intelligent (semantic)
        parser = SemanticSplitterNodeParser.from_defaults(llm=llm)

        try:
            nodes = parser.get_nodes_from_documents([doc])
            print(f"✅ Semantic Splitter : {len(nodes)} chunks générés")
            if not nodes:
                raise ValueError("Aucun chunk généré par SemanticSplitter")

        except Exception as e:
            print(f"⚠️ Fallback vers RecursiveCharacterTextSplitter suite à : {e}")
            splitter = RecursiveCharacterTextSplitter(
                chunk_size=data.max_tokens,
                chunk_overlap=data.overlap
            )
            nodes = splitter.get_nodes_from_documents([doc])
            print(f"♻️ Recursive Splitter : {len(nodes)} chunks générés")

        # ✅ Construction de la réponse
        return {
            "chunks": [node.text for node in nodes],
            "metadatas": [node.metadata for node in nodes],
            "source_id": data.source_id,
            "titre": data.titre,
            "source": data.source,
            "type": data.type,
            "error": None  # utile pour n8n ou tout autre client
        }

    except Exception as e:
        print(f"❌ Erreur critique : {e}")
        return {"error": str(e)}

# ✅ Lancement du serveur si exécution directe (mode debug)
if __name__ == "__main__":
    import uvicorn
    uvicorn.run("app:app", host="0.0.0.0", port=7860)