Spaces:
Sleeping
Sleeping
| """Centralized configuration — single source of truth for all NeuralCAD settings. | |
| All config sections use typed Pydantic models for attribute-style access. | |
| """ | |
| from __future__ import annotations | |
| import os | |
| from pathlib import Path | |
| from typing import Any | |
| import yaml | |
| from pydantic import BaseModel, Field | |
| from pydantic_settings import BaseSettings | |
| # ── Typed sub-models ────────────────────────────────────────────────────── | |
| class ServerConfig(BaseModel): | |
| web_port: int = 5000 | |
| mcp_port: int = 8000 | |
| mcp_name: str = "text-to-cnc" | |
| cors_origins: list[str] = Field(default_factory=lambda: ["*"]) | |
| mcp_startup_wait_seconds: int = 2 | |
| class PathsConfig(BaseModel): | |
| output_dir: str = "./output" | |
| web_dir: str = "./web" | |
| prompts_dir: str = "./agents/prompts" | |
| class BackendsConfig(BaseModel): | |
| default: str = "gemini" | |
| models: dict[str, str] = Field(default_factory=dict) | |
| crewai_models: dict[str, str] = Field(default_factory=dict) | |
| model_options: dict[str, list[str]] = Field(default_factory=dict) | |
| max_tokens: int = 8192 | |
| temperature: float = 0.2 | |
| class OrchestrationConfig(BaseModel): | |
| max_history: int = 30 | |
| max_active_agents: int = 3 | |
| max_retries: int = 2 | |
| max_decisions: int = 20 | |
| max_recent_decisions: int = 5 | |
| part_name_max_chars: int = 40 | |
| class ComplexityThresholds(BaseModel): | |
| five_axis_faces: int = 100 | |
| three_plus_two_faces: int = 50 | |
| class ValidationConfig(BaseModel): | |
| min_wall_thickness_mm: float = 1.5 | |
| min_fillet_radius_mm: float = 1.0 | |
| max_pocket_depth_ratio: float = 4.0 | |
| max_part_size_mm: float = 500.0 | |
| min_part_size_mm: float = 1.0 | |
| min_hole_diameter_mm: float = 1.0 | |
| complexity_thresholds: ComplexityThresholds = Field(default_factory=ComplexityThresholds) | |
| class ExportConfig(BaseModel): | |
| stl_tolerance: float = 0.01 | |
| class ToolConfig(BaseModel): | |
| diameter: float = 6 | |
| h_feed: float = 800 | |
| v_feed: float = 200 | |
| speed: float = 18000 | |
| class CAMConfig(BaseModel): | |
| default_post_processor: str = "grbl" | |
| stock_offset_mm: float = 2.0 | |
| default_step_over_percent: float = 40 | |
| tools: dict[str, ToolConfig] = Field(default_factory=dict) | |
| post_processors: list[str] = Field(default_factory=lambda: ["grbl"]) | |
| class AgentConfig(BaseModel): | |
| name: str = "" | |
| role: str = "" | |
| color: str = "#888888" | |
| avatar: str = "" | |
| goal: str = "" | |
| backstory: str = "" | |
| class RoutingConfig(BaseModel): | |
| cad_trigger_keywords: list[str] = Field(default_factory=list) | |
| keywords: dict[str, list[str]] = Field(default_factory=dict) | |
| class PlanningConfig(BaseModel): | |
| threshold: float = 8.0 | |
| weights: dict[str, float] = Field(default_factory=lambda: { | |
| "material": 3, "dimension": 1, "feature": 1, | |
| "constraint": 1, "part_name": 1, "description": 1, | |
| "axis_recommendation": 2, | |
| }) | |
| caps: dict[str, int] = Field(default_factory=lambda: { | |
| "dimension": 4, "feature": 4, "constraint": 2, | |
| }) | |
| trigger_keywords: list[str] = Field(default_factory=lambda: [ | |
| "plan", "review", "ready", "show plan", "summarize", "what do we have", | |
| ]) | |
| approved_agents: list[str] = Field(default_factory=lambda: ["cad", "cnc"]) | |
| field_agents: dict[str, str] = Field(default_factory=lambda: { | |
| "material": "engineering", | |
| "dimensions": "engineering", | |
| "features": "design", | |
| "constraints": "cnc", | |
| "axis_recommendation": "cnc", | |
| "machining_notes": "cnc", | |
| "part_name": "design", | |
| }) | |
| class GapAnalysisConfig(BaseModel): | |
| model: str = "gemini/gemini-2.5-flash" | |
| temperature: float = 0.1 | |
| max_tokens: int = 2048 | |
| class MemoryConfig(BaseModel): | |
| enabled: bool = True | |
| embedder_provider: str = "google-generativeai" | |
| embedder_model: str = "gemini-embedding-001" | |
| recency_weight: float = 0.4 | |
| semantic_weight: float = 0.4 | |
| importance_weight: float = 0.2 | |
| recency_half_life_days: float = 1.0 | |
| recall_limit: int = 5 | |
| recall_depth: str = "shallow" | |
| class CrewConfig(BaseModel): | |
| planning: bool = True | |
| collaboration: bool = True | |
| # ── Main Settings ───────────────────────────────────────────────────────── | |
| class Settings(BaseSettings): | |
| """Loads .env for secrets, then overlays config.yaml for app config.""" | |
| # .env secrets | |
| anthropic_api_key: str = "" | |
| openai_api_key: str = "" | |
| google_api_key: str = "" | |
| # Overridable via env vars | |
| neuralcad_output_dir: str = "" | |
| neuralcad_web_port: int = 0 | |
| neuralcad_mcp_port: int = 0 | |
| # Typed config sections | |
| server: ServerConfig = Field(default_factory=ServerConfig) | |
| paths: PathsConfig = Field(default_factory=PathsConfig) | |
| backends: BackendsConfig = Field(default_factory=BackendsConfig) | |
| orchestration: OrchestrationConfig = Field(default_factory=OrchestrationConfig) | |
| validation: ValidationConfig = Field(default_factory=ValidationConfig) | |
| export: ExportConfig = Field(default_factory=ExportConfig) | |
| cam: CAMConfig = Field(default_factory=CAMConfig) | |
| planning: PlanningConfig = Field(default_factory=PlanningConfig) | |
| gap_analysis: GapAnalysisConfig = Field(default_factory=GapAnalysisConfig) | |
| agents: dict[str, AgentConfig] = Field(default_factory=dict) | |
| routing: RoutingConfig = Field(default_factory=RoutingConfig) | |
| memory: MemoryConfig = Field(default_factory=MemoryConfig) | |
| crew: CrewConfig = Field(default_factory=CrewConfig) | |
| # Simple config (no sub-model needed) | |
| materials: list[str] = Field(default_factory=list) | |
| material_grades: dict[str, str] = Field(default_factory=dict) | |
| dimension_contexts: dict[str, str] = Field(default_factory=dict) | |
| fasteners: dict[str, float] = Field(default_factory=dict) | |
| fallback_messages: dict[str, str] = Field(default_factory=dict) | |
| model_config = {"env_file": ".env", "extra": "ignore"} | |
| def settings_customise_sources( | |
| cls, | |
| settings_cls: type[BaseSettings], | |
| init_settings: Any, | |
| env_settings: Any, | |
| dotenv_settings: Any, | |
| file_secret_settings: Any, | |
| ) -> tuple[Any, ...]: | |
| """Skip raw env source to avoid collisions with system env vars. | |
| Platforms like HuggingFace Spaces inject env vars (e.g. MEMORY) | |
| that clash with our complex config field names. We keep dotenv | |
| (.env file) and read the few env vars we need manually in | |
| model_post_init. | |
| """ | |
| return (init_settings, dotenv_settings, file_secret_settings) | |
| def model_post_init(self, __context: Any) -> None: | |
| # Read API keys from real env vars (Docker / HF Spaces secrets) | |
| for key in ("anthropic_api_key", "openai_api_key", "google_api_key"): | |
| if not getattr(self, key): | |
| val = os.environ.get(key.upper(), "") | |
| if val: | |
| object.__setattr__(self, key, val) | |
| # Read NEURALCAD_* overrides from env | |
| for attr, env_key, conv in ( | |
| ("neuralcad_output_dir", "NEURALCAD_OUTPUT_DIR", str), | |
| ("neuralcad_web_port", "NEURALCAD_WEB_PORT", int), | |
| ("neuralcad_mcp_port", "NEURALCAD_MCP_PORT", int), | |
| ): | |
| val = os.environ.get(env_key, "") | |
| if val and not getattr(self, attr): | |
| object.__setattr__(self, attr, conv(val)) | |
| config_path = Path(__file__).parent.parent / "config.yaml" | |
| if config_path.exists(): | |
| with open(config_path) as f: | |
| data = yaml.safe_load(f) or {} | |
| for key, value in data.items(): | |
| if hasattr(self, key) and value is not None: | |
| current = getattr(self, key) | |
| if isinstance(current, BaseModel): | |
| # Merge dict into typed sub-model | |
| if isinstance(value, dict): | |
| object.__setattr__(self, key, type(current)(**value)) | |
| elif isinstance(current, dict) and isinstance(value, dict): | |
| # For agents: convert each entry to AgentConfig | |
| if key == "agents": | |
| object.__setattr__(self, key, { | |
| k: AgentConfig(**v) if isinstance(v, dict) else v | |
| for k, v in value.items() | |
| }) | |
| elif not current: | |
| object.__setattr__(self, key, value) | |
| elif isinstance(current, list) and not current: | |
| object.__setattr__(self, key, value) | |
| elif not current: | |
| object.__setattr__(self, key, value) | |
| # ── Convenience properties ──────────────────────────────────────────── | |
| def output_dir(self) -> Path: | |
| if self.neuralcad_output_dir: | |
| return Path(self.neuralcad_output_dir) | |
| return Path(self.paths.output_dir) | |
| def web_port(self) -> int: | |
| if self.neuralcad_web_port: | |
| return self.neuralcad_web_port | |
| return self.server.web_port | |
| def mcp_port(self) -> int: | |
| if self.neuralcad_mcp_port: | |
| return self.neuralcad_mcp_port | |
| return self.server.mcp_port | |
| def default_backend(self) -> str: | |
| return self.backends.default | |
| def model_for(self) -> dict[str, str]: | |
| return self.backends.models | |
| def max_tokens(self) -> int: | |
| return self.backends.max_tokens | |
| def temperature(self) -> float: | |
| return self.backends.temperature | |
| settings = Settings() | |