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

1#!/usr/bin/env python3.10 

2""" 

3config.py — Configuration for the Asketmc RAG Discord bot. 

4 

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. 

9 

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""" 

15 

16from __future__ import annotations 

17 

18from pathlib import Path 

19from typing import Pattern, Set 

20import asyncio 

21import logging 

22import os 

23import re 

24 

25# ────────────────────────────────────────────────────────────────────────────── 

26# Paths 

27# ────────────────────────────────────────────────────────────────────────────── 

28 

29# Package root (.../src/asketmc_bot) and project root (.../LM) 

30PKG_ROOT: Path = Path(__file__).resolve().parent 

31PROJECT_ROOT: Path = PKG_ROOT.parents[1] 

32 

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" 

38 

39# Data & prompts 

40DATA_ROOT: Path = Path(os.getenv("ASKETMC_DATA_DIR", PROJECT_ROOT / "data")).resolve() 

41DOCS_PATH: Path = DATA_ROOT / "parsed" 

42 

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" 

47 

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) 

51 

52# ────────────────────────────────────────────────────────────────────────────── 

53# Rerank settings 

54# ────────────────────────────────────────────────────────────────────────────── 

55 

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" 

64 

65# ────────────────────────────────────────────────────────────────────────────── 

66# Models and parameters 

67# ────────────────────────────────────────────────────────────────────────────── 

68 

69TOP_K: int = 24 

70MIN_SCORE: float = 0.35 

71 

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" 

78 

79# Context building / chunking 

80CHUNK_SIZE: int = 512 

81CHUNK_OVERLAP: int = 128 

82LEMMA_MATCH_RATIO: float = 0.2 

83SCORE_RELATIVE_THRESHOLD: float = 0.15 

84 

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 

88 

89DEBUG: bool = True 

90 

91# ────────────────────────────────────────────────────────────────────────────── 

92# Stop-words / POS filters (RU-focused with some general noise) 

93# ────────────────────────────────────────────────────────────────────────────── 

94 

95GOOD_POS: Set[str] = { 

96 "NOUN", "ADJF", "ADJS", "VERB", "INFN", "PRTF", "GRND", "NUMR", 

97} 

98 

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} 

146 

147# ────────────────────────────────────────────────────────────────────────────── 

148# Runtime constants (used in main.py and RAG stages) 

149# ────────────────────────────────────────────────────────────────────────────── 

150 

151log = logging.getLogger("asketmc.config") 

152 

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 

166 

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) 

173 

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 

177 

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,.?!()\-—]+$") 

184 

185# Administrator permissions 

186ADMIN_IDS: Set[int] = {267614224631463937} # Discord user IDs allowed to run admin commands 

187 

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]