JTh34 commited on
Commit
4b3cbd6
·
1 Parent(s): 7c4a6d3

🚀 Déploiement automatique RAG CHU 2025-06-30 22:38:37

Browse files
backend/src/config.py CHANGED
@@ -32,10 +32,15 @@ class Settings(BaseSettings):
32
  anthropic_model: str = Field("claude-3-sonnet-20240229", env="ANTHROPIC_MODEL")
33
  embedding_model: str = Field("text-embedding-3-small", env="EMBEDDING_MODEL")
34
 
35
- # RAG Configuration
36
- chunk_size: int = Field(1000, env="CHUNK_SIZE")
37
- chunk_overlap: int = Field(200, env="CHUNK_OVERLAP")
38
- retrieval_k: int = Field(5, env="RETRIEVAL_K")
 
 
 
 
 
39
 
40
  class Config:
41
  env_file = ".env"
 
32
  anthropic_model: str = Field("claude-3-sonnet-20240229", env="ANTHROPIC_MODEL")
33
  embedding_model: str = Field("text-embedding-3-small", env="EMBEDDING_MODEL")
34
 
35
+ # # RAG Configuration
36
+ # chunk_size: int = Field(1000, env="CHUNK_SIZE")
37
+ # chunk_overlap: int = Field(200, env="CHUNK_OVERLAP")
38
+ # retrieval_k: int = Field(5, env="RETRIEVAL_K")
39
+
40
+ # RAG Configuration optimisée
41
+ chunk_size: int = Field(800, env="CHUNK_SIZE")
42
+ chunk_overlap: int = Field(100, env="CHUNK_OVERLAP")
43
+ retrieval_k: int = Field(8, env="RETRIEVAL_K")
44
 
45
  class Config:
46
  env_file = ".env"
frontend/src/App.css CHANGED
@@ -561,4 +561,33 @@ body {
561
  .card h2 {
562
  font-size: 1rem;
563
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
564
  }
 
561
  .card h2 {
562
  font-size: 1rem;
563
  }
564
+ }
565
+
566
+ /* Animation pour le curseur de streaming */
567
+ .streaming-cursor {
568
+ color: #10b981;
569
+ font-weight: bold;
570
+ animation: blink 1s infinite;
571
+ margin-left: 2px;
572
+ }
573
+
574
+ @keyframes blink {
575
+ 0%, 50% { opacity: 1; }
576
+ 51%, 100% { opacity: 0; }
577
+ }
578
+
579
+ /* Spinner médical élégant */
580
+ .medical-spinner {
581
+ width: 20px;
582
+ height: 20px;
583
+ border: 2px solid rgba(255, 255, 255, 0.1);
584
+ border-left: 2px solid #10b981;
585
+ border-radius: 50%;
586
+ animation: medical-spin 1s linear infinite;
587
+ flex-shrink: 0;
588
+ }
589
+
590
+ @keyframes medical-spin {
591
+ 0% { transform: rotate(0deg); }
592
+ 100% { transform: rotate(360deg); }
593
  }
frontend/src/components/ChatInterface.js CHANGED
@@ -5,24 +5,21 @@ const ChatInterface = ({ selectedDocument, ws }) => {
5
  const [messages, setMessages] = useState([]);
6
  const [currentMessage, setCurrentMessage] = useState('');
7
  const [isLoading, setIsLoading] = useState(false);
8
- const [showSuggestions, setShowSuggestions] = useState(false);
9
  const messagesEndRef = useRef(null);
10
 
11
  useEffect(() => {
12
  if (selectedDocument) {
13
  setMessages([{
14
  type: 'assistant',
15
- content: `Bonjour ! Je suis prêt à répondre à vos questions sur le document **${selectedDocument.filename}**.
16
-
17
- N'hésitez pas à être précis dans vos questions !`,
18
  timestamp: new Date()
19
  }]);
20
- // Afficher les suggestions quand un document est sélectionné
21
- setShowSuggestions(true);
22
  } else {
23
  setMessages([{
24
  type: 'assistant',
25
- content: `Bienvenue dans l'assistant médical RAG CHU !
26
 
27
  **Pour commencer :**
28
  1. Uploadez un document médical (PDF, DOCX, Image)
@@ -36,8 +33,6 @@ N'hésitez pas à être précis dans vos questions !`,
36
  - Consultez la console debug à droite pour suivre l'analyse`,
37
  timestamp: new Date()
38
  }]);
39
- // Masquer les suggestions si pas de document
40
- setShowSuggestions(false);
41
  }
42
  }, [selectedDocument]);
43
 
@@ -59,25 +54,41 @@ N'hésitez pas à être précis dans vos questions !`,
59
  };
60
 
