Files
codex-py/codex/config.py
Tarik Moussa b61d84d4bd feat(F-16): chunk quality gate + section classification
- codex/quality.py: 3-signal filter (length / alpha-ratio / bib-score)
  + rule-based section classifier + run_quality_pass retroactive DB pass
- codex/ingest.py: promote quality imports to module level; apply
  filter_chunks before embedding; store section column in chunks INSERT
- codex/config.py: CHUNK_MIN_CHARS / CHUNK_MIN_ALPHA_RATIO / CHUNK_MAX_BIB_SCORE
- infra/schema.sql: ALTER TABLE chunks ADD COLUMN IF NOT EXISTS section TEXT
- .env.example: document F-16 quality thresholds
- codex/cli.py: quality run sub-command (scope by --paper-id or all papers)
- tests/quality/: 35 new tests covering all quality functions
- tests/ingest/: patch filter_chunks in source-path tests to isolate ingest

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 03:22:11 +02:00

294 lines
9.4 KiB
Python

"""Application configuration via environment variables / .env file.
All settings are read from the environment (or a .env file in the project
root). Import :func:`get_settings` wherever you need configuration; the
returned object is cached after the first call.
"""
from __future__ import annotations
from functools import lru_cache
from pydantic import AliasChoices, Field
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
"""Centralised, env-driven configuration for the codex application."""
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
case_sensitive=False,
extra="ignore",
)
# ------------------------------------------------------------------
# Database
# ------------------------------------------------------------------
database_url: str = Field(
default="postgresql://researcher:change_me@localhost:5432/papers",
description=(
"libpq-compatible connection string consumed by psycopg. "
"Example: postgresql://user:pass@host:5432/dbname"
),
)
# ------------------------------------------------------------------
# External services
# ------------------------------------------------------------------
grobid_url: str = Field(
default="http://localhost:8070",
description="Base URL of the GROBID HTTP API (containerised).",
)
nougat_url: str = Field(
default="http://localhost:8080",
validation_alias=AliasChoices("NOUGAT_URL", "nougat_url"),
description="Base URL of the Nougat OCR HTTP API (containerised).",
)
ollama_base_url: str = Field(
default="http://localhost:11434",
description="Base URL of the local Ollama endpoint (optional Q&A layer).",
)
# ------------------------------------------------------------------
# Embeddings
# ------------------------------------------------------------------
embedding_model: str = Field(
default="BAAI/bge-m3",
description=(
"sentence-transformers model identifier. "
"Must match EMBEDDING_DIM. Default: BAAI/bge-m3 (1024 dims)."
),
)
embedding_dim: int = Field(
default=1024,
gt=0,
description=(
"Dimension of the dense embedding vectors. "
"Must match the output dimension of EMBEDDING_MODEL."
),
)
# ------------------------------------------------------------------
# API etiquette
# ------------------------------------------------------------------
openalex_mailto: str = Field(
default="",
description=(
"E-mail address for the OpenAlex Polite Pool (faster rate limits). "
"Required by OpenAlex ToS for automated access."
),
)
# ------------------------------------------------------------------
# F-12 Wiki-Compile
# ------------------------------------------------------------------
wiki_dir: str = Field(
default="wiki/",
description=(
"Directory where compiled wiki pages are written. "
"Relative paths are resolved from the current working directory."
),
)
wiki_llm_model: str = Field(
default="qwen2.5:7b",
description=(
"Ollama model name used for wiki synthesis. "
"Must be available at the configured Ollama endpoint (WIKI_LLM_URL or "
"OLLAMA_BASE_URL). Default: qwen2.5:7b (qwen-light profile on Jetson)."
),
)
wiki_llm_url: str | None = Field(
default=None,
description=(
"Ollama base URL for wiki synthesis. "
"When None, falls back to OLLAMA_BASE_URL "
"(http://192.168.178.103:11434 for the Jetson). "
"Set WIKI_LLM_URL to override."
),
)
wiki_top_k: int = Field(
default=12,
gt=0,
description=(
"Number of top chunks retrieved per concept for wiki synthesis. "
"Higher values improve recall at the cost of a larger LLM prompt."
),
)
wiki_min_grounding_rate: float = Field(
default=0.5,
ge=0.0,
le=1.0,
description=(
"Minimum fraction of claims that must be grounded for a page to be written "
"to wiki/<slug>.md. Pages below this threshold are quarantined to "
"wiki/draft/<slug>.md instead."
),
)
# ------------------------------------------------------------------
# F-09 Rich Parsing
# ------------------------------------------------------------------
mathpix_app_id: str | None = Field(
default=None,
description=(
"MathPix App ID for cloud formula extraction. "
"When None, pix2tex local OCR is used as fallback."
),
)
mathpix_app_key: str | None = Field(
default=None,
description=(
"MathPix App Key for cloud formula extraction. "
"Must be set together with MATHPIX_APP_ID."
),
)
pix2tex_fallback: bool = Field(
default=True,
description=(
"Enable pix2tex (local LaTeX OCR) as fallback when MathPix credentials "
"are absent. Set to False to disable formula extraction entirely without creds."
),
)
figures_dir: str = Field(
default="figures/",
description=(
"Directory where extracted figure images are written. "
"Relative paths are resolved from the current working directory."
),
)
# ------------------------------------------------------------------
# F-13 Synthesis / Lead-Engine
# ------------------------------------------------------------------
leads_dir: str = Field(
default="leads/",
description=(
"Directory where synthesis leads are written. "
"Grounded leads land in <leads_dir>/grounded/, conjectures in "
"<leads_dir>/conjectures/. INVARIANT: conjectures NEVER touch wiki/."
),
)
synthesis_llm_model: str = Field(
default="qwen2.5:7b",
description=(
"Ollama model name used for synthesis (connection / gap / improvement / "
"conjecture). Defaults to the qwen-light profile on the Jetson."
),
)
synthesis_llm_url: str | None = Field(
default=None,
description=("Ollama base URL for synthesis. When None, falls back to OLLAMA_BASE_URL."),
)
synthesis_top_k: int = Field(
default=20,
gt=0,
description=(
"Number of top chunks retrieved per synthesis probe. Higher values "
"improve recall at the cost of a larger LLM prompt."
),
)
synthesis_min_grounded_ratio: float = Field(
default=0.5,
ge=0.0,
le=1.0,
description=(
"Minimum fraction of claims that must be grounded for a grounded lead "
"(connection / gap / improvement) to be emitted. Leads below this "
"threshold are dropped rather than written to leads/grounded/."
),
)
synthesis_gap_min_coverage: int = Field(
default=2,
gt=0,
description=(
"A topic is considered a 'gap' candidate when it is covered by fewer "
"than this many distinct bibkeys in the retrieved top-K."
),
)
# ------------------------------------------------------------------
# F-14 MCP Server
# ------------------------------------------------------------------
mcp_transport: str = Field(
default="stdio",
description=(
"Transport protocol for the MCP server. "
"Allowed values: 'stdio' (default) or 'http'. "
"HTTP transport requires MCP_AUTH_TOKEN to be set."
),
)
mcp_host: str = Field(
default="127.0.0.1",
description="Bind host for HTTP transport (ignored for stdio).",
)
mcp_port: int = Field(
default=8765,
gt=0,
description="Bind port for HTTP transport (ignored for stdio).",
)
mcp_auth_token: str | None = Field(
default=None,
description=(
"Bearer token required when mcp_transport='http'. "
"If HTTP transport is requested but this is None, the server refuses to start."
),
)
# ------------------------------------------------------------------
# F-16 Chunk Quality Gate
# ------------------------------------------------------------------
chunk_min_chars: int = Field(
default=60,
gt=0,
description=(
"Minimum character count for a chunk to pass the quality gate. "
"Chunks shorter than this value are discarded before embedding."
),
)
chunk_min_alpha_ratio: float = Field(
default=0.40,
ge=0.0,
le=1.0,
description=(
"Minimum fraction of alphabetic characters in a chunk. "
"Chunks with a lower ratio are treated as OCR artefacts and discarded."
),
)
chunk_max_bib_score: float = Field(
default=0.70,
ge=0.0,
le=1.0,
description=(
"Maximum bibliography heuristic score before a chunk is rejected. "
"Score is based on DOI density, 'et al.' and year-in-brackets patterns."
),
)
@lru_cache(maxsize=1)
def get_settings() -> Settings:
"""Return the cached application settings singleton."""
return Settings()