- 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>
230 lines
7.3 KiB
Python
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()
|