61
  setMessages(prev => [...prev, userMessage]);
 
62
  setCurrentMessage('');
63
  setIsLoading(true);
64
-
65
- // Masquer les suggestions après le premier message envoyé
66
- setShowSuggestions(false);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
 
68
  try {
69
- const response = await fetch('/api/chat', {
70
  method: 'POST',
71
  headers: {
72
  'Content-Type': 'application/json',
73
  },
74
  body: JSON.stringify({
75
- question: currentMessage,
76
  document_id: selectedDocument.document_id
77
  }),
78
  });
79
 
80
  if (!response.ok) {
 
81
  let errorMessage = `Erreur HTTP ${response.status}`;
82
  try {
83
  const errorData = await response.json();
@@ -89,33 +100,130 @@ N'hésitez pas à être précis dans vos questions !`,
89
  throw new Error(errorMessage);
90
  }
91
 
92
- const responseText = await response.text();
93
- let result;
 
 
 
94
  try {
95
- result = JSON.parse(responseText);
96
- } catch (jsonError) {
97
- console.error('Réponse non-JSON reçue:', responseText);
98
- throw new Error('Réponse invalide du serveur');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
  }
100
 
101
- const assistantMessage = {
102
- type: 'assistant',
103
- content: result.response,
104
- sources: result.sources,
105
- timestamp: new Date()
106
- };
107
-
108
- setMessages(prev => [...prev, assistantMessage]);
109
-
110
  } catch (error) {
111
  console.error('Erreur chat:', error);
112
- const errorMessage = {
113
- type: 'assistant',
114
- content: `Erreur : ${error.message}`,
115
- timestamp: new Date()
116
- };
117
- setMessages(prev => [...prev, errorMessage]);
 
 
 
 
 
 
 
 
 
 
 
 
118
  } finally {
 
119
  setIsLoading(false);
120
  }
121
  };
@@ -134,35 +242,29 @@ N'hésitez pas à être précis dans vos questions !`,
134
  });
135
  };
136
 
