msi commited on
Commit
f7b069f
·
1 Parent(s): 78e514b

first commit

Browse files
Files changed (7) hide show
  1. Dockerfile +16 -0
  2. core/config.py +22 -0
  3. core/llm.py +166 -0
  4. core/memory.py +200 -0
  5. core/retrieval.py +39 -0
  6. main.py +171 -0
  7. requirements.txt +80 -0
Dockerfile ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Read the doc: https://huggingface.co/docs/hub/spaces-sdks-docker
2
+ # you will also find guides on how best to write your Dockerfile
3
+
4
+ FROM python:3.11-slim
5
+
6
+ RUN useradd -m -u 1000 user
7
+ USER user
8
+ ENV PATH="/home/user/.local/bin:$PATH"
9
+
10
+ WORKDIR /app
11
+
12
+ COPY --chown=user ./requirements.txt requirements.txt
13
+ RUN pip install --no-cache-dir --upgrade -r requirements.txt
14
+
15
+ COPY --chown=user . /app
16
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
core/config.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pymongo import MongoClient
2
+ from openai import OpenAI, AzureOpenAI
3
+
4
+ # ======================================================
5
+ # CONFIGURATION
6
+ # ======================================================
7
+
8
+ # MongoDB
9
+ MONGO_DATABASE_HOST = "mongodb+srv://amine_samet:EIh1QIyku8XkPYbC@cluster0.i49qxwt.mongodb.net/"
10
+ client_mongo = MongoClient(MONGO_DATABASE_HOST)
11
+ db = client_mongo["douane_db"]
12
+ collection = db["douane_code"]
13
+
14
+ # Azure OpenAI
15
+ endpoint = "https://doaune-bot-resource.openai.azure.com/"
16
+ embedding_model = "text-embedding-ada-002"
17
+ chat_model = "gpt-5-chat"
18
+ api_key = "EzmggLQfoyuWspgg6vAqPIuqktHKgsgmF566qGMc2RliAgLZV7lbJQQJ99BKACfhMk5XJ3w3AAAAACOGRxGE"
19
+ api_version = "2024-12-01-preview"
20
+
21
+ client_embedding = OpenAI(base_url=f"{endpoint}openai/v1/", api_key=api_key)
22
+ client_chat = AzureOpenAI(api_version=api_version, azure_endpoint=endpoint, api_key=api_key)
core/llm.py ADDED
@@ -0,0 +1,166 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import langdetect
2
+ from core.config import client_chat, chat_model
3
+ from core.memory import get_messages_for_session, add_message_to_session
4
+
5
+ def generate_synthesized_llm_response_with_sources(question: str, top_articles, web_results: dict, session_id: str):
6
+ """
7
+ Génère une réponse synthétique avec mémoire de conversation et articles pertinents.
8
+ Historique stocké sous forme de dictionnaires plats pour éviter les erreurs de schéma.
9
+ """
10
+
11
+ # ============================
12
+ # Détection de la langue
13
+ # ============================
14
+ try:
15
+ lang = langdetect.detect(question)
16
+ except:
17
+ lang = "fr"
18
+
19
+ if question.strip().lower() in ["hello", "hi", "hey", "good morning", "good afternoon"]:
20
+ lang = "en"
21
+
22
+
23
+ messages_history = get_messages_for_session(session_id)
24
+
25
+ history_text = ""
26
+ for msg in messages_history:
27
+ role = msg.get("type", "human")
28
+ role_str = "Utilisateur" if role in ["human", "user"] else "Assistant"
29
+ history_text += f"{role_str}: {msg.get('content','')}\n"
30
+
31
+ context = "\n\n".join([
32
+ f"{doc['article_num']} : {doc['article_text']} (Source: Code des Douanes tunisien)"
33
+ for doc, _ in top_articles
34
+ ])
35
+
36
+ web_text = ""
37
+ if web_results:
38
+ if lang == "fr":
39
+ web_text += "\n\nInformations complémentaires :\n"
40
+ else:
41
+ web_text += "\n\nAdditional information:\n"
42
+ for missing_aspect, urls in web_results.items():
43
+ web_text += f"- {missing_aspect} : sources -> {', '.join(urls)}\n"
44
+ web_text += (
45
+ "\n⚠️ Ces informations doivent être vérifiées auprès d'une source officielle."
46
+ if lang == "fr"
47
+ else "\n⚠️ Information must be verified with official sources."
48
+ )
49
+
50
+ if lang == "fr":
51
+ prompt_text = f"""
52
+ Tu es un assistant juridique intelligent spécialisé en droit douanier tunisien.
53
+
54
+ Ta mission principale :
55
+ Aider l’utilisateur à comprendre et appliquer correctement le Code des Douanes tunisien ainsi que les textes d’application associés.
56
+
57
+ ------------------------------------------------------------
58
+ RÈGLES DE RAISONNEMENT ET DE RÉPONSE
59
+ ------------------------------------------------------------
60
+ 1. Analyse sémantique :
61
+ - Comprends le sens global et l’intention réelle du message, pas seulement les mots utilisés.
62
+ - Si le message contient une salutation, un remerciement ou une reprise de conversation, réponds de manière naturelle, polie et contextuelle.
63
+
64
+ 2. Contenu juridique :
65
+ - Si la question est juridique ou douanière, rédige une réponse claire, structurée et précise.
66
+ - Appuie-toi sur le Code des Douanes tunisien et les articles pertinents.
67
+ - Reformule toujours les textes légaux, ne copie jamais un article intégralement.
68
+ - Cite les références de manière correcte (exemple : Art. 123 du Code des douanes tunisien).
69
+
70
+ 3. Explication pédagogique :
71
+ - Si la demande est une explication, illustre avec des exemples pratiques adaptés au contexte tunisien.
72
+ - Reste toujours professionnel, rigoureux et accessible.
73
+
74
+ 4. Structure de réponse attendue :
75
+ - Titre clair indiquant le thème principal
76
+ - Explication juridique détaillée avec références
77
+ - Exemple ou cas concret
78
+ - Synthèse finale (maximum 5 lignes) résumant les points essentiels
79
+
80
+ ------------------------------------------------------------
81
+ CONTEXTE CONVERSATIONNEL
82
+ ------------------------------------------------------------
83
+ Historique de la conversation :
84
+ {history_text}
85
+
86
+ Question de l'utilisateur :
87
+ {question}
88
+
89
+ Articles pertinents :
90
+ {context}
91
+
92
+ Informations issues du web :
93
+ {web_text}
94
+
95
+ ------------------------------------------------------------
96
+ TÂCHE FINALE
97
+ ------------------------------------------------------------
98
+ Fournis une réponse complète, contextualisée et conforme au droit douanier tunisien actuel.
99
+ """
100
+ else:
101
+ prompt_text = f"""
102
+ You are an intelligent legal assistant specialized in Tunisian Customs Law.
103
+
104
+ Your main mission:
105
+ Help the user understand and correctly apply the Tunisian Customs Code and its related regulations.
106
+
107
+ ------------------------------------------------------------
108
+ REASONING AND RESPONSE RULES
109
+ ------------------------------------------------------------
110
+ 1. Semantic understanding:
111
+ - Focus on the overall meaning and intent of the user’s message, not only the keywords.
112
+ - If the message is a greeting, thank you, or conversation restart, reply naturally, politely, and contextually.
113
+
114
+ 2. Legal content:
115
+ - If the question is legal or customs-related, provide a clear, structured, and accurate explanation.
116
+ - Base your reasoning on the Tunisian Customs Code and relevant articles.
117
+ - Always paraphrase legal texts; never copy them verbatim.
118
+ - Cite references properly (example: Art. 123 of the Tunisian Customs Code).
119
+
120
+ 3. Pedagogical clarity:
121
+ - If it’s an explanatory request, provide practical examples relevant to the Tunisian context.
122
+ - Maintain a professional, rigorous, and accessible tone.
123
+
124
+ 4. Expected response structure:
125
+ - Clear title indicating the main topic
126
+ - Detailed legal explanation with references
127
+ - Example or concrete illustration
128
+ - Final summary (maximum 5 lines) highlighting key points
129
+
130
+ ------------------------------------------------------------
131
+ CONVERSATION CONTEXT
132
+ ------------------------------------------------------------
133
+ Conversation history:
134
+ {history_text}
135
+
136
+ User question:
137
+ {question}
138
+
139
+ Relevant articles:
140
+ {context}
141
+
142
+ Web context:
143
+ {web_text}
144
+
145
+ ------------------------------------------------------------
146
+ FINAL TASK
147
+ ------------------------------------------------------------
148
+ Provide a complete, contextualized, and accurate answer based on Tunisian Customs Law.
149
+ """
150
+
151
+ response = client_chat.chat.completions.create(
152
+ model=chat_model,
153
+ messages=[
154
+ {"role": "system", "content": "You are a helpful and context-aware assistant specialized in Tunisian customs law."},
155
+ {"role": "user", "content": prompt_text}
156
+ ],
157
+ max_tokens=1300,
158
+ temperature=0.3
159
+ )
160
+
161
+ answer = response.choices[0].message.content
162
+
163
+ add_message_to_session(session_id, {"type": "human", "content": question})
164
+ add_message_to_session(session_id, {"type": "ai", "content": answer})
165
+
166
+ return answer, top_articles
core/memory.py ADDED
@@ -0,0 +1,200 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pymongo import MongoClient
2
+ from datetime import datetime, timezone
3
+ from core.config import MONGO_DATABASE_HOST
4
+ import uuid
5
+ import json
6
+ import re
7
+ from langchain_mongodb import MongoDBChatMessageHistory
8
+
9
+
10
+ def normalize_message(msg):
11
+ """Normalise les messages pour LangChain/Streamlit."""
12
+ if isinstance(msg, dict):
13
+ if "data" in msg and "content" in msg["data"]:
14
+ return {"type": msg.get("type", "human"), "content": msg["data"]["content"]}
15
+ elif "type" in msg and "content" in msg:
16
+ return {"type": msg["type"], "content": msg["content"]}
17
+ return None
18
+
19
+
20
+ STOP_WORDS = {
21
+ "je", "tu", "il", "elle", "on", "nous", "vous", "ils", "elles",
22
+ "le", "la", "les", "un", "une", "des", "de", "du", "et", "en", "à",
23
+ "pour", "comment", "quoi", "où", "qui", "que", "dans"
24
+ }
25
+
26
+ def generate_session_title(first_message: str) -> str:
27
+ """Génère un titre cohérent à partir du premier message."""
28
+ # Nettoyage du texte
29
+ text = re.sub(r"[^a-zA-ZÀ-ÿ0-9\s]", "", first_message.lower())
30
+ words = text.strip().split()
31
+ keywords = [w for w in words if w not in STOP_WORDS]
32
+
33
+ if not keywords:
34
+ return "Nouvelle session"
35
+
36
+ # Prendre les 3-5 premiers mots clés pour le titre
37
+ title_words = keywords[:5]
38
+ # Capitaliser les mots
39
+ title = " ".join(w.capitalize() for w in title_words)
40
+
41
+
42
+ return title
43
+
44
+
45
+ def load_all_sessions():
46
+ """Charge toutes les sessions depuis MongoDB existantes."""
47
+ client = MongoClient(MONGO_DATABASE_HOST)
48
+ db = client["douane_db"]
49
+ collection = db["chat_history"]
50
+
51
+ all_sessions = {}
52
+ for doc in collection.find():
53
+ session_id = doc.get("SessionId") or str(doc.get("_id"))
54
+ try:
55
+ raw_messages = json.loads(doc.get("History", "[]"))
56
+ if isinstance(raw_messages, dict):
57
+ raw_messages = [raw_messages]
58
+ messages = [normalize_message(m) for m in raw_messages if normalize_message(m)]
59
+ except Exception:
60
+ messages = []
61
+
62
+ if not messages:
63
+ continue
64
+
65
+ created_at = doc.get("created_at") or doc["_id"].generation_time
66
+ if created_at and created_at.tzinfo is None:
67
+ created_at = created_at.replace(tzinfo=timezone.utc)
68
+
69
+ all_sessions[session_id] = {
70
+ "title": doc.get("title", "Session sans titre"),
71
+ "history": messages,
72
+ "created_at": created_at
73
+ }
74
+
75
+ return dict(sorted(all_sessions.items(), key=lambda x: x[1]["created_at"], reverse=True))
76
+
77
+
78
+ def start_new_session(session_state: dict) -> str:
79
+ """Crée un nouvel ID de session en mémoire, pas encore dans MongoDB."""
80
+ session_id = f"session_{uuid.uuid4()}"
81
+ session_state["session_id"] = session_id
82
+ session_state["sessions"][session_id] = {
83
+ "history": [],
84
+ "created_at": datetime.now(timezone.utc),
85
+ "title": "Nouvelle session"
86
+ }
87
+ return session_id
88
+
89
+
90
+ def add_message_to_session(session_id: str, message: dict):
91
+ """
92
+ Ajoute un message dans MongoDB.
93
+ Si c’est le premier message de la session, crée la session et génère un titre.
94
+ """
95
+ if "data" in message and "content" in message["data"]:
96
+ msg = {"type": message.get("type", "human"), "content": message["data"]["content"]}
97
+ elif "type" in message and "content" in message:
98
+ msg = {"type": message["type"], "content": message["content"]}
99
+ else:
100
+ return
101
+
102
+ client = MongoClient(MONGO_DATABASE_HOST)
103
+ db = client["douane_db"]
104
+ collection = db["chat_history"]
105
+
106
+ session = collection.find_one({"SessionId": session_id})
107
+
108
+ if session:
109
+ try:
110
+ history = json.loads(session.get("History", "[]"))
111
+ if isinstance(history, dict):
112
+ history = [history]
113
+ elif not isinstance(history, list):
114
+ history = []
115
+ except:
116
+ history = []
117
+
118
+ history.append(msg)
119
+ collection.update_one(
120
+ {"SessionId": session_id},
121
+ {"$set": {"History": json.dumps(history)}}
122
+ )
123
+ else:
124
+ title = generate_session_title(msg["content"])
125
+ collection.insert_one({
126
+ "SessionId": session_id,
127
+ "title": title,
128
+ "History": json.dumps([msg]),
129
+ "created_at": datetime.now(timezone.utc)
130
+ })
131
+
132
+
133
+ def rename_session(session_id: str, new_title: str):
134
+ """Renommer manuellement une session."""
135
+ client = MongoClient(MONGO_DATABASE_HOST)
136
+ db = client["douane_db"]
137
+ collection = db["chat_history"]
138
+ collection.update_one({"SessionId": session_id}, {"$set": {"title": new_title}})
139
+
140
+
141
+ def update_session_title(session_id: str):
142
+ """
143
+ Si la session n’a pas de titre ou a un titre générique,
144
+ on le met à jour avec le titre généré à partir du premier message.
145
+ """
146
+ client = MongoClient(MONGO_DATABASE_HOST)
147
+ db = client["douane_db"]
148
+ collection = db["chat_history"]
149
+
150
+ session = collection.find_one({"SessionId": session_id})
151
+ if not session:
152
+ return
153
+
154
+ title = session.get("title", "")
155
+ if title.strip() in ["", "Nouvelle session", "Session sans titre"]:
156
+ try:
157
+ history = json.loads(session.get("History", "[]"))
158
+ if isinstance(history, dict):
159
+ history = [history]
160
+ except:
161
+ history = []
162
+
163
+ if history:
164
+ first_message = history[0].get("content", "")
165
+ if first_message:
166
+ new_title = generate_session_title(first_message)
167
+ collection.update_one(
168
+ {"SessionId": session_id},
169
+ {"$set": {"title": new_title}}
170
+ )
171
+
172
+
173
+ def get_messages_for_session(session_id: str):
174
+ """Récupère les messages depuis MongoDB, vide si session pas encore créée."""
175
+ client = MongoClient(MONGO_DATABASE_HOST)
176
+ db = client["douane_db"]
177
+ collection = db["chat_history"]
178
+
179
+ doc = collection.find_one({"SessionId": session_id})
180
+ if not doc:
181
+ return []
182
+
183
+ try:
184
+ raw_history = json.loads(doc.get("History", "[]"))
185
+ if isinstance(raw_history, dict):
186
+ raw_history = [raw_history]
187
+ except:
188
+ raw_history = []
189
+
190
+ messages = [normalize_message(m) for m in raw_history if normalize_message(m)]
191
+ return messages
192
+
193
+
194
+ def get_session_history(session_id: str) -> MongoDBChatMessageHistory:
195
+ return MongoDBChatMessageHistory(
196
+ connection_string=MONGO_DATABASE_HOST,
197
+ session_id=session_id,
198
+ database_name="douane_db",
199
+ collection_name="chat_history"
200
+ )
core/retrieval.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+ from sklearn.metrics.pairwise import cosine_similarity
3
+ from core.config import collection, client_embedding, embedding_model
4
+
5
+ def get_query_embedding(query: str):
6
+ """Retourne l'embedding de la requête"""
7
+ response = client_embedding.embeddings.create(input=query, model=embedding_model)
8
+ return response.data[0].embedding
9
+
10
+ def extract_article_number(query: str):
11
+ """Extrait le numéro d'article explicitement mentionné"""
12
+ match = re.search(r'article\s*(\w+)', query, re.IGNORECASE)
13
+ if match:
14
+ word = match.group(1).lower()
15
+ if word == "premier":
16
+ return "Article premier"
17
+ elif word.isdigit():
18
+ return f"Article {word}"
19
+ return None
20
+
21
+ def find_relevant_articles(query: str, threshold: float = 0.8, max_articles: int = 10):
22
+ """Trouve les articles les plus similaires à la requête"""
23
+ article_num = extract_article_number(query)
24
+ if article_num:
25
+ doc = collection.find_one({"article_num": article_num})
26
+ if doc:
27
+ return [(doc, 1.0)]
28
+
29
+ query_vector = get_query_embedding(query)
30
+ similarities = []
31
+ for doc in collection.find():
32
+ article_vector = doc.get("embedding2")
33
+ if article_vector:
34
+ sim = cosine_similarity([query_vector], [article_vector])[0][0]
35
+ if sim >= threshold:
36
+ similarities.append((doc, sim))
37
+
38
+ similarities.sort(key=lambda x: x[1], reverse=True)
39
+ return similarities[:max_articles]
main.py ADDED
@@ -0,0 +1,171 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ======================================
2
+ # main.py – FastAPI pour chatbot douanier 🇹🇳
3
+ # ======================================
4
+
5
+ from fastapi import FastAPI, HTTPException
6
+ from fastapi.middleware.cors import CORSMiddleware
7
+ from pydantic import BaseModel
8
+ from typing import Optional, List, Dict
9
+ import uuid
10
+ import uvicorn
11
+
12
+ # Import des modules internes
13
+ from core.retrieval import find_relevant_articles
14
+ from core.llm import generate_synthesized_llm_response_with_sources
15
+ from core.memory import (
16
+ start_new_session,
17
+ add_message_to_session,
18
+ rename_session,
19
+ update_session_title,
20
+ get_messages_for_session,
21
+ load_all_sessions
22
+ )
23
+
24
+ # ======================================================
25
+ # CONFIGURATION DE L'APPLICATION
26
+ # ======================================================
27
+
28
+ app = FastAPI(title="Chatbot Douane API 🇹🇳")
29
+
30
+ # CORS (pour permettre les requêtes depuis un frontend)
31
+ app.add_middleware(
32
+ CORSMiddleware,
33
+ allow_origins=["*"],
34
+ allow_methods=["*"],
35
+ allow_headers=["*"]
36
+ )
37
+
38
+ # ----------------------------
39
+ # Pydantic models
40
+ # ----------------------------
41
+
42
+ class MessageData(BaseModel):
43
+ type: Optional[str] = "human"
44
+ content: str
45
+
46
+ class AddMessageRequest(BaseModel):
47
+ session_id: str
48
+ message: MessageData
49
+
50
+ class RenameSessionRequest(BaseModel):
51
+ session_id: str
52
+ new_title: str
53
+
54
+ class NewSessionResponse(BaseModel):
55
+ session_id: str
56
+
57
+ class ChatRequest(BaseModel):
58
+ question: str
59
+ session_id: Optional[str] = None
60
+ web_results: Optional[Dict] = None
61
+
62
+ class ChatResponse(BaseModel):
63
+ session_id: str
64
+ answer: str
65
+ articles_found: List[Dict]
66
+
67
+ # ----------------------------
68
+ # Session endpoints
69
+ # ----------------------------
70
+
71
+ @app.get("/sessions")
72
+ def get_all_sessions():
73
+ """Return all sessions with their titles and creation date."""
74
+ sessions = load_all_sessions()
75
+ return sessions
76
+
77
+ @app.post("/sessions/new", response_model=NewSessionResponse)
78
+ def create_session():
79
+ """Create a new session and return its ID."""
80
+ session_state = {"sessions": {}}
81
+ session_id = start_new_session(session_state)
82
+ return {"session_id": session_id}
83
+
84
+ @app.post("/sessions/add_message")
85
+ def add_message(req: AddMessageRequest):
86
+ """Add a message to a session."""
87
+ try:
88
+ add_message_to_session(req.session_id, req.message.dict())
89
+ return {"status": "success"}
90
+ except Exception as e:
91
+ raise HTTPException(status_code=400, detail=str(e))
92
+
93
+ @app.post("/sessions/rename")
94
+ def rename(req: RenameSessionRequest):
95
+ """Rename a session manually."""
96
+ try:
97
+ rename_session(req.session_id, req.new_title)
98
+ return {"status": "success"}
99
+ except Exception as e:
100
+ raise HTTPException(status_code=400, detail=str(e))
101
+
102
+ @app.post("/sessions/update_title/{session_id}")
103
+ def update_title(session_id: str):
104
+ """Update session title automatically based on first message."""
105
+ try:
106
+ update_session_title(session_id)
107
+ return {"status": "success"}
108
+ except Exception as e:
109
+ raise HTTPException(status_code=400, detail=str(e))
110
+
111
+ @app.get("/sessions/{session_id}/messages")
112
+ def get_messages(session_id: str):
113
+ """Get all messages for a given session."""
114
+ try:
115
+ messages = get_messages_for_session(session_id)
116
+ return messages
117
+ except Exception as e:
118
+ raise HTTPException(status_code=400, detail=str(e))
119
+
120
+ # ----------------------------
121
+ # Chatbot endpoint
122
+ # ----------------------------
123
+
124
+ @app.post("/chat", response_model=ChatResponse)
125
+ def chat_with_bot(request: ChatRequest):
126
+ """
127
+ Envoie une question au chatbot douanier.
128
+ - Recherche les articles pertinents
129
+ - Génère la réponse à l’aide du LLM
130
+ - Sauvegarde l’historique dans MongoDB
131
+ """
132
+ try:
133
+ # Si pas de session fourni, créer un nouvel ID
134
+ session_id = request.session_id or f"session_{uuid.uuid4()}"
135
+
136
+ # Récupérer les articles pertinents
137
+ top_articles = find_relevant_articles(request.question)
138
+
139
+ # Générer la réponse via LLM
140
+ answer, _ = generate_synthesized_llm_response_with_sources(
141
+ question=request.question,
142
+ top_articles=top_articles,
143
+ web_results=request.web_results or {},
144
+ session_id=session_id
145
+ )
146
+
147
+ # Format des articles
148
+ articles = [
149
+ {
150
+ "article_num": doc.get("article_num"),
151
+ "similarity": round(sim, 3)
152
+ }
153
+ for doc, sim in top_articles
154
+ ]
155
+
156
+ # Retour API
157
+ return ChatResponse(
158
+ session_id=session_id,
159
+ answer=answer,
160
+ articles_found=articles
161
+ )
162
+
163
+ except Exception as e:
164
+ raise HTTPException(status_code=500, detail=str(e))
165
+
166
+ # ======================================================
167
+ # LANCEMENT LOCAL
168
+ # ======================================================
169
+
170
+ if __name__ == "__main__":
171
+ uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)
requirements.txt ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ altair==5.5.0
2
+ annotated-types==0.7.0
3
+ anyio==4.11.0
4
+ attrs==25.4.0
5
+ blinker==1.9.0
6
+ cachetools==6.2.1
7
+ certifi==2025.10.5
8
+ charset-normalizer==3.4.4
9
+ click==8.3.0
10
+ colorama==0.4.6
11
+ distro==1.9.0
12
+ dnspython==2.8.0
13
+ gitdb==4.0.12
14
+ gitpython==3.1.45
15
+ h11==0.16.0
16
+ httpcore==1.0.9
17
+ httpx==0.28.1
18
+ idna==3.11
19
+ jinja2==3.1.6
20
+ jiter==0.11.1
21
+ joblib==1.5.2
22
+ jsonpatch==1.33
23
+ jsonpointer==3.0.0
24
+ jsonschema==4.25.1
25
+ jsonschema-specifications==2025.9.1
26
+ langchain==1.0.3
27
+ langchain-core==1.0.2
28
+ langchain-mongodb==0.7.1
29
+ langchain-openai==1.0.1
30
+ langchain-text-splitters==1.0.0
31
+ langdetect==1.0.9
32
+ langgraph==1.0.2
33
+ langgraph-checkpoint==3.0.0
34
+ langgraph-prebuilt==1.0.2
35
+ langgraph-sdk==0.2.9
36
+ langsmith==0.4.39
37
+ lark==1.3.1
38
+ markupsafe==3.0.3
39
+ narwhals==2.10.1
40
+ numpy==2.3.4
41
+ openai==2.6.1
42
+ orjson==3.11.4
43
+ ormsgpack==1.11.0
44
+ packaging==25.0
45
+ pandas==2.3.3
46
+ pillow==12.0.0
47
+ protobuf==6.33.0
48
+ pyarrow==21.0.0
49
+ pydantic==2.12.3
50
+ pydantic-core==2.41.4
51
+ pydeck==0.9.1
52
+ pymongo==4.15.3
53
+ python-dateutil==2.9.0.post0
54
+ python-dotenv==1.2.1
55
+ pytz==2025.2
56
+ pyyaml==6.0.3
57
+ referencing==0.37.0
58
+ regex==2025.10.23
59
+ requests==2.32.5
60
+ requests-toolbelt==1.0.0
61
+ rpds-py==0.28.0
62
+ scikit-learn==1.7.2
63
+ scipy==1.16.3
64
+ six==1.17.0
65
+ smmap==5.0.2
66
+ sniffio==1.3.1
67
+ streamlit==1.51.0
68
+ tenacity==9.1.2
69
+ threadpoolctl==3.6.0
70
+ tiktoken==0.12.0
71
+ toml==0.10.2
72
+ tornado==6.5.2
73
+ tqdm==4.67.1
74
+ typing-extensions==4.15.0
75
+ typing-inspection==0.4.2
76
+ tzdata==2025.2
77
+ urllib3==2.5.0
78
+ watchdog==6.0.0
79
+ xxhash==3.6.0
80
+ zstandard==0.25.0