CoolFace
Apppublic

DavidL72Code/UMB_Sustainable_Chatbot

sourceHugging Faceupdated 5d agoView on Hugging Face
0likes
config.py110 linesDownload Raw Back to root
1"""Configuration and the small shared types.2 3Split out of Chatbot.py unchanged. Every other module reads settings from here,4so it must not import any of them back — keep this file free of pipeline5imports or the dependency graph turns into a cycle.6 7PROJECT_ROOT stays correct after the move because this module sits beside8Chatbot.py at the repository root; both resolve to the same directory. Moving9either into a subpackage would break the 27 call sites that hang off it.10"""11from __future__ import annotations12 13import os14from pathlib import Path15from typing import Callable16 17 18LLMCallable = Callable[[str], str]19 20 21 22class ChatbotConfig:23    collection_name: str = "docs"24    persist_directory: str = "./chroma_db"25    seed_documents_directory: str = os.getenv("SEED_DOCUMENTS_DIRECTORY", "./SEED_DOCUMENTS")26    force_reindex: bool = os.getenv("FORCE_REINDEX", "").lower() in {"1", "true", "yes"}27    embedding_model_name: str = os.getenv("EMBEDDING_MODEL", "BAAI/bge-base-en-v1.5")28    # bge is asymmetric: passages are embedded bare, queries need this prefix.29    # Omitting it measurably degrades retrieval quality.30    query_embedding_prefix: str = os.getenv(31        "QUERY_EMBEDDING_PREFIX",32        "Represent this sentence for searching relevant passages: ",33    )34    chunk_size: int = 51235    chunk_overlap: int = 5036    summary_chunk_size: int = 140037    summary_chunk_overlap: int = 14038    # 5 meant only the top 5 reranked chunks seeded the context. Measured on the39    # 2026-08-29 set, 12 put the answer-bearing document in front of the model40    # far more often, on both previously-failing and previously-passing questions.41    top_k: int = 1042    retrieval_candidate_pool: int = 1243    document_neighbor_count: int = int(os.getenv("DOCUMENT_NEIGHBOR_COUNT", "2"))44    document_neighbor_limit: int = int(os.getenv("DOCUMENT_NEIGHBOR_LIMIT", "8"))45    recent_history_turns: int = int(os.getenv("RECENT_HISTORY_TURNS", "6"))46    # Off by default, deliberately. This read "1" for a long time, but every47    # planner call was failing on a 400 the caller swallowed, so the effective48    # behaviour was off and both 208-question benchmarks were measured that way.49    # Repairing the call (see _MODELS_WITHOUT_THINKING) made the planner50    # authoritative again and changed 5 of 20 spot-check answers, two of them51    # regressions: entity questions rerouted from the entity registry to the52    # document registry and started listing filenames instead of people.53    # Leave this off until a full 208 run with it on beats the current baseline.54    always_llm_query_planning: bool = os.getenv("ALWAYS_LLM_QUERY_PLANNING", "0").lower() in {"1", "true", "yes"}55    gemini_api_key: str = os.getenv("GEMINI_API_KEY", "")56    gemini_model: str = os.getenv("GEMINI_MODEL", "gemini-3.5-flash-lite")57    rewrite_model: str = os.getenv("REWRITE_MODEL", "gemma-4-26b-a4b-it")58    # Default to greedy decoding. At 0.7 the same question returned a different59    # answer on every run, which made pipeline behaviour unobservable: a fix and60    # a coin flip looked identical. With this at 0.0, _gemini_gen_config pins61    # top_k=1/top_p=1.0 and a fixed seed, so a given question and evidence62    # produce one answer. Set GEMINI_TEMPERATURE=0.7 to restore varied phrasing.63    gemini_temperature: float = float(os.getenv("GEMINI_TEMPERATURE", "0.0"))64    web_host: str = os.getenv("CHATBOT_HOST", "0.0.0.0")65    web_port: int = int(os.getenv("PORT", os.getenv("CHATBOT_PORT", "7860")))66    cors_origins: str = os.getenv("CORS_ORIGINS", "")67    trust_proxy_headers: bool = os.getenv("TRUST_PROXY_HEADERS", "0").lower() in {"1", "true", "yes"}68    dashboard_trace_mode: str = os.getenv("DASHBOARD_TRACE_MODE", "staff").strip().lower()69    admin_username: str = os.getenv("ADMIN_USERNAME", "").strip()70    admin_password_hash: str = os.getenv("ADMIN_PASSWORD_HASH", "").strip()71    dashboard_session_secret: str = os.getenv("DASHBOARD_SESSION_SECRET", "").strip()72    admin_users_json: str = os.getenv("ADMIN_USERS_JSON", "").strip()73    evidence_selection: bool = os.getenv("EVIDENCE_SELECTION", "1").lower() in {"1", "true", "yes"}74    # Cross-encoder reranking. Off by default: measured on this corpus it costs75    # 60s per question with BAAI/bge-reranker-base (44s at max_length=256) and76    # 7.6s with ms-marco-MiniLM-L-6-v2, against a pool of ~66 chunks on CPU.77    # MPS was slower than CPU at this batch size. Enable only with a GPU or a78    # smaller candidate pool.79    cross_encoder_rerank: bool = os.getenv("CROSS_ENCODER_RERANK", "0").lower() in {"1", "true", "yes"}80    cross_encoder_model: str = os.getenv("CROSS_ENCODER_MODEL", "BAAI/bge-reranker-base").strip()81    cross_encoder_max_length: int = int(os.getenv("CROSS_ENCODER_MAX_LENGTH", "512"))82    cross_encoder_top_n: int = int(os.getenv("CROSS_ENCODER_TOP_N", "40"))83    evidence_selection_model: str = os.getenv("EVIDENCE_SELECTION_MODEL", "").strip()84    # Answers scoring below this retrieval strength are kept for staff review.85    # Set FLAG_MIN_SCORE_GAP above 0 to also flag near-tied top chunks.86    flag_min_top_score: float = float(os.getenv("FLAG_MIN_TOP_SCORE", "0.90"))87    flag_min_score_gap: float = float(os.getenv("FLAG_MIN_SCORE_GAP", "0"))88    # With Supabase configured the local JSONL is redundant, and on an89    # ephemeral Space it is lost on restart anyway. Keep it for local dev.90    chat_log_to_file: bool = os.getenv("CHAT_LOG_TO_FILE", "").lower() in {"1", "true", "yes"}91    debug_mode: bool = os.getenv("FLASK_DEBUG", "0") == "1"92    chat_rate_limit_count: int = int(os.getenv("CHAT_RATE_LIMIT_COUNT", "10"))93    chat_rate_limit_window_seconds: int = int(os.getenv("CHAT_RATE_LIMIT_WINDOW_SECONDS", "60"))94    suggestions_rate_limit_count: int = int(os.getenv("SUGGESTIONS_RATE_LIMIT_COUNT", "30"))95    suggestions_rate_limit_window_seconds: int = int(os.getenv("SUGGESTIONS_RATE_LIMIT_WINDOW_SECONDS", "60"))96    suggestions_verify_retrieval: bool = os.getenv("SUGGESTIONS_VERIFY_RETRIEVAL", "0").lower() in {"1", "true", "yes"}97    conversation_ttl_seconds: int = int(os.getenv("CONVERSATION_TTL_SECONDS", "3600"))98 99 100class SourceDocument(dict):101    pass102 103 104class ConversationTurn(dict):105    pass106 107 108PROJECT_ROOT = Path(__file__).resolve().parent109CHAT_LOG_PATH = PROJECT_ROOT / "logs" / "chat_events.jsonl"110