Spaces:
Sleeping
Sleeping
Update src/streamlit_app.py
Browse files- 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
|
| 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-
|
| 46 |
-
"name": "Gemini
|
| 47 |
"provider": "google",
|
| 48 |
-
"type": "text",
|
| 49 |
"params": {
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
},
|
| 54 |
},
|
| 55 |
-
|
| 56 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
]
|
| 58 |
|
| 59 |
-
# Separate models lists by type (still useful for
|
| 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
|
| 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": #
|
| 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 |
-
#
|
| 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 |
-
|
| 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}"
|
| 265 |
-
|
| 266 |
-
payload = {}
|
| 267 |
-
response_parser = None
|
| 268 |
|
|
|
|
|
|
|
| 269 |
if model_type == 'text':
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 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 |
-
|
| 292 |
-
|
| 293 |
-
|
| 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 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 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
|
| 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:
|
| 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
|
| 404 |
gemini_history_parts = []
|
| 405 |
|
| 406 |
-
#
|
| 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 |
-
|
| 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
|
| 434 |
response = model.generate_content(
|
| 435 |
contents=gemini_history_parts,
|
| 436 |
generation_config=generation_config,
|
| 437 |
-
request_options={'timeout': 180}
|
| 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"
|
| 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"
|
| 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 |
-
|
|
|
|
|
|
|
| 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 (
|
| 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 |
-
|
| 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 |
-
#
|
| 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 |
-
|
| 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 |
-
|
| 560 |
-
|
| 561 |
-
|
| 562 |
-
|
| 563 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 == '
|
| 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 |
-
|
|
|
|
|
|
|
| 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
|
| 619 |
# -----------------------------------------------------------------------------
|
| 620 |
with st.expander("📖 Manuel d'utilisation de KolaChatBot", expanded=False):
|
| 621 |
st.markdown("""
|
| 622 |
-
Bienvenue sur KolaChatBot - Une application de chat 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 |
-
|
| 630 |
|
| 631 |
-
* **Sélection du Modèle :**
|
| 632 |
-
- **Important :** Assurez-vous que
|
| 633 |
-
- Changer de modèle **ne réinitialise pas** automatiquement les
|
| 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
|
| 636 |
-
|
| 637 |
-
|
| 638 |
* **Sélection des Avatars.**
|
| 639 |
-
* **Gestion de la Conversation :**
|
| 640 |
-
* **Exporter la Conversation :** Téléchargez l'historique.
|
| 641 |
|
| 642 |
**3. Limitations :**
|
| 643 |
-
- **L'application est configurée uniquement pour la tâche de génération 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
|
| 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
|
| 653 |
# -----------------------------------------------------------------------------
|
| 654 |
with st.sidebar:
|
| 655 |
st.header("🛠️ Configuration de KolaChatBot")
|
| 656 |
|
| 657 |
-
#
|
| 658 |
-
st.subheader("🎯 Tâche IA : Texte (fixée)")
|
| 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:
|
| 665 |
st.warning("Aucun modèle disponible défini dans la liste AVAILABLE_MODELS.")
|
| 666 |
-
st.session_state.selected_model_id = None
|
| 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:
|
| 676 |
-
|
| 677 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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",
|
| 688 |
-
help="Sélectionnez un modèle disponible
|
| 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
|
| 707 |
-
st.rerun()
|
|
|
|
| 708 |
|
| 709 |
-
# Check required API Key for the selected model
|
| 710 |
-
if st.session_state.selected_model_id:
|
| 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 |
-
|
| 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,
|
| 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,
|
| 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,
|
| 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.
|
| 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
|
| 775 |
-
|
| 776 |
-
|
| 777 |
-
|
| 778 |
-
|
| 779 |
-
|
| 780 |
-
|
| 781 |
-
|
| 782 |
-
|
| 783 |
-
|
| 784 |
-
|
| 785 |
-
|
| 786 |
-
|
| 787 |
-
|
| 788 |
-
|
| 789 |
-
|
| 790 |
-
|
| 791 |
-
|
| 792 |
-
|
| 793 |
-
|
| 794 |
-
st.
|
| 795 |
-
|
| 796 |
-
|
| 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:
|
| 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 |
-
|
| 834 |
-
|
| 835 |
-
|
| 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 |
-
|
| 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=
|
| 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"], "❓")
|
| 929 |
|
| 930 |
with st.chat_message(message["role"], avatar=avatar_type):
|
| 931 |
-
message_type = message.get("type", "text")
|
| 932 |
if message_type == "text":
|
| 933 |
-
st.markdown(message.get("content", ""))
|
| 934 |
-
elif message_type == "t2i_prompt": #
|
| 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): #
|
| 938 |
-
|
| 939 |
-
|
| 940 |
-
|
| 941 |
-
|
| 942 |
-
|
| 943 |
-
|
| 944 |
-
|
| 945 |
-
|
| 946 |
-
|
| 947 |
-
|
| 948 |
-
|
| 949 |
-
|
| 950 |
-
|
| 951 |
-
|
| 952 |
-
|
| 953 |
-
|
| 954 |
-
|
| 955 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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 |
-
|
| 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()
|
| 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
|
| 1020 |
-
|
| 1021 |
-
|
| 1022 |
-
|
| 1023 |
-
|
| 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:
|
|
|
|
| 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", "❓")
|
| 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 |
-
|
| 1057 |
-
|
| 1058 |
-
|
| 1059 |
-
|
| 1060 |
-
|
| 1061 |
-
|
| 1062 |
-
|
| 1063 |
-
|
| 1064 |
-
|
| 1065 |
-
|
| 1066 |
-
|
| 1067 |
-
|
| 1068 |
-
|
| 1069 |
-
|
| 1070 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|