137
- const suggestedQuestions = [
138
- "Quelle est la posologie recommandée ?",
139
- "Quels sont les critères de gravité ?",
140
- "Y a-t-il des contre-indications ?",
141
- "Quelle est la durée de traitement ?",
142
- "Quelles sont les alternatives thérapeutiques ?"
143
- ];
144
-
145
  return (
146
  <div className="card">
147
  <h2>{selectedDocument ? `Chat avec ${selectedDocument.filename}` : 'Assistant Médical'}</h2>
148
 
149
  <div className="chat-container">
150
  <div className="chat-messages">
151
- {messages.map((message, index) => (
 
 
152
  <div
153
- key={index}
154
  className={`message ${message.type === 'user' ? 'message-user' : 'message-assistant'}`}
155
  >
156
  <div className="message-content">
157
  {message.type === 'user' ? (
158
  <p>{message.content}</p>
159
  ) : (
160
- <ReactMarkdown>{message.content}</ReactMarkdown>
 
 
161
  )}
162
  </div>
163
 
164
-
165
-
166
  <div style={{ fontSize: '0.7rem', opacity: 0.6, marginTop: '0.5rem' }}>
167
  {formatTimestamp(message.timestamp)}
168
  </div>
@@ -172,8 +274,16 @@ N'hésitez pas à être précis dans vos questions !`,
172
  {isLoading && (
173
  <div className="message message-assistant">
174
  <div className="message-content">
175
- <div className="loading"></div>
176
- <span style={{ marginLeft: '0.5rem' }}>Réflexion en cours...</span>
 
 
 
 
 
 
 
 
177
  </div>
178
  </div>
179
  )}
@@ -181,44 +291,6 @@ N'hésitez pas à être précis dans vos questions !`,
181
  <div ref={messagesEndRef} />
182
  </div>
183
 
184
- {/* Suggestions au-dessus de la zone de saisie */}
185
- {showSuggestions && selectedDocument && (
186
- <div style={{
187
- marginBottom: '1rem',
188
- padding: '1rem',
189
- background: 'rgba(255,255,255,0.05)',
190
- borderRadius: '8px',
191
- border: '1px solid rgba(255,255,255,0.1)'
192
- }}>
193
- <p style={{
194
- fontSize: '0.9rem',
195
- color: 'rgba(255,255,255,0.8)',
196
- marginBottom: '0.75rem',
197
- fontWeight: '500'
198
- }}>
199
- Suggestions de questions :
200
- </p>
201
- <div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.5rem' }}>
202
- {suggestedQuestions.map((question, index) => (
203
- <button
204
- key={index}
205
- onClick={() => setCurrentMessage(question)}
206
- className="btn btn-secondary btn-small"
207
- style={{
208
- fontSize: '0.8rem',
209
- padding: '0.4rem 0.8rem',
210
- background: 'rgba(15, 118, 110, 0.2)',
211
- border: '1px solid rgba(15, 118, 110, 0.3)',
212
- color: 'rgba(255,255,255,0.9)'
213
- }}
214
- >
215
- {question}
216
- </button>
217
- ))}
218
- </div>
219
- </div>
220
- )}
221
-
222
  <div className="chat-input-container">
223
  <textarea
224
  className="input chat-input textarea"
 
5
  const [messages, setMessages] = useState([]);
6
  const [currentMessage, setCurrentMessage] = useState('');
7
  const [isLoading, setIsLoading] = useState(false);
 
8
  const messagesEndRef = useRef(null);
9
 
10
  useEffect(() => {
11
  if (selectedDocument) {
12
  setMessages([{
13
  type: 'assistant',
14
+ content: `Je suis prêt à répondre à vos questions sur le document:
15
+ **${selectedDocument.filename}**.
16
+ N'hésitez pas à être précis dans vos questions.`,
17
  timestamp: new Date()
18
  }]);
 
 
19
  } else {
20
  setMessages([{
21
  type: 'assistant',
22
+ content: `Bienvenue dans l'assistant médical RAG CHU.
23
 
24
  **Pour commencer :**
25
  1. Uploadez un document médical (PDF, DOCX, Image)
 
33
  - Consultez la console debug à droite pour suivre l'analyse`,
34
  timestamp: new Date()
35
  }]);
 
 
36
  }
37
  }, [selectedDocument]);
38
 
 
54
  };
55
 
56
  setMessages(prev => [...prev, userMessage]);
57
+ const questionToSend = currentMessage;
58
  setCurrentMessage('');
59
  setIsLoading(true);
60
+
61
+ // ID pour le message assistant (créé seulement quand on a du contenu)
62
+ const assistantMessageId = Date.now();
63
+ let assistantMessageCreated = false;
64
+
65
+ // Timeout de sécurité pour éviter les blocages
66
+ const timeoutId = setTimeout(() => {
67
+ console.warn('Timeout: Réinitialisation forcée de isLoading');
68
+ setIsLoading(false);
69
+ if (assistantMessageCreated) {
70
+ setMessages(prev => prev.map(msg =>
71
+ msg.id === assistantMessageId
72
+ ? { ...msg, content: msg.content || "Timeout - Veuillez réessayer", isStreaming: false }
73
+ : msg
74
+ ));
75
+ }
76
+ }, 30000); // 30 secondes de timeout
77
 
78
  try {
79
+ const response = await fetch('/api/chat/stream', {
80
  method: 'POST',
81
  headers: {
82
  'Content-Type': 'application/json',
83
  },
84
  body: JSON.stringify({
85
+ question: questionToSend,
86
  document_id: selectedDocument.document_id
87
  }),
88
  });
89
 
90
  if (!response.ok) {
91
+ clearTimeout(timeoutId);
92
  let errorMessage = `Erreur HTTP ${response.status}`;
93
  try {
94
  const errorData = await response.json();
 
100
  throw new Error(errorMessage);
101
  }
102
 
103
+ const reader = response.body.getReader();
104
+ const decoder = new TextDecoder();
105
+ let buffer = '';
106
+ let streamEnded = false;
107
+
108
  try {
109
+ while (true) {
110
+ const { done, value } = await reader.read();
111
+ if (done) {
112
+ streamEnded = true;
113
+ break;
114
+ }
115
+
116
+ buffer += decoder.decode(value, { stream: true });
117
+ const lines = buffer.split('\n');
118
+ buffer = lines.pop();
119
+
120
+ for (const line of lines) {
121
+ if (line.startsWith('data: ')) {
122
+ try {
123
+ const data = JSON.parse(line.slice(6));
124
+
125
+ switch (data.type) {
126
+ case 'start':
127
+ console.log('🚀 Début streaming:', data.message);
128
+ break;
129
+
130
+ case 'chunk':
131
+ // Créer le message assistant au premier chunk seulement
132
+ if (!assistantMessageCreated && data.content) {
133
+ const initialAssistantMessage = {
134
+ id: assistantMessageId,
135
+ type: 'assistant',
136
+ content: data.content,
137
+ timestamp: new Date(),
138
+ isStreaming: true
139
+ };
140
+ setMessages(prev => [...prev, initialAssistantMessage]);
141
+ assistantMessageCreated = true;
142
+ } else if (assistantMessageCreated) {
143
+ // Mettre à jour le message assistant avec le nouveau chunk
144
+ setMessages(prev => prev.map(msg =>
145
+ msg.id === assistantMessageId
146
+ ? { ...msg, content: msg.content + data.content }
147
+ : msg
148
+ ));
149
+ }
150
+ break;
151
+
152
+ case 'end':
153
+ console.log('✅ Fin streaming:', data.message);
154
+ streamEnded = true;
155
+ if (assistantMessageCreated) {
156
+ // Marquer le message comme terminé
157
+ setMessages(prev => prev.map(msg =>
158
+ msg.id === assistantMessageId
159
+ ? { ...msg, isStreaming: false }
160
+ : msg
161
+ ));
162
+ }
163
+ break;
164
+
165
+ case 'error':
166
+ console.error('❌ Erreur streaming:', data.message);
167
+ streamEnded = true;
168
+ if (!assistantMessageCreated) {
169
+ // Créer un message d'erreur
170
+ const errorMessage = {
171
+ id: assistantMessageId,
172
+ type: 'assistant',
173
+ content: `Erreur: ${data.message}`,
174
+ timestamp: new Date(),
175
+ isStreaming: false
176
+ };
177
+ setMessages(prev => [...prev, errorMessage]);
178
+ assistantMessageCreated = true;
179
+ } else {
180
+ setMessages(prev => prev.map(msg =>
181
+ msg.id === assistantMessageId
182
+ ? { ...msg, content: `Erreur: ${data.message}`, isStreaming: false }
183
+ : msg
184
+ ));
185
+ }
186
+ break;
187
+ }
188
+ } catch (e) {
189
+ console.error('Erreur parsing JSON:', e);
190
+ }
191
+ }
192
+ }
193
+ }
194
+ } finally {
195
+ // S'assurer que le stream est marqué comme terminé
196
+ if (!streamEnded && assistantMessageCreated) {
197
+ setMessages(prev => prev.map(msg =>
198
+ msg.id === assistantMessageId
199
+ ? { ...msg, isStreaming: false }
200
+ : msg
201
+ ));
202
+ }
203
  }
204
 
 
 
 
 
 
 
 
 
 
205
  } catch (error) {
206
  console.error('Erreur chat:', error);
207
+ if (!assistantMessageCreated) {
208
+ // Créer un message d'erreur
209
+ const errorMessage = {
210
+ id: assistantMessageId,
211
+ type: 'assistant',
212
+ content: `Erreur : ${error.message}`,
213
+ timestamp: new Date(),
214
+ isStreaming: false
215
+ };
216
+ setMessages(prev => [...prev, errorMessage]);
217
+ } else {
218
+ // Remplacer le message assistant par l'erreur
219
+ setMessages(prev => prev.map(msg =>
220
+ msg.id === assistantMessageId
221
+ ? { ...msg, content: `Erreur : ${error.message}`, isStreaming: false }
222
+ : msg
223
+ ));
224
+ }
225
  } finally {
226
+ clearTimeout(timeoutId);
227
  setIsLoading(false);
228
  }
229
  };
 
242
  });
243
  };
