File size: 4,339 Bytes
f39e265
 
 
 
 
 
 
 
 
 
 
 
 
be97847
e287216
f39e265
87149fa
 
 
4638978
e287216
 
be97847
d7d27a3
be97847
d7d27a3
 
 
f39e265
 
63808a2
 
 
87149fa
 
d7d27a3
f39e265
 
 
 
 
 
 
 
e287216
f39e265
 
 
87149fa
f39e265
be97847
d7d27a3
63808a2
87149fa
 
f39e265
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9d8bbd6
 
e287216
9d8bbd6
e287216
f39e265
 
e287216
f39e265
e287216
f39e265
87149fa
f39e265
be97847
d7d27a3
63808a2
87149fa
 
f39e265
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
"""Application configuration, loaded once from the environment."""

import os
from dataclasses import dataclass
from functools import lru_cache
from pathlib import Path

from dotenv import load_dotenv

load_dotenv()

PROJECT_ROOT = Path(__file__).parent

DEFAULT_MODEL = "openrouter/free"
OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1"
DEFAULT_TEMPERATURE = 0.0
# Drafting a cover letter is writing, not analysis: a little sampling gives
# natural prose. Kept low so it does not start inventing detail.
DEFAULT_DRAFTING_TEMPERATURE = 0.4
# The preset actions (ATS keyword tables, detailed reviews) run long, so 2048
# truncated them mid-answer. This leaves room for the profile, job description,
# and conversation history while avoiding an unnecessarily large response cap.
DEFAULT_MAX_TOKENS = 20000
# Prose skill output gets its own response cap.
DEFAULT_SKILL_MAX_TOKENS = 6500
# ATS returns a structured classification for every keyword, so it needs more
# output room than the prose skills.
DEFAULT_ATS_MAX_TOKENS = 12000
DEFAULT_LANGFUSE_HOST = "https://cloud.langfuse.com"
DEFAULT_CHECKPOINT_DB = "checkpoints.sqlite"
# Turns of conversation replayed to the model. Older turns stay in the
# checkpoint (nothing is deleted) but are not resent.
DEFAULT_MAX_HISTORY_TURNS = 10
# Routing and retrieval. ONNX runtime, so no torch dependency.
DEFAULT_EMBEDDING_MODEL = "sentence-transformers/all-MiniLM-L6-v2"
DEFAULT_RETRIEVAL_TOP_K = 10


class ConfigError(RuntimeError):
    """Raised when the environment is missing or misconfigured."""


@dataclass(frozen=True)
class Settings:
    openrouter_api_key: str
    candidate_name: str
    model: str
    temperature: float
    drafting_temperature: float
    max_tokens: int
    skill_max_tokens: int
    ats_max_tokens: int
    max_history_turns: int
    embedding_model: str
    retrieval_top_k: int
    checkpoint_db: Path
    langfuse_public_key: str
    langfuse_secret_key: str
    langfuse_host: str

    @property
    def tracing_enabled(self) -> bool:
        """Tracing turns itself on only when both Langfuse keys are present."""
        return bool(self.langfuse_public_key and self.langfuse_secret_key)


def _get_float(name, default):
    raw = os.getenv(name)
    if raw is None or not raw.strip():
        return default
    try:
        return float(raw)
    except ValueError:
        raise ConfigError(f"{name} must be a number, got {raw!r}")


def _get_int(name, default):
    raw = os.getenv(name)
    if raw is None or not raw.strip():
        return default
    try:
        return int(raw)
    except ValueError:
        raise ConfigError(f"{name} must be an integer, got {raw!r}")


@lru_cache(maxsize=2)
def get_settings(require_api_key=True) -> Settings:
    api_key = os.getenv("OPENROUTER_API_KEY", "").strip()
    if require_api_key and not api_key:
        raise ConfigError("OPENROUTER_API_KEY is not set. Add it to your .env file.")

    return Settings(
        openrouter_api_key=api_key,
        candidate_name=os.getenv("CANDIDATE_NAME", "").strip() or "the candidate",
        model=os.getenv("OPENROUTER_MODEL", "").strip() or DEFAULT_MODEL,
        temperature=_get_float("LLM_TEMPERATURE", DEFAULT_TEMPERATURE),
        drafting_temperature=_get_float("LLM_DRAFTING_TEMPERATURE", DEFAULT_DRAFTING_TEMPERATURE),
        max_tokens=_get_int("LLM_MAX_TOKENS", DEFAULT_MAX_TOKENS),
        skill_max_tokens=_get_int("SKILL_MAX_TOKENS", DEFAULT_SKILL_MAX_TOKENS),
        ats_max_tokens=_get_int("ATS_MAX_TOKENS", DEFAULT_ATS_MAX_TOKENS),
        max_history_turns=_get_int("MAX_HISTORY_TURNS", DEFAULT_MAX_HISTORY_TURNS),
        embedding_model=os.getenv("EMBEDDING_MODEL", "").strip() or DEFAULT_EMBEDDING_MODEL,
        retrieval_top_k=_get_int("RETRIEVAL_TOP_K", DEFAULT_RETRIEVAL_TOP_K),
        checkpoint_db=PROJECT_ROOT / (
            os.getenv("CHECKPOINT_DB", "").strip() or DEFAULT_CHECKPOINT_DB
        ),
        langfuse_public_key=os.getenv("LANGFUSE_PUBLIC_KEY", "").strip(),
        langfuse_secret_key=os.getenv("LANGFUSE_SECRET_KEY", "").strip(),
        # LANGFUSE_HOST is the SDK's own name; LANGFUSE_BASE_URL is accepted as an alias.
        langfuse_host=(
            os.getenv("LANGFUSE_HOST", "").strip()
            or os.getenv("LANGFUSE_BASE_URL", "").strip()
            or DEFAULT_LANGFUSE_HOST
        ),
    )