Files
codex-py/codex/config.py
Tarik Moussa 8cf0cc7e01 feat(synthesis): Lead/Provenance model + grounded leads + conjecture generator
- codex/synthesis.py: Lead/Provenance dataclasses, find_connections/gaps/improvements,
  propose_conjectures (status=unverified, quarantined in leads/conjectures/),
  write_leads (grounded→leads/grounded/, conjectures→leads/conjectures/ HARD invariant)
- codex/cli.py: synthesis leads/conjectures/report command group
- codex/config.py: F-13 settings (leads_dir, synthesis_llm_*, synthesis_top_k,
  synthesis_min_grounded_ratio, synthesis_gap_min_coverage)
- tests/synthesis/: 42 tests covering model, grounding, conjecture invariant, quarantine, CLI
- spike/: F-13 spike script + output (live run attempted)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-14 08:37:42 +02:00

230 lines
7.3 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-13 Synthesis / Lead-Engine
# ------------------------------------------------------------------
leads_dir: str = Field(
default="leads/",
description=(
"Directory where generated leads are written. "
"Grounded leads → leads/grounded/, conjectures → leads/conjectures/. "
"Relative paths are resolved from the current working directory."
),
)
synthesis_llm_model: str = Field(
default="qwen2.5:7b",
description=(
"Ollama model name used for synthesis. "
"Must be available at the configured Ollama endpoint."
),
)
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 topic for synthesis.",
)
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 lead to be "
"emitted as grounded (not marked ⚠)."
),
)
synthesis_gap_min_coverage: int = Field(
default=2,
gt=0,
description=(
"Minimum number of papers covering a topic before it is NOT flagged as a gap. "
"Topics with fewer papers → gap lead generated."
),
)
# ------------------------------------------------------------------
# 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."
),
)
@lru_cache(maxsize=1)
def get_settings() -> Settings:
"""Return the cached application settings singleton."""
return Settings()