Vendors curry_core.py from Athena-Pro/Curry (with the str.format()/format_map() sandbox-escape fix from https://github.com/Athena-Pro/Curry/pull/4 already applied) into synapse/, since Curry itself isn't a pip-installable package - it's meant to be pointed at via a config path, which only works from a source checkout. Vendoring a single self-contained, stdlib-only file ships it inside NexusOS's own wheel with no extra dependency to reconcile. synapse/curry_store.py opens it into a module-level singleton (curry_db) at import time, the same pattern as memory.store.store and playbooks.store.playbook_store, and main.py imports it so it's genuinely initialized at process startup - preloaded, not lazy-on-first-use. Backed by its own CURRY_DB file (nexus_config.py), separate from memory.db. NexusOS builds exactly one wheel (py3-none-any, no compiled extensions) - there is no separate Windows/macOS/Linux artifact; platform differences are handled by requirement overlays at install time, not by building different wheels. Verified the same wheel actually carries this correctly: built it, confirmed twine check passes, confirmed synapse/curry_core.py and curry_store.py are present in the archive (bin/check.sh's packaging gate now asserts this too), then installed that exact wheel into a throwaway venv and round-tripped a declare_constant/get_constant_latest call against it with no source checkout present - proving "preloaded and ready to be called" holds from the shipped artifact, not just editable-install execution. Android/Termux is unaffected by this change in either direction: it already has a separate, documented, pre-existing blocker in docs/TERMUX.md (no published Android pydantic-core wheel) that has nothing to do with Curry, which is pure stdlib and adds no new native/binary dependency. Scope: preload only, nothing wired into a chat-facing tool yet - no model or user-authored content reaches declare_function/call_function today. Verified: 216 backend tests pass (4 new in test_curry_store.py, including a regression test proving the vendored sandbox fix survived the copy); the 12 pre-existing C/C++/Rust toolchain failures are unrelated and unchanged. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
446 lines
18 KiB
Python
446 lines
18 KiB
Python
# config.py
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import shutil
|
|
from importlib import metadata
|
|
from pathlib import Path
|
|
from typing import Dict, Any
|
|
|
|
# --- ENV ---
|
|
try:
|
|
from dotenv import load_dotenv # optional
|
|
load_dotenv()
|
|
except Exception:
|
|
pass
|
|
|
|
# --- INSTALL / RESOURCE LAYOUT ---
|
|
PACKAGE_DIR = Path(__file__).resolve().parent
|
|
_CHECKOUT_ROOT = PACKAGE_DIR.parent
|
|
SOURCE_CHECKOUT = (
|
|
(_CHECKOUT_ROOT / "VERSION").is_file()
|
|
and (_CHECKOUT_ROOT / "interface" / "web" / "package.json").is_file()
|
|
)
|
|
|
|
# PROJECT_ROOT remains the source checkout for developer installs. In a wheel it
|
|
# is the installed package directory; writable state is deliberately elsewhere.
|
|
PROJECT_ROOT = Path(os.getenv("NEXUS_PROJECT_ROOT", "")).expanduser() if os.getenv(
|
|
"NEXUS_PROJECT_ROOT"
|
|
) else (_CHECKOUT_ROOT if SOURCE_CHECKOUT else PACKAGE_DIR)
|
|
PROJECT_ROOT = PROJECT_ROOT.resolve()
|
|
RESOURCE_ROOT = PROJECT_ROOT if SOURCE_CHECKOUT else PACKAGE_DIR / "_resources"
|
|
|
|
|
|
def _user_dir(env_name: str, windows_leaf: str, xdg_name: str, xdg_fallback: str) -> Path:
|
|
# os.getenv's default only applies when a variable is *unset*. An exported
|
|
# but empty XDG_DATA_HOME / LOCALAPPDATA would otherwise give Path("") ==
|
|
# ".", scattering state through whatever the cwd happened to be. The XDG
|
|
# spec says to treat an empty value as unset, so `or` - not a default arg.
|
|
override = os.getenv(env_name, "").strip()
|
|
if override:
|
|
return Path(override).expanduser().resolve()
|
|
if os.name == "nt":
|
|
base = Path(os.getenv("LOCALAPPDATA", "").strip() or Path.home() / "AppData" / "Local")
|
|
return (base / windows_leaf).expanduser().resolve()
|
|
base = Path(os.getenv(xdg_name, "").strip() or Path.home() / xdg_fallback).expanduser()
|
|
return (base / "nexusos").resolve()
|
|
|
|
|
|
CONFIG_DIR = _user_dir("NEXUS_CONFIG_DIR", "NexusOS", "XDG_CONFIG_HOME", ".config")
|
|
CONFIG_FILE = CONFIG_DIR / "config.json"
|
|
|
|
|
|
def read_user_config() -> dict[str, Any]:
|
|
try:
|
|
data = json.loads(CONFIG_FILE.read_text(encoding="utf-8"))
|
|
return data if isinstance(data, dict) else {}
|
|
except (OSError, ValueError, TypeError):
|
|
return {}
|
|
|
|
|
|
def write_user_config(values: dict[str, Any]) -> None:
|
|
"""Atomically persist CLI-managed configuration."""
|
|
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
tmp = CONFIG_FILE.with_suffix(".tmp")
|
|
tmp.write_text(json.dumps(values, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
tmp.replace(CONFIG_FILE)
|
|
|
|
|
|
USER_CONFIG = read_user_config()
|
|
|
|
|
|
def _value(key: str, env_name: str, default: Any) -> Any:
|
|
raw = os.getenv(env_name)
|
|
return raw if raw not in (None, "") else USER_CONFIG.get(key, default)
|
|
|
|
|
|
def _int_value(key: str, env_name: str, default: int) -> int:
|
|
try:
|
|
return int(_value(key, env_name, default))
|
|
except (TypeError, ValueError):
|
|
return default
|
|
|
|
|
|
def _configured_path(key: str, env_name: str, default: Path) -> Path:
|
|
return Path(str(_value(key, env_name, default))).expanduser().resolve()
|
|
|
|
|
|
# --- VERSION (repo file in a checkout; distribution metadata in a wheel) ---
|
|
try:
|
|
if SOURCE_CHECKOUT:
|
|
VERSION = (PROJECT_ROOT / "VERSION").read_text(encoding="utf-8").strip()
|
|
else:
|
|
VERSION = metadata.version("nexusos-ai")
|
|
except Exception:
|
|
try:
|
|
VERSION = (RESOURCE_ROOT / "VERSION").read_text(encoding="utf-8").strip()
|
|
except Exception:
|
|
VERSION = "0.0.0"
|
|
|
|
# --- MODEL DEFAULTS ---
|
|
# Single source of truth for the three models NexusOS ships with. The installers
|
|
# pull and pin these, the runtime falls back to them; keeping them in one place
|
|
# is what stops installer and backend from drifting apart.
|
|
#
|
|
# Chat: llama3.1:8b - a strong non-reasoning instruct model (~4.9 GB; overflows a
|
|
# 4 GB GPU into CPU/RAM). Chosen over Qwen3 because Qwen3 is a reasoning model:
|
|
# smart only with its slow <think> step, weak without it.
|
|
# Memory: the curator that extracts facts and titles conversations. It is the
|
|
# CHAT model on purpose, not a second one: the chat model is already resident in
|
|
# VRAM and warm, so extraction costs no extra load. A distinct curator (mistral)
|
|
# did not fit alongside it and had to be pinned to CPU (num_gpu=0), which made
|
|
# every extraction a slow prompt-eval on a model too small to follow the
|
|
# curator prompt's negative rules reliably.
|
|
# Embed: nomic-embed-text - powers semantic recall of past conversations
|
|
# (OllamaManager.embed / store.semantic_search_conversations). Without it,
|
|
# recall silently degrades to lexical substring matching.
|
|
DEFAULT_CHAT_MODEL = "llama3.1:8b"
|
|
DEFAULT_MEMORY_MODEL = DEFAULT_CHAT_MODEL
|
|
DEFAULT_EMBED_MODEL = "nomic-embed-text"
|
|
|
|
# --- CORE DIRECTORIES ---
|
|
_DEFAULT_STATE = PROJECT_ROOT if SOURCE_CHECKOUT else _user_dir(
|
|
"NEXUS_HOME", "NexusOS", "XDG_DATA_HOME", ".local/share"
|
|
)
|
|
STATE_DIR = _configured_path("home", "NEXUS_HOME", _DEFAULT_STATE)
|
|
_USE_CHECKOUT_STATE = SOURCE_CHECKOUT and not os.getenv("NEXUS_HOME", "").strip()
|
|
DATA_DIR = _configured_path("data_dir", "NEXUS_DATA_DIR", (
|
|
PROJECT_ROOT / "data" if _USE_CHECKOUT_STATE else STATE_DIR / "data"
|
|
))
|
|
MODELS_DIR = _configured_path("models_dir", "NEXUS_MODELS_DIR", (
|
|
PROJECT_ROOT / "models" if _USE_CHECKOUT_STATE else STATE_DIR / "models"
|
|
))
|
|
RUNTIME_DIR = _configured_path("runtime_dir", "NEXUS_RUNTIME_DIR", (
|
|
PROJECT_ROOT / "runtime" if _USE_CHECKOUT_STATE else STATE_DIR / "runtime"
|
|
))
|
|
|
|
MEMORY_DIR = _configured_path("memory_dir", "NEXUS_MEMORY_DIR", (
|
|
PROJECT_ROOT / "synapse" / "memory" if _USE_CHECKOUT_STATE else DATA_DIR
|
|
))
|
|
|
|
LOGS_DIR = RUNTIME_DIR / "logs"
|
|
CACHE_DIR = RUNTIME_DIR / "cache"
|
|
TEMP_DIR = RUNTIME_DIR / "tmp"
|
|
|
|
# --- APPLICATION SUBSYSTEM DIRECTORIES ---
|
|
PLAYBOOK_DIR = DATA_DIR / "playbooks" # YAML playbook files (PlaybookFileStore)
|
|
UPLOADS_DIR = DATA_DIR / "uploads"
|
|
EXPORTS_DIR = DATA_DIR / "exports"
|
|
WEB_DIST_DIR = (
|
|
PROJECT_ROOT / "interface" / "web" / "dist"
|
|
if SOURCE_CHECKOUT else RESOURCE_ROOT / "web"
|
|
)
|
|
FRONTEND_SOURCE_DIR = PROJECT_ROOT / "interface" / "web"
|
|
ASSETS_DIR = PROJECT_ROOT / "assets" if SOURCE_CHECKOUT else RESOURCE_ROOT / "assets"
|
|
SEED_PLAYBOOK_DIR = (
|
|
PROJECT_ROOT / "data" / "playbooks"
|
|
if SOURCE_CHECKOUT else RESOURCE_ROOT / "playbooks"
|
|
)
|
|
|
|
# --- DATABASE / STORAGE FILES (match your repo) ---
|
|
MEMORY_DB = _configured_path("memory_db", "NEXUS_MEMORY_DB", MEMORY_DIR / "memory.db")
|
|
# Vendored Curry (synapse/curry_core.py) database: immutable versioned
|
|
# constants/functions/models + inference provenance. Separate file from
|
|
# MEMORY_DB on purpose - Curry's schema and lifecycle are independent of the
|
|
# memory/conversation store.
|
|
CURRY_DB = _configured_path("curry_db", "NEXUS_CURRY_DB", DATA_DIR / "curry.db")
|
|
|
|
# --- LOG FILES ---
|
|
BACKEND_LOG = RUNTIME_DIR / "backend.log"
|
|
OLLAMA_LOG = LOGS_DIR / "ollama.log"
|
|
CHAT_LOG = LOGS_DIR / "chat.log"
|
|
|
|
# --- ENSURE REQUIRED DIRECTORIES EXIST ---
|
|
_REQUIRED_DIRS = (
|
|
STATE_DIR,
|
|
DATA_DIR,
|
|
MODELS_DIR,
|
|
RUNTIME_DIR,
|
|
LOGS_DIR,
|
|
MEMORY_DIR,
|
|
CACHE_DIR,
|
|
TEMP_DIR,
|
|
PLAYBOOK_DIR,
|
|
UPLOADS_DIR,
|
|
EXPORTS_DIR,
|
|
MEMORY_DB.parent,
|
|
CURRY_DB.parent,
|
|
)
|
|
|
|
|
|
def init_state() -> list[Path]:
|
|
"""Create writable state and seed playbooks on a first wheel install."""
|
|
for directory in _REQUIRED_DIRS:
|
|
directory.mkdir(parents=True, exist_ok=True)
|
|
copied: list[Path] = []
|
|
if SEED_PLAYBOOK_DIR.resolve() != PLAYBOOK_DIR.resolve() and SEED_PLAYBOOK_DIR.is_dir():
|
|
for source in SEED_PLAYBOOK_DIR.glob("*.yaml"):
|
|
target = PLAYBOOK_DIR / source.name
|
|
if not target.exists():
|
|
shutil.copy2(source, target)
|
|
copied.append(target)
|
|
return copied
|
|
|
|
|
|
INITIALIZED_FILES = init_state()
|
|
|
|
# --- PATH ACCESSOR (fail-fast) ---
|
|
def path(name: str) -> Path:
|
|
"""
|
|
Return a Path for a known name. Raises KeyError if name is unknown.
|
|
"""
|
|
mapping = {
|
|
"root": PROJECT_ROOT,
|
|
"resources": RESOURCE_ROOT,
|
|
"state": STATE_DIR,
|
|
"config": CONFIG_FILE,
|
|
"data": DATA_DIR,
|
|
"models": MODELS_DIR,
|
|
"runtime": RUNTIME_DIR,
|
|
"logs": LOGS_DIR,
|
|
"memory": MEMORY_DIR,
|
|
"cache": CACHE_DIR,
|
|
"tmp": TEMP_DIR,
|
|
"playbooks": PLAYBOOK_DIR,
|
|
"uploads": UPLOADS_DIR,
|
|
"exports": EXPORTS_DIR,
|
|
"web": WEB_DIST_DIR,
|
|
"assets": ASSETS_DIR,
|
|
"memory_db": MEMORY_DB,
|
|
"backend_log": BACKEND_LOG,
|
|
"ollama_log": OLLAMA_LOG,
|
|
"chat_log": CHAT_LOG,
|
|
}
|
|
try:
|
|
return mapping[name]
|
|
except KeyError:
|
|
raise KeyError(f"Unknown config path name: {name}")
|
|
|
|
# --- Settings class and exported instance ---
|
|
def _normalize_ollama_host(raw: str) -> str:
|
|
"""Turn an OLLAMA_HOST value into a URL a client can actually connect to.
|
|
|
|
OLLAMA_HOST is Ollama's *server bind* variable, and the common way to expose
|
|
Ollama on a LAN is `OLLAMA_HOST=0.0.0.0:11434`. Taken literally as a client
|
|
base URL that is unusable twice over: 0.0.0.0 means "every local interface"
|
|
to a listener but is not a destination, and there is no scheme for httpx to
|
|
parse. The result was a silent empty model list, because list_models()
|
|
catches everything and returns [].
|
|
|
|
So: supply the scheme when it's missing, and rewrite wildcard binds to
|
|
loopback. An explicit host is left alone — someone pointing at a real remote
|
|
Ollama means it.
|
|
"""
|
|
host = (raw or "").strip().rstrip("/")
|
|
if not host:
|
|
return "http://127.0.0.1:11434"
|
|
if "://" not in host:
|
|
host = f"http://{host}"
|
|
scheme, _, rest = host.partition("://")
|
|
hostport = rest.split("/", 1)[0]
|
|
name, sep, port = hostport.rpartition(":")
|
|
if not sep: # no port given
|
|
name, port = hostport, ""
|
|
# 0.0.0.0 and :: are bind-any; from a client they mean "this machine".
|
|
if name.strip("[]") in ("0.0.0.0", "::", ""):
|
|
name = "127.0.0.1"
|
|
return f"{scheme}://{name}:{port}" if port else f"{scheme}://{name}"
|
|
|
|
|
|
class Settings:
|
|
"""
|
|
Lightweight settings container. Use `settings` instance for runtime access,
|
|
or `Settings` class for typing/tests.
|
|
"""
|
|
def __init__(self) -> None:
|
|
self.version: str = VERSION
|
|
self.source_checkout: bool = SOURCE_CHECKOUT
|
|
self.project_root: Path = PROJECT_ROOT
|
|
self.resource_root: Path = RESOURCE_ROOT
|
|
self.state_dir: Path = STATE_DIR
|
|
self.config_file: Path = CONFIG_FILE
|
|
self.data_dir: Path = DATA_DIR
|
|
self.models_dir: Path = MODELS_DIR
|
|
self.runtime_dir: Path = RUNTIME_DIR
|
|
self.memory_dir: Path = MEMORY_DIR
|
|
self.logs_dir: Path = LOGS_DIR
|
|
self.web_dist_dir: Path = WEB_DIST_DIR
|
|
self.frontend_source_dir: Path = FRONTEND_SOURCE_DIR
|
|
self.assets_dir: Path = ASSETS_DIR
|
|
|
|
# DB files
|
|
self.memory_db: Path = MEMORY_DB
|
|
|
|
# Logs
|
|
self.backend_log: Path = BACKEND_LOG
|
|
self.ollama_log: Path = OLLAMA_LOG
|
|
self.chat_log: Path = CHAT_LOG
|
|
|
|
# Env overrides. Two values from one variable, because OLLAMA_HOST means
|
|
# two different things: where a server should LISTEN, and where a client
|
|
# should CONNECT. `ollama_bind` keeps the user's literal intent for a
|
|
# serve we spawn (0.0.0.0 to expose it on the LAN); `ollama_host` is the
|
|
# connectable form for our own requests.
|
|
self.provider: str = str(_value("provider", "NEXUS_PROVIDER", "ollama"))
|
|
# A Nexus provider setting is more specific than the legacy Ollama bind
|
|
# variable. This matters on a desktop that has OLLAMA_HOST globally set
|
|
# but configures NexusOS to use a different remote inference machine.
|
|
provider_url = str(_value("provider_url", "NEXUS_PROVIDER_URL", "")).strip()
|
|
configured_host = (
|
|
provider_url
|
|
or os.getenv("OLLAMA_HOST", "").strip()
|
|
or "http://127.0.0.1:11434"
|
|
)
|
|
self.ollama_bind: str = configured_host or "127.0.0.1:11434"
|
|
self.ollama_host: str = _normalize_ollama_host(
|
|
configured_host
|
|
)
|
|
self.provider_url: str = self.ollama_host
|
|
self.manage_ollama: bool = self.provider == "ollama"
|
|
self.ollama_timeout: int = _int_value("provider_timeout", "OLLAMA_TIMEOUT", 120)
|
|
self.bind_host: str = str(_value(
|
|
"bind_host", "NEXUS_BIND_HOST", "127.0.0.1"
|
|
))
|
|
self.backend_port: int = _int_value("backend_port", "NEXUS_BACKEND_PORT", 8000)
|
|
self.memory_port: int = _int_value("memory_port", "NEXUS_MEMORY_PORT", 8001)
|
|
self.api_url: str = str(_value(
|
|
"api_url", "NEXUS_API", f"http://127.0.0.1:{self.backend_port}"
|
|
)).rstrip("/")
|
|
self.memory_url: str = str(_value(
|
|
"memory_url", "NEXUS_MEMORY_URL", f"http://127.0.0.1:{self.memory_port}"
|
|
)).rstrip("/")
|
|
|
|
def as_dict(self) -> Dict[str, Any]:
|
|
return {
|
|
"version": self.version,
|
|
"source_checkout": self.source_checkout,
|
|
"project_root": str(self.project_root),
|
|
"resource_root": str(self.resource_root),
|
|
"state_dir": str(self.state_dir),
|
|
"config_file": str(self.config_file),
|
|
"data_dir": str(self.data_dir),
|
|
"models_dir": str(self.models_dir),
|
|
"runtime_dir": str(self.runtime_dir),
|
|
"memory_dir": str(self.memory_dir),
|
|
"memory_db": str(self.memory_db),
|
|
"web_dist_dir": str(self.web_dist_dir),
|
|
"provider": self.provider,
|
|
"ollama_host": self.ollama_host,
|
|
"ollama_timeout": self.ollama_timeout,
|
|
"api_url": self.api_url,
|
|
"bind_host": self.bind_host,
|
|
"backend_port": self.backend_port,
|
|
"memory_port": self.memory_port,
|
|
"memory_url": self.memory_url,
|
|
}
|
|
|
|
# --- local-access allowlists (shared by the backend + memory FastAPI apps) ---
|
|
# The REST APIs are unauthenticated, so they are meant to be reached only from
|
|
# this machine. Two independent browser-side defenses depend on these lists:
|
|
# * ALLOWED_ORIGINS drives CORS — blocks a malicious page from *reading*
|
|
# responses cross-origin (was previously "*", which let any site read them).
|
|
# * ALLOWED_HOSTS drives TrustedHostMiddleware — rejects a foreign Host header,
|
|
# which is what stops DNS-rebinding (same-origin from the browser's view, so
|
|
# CORS can't help there).
|
|
# Both accept a comma-separated env override for the intentional-LAN case, to be
|
|
# paired with real auth. NEXUS_ALLOWED_HOSTS=* disables the Host check.
|
|
def _csv_env(name: str, default: list) -> list:
|
|
raw = os.getenv(name, "").strip()
|
|
return [p.strip() for p in raw.split(",") if p.strip()] if raw else list(default)
|
|
|
|
_LOCAL_HOSTS = ["localhost", "127.0.0.1", "[::1]", "::1", "testserver"]
|
|
_LOCAL_ORIGINS = [
|
|
f"http://{h}:{p}"
|
|
for h in ("localhost", "127.0.0.1")
|
|
for p in (
|
|
_int_value("backend_port", "NEXUS_BACKEND_PORT", 8000),
|
|
_int_value("memory_port", "NEXUS_MEMORY_PORT", 8001),
|
|
5173,
|
|
)
|
|
]
|
|
_LOCAL_ORIGINS.extend(["capacitor://localhost", "https://localhost"])
|
|
ALLOWED_HOSTS = _csv_env("NEXUS_ALLOWED_HOSTS", _LOCAL_HOSTS)
|
|
ALLOWED_ORIGINS = _csv_env("NEXUS_ALLOWED_ORIGINS", _LOCAL_ORIGINS)
|
|
|
|
|
|
# --- resource limits (DoS guardrails for the unauthenticated local APIs) ---
|
|
# Even local-only, an unbounded base64 upload or a flood of concurrent inference
|
|
# requests can exhaust RAM/CPU. These are generous defaults for single-user use,
|
|
# all env-overridable.
|
|
def _int_env(name: str, default: int) -> int:
|
|
try:
|
|
return int((os.getenv(name) or "").strip() or default)
|
|
except ValueError:
|
|
return default
|
|
|
|
MAX_REQUEST_BYTES = _int_env("NEXUS_MAX_REQUEST_MB", 32) * 1024 * 1024
|
|
MAX_UPLOAD_BYTES = _int_env("NEXUS_MAX_UPLOAD_MB", 20) * 1024 * 1024
|
|
MAX_PDF_PAGES = _int_env("NEXUS_MAX_PDF_PAGES", 500)
|
|
MAX_CONCURRENT_CHATS = _int_env("NEXUS_MAX_CONCURRENT_CHATS", 4)
|
|
MAX_CONCURRENT_UPLOADS = _int_env("NEXUS_MAX_CONCURRENT_UPLOADS", 2)
|
|
|
|
# Opt-in allowlist for /models/pull. Empty (default) = unrestricted, preserving
|
|
# current behaviour. Set NEXUS_MODEL_ALLOWLIST=mistral,llama3 to bound which
|
|
# models can be downloaded; a bare repo name (before the ':tag') matches all of
|
|
# its tags, so "mistral" permits "mistral:latest", "mistral:7b", etc.
|
|
MODEL_ALLOWLIST = _csv_env("NEXUS_MODEL_ALLOWLIST", [])
|
|
|
|
|
|
def model_pull_allowed(name: str) -> bool:
|
|
"""True if `name` may be pulled: always when no allowlist is configured,
|
|
otherwise when the full name or its repo part (before the first ':') is
|
|
listed. Case-insensitive."""
|
|
if not MODEL_ALLOWLIST:
|
|
return True
|
|
n = (name or "").strip().lower()
|
|
if not n:
|
|
return False
|
|
allow = {a.lower() for a in MODEL_ALLOWLIST}
|
|
return n in allow or n.split(":", 1)[0] in allow
|
|
|
|
# exported instance
|
|
settings = Settings()
|
|
|
|
# explicit exports for static checkers and IDEs
|
|
__all__ = ["Settings", "settings", "path", "VERSION",
|
|
"DEFAULT_CHAT_MODEL", "DEFAULT_MEMORY_MODEL", "DEFAULT_EMBED_MODEL",
|
|
"PACKAGE_DIR", "PROJECT_ROOT", "RESOURCE_ROOT", "SOURCE_CHECKOUT",
|
|
"STATE_DIR", "CONFIG_DIR", "CONFIG_FILE", "USER_CONFIG",
|
|
"read_user_config", "write_user_config", "init_state", "INITIALIZED_FILES",
|
|
"DATA_DIR", "MODELS_DIR", "RUNTIME_DIR",
|
|
"MEMORY_DIR", "LOGS_DIR", "PLAYBOOK_DIR", "UPLOADS_DIR",
|
|
"EXPORTS_DIR", "MEMORY_DB", "CURRY_DB", "WEB_DIST_DIR", "FRONTEND_SOURCE_DIR",
|
|
"ASSETS_DIR", "SEED_PLAYBOOK_DIR",
|
|
"BACKEND_LOG", "OLLAMA_LOG", "CHAT_LOG",
|
|
"ALLOWED_HOSTS", "ALLOWED_ORIGINS",
|
|
"MAX_REQUEST_BYTES", "MAX_UPLOAD_BYTES", "MAX_PDF_PAGES",
|
|
"MAX_CONCURRENT_CHATS", "MAX_CONCURRENT_UPLOADS",
|
|
"MODEL_ALLOWLIST", "model_pull_allowed"]
|
|
|
|
# --- quick runtime sanity check when run directly (no side effects on import) ---
|
|
if __name__ == "__main__":
|
|
print("Config paths:")
|
|
for key in ("root", "data", "models", "runtime", "memory", "memory_db"):
|
|
print(f" {key}: {path(key)}")
|