| """Gradio event handlers and chat history normalization.""" |
|
|
| from __future__ import annotations |
|
|
| from typing import Any, Dict, Generator, List, Optional, Sequence, Tuple |
|
|
| from .config import LOGGER |
| from .model import contains_malformed_output, stream_model_reply |
| from .prompts import CRISIS_MESSAGE, LOADING_MESSAGE, contains_crisis_language |
|
|
|
|
| def extract_message_text(content: Any) -> str: |
| """Read text from plain and Gradio-normalized Chatbot message content.""" |
| if isinstance(content, str): |
| return content |
|
|
| if isinstance(content, list): |
| text_parts: List[str] = [] |
| for item in content: |
| if isinstance(item, dict) and isinstance(item.get("text"), str): |
| text_parts.append(item["text"]) |
| elif hasattr(item, "text") and isinstance(item.text, str): |
| text_parts.append(item.text) |
| return "\n".join(text_parts) |
|
|
| if isinstance(content, dict) and isinstance(content.get("text"), str): |
| return content["text"] |
|
|
| if hasattr(content, "text") and isinstance(content.text, str): |
| return content.text |
|
|
| return "" |
|
|
|
|
| def normalize_history(history: Optional[Sequence[Any]]) -> List[Dict[str, str]]: |
| """Accept modern messages or legacy tuple history and return chat messages.""" |
| messages: List[Dict[str, str]] = [] |
|
|
| for item in history or []: |
| if isinstance(item, dict): |
| role = item.get("role") |
| content = extract_message_text(item.get("content")) |
| if role in {"user", "assistant"} and content.strip(): |
| if role == "assistant" and contains_malformed_output(content): |
| continue |
| messages.append({"role": role, "content": content}) |
| continue |
|
|
| if hasattr(item, "role") and hasattr(item, "content"): |
| role = getattr(item, "role") |
| content = extract_message_text(getattr(item, "content")) |
| if role in {"user", "assistant"} and content.strip(): |
| if role == "assistant" and contains_malformed_output(content): |
| continue |
| messages.append({"role": role, "content": content}) |
| continue |
|
|
| if isinstance(item, (list, tuple)) and len(item) == 2: |
| user_text, assistant_text = item |
| if isinstance(user_text, str) and user_text.strip(): |
| messages.append({"role": "user", "content": user_text}) |
| if isinstance(assistant_text, str) and assistant_text.strip(): |
| messages.append({"role": "assistant", "content": assistant_text}) |
|
|
| return messages |
|
|
|
|
| def add_user_message( |
| message: str, |
| history: Optional[Sequence[Any]], |
| ) -> Tuple[List[Dict[str, str]], str]: |
| clean_message = (message or "").strip() |
| messages = normalize_history(history) |
|
|
| if not clean_message: |
| return messages, "" |
|
|
| messages.append({"role": "user", "content": clean_message}) |
| return messages, "" |
|
|
|
|
| def add_quick_prompt( |
| prompt: str, |
| history: Optional[Sequence[Any]], |
| ) -> Tuple[List[Dict[str, str]], str]: |
| return add_user_message(prompt, history) |
|
|
|
|
| def generate_assistant_reply( |
| history: Optional[Sequence[Any]], |
| ) -> Generator[List[Dict[str, str]], None, None]: |
| messages = normalize_history(history) |
|
|
| if not messages or messages[-1]["role"] != "user": |
| yield messages |
| return |
|
|
| latest_user_message = messages[-1]["content"] |
|
|
| if contains_crisis_language(latest_user_message): |
| messages.append({"role": "assistant", "content": CRISIS_MESSAGE}) |
| yield messages |
| return |
|
|
| messages.append({"role": "assistant", "content": LOADING_MESSAGE}) |
| yield messages |
|
|
| try: |
| for token in stream_model_reply(messages[:-1]): |
| if messages[-1]["content"] == LOADING_MESSAGE: |
| messages[-1]["content"] = "" |
| messages[-1]["content"] += token |
| yield messages |
| except (FileNotFoundError, RuntimeError) as exc: |
| LOGGER.warning("SolaceLLM is not ready: %s", exc) |
| messages[-1]["content"] = ( |
| "I couldn't reach SolaceLLM locally yet. " |
| f"{exc}\n\nCheck that `llama-cpp-python` is installed and either " |
| "`SOLACE_MODEL_PATH` points to a local GGUF file or `SOLACE_MODEL_REPO` " |
| "points to a Hugging Face GGUF repo." |
| ) |
| yield messages |
| except Exception as exc: |
| LOGGER.exception("Unable to generate a SolaceLLM response") |
| messages[-1]["content"] = ( |
| "I couldn't reach SolaceLLM locally yet. " |
| f"{exc}\n\nCheck that `llama-cpp-python` is installed and either " |
| "`SOLACE_MODEL_PATH` points to a local GGUF file or `SOLACE_MODEL_REPO` " |
| "points to a Hugging Face GGUF repo." |
| ) |
| yield messages |
|
|
|
|