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:
NSCT Agent
2026-08-23 11:33:45 +00:00
commit e9410be941
28 changed files with 4192 additions and 0 deletions

116
src/nsct/config.py Normal file
View 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