feat(core): add core layer with config, clients, exceptions, models, and app factory
This commit is contained in:
1
src/core/__init__.py
Normal file
1
src/core/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""Core layer — config, dependencies, exceptions, and shared models."""
|
||||||
39
src/core/config.py
Normal file
39
src/core/config.py
Normal 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
20
src/core/dependencies.py
Normal 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
52
src/core/exceptions.py
Normal 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
59
src/core/models.py
Normal 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
43
src/main.py
Normal 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()
|
||||||
Reference in New Issue
Block a user