244
 
 
 
 
 
 
 
 
 
245
  return (
246
  <div className="card">
247
  <h2>{selectedDocument ? `Chat avec ${selectedDocument.filename}` : 'Assistant Médical'}</h2>
248
 
249
  <div className="chat-container">
250
  <div className="chat-messages">
251
+ {messages
252
+ .filter(message => message.content && message.content.trim() !== '')
253
+ .map((message, index) => (
254
  <div
255
+ key={message.id || index}
256
  className={`message ${message.type === 'user' ? 'message-user' : 'message-assistant'}`}
257
  >
258
  <div className="message-content">
259
  {message.type === 'user' ? (
260
  <p>{message.content}</p>
261
  ) : (
262
+ <div>
263
+ <ReactMarkdown>{message.content}</ReactMarkdown>
264
+ </div>
265
  )}
266
  </div>
267
 
 
 
268
  <div style={{ fontSize: '0.7rem', opacity: 0.6, marginTop: '0.5rem' }}>
269
  {formatTimestamp(message.timestamp)}
270
  </div>
 
274
  {isLoading && (
275
  <div className="message message-assistant">
276
  <div className="message-content">
277
+ <div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
278
+ <div className="medical-spinner"></div>
279
+ <span style={{
280
+ fontSize: '0.9rem',
281
+ color: 'rgba(255,255,255,0.9)',
282
+ fontWeight: '400'
283
+ }}>
284
+ Consultation des protocoles de soins
285
+ </span>
286
+ </div>
287
  </div>
288
  </div>
289
  )}
 
291
  <div ref={messagesEndRef} />
292
  </div>
293
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
294
  <div className="chat-input-container">
295
  <textarea
296
  className="input chat-input textarea"