diff --git a/.env.example b/.env.example index b659595..654fe1a 100644 --- a/.env.example +++ b/.env.example @@ -15,6 +15,16 @@ ENGINE_PORT=8088 ORACLE_TEMPERATURE=0.5 # 0=cautious, 1=imaginative ORACLE_TIMEOUT_SEC=180 +# ── Swarm persona models (optional) ── +# Override the LLM model used for individual swarm personas during deliberation. +# Each persona can use a different model suited to its lens. When unset, the +# persona falls back to the main LLM_MODEL. Useful for running a reasoning +# model on the Skeptic while keeping faster models on other personas. +# SWARM_MODEL_STRATEGIST=qwen2.5:32b +# SWARM_MODEL_ECONOMIST=qwen2.5:32b +# SWARM_MODEL_NATURALIST=gemma3:12b +# SWARM_MODEL_SKEPTIC=deepseek-r1:14b + # ── Predictions ── HORIZONS=24h,week,month,year # time horizons to forecast PREDICTIONS_PER_HORIZON=3 diff --git a/engine/config.py b/engine/config.py index 6ab3e3c..20adf77 100644 --- a/engine/config.py +++ b/engine/config.py @@ -80,6 +80,16 @@ class Config: # ── Swarm (a council of LLM personas deliberates each forecast) ── swarm_enabled: bool = field(default_factory=lambda: _b("SWARM_ENABLED", True)) + swarm_persona_models: dict[str, str] = field(default_factory=lambda: { + name: model + for name, model in [ + ("Strategist", os.environ.get("SWARM_MODEL_STRATEGIST", "")), + ("Economist", os.environ.get("SWARM_MODEL_ECONOMIST", "")), + ("Naturalist", os.environ.get("SWARM_MODEL_NATURALIST", "")), + ("Skeptic", os.environ.get("SWARM_MODEL_SKEPTIC", "")), + ] + if model + }) def summary(self) -> dict: return { diff --git a/engine/oracle.py b/engine/oracle.py index 597d7e1..362ac05 100644 --- a/engine/oracle.py +++ b/engine/oracle.py @@ -94,8 +94,8 @@ async def predict(self, brief: WorldBrief, on_stage: StageCB = None) -> list[Pre async def _chat(self, user: str) -> str: return await self._complete([{"role": "system", "content": SYSTEM}, {"role": "user", "content": user}], 1400) - async def _complete(self, messages: list[dict], max_tokens: int = 900) -> str: - body = {"model": self.model, "messages": messages, "temperature": CONFIG.temperature, "max_tokens": max_tokens} + async def _complete(self, messages: list[dict], max_tokens: int = 900, model: str | None = None) -> str: + body = {"model": model or self.model, "messages": messages, "temperature": CONFIG.temperature, "max_tokens": max_tokens} async with httpx.AsyncClient(verify=HTTPX_VERIFY, timeout=CONFIG.request_timeout) as c: r = await c.post(f"{self.base}/chat/completions", json=body, headers={"Authorization": f"Bearer {self.key}"}) diff --git a/engine/swarm.py b/engine/swarm.py index 7f30651..cdbc145 100644 --- a/engine/swarm.py +++ b/engine/swarm.py @@ -11,6 +11,7 @@ import json import logging +from .config import CONFIG from .models import AgentView, Prediction, WorldBrief log = logging.getLogger("pythia.swarm") @@ -50,8 +51,9 @@ def _persona_messages(name: str, lens: str, brief_text: str, preds: list[Predict async def _ask(oracle, name: str, lens: str, brief_text: str, preds: list[Prediction]) -> tuple[str, dict[int, tuple[float, str]]]: """Run one persona; return its {prediction_index: (probability, note)} map.""" + persona_model = CONFIG.swarm_persona_models.get(name) try: - text = await oracle._complete(_persona_messages(name, lens, brief_text, preds), max_tokens=1300) + text = await oracle._complete(_persona_messages(name, lens, brief_text, preds), max_tokens=1300, model=persona_model) except Exception as e: # noqa: BLE001 log.warning("swarm persona %s failed: %s", name, e) return name, {}