Spaces:
Running
Running
| 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() | |