Sidoineko commited on
Commit
2991cb4
·
verified ·
1 Parent(s): a135d71

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +251 -453
src/streamlit_app.py CHANGED
@@ -12,6 +12,7 @@ from io import BytesIO # Pour lire les données binaires de l'image
12
  import google.generativeai as genai
13
 
14
  # Import DuckDuckGo Search (for web search feature)
 
15
  from duckduckgo_search import DDGS # Importe la classe DDGS
16
 
17
  # -----------------------------------------------------------------------------
@@ -29,7 +30,7 @@ HF_BASE_API_URL = "https://api-inference.huggingface.co/models/"
29
  # -----------------------------------------------------------------------------
30
  # Structure: {'id': 'model_id', 'name': 'Display Name', 'provider': 'huggingface'/'google', 'type': 'text'/'t2i', 'params': {...} }
31
  AVAILABLE_MODELS = [
32
- # --- Google Text Models (Only keeping 1.5 Flash/Pro as requested) ---
33
  {
34
  "id": "gemini-1.5-flash-latest",
35
  "name": "Gemini 1.5 Flash (Google)",
@@ -41,27 +42,39 @@ AVAILABLE_MODELS = [
41
  "top_p": 0.9,
42
  },
43
  },
44
- {
45
- "id": "gemini-1.5-pro-latest",
46
- "name": "Gemini 1.5 Pro (Google)",
47
  "provider": "google",
48
- "type": "text",
49
  "params": {
50
- "max_new_tokens": 200,
51
- "temperature": 0.6,
52
- "top_p": 0.9,
53
  },
54
  },
55
- # Removed: Mistral (Hugging Face Text)
56
- # Removed: Stable Diffusion XL Base 1.0 (Hugging Face T2I)
 
 
 
 
 
 
 
 
 
 
 
 
57
  ]
58
 
59
- # Separate models lists by type (still useful for default selection and filtering params)
60
  MODELS_BY_TYPE = {m_type: [m for m in AVAILABLE_MODELS if m['type'] == m_type] for m_type in set(m['type'] for m in AVAILABLE_MODELS)}
61
 
62
  # Default task is now fixed to 'text'
63
  DEFAULT_TASK = 'text'
64
- # Find the first text model as default (will be Gemini 1.5 Flash now due to ordering)
65
  first_text_model = next((m for m in AVAILABLE_MODELS if m['type'] == DEFAULT_TASK), None)
66
  if first_text_model:
67
  DEFAULT_MODEL_ID = first_text_model['id']
@@ -127,6 +140,7 @@ if '_prev_model_id_before_selectbox' not in st.session_state:
127
  if 'enable_web_search' not in st.session_state:
128
  st.session_state.enable_web_search = False
129
 
 
130
  # -----------------------------------------------------------------------------
131
  # Helper pour formater les exports (Adapté pour images)
132
  # -----------------------------------------------------------------------------
@@ -155,9 +169,9 @@ def format_history_to_json(chat_history: list[dict]) -> str:
155
 
156
  if export_msg["type"] == "text":
157
  export_msg["content"] = message.get("content", "")
158
- elif export_msg["type"] == "t2i_prompt": # User prompt for T2I
159
  export_msg["content"] = message.get("content", "")
160
- elif export_msg["type"] == "t2i" and "prompt" in message: # AI image response
161
  export_msg["prompt"] = message["prompt"]
162
  export_msg["image_placeholder"] = "(Image non incluse dans l'export JSON)" # Indicate image was here
163
  if "content" in message: # Include error message if it was an error T2I response
@@ -181,9 +195,9 @@ def format_history_to_md(chat_history: list[dict]) -> str:
181
  lines.append(f"### {role_label}\n\n")
182
  if content_type == "text":
183
  lines.append(f"{message.get('content', '')}\n\n")
184
- elif content_type == "t2i_prompt":
185
  lines.append(f"*Prompt image:* {message.get('content', '')}\n\n")
186
- elif content_type == "t2i": # AI image response
187
  prompt_text = message.get('prompt', 'Pas de prompt enregistré')
188
  error_text = message.get('content', '') # Potential error message
189
  lines.append(f"*Image générée (prompt: {prompt_text})*\n")
@@ -200,195 +214,55 @@ def format_history_to_md(chat_history: list[dict]) -> str:
200
  # -----------------------------------------------------------------------------
201
  # LLM API helper (Unified call logic)
202
  # -----------------------------------------------------------------------------
203
- # This function is specifically for text prompt building for Mistral-style models
204
- def build_mistral_prompt(system_message: str, chat_history_for_prompt: list[dict]) -> str:
205
- """
206
- Builds a prompt string suitable for Mistral-style instruction models using the
207
- chat history (text messages only).
208
- Assumes chat_history_for_prompt contains the messages relevant
209
- to the model turns, starting with the first actual user turn (potentially including system)
210
- and ending with the current user message.
211
- """
212
- formatted_prompt = ""
213
- system_message_added = False
214
-
215
- # Only include text messages in history for prompt building
216
- # Filter out non-text messages and the initial assistant starter message if it's not the only one
217
- text_chat_history = [
218
- msg for msg in chat_history_for_prompt
219
- if msg.get("type", "text") == "text" # Only include messages explicitly typed as 'text'
220
- and not (msg['role'] == 'assistant' and msg['content'] == st.session_state.starter_message and len(chat_history_for_prompt) > 1)
221
- ]
222
-
223
-
224
- for message in text_chat_history:
225
- if message["role"] == "user":
226
- if not system_message_added and system_message:
227
- formatted_prompt += f"<s>[INST] <<SYS>>\n{system_message}\n<</SYS>>\n\n{message['content']} [/INST]"
228
- system_message_added = True
229
- else:
230
- # If system message already added or not present, just add the user turn
231
- # If the previous turn was assistant, this starts a new user turn
232
- if formatted_prompt.endswith("</s>"):
233
- formatted_prompt += f"<s>[INST] {message['content']} [/INST]"
234
- else:
235
- # This case might happen if the very first message is user and no system message,
236
- # or if there's an unexpected sequence. Append directly within INST.
237
- # This might not be the perfect Mistral format for all cases but handles simple turns.
238
- formatted_prompt += f"<s>[INST] {message['content']} [/INST]"
239
-
240
-
241
- elif message["role"] == "assistant":
242
- # Add assistant response directly after the corresponding user INST block
243
- # Ensure the prompt ends with [/INST] before adding assistant response
244
- if formatted_prompt.endswith("[/INST]"):
245
- formatted_prompt += f" {message['content']}</s>"
246
- # else: If the prompt doesn't end with [/INST], the sequence is likely wrong
247
- # We will skip this assistant message for prompt building.
248
-
249
-
250
- return formatted_prompt
251
-
252
 
253
  def call_hf_inference(model_id: str, payload_inputs: any, params: dict, model_type: str) -> any:
254
  """
255
  Calls the Hugging Face Inference API for either text generation or text-to-image.
256
- payload_inputs is the main input (string for text, string for t2i prompt).
257
- params depends on the model_type.
258
- Returns string for text, bytes for image, or error string.
259
  """
260
  if not HUGGINGFACEHUB_API_TOKEN:
261
  return "Erreur d'API Hugging Face: Le token HUGGINGFACEHUB_API_TOKEN est introuvable."
262
 
263
  headers = {"Authorization": f"Bearer {HUGGINGFACEHUB_API_TOKEN}"}
264
- url = f"{HF_BASE_API_URL}{model_id}" # Corrected URL to include model_id
265
-
266
- payload = {}
267
- response_parser = None
268
 
 
 
269
  if model_type == 'text':
270
- payload = {
271
- "inputs": payload_inputs, # The prompt string built elsewhere
272
- "parameters": {
273
- "max_new_tokens": params.get("max_new_tokens", 200),
274
- "temperature": params.get("temperature", 0.6),
275
- "top_p": params.get("top_p", 0.9),
276
- "return_full_text": False,
277
- "num_return_sequences": 1,
278
- "do_sample": params.get("temperature", 0.6) > 1e-2, # Do sampling if temperature > 0
279
- },
280
- "options": {"wait_for_model": True, "use_cache": False}
281
- }
282
- def parse_text_response(response_data):
283
- if isinstance(response_data, list) and response_data and "generated_text" in response_data[0]:
284
- return response_data[0]["generated_text"].strip()
285
- else:
286
- return f"Erreur API Hugging Face (Format): Réponse texte inattendue - {response_data}"
287
- response_parser = parse_text_response
288
- response_is_json = True # Expected response format
289
 
290
  elif model_type == 't2i':
291
- payload = {
292
- "inputs": payload_inputs, # The prompt string (user input)
293
- "parameters": {
294
- "negative_prompt": params.get("negative_prompt", ""),
295
- "num_inference_steps": params.get("num_inference_steps", 50),
296
- "guidance_scale": params.get("guidance_scale", 7.5),
297
- "height": params.get("image_height", 512),
298
- "width": params.get("image_width", 512),
299
- },
300
- "options": {"wait_for_model": True, "use_cache": False}
301
- }
302
- def parse_t2i_response(response):
303
- # Response for T2I is binary image data
304
- if response.content:
305
- return response.content # Return bytes
306
- else:
307
- return f"Erreur API Hugging Face (T2I): Réponse image vide ou inattendue."
308
- response_parser = parse_t2i_response
309
- response_is_json = False # Expected response format is binary/image
310
 
311
  else:
312
  return f"Erreur interne: Type de modèle Hugging Face '{model_type}' inconnu."
313
 
314
 
