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

184
src/nsct/models/schemas.py Normal file
View File

@@ -0,0 +1,184 @@
"""Pydantic v2 schemas for NSCT data objects."""
from __future__ import annotations
from datetime import datetime
from enum import Enum
from typing import Any
from uuid import UUID, uuid4
from pydantic import BaseModel, Field
# ---------------------------------------------------------------------------
# Enums
# ---------------------------------------------------------------------------
class ClaimType(str, Enum):
"""Types of claims that can be extracted from sources."""
FACTUAL = "factual"
OPINION = "opinion"
PREDICTION = "prediction"
EVALUATION = "evaluation"
COMPARISON = "comparison"
class EvidenceRelationType(str, Enum):
"""Relationship types between two claims."""
AGREES = "agrees"
DISAGREES = "disagrees"
NEUTRAL = "neutral"
PARTIALLY_AGREES = "partially_agrees"
PARTIALLY_DISAGREES = "partially_disagrees"
CONTRADICTS = "contradicts"
class SourceType(str, Enum):
"""Classification of a source."""
NEWS = "news"
ACADEMIC = "academic"
BLOG = "blog"
GOVERNMENT = "government"
CORPORATE = "corporate"
SOCIAL_MEDIA = "social_media"
DOCUMENT = "document"
OTHER = "other"
class EdgeRelation(str, Enum):
"""Relationship between two sources."""
CITATION = "citation"
CORROBORATION = "corroboration"
CONTRADICTION = "contradiction"
DEPENDS_ON = "depends_on"
SUPPLEMENTS = "supplements"
# ---------------------------------------------------------------------------
# SearchQuery
# ---------------------------------------------------------------------------
class SearchQuery(BaseModel):
"""Represents a user-initiated search/research query."""
id: UUID = Field(default_factory=uuid4)
research_id: UUID = Field(default_factory=uuid4)
query: str = Field(..., min_length=1, description="The search query text.")
purpose: str | None = Field(default=None, description="Intent behind the query.")
language: str = Field(default="de", description="Language code, e.g. 'de', 'en'.")
category: str | None = Field(default=None, description="Optional category for grouping.")
created_at: datetime = Field(default_factory=datetime.utcnow)
model_config = {"frozen": True}
# ---------------------------------------------------------------------------
# Source
# ---------------------------------------------------------------------------
class Source(BaseModel):
"""A single retrieved source (web page, document, etc.)."""
id: UUID = Field(default_factory=uuid4)
url: str = Field(..., description="Original URL.")
canonical_url: str | None = Field(default=None, description="Canonical URL after redirect resolution.")
domain: str = Field(..., description="Extracted domain.")
title: str | None = Field(default=None)
author: str | None = Field(default=None)
publisher: str | None = Field(default=None)
publication_date: datetime | None = Field(default=None)
retrieved_at: datetime = Field(default_factory=datetime.utcnow)
content_type: str | None = Field(default=None, description="MIME type.")
source_type: SourceType | None = Field(default=None)
language: str = Field(default="unknown")
content_hash: str | None = Field(default=None, description="SHA-256 hash of raw content.")
parent_source_id: UUID | None = Field(default=None, description="Parent source for mirrors/canonical pairs.")
content: str | None = Field(default=None, description="Extracted text content.")
model_config = {"frozen": True}
# ---------------------------------------------------------------------------
# Claim
# ---------------------------------------------------------------------------
class Claim(BaseModel):
"""A factual assertion extracted from a Source."""
id: UUID = Field(default_factory=uuid4)
source_id: UUID = Field(..., description="UUID of the Source this claim came from.")
claim: str = Field(..., description="Original claim text.")
normalized_claim: str | None = Field(default=None, description="De-biased / neutral claim wording.")
claim_type: ClaimType = Field(default=ClaimType.FACTUAL)
subject: str | None = Field(default=None)
predicate: str | None = Field(default=None)
object: str | None = Field(default=None)
evidence_span: str | None = Field(default=None, description="Span of text in the source that supports this claim.")
confidence: float = Field(default=1.0, ge=0.0, le=1.0, description="Extraction confidence (0-1).")
event_date: datetime | None = Field(default=None)
model_config = {"frozen": True}
# ---------------------------------------------------------------------------
# EvidenceRelation
# ---------------------------------------------------------------------------
class EvidenceRelation(BaseModel):
"""Relationship between two claims."""
claim_a: UUID = Field(..., description="UUID of the first claim.")
claim_b: UUID = Field(..., description="UUID of the second claim.")
relation: EvidenceRelationType = Field(...)
confidence: float = Field(default=0.5, ge=0.0, le=1.0)
reason: str | None = Field(default=None, description="Free-text explanation of the relation.")
# ---------------------------------------------------------------------------
# CitationEdge
# ---------------------------------------------------------------------------
class CitationEdge(BaseModel):
"""Directed edge between two sources indicating a relationship."""
source_from: UUID = Field(..., description="UUID of the citing source.")
source_to: UUID = Field(..., description="UUID of the cited source.")
relation: EdgeRelation = Field(...)
confidence: float = Field(default=1.0, ge=0.0, le=1.0)
# ---------------------------------------------------------------------------
# ResearchReport
# ---------------------------------------------------------------------------
class ResearchReport(BaseModel):
"""Final aggregated research report."""
research_id: UUID = Field(default_factory=uuid4)
query: str = Field(...)
summary: str = Field(default="", description="Human-readable summary.")
findings: list[str] = Field(default_factory=list, description="List of key findings.")
disagreements: list[dict[str, Any]] = Field(
default_factory=list, description="Conflicting claims with details."
)
uncertainties: list[str] = Field(default_factory=list, description="Known uncertainties.")
source_statistics: dict[str, Any] = Field(
default_factory=dict, description="Stats: counts per source type, language, etc."
)
methodology: str = Field(
default="", description="Description of the methodology used to produce the report."
)
generated_at: datetime = Field(default_factory=datetime.utcnow)
model_config = {"frozen": True}