brain / src /config.py
Leon4gr45's picture
Deploy complete BrainAPI source d15a5889e878
2f1e7ff verified
Raw History Blame Contribute Delete
30.2 kB
"""
File: /config.py
Created Date: Sunday October 19th 2025
Author: Christian Nonis <alch.infoemail@gmail.com>
-----
Last Modified: Sunday March 29th 2026 3:27:28 am
Modified By: Christian Nonis <alch.infoemail@gmail.com>
-----
"""
# pylint: disable=too-few-public-methods
import logging
import os
from typing import Literal
import dotenv
from pathlib import Path
_project_root = Path(__file__).resolve().parent.parent
_env_name = os.getenv("ENV")
_env_path = _project_root / f".env{'.' + _env_name if _env_name else ''}"
dotenv.load_dotenv(dotenv_path=_env_path)
from src.services.redaction import install_log_redaction
install_log_redaction()
logger = logging.getLogger(__name__)
def _normalize_openai_base_url(raw: str | None) -> str | None:
if not raw or not raw.strip():
return None
normalized = raw.strip().rstrip("/")
if normalized in ("https://api.openai.com/v1", "https://api.openai.com"):
return None
return normalized
def validate_pipeline_mode(mode: str | None) -> Literal["lightweight", "accurate"]:
if mode not in ("lightweight", "accurate"):
raise ValueError(
f"Invalid PIPELINE_MODE: {mode!r}. Expected 'lightweight' or 'accurate'."
)
return mode
def validate_ingest_architect_mode(
mode: str | None,
) -> Literal["tooler", "schema", "batch"]:
normalized = (mode or "batch").strip().lower()
aliases = {
"tooler": "tooler",
"current": "tooler",
"schema": "schema",
"batch": "batch",
}
if normalized not in aliases:
raise ValueError(
f"Invalid INGEST_ARCHITECT_MODE: {mode!r}. "
"Expected 'tooler', 'schema', or 'batch'."
)
return aliases[normalized] # type: ignore[return-value]
def validate_ingest_architect_prior_context(
mode: str | None,
) -> Literal["auto", "scratchpad", "raw"]:
normalized = (mode or "auto").strip().lower()
if normalized in ("auto", "scratchpad", "raw"):
return normalized # type: ignore[return-value]
raise ValueError(
f"Invalid INGEST_ARCHITECT_PRIOR_CONTEXT: {mode!r}. "
"Expected 'auto', 'scratchpad', or 'raw'."
)
def validate_search_fusion(value: str | None) -> Literal["rrf", "cc"]:
normalized = (value or "rrf").strip().lower()
if normalized in ("rrf", "cc"):
return normalized # type: ignore[return-value]
raise ValueError(
f"Invalid SEARCH_FUSION: {value!r}. Expected 'rrf' or 'cc'."
)
def validate_context_passage_mode(
value: str | None,
) -> Literal["hybrid", "bm25", "dense", "ilike"]:
normalized = (value or "hybrid").strip().lower()
if normalized in ("hybrid", "bm25", "dense", "ilike"):
return normalized # type: ignore[return-value]
raise ValueError(
f"Invalid CONTEXT_PASSAGE_MODE: {value!r}. "
"Expected 'hybrid', 'bm25', 'dense', or 'ilike'."
)
def validate_search_config(
*,
enabled: bool,
use_dense: bool,
use_bm25: bool,
data_db: str,
bm25_k1: float,
bm25_b: float,
) -> None:
if bm25_k1 <= 0:
raise ValueError(f"Invalid SEARCH_BM25_K1: {bm25_k1!r}. Must be > 0.")
if not 0.0 <= bm25_b <= 1.0:
raise ValueError(
f"Invalid SEARCH_BM25_B: {bm25_b!r}. Must be between 0 and 1."
)
if not enabled:
return
if use_bm25 and (data_db or "").strip().lower() != "postgresql":
raise ValueError(
"SEARCH_USE_BM25=true requires DATA_DB=postgresql "
"(BM25 indexes are Postgres-only)."
)
if not use_dense and not use_bm25:
raise ValueError(
"SEARCH_ENABLED=true requires SEARCH_USE_DENSE=true "
"and/or SEARCH_USE_BM25=true."
)
class AzureConfig:
"""
Configuration class for the Azure configuration.
"""
def __init__(self):
self.small_llm_model = os.getenv("AZURE_SMALL_LLM_MODEL", "gpt-4o-mini")
self.large_llm_model = os.getenv("AZURE_LARGE_LLM_MODEL")
self.llm_api_version = os.getenv("AZURE_LLM_API_VERSION") or os.getenv(
"AZURE_LARGE_LLM_API_VERSION"
)
self.llm_endpoint = os.getenv("AZURE_LLM_ENDPOINT") or os.getenv(
"AZURE_LARGE_LLM_ENDPOINT"
)
self.llm_subscription_key = os.getenv("AZURE_LLM_SUBSCRIPTION_KEY") or os.getenv(
"AZURE_LARGE_LLM_SUBSCRIPTION_KEY"
)
self.embedding_model = os.getenv("AZURE_EMBEDDING_MODEL", "text-embedding-3-large")
self.embedding_full_endpoint = os.getenv("AZURE_EMBEDDING_FULL_ENDPOINT")
self.embedding_key = os.getenv("AZURE_EMBEDDING_KEY")
def validate_llm(self):
if [
self.small_llm_model,
self.large_llm_model,
self.llm_api_version,
self.llm_endpoint,
self.llm_subscription_key,
].count(None) > 0:
raise ValueError("Azure LLM configuration is not complete")
def validate_embeddings(self):
if [self.embedding_full_endpoint, self.embedding_key].count(None) > 0:
raise ValueError("Azure embeddings configuration is not complete")
class OpenAIConfig:
def __init__(self):
self.api_key = os.getenv("OPENAI_API_KEY")
self.base_url = _normalize_openai_base_url(os.getenv("OPENAI_BASE_URL"))
self.small_llm_model = os.getenv("OPENAI_SMALL_LLM_MODEL", "gpt-4o-mini")
self.large_llm_model = os.getenv("OPENAI_LARGE_LLM_MODEL", "gpt-4o")
self.embedding_model = os.getenv(
"OPENAI_EMBEDDING_MODEL", "text-embedding-3-large"
)
def validate_llm(self, require_large: bool):
if not self.api_key:
raise ValueError("OpenAI API key is not set")
if not self.small_llm_model:
raise ValueError("OpenAI small LLM model is not set")
if require_large and not self.large_llm_model:
raise ValueError("OpenAI large LLM model is not set")
def validate_embeddings(self):
if not self.api_key:
raise ValueError("OpenAI API key is not set")
if not self.embedding_model:
raise ValueError("OpenAI embedding model is not set")
class AnthropicConfig:
def __init__(self):
self.api_key = os.getenv("ANTHROPIC_API_KEY")
self.small_llm_model = os.getenv(
"ANTHROPIC_SMALL_LLM_MODEL", "claude-3-5-haiku-latest"
)
self.large_llm_model = os.getenv(
"ANTHROPIC_LARGE_LLM_MODEL", "claude-sonnet-4-20250514"
)
def validate_llm(self, require_large: bool):
if not self.api_key:
raise ValueError("Anthropic API key is not set")
if not self.small_llm_model:
raise ValueError("Anthropic small LLM model is not set")
if require_large and not self.large_llm_model:
raise ValueError("Anthropic large LLM model is not set")
class DeepSeekConfig:
def __init__(self):
self.api_key = os.getenv("DEEPSEEK_API_KEY")
self.base_url = "https://api.deepseek.com"
self.small_llm_model = os.getenv(
"DEEPSEEK_SMALL_LLM_MODEL", "deepseek-v4-flash"
)
self.large_llm_model = os.getenv(
"DEEPSEEK_LARGE_LLM_MODEL", "deepseek-v4-pro"
)
def validate_llm(self, require_large: bool):
if not self.api_key:
raise ValueError("DeepSeek API key is not set")
if not self.small_llm_model:
raise ValueError("DeepSeek small LLM model is not set")
if require_large and not self.large_llm_model:
raise ValueError("DeepSeek large LLM model is not set")
class GCPConfig:
"""
Configuration class for the GCP configuration.
"""
def __init__(self):
credentials_path = os.getenv("GCP_CREDENTIALS_PATH")
if not credentials_path:
credentials_path = str(
Path(__file__).parent.parent / "gcp_credentials.json"
)
else:
credentials_path = os.path.expanduser(credentials_path)
if not os.path.isabs(credentials_path):
credentials_path = str(Path(__file__).parent.parent / credentials_path)
logger.debug("GCP credentials_path: %s", credentials_path)
self.credentials_path = credentials_path
self.project_id = os.getenv("GCP_PROJECT_ID")
self.small_llm_model = os.getenv("GCP_SMALL_LLM_MODEL")
self.large_llm_model = os.getenv("GCP_LARGE_LLM_MODEL")
self.embedding_model = os.getenv("GCP_EMBEDDING_MODEL", "text-embedding-005")
def validate_llm(self, require_large: bool):
if not os.path.exists(self.credentials_path):
raise FileNotFoundError(
f"Credentials file not found at: {self.credentials_path}. "
"Please set GCP_CREDENTIALS_PATH environment variable or place gcp_credentials.json in the project root."
)
required = [self.credentials_path, self.project_id, self.small_llm_model]
if require_large:
required.append(self.large_llm_model)
if required.count(None) > 0:
raise ValueError("GCP LLM configuration is not complete")
def validate_embeddings(self):
if not os.path.exists(self.credentials_path):
raise FileNotFoundError(
f"Credentials file not found at: {self.credentials_path}. "
"Please set GCP_CREDENTIALS_PATH environment variable or place gcp_credentials.json in the project root."
)
if [self.credentials_path, self.project_id, self.embedding_model].count(None) > 0:
raise ValueError("GCP embeddings configuration is not complete")
class BedrockConfig:
def __init__(self):
self.region = os.getenv("BEDROCK_REGION")
self.access_key_id = os.getenv("BEDROCK_ACCESS_KEY_ID")
self.secret_access_key = os.getenv("BEDROCK_SECRET_ACCESS_KEY")
self.session_token = os.getenv("BEDROCK_SESSION_TOKEN")
self.small_llm_model = os.getenv("BEDROCK_SMALL_LLM_MODEL")
self.large_llm_model = os.getenv("BEDROCK_LARGE_LLM_MODEL")
self.embedding_model = os.getenv("BEDROCK_EMBEDDING_MODEL")
def _validate_auth(self):
if [self.region, self.access_key_id, self.secret_access_key].count(None) > 0:
raise ValueError("Bedrock authentication is not complete")
def validate_llm(self, require_large: bool):
self._validate_auth()
required = [self.small_llm_model]
if require_large:
required.append(self.large_llm_model)
if required.count(None) > 0:
raise ValueError("Bedrock LLM configuration is not complete")
def validate_embeddings(self):
self._validate_auth()
if [self.embedding_model].count(None) > 0:
raise ValueError("Bedrock embeddings configuration is not complete")
class RedisConfig:
"""
Configuration class for the Redis configuration.
"""
def __init__(self):
self.host = os.getenv("REDIS_HOST", "localhost")
port_str = os.getenv("REDIS_PORT", "6379")
self.port = int(port_str) if port_str else 6379
self.password = os.getenv("REDIS_PASSWORD") or None
class Neo4jConfig:
"""
Configuration class for the Neo4j configuration.
"""
def __init__(self):
self.host = os.getenv("NEO4J_HOST", "localhost")
self.port = os.getenv("NEO4J_PORT", "7687")
self.username = os.getenv("NEO4J_USERNAME", "neo4j")
self.password = os.getenv("NEO4J_PASSWORD", "password")
class PostgreSQLConfig:
"""
Configuration class for the PostgreSQL graph / data / vector stores.
BrainAPI uses one Postgres database per brain. Two cluster-level names are
therefore needed beyond the connection credentials:
* ``system_database`` — the registry database (mirrors Mongo's
``MONGO_SYSTEM_DATABASE``). It stores the ``data_brains`` table that
catalogs every brain alongside its PAT and metadata.
* ``maintenance_database`` — the database used solely for
``CREATE DATABASE`` statements when a new brain is provisioned. Almost
every cluster ships with ``postgres`` available for this role.
``POSTGRES_DATABASE`` is retained as the source of truth for the system
database to preserve backwards compatibility with existing deployments.
``POSTGRES_SYSTEM_DATABASE`` takes precedence when set explicitly.
"""
def __init__(self):
self.host = os.getenv("POSTGRES_HOST")
port_str = os.getenv("POSTGRES_PORT")
self.port = int(port_str) if port_str else None
self.username = os.getenv("POSTGRES_USERNAME")
self.password = os.getenv("POSTGRES_PASSWORD")
system_db = os.getenv("POSTGRES_SYSTEM_DATABASE") or os.getenv(
"POSTGRES_DATABASE"
)
self.system_database = system_db
self.database = system_db
self.maintenance_database = os.getenv(
"POSTGRES_MAINTENANCE_DATABASE", "postgres"
)
def validate_credentials(self) -> None:
"""Validate only what is required to open a Postgres connection."""
if [self.host, self.port, self.username, self.password].count(None) > 0:
raise ValueError("PostgreSQL connection configuration is not complete")
def validate(self) -> None:
self.validate_credentials()
if not self.system_database:
raise ValueError(
"PostgreSQL system database is not configured "
"(set POSTGRES_DATABASE or POSTGRES_SYSTEM_DATABASE)"
)
class MilvusConfig:
"""
Configuration class for the Milvus configuration.
"""
def __init__(self):
self.host = os.getenv("MILVUS_HOST", "localhost")
port_str = os.getenv("MILVUS_PORT", "19530")
self.port = int(port_str) if port_str else 19530
self.uri = os.getenv("MILVUS_URI")
self.token = os.getenv("MILVUS_TOKEN")
class EmbeddingsConfig:
"""
Configuration class for the Embeddings configuration.
"""
def __init__(self, mode: str, *, provider: str = "", model: str = ""):
self.local_model = os.getenv("EMBEDDINGS_LOCAL_MODEL", "BAAI/bge-small-en-v1.5")
self.small_model = os.getenv("EMBEDDINGS_SMALL_MODEL", "BAAI/bge-small-en-v1.5")
gemma = provider == "ollama" and model == "embeddinggemma-2:270m"
self.profile = os.getenv("EMBEDDING_PROFILE", "embeddinggemma2-text-v1" if gemma else "")
default_dimension = 512 if gemma else 384
self.embedding_nodes_dimension = (
int(os.getenv("EMBEDDING_NODES_DIMENSION"))
if os.getenv("EMBEDDING_NODES_DIMENSION")
else default_dimension
)
self.embedding_triplets_dimension = (
int(os.getenv("EMBEDDING_TRIPLETS_DIMENSION"))
if os.getenv("EMBEDDING_TRIPLETS_DIMENSION")
else default_dimension
)
self.embedding_observations_dimension = (
int(os.getenv("EMBEDDING_OBSERVATIONS_DIMENSION"))
if os.getenv("EMBEDDING_OBSERVATIONS_DIMENSION")
else default_dimension
)
self.embedding_data_dimension = (
int(os.getenv("EMBEDDING_DATA_DIMENSION"))
if os.getenv("EMBEDDING_DATA_DIMENSION")
else default_dimension
)
self.embedding_relationships_dimension = (
int(os.getenv("EMBEDDING_RELATIONSHIPS_DIMENSION"))
if os.getenv("EMBEDDING_RELATIONSHIPS_DIMENSION")
else default_dimension
)
self.native_dimension = int(os.getenv("EMBEDDING_NATIVE_DIMENSION", "768" if gemma else str(self.embedding_data_dimension)))
self.mrl_dimension = int(os.getenv("EMBEDDING_MRL_DIMENSION", "512" if gemma else "0"))
dimensions = {
self.embedding_nodes_dimension, self.embedding_triplets_dimension,
self.embedding_observations_dimension, self.embedding_data_dimension,
self.embedding_relationships_dimension,
}
if min(dimensions) < 1 or self.native_dimension < 1 or self.mrl_dimension < 0:
raise ValueError("Embedding dimensions must be positive (MRL may be zero to disable it)")
if gemma or self.profile == "embeddinggemma2-text-v1":
if not (gemma and self.profile == "embeddinggemma2-text-v1" and self.native_dimension == 768
and self.mrl_dimension == 512 and dimensions == {512}):
raise ValueError("embeddinggemma2-text-v1 requires ollama embeddinggemma-2:270m, native 768, MRL 512 and all stored dimensions 512; use a new index when changing profiles")
elif self.mrl_dimension:
raise ValueError("MRL is only supported by the qualified embeddinggemma2-text-v1 profile")
class MongoConfig:
"""
Configuration class for the Mongo configuration.
"""
def __init__(self):
self.host = os.getenv("MONGO_HOST", "localhost")
self.port = int(os.getenv("MONGO_PORT")) if os.getenv("MONGO_PORT") else 27017
self.username = os.getenv("MONGO_USERNAME", "admin")
self.password = os.getenv("MONGO_PASSWORD", "password")
self.connection_string = os.getenv("MONGO_CONNECTION_STRING")
self.system_database = os.getenv("MONGO_SYSTEM_DATABASE", "system")
class CeleryConfig:
"""
Configuration class for the Celery configuration.
"""
def __init__(self):
"""
Initialize CeleryConfig by loading the worker concurrency setting from the environment.
Sets `self.worker_concurrency` from the `CELERY_WORKER_CONCURRENCY` environment variable and validates its presence.
"""
self.worker_concurrency = os.getenv("CELERY_WORKER_CONCURRENCY", "2")
class OllamaConfig:
"""
Configuration class for the Ollama configuration.
"""
def __init__(self):
self.host = os.getenv("OLLAMA_HOST", "localhost")
self.port = os.getenv("OLLAMA_PORT", "11434")
self.llm_small_model = os.getenv("OLLAMA_LLM_SMALL_MODEL", "llama3.2")
self.llm_large_model = os.getenv("OLLAMA_LLM_LARGE_MODEL", "llama3.2")
self.embedding_model = os.getenv("OLLAMA_EMBEDDING_MODEL") or os.getenv(
"EMBEDDINGS_LOCAL_MODEL", "embeddinggemma-2:270m"
)
self.embedding_keep_alive = os.getenv("OLLAMA_EMBEDDING_KEEP_ALIVE", "10m")
self.num_threads = int(os.getenv("OLLAMA_NUM_THREADS", "2"))
if self.num_threads < 1:
raise ValueError("OLLAMA_NUM_THREADS must be positive")
class SpacyConfig:
"""
Configuration class for the Spacy configuration.
"""
def __init__(self):
self.keep_models_in_memory = (
os.getenv("SPACY_KEEP_MODELS_IN_MEMORY", "false") == "true"
)
SEARCH_FTS_REGCONFIGS = frozenset({"italian", "spanish", "simple"})
_MODES = ("local", "remote")
_PROVIDERS = (
"ollama",
"azure",
"openai",
"anthropic",
"deepseek",
"gcp_vertex",
"amazon_bedrock",
"fastembed",
"mock",
)
class Config:
"""
Configuration class for the application.
"""
@property
def brainpat_token(self):
"""Deprecated compatibility view of the sole administrator credential."""
return self.system_pat
def __init__(self):
"""
Initialize the application's central configuration by composing environment-backed sub-configurations and loading runtime flags.
This constructor instantiates Azure, Redis, Neo4j, Milvus, Embeddings, Mongo, GCP, and Celery configuration objects, reads the RUN_GRAPH_CONSOLIDATOR flag into `run_graph_consolidator`, and resolves the canonical administrator credential.
Raises:
ValueError: If administrator credentials conflict or are missing in production.
"""
from src.services.auth_config import resolve_system_pat
self.system_pat = resolve_system_pat()
self.models_mode = os.getenv("MODELS_MODE", "local")
if self.models_mode not in _MODES:
self.models_mode = "local"
default_small_provider = "ollama" if self.models_mode == "local" else "gcp_vertex"
default_large_provider = "ollama" if self.models_mode == "local" else "azure"
default_embeddings_provider = (
"ollama" if self.models_mode == "local" else "azure"
)
self.llm_small_provider = os.getenv("LLM_SMALL_PROVIDER", default_small_provider)
self.llm_large_provider = os.getenv("LLM_LARGE_PROVIDER", default_large_provider)
self.embeddings_provider = os.getenv(
"EMBEDDINGS_PROVIDER", default_embeddings_provider
)
for provider in (
self.llm_small_provider,
self.llm_large_provider,
self.embeddings_provider,
):
if provider not in _PROVIDERS:
raise ValueError(f"Invalid provider: {provider}")
use_azure_llm = self.llm_small_provider == "azure" or self.llm_large_provider == "azure"
use_azure_embeddings = self.embeddings_provider == "azure"
use_gcp_llm = self.llm_small_provider == "gcp_vertex" or self.llm_large_provider == "gcp_vertex"
use_gcp_large = self.llm_large_provider == "gcp_vertex"
use_gcp_embeddings = self.embeddings_provider == "gcp_vertex"
use_bedrock_llm = (
self.llm_small_provider == "amazon_bedrock"
or self.llm_large_provider == "amazon_bedrock"
)
use_bedrock_large = self.llm_large_provider == "amazon_bedrock"
use_bedrock_embeddings = self.embeddings_provider == "amazon_bedrock"
use_openai_llm = (
self.llm_small_provider == "openai" or self.llm_large_provider == "openai"
)
use_openai_large = self.llm_large_provider == "openai"
use_openai_embeddings = self.embeddings_provider == "openai"
use_anthropic_llm = (
self.llm_small_provider == "anthropic"
or self.llm_large_provider == "anthropic"
)
use_anthropic_large = self.llm_large_provider == "anthropic"
use_deepseek_llm = (
self.llm_small_provider == "deepseek"
or self.llm_large_provider == "deepseek"
)
use_deepseek_large = self.llm_large_provider == "deepseek"
use_ollama = (
self.llm_small_provider == "ollama"
or self.llm_large_provider == "ollama"
or self.embeddings_provider == "ollama"
)
self.azure = AzureConfig() if use_azure_llm or use_azure_embeddings else None
if self.azure is not None:
if use_azure_llm:
self.azure.validate_llm()
if use_azure_embeddings:
self.azure.validate_embeddings()
self.gcp = GCPConfig() if use_gcp_llm or use_gcp_embeddings else None
if self.gcp is not None:
if use_gcp_llm:
self.gcp.validate_llm(require_large=use_gcp_large)
if use_gcp_embeddings:
self.gcp.validate_embeddings()
self.bedrock = BedrockConfig() if use_bedrock_llm or use_bedrock_embeddings else None
if self.bedrock is not None:
if use_bedrock_llm:
self.bedrock.validate_llm(require_large=use_bedrock_large)
if use_bedrock_embeddings:
self.bedrock.validate_embeddings()
self.openai = OpenAIConfig() if use_openai_llm or use_openai_embeddings else None
if self.openai is not None:
if use_openai_llm:
self.openai.validate_llm(require_large=use_openai_large)
if use_openai_embeddings:
self.openai.validate_embeddings()
self.anthropic = AnthropicConfig() if use_anthropic_llm else None
if self.anthropic is not None:
self.anthropic.validate_llm(require_large=use_anthropic_large)
self.deepseek = DeepSeekConfig() if use_deepseek_llm else None
if self.deepseek is not None:
self.deepseek.validate_llm(require_large=use_deepseek_large)
self.ollama = OllamaConfig() if use_ollama else None
self.embeddings = EmbeddingsConfig(
mode=self.models_mode, provider=self.embeddings_provider,
model=self.ollama.embedding_model if self.embeddings_provider == "ollama" else "",
)
self.vector_db = os.getenv("VECTOR_DB", "milvus")
self.data_db = os.getenv("DATA_DB", "mongo")
self.graph_db = os.getenv("GRAPH_DB", "neo4j")
need_mongo = self.data_db.strip().lower() == "mongo"
need_milvus = self.vector_db.strip().lower() == "milvus"
need_neo4j = self.graph_db.strip().lower() == "neo4j"
need_postgres = (
self.data_db.strip().lower() == "postgresql"
or self.vector_db.strip().lower() == "postgresql"
or self.graph_db.strip().lower() == "networkx"
)
self.redis = RedisConfig()
self.neo4j = Neo4jConfig() if need_neo4j else None
self.postgresql = PostgreSQLConfig() if need_postgres else None
self.milvus = MilvusConfig() if need_milvus else None
self.mongo = MongoConfig() if need_mongo else None
self.celery = CeleryConfig()
self.spacy = SpacyConfig()
self.run_graph_consolidator = (
os.getenv("RUN_GRAPH_CONSOLIDATOR", "false") == "true"
)
self.run_observations = os.getenv("RUN_OBSERVATIONS", "false") == "true"
self.janitor_batch_size = int(os.getenv("JANITOR_BATCH_SIZE", "20"))
self.ingest_janitor_max_llm_calls = int(
os.getenv("INGEST_JANITOR_MAX_LLM_CALLS", "2")
)
# Accurate-mode hotpath: default to cheap/fast. Set false to restore legacy
# Architect-full-text + per-create Janitor for A/B measurement.
self.ingest_architect_per_unit = (
os.getenv("INGEST_ARCHITECT_PER_UNIT", "true") == "true"
)
self.ingest_defer_janitor = (
os.getenv("INGEST_DEFER_JANITOR", "true") == "true"
)
self.ingest_architect_mode: Literal["tooler", "schema", "batch"] = (
validate_ingest_architect_mode(
os.getenv("INGEST_ARCHITECT_MODE", "batch")
)
)
self.ingest_architect_max_schema_calls = int(
os.getenv("INGEST_ARCHITECT_MAX_SCHEMA_CALLS", "3")
)
self.ingest_architect_escalate_max_turns = int(
os.getenv("INGEST_ARCHITECT_ESCALATE_MAX_TURNS", "3")
)
self.ingest_architect_escalate = (
os.getenv("INGEST_ARCHITECT_ESCALATE", "true") == "true"
)
self.ingest_architect_dense_entity_threshold = int(
os.getenv("INGEST_ARCHITECT_DENSE_ENTITY_THRESHOLD", "12")
)
self.ingest_architect_dense_max_chars = int(
os.getenv("INGEST_ARCHITECT_DENSE_MAX_CHARS", "1200")
)
self.ingest_architect_prior_context: Literal[
"auto", "scratchpad", "raw"
] = validate_ingest_architect_prior_context(
os.getenv("INGEST_ARCHITECT_PRIOR_CONTEXT", "auto")
)
self.ingest_architect_scratchpad_token_cap = int(
os.getenv("INGEST_ARCHITECT_SCRATCHPAD_TOKEN_CAP", "500")
)
self.docparser_endpoint = os.getenv("DOCPARSER_ENDPOINT")
self.docparser_token = os.getenv("DOCPARSER_TOKEN")
self.app_host = os.getenv("APP_HOST")
self.pipeline_mode: Literal["lightweight", "accurate"] = validate_pipeline_mode(
os.getenv("PIPELINE_MODE", "accurate")
)
self.ocr_mode: Literal["docling", "docparser"] = os.getenv(
"OCR_MODE", "docling"
)
self.agentic_architecture: Literal["custom", "langchain"] = os.getenv(
"AGENTIC_ARCHITECTURE", "custom"
)
self.search_enabled = os.getenv("SEARCH_ENABLED", "false") == "true"
self.search_use_dense = os.getenv("SEARCH_USE_DENSE", "true") == "true"
self.search_use_bm25 = os.getenv("SEARCH_USE_BM25", "true") == "true"
community_raw = os.getenv("SEARCH_COMMUNITY_LABELS", "TYPE,CLASS,TOPIC")
community_labels = [
part.strip() for part in community_raw.split(",") if part.strip()
]
self.search_community_labels = community_labels or [
"TYPE",
"CLASS",
"TOPIC",
]
self.search_neighbor_fanout = int(os.getenv("SEARCH_NEIGHBOR_FANOUT", "50"))
self.search_fusion: Literal["rrf", "cc"] = validate_search_fusion(
os.getenv("SEARCH_FUSION", "rrf")
)
self.search_fusion_alpha = float(os.getenv("SEARCH_FUSION_ALPHA", "0.5"))
self.search_literal_fill = os.getenv("SEARCH_LITERAL_FILL", "false") == "true"
self.search_bm25_k1 = float(os.getenv("SEARCH_BM25_K1", "1.2"))
self.search_bm25_b = float(os.getenv("SEARCH_BM25_B", "0.75"))
self.search_fts_regconfig = os.getenv("SEARCH_FTS_REGCONFIG", "").strip().lower()
self.search_fts_brains = frozenset(
part.strip()
for part in os.getenv("SEARCH_FTS_BRAINS", "").split(",")
if part.strip()
)
self.context_passage_mode: Literal[
"hybrid", "bm25", "dense", "ilike"
] = validate_context_passage_mode(
os.getenv("CONTEXT_PASSAGE_MODE", "hybrid")
)
validate_search_config(
enabled=self.search_enabled,
use_dense=self.search_use_dense,
use_bm25=self.search_use_bm25,
data_db=self.data_db,
bm25_k1=self.search_bm25_k1,
bm25_b=self.search_bm25_b,
)
def search_fts_regconfig_for_brain(self, brain_id: str) -> str | None:
bid = (brain_id or "").strip()
if not bid.lower().startswith("searchbench"):
return None
if bid not in self.search_fts_brains:
return None
if self.search_fts_regconfig not in SEARCH_FTS_REGCONFIGS:
return None
return self.search_fts_regconfig
config = Config()