Source code for herethere.there.ai.config
"""Configuration for %%there ai."""
from dataclasses import dataclass
from pathlib import Path
from herethere.everywhere.config import load_prefixed_env, prefixed_key
@dataclass(frozen=True)
class AIConfig:
"""OpenAI-compatible provider configuration for %%there ai."""
base_url: str
model: str
api_key: str
temperature: float
timeout: float
prompts: tuple[str, ...] | None = None
@classmethod
def load(cls, prefix: str = "there_ai", path: str = None) -> "AIConfig":
"""Load AI configuration from dotenv and environment variables."""
env = load_prefixed_env(prefix=prefix, path=path)
return cls.load_from_dict(env=env, prefix=prefix)
@classmethod
def load_from_dict(cls, *, env: dict[str, str], prefix: str) -> "AIConfig":
base_url = env.get(
prefixed_key(prefix=prefix, key="base_url"),
"https://api.openai.com/v1",
)
model = env.get(prefixed_key(prefix=prefix, key="model"), "").strip()
api_key = env.get(prefixed_key(prefix=prefix, key="api_key"), "")
temperature_text = env.get(
prefixed_key(prefix=prefix, key="temperature"),
"0.2",
)
timeout_text = env.get(prefixed_key(prefix=prefix, key="timeout"), "300")
prompts_text = env.get(prefixed_key(prefix=prefix, key="prompts"), "").strip()
if not model:
raise AIConfigError(
"herethere AI generation is not configured. "
f"Set {prefixed_key(prefix=prefix, key='model')}. "
"For hosted providers, also set "
f"{prefixed_key(prefix=prefix, key='api_key')}. "
"Optionally set "
f"{prefixed_key(prefix=prefix, key='base_url')}."
)
try:
temperature = float(temperature_text)
except ValueError as exc:
raise AIConfigError(
f"{prefixed_key(prefix=prefix, key='temperature')} must be a number, "
f"got {temperature_text!r}."
) from exc
try:
timeout = float(timeout_text)
except ValueError as exc:
raise AIConfigError(
f"{prefixed_key(prefix=prefix, key='timeout')} must be a number, "
f"got {timeout_text!r}."
) from exc
if timeout <= 0:
raise AIConfigError(
f"{prefixed_key(prefix=prefix, key='timeout')} must be greater than 0."
)
return cls(
base_url=base_url.rstrip("/"),
model=model,
api_key=api_key,
temperature=temperature,
timeout=timeout,
prompts=_split_prompt_names(prompts_text) or None,
)
class AIConfigError(RuntimeError):
"""Raised when AI generation is not configured."""
@dataclass
class AIConfigStore:
"""Mutable session state for %%there ai config loading."""
path: str | None = None
_ai_config_store = AIConfigStore()
def _split_prompt_names(value: str) -> tuple[str, ...]:
return tuple(part.strip() for part in value.split(",") if part.strip())
[docs]
def set_ai_config_path(path: str) -> None:
"""Set the session AI config file path used by %%there ai."""
normalized = path.strip()
if not normalized:
raise ValueError("AI config path cannot be empty")
if not Path(normalized).is_file():
raise AIConfigError(f"AI config file does not exist: {normalized}")
_ai_config_store.path = normalized
[docs]
def clear_ai_config_path() -> None:
"""Clear the session AI config path and return to default discovery."""
_ai_config_store.path = None
def get_ai_config() -> AIConfig:
return AIConfig.load(path=_ai_config_store.path)