feat(core): add core layer with config, clients, exceptions, models, and app factory

This commit is contained in:
2026-07-21 18:51:57 +03:30
parent c5bf8d5e1e
commit 4edc355ae5
6 changed files with 214 additions and 0 deletions

1
src/core/__init__.py Normal file
View File

@@ -0,0 +1 @@
"""Core layer — config, dependencies, exceptions, and shared models."""

39
src/core/config.py Normal file
View File

@@ -0,0 +1,39 @@
"""Application configuration loaded from .env via pydantic-settings."""
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
"""All environment variables with defaults and validation."""
model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8")
# OpenAI
openai_api_key: str
embedding_model: str = "text-embedding-3-small"
llm_model: str = "gpt-4o-mini"
# Qdrant
qdrant_url: str = "http://localhost:6333"
qdrant_api_key: str | None = None
# Retrieval
top_k: int = 5
# LLM generation
temperature: float = 0.0
max_tokens: int = 1024
# Chunking defaults
chunk_size: int = 512
chunk_overlap: int = 50
# Semantic chunking
semantic_threshold: float = 0.3
semantic_min_chunk_size: int = 3
# Database
database_url: str = "sqlite:///./data/chunking_benchmark.db"
settings = Settings()

20
src/core/dependencies.py Normal file
View File

@@ -0,0 +1,20 @@
"""Dependency injection singletons for OpenAI and Qdrant clients."""
from functools import lru_cache
from openai import OpenAI
from qdrant_client import QdrantClient
from src.core.config import settings
@lru_cache()
def get_openai_client() -> OpenAI:
"""Return a cached OpenAI client singleton."""
return OpenAI(api_key=settings.openai_api_key)
@lru_cache()
def get_qdrant_client() -> QdrantClient:
"""Return a cached Qdrant client singleton."""
return QdrantClient(url=settings.qdrant_url, api_key=settings.qdrant_api_key)

52
src/core/exceptions.py Normal file
View File

@@ -0,0 +1,52 @@
"""Custom exception classes and FastAPI exception handlers."""
from fastapi import Request
from fastapi.responses import JSONResponse
class ChunkingError(Exception):
"""Base exception for chunking-related errors."""
class StrategyNotFoundError(ChunkingError):
"""Raised when an unknown strategy name is requested."""
class DocumentProcessingError(ChunkingError):
"""Raised when a document cannot be parsed or processed."""
class EnrichmentError(ChunkingError):
"""Raised when the contextual enrichment LLM call fails."""
class EmbeddingError(ChunkingError):
"""Raised when the OpenAI embedding API call fails."""
class QdrantError(ChunkingError):
"""Raised when a Qdrant operation fails."""
class BenchmarkError(Exception):
"""Base exception for benchmarking-related errors."""
class DryRunError(BenchmarkError):
"""Raised when a dry-run estimation fails."""
async def chunking_exception_handler(request: Request, exc: ChunkingError) -> JSONResponse:
"""Handle all chunking-related errors."""
return JSONResponse(
status_code=400,
content={"detail": str(exc), "type": type(exc).__name__},
)
async def benchmark_exception_handler(request: Request, exc: BenchmarkError) -> JSONResponse:
"""Handle all benchmarking-related errors."""
return JSONResponse(
status_code=400,
content={"detail": str(exc), "type": type(exc).__name__},
)

59
src/core/models.py Normal file
View File

@@ -0,0 +1,59 @@
"""Shared Pydantic models used across all domains."""
from enum import Enum
from typing import Optional
from pydantic import BaseModel, Field
class StrategyName(str, Enum):
"""Canonical identifiers for each chunking strategy."""
CONTEXTUAL_STRUCTURE = "contextual_structure"
PARENT_CHILD = "parent_child"
SEMANTIC = "semantic"
MARKDOWN_STRUCTURE = "markdown_structure"
RECURSIVE = "recursive"
class Chunk(BaseModel):
"""Unified chunk model returned by all five chunking strategies.
Strategy-specific fields (parent_id, enriched_content) are nullable
when not applicable to the strategy that produced the chunk.
"""
document_name: str
chunk_id: str
strategy_name: StrategyName
chunk_index: int
text: str
token_count: int
character_count: int
parent_id: Optional[str] = None
enriched_content: Optional[str] = None
class ChunkMetadata(BaseModel):
"""Payload stored alongside every chunk vector in Qdrant."""
document_name: str
chunk_id: str
strategy_name: StrategyName
chunk_index: int
token_count: int
character_count: int
parent_id: Optional[str] = None
def chunk_to_metadata(chunk: Chunk) -> ChunkMetadata:
"""Extract metadata payload from a Chunk for Qdrant storage."""
return ChunkMetadata(
document_name=chunk.document_name,
chunk_id=chunk.chunk_id,
strategy_name=chunk.strategy_name,
chunk_index=chunk.chunk_index,
token_count=chunk.token_count,
character_count=chunk.character_count,
parent_id=chunk.parent_id,
)

43
src/main.py Normal file
View File

@@ -0,0 +1,43 @@
"""FastAPI app factory. Composes all domain routers and middleware."""
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from src.core.exceptions import (
ChunkingError,
BenchmarkError,
chunking_exception_handler,
benchmark_exception_handler,
)
def create_app() -> FastAPI:
"""Create and configure the FastAPI application.
Mounts domain routers and registers exception handlers.
Routers are imported lazily — domains are added in later phases.
"""
app = FastAPI(
title="RAG Chunking Benchmarker",
description="Benchmark five chunking strategies on regulatory documents. "
"Compare retrieval quality, answer faithfulness, and cost.",
version="0.1.0",
)
# CORS — allow all origins for development
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Register exception handlers
app.add_exception_handler(ChunkingError, chunking_exception_handler)
app.add_exception_handler(BenchmarkError, benchmark_exception_handler)
return app
app = create_app()