Janus-backend / backend /app /config.py
DevodG's picture
deploy: Janus full system stabilization
24f95f0
import os
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent
try:
from dotenv import load_dotenv
# Load .env from the backend directory
env_path = BASE_DIR.parent / ".env"
load_dotenv(env_path)
except ImportError:
pass
PROMPTS_DIR = BASE_DIR / "prompts"
# Runtime state directory.
# Override with JANUS_DATA_DIR when you want state on a persistent volume.
DATA_DIR = Path(os.getenv("JANUS_DATA_DIR", str(BASE_DIR / "data"))).expanduser()
MEMORY_DIR = DATA_DIR / "memory"
SIMULATION_DIR = DATA_DIR / "simulations"
SENTINEL_DIR = DATA_DIR / "sentinel"
# Prompt loader
def load_prompt(name: str) -> str:
"""Load a prompt file by name (without .txt extension)."""
path = PROMPTS_DIR / f"{name}.txt"
if not path.exists():
return f"You are the {name} agent in MiroOrg v2. Be helpful and precise."
return path.read_text(encoding="utf-8").strip()
APP_VERSION = os.getenv("APP_VERSION", "1.0.1")
PRIMARY_PROVIDER = os.getenv("PRIMARY_PROVIDER", "huggingface").lower()
FALLBACK_PROVIDER = os.getenv("FALLBACK_PROVIDER", "openrouter").lower()
# Hugging Face token support - prioritizes HUGGINGFACE_API_KEY then HF_TOKEN (Spaces default)
HUGGINGFACE_API_KEY = os.getenv("HUGGINGFACE_API_KEY", os.getenv("HF_TOKEN", ""))
HUGGINGFACE_MODEL = os.getenv("HUGGINGFACE_MODEL", "openai/gpt-oss-120b")
GEMINI_API_KEY = os.getenv("GEMINI_API_KEY", "")
GROQ_API_KEY = os.getenv("GROQ_API_KEY", "")
CLOUDFLARE_ACCOUNT_ID = os.getenv("CLOUDFLARE_ACCOUNT_ID", "")
CLOUDFLARE_API_TOKEN = os.getenv("CLOUDFLARE_API_TOKEN", "")
OPENROUTER_API_KEY = os.getenv("OPENROUTER_API_KEY", "")
OPENROUTER_BASE_URL = os.getenv("OPENROUTER_BASE_URL", "https://openrouter.ai/api/v1")
OPENROUTER_CHAT_MODEL = os.getenv("OPENROUTER_CHAT_MODEL", "openrouter/free")
OPENROUTER_REASONER_MODEL = os.getenv("OPENROUTER_REASONER_MODEL", "openrouter/free")
OPENROUTER_SITE_URL = os.getenv("OPENROUTER_SITE_URL", "")
OPENROUTER_APP_NAME = os.getenv("OPENROUTER_APP_NAME", "MiroOrg Basic")
OLLAMA_ENABLED = os.getenv("OLLAMA_ENABLED", "true").lower() == "true"
OLLAMA_BASE_URL = os.getenv("OLLAMA_BASE_URL", "http://127.0.0.1:11434/api")
OLLAMA_CHAT_MODEL = os.getenv("OLLAMA_CHAT_MODEL", "qwen2.5:3b-instruct")
OLLAMA_REASONER_MODEL = os.getenv("OLLAMA_REASONER_MODEL", "qwen2.5:3b-instruct")
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "")
OPENAI_BASE_URL = os.getenv("OPENAI_BASE_URL", "https://api.openai.com/v1")
OPENAI_CHAT_MODEL = os.getenv("OPENAI_CHAT_MODEL", "gpt-4o-mini")
OPENAI_REASONER_MODEL = os.getenv("OPENAI_REASONER_MODEL", "gpt-4o")
TAVILY_API_KEY = os.getenv("TAVILY_API_KEY", "")
# News APIs - multiple providers for redundancy
NEWS_API_KEY = os.getenv("NEWS_API_KEY", os.getenv("NEWSAPI_KEY", ""))
NEWSAPI_KEY = NEWS_API_KEY
NEWDATA_API_KEY = os.getenv("NEWDATA_API_KEY", "")
GNEWS_API_KEY = os.getenv("GNEWS_API_KEY", "")
NEWSAPI_ORG_KEY = os.getenv("NEWSAPI_ORG_KEY", "")
ALPHAVANTAGE_API_KEY = os.getenv("ALPHAVANTAGE_API_KEY", "")
JINA_READER_BASE = os.getenv("JINA_READER_BASE", "https://r.jina.ai/http://")
# Financial data APIs
FINNHUB_API_KEY = os.getenv("FINNHUB_API_KEY", "")
FMP_API_KEY = os.getenv("FMP_API_KEY", "")
EODHD_API_KEY = os.getenv("EODHD_API_KEY", "")
MIROFISH_ENABLED = False # Deprecated β€” using native simulation engine
MIROFISH_API_BASE = ""
MIROFISH_TIMEOUT_SECONDS = 0
CRAWLER_ENABLED = os.getenv("CRAWLER_ENABLED", "true").lower() == "true"
CRAWLER_TIMEOUT = int(os.getenv("CRAWLER_TIMEOUT", "30"))
SIMULATION_TRIGGER_KEYWORDS = [
item.strip().lower()
for item in os.getenv(
"SIMULATION_TRIGGER_KEYWORDS",
"simulate,predict,what if,reaction,scenario,public opinion,policy impact,market impact,digital twin",
).split(",")
if item.strip()
]
# Domain pack configuration
FINANCE_DOMAIN_PACK_ENABLED = (
os.getenv("FINANCE_DOMAIN_PACK_ENABLED", "true").lower() == "true"
)
# Configuration validation
import logging
logger = logging.getLogger(__name__)
def validate_config():
"""Validate configuration on startup and log warnings/errors."""
errors = []
warnings = []
# Validate primary provider configuration
primary = PRIMARY_PROVIDER.lower()
if primary not in ["huggingface", "openrouter", "ollama", "openai"]:
errors.append(
f"PRIMARY_PROVIDER '{PRIMARY_PROVIDER}' is not supported. Must be one of: huggingface, openrouter, ollama, openai"
)
if primary == "huggingface" and not HUGGINGFACE_API_KEY:
warnings.append(
"PRIMARY_PROVIDER is 'huggingface' but HUGGINGFACE_API_KEY is missing - relying on fallback"
)
if primary == "openrouter" and not OPENROUTER_API_KEY:
warnings.append(
"PRIMARY_PROVIDER is 'openrouter' but OPENROUTER_API_KEY is missing - relying on fallback"
)
if primary == "openai" and not OPENAI_API_KEY:
warnings.append(
"PRIMARY_PROVIDER is 'openai' but OPENAI_API_KEY is missing - relying on fallback"
)
if primary == "ollama" and not OLLAMA_ENABLED:
warnings.append(
"PRIMARY_PROVIDER is 'ollama' but OLLAMA_ENABLED is false - relying on fallback"
)
# Validate fallback provider configuration
fallback = FALLBACK_PROVIDER.lower()
if fallback not in ["huggingface", "openrouter", "ollama", "openai"]:
errors.append(
f"FALLBACK_PROVIDER '{FALLBACK_PROVIDER}' is not supported. Must be one of: huggingface, openrouter, ollama, openai"
)
if fallback == "huggingface" and not HUGGINGFACE_API_KEY:
warnings.append(
"FALLBACK_PROVIDER is 'huggingface' but HUGGINGFACE_API_KEY is missing - fallback will fail"
)
if fallback == "openrouter" and not OPENROUTER_API_KEY:
warnings.append(
"FALLBACK_PROVIDER is 'openrouter' but OPENROUTER_API_KEY is missing - fallback will fail"
)
if fallback == "openai" and not OPENAI_API_KEY:
warnings.append(
"FALLBACK_PROVIDER is 'openai' but OPENAI_API_KEY is missing - fallback will fail"
)
if fallback == "ollama" and not OLLAMA_ENABLED:
warnings.append(
"FALLBACK_PROVIDER is 'ollama' but OLLAMA_ENABLED is false - fallback will fail"
)
# Validate optional API keys
if not TAVILY_API_KEY:
warnings.append(
"TAVILY_API_KEY is missing - web search functionality will be limited"
)
if not NEWSAPI_KEY:
warnings.append(
"NEWS_API_KEY is missing - news research functionality will be limited"
)
if not ALPHAVANTAGE_API_KEY:
warnings.append(
"ALPHAVANTAGE_API_KEY is missing - financial data functionality will be limited"
)
if not FINNHUB_API_KEY:
warnings.append(
"FINNHUB_API_KEY is missing - historical data fallback will be limited"
)
if not FMP_API_KEY:
warnings.append(
"FMP_API_KEY is missing - historical data fallback will be limited"
)
if not EODHD_API_KEY:
warnings.append(
"EODHD_API_KEY is missing - historical data fallback will be limited"
)
# Validate MiroFish configuration
if MIROFISH_ENABLED and not MIROFISH_API_BASE:
warnings.append("MIROFISH_ENABLED is true but MIROFISH_API_BASE is missing")
# Validate data directories
try:
DATA_DIR.mkdir(parents=True, exist_ok=True)
MEMORY_DIR.mkdir(parents=True, exist_ok=True)
SIMULATION_DIR.mkdir(parents=True, exist_ok=True)
except Exception as e:
errors.append(f"Failed to create data directories: {e}")
# Log results β€” NEVER exit, always allow degraded mode
if errors:
logger.error(
"Configuration validation errors (app will start in degraded mode):"
)
for error in errors:
logger.error(f" - {error}")
if warnings:
logger.warning("Configuration validation completed with warnings:")
for warning in warnings:
logger.warning(f" - {warning}")
else:
logger.info("Configuration validation passed")
return warnings
# ── Data directory initialization ────────────────────────────────────────────
ALL_DATA_DIRS = [
DATA_DIR,
MEMORY_DIR,
SIMULATION_DIR,
DATA_DIR / "memory",
DATA_DIR / "simulations",
DATA_DIR / "knowledge",
DATA_DIR / "learning",
DATA_DIR / "cache",
DATA_DIR / "adaptive",
DATA_DIR / "sentinel",
DATA_DIR / "sentinel" / "pending_patches",
DATA_DIR / "curiosity",
DATA_DIR / "daemon",
DATA_DIR / "dreams",
DATA_DIR / "memory_graph",
DATA_DIR / "curation",
]
def ensure_data_dirs():
"""Idempotent: create all runtime data dirs. Call once at startup."""
for d in ALL_DATA_DIRS:
try:
d.mkdir(parents=True, exist_ok=True)
except Exception as e:
logger.warning(f"Failed to create data dir {d}: {e}")
# ── Feature Flags ────────────────────────────────────────────────────────────
FEATURES = {
"daemon": os.getenv("FEATURE_DAEMON", "true").lower() == "true",
"learning": os.getenv("FEATURE_LEARNING", "false").lower() == "true",
"sentinel": os.getenv("SENTINEL_ENABLED", os.getenv("FEATURE_SENTINEL", "true")).lower() == "true",
"simulation": os.getenv("SIMULATION_ENABLED", "true").lower() == "true",
"adaptive": os.getenv("FEATURE_ADAPTIVE", "false").lower() == "true",
"self_training": os.getenv("FEATURE_SELF_TRAINING", "false").lower() == "true",
"experimental": os.getenv("FEATURE_EXPERIMENTAL", "false").lower() == "true",
"user_patterns": os.getenv("FEATURE_USER_PATTERNS", "false").lower() == "true",
"lora": os.getenv("FEATURE_LORA", "false").lower() == "true",
}
def get_feature_status():
"""Return current feature flag status."""
return {
name: {"enabled": enabled, "env_var": f"FEATURE_{name.upper()}"}
for name, enabled in FEATURES.items()
}
# ── Learning layer configuration ─────────────────────────────────────────────
LEARNING_ENABLED = os.getenv("LEARNING_ENABLED", "false").lower() == "true"
KNOWLEDGE_MAX_SIZE_MB = int(os.getenv("KNOWLEDGE_MAX_SIZE_MB", "200"))
LEARNING_SCHEDULE_INTERVAL = int(os.getenv("LEARNING_SCHEDULE_INTERVAL", "6")) # hours
LEARNING_BATCH_SIZE = int(os.getenv("LEARNING_BATCH_SIZE", "10"))
LEARNING_TOPICS = [
item.strip()
for item in os.getenv(
"LEARNING_TOPICS",
"finance,markets,global equities,top companies,central banks,semiconductors,energy,technology,policy,india markets,china economy",
).split(",")
if item.strip()
]
def get_config():
"""Get configuration object for dependency injection."""
class Config:
app_version = APP_VERSION
primary_provider = PRIMARY_PROVIDER
fallback_provider = FALLBACK_PROVIDER
huggingface_api_key = HUGGINGFACE_API_KEY
huggingface_model = HUGGINGFACE_MODEL
gemini_api_key = GEMINI_API_KEY
groq_api_key = GROQ_API_KEY
cloudflare_account_id = CLOUDFLARE_ACCOUNT_ID
cloudflare_api_token = CLOUDFLARE_API_TOKEN
openrouter_api_key = OPENROUTER_API_KEY
openrouter_base_url = OPENROUTER_BASE_URL
openrouter_chat_model = OPENROUTER_CHAT_MODEL
openrouter_reasoner_model = OPENROUTER_REASONER_MODEL
ollama_enabled = OLLAMA_ENABLED
ollama_base_url = OLLAMA_BASE_URL
ollama_chat_model = OLLAMA_CHAT_MODEL
ollama_reasoner_model = OLLAMA_REASONER_MODEL
openai_api_key = OPENAI_API_KEY
openai_base_url = OPENAI_BASE_URL
openai_chat_model = OPENAI_CHAT_MODEL
openai_reasoner_model = OPENAI_REASONER_MODEL
tavily_api_key = TAVILY_API_KEY
newsapi_key = NEWSAPI_KEY
alphavantage_api_key = ALPHAVANTAGE_API_KEY
mirofish_enabled = MIROFISH_ENABLED
mirofish_api_base = MIROFISH_API_BASE
crawler_enabled = CRAWLER_ENABLED
crawler_timeout = CRAWLER_TIMEOUT
data_dir = str(DATA_DIR)
memory_dir = str(MEMORY_DIR)
simulation_dir = str(SIMULATION_DIR)
prompts_dir = str(PROMPTS_DIR)
learning_enabled = LEARNING_ENABLED
knowledge_max_size_mb = KNOWLEDGE_MAX_SIZE_MB
learning_schedule_interval = LEARNING_SCHEDULE_INTERVAL
learning_batch_size = LEARNING_BATCH_SIZE
learning_topics = LEARNING_TOPICS
sentinel_enabled = FEATURES["sentinel"]
return Config()