Files

217 lines
6.3 KiB
Python

import time
from collections import defaultdict
from config import (
MAX_HISTORY_MESSAGES,
MAX_TOKENS_DEFAULT,
DEFAULT_SYSTEM_PROMPT,
IMAGE_DAILY_LIMIT,
MAX_INPUT_TOKENS,
)
# chat_id → list of {"role": ..., "content": ...}
_histories: dict[int, list] = defaultdict(list)
# user_id → settings dict
_user_settings: dict[int, dict] = {}
_system_prompt: str = DEFAULT_SYSTEM_PROMPT
_start_time: float = time.time()
_msg_count: int = 0
_event_counts: dict[str, int] = defaultdict(int)
_last_error: str = ""
# ── History ──────────────────────────────────────────────────
def get_history(chat_id: int) -> list:
return list(_histories[chat_id])
def add_message(chat_id: int, role: str, content):
_histories[chat_id].append({"role": role, "content": content})
if len(_histories[chat_id]) > MAX_HISTORY_MESSAGES:
_histories[chat_id] = _histories[chat_id][-MAX_HISTORY_MESSAGES:]
def clear_history(chat_id: int):
_histories[chat_id].clear()
def clear_all_histories():
_histories.clear()
def pop_last_message(chat_id: int):
"""Remove the most recent message from the history (for error rollback)."""
if _histories[chat_id]:
_histories[chat_id].pop()
def history_summary(chat_id: int, n: int = 6) -> str:
msgs = _histories[chat_id][-n:]
lines = []
for m in msgs:
content = m["content"]
if isinstance(content, list):
text = "[image/media]"
else:
text = str(content)[:200]
lines.append(f"{m['role'].capitalize()}: {text}")
return "\n".join(lines) if lines else "(empty)"
def estimate_tokens(text) -> int:
if text is None:
return 0
if not isinstance(text, str):
text = str(text)
# Conservative enough for cost guarding without provider-specific tokenizers.
return max(1, len(text) // 3)
def estimate_input_tokens(messages: list[dict], system_prompt: str = "") -> int:
total = estimate_tokens(system_prompt)
for msg in messages:
total += estimate_tokens(msg.get("role", ""))
total += estimate_tokens(msg.get("content", ""))
return total
def reset_chat_if_input_too_large(chat_id: int, system_prompt: str = "", keep_last: bool = True) -> bool:
messages = get_history(chat_id)
if estimate_input_tokens(messages, system_prompt) <= MAX_INPUT_TOKENS:
return False
last = messages[-1:] if keep_last else []
clear_history(chat_id)
for msg in last:
add_message(chat_id, msg.get("role", "user"), msg.get("content", ""))
record_event("history_auto_resets")
return True
# ── User settings ─────────────────────────────────────────────
def get_user_settings(user_id: int) -> dict:
if user_id not in _user_settings:
_user_settings[user_id] = {
"max_tokens": MAX_TOKENS_DEFAULT,
"search_enabled": True,
"lang": "auto",
}
return _user_settings[user_id]
def set_user_setting(user_id: int, key: str, value):
get_user_settings(user_id)[key] = value
_autosave()
# ── System prompt ─────────────────────────────────────────────
def get_system_prompt() -> str:
return _system_prompt
def set_system_prompt(prompt: str):
global _system_prompt
_system_prompt = prompt
_autosave()
def reset_system_prompt():
global _system_prompt
_system_prompt = DEFAULT_SYSTEM_PROMPT
_autosave()
def export_settings() -> dict:
return {
"system_prompt": _system_prompt,
"user_settings": {
str(uid): dict(settings)
for uid, settings in _user_settings.items()
},
}
def import_settings(data: dict):
global _system_prompt, _user_settings
if isinstance(data.get("system_prompt"), str):
_system_prompt = data["system_prompt"]
raw_settings = data.get("user_settings", {})
if isinstance(raw_settings, dict):
loaded: dict[int, dict] = {}
for uid, settings in raw_settings.items():
try:
user_id = int(uid)
except (TypeError, ValueError):
continue
if isinstance(settings, dict):
loaded[user_id] = dict(settings)
_user_settings = loaded
# ── Stats ─────────────────────────────────────────────────────
def increment_msg():
global _msg_count
_msg_count += 1
def record_event(name: str, amount: int = 1):
_event_counts[name] += amount
def record_error(message: str):
global _last_error
_event_counts["errors"] += 1
_last_error = str(message)[:300]
def _autosave():
try:
from services import settings as settings_store
settings_store.save()
except Exception:
pass
# ── Image rate limiting (cost guard) ─────────────────────────
_image_log: dict[int, list[float]] = defaultdict(list) # user_id → timestamps
def can_generate_image(user_id: int) -> bool:
cutoff = time.time() - 86400
_image_log[user_id] = [t for t in _image_log[user_id] if t > cutoff]
return len(_image_log[user_id]) < IMAGE_DAILY_LIMIT
def record_image(user_id: int):
_image_log[user_id].append(time.time())
record_event("images_generated")
def images_used_today(user_id: int) -> int:
cutoff = time.time() - 86400
return len([t for t in _image_log[user_id] if t > cutoff])
# ── Stats ─────────────────────────────────────────────────────
def get_stats() -> dict:
elapsed = int(time.time() - _start_time)
h, remainder = divmod(elapsed, 3600)
m, s = divmod(remainder, 60)
return {
"uptime": f"{h}h {m}m {s}s",
"uptime_seconds": elapsed,
"messages_processed": _msg_count,
"active_chats": len([k for k, v in _histories.items() if v]),
"stored_history_messages": sum(len(v) for v in _histories.values()),
"events": dict(_event_counts),
"last_error": _last_error,
}