Stage 0: Repository und Architekturgrundlage
- Pyproject.toml mit FastAPI, Pydantic v2, SQLAlchemy, httpx, asyncio, BeautifulSoup4, selectolax, trafilatura, uvicorn, pytest-asyncio - Multi-stage Dockerfile (Python 3.12-slim, Non-Root-User nsct) - docker-compose.yml (nsct-api + postgres + optional searxng) - .env.example mit allen Config-Parametern - Config-System: AppSettings mit LLMConfig, VisionConfig, AudioConfig, DatabaseConfig — komplett aus Environment, keine Hardcodes - Strukturiertes Logging mit research_id/llm_request_id Tracking - Pydantic v2 Schemas: SearchQuery, Source, Claim, EvidenceRelation, CitationEdge, ResearchReport - SQLAlchemy 2.0 Declarative Models + async Engine Factory - SSRF-Schutz: URL-Validation, IP-Blocklist (RFC1918, Cloud Metadata, file://, ftp://) - Provider-Interfaces: LLMProvider, VisionProvider, AudioProvider, SearchProvider, ContentFetcher als ABCs - Health-Endpoints: /health, /ready (LLM-Connect-Test), /providers - FastAPI App mit CORS, lifespan (LLM Pre-Flight) - CLI-Stub mit Entry-Points: nsct, nsct-core, nsct-api - 6 Test-Cases: /health, /ready, /providers + No-Secrets-Test - Vollständige Dokumentation: README, ARCHITECTURE, SECURITY, METHODOLOGY, API, DEPLOYMENT - .gitignore (Python, Docker, IDE, .env)
This commit is contained in:
116
src/nsct/config.py
Normal file
116
src/nsct/config.py
Normal file
@@ -0,0 +1,116 @@
|
||||
"""Central configuration — all values from environment, zero hard-coded secrets."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
|
||||
|
||||
class LLMConfig(BaseModel):
|
||||
"""LLM worker configuration."""
|
||||
|
||||
base_url: str = Field(..., description="Base URL of the LLM provider (OpenAI-compatible).")
|
||||
model: str = Field(..., description="Model identifier to use for LLM calls.")
|
||||
max_concurrency: int = Field(default=3, ge=1, description="Max parallel LLM requests.")
|
||||
|
||||
def get_secret(self) -> str:
|
||||
"""Return the raw API key string if present."""
|
||||
key = os.environ.get("NSCT_LLM_API_KEY", "")
|
||||
return key if key else ""
|
||||
|
||||
|
||||
class VisionConfig(BaseModel):
|
||||
"""Vision (image/video) model configuration."""
|
||||
|
||||
base_url: str = Field(..., description="Base URL of the vision provider endpoint.")
|
||||
model: str = Field(..., description="Vision model identifier.")
|
||||
|
||||
def get_secret(self) -> str:
|
||||
key = os.environ.get("NSCT_VISION_API_KEY", "")
|
||||
return key if key else ""
|
||||
|
||||
|
||||
class AudioConfig(BaseModel):
|
||||
"""Audio / speech model configuration."""
|
||||
|
||||
base_url: str = Field(..., description="Base URL of the audio provider endpoint.")
|
||||
model: str = Field(default="default", description="Audio model identifier.")
|
||||
|
||||
def get_secret(self) -> str:
|
||||
key = os.environ.get("NSCT_AUDIO_API_KEY", "")
|
||||
return key if key else ""
|
||||
|
||||
|
||||
class DatabaseConfig(BaseModel):
|
||||
"""Database connection configuration."""
|
||||
|
||||
url: str = Field(
|
||||
...,
|
||||
description="Database URL (postgresql+asyncpg://...) or a default SQLite fallback.",
|
||||
)
|
||||
|
||||
|
||||
class AppSettings(BaseModel):
|
||||
"""Top-level application settings."""
|
||||
|
||||
llm: LLMConfig = Field(default_factory=LLMConfig)
|
||||
vision: VisionConfig = Field(default_factory=VisionConfig)
|
||||
audio: AudioConfig = Field(default_factory=AudioConfig)
|
||||
postgres: DatabaseConfig = Field(default_factory=DatabaseConfig)
|
||||
debug: bool = Field(default=False)
|
||||
searxng_base_url: str | None = Field(default=None, description="SearXNG instance URL.")
|
||||
|
||||
@staticmethod
|
||||
def from_env() -> "AppSettings":
|
||||
"""Build AppSettings entirely from environment variables."""
|
||||
llm_base_url = os.environ.get("NSCT_LLM_BASE_URL", "")
|
||||
llm_model = os.environ.get("NSCT_LLM_MODEL", "")
|
||||
llm_concurrency = int(os.environ.get("NSCT_LLM_MAX_CONCURRENCY", "3"))
|
||||
|
||||
vision_base_url = os.environ.get("NSCT_VISION_BASE_URL", "")
|
||||
vision_model = os.environ.get("NSCT_VISION_MODEL", "")
|
||||
|
||||
audio_base_url = os.environ.get("NSCT_AUDIO_BASE_URL", "")
|
||||
audio_model = os.environ.get("NSCT_AUDIO_MODEL", "default")
|
||||
|
||||
db_url = os.environ.get("NSCT_DB_URL", "")
|
||||
|
||||
llm_cfg = LLMConfig(
|
||||
base_url=llm_base_url,
|
||||
model=llm_model,
|
||||
max_concurrency=llm_concurrency,
|
||||
)
|
||||
|
||||
vision_cfg = VisionConfig(
|
||||
base_url=vision_base_url,
|
||||
model=vision_model,
|
||||
)
|
||||
|
||||
audio_cfg = AudioConfig(
|
||||
base_url=audio_base_url,
|
||||
model=audio_model,
|
||||
)
|
||||
|
||||
postgres_cfg = DatabaseConfig(url=db_url)
|
||||
|
||||
debug = os.environ.get("NSCT_DEBUG", "false").lower() == "true"
|
||||
searxng_url = os.environ.get("NSCT_SEARXNG_BASE_URL", None)
|
||||
|
||||
return AppSettings(
|
||||
llm=llm_cfg,
|
||||
vision=vision_cfg,
|
||||
audio=audio_cfg,
|
||||
postgres=postgres_cfg,
|
||||
debug=debug,
|
||||
searxng_base_url=searxng_url,
|
||||
)
|
||||
|
||||
@property
|
||||
def config_dir(self) -> Path:
|
||||
"""Return a config directory path for persisting runtime data."""
|
||||
data_dir = Path.home() / ".nsct"
|
||||
data_dir.mkdir(parents=True, exist_ok=True)
|
||||
return data_dir
|
||||
Reference in New Issue
Block a user