97afa82594
The chat had no thinking in it: respond() was a single gpt-4o call in default- assistant voice (numbered lists, 'would you like to...', vague). All the cognition work was background-only. This brings a thought step into the conversation. - chat: before answering a substantive turn (trivial 'ok/lol' skipped), a private _deliberate() pass — "what do you ACTUALLY think, your real take, the substance, no pleasantries" — drawing on her in-context threads/journal. The thinking is then injected as the LAST system note with voice enforcement (answer from this; no numbered list / how-to outline unless asked; no 'would you like to' closer), so it beats gpt-4o's boilerplate at the most influential position. Logged to /logs. - Wired into respond() + respond_stream(). Config CHAT_DELIBERATE (default on) to disable if the extra call's latency annoys. - persona: "talk, don't outline" — prose over listicles, the first concrete move over a survey of options. - test_chat.py (gating + note composition + disabled). Suite 84, ruff clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
87 lines
4.7 KiB
Python
87 lines
4.7 KiB
Python
"""Environment-driven configuration."""
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
|
|
from dotenv import load_dotenv
|
|
|
|
load_dotenv()
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Config:
|
|
local_base_url: str
|
|
local_model: str
|
|
mi50_base_url: str # OpenAI-compatible llama.cpp server on the MI50 box
|
|
mi50_model: str
|
|
openai_api_key: str
|
|
cloud_model: str # cloud model for bulk/consolidation work (cheap)
|
|
chat_model: str # cloud model for live chat (stronger; persona fidelity)
|
|
embed_backend: str # "cloud" (OpenAI) or "local" (Ollama)
|
|
embed_model: str # OpenAI embedding model
|
|
local_embed_model: str # Ollama embedding model
|
|
embed_base_url: str # Ollama endpoint for embeddings (own box, decoupled from local chat)
|
|
summary_backend: str # backend for memory consolidation (summaries/profile/narrative)
|
|
introspection_backend: str # backend for reflect()/think() — her *voice* (may differ)
|
|
introspection_model: str | None # model override for introspection (e.g. a steerable tune)
|
|
db_path: Path
|
|
# Proactive reach-out (ntfy push). Empty ntfy_url disables pinging.
|
|
ntfy_url: str # base url, e.g. "http://10.0.0.41:8090"
|
|
ntfy_topic: str # topic to publish to, e.g. "lyra"
|
|
web_url: str # base url of the Lyra web app, for push tap-through links
|
|
timezone: str # IANA tz for quiet hours / local time
|
|
ping_salience: float # hard floor for any push (0 = her decision drives it)
|
|
ping_auto_salience: float # a thought this salient auto-pings even without an explicit reach-out
|
|
ping_cooldown_min: int # min minutes between AUTO pushes (explicit reach-outs bypass it)
|
|
ping_quiet_hours: str # local "start-end" 24h window to stay silent, e.g. "1-9"
|
|
digest_hour: int # local hour (0-23) to send her daily "what I've been thinking" digest
|
|
chat_deliberate: bool # think privately before answering substantive chat turns
|
|
# External input feed (her #1: react to the world). Comma-separated RSS/Atom URLs.
|
|
feeds: tuple[str, ...]
|
|
feed_react_prob: float # chance a would-be new thread reacts to a feed item instead
|
|
|
|
|
|
def _csv(name: str, default: str) -> tuple[str, ...]:
|
|
raw = os.getenv(name, default)
|
|
return tuple(u.strip() for u in raw.split(",") if u.strip())
|
|
|
|
|
|
def load() -> Config:
|
|
_summary = os.getenv("SUMMARY_BACKEND", "local").lower()
|
|
return Config(
|
|
local_base_url=os.getenv("LOCAL_BASE_URL", "http://localhost:11434"),
|
|
local_model=os.getenv("LOCAL_MODEL", "qwen2.5:7b-instruct"),
|
|
mi50_base_url=os.getenv("MI50_BASE_URL", "http://10.0.0.42:8080/v1"),
|
|
mi50_model=os.getenv("MI50_MODEL", "local-gpu"),
|
|
openai_api_key=os.getenv("OPENAI_API_KEY", ""),
|
|
cloud_model=os.getenv("CLOUD_MODEL", "gpt-4o-mini"),
|
|
chat_model=os.getenv("CHAT_MODEL", "gpt-4o"),
|
|
embed_backend=os.getenv("EMBED_BACKEND", "cloud").lower(),
|
|
embed_model=os.getenv("EMBED_MODEL", "text-embedding-3-small"),
|
|
local_embed_model=os.getenv("LOCAL_EMBED_MODEL", "nomic-embed-text"),
|
|
# Embeddings can live on their own always-on box, separate from the local
|
|
# chat backend. Defaults to LOCAL_BASE_URL so existing setups are unchanged.
|
|
embed_base_url=os.getenv("EMBED_BASE_URL", os.getenv("LOCAL_BASE_URL", "http://localhost:11434")),
|
|
summary_backend=_summary,
|
|
# Introspection (reflect/think) can run on a different model than consolidation —
|
|
# e.g. a steerable tune for her voice, while the capable model keeps her memory
|
|
# accurate. Defaults to the summary backend so unset = unchanged behavior.
|
|
introspection_backend=os.getenv("INTROSPECTION_BACKEND", _summary).lower(),
|
|
introspection_model=os.getenv("INTROSPECTION_MODEL") or None,
|
|
db_path=Path(os.getenv("LYRA_DB_PATH", "data/lyra.db")),
|
|
ntfy_url=os.getenv("NTFY_URL", "").rstrip("/"),
|
|
ntfy_topic=os.getenv("NTFY_TOPIC", "lyra"),
|
|
web_url=os.getenv("LYRA_WEB_URL", "").rstrip("/"),
|
|
timezone=os.getenv("LYRA_TIMEZONE", "America/New_York"),
|
|
ping_salience=float(os.getenv("PING_SALIENCE", "0.0")), # her decision drives pinging; optional floor
|
|
ping_auto_salience=float(os.getenv("PING_AUTO_SALIENCE", "0.8")),
|
|
ping_cooldown_min=int(os.getenv("PING_COOLDOWN_MIN", "60")),
|
|
ping_quiet_hours=os.getenv("PING_QUIET_HOURS", "1-9"),
|
|
digest_hour=int(os.getenv("DIGEST_HOUR", "18")),
|
|
chat_deliberate=os.getenv("CHAT_DELIBERATE", "true").lower() not in ("0", "false", "no"),
|
|
feeds=_csv("LYRA_FEEDS", "https://hnrss.org/frontpage,https://www.pokernews.com/rss.php"),
|
|
feed_react_prob=float(os.getenv("FEED_REACT_PROB", "0.5")),
|
|
)
|