File size: 5,002 Bytes
4ef118d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
592cb1d
 
 
 
4ef118d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
"""
Application configuration using Pydantic Settings.
Loads configuration from environment variables and .env files.
"""

import os
from pathlib import Path

from pydantic import Field, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict


def get_backend_dir() -> Path:
    """Get the backend directory path."""
    # Try to get from environment first
    if env_path := os.getenv("BACKEND_DIR"):
        return Path(env_path)
    # Fallback to script location
    return Path(__file__).parent.parent


class Settings(BaseSettings):
    """Application settings loaded from environment variables."""

    # Server Configuration
    host: str = Field(default="127.0.0.1", alias="HOST")
    port: int = Field(default=3002, alias="PORT")

    # CORS Configuration
    frontend_url: str = Field(default="http://localhost:3000", alias="FRONTEND_URL")
    frontend_urls: str = Field(
        default="http://localhost:3000",
        alias="FRONTEND_URLS",
    )

    # SSE Configuration
    sse_flush_ms: int = Field(default=50, alias="SSE_FLUSH_MS")
    sse_heartbeat_ms: int = Field(default=10000, alias="SSE_HEARTBEAT_MS")

    # Supabase Configuration
    supabase_project_name: str = Field(default="", alias="SUPABASE_PROJECT_NAME")
    supabase_url: str = Field(default="", alias="SUPABASE_URL")
    supabase_password: str = Field(default="", alias="SUPABASE_PASSWORD")
    supabase_service_role_key: str = Field(default="", alias="SUPABASE_SERVICE_ROLE_KEY")
    supabase_db_url: str = Field(default="", alias="SUPABASE_DB_URL")

    # Tavily API (for web search)
    tavily_api_key: str = Field(default="", alias="TAVILY_API_KEY")

    # Jina AI (for webpage reading)
    jina_api_key: str = Field(default="", alias="JINA_API_KEY")

    # Debug Flags
    debug_stream: bool = Field(default=False, alias="DEBUG_STREAM")
    debug_tools: bool = Field(default=False, alias="DEBUG_TOOLS")
    debug_sources: bool = Field(default=False, alias="DEBUG_SOURCES")

    # Context Message Limit
    context_message_limit: int = Field(default=50, alias="CONTEXT_MESSAGE_LIMIT")

    # Database Providers (backend-managed)
    database_providers_json: str = Field(default="", alias="DATABASE_PROVIDERS")
    db_access_key: str = Field(default="", alias="DB_PROVIDER_ACCESS_KEY")
    database_provider: str = Field(default="", alias="DATABASE_PROVIDER")
    database_url: str = Field(default="", alias="DATABASE_URL")
    database_path: str = Field(default="", alias="DATABASE_PATH")
    database_label: str = Field(default="", alias="DATABASE_LABEL")

    # Session Summary Configuration
    summary_lite_provider: str = Field(default="openai", alias="SUMMARY_LITE_PROVIDER")
    summary_lite_model: str = Field(default="gpt-4o-mini", alias="SUMMARY_LITE_MODEL")
    summary_agent_api_key: str = Field(default="", alias="SUMMARY_AGENT_API_KEY")
    summary_lite_base_url: str | None = Field(default=None, alias="SUMMARY_LITE_BASE_URL")

    # Memory Lite Model Configuration (Legacy / Long Term Memory)
    memory_lite_provider: str = Field(default="openai", alias="MEMORY_LITE_PROVIDER")
    memory_lite_model: str = Field(default="gpt-4o-mini", alias="MEMORY_LITE_MODEL")
    memory_agent_api_key: str = Field(default="", alias="MEMORY_AGENT_API_KEY")
    memory_lite_base_url: str | None = Field(default=None, alias="MEMORY_LITE_BASE_URL")


    # Model configuration
    @property
    def allowed_origins(self) -> list[str]:
        """Parse frontend_urls into a list of allowed origins."""
        return [origin.strip() for origin in self.frontend_urls.split(",") if origin.strip()]

    model_config = SettingsConfigDict(
        env_file=".env",
        env_file_encoding="utf-8",
        env_ignore_empty=True,
        extra="ignore",
    )

    @field_validator("sse_flush_ms", "sse_heartbeat_ms")
    @classmethod
    def validate_positive(cls, v: int) -> int:
        """Validate that numeric values are non-negative."""
        return max(0, v)


# Global settings instance
_settings: Settings | None = None


def get_settings() -> Settings:
    """Get the global settings instance (singleton)."""
    global _settings
    if _settings is None:
        if os.getenv("QURIO_ELECTRON", "0") == "1":
            _settings = Settings(_env_file=None)
            return _settings

        # Search paths for env files (priority order)
        src_dir = Path(__file__).parent
        backend_dir = src_dir.parent

        candidates = [
            src_dir / ".env.local",
            backend_dir / ".env.local",
            src_dir / ".env",
            backend_dir / ".env",
        ]

        env_file = None
        for candidate in candidates:
            if candidate.exists():
                env_file = str(candidate)
                break

        _settings = Settings(_env_file=env_file)
    return _settings


def reload_settings() -> Settings:
    """Reload settings from environment variables."""
    global _settings
    _settings = None
    return get_settings()