mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-01 12:02:05 +00:00
_normalize_preset uses bare float() and int() to coerce reference_temperature, aggregator_temperature, and max_tokens from config.yaml. When a user hand-edits a non-numeric value (e.g. max_tokens: "8k" or reference_temperature: "hot"), the coercion raises ValueError. Since normalize_moa_config runs on every model-selection and MoA turn (via resolve_moa_preset), the crash is unrecoverable and blocks all MoA usage until the config is manually fixed. Replace the bare casts with _coerce_float / _coerce_int helpers that fall back to the default on TypeError/ValueError instead of raising.
195 lines
6.4 KiB
Python
195 lines
6.4 KiB
Python
"""Mixture-of-Agents configuration and slash-command helpers."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import base64
|
|
import json
|
|
from copy import deepcopy
|
|
from typing import Any
|
|
|
|
MOA_MARKER_PREFIX = "__HERMES_MOA_TURN_V1__"
|
|
DEFAULT_MOA_PRESET_NAME = "default"
|
|
|
|
DEFAULT_MOA_REFERENCE_MODELS: list[dict[str, str]] = [
|
|
{"provider": "openai-codex", "model": "gpt-5.5"},
|
|
{"provider": "openrouter", "model": "deepseek/deepseek-v4-pro"},
|
|
]
|
|
|
|
DEFAULT_MOA_AGGREGATOR: dict[str, str] = {
|
|
"provider": "openrouter",
|
|
"model": "anthropic/claude-opus-4.8",
|
|
}
|
|
|
|
|
|
def _coerce_float(value: Any, default: float) -> float:
|
|
if value is None or value == "":
|
|
return default
|
|
try:
|
|
return float(value)
|
|
except (TypeError, ValueError):
|
|
return default
|
|
|
|
|
|
def _coerce_int(value: Any, default: int) -> int:
|
|
if value is None or value == "":
|
|
return default
|
|
try:
|
|
return int(value)
|
|
except (TypeError, ValueError):
|
|
try:
|
|
return int(float(value))
|
|
except (TypeError, ValueError):
|
|
return default
|
|
|
|
|
|
def _clean_slot(slot: Any) -> dict[str, str] | None:
|
|
if not isinstance(slot, dict):
|
|
return None
|
|
provider = str(slot.get("provider") or "").strip()
|
|
model = str(slot.get("model") or "").strip()
|
|
if not provider or not model:
|
|
return None
|
|
return {"provider": provider, "model": model}
|
|
|
|
|
|
def _default_preset() -> dict[str, Any]:
|
|
return {
|
|
"reference_models": deepcopy(DEFAULT_MOA_REFERENCE_MODELS),
|
|
"aggregator": deepcopy(DEFAULT_MOA_AGGREGATOR),
|
|
"reference_temperature": 0.6,
|
|
"aggregator_temperature": 0.4,
|
|
"max_tokens": 4096,
|
|
"enabled": True,
|
|
}
|
|
|
|
|
|
def _normalize_preset(raw: Any) -> dict[str, Any]:
|
|
if not isinstance(raw, dict):
|
|
raw = {}
|
|
|
|
refs = [_clean_slot(item) for item in raw.get("reference_models") or []]
|
|
refs = [item for item in refs if item is not None]
|
|
if not refs:
|
|
refs = deepcopy(DEFAULT_MOA_REFERENCE_MODELS)
|
|
|
|
aggregator = _clean_slot(raw.get("aggregator")) or deepcopy(DEFAULT_MOA_AGGREGATOR)
|
|
|
|
return {
|
|
"enabled": bool(raw.get("enabled", True)),
|
|
"reference_models": refs,
|
|
"aggregator": aggregator,
|
|
"reference_temperature": _coerce_float(raw.get("reference_temperature"), 0.6),
|
|
"aggregator_temperature": _coerce_float(raw.get("aggregator_temperature"), 0.4),
|
|
"max_tokens": _coerce_int(raw.get("max_tokens"), 4096),
|
|
}
|
|
|
|
|
|
def normalize_moa_config(raw: Any) -> dict[str, Any]:
|
|
"""Return validated MoA config with named presets.
|
|
|
|
Backward compatible with the first PR shape where ``moa`` itself contained
|
|
``reference_models`` and ``aggregator`` directly.
|
|
"""
|
|
if not isinstance(raw, dict):
|
|
raw = {}
|
|
|
|
presets_raw = raw.get("presets")
|
|
presets: dict[str, dict[str, Any]] = {}
|
|
if isinstance(presets_raw, dict):
|
|
for name, preset in presets_raw.items():
|
|
clean_name = str(name or "").strip()
|
|
if clean_name:
|
|
presets[clean_name] = _normalize_preset(preset)
|
|
|
|
# Legacy flat config becomes the default preset.
|
|
if not presets:
|
|
presets[DEFAULT_MOA_PRESET_NAME] = _normalize_preset(raw)
|
|
|
|
default_name = str(raw.get("default_preset") or "").strip()
|
|
if not default_name or default_name not in presets:
|
|
default_name = next(iter(presets), DEFAULT_MOA_PRESET_NAME)
|
|
if default_name not in presets:
|
|
presets[default_name] = _default_preset()
|
|
|
|
active_name = str(raw.get("active_preset") or "").strip()
|
|
if active_name not in presets:
|
|
active_name = ""
|
|
|
|
active = presets[default_name]
|
|
return {
|
|
"default_preset": default_name,
|
|
"active_preset": active_name,
|
|
"presets": presets,
|
|
# Compatibility/flattened view for existing dashboard/desktop callers.
|
|
"reference_models": deepcopy(active["reference_models"]),
|
|
"aggregator": deepcopy(active["aggregator"]),
|
|
"reference_temperature": active["reference_temperature"],
|
|
"aggregator_temperature": active["aggregator_temperature"],
|
|
"max_tokens": active["max_tokens"],
|
|
"enabled": active["enabled"],
|
|
}
|
|
|
|
|
|
def list_moa_presets(config: Any) -> list[str]:
|
|
cfg = normalize_moa_config(config)
|
|
return list(cfg["presets"].keys())
|
|
|
|
|
|
def resolve_moa_preset(config: Any, name: str | None = None) -> dict[str, Any]:
|
|
cfg = normalize_moa_config(config)
|
|
preset_name = str(name or cfg.get("default_preset") or DEFAULT_MOA_PRESET_NAME).strip()
|
|
preset = cfg["presets"].get(preset_name)
|
|
if preset is None:
|
|
raise KeyError(preset_name)
|
|
return deepcopy(preset)
|
|
|
|
|
|
def exact_moa_preset_name(config: Any, text: str) -> str | None:
|
|
wanted = str(text or "").strip()
|
|
if not wanted:
|
|
return None
|
|
cfg = normalize_moa_config(config)
|
|
return wanted if wanted in cfg["presets"] else None
|
|
|
|
|
|
def set_active_moa_preset(config: Any, name: str | None) -> dict[str, Any]:
|
|
cfg = normalize_moa_config(config)
|
|
clean = str(name or "").strip()
|
|
if clean and clean not in cfg["presets"]:
|
|
raise KeyError(clean)
|
|
cfg["active_preset"] = clean
|
|
return cfg
|
|
|
|
|
|
def encode_moa_turn(prompt: str, config: Any = None, preset: str | None = None) -> str:
|
|
"""Encode a /moa one-shot turn for frontends that can only send text."""
|
|
payload = {
|
|
"prompt": str(prompt or ""),
|
|
"config": resolve_moa_preset(config or {}, preset),
|
|
}
|
|
encoded = base64.urlsafe_b64encode(
|
|
json.dumps(payload, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
|
|
).decode("ascii")
|
|
return f"{MOA_MARKER_PREFIX}{encoded}"
|
|
|
|
|
|
def decode_moa_turn(message: Any) -> tuple[str, dict[str, Any] | None]:
|
|
"""Decode a hidden /moa one-shot marker."""
|
|
if not isinstance(message, str) or not message.startswith(MOA_MARKER_PREFIX):
|
|
return message, None
|
|
encoded = message[len(MOA_MARKER_PREFIX):].strip()
|
|
try:
|
|
payload = json.loads(base64.urlsafe_b64decode(encoded.encode("ascii")).decode("utf-8"))
|
|
except Exception:
|
|
return message, None
|
|
prompt = str(payload.get("prompt") or "")
|
|
return prompt, _normalize_preset(payload.get("config") or {})
|
|
|
|
|
|
def build_moa_turn_prompt(user_prompt: str, config: Any = None, preset: str | None = None) -> str:
|
|
"""Build the hidden one-shot payload used by TUI/gateway routing."""
|
|
return encode_moa_turn(user_prompt, config, preset=preset)
|
|
|
|
|
|
def moa_usage() -> str:
|
|
return "Usage: /moa [preset-name | prompt] (bare /moa toggles the default preset)"
|