import os from pathlib import Path import yaml from dotenv import load_dotenv load_dotenv() def _yaml_get(*keys, default=None): _CONFIG_PATH = Path(__file__).resolve().parent / "config.yaml" _yaml = {} if _CONFIG_PATH.is_file(): with open(_CONFIG_PATH, "r", encoding="utf-8") as file_object: _yaml = yaml.safe_load(file_object) or {} current = _yaml for key in keys: if key in current: current = current[key] else: return default return current if current is not None else default ragroleplay_db_path = _yaml_get("ragroleplay", "db_path", default="./.ragroleplay" ) ragroleplay_vector_size = _yaml_get("ragroleplay", "vector_size", default=768 ) # used on table create only ragroleplay_model_url = _yaml_get("ragroleplay", "model_url", default="http://localhost:11434/api/embed" ) ragroleplay_model_name = _yaml_get("ragroleplay", "model_name", default="nomic-embed-text" ) # Need to download model from local ollama llm_timeout = int(_yaml_get("llm", "timeout", default=600)) _providers = _yaml_get("llm", "providers", default=[]) MODELS_ITEMS = [] for prov in _providers: pname = prov.get("name", "") base_url = prov.get("base_url", "").rstrip("/") api_key = prov.get("api_key", "") models = prov.get("models", []) for m in models: model_name = m.get("name", "") is_default = m.get("default", False) MODELS_ITEMS.append({ "model" : model_name, "provider" : pname, "base_url" : base_url, "api_key" : api_key, "default" : is_default, }) def resolve_provider(base_url: str, model: str) -> str | None: base_url = base_url.rstrip("/") for item in MODELS_ITEMS: if item["base_url"] == base_url and item["model"] == model: return item["provider"] return None # Cari model default — pertama yg marked default: true, fallback ke yg pertama llm_baseurl = "" llm_model = "" llm_api_key = "" for item in MODELS_ITEMS: if item.get("default"): llm_baseurl = item["base_url" ] llm_model = item["model" ] llm_api_key = item["api_key" ] break if not llm_model and MODELS_ITEMS: llm_baseurl = MODELS_ITEMS[0]["base_url" ] llm_model = MODELS_ITEMS[0]["model" ] llm_api_key = MODELS_ITEMS[0]["api_key" ] XMPP_USERNAME = os.getenv("XMPP_USERNAME", default="") XMPP_PASSWORD = os.getenv("XMPP_PASSWORD", default="") AGENT_MAX_ITERATIONS = int(os.getenv("AGENT_MAX_ITERATIONS", default=_yaml_get("agent", "max_iterations", default="30"))) AGENT_MAX_TOOL_OUTPUT = int(os.getenv("AGENT_MAX_TOOL_OUTPUT", default=_yaml_get("agent", "max_tool_output", default="40000"))) AGENT_SKILL = "programmer" PERSONALITY_NAME = "Hendrik" PERSONALITY_AGE = "" PERSONALITY_GENDER = "" PERSONALITY_TONE = "casual" PERSONALITY_VERBOSITY = "balanced" PERSONALITY_HUMOR = "light" PERSONALITY_LANGUAGE = "id" PERSONALITY_MOOD = "cheerful" PERSONALITY_CATCHPHRASES = "" # ─── Character & Skills ──────────────────────────────────────────────────────── AGENT_CHARACTER = _yaml_get("agent", "character", default="").strip().lower() AGENT_SKILLS = os.getenv("AGENT_SKILLS", default="").strip().lower() # ─── XMPP (non-credential dari YAML, credential dari .env) ───────────────────── XMPP_ENABLED = os.getenv("XMPP_ENABLED", default=str(_yaml_get("xmpp", "enabled", default="false"))).strip().lower() in ("true", "1", "yes") XMPP_MUC_ROOMS = os.getenv("XMPP_MUC_ROOMS", default=_yaml_get("xmpp", "muc_rooms", default="")).strip() XMPP_NICKNAME = os.getenv("XMPP_NICKNAME", default=_yaml_get("xmpp", "nickname", default="")).strip() XMPP_SELECTIVE_RESPONSE = os.getenv("XMPP_SELECTIVE_RESPONSE", default=str(_yaml_get("xmpp", "selective_response", default="true"))).strip().lower() in ("true", "1", "yes") # ─── Telegram (non-credential dari YAML, credential dari .env) ───────────────── TELEGRAM_ENABLED = os.getenv("TELEGRAM_ENABLED", default=str(_yaml_get("telegram", "enabled", default="false"))).strip().lower() in ("true", "1", "yes") TELEGRAM_TOKEN = os.getenv("TELEGRAM_TOKEN", default="") TELEGRAM_ALLOWED_GROUP_IDS = os.getenv("TELEGRAM_ALLOWED_GROUP_IDS", default=_yaml_get("telegram", "allowed_group_ids", default="")).strip() TELEGRAM_SELECTIVE_RESPONSE = os.getenv("TELEGRAM_SELECTIVE_RESPONSE", default=str(_yaml_get("telegram", "selective_response", default="true"))).strip().lower() in ("true", "1", "yes") # ─── Session (TinyDB) ──────────────────────────────────────────────────────────── SESSION_DB_PATH = os.path.expanduser( os.getenv("SESSION_DB_PATH", default=_yaml_get("session", "db_path", default="~/.config/hendrik/sessions.json")) ) # Humanize Delay (YAML) READ_DELAY_MIN = float( _yaml_get("delay", "read_min", default="1.0" ) ) READ_DELAY_MAX = float( _yaml_get("delay", "read_max", default="2.0" ) ) TYPING_SPEED = float( _yaml_get("delay", "typing_speed", default="15.0" ) ) TYPING_MAX = float( _yaml_get("delay", "typing_max", default="10.0" ) ) # ─── Character Loader ────────────────────────────────────────────────────────── CHARACTERS_DIR = Path(__file__).resolve().parent / "agent" / "characters" _yaml_to_config_key = { "AGENT_SKILL": "skill", "PERSONALITY_NAME": "name", "PERSONALITY_AGE": "age", "PERSONALITY_GENDER": "gender", "PERSONALITY_TONE": "tone", "PERSONALITY_VERBOSITY": "verbosity", "PERSONALITY_HUMOR": "humor", "PERSONALITY_LANGUAGE": "language", "PERSONALITY_MOOD": "mood", "PERSONALITY_CATCHPHRASES": "catchphrases", } _lowercase_keys = {"AGENT_SKILL", "PERSONALITY_TONE", "PERSONALITY_VERBOSITY", "PERSONALITY_HUMOR", "PERSONALITY_LANGUAGE", "PERSONALITY_MOOD"} def load_character(char_override=None): """Load personality dari character directory. Bisa di-call ulang untuk override.""" global AGENT_CHARACTER, AGENT_SKILL, PERSONALITY_NAME, PERSONALITY_AGE, PERSONALITY_GENDER global PERSONALITY_TONE, PERSONALITY_VERBOSITY, PERSONALITY_HUMOR, PERSONALITY_LANGUAGE global PERSONALITY_MOOD, PERSONALITY_CATCHPHRASES if char_override: AGENT_CHARACTER = char_override.strip().lower() if not AGENT_CHARACTER: return personality_yaml_path = CHARACTERS_DIR / AGENT_CHARACTER / "personality.yaml" character_config_path = CHARACTERS_DIR / AGENT_CHARACTER / "character.md" if personality_yaml_path.is_file(): try: _character_env = yaml.safe_load(personality_yaml_path.read_text(encoding="utf-8")) or {} if not isinstance(_character_env, dict): _character_env = {} except Exception as _e: print(f"[config] Warning: gagal load personality.yaml untuk '{AGENT_CHARACTER}': {_e}", flush=True) _character_env = {} elif character_config_path.is_file(): _character_env = {} for line in character_config_path.read_text(encoding="utf-8").splitlines(): line = line.strip() if not line or line.startswith("#"): continue if "=" in line: key, value = line.split("=", 1) _character_env[key.strip()] = value.strip() else: return if not _character_env: return _defaults = { "AGENT_SKILL": AGENT_SKILL, "PERSONALITY_NAME": PERSONALITY_NAME, "PERSONALITY_AGE": PERSONALITY_AGE, "PERSONALITY_GENDER": PERSONALITY_GENDER, "PERSONALITY_TONE": PERSONALITY_TONE, "PERSONALITY_VERBOSITY": PERSONALITY_VERBOSITY, "PERSONALITY_HUMOR": PERSONALITY_HUMOR, "PERSONALITY_LANGUAGE": PERSONALITY_LANGUAGE, "PERSONALITY_MOOD": PERSONALITY_MOOD, "PERSONALITY_CATCHPHRASES": PERSONALITY_CATCHPHRASES, } for key, fallback in _defaults.items(): yaml_key = _yaml_to_config_key.get(key, key.lower()) raw = _character_env.get(yaml_key, _character_env.get(key, fallback)) value = str(raw).strip() if raw is not None else "" if key in _lowercase_keys: value = value.lower() or fallback if key == "PERSONALITY_NAME" and not value: value = fallback _defaults[key] = value for key, value in _defaults.items(): globals()[key] = value # Load character dari config.yaml saat import time load_character()