315
- if response_parser is None: # Should not happen if block above is complete
316
- return f"Erreur interne: Le type de modèle '{model_type}' n'a pas de parseur de réponse défini."
317
-
318
- try:
319
- # For T2I, `json` parameter in requests post is not used, `data` is used with the raw prompt bytes.
320
- # For text, `json` is used.
321
- if model_type == 'text':
322
- response = requests.post(url, headers=headers, json=payload, timeout=300)
323
- elif model_type == 't2i':
324
- # `inputs` is the prompt string in payload, but for HF Inference API for T2I,
325
- # sometimes the prompt is passed as raw `data` in the request body.
326
- # However, the structure of payload for text-to-image is typically JSON as well.
327
- # The API for stable-diffusion-xl-base-1.0 expects a JSON payload.
328
- response = requests.post(url, headers=headers, json=payload, timeout=300)
329
-
330
- response.raise_for_status() # Raise an HTTPError for bad responses (4xx or 5xx)
331
-
332
- # Check content type based on expected format
333
- if response_is_json:
334
- # Expecting JSON (e.g., text models)
335
- if 'application/json' in response.headers.get('Content-Type', '').lower():
336
- response_data = response.json()
337
- return response_parser(response_data)
338
- else:
339
- # Received non-JSON when JSON was expected
340
- return f"Erreur API Hugging Face: Type de contenu inattendu ({response.headers.get('Content-Type', 'N/A')}) - attendu JSON.\nResponse content: {response.text[:500]}..." # Include some response text for debugging
341
- else: # Expecting binary/image (e.g., t2i models)
342
- if 'image/' in response.headers.get('Content-Type', '').lower() or 'binary' in response.headers.get('Content-Type', '').lower() or ('application/json' not in response.headers.get('Content-Type', '').lower() and response.content):
343
- return response_parser(response) # Pass the full response object or just content if parser needs it
344
- elif 'application/json' in response.headers.get('Content-Type', '').lower():
345
- # Sometimes errors come back as JSON even for binary endpoints
346
- error_detail = response.json()
347
- error_message = error_detail.get('error', str(error_detail))
348
- if isinstance(error_message, list): error_message = ". ".join(error_message)
349
- estimated_time = error_detail.get('estimated_time', None)
350
- if estimated_time:
351
- return f"Erreur API Hugging Face ({response.status_code}): {error_message}. Le modèle est peut-être en chargement, veuillez réessayer dans environ {_format_time(estimated_time)}."
352
- return f"Erreur API Hugging Face ({response.status_code}): {error_message}"
353
- else:
354
- # Received unexpected content type
355
- return f"Erreur API Hugging Face: Type de contenu inattendu ({response.headers.get('Content-Type', 'N/A')}) - attendu image/binary.\nResponse content: {response.text[:500]}..." # Include some response text for debugging
356
 
 
 
 
357
 
358
- except requests.exceptions.Timeout:
359
- return "Erreur API Hugging Face: Délai d'attente dépassé pour la requête (300s). Le modèle est peut-être lent à charger. Veuillez réessayer."
360
- except requests.exceptions.ConnectionError as e:
361
- return f"Erreur API Hugging Face: Impossible de se connecter à l'API. Détails: {e}"
362
- except requests.exceptions.HTTPError as e:
363
- # Try to parse error details if available, otherwise display status code and text
364
- try:
365
- if 'application/json' in e.response.headers.get('Content-Type', '').lower():
366
- error_detail = e.response.json()
367
- error_message = error_detail.get('error', str(error_detail))
368
- if isinstance(error_message, list): error_message = ". ".join(error_message)
369
- estimated_time = error_detail.get('estimated_time', None)
370
- if estimated_time:
371
- return f"Erreur API Hugging Face ({e.response.status_code}): {error_message}. Le modèle est peut-être en chargement, veuillez réessayer dans environ {_format_time(estimated_time)}."
372
- return f"Erreur API Hugging Face ({e.response.status_code}): {error_message}"
373
- else:
374
- # Non-JSON error response
375
- return f"Erreur API Hugging Face ({e.response.status_code}): {e.response.text[:500]}..." # Limit raw text length
376
-
377
- except Exception: # Catch any error during JSON decoding or key access
378
- return f"Erreur API Hugging Face ({e.response.status_code}): Impossible d'obtenir les détails de l'erreur. Réponse brute: {e.response.text[:200]}..." # Limit raw text length
379
- except Exception as e:
380
- return f"Erreur inconnue lors de l'appel API Hugging Face: {e}"
381
 
382
 
383
  def call_google_api(model_id: str, system_message: str, chat_history_for_api: list[dict], params: dict) -> str:
384
  """
385
  Calls the Google Generative AI API (Text models only).
386
- Note: The system_message parameter is included here for consistency,
387
- but it is NOT passed to generate_content to avoid the 'unexpected keyword argument' error.
388
- This means the system message is currently NOT applied to Google models.
389
  """
390
  if not GOOGLE_API_KEY:
391
- return "Erreur d'API Google: Le token GOOGLE_API_KEY est introuvable."
392
 
393
  try:
394
  genai.configure(api_key=GOOGLE_API_KEY)
