"""Shared constants for the DPS Coach desktop application."""
from __future__ import annotations
import os
import sys
from pathlib import Path
APP_NAME = "DPS Coach"
PROJECT_ROOT = Path(__file__).resolve().parents[1]
SAMPLE_DATA_DIR = PROJECT_ROOT / "sample_data"
DEFAULT_LIMIT_RUNS = 5
MIN_LIMIT_RUNS = 1
MAX_LIMIT_RUNS = 200
SAFETY_LIMIT_RUNS = 20
MCP_SERVER_MODULE = "mcp_server"
PYTHON_EXECUTABLE = Path(sys.executable)
BANNER_IMAGE_PATH = PROJECT_ROOT / "banner.png"
DEFAULT_MODEL_FILENAME = "qwen2.5-7b-instruct-q4_k_m.gguf"
DEFAULT_MODEL_DOWNLOAD_URL = (
"https://huggingface.co/bartowski/Qwen2.5-7B-Instruct-GGUF/resolve/main/Qwen2.5-7B-Instruct-Q4_K_M.gguf"
)
# File content SHA256 (from HuggingFace Large File Pointer Details, not Xet pointer)
DEFAULT_MODEL_SHA256 = "65b8fcd92af6b4fefa935c625d1ac27ea29dcb6ee14589c55a8f115ceaaa1423"
DEFAULT_MODEL_DOWNLOAD_SIZE_TEXT = "~4.7 GB"
DEFAULT_MODEL_MIN_SIZE_MB = 4200.0
FALLBACK_MODEL_FILENAME = "qwen2.5-0.5b-instruct-q4_k_m.gguf"
FALLBACK_MODEL_SHA256 = "9fecc3b3cd76bba89d504f29b616eedf7da85b96540e490ca5824d3f7d2776a0"
FALLBACK_MODEL_MIN_SIZE_MB = 600.0
FALLBACK_MODEL_DOWNLOAD_SIZE_TEXT = "~638 MB"
def _get_runtime_models_dir() -> Path:
"""Return the single source of truth for runtime model storage."""
appdata = os.environ.get("APPDATA")
if not appdata:
raise RuntimeError("APPDATA environment variable not set.")
return Path(appdata) / "DPSCoach" / "models"
def _resolve_model_path(filename: str) -> Path:
"""Resolve model path in runtime directory."""
return _get_runtime_models_dir() / filename
RUNTIME_MODELS_DIR = _get_runtime_models_dir()
DEFAULT_MODEL_PATH = _resolve_model_path(DEFAULT_MODEL_FILENAME)
FALLBACK_MODEL_PATH = _resolve_model_path(FALLBACK_MODEL_FILENAME)
COACH_MAX_TOOL_CALLS = 1
COACH_MAX_RESPONSE_TOKENS = 256
COACH_TEMPERATURE = 0.2
COACH_TOP_P = 0.9
COACH_REPEAT_PENALTY = 1.15
COACH_STOP_SEQUENCES = ["SQL:", "ANSWER:", "You:", "User:"]
COACH_CONTEXT_RECENT_RUNS = 5
COACH_CONTEXT_TOP_SKILLS = 20
COACH_SYSTEM_PROMPT = (
"You are DPS Coach, an analyst for Throne & Liberty combat logs. Never refuse questions.\n"
"Protocol:\n"
"1) Planning step: respond with EXACTLY one line: either `SQL: <single SELECT...>` using the events table (read-only, <=20 lines) or `ANSWER: <short explanation>` if SQL is unnecessary.\n"
"2) If you produced SQL and receive a table, reply with `ANSWER: ...` referencing the data. Do not emit another SQL block."
)
COACH_SUGGESTED_QUESTIONS = [
"Top damage skills?",
"Which skills land the most hits?",
"Crit-heavy skills?",
"Best DPS run?",
"Lowest DPS run?",
"Why did my crit rate drop?",
"Which skill fell off?",
"Show damage trend over time",
"Heavy attack efficiency?",
"Skill efficiency ranking",
]
TL_CLASSES = [
"All Classes",
"Greatsword",
"Sword & Shield",
"Dagger",
"Crossbow",
"Longbow",
"Staff",
"Wand",
]
def resolve_default_log_dir() -> Path:
"""Return the best-effort default TL log directory for Windows installs."""
local_app_data = os.environ.get("LOCALAPPDATA")
if local_app_data:
candidate = Path(local_app_data) / "TL" / "SAVED" / "COMBATLOGS"
if candidate.exists():
return candidate
return candidate
return SAMPLE_DATA_DIR
def resolve_sample_log_dir() -> Path:
return SAMPLE_DATA_DIR