Self-hosted, Docker-based agentic troubleshooting platform: FastAPI backend + LangGraph agent, Next.js UI, tiered LLM routing (local Ollama -> Gemini -> DeepSeek -> OpenRouter), MCP server manager, encrypted device credentials, RBAC, audit log, project-memory + Obsidian integrations, and editable troubleshooting decision rules tuned for the GeneseasX vessel stack. Co-authored-by: Cursor <cursoragent@cursor.com>
116 lines
4.3 KiB
Python
116 lines
4.3 KiB
Python
"""Application configuration, loaded from environment variables."""
|
|
from __future__ import annotations
|
|
|
|
from functools import lru_cache
|
|
|
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
|
|
|
|
class Settings(BaseSettings):
|
|
model_config = SettingsConfigDict(env_file=".env", extra="ignore", case_sensitive=False)
|
|
|
|
# Core security
|
|
secret_key: str = "dev-insecure-secret-change-me"
|
|
credential_encryption_key: str = ""
|
|
access_token_expire_minutes: int = 60 * 12
|
|
algorithm: str = "HS256"
|
|
|
|
first_admin_email: str = "admin@agentic.local"
|
|
first_admin_password: str = "changeme"
|
|
|
|
# Database / cache
|
|
database_url: str = "postgresql+asyncpg://agentic:agentic@postgres:5432/agentic_os"
|
|
redis_url: str = "redis://redis:6379/0"
|
|
|
|
# CORS: comma-separated allowed browser origins (no wildcard with credentials).
|
|
cors_allow_origins: str = "http://localhost:3000"
|
|
|
|
# LLM routing
|
|
litellm_base_url: str = "http://litellm:4000"
|
|
litellm_master_key: str = "sk-litellm-master-change-me"
|
|
ollama_base_url: str = "http://host.docker.internal:11434"
|
|
local_triage_model: str = "qwen2.5:7b-instruct"
|
|
cloud_reasoning_model: str = "openrouter/deepseek/deepseek-chat"
|
|
|
|
# Tiered LLM routing (local → gemini → deepseek → openrouter)
|
|
llm_local_model: str = "qwen2.5:7b-instruct"
|
|
llm_economy_gemini: str = "gemini-2.5-flash"
|
|
llm_economy_deepseek: str = "deepseek-chat"
|
|
llm_economy_openrouter: str = "deepseek/deepseek-chat"
|
|
llm_premium_gemini: str = "gemini-2.5-pro"
|
|
llm_premium_deepseek: str = "deepseek-reasoner"
|
|
llm_premium_openrouter: str = "anthropic/claude-3.5-sonnet"
|
|
llm_auto_escalate: bool = True
|
|
llm_max_tier: str = "premium"
|
|
llm_cloud_provider_order: str = "gemini,deepseek,openrouter"
|
|
|
|
# Cloud API keys
|
|
gemini_api_key: str = ""
|
|
openrouter_api_key: str = ""
|
|
openrouter_provisioning_key: str = ""
|
|
deepseek_api_key: str = ""
|
|
|
|
# Balance polling
|
|
balance_poll_interval_seconds: int = 900
|
|
min_balance_usd: float = 1.0
|
|
|
|
# Gitea / MCP repos
|
|
gitea_base_url: str = "http://10.77.30.250:3000"
|
|
gitea_username: str = "nearxos"
|
|
gitea_token: str = ""
|
|
mcp_repos: str = "ssh-generic-mcp,fortigate-mcp,fortiswitch-mcp,pfsense-mcp,proxmox-mcp,asterisk-mcp"
|
|
|
|
# Obsidian
|
|
obsidian_vault_repo: str = "homelab-vault"
|
|
obsidian_tasks_folder: str = "agentic-os-tasks"
|
|
obsidian_auto_push: bool = True
|
|
|
|
# Project memory MCP
|
|
memory_mcp_url: str = "http://10.77.30.184:8787/mcp"
|
|
memory_mcp_token: str = ""
|
|
memory_task_project_id: str = "agentic-os-task"
|
|
memory_search_projects: str = "agentic-os-task,agentic-os,global"
|
|
obsidian_search_paths: str = "agentic-os-tasks,Meta/Agent-Memory"
|
|
|
|
# Troubleshooting decision rules (YAML, editable via Admin → Rules)
|
|
troubleshooting_rules_path: str = "/app/rules/troubleshooting.yaml"
|
|
|
|
# Runtime paths (inside container)
|
|
runtime_dir: str = "/runtime"
|
|
|
|
# MCP self-development: agent patches MCP source on tool failure / missing capability
|
|
mcp_development_enabled: bool = True
|
|
mcp_dev_auto_push: bool = False # when false, patch applies + reload; git push needs approval
|
|
mcp_dev_model_tier: str = "premium"
|
|
|
|
# Worker: max tasks executed in parallel (MCP sessions are shared — use 1 for same-vessel safety)
|
|
worker_max_concurrent_tasks: int = 1
|
|
|
|
@property
|
|
def cors_origin_list(self) -> list[str]:
|
|
return [o.strip() for o in self.cors_allow_origins.split(",") if o.strip()]
|
|
|
|
@property
|
|
def mcp_repo_list(self) -> list[str]:
|
|
return [r.strip() for r in self.mcp_repos.split(",") if r.strip()]
|
|
|
|
def gitea_clone_url(self, repo: str) -> str:
|
|
"""Authenticated clone URL for a Gitea repo (preserves http/https from base URL)."""
|
|
base = self.gitea_base_url.rstrip("/")
|
|
if self.gitea_token:
|
|
from urllib.parse import urlparse
|
|
|
|
parsed = urlparse(base if "://" in base else f"http://{base}")
|
|
scheme = parsed.scheme or "http"
|
|
host = parsed.netloc or parsed.path
|
|
return f"{scheme}://{self.gitea_username}:{self.gitea_token}@{host}/{self.gitea_username}/{repo}.git"
|
|
return f"{base}/{self.gitea_username}/{repo}.git"
|
|
|
|
|
|
@lru_cache
|
|
def get_settings() -> Settings:
|
|
return Settings()
|
|
|
|
|
|
settings = get_settings()
|