@@ -400,45 +274,31 @@ def call_google_api(model_id: str, system_message: str, chat_history_for_api: li
400
 
401
  # Prepare history for the Gemini API
402
  # The standard format is a list of dicts: [{'role': 'user', 'parts': [...]}, {'role': 'model', 'parts': ...]}]
403
- # `chat_history_for_api` here is the list of messages relevant to the model turns (filtered by text type and excluding initial assistant message).
404
  gemini_history_parts = []
405
 
406
- # === IMPORTANT ===
407
- # The 'system_instruction' parameter has been removed from generate_content
408
- # to fix the 'unexpected keyword argument' error you reported.
409
- # This means the system_message defined in the sidebar will NOT be used
410
- # by Google models with this version of the code.
411
- # If 'system_instruction' becomes supported by your model version in the future,
412
- # you can re-add it here: system_instruction=system_message if system_message else None
413
- # But for now, we pass only contents and generation_config.
414
 
415
  for msg in chat_history_for_api:
416
- # Only include text messages in history sent to Google Text models
417
- # We assume chat_history_for_api already contains only text messages for this function.
418
- if msg.get("type", "text") == "text": # Double check type safety
419
  # Map roles: Streamlit 'user' -> Google 'user', Streamlit 'assistant' -> Google 'model'
420
  role = 'user' if msg['role'] == 'user' else 'model'
421
  gemini_history_parts.append({"role": role, "parts": [msg['content']]})
422
- # Note: Gemini can handle multimodal (`parts` can contain text/image/etc.),
423
- # but this specific `call_google_api` function is currently designed
424
- # for text-only history to ensure compatibility with text models.
425
 
426
  generation_config = genai.types.GenerationConfig(
427
  max_output_tokens=params.get("max_new_tokens", 200),
428
  temperature=params.get("temperature", 0.6),
429
  top_p=params.get("top_p", 0.9),
430
- # top_k=params.get("top_k", None), # Gemini doesn't use top_k directly in config, but is part of nucleus sampling via top_p
431
  )
432
 
433
- # Call generate_content *without* the 'system_instruction' argument to fix the error
434
  response = model.generate_content(
435
  contents=gemini_history_parts,
436
  generation_config=generation_config,
437
- request_options={'timeout': 180} # Increased timeout slightly
438
  )
439
 
440
  # Process the response
441
- # Access response text safely, handling potential empty responses or blocks
442
  if response.candidates:
443
  if response.candidates[0].content and response.candidates[0].content.parts:
444
  generated_text = "".join(part.text for part in response.candidates[0].content.parts)
@@ -454,7 +314,7 @@ def call_google_api(model_id: str, system_message: str, chat_history_for_api: li
454
  return f"API Google: Votre message a été bloqué ({response.prompt_feedback.block_reason}). Raison détaillée: {response.prompt_feedback.safety_ratings}"
455
  else:
456
  # No candidates and no block feedback - unknown error
457
- return f"Erreur API Google: Aucune réponse générée pour une raison inconnue. Debug info: {response}"
458
 
459
  except Exception as e:
460
  # Catch any other exceptions during the API call
@@ -466,7 +326,7 @@ def _format_time(seconds):
466
  if not isinstance(seconds, (int, float)) or seconds < 0: return "N/A"
467
  minutes = int(seconds // 60)
468
  remaining_seconds = int(seconds % 60)
469
- if minutes > 0: return f"{minutes} min {remaining_seconds} sec" # Corrected string formatting
470
  return f"{remaining_seconds} sec"
471
 
472
  # -----------------------------------------------------------------------------
@@ -478,7 +338,9 @@ def perform_web_search(query: str, num_results: int = 3) -> str:
478
  # Use DDGS.text for simple text search (DDGS class)
479
  # It's best practice to use DDGS as a context manager to ensure proper session closure
480
  with DDGS() as ddgs:
481
- results = ddgs.text(keywords=query, max_results=num_results)
 
 
482
  # Convert generator to list for iteration
483
  results_list = list(results)
484
 
@@ -495,13 +357,13 @@ def perform_web_search(query: str, num_results: int = 3) -> str:
495
  return f"Erreur lors de la recherche web: {e}"
496
 
497
  # -----------------------------------------------------------------------------
498
- # Generation Functions (Separated by Task Type)
499
  # -----------------------------------------------------------------------------
500
 
501
  def get_text_response(selected_model_id: str, system_prompt: str, full_chat_history: list[dict]):
502
  """
503
  Handles text generation request using the selected text model.
504
- Requires the selected model to be of type 'text'.
505
  """
506
  # Model type compatibility is checked by the caller before calling this function.
507
  selected_model_info = next((m for m in AVAILABLE_MODELS if m['id'] == selected_model_id), None)
@@ -540,61 +402,60 @@ def get_text_response(selected_model_id: str, system_prompt: str, full_chat_hist
540
  if st.session_state.enable_web_search and model_provider == 'google':
541
  last_user_query = full_chat_history[-1]['content'] # Get the user's last actual query
542
 
543
- # Add a temporary message to the chat indicating search is happening
544
- # This message will be updated later.
545
  st.session_state.chat_history.append({"role": "assistant", "content": f"*(KolaChatBot recherche sur le web pour '{last_user_query}'...)*", "type": "text"})
546
- # No rerun here. The main spinner will now handle the visual update implicitly.
547
-
548
- # Perform the actual web search
549
- search_results = perform_web_search(last_user_query)
550
-
551
- # Create a copy of the history for the API call to avoid modifying session_state directly for previous turns.
552
- temp_api_history = []
553
- for msg in actual_conversation_history_for_prompt:
554
- temp_api_history.append(msg.copy()) # Make sure to copy the dict
555
-
556
- # Prepend search results to the *last user message* in the temp history
557
- context_message_content = f"Voici des informations pertinentes du web sur le sujet de la dernière question :\n```\n{search_results}\n```\n\nEn te basant sur ces informations (si pertinentes) et notre conversation, réponds à la question : "
558
 
559
- if temp_api_history and temp_api_history[-1]['role'] == 'user' and temp_api_history[-1]['type'] == 'text':
560
- temp_api_history[-1]['content'] = context_message_content + temp_api_history[-1]['content']
561
- else:
562
- # Fallback: add context as a separate user message if last message isn't a simple user text
563
- temp_api_history.append({"role": "user", "content": context_message_content, "type": "text"})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
564
 
565
- # Call the Google API with the modified history
566
- response_content = call_google_api(model_id, system_prompt, temp_api_history, params)
567
-
568
- # Update the search message in chat history to indicate results were used
569
- # We need to find the search message we added earlier.
570
- # A simple way is to check the last message's content if it matches our pattern.
571
- # This update will be visible after the next rerun when the assistant's main response is displayed.
572
- if st.session_state.chat_history[-1]["content"].startswith("*(KolaChatBot recherche web"):
573
- st.session_state.chat_history[-1]["content"] = f"*(KolaChatBot a terminé la recherche web pour '{last_user_query}'. Résultats intégrés à la réponse.)*"
574
 
575
  else: # No web search or not a Google model
576
- if model_provider == 'huggingface':
577
- llm_prompt = build_mistral_prompt(system_prompt, actual_conversation_history_for_prompt)
578
- if not llm_prompt: return {"role": "assistant", "content": "Erreur lors de la construction du prompt pour Hugging Face (texte).", "type": "text"}
579
- response_content = call_hf_inference(model_id, llm_prompt, params, selected_model_info['type'])
580
- elif model_provider == 'google':
581
  # This path is for Google models WITHOUT web search enabled
582
  response_content = call_google_api(model_id, system_prompt, actual_conversation_history_for_prompt, params)
583
  else:
584
- response_content = f"Erreur interne: Fournisseur API '{model_provider}' inconnu pour le modèle texte '{model_id}'."
 
 
585
 
586
  return {"role": "assistant", "content": response_content, "type": "text"}
587
 
588
 
 
589
  def get_image_response(selected_model_id: str, user_prompt: str):
590
- """
591
- Handles image generation request using the selected T2I model.
592
- This function is now effectively disabled by the app's fixed 'text' task.
593
- """
594
- # Model type compatibility is checked by the caller before calling this function.
595
- # This function is here for structural completeness but won't be executed in this config.
596
- st.warning("La tâche 'Image' n'est plus supportée dans cette configuration de l'application.")
597
- return {"role": "assistant", "content": "Cette application est configurée pour la génération de texte uniquement. La génération d'images n'est pas disponible.", "type": "text", "prompt": user_prompt}
598
 
599
 
600
  # -----------------------------------------------------------------------------
@@ -615,103 +476,96 @@ st.markdown(f"*Tâche actuelle : **{current_task_label}***\n*Modèle : **`{selec
615
 
616
 
617
  # -----------------------------------------------------------------------------
618
- # Manuel d'utilisation (Update with task separation)
619
  # -----------------------------------------------------------------------------
620
  with st.expander("📖 Manuel d'utilisation de KolaChatBot", expanded=False):
621
  st.markdown("""
622
- Bienvenue sur KolaChatBot - Une application de chat IA multi-modèles ! Voici comment tirer le meilleur parti de notre assistant IA :
623
 
624
  **1. Comment interagir ?**
625
  - **La tâche est fixée sur Texte (conversation).**
626
  - **Entrer votre prompt :** Tapez votre message dans la zone de texte en bas et appuyez sur Entrée.
627
 
628
  **2. Paramètres dans la barre latérale (Sidebar) :**
629
- La barre latérale adapte ses options au modèle de texte sélectionné.
630
 
631
- * **Sélection du Modèle :** Une liste déroulante affichant les modèles disponibles. Choisissez un modèle de texte pour converser.
632
- - **Important :** Assurez-vous que les tokens API (`HUGGINGFACEHUB_API_TOKEN` ou `GOOGLE_API_KEY`) nécessaires au fournisseur du modèle sélectionné sont configurés dans votre fichier `.env` ou comme secrets dans votre espace Hugging Face.
633
- - Changer de modèle **ne réinitialise pas** automatiquement les paramètres de créativité/longueur. Les paramètres s'appliquent lors d'une nouvelle conversation.
634
  * **Option "Activer la recherche web" :** Activez cette option pour que l'IA (via les modèles Google Gemini) recherche sur le web et utilise les résultats pour enrichir ses réponses.
635
- * **Paramètres spécifiques au Modèle :** Les options de paramètres affichées changent en fonction du modèle sélectionné (uniquement des modèles texte ici).
636
- * **Pour un modèle "Texte" :** Message Système / Personnalité, Max New Tokens (longueur max réponse), Temperature (créativité), Top-P (sampling).
637
- - **Attention :** Modifier les paramètres spécifiques et cliquer sur "Appliquer Paramètres & Nouvelle Conversation" **efface** également l'historique/conversation.
638
  * **Sélection des Avatars.**
639
- * **Gestion de la Conversation :** Boutons pour appliquer les paramètres actuels (et démarrer une nouvelle conversation) ou simplement effacer l'historique actuel.
640
- * **Exporter la Conversation :** Téléchargez l'historique. Notez que les images générées ne sont pas incluses dans les exports TXT et Markdown, seul le prompt l'est.
641
 
642
  **3. Limitations :**
643
- - **L'application est configurée uniquement pour la tâche de génération de texte.** Si vous sélectionnez un modèle d'image (non recommandé dans cette configuration), toute tentative de génération entraînera une erreur de compatibilité affichée dans le chat. Vous devez utiliser un modèle de texte.
644
- - La fonction "Recherche Web" est actuellement implémentée uniquement pour les modèles Google Gemini et peut entraîner des délais supplémentaires.
645
  - Notez que l'application de votre **Message Système / Personnalité** aux modèles Google dépend de la prise en charge de la fonctionnalité `system_instruction` par la version de la bibliothèque Google Generative AI et le modèle spécifique utilisé. Si vous obtenez des erreurs (`unexpected keyword argument 'system_instruction'`), le message système sera ignoré pour ces modèles.
646
- - Cette application ne stocke pas les images de manière permanente. Elles sont présentes dans l'historique de session tant que l'application tourne.
647
- - Les modèles gratuits sur l'API Hugging Face peuvent avoir des files d'attente ou des limites d'utilisation.
648
 
649
  Amusez-vous bien avec KolaChatBot !
650
  """)
651
  # -----------------------------------------------------------------------------
652
- # Sidebar settings (with task, model, and param separation)
653
  # -----------------------------------------------------------------------------
654
  with st.sidebar:
655
  st.header("🛠️ Configuration de KolaChatBot")
656
 
657
- # Removed the radio button for task selection. Task is now fixed to 'text'.
658
- st.subheader("🎯 Tâche IA : Texte (fixée)") # Indicate that task is fixed
659
- # The st.session_state.selected_task is already initialized to 'text' and not changed here.
660
 
661
  st.subheader("🧠 Sélection du Modèle")
662
  all_available_models = AVAILABLE_MODELS
663
 
664
- if not all_available_models: # Check if ANY models are defined
665
  st.warning("Aucun modèle disponible défini dans la liste AVAILABLE_MODELS.")
666
- st.session_state.selected_model_id = None # Ensure no model is selected
667
  else:
668
  model_options = {model['id']: model['name'] for model in all_available_models}
669
 
670
- # Determine the index of the currently selected model ID within the list of ALL model options
671
- # If the current ID is not in the list, default to the first option
672
  current_model_index = 0
673
  if st.session_state.selected_model_id in model_options:
674
  current_model_index = list(model_options.keys()).index(st.session_state.selected_model_id)
675
- elif all_available_models: # If no match, default to first available
676
- st.session_state.selected_model_id = all_available_models[0]['id']
677
- current_model_index = 0
 
 
 
 
 
678
 
679
- # Store the value of selected_model_id *before* the selectbox potentially changes it
680
  st.session_state._prev_model_id_before_selectbox = st.session_state.selected_model_id
681
- # The selectbox's value will update st.session_state.selected_model_id because of the 'key'
682
  selected_model_id_from_selectbox_value = st.selectbox(
683
  "Choisir le modèle :",
684
  options=list(model_options.keys()),
685
  format_func=lambda x: model_options[x],
686
  index=current_model_index,
687
- key="selected_model_id", # Directly link to session state variable
688
- help="Sélectionnez un modèle disponible. Pour cette application, seul un modèle de texte sera fonctionnel."
689
  )
690
- # st.session_state.selected_model_id is now updated automatically by the key="selected_model_id"
691
 
692
 
693
  # --- Model Change Detection (without auto-resetting params) ---
694
- # We still need to detect if the model changed to potentially reset history
695
- # and to update which parameters are displayed in the expander.
696
  model_id_changed_this_run = st.session_state.selected_model_id != st.session_state.get('_prev_model_id_before_selectbox')
697
-
698
- # Task is fixed, so task_changed_this_run is always False
699
- task_changed_this_run = False # Explicitly set to False as radio button is removed.
700
 
701
  # If model changed (and not triggered by an explicit button reset), reset history
702
- # Parameters are no longer auto-reset here. They only reset on explicit button click.
703
  if model_id_changed_this_run and not st.session_state.get('_reset_triggered', False):
704
  st.session_state.chat_history = [{"role": "assistant", "content": st.session_state.starter_message, "type": "text"}]
705
  st.info(f"⚠️ Modèle changé. La conversation a été réinitialisée. Les paramètres restent les mêmes jusqu'à application explicite.")
706
- # Do not set _reset_triggered here, as it's not a full settings reset.
707
- st.rerun() # Rerun to refresh UI after history reset.
 
708
 
709
- # Check required API Key for the selected model (This check runs after a potential rerun)
710
- if st.session_state.selected_model_id: # Only check if a model ID is actually selected
711
  current_model_info_check = next((m for m in AVAILABLE_MODELS if m['id'] == st.session_state.selected_model_id), None)
712
  if current_model_info_check:
713
  current_model_provider = current_model_info_check['provider']
714
- if current_model_provider == 'huggingface':
715
  if not HUGGINGFACEHUB_API_TOKEN:
716
  st.warning("❌ Le token Hugging Face est manquant (`HUGGINGFACEHUB_API_TOKEN`). Les modèles Hugging Face ne fonctionneront pas.")
717
  elif current_model_provider == 'google':
@@ -721,20 +575,17 @@ with st.sidebar:
721
 
722
  # --- Dynamic Parameter Settings based on the SELECTED MODEL's TYPE ---
723
  st.subheader("⚙️ Paramètres")
724
- # Get the info for the *currently selected* model to determine which parameters to show
725
  current_selected_model_info = next((m for m in AVAILABLE_MODELS if m['id'] == st.session_state.selected_model_id), None)
726
 
727
  if st.session_state.selected_model_id and current_selected_model_info:
728
- # Use expander title to indicate which model's params are shown
729
- expander_title = f"Ajuster Paramètres : {current_selected_model_info['type'].capitalize()} ({current_selected_model_info['name']})"
730
  with st.expander(expander_title, expanded=True):
 
731
  if current_selected_model_info['type'] == 'text':
732
- # Link input widgets directly to session state parameters
733
- # Max New Tokens: Increased max_value to 8192
734
  st.session_state.max_response_length = st.number_input(
735
  "Max New Tokens (longueur max réponse) :",
736
- min_value=20, max_value=8192,
737
- value=st.session_state.max_response_length, # No None check here, as it's initialized on startup
738
  step=10,
739
  key="max_new_tokens_input",
740
  help="Longueur maximale de la réponse de l'IA (en jetons ou tokens).",
@@ -742,7 +593,7 @@ with st.sidebar:
742
  st.session_state.temperature = st.slider(
743
  "Temperature (créativité) :",
744
  min_value=0.0, max_value=2.0,
745
- value=st.session_state.temperature, # No None check here
746
  step=0.01,
747
  key="temperature_input",
748
  help="Contrôle le caractère aléatoire des réponses. Plus élevé = plus créatif/imprévisible.",
@@ -750,19 +601,18 @@ with st.sidebar:
750
  st.session_state.top_p = st.slider(
751
  "Top-P (sampling) :",
752
  min_value=0.01, max_value=1.0,
753
- value=st.session_state.top_p, # No None check here
754
  step=0.01,
755
  key="top_p_input",
756
  help="Contrôle la diversité en limitant les options de tokens. Plus bas = moins diversifié. 1.0 = désactivé.",
757
  )
758
 
759
- # Handle System and Starter messages here for text task
760
  st.session_state.system_message = st.text_area(
761
  "Message Système / Personnalité :",
762
  value=st.session_state.system_message,
763
  height=100,
764
  key="system_message_input",
765
- help="Décrivez le rôle ou le style que l'IA de texte doit adopter. Sa capacité à suivre cette consigne dépend du modèle. Notez que cette consigne peut être ignorée par certains modèles ou fournisseurs si la fonctionnalité dédiée n'est pas supportée (voir Limitations).",
766
  )
767
  st.session_state.starter_message = st.text_area(
768
  "Message de Bienvenue de l'IA :",
@@ -771,85 +621,44 @@ with st.sidebar:
771
  key="starter_message_input",
772
  help="Le premier message que l'IA affichera au début d'une nouvelle conversation textuelle ou après un reset.",
773
  )
774
- # Web Search Checkbox - only displayed for text models
775
- st.session_state.enable_web_search = st.checkbox(
776
- "Activer la recherche web (pour les modèles Google)",
777
- value=st.session_state.enable_web_search,
778
- key="web_search_checkbox",
779
- help="Si coché, les modèles Google Gemini effectueront une recherche DuckDuckGo et utiliseront les résultats pour répondre à votre prompt."
780
- )
781
-
782
-
783
- elif current_selected_model_info['type'] == 't2i':
784
- # Link input widgets directly to session state T2I parameters
785
- # Display parameters for T2I model even if task is fixed to Text
786
- st.info("Note: Les paramètres d'image sont affichés car ce modèle est de type 'Image'. Cependant, la tâche de l'application est fixée sur 'Texte'. Les prompts images ne généreront pas d'images mais des erreurs de compatibilité.")
787
- st.session_state.num_inference_steps = st.number_input(
788
- "Nombre d'étapes d'inférence :",
789
- min_value=1, max_value=200,
790
- value=st.session_state.num_inference_steps, step=5,
791
- key="num_inference_steps_input",
792
- help="Nombre d'étapes dans le processus de diffusion. Plus élevé = potentiellement meilleure qualité mais plus lent."
793
- )
794
- st.session_state.guidance_scale = st.number_input(
795
- "Échelle de guidage (CFG) :",
796
- min_value=0.0, max_value=20.0,
797
- value=st.session_state.guidance_scale, step=0.5,
798
- key="guidance_scale_input",
799
- help="Dans quelle mesure l'IA doit suivre le prompt. Plus élevé = plus fidèle au prompt mais potentiellement moins créatif."
800
- )
801
- st.session_state.image_height = st.number_input(
802
- "Hauteur de l'image :",
803
- min_value=128, max_value=1024,
804
- value=st.session_state.image_height, step=64,
805
- key="image_height_input",
806
- help="Hauteur en pixels de l'image générée."
807
- )
808
- st.session_state.image_width = st.number_input(
809
- "Largeur de l'image :",
810
- min_value=128, max_value=1024,
811
- value=st.session_state.image_width, step=64,
812
- key="image_width_input",
813
- help="Largeur en pixels de l'image générée."
814
- )
815
- st.session_state.negative_prompt = st.text_area(
816
- "Prompt Négatif :",
817
- value=st.session_state.negative_prompt,
818
- height=100,
819
- key="negative_prompt_input",
820
- help="Ce que vous NE voulez PAS voir dans l'image."
821
- )
822
-
823
- # Add a message if model type is not explicitly handled for parameters
824
- if current_selected_model_info and current_selected_model_info['type'] not in ['text', 't2i']:
825
  st.info(f"Type de modèle sélectionné ('{current_selected_model_info['type']}') inconnu ou non pris en charge pour l'affichage des paramètres.")
826
 
827
- else: # This ELSE correctly pairs with the outer 'if st.session_state.selected_model_id...'
828
  st.info("Aucun modèle sélectionné, les paramètres ne sont pas disponibles.")
829
 
830
 
831
  # Flag used to detect if 'Appliquer Paramètres' or 'Effacer' was clicked
832
  # and prevent regeneration on the immediate rerun
833
- # Reset this flag at the beginning of the sidebar block after potential check
834
- # (already done above, but repeating for clarity if this block were moved)
835
- # reset_just_triggered = st.session_state.get('_reset_triggered', False)
836
- # if reset_just_triggered:
837
- # st.session_state._reset_triggered = False # Clear the flag
838
-
839
-
840
- st.subheader("👤 Interface Utilisateur")
841
- with st.expander("Choisir les Avatars Cyberpunk", expanded=False):
842
- st.markdown("*Sélection des avatars personnalisés :*")
843
- col1_avatar, col2_avatar = st.columns(2)
844
- with col1_avatar:
845
- st.session_state.avatars["assistant"] = st.selectbox("Avatar IA", options=["🤖", "🎨", "✨", "💡", "🌟", "👩🏽‍⚕️", "👨🏿‍🎓", "⚙️"], index=0, key="avatar_ia_select")
846
- with col2_avatar:
847
- st.session_state.avatars["user"] = st.selectbox("Avatar Utilisateur", options=["👤", "👩‍💻", "👨‍🎓", "❓", "💡", "🧑‍🔧", "👩🏽‍🔬", "🕵🏽", "🧑‍🚀"], index=0, key="avatar_user_select")
848
 
849
  st.subheader("🔄 Gestion de la Conversation")
850
  # Reset button that applies *all* parameters (system/starter/model) and starts new conversation
851
- # The help message is adapted since task is fixed.
852
- if st.button("♻️ Appliquer Paramètres & Nouvelle Conversation", type="primary", help=f"Applique les paramètres actuels et démarre une nouvelle conversation texte en effaçant l'historique."):
853
  # Parameters are already updated in session state via key linking
854
  # Reset history.
855
  st.session_state.chat_history = [{"role": "assistant", "content": st.session_state.starter_message, "type": "text"}]
@@ -858,7 +667,7 @@ with st.sidebar:
858
  st.rerun() # Rerun immediately to refresh UI
859
 
860
  # Clear history button - simpler, just history
861
- if st.button("🗑️ Effacer la Conversation Actuelle", help=f"Efface l'historique de conversation mais conserve les paramètres actuels."):
862
  # Reset history.
863
  st.session_state.chat_history = [{"role": "assistant", "content": st.session_state.starter_message, "type": "text"}]
864
  st.info("Conversation actuelle effacée.")
@@ -919,61 +728,58 @@ with st.sidebar:
919
  chat_interface_container = st.container(height=600, border=True)
920
 
921
  with chat_interface_container:
 
 
 
922
  # Display existing messages from chat_history.
923
- # System messages are never displayed. Messages can be text or image.
924
  for message in st.session_state.chat_history:
925
- if message["role"] == "system":
926
- continue # Skip system messages
927
 
928
- avatar_type = st.session_state.avatars.get(message["role"], "❓") # Default avatar
929
 
930
  with st.chat_message(message["role"], avatar=avatar_type):
931
- message_type = message.get("type", "text") # Default to text type
932
  if message_type == "text":
933
- st.markdown(message.get("content", "")) # Display text content
934
- elif message_type == "t2i_prompt": # User prompt for T2I task
935
- # Display user's prompt for an image generation request
936
  st.markdown(f"Prompt image : *{message.get('content', 'Prompt vide')}*")
937
- elif message_type == "t2i" and ("image_data" in message or "content" in message): # AI image response (success or error)
938
- # Display generated image or error message
939
- prompt_text = message.get('prompt', 'Pas de prompt enregistré')
940
- error_text = message.get('content', '') # Potential error message content
941
-
942
- st.markdown(f"Prompt: *{prompt_text}*") # Always display the prompt for context
943
- if "image_data" in message:
944
- try:
945
- image = Image.open(BytesIO(message["image_data"]))
946
- st.image(image, caption="Image générée")
947
- except Exception as e:
948
- st.error(f"Erreur lors de l'affichage de l'image : {e}")
949
- elif error_text: # Display error message if image data is not present but content is
950
- st.error(f"Échec de la génération d'image: {error_text}")
951
- else: # Fallback for unexpected t2i message format
952
- st.warning("Réponse image de format inattendu.")
953
-
954
-
955
- # Add other message types if needed in the future (e.g., multimodal)
 
 
 
 
 
 
 
 
956
 
957
  # Area for the chat input box
958
  input_container = st.container()
959
  with input_container:
960
- # Check if a model is selected before showing the input box and enabling it
961
  if st.session_state.selected_model_id is not None:
962
- # Get the type of the currently selected model to refine the placeholder text
963
  current_selected_model_info = next((m for m in AVAILABLE_MODELS if m['id'] == st.session_state.selected_model_id), None)
964
- if current_selected_model_info and current_selected_model_info['type'] == 'text':
965
- placeholder_text = f"Entrez votre message pour la conversation texte..."
966
- elif current_selected_model_info and current_selected_model_info['type'] == 't2i':
967
- # Even though task is fixed to text, if a T2I model is selected, its param controls still show.
968
- # Placeholder reflects the *model type* but the app's task is text.
969
- placeholder_text = f"Prompt pour un modèle d'image (mais l'application est en mode texte)..."
970
- else: # Fallback if model type is unknown or model info missing
971
- placeholder_text = f"Entrez votre prompt pour la tâche Texte..."
972
-
973
  user_input = st.chat_input(placeholder=placeholder_text, disabled=False)
974
  else:
975
  user_input = st.chat_input(placeholder=f"Veuillez configurer un modèle disponible pour démarrer...", disabled=True)
976
- # Display a message prompting the user if no model is selected
977
  st.info(f"Veuillez sélectionner un modèle disponible dans la barre latérale pour commencer. Il n'y a actuellement aucun modèle configuré ou sélectionné.")
978
 
979
 
@@ -982,11 +788,7 @@ with input_container:
982
  # AND a model is selected AND the reset flag is not set (from buttons)
983
  if user_input and st.session_state.selected_model_id and not st.session_state.get('_reset_triggered', False):
984
  # 1. Append the user's message to the chat history
985
- user_message_entry = {"role": "user", "content": user_input}
986
-
987
- # The user's input type is now always 'text' because the task is fixed to 'text'.
988
- user_message_entry["type"] = "text" # Input is always considered text now
989
-
990
  st.session_state.chat_history.append(user_message_entry)
991
 
992
  # --- Trigger a rerun to immediately show the user message ---
@@ -1004,10 +806,9 @@ if st.session_state.chat_history and st.session_state.chat_history[-1]["role"] =
1004
  selected_model_info = next((m for m in AVAILABLE_MODELS if m['id'] == st.session_state.selected_model_id), None)
1005
 
1006
  if not selected_model_info:
1007
- # Display error in chat if model not found (should ideally be caught by sidebar logic)
1008
- error_message = {"role": "assistant", "content": f"Erreur: Modèle sélectionné '{st.session_state.selected_model_id}' introuvable dans la liste AVAILABLE_MODELS.", "type": "text"}
1009
  st.session_state.chat_history.append(error_message)
1010
- st.rerun() # Rerun to display error
1011
  else:
1012
  current_model_display_name = selected_model_info['name']
1013
  current_model_type = selected_model_info['type'] # Get type from the *selected model* info
@@ -1016,58 +817,55 @@ if st.session_state.chat_history and st.session_state.chat_history[-1]["role"] =
1016
  last_user_message_content = last_user_message.get("content", "")
1017
 
1018
 
1019
- # === Task/Model/Input Type Compatibility Check (Simplified as task is fixed to 'text') ===
1020
- is_compatible = False
1021
- error_reason = ""
1022
-
1023
- # Since selected_task is always 'text' now, we only need to check model type and input type
1024
- if current_model_type == 'text':
1025
- if last_user_message_type == 'text':
1026
- is_compatible = True
1027
- else: # This path should not be hit if input type is always 'text' for user_message_entry
1028
- error_reason = f"Votre dernier message (Type: '{last_user_message_type}') n'est pas du texte, requis pour la tâche 'Texte'."
1029
- # If current_model_type is 't2i' (as per new model list, SDXL is gone, so this won't be hit unless you manually add it back)
1030
- # else: # current_model_type is 't2i' or other
1031
- # error_reason = f"Le modèle sélectionné ('{current_model_display_name}' - Type: '{current_model_type}') n'est pas un modèle de texte, requis pour la tâche 'Texte'."
1032
- # Since we removed t2i models from AVAILABLE_MODELS, this 'else' is unreachable for standard flow.
1033
- # But for safety, we can keep the logic if you were to add them back later.
1034
- # However, for this version, the only way to get here with an incompatible model is if AVAILABLE_MODELS is messed up.
1035
- # The line above already handles it if current_model_type is NOT 'text'.
1036
- # Let's simplify the error check logic slightly, as there are only text models now.
1037
- if not (current_model_type == 'text' and last_user_message_type == 'text'):
1038
- error_message_content = f"Erreur de compatibilité : La tâche de l'application est 'Texte', mais le modèle sélectionné ('{current_model_display_name}' - Type: '{current_model_type}') ou votre dernier message (Type: '{last_user_message_type}') n'est pas compatible.\nVeuillez sélectionner un modèle de texte pour continuer la conversation."
1039
  error_message_entry = {"role": "assistant", "content": error_message_content, "type": "text"}
1040
  st.session_state.chat_history.append(error_message_entry)
1041
  st.rerun()
1042
- else: # All checks passed, proceed with generation
 
1043
  assistant_response_entry = None
1044
 
1045
  with chat_interface_container: # Place spinner within the chat container where response will appear
1046
- assistant_avatar = st.session_state.avatars.get("assistant", "❓") # Get assistant avatar
1047
  with st.chat_message("assistant", avatar=assistant_avatar):
 
1048
  with st.spinner(f"KolaChatBot utilise {current_model_display_name} pour générer... 🤔"):
1049
-
1050
- # Since selected_task is always 'text' and compatibility is checked, we only call get_text_response
1051
  assistant_response_entry = get_text_response(
1052
  selected_model_id=st.session_state.selected_model_id,
1053
  system_prompt=st.session_state.system_message,
1054
  full_chat_history=st.session_state.chat_history # Pass full history
1055
  )
1056
- # Display the content from the response entry object returned by get_text_response
1057
- if assistant_response_entry and assistant_response_entry.get("type") == "text":
1058
- st.markdown(assistant_response_entry.get("content", "Erreur: Réponse texte vide."))
1059
- elif assistant_response_entry: # Handle errors returned as text from the generation function
1060
- st.error(f"Erreur lors de la génération: {assistant_response_entry.get('content', 'Raison inconnue.')}")
1061
- else: # Handle None response from get_text_response
1062
- st.error("La fonction de génération de texte n'a pas renvoyé de réponse valide.")
1063
-
1064
-
1065
- # --- Append the generated response entry to chat history ---
1066
- # Ensure we have received a valid response entry object before appending
1067
- if assistant_response_entry is not None:
1068
- st.session_state.chat_history.append(assistant_response_entry)
1069
- # No need for another st.rerun() after appending and displaying,
1070
- # the UI is already updated.
 
 
 
 
 
 
 
 
 
 
 
1071
 
1072
  # --- Clear reset flag at the very end of the script if it was set ---
1073
  # This ensures the flag is only True for one full rerun cycle after a button click
 
12
  import google.generativeai as genai
13
 
14
  # Import DuckDuckGo Search (for web search feature)
15
+ # Assurez-vous d'avoir installé cette bibliothèque : pip install duckduckgo_search
16
  from duckduckgo_search import DDGS # Importe la classe DDGS
17
 
18
  # -----------------------------------------------------------------------------
 
30
  # -----------------------------------------------------------------------------
31
  # Structure: {'id': 'model_id', 'name': 'Display Name', 'provider': 'huggingface'/'google', 'type': 'text'/'t2i', 'params': {...} }
32
  AVAILABLE_MODELS = [
33
+ # --- Google Text Models (Only keeping Flash as requested) ---
34
  {
35
  "id": "gemini-1.5-flash-latest",
36
  "name": "Gemini 1.5 Flash (Google)",
 
42
  "top_p": 0.9,
43
  },
44
  },
45
+ {
46
+ "id": "gemini-2.0-flash-lite-001", # Ajouté comme demandé
47
+ "name": "Gemini 2.0 Flash Lite 001 (Google)",
48
  "provider": "google",
49
+ "type": "text", # Supposé compatible pour le chat texte
50
  "params": {
51
+ "max_new_tokens": 200,
52
+ "temperature": 0.6,
53
+ "top_p": 0.9,
54
  },
55
  },
56
+ {
57
+ "id": "gemini-2.0-flash-preview-image-generation", # Ajouté comme demandé - NOTE : Son comportement exact comme modèle de chat texte peut varier.
58
+ "name": "Gemini 2.0 Flash Preview Image Gen (Google)",
59
+ "provider": "google",
60
+ "type": "text", # Supposé compatible pour le chat texte malgré son nom
61
+ "params": { # Paramètres par défaut pour le texte
62
+ "max_new_tokens": 200,
63
+ "temperature": 0.6,
64
+ "top_p": 0.9,
65
+ },
66
+ },
67
+ # Retiré : gemini-1.5-pro-latest, gemini-pro, gemini-2.5-flash-preview, gemini-2.5-pro-preview
68
+ # Retiré : Mistral (Hugging Face Text)
69
+ # Retiré : Stable Diffusion XL Base 1.0 (Hugging Face T2I) et les autres SD.
70
  ]
71
 
72
+ # Separate models lists by type (still potentially useful for future expansion)
73
  MODELS_BY_TYPE = {m_type: [m for m in AVAILABLE_MODELS if m['type'] == m_type] for m_type in set(m['type'] for m in AVAILABLE_MODELS)}
74
 
75
  # Default task is now fixed to 'text'
76
  DEFAULT_TASK = 'text'
77
+ # Find the first text model as default
78
  first_text_model = next((m for m in AVAILABLE_MODELS if m['type'] == DEFAULT_TASK), None)
79
  if first_text_model:
80
  DEFAULT_MODEL_ID = first_text_model['id']
 
140
  if 'enable_web_search' not in st.session_state:
141
  st.session_state.enable_web_search = False
142
 
143
+
144
  # -----------------------------------------------------------------------------
145
  # Helper pour formater les exports (Adapté pour images)
146
  # -----------------------------------------------------------------------------
 
169
 
170
  if export_msg["type"] == "text":
171
  export_msg["content"] = message.get("content", "")
172
+ elif export_msg["type"] == "t2i_prompt": # User prompt for T2I (kept for handling potential old history)
173
  export_msg["content"] = message.get("content", "")
174
+ elif export_msg["type"] == "t2i" and "prompt" in message: # AI image response (kept for handling potential old history)
175
  export_msg["prompt"] = message["prompt"]
176
  export_msg["image_placeholder"] = "(Image non incluse dans l'export JSON)" # Indicate image was here
177
  if "content" in message: # Include error message if it was an error T2I response
 
195
  lines.append(f"### {role_label}\n\n")
196
  if content_type == "text":
197
  lines.append(f"{message.get('content', '')}\n\n")
198
+ elif content_type == "t2i_prompt": # Kept for handling potential old history
199
  lines.append(f"*Prompt image:* {message.get('content', '')}\n\n")
200
+ elif content_type == "t2i": # Kept for handling potential old history
201
  prompt_text = message.get('prompt', 'Pas de prompt enregistré')
202
  error_text = message.get('content', '') # Potential error message
203
  lines.append(f"*Image générée (prompt: {prompt_text})*\n")
 
214
  # -----------------------------------------------------------------------------
215
  # LLM API helper (Unified call logic)
216
  # -----------------------------------------------------------------------------
217
+ # Removed build_mistral_prompt as no Mistral models
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
218
 
219
  def call_hf_inference(model_id: str, payload_inputs: any, params: dict, model_type: str) -> any:
220
  """
221
  Calls the Hugging Face Inference API for either text generation or text-to-image.
222
+ This function is now only relevant for T2I calls if you were to add back T2I models.
 
 
223
  """
224
  if not HUGGINGFACEHUB_API_TOKEN:
225
  return "Erreur d'API Hugging Face: Le token HUGGINGFACEHUB_API_TOKEN est introuvable."
226
 
227
  headers = {"Authorization": f"Bearer {HUGGINGFACEHUB_API_TOKEN}"}
228
+ url = f"{HF_BASE_API_URL}{model_id}"
 
 
 
229
 
230
+ # Since we removed HF text models, this branch is not expected to be hit in this config.
231
+ # If you add HF text models back, ensure the prompt building is done by the caller.
232
  if model_type == 'text':
233
+ # This case should not be hit with the current AVAILABLE_MODELS list
234
+ st.error("Erreur interne: Modèle texte Hugging Face non pris en charge dans cette configuration.")
235
+ return {"role": "assistant", "content": "Erreur interne: Modèle texte Hugging Face non pris en charge dans cette configuration.", "type": "text"}
236
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
237
 
238
  elif model_type == 't2i':
239
+ # This case should not be hit with the current AVAILABLE_MODELS list
240
+ st.error("Erreur interne: Modèle Text-to-Image Hugging Face non pris en charge dans cette configuration.")
241
+ return {"role": "assistant", "content": "Erreur interne: Modèle Text-to-Image Hugging Face non pris en charge dans cette configuration.", "type": "text", "prompt": payload_inputs}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
242
 
243
  else:
244
  return f"Erreur interne: Type de modèle Hugging Face '{model_type}' inconnu."
245
 
246
 
247
+ # The code below this point would only be reached if you added back HF models.
248
+ # The structure is kept but not actively used in this config.
249
+ payload = {}
250
+ response_parser = None
251
+ response_is_json = False # Default to False for safety
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
252
 
253
+ # ... (rest of the hf inference logic is now effectively unreachable) ...
254
+ # To avoid linting errors or confusion, let's just make it return an error for any other type.
255
+ return f"Erreur interne: Type de modèle Hugging Face '{model_type}' non géré dans l'appel API."
256
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
257
 
258
 
259
  def call_google_api(model_id: str, system_message: str, chat_history_for_api: list[dict], params: dict) -> str:
260
  """
261
  Calls the Google Generative AI API (Text models only).
262
+ Note: The system_message is included for consistency but IGNORED in the API call.
 
 
263
  """
264
  if not GOOGLE_API_KEY:
265
+ return "Erreur d'API Google: La clé API Google est introuvable."
266
 
267
  try:
268
  genai.configure(api_key=GOOGLE_API_KEY)
 
274
 
275
  # Prepare history for the Gemini API
276
  # The standard format is a list of dicts: [{'role': 'user', 'parts': [...]}, {'role': 'model', 'parts': ...]}]
277
+ # `chat_history_for_api` here is the list of messages relevant to the model turns (filtered by text type).
278
  gemini_history_parts = []
279
 
280
+ # System message is ignored for API call (as per previous fix)
 
 
 
 
 
 
 
281
 
282
  for msg in chat_history_for_api:
283
+ if msg.get("type", "text") == "text":
 
 
284
  # Map roles: Streamlit 'user' -> Google 'user', Streamlit 'assistant' -> Google 'model'
285
  role = 'user' if msg['role'] == 'user' else 'model'
286
  gemini_history_parts.append({"role": role, "parts": [msg['content']]})
 
 
 
287
 
288
  generation_config = genai.types.GenerationConfig(
289
  max_output_tokens=params.get("max_new_tokens", 200),
290
  temperature=params.get("temperature", 0.6),
291
  top_p=params.get("top_p", 0.9),
 
292
  )
293
 
294
+ # Call generate_content *without* the 'system_instruction' argument
295
  response = model.generate_content(
296
  contents=gemini_history_parts,
297
  generation_config=generation_config,
298
+ request_options={'timeout': 180}
299
  )
300
 
301
  # Process the response
 
302
  if response.candidates:
303
  if response.candidates[0].content and response.candidates[0].content.parts:
304
  generated_text = "".join(part.text for part in response.candidates[0].content.parts)
 
314
  return f"API Google: Votre message a été bloqué ({response.prompt_feedback.block_reason}). Raison détaillée: {response.prompt_feedback.safety_ratings}"
315
  else:
316
  # No candidates and no block feedback - unknown error
317
+ return f"API Google: Aucune réponse générée pour une raison inconnue. Debug info: {response}"
318
 
319
  except Exception as e:
320
  # Catch any other exceptions during the API call
 
326
  if not isinstance(seconds, (int, float)) or seconds < 0: return "N/A"
327
  minutes = int(seconds // 60)
328
  remaining_seconds = int(seconds % 60)
329
+ if minutes > 0: return f"{minutes} min {remaining_seconds} sec"
330
  return f"{remaining_seconds} sec"
331
 
332
  # -----------------------------------------------------------------------------
 
338
  # Use DDGS.text for simple text search (DDGS class)
339
  # It's best practice to use DDGS as a context manager to ensure proper session closure
340
  with DDGS() as ddgs:
341
+ # Set region for potentially better results
342
+ results = ddgs.text(keywords=query, max_results=num_results, region='fr-fr') # Added region
343
+
344
  # Convert generator to list for iteration
345
  results_list = list(results)
346
 
 
357
  return f"Erreur lors de la recherche web: {e}"
358
 
359
  # -----------------------------------------------------------------------------
360
+ # Generation Functions (Simplified as task is fixed to Text)
361
  # -----------------------------------------------------------------------------
362
 
363
  def get_text_response(selected_model_id: str, system_prompt: str, full_chat_history: list[dict]):
364
  """
365
  Handles text generation request using the selected text model.
366
+ This is the primary generation function in this text-only configuration.
367
  """
368
  # Model type compatibility is checked by the caller before calling this function.
369
  selected_model_info = next((m for m in AVAILABLE_MODELS if m['id'] == selected_model_id), None)
 
402
  if st.session_state.enable_web_search and model_provider == 'google':
403
  last_user_query = full_chat_history[-1]['content'] # Get the user's last actual query
404
 
405
+ # Display "Recherche en cours..." message
 
406
  st.session_state.chat_history.append({"role": "assistant", "content": f"*(KolaChatBot recherche sur le web pour '{last_user_query}'...)*", "type": "text"})
407
+ st.rerun() # Force a rerun to display this message immediately
 
 
 
 
 
 
 
 
 
 
 
408
 
409
+ # This part runs on the *next* rerun
410
+ try:
411
+ # Perform the actual web search
412
+ search_results = perform_web_search(last_user_query)
413
+
414
+ # Create a copy of the history for the API call and inject the search results
415
+ temp_api_history = []
416
+ for msg in actual_conversation_history_for_prompt:
417
+ temp_api_history.append(msg.copy()) # Make sure to copy the dict
418
+
419
+ # Prepend search results to the *last user message* in the temp history
420
+ context_message_content = f"Voici des informations pertinentes du web sur le sujet de la dernière question :\n```\n{search_results}\n```\n\nEn te basant sur ces informations (si pertinentes) et notre conversation, réponds à la question : "
421
+
422
+ if temp_api_history and temp_api_history[-1]['role'] == 'user' and temp_api_history[-1]['type'] == 'text':
423
+ temp_api_history[-1]['content'] = context_message_content + temp_api_history[-1]['content']
424
+ else:
425
+ # Fallback: add context as a separate user message if last message isn't a simple user text
426
+ temp_api_history.append({"role": "user", "content": context_message_content, "type": "text"})
427
+
428
+ # Call the Google API with the modified history
429
+ response_content = call_google_api(model_id, system_prompt, temp_api_history, params)
430
+
431
+ # Update the search message in chat history to indicate results were used
432
+ # Find the message we added earlier and update its content
433
+ if st.session_state.chat_history and st.session_state.chat_history[-1]["content"].startswith("*(KolaChatBot recherche web"):
434
+ st.session_state.chat_history[-1]["content"] = f"*(KolaChatBot a terminé la recherche web. Résultats intégrés à la réponse.)*"
435
+
436
+
437
+ except Exception as e:
438
+ response_content = f"Erreur lors de la recherche web ou de l'intégration des résultats : {e}"
439
+ # If search failed, update the temporary message to show the error
440
+ if st.session_state.chat_history and st.session_state.chat_history[-1]["content"].startswith("*(KolaChatBot recherche web"):
441
+ st.session_state.chat_history[-1]["content"] = f"*(KolaChatBot a rencontré une erreur lors de la recherche web : {e})*"
442
 
 
 
 
 
 
 
 
 
 
443
 
444
  else: # No web search or not a Google model
445
+ if model_provider == 'google':
 
 
 
 
446
  # This path is for Google models WITHOUT web search enabled
447
  response_content = call_google_api(model_id, system_prompt, actual_conversation_history_for_prompt, params)
448
  else:
449
+ # This branch is effectively unreachable with the current AVAILABLE_MODELS list (only Google Text models)
450
+ response_content = f"Erreur interne: Fournisseur API '{model_provider}' inconnu pour le modèle texte '{model_id}'."
451
+
452
 
453
  return {"role": "assistant", "content": response_content, "type": "text"}
454
 
455
 
456
+ # This function is no longer used in this text-only configuration.
457
  def get_image_response(selected_model_id: str, user_prompt: str):
458
+ return {"role": "assistant", "content": "La génération d'images n'est pas disponible dans cette version de l'application.", "type": "text", "prompt": user_prompt}
 
 
 
 
 
 
 
459
 
460
 
461
  # -----------------------------------------------------------------------------
 
476
 
477
 
478
  # -----------------------------------------------------------------------------
479
+ # Manuel d'utilisation (Update for text-only and RAG)
480
  # -----------------------------------------------------------------------------
481
  with st.expander("📖 Manuel d'utilisation de KolaChatBot", expanded=False):
482
  st.markdown("""
483
+ Bienvenue sur KolaChatBot - Une application de chat IA ! Voici comment tirer le meilleur parti de notre assistant IA :
484
 
485
  **1. Comment interagir ?**
486
  - **La tâche est fixée sur Texte (conversation).**
487
  - **Entrer votre prompt :** Tapez votre message dans la zone de texte en bas et appuyez sur Entrée.
488
 
489
  **2. Paramètres dans la barre latérale (Sidebar) :**
490
+ Ajustez les paramètres pour la génération de texte.
491
 
492
+ * **Sélection du Modèle :** Choisissez un modèle Google Gemini Flash pour converser.
493
+ - **Important :** Assurez-vous que la clé API Google (`GOOGLE_API_KEY`) est configurée dans vos secrets.
494
+ - Changer de modèle **ne réinitialise pas** automatiquement les autres paramètres. Les paramètres actuels seront utilisés lors de la prochaine génération.
495
  * **Option "Activer la recherche web" :** Activez cette option pour que l'IA (via les modèles Google Gemini) recherche sur le web et utilise les résultats pour enrichir ses réponses.
496
+ * **Paramètres de Génération :** Ajustez la longueur maximale de la réponse, la température (créativité), et le Top-P (sampling).
497
+ * **Message Système / Personnalité :** Définissez le rôle ou le style que l'IA de texte doit adopter.
498
+ * **Message de Bienvenue de l'IA :** Personnalisez le message initial.
499
  * **Sélection des Avatars.**
500
+ * **Gestion de la Conversation :** Utilisez les boutons pour appliquer les paramètres actuels (et démarrer une nouvelle conversation) ou simplement effacer l'historique actuel.
501
+ * **Exporter la Conversation :** Téléchargez l'historique.
502
 
503
  **3. Limitations :**
504
+ - **L'application est configurée uniquement pour la tâche de génération de texte.** Seuls les modèles Google Gemini Flash sont disponibles. Tenter d'utiliser un autre type de modèle entraînerait une erreur.
505
+ - La fonction "Recherche Web" est actuellement implémentée uniquement pour les modèles Google Gemini et peut entraîner des délais supplémentaires. Les résultats de recherche web s'affichent temporairement dans le chat avant la réponse de l'IA.
506
  - Notez que l'application de votre **Message Système / Personnalité** aux modèles Google dépend de la prise en charge de la fonctionnalité `system_instruction` par la version de la bibliothèque Google Generative AI et le modèle spécifique utilisé. Si vous obtenez des erreurs (`unexpected keyword argument 'system_instruction'`), le message système sera ignoré pour ces modèles.
507
+ - Cette application ne stocke pas les données de manière permanente. L'historique est présent dans la session tant que l'application tourne.
 
508
 
509
  Amusez-vous bien avec KolaChatBot !
510
  """)
511
  # -----------------------------------------------------------------------------
512
+ # Sidebar settings
513
  # -----------------------------------------------------------------------------
514
  with st.sidebar:
515
  st.header("🛠️ Configuration de KolaChatBot")
516
 
517
+ # Task is fixed to 'text'
518
+ st.subheader("🎯 Tâche IA : Texte (fixée)")
 
519
 
520
  st.subheader("🧠 Sélection du Modèle")
521
  all_available_models = AVAILABLE_MODELS
522
 
523
+ if not all_available_models:
524
  st.warning("Aucun modèle disponible défini dans la liste AVAILABLE_MODELS.")
525
+ st.session_state.selected_model_id = None
526
  else:
527
  model_options = {model['id']: model['name'] for model in all_available_models}
528
 
 
 
529
  current_model_index = 0
530
  if st.session_state.selected_model_id in model_options:
531
  current_model_index = list(model_options.keys()).index(st.session_state.selected_model_id)
532
+ elif all_available_models:
533
+ default_model_from_list = next((m for m in AVAILABLE_MODELS), None)
534
+ if default_model_from_list:
535
+ st.session_state.selected_model_id = default_model_from_list['id']
536
+ current_model_index = 0
537
+ else:
538
+ st.session_state.selected_model_id = None
539
+
540
 
 
541
  st.session_state._prev_model_id_before_selectbox = st.session_state.selected_model_id
 
542
  selected_model_id_from_selectbox_value = st.selectbox(
543
  "Choisir le modèle :",
544
  options=list(model_options.keys()),
545
  format_func=lambda x: model_options[x],
546
  index=current_model_index,
547
+ key="selected_model_id",
548
+ help="Sélectionnez un modèle disponible (uniquement modèles texte pris en charge dans cette version)."
549
  )
 
550
 
551
 
552
  # --- Model Change Detection (without auto-resetting params) ---
 
 
553
  model_id_changed_this_run = st.session_state.selected_model_id != st.session_state.get('_prev_model_id_before_selectbox')
 
 
 
554
 
555
  # If model changed (and not triggered by an explicit button reset), reset history
 
556
  if model_id_changed_this_run and not st.session_state.get('_reset_triggered', False):
557
  st.session_state.chat_history = [{"role": "assistant", "content": st.session_state.starter_message, "type": "text"}]
558
  st.info(f"⚠️ Modèle changé. La conversation a été réinitialisée. Les paramètres restent les mêmes jusqu'à application explicite.")
559
+ # Do not set _reset_triggered here
560
+ st.rerun()
561
+
562
 
563
+ # Check required API Key for the selected model
564
+ if st.session_state.selected_model_id:
565
  current_model_info_check = next((m for m in AVAILABLE_MODELS if m['id'] == st.session_state.selected_model_id), None)
566
  if current_model_info_check:
567
  current_model_provider = current_model_info_check['provider']
568
+ if current_model_provider == 'huggingface': # Should not happen with current models
569
  if not HUGGINGFACEHUB_API_TOKEN:
570
  st.warning("❌ Le token Hugging Face est manquant (`HUGGINGFACEHUB_API_TOKEN`). Les modèles Hugging Face ne fonctionneront pas.")
571
  elif current_model_provider == 'google':
 
575
 
576
  # --- Dynamic Parameter Settings based on the SELECTED MODEL's TYPE ---
577
  st.subheader("⚙️ Paramètres")
 
578
  current_selected_model_info = next((m for m in AVAILABLE_MODELS if m['id'] == st.session_state.selected_model_id), None)
579
 
580
  if st.session_state.selected_model_id and current_selected_model_info:
581
+ expander_title = f"Ajuster Paramètres : {current_selected_model_info['name']}" # Simplified title
 
582
  with st.expander(expander_title, expanded=True):
583
+ # Since only text models are available, only display text parameters
584
  if current_selected_model_info['type'] == 'text':
 
 
585
  st.session_state.max_response_length = st.number_input(
586
  "Max New Tokens (longueur max réponse) :",
587
+ min_value=20, max_value=8192, # Increased max_value
588
+ value=st.session_state.max_response_length,
589
  step=10,
590
  key="max_new_tokens_input",
591
  help="Longueur maximale de la réponse de l'IA (en jetons ou tokens).",
 
593
  st.session_state.temperature = st.slider(
594
  "Temperature (créativité) :",
595
  min_value=0.0, max_value=2.0,
596
+ value=st.session_state.temperature,
597
  step=0.01,
598
  key="temperature_input",
599
  help="Contrôle le caractère aléatoire des réponses. Plus élevé = plus créatif/imprévisible.",
 
601
  st.session_state.top_p = st.slider(
602
  "Top-P (sampling) :",
603
  min_value=0.01, max_value=1.0,
604
+ value=st.session_state.top_p,
605
  step=0.01,
606
  key="top_p_input",
607
  help="Contrôle la diversité en limitant les options de tokens. Plus bas = moins diversifié. 1.0 = désactivé.",
608
  )
609
 
 
610
  st.session_state.system_message = st.text_area(
611
  "Message Système / Personnalité :",
612
  value=st.session_state.system_message,
613
  height=100,
614
  key="system_message_input",
615
+ help="Décrivez le rôle ou le style que l'IA de texte doit adopter. Notez que cette consigne peut être ignorée par certains modèles/versions API (voir Limitations).",
616
  )
617
  st.session_state.starter_message = st.text_area(
618
  "Message de Bienvenue de l'IA :",
 
621
  key="starter_message_input",
622
  help="Le premier message que l'IA affichera au début d'une nouvelle conversation textuelle ou après un reset.",
623
  )
624
+ # Web Search Checkbox - only displayed for Google models
625
+ if current_selected_model_info['provider'] == 'google':
626
+ st.session_state.enable_web_search = st.checkbox(
627
+ "Activer la recherche web (pour les modèles Google)",
628
+ value=st.session_state.enable_web_search,
629
+ key="web_search_checkbox",
630
+ help="Si coché, les modèles Google Gemini effectueront une recherche DuckDuckGo et utiliseront les résultats pour répondre à votre prompt."
631
+ )
632
+ else: # If a non-Google model were added back
633
+ # Ensure checkbox state is not changed and it's disabled
634
+ # Use a unique key based on model_id to prevent state interference if model changes
635
+ st.session_state.enable_web_search = st.checkbox(
636
+ "Activer la recherche web (pour les modèles Google)",
637
+ value=False, # Always False if not Google
638
+ key=f"web_search_checkbox_{current_selected_model_info.get('id', 'nongoogle')}", # Unique key
639
+ disabled=True,
640
+ help="La recherche web n'est disponible que pour les modèles Google Gemini.",
641
+ )
642
+
643
+ elif current_selected_model_info['type'] == 't2i': # This branch should not be hit
644
+ st.info("Note: Ce modèle est de type 'Image', mais l'application ne supporte que la tâche 'Texte'.")
645
+
646
+ if current_selected_model_info and current_selected_model_info['type'] not in ['text', 't2i']: # Should not be hit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
647
  st.info(f"Type de modèle sélectionné ('{current_selected_model_info['type']}') inconnu ou non pris en charge pour l'affichage des paramètres.")
648
 
649
+ else:
650
  st.info("Aucun modèle sélectionné, les paramètres ne sont pas disponibles.")
651
 
652
 
653
  # Flag used to detect if 'Appliquer Paramètres' or 'Effacer' was clicked
654
  # and prevent regeneration on the immediate rerun
655
+ if st.session_state.get('_reset_triggered', False):
656
+ st.session_state._reset_triggered = False
657
+
 
 
 
 
 
 
 
 
 
 
 
 
658
 
659
  st.subheader("🔄 Gestion de la Conversation")
660
  # Reset button that applies *all* parameters (system/starter/model) and starts new conversation
661
+ if st.button("♻️ Appliquer Paramètres & Nouvelle Conversation", type="primary", help="Applique les paramètres actuels et démarre une nouvelle conversation texte en effaçant l'historique."):
 
662
  # Parameters are already updated in session state via key linking
663
  # Reset history.
664
  st.session_state.chat_history = [{"role": "assistant", "content": st.session_state.starter_message, "type": "text"}]
 
667
  st.rerun() # Rerun immediately to refresh UI
668
 
669
  # Clear history button - simpler, just history
670
+ if st.button("🗑️ Effacer la Conversation Actuelle", help="Efface l'historique de conversation mais conserve les paramètres actuels."):
671
  # Reset history.
672
  st.session_state.chat_history = [{"role": "assistant", "content": st.session_state.starter_message, "type": "text"}]
673
  st.info("Conversation actuelle effacée.")
 
728
  chat_interface_container = st.container(height=600, border=True)
729
 
730
  with chat_interface_container:
731
+ # Embed a div with a specific ID for the chat history messages
732
+ st.markdown("<div id='chat_history_container_scroll_area'>", unsafe_allow_html=True)
733
+
734
  # Display existing messages from chat_history.
 
735
  for message in st.session_state.chat_history:
736
+ if message["role"] == "system": continue
 
737
 
738
+ avatar_type = st.session_state.avatars.get(message["role"], "❓")
739
 
740
  with st.chat_message(message["role"], avatar=avatar_type):
741
+ message_type = message.get("type", "text")
742
  if message_type == "text":
743
+ st.markdown(message.get("content", ""))
744
+ elif message_type == "t2i_prompt": # Should not happen with current config
 
745
  st.markdown(f"Prompt image : *{message.get('content', 'Prompt vide')}*")
746
+ elif message_type == "t2i" and ("image_data" in message or "content" in message): # Should not happen with current config
747
+ st.markdown(f"Prompt: *{message.get('prompt', 'Pas de prompt enregistré')}*")
748
+ if "image_data" in message:
749
+ try: st.image(Image.open(BytesIO(message["image_data"])), caption="Image générée")
750
+ except Exception as e: st.error(f"Erreur affichage image : {e}")
751
+ elif message.get('content'): st.error(f"Échec génération image: {message.get('content')}")
752
+ else: st.warning("Réponse image format inattendu.")
753
+
754
+
755
+ # Close the div for the scroll area
756
+ st.markdown("</div>", unsafe_allow_html=True)
757
+
758
+ # JavaScript to scroll to the bottom
759
+ # Needs to be inside the container's 'with' block or just below it.
760
+ # Running it after all messages are rendered ensures it scrolls to the *new* last message.
761
+ st.markdown(
762
+ """
763
+ <script>
764
+ var chatHistory = document.getElementById('chat_history_container_scroll_area');
765
+ if (chatHistory) {
766
+ chatHistory.scrollTop = chatHistory.scrollHeight;
767
+ }
768
+ </script>
769
+ """,
770
+ unsafe_allow_html=True,
771
+ )
772
+
773
 
774
  # Area for the chat input box
775
  input_container = st.container()
776
  with input_container:
 
777
  if st.session_state.selected_model_id is not None:
 
778
  current_selected_model_info = next((m for m in AVAILABLE_MODELS if m['id'] == st.session_state.selected_model_id), None)
779
+ placeholder_text = f"Entrez votre message pour la conversation texte..." # Fixed placeholder text as task is text
 
 
 
 
 
 
 
 
780
  user_input = st.chat_input(placeholder=placeholder_text, disabled=False)
781
  else:
782
  user_input = st.chat_input(placeholder=f"Veuillez configurer un modèle disponible pour démarrer...", disabled=True)
 
783
  st.info(f"Veuillez sélectionner un modèle disponible dans la barre latérale pour commencer. Il n'y a actuellement aucun modèle configuré ou sélectionné.")
784
 
785
 
 
788
  # AND a model is selected AND the reset flag is not set (from buttons)
789
  if user_input and st.session_state.selected_model_id and not st.session_state.get('_reset_triggered', False):
790
  # 1. Append the user's message to the chat history
791
+ user_message_entry = {"role": "user", "content": user_input, "type": "text"} # Always text input now
 
 
 
 
792
  st.session_state.chat_history.append(user_message_entry)
793
 
794
  # --- Trigger a rerun to immediately show the user message ---
 
806
  selected_model_info = next((m for m in AVAILABLE_MODELS if m['id'] == st.session_state.selected_model_id), None)
807
 
808
  if not selected_model_info:
809
+ error_message = {"role": "assistant", "content": f"Erreur: Modèle sélectionné '{st.session_state.selected_model_id}' introuvable.", "type": "text"}
 
810
  st.session_state.chat_history.append(error_message)
811
+ st.rerun()
812
  else:
813
  current_model_display_name = selected_model_info['name']
814
  current_model_type = selected_model_info['type'] # Get type from the *selected model* info
 
817
  last_user_message_content = last_user_message.get("content", "")
818
 
819
 
820
+ # === Task/Model Compatibility Check (Simplified as task is fixed to 'text') ===
821
+ # Check if the selected model is of type 'text'
822
+ if current_model_type != 'text':
823
+ # If the model type is not text, display compatibility error
824
+ error_message_content = f"Erreur de compatibilité : L'application ne supporte que les modèles de texte, mais le modèle sélectionné ('{current_model_display_name}' - Type: '{current_model_type}') n'est pas un modèle de texte.\nVeuillez sélectionner un modèle de texte pour continuer la conversation."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
825
  error_message_entry = {"role": "assistant", "content": error_message_content, "type": "text"}
826
  st.session_state.chat_history.append(error_message_entry)
827
  st.rerun()
828
+ else:
829
+ # === Task and Model are Compatible (Text task, Text model) - Proceed with Generation ===
830
  assistant_response_entry = None
831
 
832
  with chat_interface_container: # Place spinner within the chat container where response will appear
833
+ assistant_avatar = st.session_state.avatars.get("assistant", "❓")
834
  with st.chat_message("assistant", avatar=assistant_avatar):
835
+ # The spinner message is shown here, wrapping the API call
836
  with st.spinner(f"KolaChatBot utilise {current_model_display_name} pour générer... 🤔"):
837
+ # Call get_text_response (which now handles RAG internally for Google)
 
838
  assistant_response_entry = get_text_response(
839
  selected_model_id=st.session_state.selected_model_id,
840
  system_prompt=st.session_state.system_message,
841
  full_chat_history=st.session_state.chat_history # Pass full history
842
  )
843
+ # Display the content from the response entry object returned by get_text_response
844
+ # This happens *after* the spinner block
845
+ if assistant_response_entry and assistant_response_entry.get("type") == "text":
846
+ st.markdown(assistant_response_entry.get("content", "Erreur: Réponse texte vide."))
847
+ elif assistant_response_entry: # Handle errors returned as text from the generation function
848
+ st.error(f"Erreur lors de la génération: {assistant_response_entry.get('content', 'Raison inconnue.')}")
849
+ else: # Handle None response from get_text_response
850
+ st.error("La fonction de génération de texte n'a pas renvoyé de réponse valide.")
851
+
852
+
853
+ # --- Append the generated response entry to chat history ---
854
+ if assistant_response_entry is not None:
855
+ # Check if the last message added was the search message placeholder.
856
+ # If so, replace it with the actual response. Otherwise, just append.
857
+ if st.session_state.chat_history[-1]["role"] == "assistant" and \
858
+ st.session_state.chat_history[-1]["content"].startswith("*(KolaChatBot recherche web"):
859
+ # Remove the temporary search message
860
+ st.session_state.chat_history.pop()
861
+ # Append the actual response
862
+ st.session_state.chat_history.append(assistant_response_entry)
863
+ else:
864
+ # Just append the response if no search message was pending
865
+ st.session_state.chat_history.append(assistant_response_entry)
866
+
867
+ # No need for another st.rerun() here, the UI is already updated by the first rerun.
868
+
869
 
870
  # --- Clear reset flag at the very end of the script if it was set ---
871
  # This ensures the flag is only True for one full rerun cycle after a button click