Files
2026-05-12 19:40:31 +09:00

411 lines
13 KiB
Python

"""Configuration management for OntoCast.
This module provides hierarchical configuration classes that map to the
environment variables and usage patterns in the OntoCast system.
"""
from enum import StrEnum
from pathlib import Path
from typing import Literal
from pydantic import AliasChoices, Field, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
from ontocast.onto.constants import DEFAULT_DATASET, DEFAULT_ONTOLOGIES_DATASET
from ontocast.onto.enum import RenderMode
class LLMProvider(StrEnum):
"""Supported LLM providers."""
OPENAI = "openai"
OLLAMA = "ollama"
class LLMModelNameAbstract(StrEnum):
"""Abstract base class for all model names."""
class OpenAIModel(LLMModelNameAbstract):
"""OpenAI model names."""
GPT4_O = "gpt-4o"
GPT4_O_MINI = "gpt-4o-mini"
GPT4_1 = "gpt-41"
GPT4_1_MINI = "gpt-41-mini"
GPT5 = "gpt-5"
GPT5_MINI = "gpt-5-mini"
GPT5_NANO = "gpt-5-nano"
class OllamaModel(LLMModelNameAbstract):
"""Ollama model names."""
QWEN2_5 = "qwen2.5"
QWEN2_5_72B = "qwen2.5:72b"
LLAMA3_1 = "llama3.1"
LLAMA3_1_70B = "llama3.1:70b"
GRANITE3_3_2B = "granite3.3:2b"
GRANITE3_3_8B = "granite3.3:8b"
LLMModelName = OpenAIModel | OllamaModel
class WebSearchProvider(StrEnum):
"""Supported web-search providers."""
DUCKDUCKGO = "duckduckgo"
class LLMConfig(BaseSettings):
"""LLM configuration settings."""
provider: LLMProvider = Field(
default=LLMProvider.OPENAI, description="LLM provider"
)
model_name: LLMModelName = Field(
default=OpenAIModel.GPT4_O_MINI, description="LLM model name"
)
temperature: float = Field(default=0.0, description="LLM temperature setting")
base_url: str | None = Field(
default=None, description="LLM base URL (for ollama, etc.)"
)
api_key: str | None = Field(default=None, description="API key for LLM provider")
model_config = SettingsConfigDict(
env_prefix="LLM_",
case_sensitive=False,
)
@field_validator("model_name")
@classmethod
def validate_model_name(cls, v: LLMModelName, info) -> LLMModelName:
"""Validate that model_name is compatible with the provider."""
if "provider" not in info.data:
return v
provider = info.data["provider"]
if provider == LLMProvider.OPENAI and not isinstance(v, OpenAIModel):
raise ValueError(
f"Model {v} is not compatible with OpenAI provider. Use OpenAIModel values."
)
if provider == LLMProvider.OLLAMA and not isinstance(v, OllamaModel):
raise ValueError(
f"Model {v} is not compatible with Ollama provider. Use OllamaModel values."
)
return v
class ChunkConfig(BaseSettings):
"""Chunking configuration settings."""
breakpoint_threshold_type: Literal[
"percentile", "standard_deviation", "interquartile", "gradient"
] = Field(
default="percentile", description="Type of threshold calculation for chunking"
)
breakpoint_threshold_amount: float = Field(
default=95.0, description="Threshold amount for breakpoint detection"
)
min_size: int = Field(default=3000, description="Minimum chunk size in characters")
max_size: int = Field(default=12000, description="Maximum chunk size in characters")
model_config = SettingsConfigDict(
env_prefix="CHUNK_",
case_sensitive=False,
)
class ServerConfig(BaseSettings):
"""Server configuration settings."""
port: int = Field(default=8999, description="Server port")
base_recursion_limit: int = Field(
default=1000, description="Recursion limit for workflow"
)
estimated_chunks: int = Field(default=30, description="Estimated number of chunks")
max_visits_per_node: int = Field(
default=1,
ge=1,
description="Maximum number of visits allowed per node",
validation_alias=AliasChoices("max_visits_per_node", "max_visits"),
)
render_mode: RenderMode = Field(
default=RenderMode.ONTOLOGY_AND_FACTS,
description="Rendering mode: ontology, facts, or ontology_and_facts.",
)
ontology_max_triples: int | None = Field(
default=50000,
description="Maximum number of triples allowed in ontology graph. "
"Updates that would exceed this limit are skipped with a warning. "
"Set to None for unlimited.",
)
parallel_workers: int = Field(
default=4,
description="Maximum number of concurrent unit workers in parallel pipeline",
)
parallel_facts_retries: int = Field(
default=3,
description="Retry budget for unit facts loop",
)
parallel_ontology_retries: int = Field(
default=3,
description="Retry budget for unit ontology loop",
)
enable_ontology_consolidation: bool = Field(
default=False,
description="Run optional ontology consolidation pass after normalization",
)
model_config = SettingsConfigDict(
case_sensitive=False,
)
class Neo4jConfig(BaseSettings):
"""Neo4j triple store configuration."""
uri: str | None = Field(default=None, description="Neo4j URI")
auth: str | None = Field(default=None, description="Neo4j authentication")
port: int = Field(default=7476, description="Neo4j HTTP port")
bolt_port: int = Field(default=7689, description="Neo4j Bolt port")
model_config = SettingsConfigDict(
env_prefix="NEO4J_",
case_sensitive=False,
)
class FusekiConfig(BaseSettings):
"""Fuseki triple store configuration."""
uri: str | None = Field(default=None, description="Fuseki URI")
auth: str | None = Field(default=None, description="Fuseki authentication")
dataset: str = Field(default=DEFAULT_DATASET, description="Fuseki dataset name")
ontologies_dataset: str = Field(
default=DEFAULT_ONTOLOGIES_DATASET,
description="Fuseki dataset name for ontologies",
)
model_config = SettingsConfigDict(
env_prefix="FUSEKI_",
case_sensitive=False,
)
class DomainConfig(BaseSettings):
"""Domain and URI configuration."""
current_domain: str = Field(
default="https://example.com", description="Current domain for URI generation"
)
model_config = SettingsConfigDict(
case_sensitive=False,
)
class PathConfig(BaseSettings):
"""Path and directory configuration."""
working_directory: Path | None = Field(
default=None,
description="Working directory for OntoCast (required if filesystem_manager is enabled)",
)
ontology_directory: Path | None = Field(
default=None, description="Directory containing ontology files"
)
cache_dir: Path | None = Field(
default=None, description="Cache directory for LLM responses and tool outputs"
)
model_config = SettingsConfigDict(
env_prefix="ONTOCAST_",
case_sensitive=False,
)
class WebSearchConfig(BaseSettings):
"""Optional web-search settings for ontology grounding."""
enabled: bool = Field(
default=False,
description=(
"Enable optional web grounding. Node execution still starts without "
"search and only searches when node output requests it."
),
)
provider: WebSearchProvider = Field(
default=WebSearchProvider.DUCKDUCKGO, description="Web-search provider"
)
top_k: int = Field(default=3, ge=1, le=10, description="Number of results to fetch")
timeout_seconds: float = Field(
default=8.0, ge=1.0, le=60.0, description="Search request timeout"
)
max_snippet_chars: int = Field(
default=400, ge=80, le=2000, description="Snippet truncation limit per hit"
)
max_total_chars: int = Field(
default=1800, ge=200, le=10000, description="Total evidence text budget"
)
ontology_render_enabled: bool = Field(
default=True,
description=(
"Allow search-eligible retries for ontology render prompts "
"(first pass remains no-search)."
),
)
ontology_critic_enabled: bool = Field(
default=True,
description=(
"Allow search-eligible retries for ontology critic prompts "
"(first pass remains no-search)."
),
)
facts_render_enabled: bool = Field(
default=False,
description=(
"Allow search-eligible retries for facts render prompts "
"(first pass remains no-search)."
),
)
facts_critic_enabled: bool = Field(
default=False,
description=(
"Allow search-eligible retries for facts critic prompts "
"(first pass remains no-search)."
),
)
planner_enabled: bool = Field(
default=True, description="Enable LLM planner for web-search decisions"
)
planner_max_queries: int = Field(
default=3, ge=1, le=8, description="Maximum focused search queries per node"
)
planner_min_query_chars: int = Field(
default=12,
ge=3,
le=100,
description="Minimum query length accepted by guardrails",
)
planner_min_confidence: float = Field(
default=0.35,
ge=0.0,
le=1.0,
description="Minimum planner confidence to run search",
)
reuse_evidence_across_attempt: bool = Field(
default=True,
description=("Reuse node-scoped evidence between retries for the same unit."),
)
min_snippet_chars: int = Field(
default=40,
ge=0,
le=1000,
description="Minimum snippet length to keep a search hit",
)
allowed_domains: list[str] = Field(
default_factory=list,
description="Optional allowlist of source domains for evidence",
)
blocked_domains: list[str] = Field(
default_factory=list,
description="Optional blocklist of source domains for evidence",
)
region: str = Field(default="wt-wt", description="DuckDuckGo region code")
safesearch: str = Field(
default="moderate", description="DuckDuckGo safesearch mode"
)
@field_validator("allowed_domains", "blocked_domains", mode="before")
@classmethod
def parse_domains(cls, value: str | list[str]) -> list[str]:
if isinstance(value, list):
return [entry.strip().lower() for entry in value if entry.strip()]
if isinstance(value, str):
raw_values = [entry.strip().lower() for entry in value.split(",")]
return [entry for entry in raw_values if entry]
return []
model_config = SettingsConfigDict(
env_prefix="WEB_SEARCH_",
case_sensitive=False,
)
class AggregationConfig(BaseSettings):
"""Aggregation settings for entity clustering/disambiguation."""
embedding_model: str = Field(
default="paraphrase-multilingual-MiniLM-L12-v2",
description="Sentence-transformers model name used for entity embeddings.",
)
similarity_threshold: float = Field(
default=0.80,
ge=0.0,
le=1.0,
description="Cosine similarity threshold used by DBSCAN clustering.",
)
model_config = SettingsConfigDict(
env_prefix="AGG_",
case_sensitive=False,
)
class ToolConfig(BaseSettings):
"""Configuration for tools (LLM, triple stores, paths, chunking)."""
llm_config: LLMConfig = Field(default_factory=LLMConfig)
chunk_config: ChunkConfig = Field(default_factory=ChunkConfig)
path_config: PathConfig = Field(default_factory=PathConfig)
neo4j: Neo4jConfig = Field(default_factory=Neo4jConfig)
fuseki: FusekiConfig = Field(default_factory=FusekiConfig)
domain: DomainConfig = Field(default_factory=DomainConfig)
web_search: WebSearchConfig = Field(default_factory=WebSearchConfig)
aggregation: AggregationConfig = Field(default_factory=AggregationConfig)
class Config(BaseSettings):
"""Main OntoCast configuration.
This class aggregates all configuration sections and provides
a unified interface for accessing configuration values.
"""
# Tool configuration (for ToolBox)
tool_config: ToolConfig = Field(default_factory=ToolConfig)
# Server configuration (for serve.py)
server: ServerConfig = Field(default_factory=ServerConfig)
# Additional settings
logging_level: str | None = Field(default=None, description="Logging level")
model_config = SettingsConfigDict(
case_sensitive=False,
extra="ignore",
)
def get_tool_config(self) -> ToolConfig:
"""Get tool configuration.
Returns:
ToolConfig: Configuration for tools
"""
return self.tool_config
def validate_llm_config(self) -> None:
"""Validate LLM configuration and raise errors for missing required settings."""
if (
self.tool_config.llm_config.provider == LLMProvider.OPENAI
and not self.tool_config.llm_config.api_key
):
raise ValueError(
"LLM_API_KEY environment variable is required for OpenAI provider"
)