Coverage for src/asketmc_bot/config.py: 96%
71 statements
« prev ^ index » next coverage.py v7.5.1, created at 2026-04-08 17:14 +0000
« prev ^ index » next coverage.py v7.5.1, created at 2026-04-08 17:14 +0000
1#!/usr/bin/env python3.10
2"""
3config.py — Configuration for the Asketmc RAG Discord bot.
5Goals:
6- Clear separation of concerns: path layout, model/RAG settings, runtime limits.
7- All paths resolve to absolute Paths and live under project root by default.
8- No .env reads here (handled in main.py); values are code-defined for stability.
10Environment overrides (optional, read by your own code where applicable):
11- ASKETMC_VAR_DIR → directory for logs/caches (default: <PROJECT_ROOT>/var)
12- ASKETMC_DATA_DIR → directory for data (default: <PROJECT_ROOT>/data)
13- ASKETMC_PROMPTS_DIR → directory for prompt files (default: <PKG_ROOT>/data)
14"""
16from __future__ import annotations
18from pathlib import Path
19from typing import Pattern, Set
20import asyncio
21import logging
22import os
23import re
25# ──────────────────────────────────────────────────────────────────────────────
26# Paths
27# ──────────────────────────────────────────────────────────────────────────────
29# Package root (.../src/asketmc_bot) and project root (.../LM)
30PKG_ROOT: Path = Path(__file__).resolve().parent
31PROJECT_ROOT: Path = PKG_ROOT.parents[1]
33# Mutable artifacts (logs, caches)
34VAR_ROOT: Path = Path(os.getenv("ASKETMC_VAR_DIR", PROJECT_ROOT / "var")).resolve()
35LOG_DIR: Path = VAR_ROOT / "logs"
36CACHE_PATH: Path = VAR_ROOT / "rag_cache"
37HASH_FILE: Path = CACHE_PATH / "docs_hash.json"
39# Data & prompts
40DATA_ROOT: Path = Path(os.getenv("ASKETMC_DATA_DIR", PROJECT_ROOT / "data")).resolve()
41DOCS_PATH: Path = DATA_ROOT / "parsed"
43PROMPTS_DIR: Path = Path(os.getenv("ASKETMC_PROMPTS_DIR", PKG_ROOT / "data")).resolve()
44PROMPT_STRICT: Path = PROMPTS_DIR / "system_prompt_strict.txt"
45PROMPT_REASON: Path = PROMPTS_DIR / "system_prompt_reason.txt"
46PROMPT_REPHRASE: Path = PROMPTS_DIR / "rephrase.txt"
48# Ensure directories exist (idempotent)
49for _p in (VAR_ROOT, LOG_DIR, CACHE_PATH, DATA_ROOT, PROMPTS_DIR):
50 _p.mkdir(parents=True, exist_ok=True)
52# ──────────────────────────────────────────────────────────────────────────────
53# Rerank settings
54# ──────────────────────────────────────────────────────────────────────────────
56RERANKER_MODEL_NAME: str = "BAAI/bge-reranker-v2-m3"
57RERANK_INPUT_K: int = 18
58RERANK_OUTPUT_K: int = 9
59BATCH_SIZE: int = 8
60MAX_LEN: int = 512
61QUERY_MAX_CHARS: int = 2048
62EXECUTOR_WORKERS: int = 2
63RERANKER_DEVICE: str = "cpu" # or "cuda"
65# ──────────────────────────────────────────────────────────────────────────────
66# Models and parameters
67# ──────────────────────────────────────────────────────────────────────────────
69TOP_K: int = 24
70MIN_SCORE: float = 0.35
72# Remote / local LLM defaults (OpenRouter / Ollama)
73OR_MODEL: str = "xiaomi/mimo-v2-flash:free"
74LOCAL_MODEL: str = "llama3.1:8b"
75OR_MAX_TOKENS: int = 2048
76API_URL: str = "https://openrouter.ai/api/v1/chat/completions"
77OLLAMA_URL: str = "http://127.0.0.1:11434/api/generate"
79# Context building / chunking
80CHUNK_SIZE: int = 512
81CHUNK_OVERLAP: int = 128
82LEMMA_MATCH_RATIO: float = 0.2
83SCORE_RELATIVE_THRESHOLD: float = 0.15
85# Context limits (align with main.py expectations)
86CTX_LEN_REMOTE: int = 20_000 # when using remote model
87CTX_LEN_LOCAL: int = 12_000 # when using local model
89DEBUG: bool = True
91# ──────────────────────────────────────────────────────────────────────────────
92# Stop-words / POS filters (RU-focused with some general noise)
93# ──────────────────────────────────────────────────────────────────────────────
95GOOD_POS: Set[str] = {
96 "NOUN", "ADJF", "ADJS", "VERB", "INFN", "PRTF", "GRND", "NUMR",
97}
99STOP_WORDS: Set[str] = {
100 # pronouns
101 "я", "ты", "он", "она", "оно", "мы", "вы", "они",
102 "мой", "твой", "его", "её", "наш", "ваш", "их", "свой",
103 "меня", "тебя", "него", "неё", "нас", "вас", "них",
104 "мне", "тебе", "ему", "ей", "нам", "вам", "им",
105 "этот", "эта", "это", "эти", "тот", "та", "то", "те",
106 "такой", "такая", "такое", "такие",
107 "кто", "что", "ничто", "никто", "некто", "нечто",
108 "кто-то", "что-то", "кто-нибудь", "что-нибудь",
109 "кто-либо", "что-либо",
110 # prepositions
111 "в", "во", "на", "за", "к", "ко", "с", "со", "от", "перед",
112 "при", "об", "обо", "по", "до", "из", "иза", "без", "для",
113 "над", "под", "между", "около", "через", "про", "среди",
114 "из-за", "из-под", "внутри", "вне", "после", "согласно",
115 # conjunctions
116 "и", "а", "но", "да", "или", "либо", "тоже", "также",
117 "что", "чтобы", "если", "когда", "пока", "хотя", "потому",
118 "поскольку", "так как", "раз", "как", "будто",
119 # particles
120 "ли", "бы", "же", "ведь", "разве", "уж", "то", "де",
121 "даже", "вон", "вот", "лишь", "только", "именно", "как-раз",
122 "чуть-ли", "едва-ли",
123 # modal/parenthetical
124 "можно", "нельзя", "нужно", "надо", "следует", "должно",
125 "может", "наверное", "возможно", "пожалуй", "кажется",
126 "видимо", "якобы", "типа",
127 # copulas/aux
128 "быть", "есть", "нет", "являться", "становиться", "стать",
129 "бывать", "оказаться", "мочь", "смочь",
130 # dummy verbs
131 "делать", "сделать", "делаться", "сказать", "говорить",
132 "думать", "хотеть", "хотеться", "получаться",
133 # interrogatives
134 "где", "куда", "откуда", "зачем", "почему", "когда", "сколько", "как", "каков",
135 # abstract/general nouns
136 "дело", "ситуация", "случай", "момент", "период", "процесс",
137 "способ", "метод", "вариант", "часть", "раз", "два", "три", "несколько", "много",
138 # interjections/phrases
139 "да", "нет", "ага", "угу", "ох", "ах", "ой", "эй", "эх",
140 "ладно", "ок", "ну", "блин", "чёрт", "тьфу",
141 "спасибо", "пожалуйста", "извините", "простите",
142 "здравствуй", "привет", "пока",
143 # chat noise
144 "сорри", "имхо", "лол", "кек", "хех",
145}
147# ──────────────────────────────────────────────────────────────────────────────
148# Runtime constants (used in main.py and RAG stages)
149# ──────────────────────────────────────────────────────────────────────────────
151log = logging.getLogger("asketmc.config")
153def get_conf(name: str, default, typ=None):
154 """
155 Fetch a configuration variable from this module's globals with optional type casting.
156 Allows overriding via monkey-patching in tests; does NOT read .env directly.
157 """
158 try:
159 val = globals().get(name, default)
160 if typ is not None and not isinstance(val, typ):
161 val = typ(val)
162 return val
163 except Exception as e:
164 log.warning("[config.get_conf] Error retrieving %s: %s", name, e)
165 return default
167# Retries, context limits, housekeeping
168OR_RETRIES: int = 3
169HTTP_CONN_LIMIT: int = 5 # max concurrent HTTP sessions
170HTTP_TIMEOUT_TOTAL: int = 240 # total request timeout (seconds)
171OPENROUTER_BLOCK_SEC: int = 120 # circuit-breaker base block (seconds)
172OPENROUTER_BLOCK_MAX_SEC: int = 900 # circuit-breaker max block (seconds)
174USER_LAST_CLEAN: int = 3600 # Cleanup interval for user_last (seconds)
175EMBED_LOG_EVERY: int = 1000 # Log every N embeddings
176LEMMA_CACHE_SIZE: int = 200_000 # Lemmatization cache size
178# Discord and rate limiting
179ALLOWED_CHANNELS: Set[int] = {1384890201544982568} # replace with your channel ID(s)
180MAX_QUESTION_LEN: int = 500
181USER_COOLDOWN: int = 10 # seconds
182REQUEST_SEMAPHORE: asyncio.Semaphore = asyncio.Semaphore(3)
183ALLOWED_CHARS: Pattern[str] = re.compile(r"^[ а-яА-ЯёЁa-zA-Z0-9,.?!()\-—]+$")
185# Administrator permissions
186ADMIN_IDS: Set[int] = {267614224631463937} # Discord user IDs allowed to run admin commands
188__all__ = [
189 # Paths
190 "PKG_ROOT", "PROJECT_ROOT", "VAR_ROOT", "DATA_ROOT", "PROMPTS_DIR",
191 "DOCS_PATH", "CACHE_PATH", "HASH_FILE", "LOG_DIR",
192 "PROMPT_STRICT", "PROMPT_REASON", "PROMPT_REPHRASE",
193 # Rerank
194 "RERANKER_MODEL_NAME", "RERANK_INPUT_K", "RERANK_OUTPUT_K", "BATCH_SIZE",
195 "MAX_LEN", "QUERY_MAX_CHARS", "EXECUTOR_WORKERS", "RERANKER_DEVICE",
196 # Models / RAG params
197 "TOP_K", "MIN_SCORE", "OR_MODEL", "LOCAL_MODEL", "OR_MAX_TOKENS",
198 "API_URL", "OLLAMA_URL",
199 "CHUNK_SIZE", "CHUNK_OVERLAP", "LEMMA_MATCH_RATIO", "SCORE_RELATIVE_THRESHOLD",
200 "CTX_LEN_REMOTE", "CTX_LEN_LOCAL", "DEBUG",
201 "GOOD_POS", "STOP_WORDS",
202 # Runtime constants
203 "get_conf", "OR_RETRIES", "HTTP_CONN_LIMIT", "HTTP_TIMEOUT_TOTAL",
204 "OPENROUTER_BLOCK_SEC", "OPENROUTER_BLOCK_MAX_SEC",
205 "USER_LAST_CLEAN", "EMBED_LOG_EVERY", "LEMMA_CACHE_SIZE",
206 # Discord / limits / admin
207 "ALLOWED_CHANNELS", "MAX_QUESTION_LEN", "USER_COOLDOWN",
208 "REQUEST_SEMAPHORE", "ALLOWED_CHARS", "ADMIN_IDS",
209]