hendrik/config.py

330 lines
13 KiB
Python

import os
from pathlib import Path
import yaml
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
def _as_list(value):
if isinstance(value, list):
return [str(v).strip() for v in value if str(v).strip()]
if value:
print(f"[config] Warning: '{value}' bukan YAML list, diabaikan. Gunakan format list.", flush=True)
return []
TUI_USER_ID = _yaml_get("user", "id", default=None)
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
# Model Option (moop) — penyimpanan model/provider/key pindah ke sqlite.
moop_db_path = os.path.expanduser(
_yaml_get("moop", "db_path", default="~/.config/hendrik/moop.sqlite3")
)
llm_timeout = int(_yaml_get("llm", "timeout", default=600))
XMPP_USERNAME = _yaml_get("xmpp", "username", default="")
XMPP_PASSWORD = _yaml_get("xmpp", "password", default="")
AGENT_MAX_ITERATIONS = int(_yaml_get("agent", "max_iterations", default="30"))
AGENT_MAX_TOOL_OUTPUT = int(_yaml_get("agent", "max_tool_output", default="40000"))
PERSONALITY_NAME = ""
PERSONALITY_CODENAME = ""
# ─── Character & Skills ────────────────────────────────────────────────────────
AGENT_CHARACTER = _yaml_get("agent", "character", default="").strip().lower()
AGENT_SKILLS = _yaml_get("agent", "skills", default="").strip().lower()
# ─── XMPP ──────────────────────────────────────────────────────────────────────
XMPP_ENABLED = str(_yaml_get("xmpp", "enabled", default="false")).strip().lower() in ("true", "1", "yes")
XMPP_MUC_ROOMS = _as_list(_yaml_get("xmpp", "muc_rooms", default=[]))
XMPP_NICKNAME = _yaml_get("xmpp", "nickname", default="").strip()
XMPP_DEBUG = str(_yaml_get("xmpp", "debug", default="false")).strip().lower() in ("true", "1", "yes")
XMPP_SELECTIVE_RESPONSE = str(_yaml_get("xmpp", "selective_response", default="true")).strip().lower() in ("true", "1", "yes")
# ─── Telegram ──────────────────────────────────────────────────────────────────
TELEGRAM_ENABLED = str(_yaml_get("telegram", "enabled", default="false")).strip().lower() in ("true", "1", "yes")
TELEGRAM_TOKEN = _yaml_get("telegram", "token", default="")
TELEGRAM_ALLOWED_GROUP_IDS = _as_list(_yaml_get("telegram", "allowed_group_ids", default=[]))
TELEGRAM_SELECTIVE_RESPONSE = str(_yaml_get("telegram", "selective_response", default="true")).strip().lower() in ("true", "1", "yes")
# ─── Session (TinyDB) ────────────────────────────────────────────────────────────
SESSION_DB_PATH = os.path.expanduser(
_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" ) )
# ─── Image Generation (OpenRouter) ──────────────────────────────────────────────
# IMAGEGEN model kini dikelola via moop (sqlite), bukan config.yaml.
# ─── Character Loader ──────────────────────────────────────────────────────────
CHARACTERS_DIR = Path(__file__).resolve().parent / "agent" / "characters"
def _normalize_skills(value) -> str:
"""Normalisasi field 'skill' dari info.yaml (YAML list atau CSV string)
menjadi CSV lowercase tanpa spasi, mis. 'roleplayer,programmer'."""
if value is None:
return ""
if isinstance(value, list):
parts = [str(v).strip().lower() for v in value if str(v).strip()]
else:
parts = [p.strip().lower() for p in str(value).split(",") if p.strip()]
return ",".join(parts)
def load_character(char_override=None):
"""Load character info dari info.yaml. Bisa di-call ulang untuk override."""
global AGENT_CHARACTER, PERSONALITY_NAME, PERSONALITY_CODENAME, AGENT_SKILLS
if char_override:
AGENT_CHARACTER = char_override.strip().lower()
if not AGENT_CHARACTER:
return
info_yaml_path = CHARACTERS_DIR / AGENT_CHARACTER / "info.yaml"
if not info_yaml_path.is_file():
return
try:
data = yaml.safe_load(info_yaml_path.read_text(encoding="utf-8")) or {}
if not isinstance(data, dict):
data = {}
except Exception as _e:
print(f"[config] Warning: gagal load info.yaml untuk '{AGENT_CHARACTER}': {_e}", flush=True)
data = {}
if not data:
return
name = str(data.get("name", "")).strip()
codename = str(data.get("codename", "")).strip()
if name:
PERSONALITY_NAME = name
if codename:
PERSONALITY_CODENAME = codename
elif name:
PERSONALITY_CODENAME = name
# Skill: sumber utama adalah karakter (info.yaml), config.yaml hanya fallback
char_skills = _normalize_skills(data.get("skill", data.get("mode")))
if char_skills:
AGENT_SKILLS = char_skills
SERVICE_YAML_PATH = None # Path service.yaml milik karakter aktif, jika ada
def _reset_service_from_config():
"""Reset konfigurasi service (xmpp/telegram) dari config.yaml global."""
global XMPP_ENABLED, XMPP_USERNAME, XMPP_PASSWORD, XMPP_MUC_ROOMS
global XMPP_NICKNAME, XMPP_DEBUG, XMPP_SELECTIVE_RESPONSE
global TELEGRAM_ENABLED, TELEGRAM_TOKEN, TELEGRAM_ALLOWED_GROUP_IDS
global TELEGRAM_SELECTIVE_RESPONSE, SERVICE_YAML_PATH
XMPP_USERNAME = _yaml_get("xmpp", "username", default="")
XMPP_PASSWORD = _yaml_get("xmpp", "password", default="")
XMPP_ENABLED = str(_yaml_get("xmpp", "enabled", default="false")).strip().lower() in ("true", "1", "yes")
XMPP_MUC_ROOMS = _as_list(_yaml_get("xmpp", "muc_rooms", default=[]))
XMPP_NICKNAME = _yaml_get("xmpp", "nickname", default="").strip()
XMPP_DEBUG = str(_yaml_get("xmpp", "debug", default="false")).strip().lower() in ("true", "1", "yes")
XMPP_SELECTIVE_RESPONSE = str(_yaml_get("xmpp", "selective_response", default="true")).strip().lower() in ("true", "1", "yes")
TELEGRAM_ENABLED = str(_yaml_get("telegram", "enabled", default="false")).strip().lower() in ("true", "1", "yes")
TELEGRAM_TOKEN = _yaml_get("telegram", "token", default="")
TELEGRAM_ALLOWED_GROUP_IDS = _as_list(_yaml_get("telegram", "allowed_group_ids", default=[]))
TELEGRAM_SELECTIVE_RESPONSE = str(_yaml_get("telegram", "selective_response", default="true")).strip().lower() in ("true", "1", "yes")
SERVICE_YAML_PATH = None
def load_service_config(char_override=None):
"""Override konfigurasi service (xmpp/telegram) dari service.yaml milik
karakter aktif, jika file tersebut ada di agent/characters/<char>/.
Memanggil load_character() bila char_override diberikan."""
global XMPP_ENABLED, XMPP_USERNAME, XMPP_PASSWORD, XMPP_MUC_ROOMS
global XMPP_NICKNAME, XMPP_DEBUG, XMPP_SELECTIVE_RESPONSE
global TELEGRAM_ENABLED, TELEGRAM_TOKEN, TELEGRAM_ALLOWED_GROUP_IDS
global TELEGRAM_SELECTIVE_RESPONSE, SERVICE_YAML_PATH
_reset_service_from_config()
if char_override:
load_character(char_override=char_override)
if not AGENT_CHARACTER:
return
service_yaml_path = CHARACTERS_DIR / AGENT_CHARACTER / "service.yaml"
if not service_yaml_path.is_file():
return
try:
data = yaml.safe_load(service_yaml_path.read_text(encoding="utf-8")) or {}
if not isinstance(data, dict):
data = {}
except Exception as _e:
print(f"[config] Warning: gagal load service.yaml untuk '{AGENT_CHARACTER}': {_e}", flush=True)
data = {}
if not data:
return
SERVICE_YAML_PATH = service_yaml_path
xmpp_data = data.get("xmpp") or {}
if "enabled" in xmpp_data:
XMPP_ENABLED = str(xmpp_data["enabled"]).strip().lower() in ("true", "1", "yes")
if "username" in xmpp_data:
XMPP_USERNAME = str(xmpp_data["username"]).strip()
if "password" in xmpp_data:
XMPP_PASSWORD = str(xmpp_data["password"]).strip()
if "muc_rooms" in xmpp_data:
XMPP_MUC_ROOMS = _as_list(xmpp_data["muc_rooms"])
if "nickname" in xmpp_data:
XMPP_NICKNAME = str(xmpp_data["nickname"]).strip()
if "debug" in xmpp_data:
XMPP_DEBUG = str(xmpp_data["debug"]).strip().lower() in ("true", "1", "yes")
if "selective_response" in xmpp_data:
XMPP_SELECTIVE_RESPONSE = str(xmpp_data["selective_response"]).strip().lower() in ("true", "1", "yes")
telegram_data = data.get("telegram") or {}
if "enabled" in telegram_data:
TELEGRAM_ENABLED = str(telegram_data["enabled"]).strip().lower() in ("true", "1", "yes")
if "token" in telegram_data:
TELEGRAM_TOKEN = str(telegram_data["token"]).strip()
if "allowed_group_ids" in telegram_data:
TELEGRAM_ALLOWED_GROUP_IDS = _as_list(telegram_data["allowed_group_ids"])
if "selective_response" in telegram_data:
TELEGRAM_SELECTIVE_RESPONSE = str(telegram_data["selective_response"]).strip().lower() in ("true", "1", "yes")
# ─── Model Config (model.yaml per karakter) ────────────────────────────────────
MODEL_YAML_PATH = None # Path model.yaml milik karakter aktif, jika ada
CHARACTER_MODELS = {} # {type_name: model_set_name} dari model.yaml
def _reset_model_config():
"""Reset konfigurasi model dari model.yaml."""
global MODEL_YAML_PATH, CHARACTER_MODELS
MODEL_YAML_PATH = None
CHARACTER_MODELS = {}
def load_model_config(char_override=None):
"""Terapkan model.yaml milik karakter aktif ke default moop (sqlite).
Jika file agent/characters/<char>/model.yaml ada, tiap tipe yang terisi
(llm/embedding/imagegen/imagevision) akan dijadikan default model set
untuk tipe tersebut. Tipe kosong/missing tidak diubah; karakter tanpa
model.yaml akan memakai default yang sudah ada.
Mengubah default DB (moop.set_default) sehingga berlaku persist, sama
seperti aksi 'set as default' di TUI. import moop dilakukan lazy untuk
menghindari circular import (moop meng-import config).
"""
global MODEL_YAML_PATH, CHARACTER_MODELS
_reset_model_config()
if char_override:
cfg_char = char_override.strip().lower()
else:
cfg_char = AGENT_CHARACTER
if not cfg_char:
return
model_yaml_path = CHARACTERS_DIR / cfg_char / "model.yaml"
if not model_yaml_path.is_file():
return
try:
data = yaml.safe_load(model_yaml_path.read_text(encoding="utf-8")) or {}
if not isinstance(data, dict):
data = {}
except Exception as _e:
print(f"[config] Warning: gagal load model.yaml untuk '{cfg_char}': {_e}", flush=True)
return
if not data:
return
from lib import moop # lazy import
applied = []
for tname in moop.MODEL_TYPES:
value = data.get(tname)
if value is None:
continue
set_name = str(value).strip()
if not set_name:
continue
s = moop.get_set_by_name(None, set_name)
if not s:
print(
f"[config] Warning: model.yaml '{cfg_char}' merujuk set "
f"'{set_name}' untuk tipe '{tname}', tapi set tidak ditemukan. Diabaikan.",
flush=True,
)
continue
chain = moop.resolve_chain(None, s["id"])
if not chain:
print(
f"[config] Warning: model.yaml '{cfg_char}' merujuk set "
f"'{set_name}' untuk tipe '{tname}', tapi set tidak punya model aktif. Diabaikan.",
flush=True,
)
continue
moop.set_default(None, tname, s["id"])
CHARACTER_MODELS[tname] = set_name
applied.append(f"{tname}={set_name}")
if applied:
MODEL_YAML_PATH = model_yaml_path
print(f"[config] Model config dari model.yaml: {', '.join(applied)}", flush=True)
# Load character & service dari config.yaml saat import time
load_character()
load_service_config()