139
ai-web-agent-console/ai_web_agent_console/profile.py
Normal file
139
ai-web-agent-console/ai_web_agent_console/profile.py
Normal file
@@ -0,0 +1,139 @@
|
||||
"""Configuration-backed product profile for a reusable agent console."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Any, Mapping
|
||||
|
||||
|
||||
class AppProfileError(RuntimeError):
|
||||
"""Safe error for an invalid product profile."""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AppProfile:
|
||||
product_name: str
|
||||
short_name: str
|
||||
page_title: str
|
||||
page_icon: str
|
||||
header_title: str
|
||||
header_description: str
|
||||
login_kicker: str
|
||||
login_title: str
|
||||
login_description: str
|
||||
login_footer: str
|
||||
primary_color: str
|
||||
text_color: str
|
||||
muted_color: str
|
||||
border_color: str
|
||||
|
||||
|
||||
_ENV_FIELD_NAMES = {
|
||||
"product_name": "AGENT_CONSOLE_NAME",
|
||||
"short_name": "AGENT_CONSOLE_SHORT_NAME",
|
||||
"page_title": "AGENT_CONSOLE_PAGE_TITLE",
|
||||
"page_icon": "AGENT_CONSOLE_PAGE_ICON",
|
||||
"header_title": "AGENT_CONSOLE_HEADER_TITLE",
|
||||
"header_description": "AGENT_CONSOLE_HEADER_DESCRIPTION",
|
||||
"login_kicker": "AGENT_CONSOLE_LOGIN_KICKER",
|
||||
"login_title": "AGENT_CONSOLE_LOGIN_TITLE",
|
||||
"login_description": "AGENT_CONSOLE_LOGIN_DESCRIPTION",
|
||||
"login_footer": "AGENT_CONSOLE_LOGIN_FOOTER",
|
||||
"primary_color": "AGENT_CONSOLE_PRIMARY_COLOR",
|
||||
"text_color": "AGENT_CONSOLE_TEXT_COLOR",
|
||||
"muted_color": "AGENT_CONSOLE_MUTED_COLOR",
|
||||
"border_color": "AGENT_CONSOLE_BORDER_COLOR",
|
||||
}
|
||||
|
||||
|
||||
def _string(section: Mapping[str, Any], key: str, fallback: str = "") -> str:
|
||||
return str(section.get(key) or fallback).strip()
|
||||
|
||||
|
||||
def resolve_profile_path(default_path: Path) -> Path:
|
||||
configured = os.environ.get("AGENT_CONSOLE_PROFILE_PATH", "").strip()
|
||||
if not configured:
|
||||
return default_path
|
||||
path = Path(configured).expanduser()
|
||||
return path if path.is_absolute() else default_path.parent / path
|
||||
|
||||
|
||||
def _dotenv_value(name: str, path: Path | None) -> str:
|
||||
"""Read one simple KEY=value runtime setting without importing a dotenv lib."""
|
||||
|
||||
if path is None:
|
||||
return ""
|
||||
try:
|
||||
lines = path.read_text(encoding="utf-8").splitlines()
|
||||
except (OSError, UnicodeError):
|
||||
return ""
|
||||
prefix = f"{name}="
|
||||
for line in lines:
|
||||
stripped = line.strip()
|
||||
if stripped.startswith(prefix):
|
||||
return stripped[len(prefix) :].strip().strip('"').strip("'")
|
||||
return ""
|
||||
|
||||
|
||||
def _apply_environment_overrides(profile: AppProfile, env_file: Path | None) -> AppProfile:
|
||||
"""Apply deployment-specific presentation values without a code change."""
|
||||
|
||||
values = {
|
||||
field_name: (
|
||||
os.environ.get(env_name, "").strip()
|
||||
or _dotenv_value(env_name, env_file)
|
||||
or getattr(profile, field_name)
|
||||
)
|
||||
for field_name, env_name in _ENV_FIELD_NAMES.items()
|
||||
}
|
||||
return AppProfile(**values)
|
||||
|
||||
|
||||
def load_app_profile(default_path: Path, env_file: Path | None = None) -> AppProfile:
|
||||
"""Load the selectable product skin without coupling it to a PoC name."""
|
||||
|
||||
path = resolve_profile_path(default_path)
|
||||
try:
|
||||
payload: Any = json.loads(path.read_text(encoding="utf-8"))
|
||||
except (OSError, UnicodeError, ValueError):
|
||||
raise AppProfileError(f"애플리케이션 프로필을 읽지 못했습니다: {path}") from None
|
||||
if not isinstance(payload, Mapping):
|
||||
raise AppProfileError("애플리케이션 프로필 형식이 올바르지 않습니다.")
|
||||
product = payload.get("product")
|
||||
theme = payload.get("theme")
|
||||
if not isinstance(product, Mapping) or not isinstance(theme, Mapping):
|
||||
raise AppProfileError("애플리케이션 프로필에 product와 theme 객체가 필요합니다.")
|
||||
|
||||
profile = _apply_environment_overrides(AppProfile(
|
||||
product_name=_string(product, "name"),
|
||||
short_name=_string(product, "short_name"),
|
||||
page_title=_string(product, "page_title"),
|
||||
page_icon=_string(product, "page_icon", "🤖"),
|
||||
header_title=_string(product, "header_title"),
|
||||
header_description=_string(product, "header_description"),
|
||||
login_kicker=_string(product, "login_kicker"),
|
||||
login_title=_string(product, "login_title"),
|
||||
login_description=_string(product, "login_description"),
|
||||
login_footer=_string(product, "login_footer"),
|
||||
primary_color=_string(theme, "primary_color"),
|
||||
text_color=_string(theme, "text_color"),
|
||||
muted_color=_string(theme, "muted_color"),
|
||||
border_color=_string(theme, "border_color"),
|
||||
), env_file)
|
||||
required = (
|
||||
profile.product_name,
|
||||
profile.short_name,
|
||||
profile.page_title,
|
||||
profile.header_title,
|
||||
profile.login_title,
|
||||
profile.primary_color,
|
||||
profile.text_color,
|
||||
profile.muted_color,
|
||||
profile.border_color,
|
||||
)
|
||||
if not all(required):
|
||||
raise AppProfileError("애플리케이션 프로필의 필수 표시값 또는 색상이 비어 있습니다.")
|
||||
return profile
|
||||
Reference in New Issue
Block a user