tiphys.config
Tiphys configuration using Pydantic Settings.
Supports loading from:
- Environment variables (TIPHYS_ prefix)
- .env file
- config.json file
1""" 2Tiphys configuration using Pydantic Settings. 3 4Supports loading from: 5- Environment variables (TIPHYS_ prefix) 6- .env file 7- config.json file 8""" 9 10from enum import Enum 11from pathlib import Path 12from typing import Any, Literal 13 14from pydantic import BaseModel, Field 15from pydantic_settings import BaseSettings, SettingsConfigDict 16 17 18class LLMProvider(str, Enum): 19 """Supported LLM providers.""" 20 21 OPENROUTER = "openrouter" # OpenRouter (multi-model gateway) 22 OPENAI = "openai" # OpenAI direct 23 ANTHROPIC = "anthropic" # Anthropic direct 24 OLLAMA = "ollama" # Local Ollama 25 CUSTOM = "custom" # Custom API endpoint 26 27 28class DmScope(str, Enum): 29 """DM session scoping modes.""" 30 31 MAIN = "main" # All DMs share context 32 PER_PEER = "per_peer" # Each user gets own context 33 PER_CHANNEL_PEER = "per_channel_peer" 34 FULL = "full" # Maximum granularity 35 36 37class LogLevel(str, Enum): 38 """Logging levels.""" 39 40 DEBUG = "DEBUG" 41 INFO = "INFO" 42 WARNING = "WARNING" 43 ERROR = "ERROR" 44 45 46class LLMSettings(BaseModel): 47 """LLM provider configuration.""" 48 49 provider: LLMProvider = Field( 50 default=LLMProvider.OPENROUTER, 51 description="LLM provider to use", 52 ) 53 model: str = Field( 54 default="anthropic/claude-3.5-sonnet", 55 description="Model identifier (provider-specific format)", 56 ) 57 api_key: str | None = Field( 58 default=None, 59 description="API key for the provider (or use env vars)", 60 ) 61 api_base: str | None = Field( 62 default=None, 63 description="Custom API base URL (auto-set for known providers)", 64 ) 65 temperature: float = Field( 66 default=0.7, 67 ge=0.0, 68 le=2.0, 69 description="Default sampling temperature", 70 ) 71 max_tokens: int | None = Field( 72 default=None, 73 description="Default max tokens to generate", 74 ) 75 76 # OpenRouter-specific settings 77 openrouter_site_url: str | None = Field( 78 default=None, 79 description="Your site URL for OpenRouter rankings", 80 ) 81 openrouter_app_name: str | None = Field( 82 default="Tiphys", 83 description="Your app name for OpenRouter rankings", 84 ) 85 86 def get_api_base(self) -> str | None: 87 """Get the API base URL for the configured provider.""" 88 if self.api_base: 89 return self.api_base 90 91 match self.provider: 92 case LLMProvider.OPENROUTER: 93 return "https://openrouter.ai/api/v1" 94 case LLMProvider.OLLAMA: 95 return "http://localhost:11434" 96 case _: 97 return None 98 99 def get_model_id(self) -> str: 100 """Get the full model ID for LiteLLM.""" 101 # For OpenRouter, LiteLLM expects "openrouter/<model>" 102 if self.provider == LLMProvider.OPENROUTER: 103 if not self.model.startswith("openrouter/"): 104 return f"openrouter/{self.model}" 105 elif self.provider == LLMProvider.OLLAMA and not self.model.startswith("ollama/"): 106 return f"ollama/{self.model}" 107 return self.model 108 109 def get_extra_headers(self) -> dict[str, str]: 110 """Get extra headers for API requests.""" 111 headers: dict[str, str] = {} 112 113 if self.provider == LLMProvider.OPENROUTER: 114 if self.openrouter_site_url: 115 headers["HTTP-Referer"] = self.openrouter_site_url 116 if self.openrouter_app_name: 117 headers["X-Title"] = self.openrouter_app_name 118 119 return headers 120 121 122class SandboxSettings(BaseModel): 123 """Sandbox configuration for secure execution.""" 124 125 enabled: bool = Field(default=False, description="Enable sandboxed execution") 126 image: str = Field(default="python:3.12-slim", description="Docker image") 127 memory_limit: str = Field(default="512m", description="Memory limit") 128 cpu_limit: float = Field(default=1.0, ge=0.1, le=16.0, description="CPU limit") 129 timeout_seconds: int = Field(default=300, ge=10, le=3600, description="Timeout") 130 network_enabled: bool = Field(default=False, description="Allow network access") 131 sandboxed_tools: list[str] = Field( 132 default_factory=lambda: ["run_command"], 133 description="Tools to run in sandbox", 134 ) 135 136 137class BudgetConfig(BaseModel): 138 """Castor budget configuration for resource tracking.""" 139 140 enabled: bool = Field(default=True, description="Enable Castor budget tracking") 141 default_budgets: dict[str, float] = Field( 142 default_factory=lambda: {"api": 100.0, "compute": 50.0}, 143 description="Default resource budgets per session", 144 ) 145 per_channel_overrides: dict[str, dict[str, float]] = Field( 146 default_factory=dict, 147 description="Per-channel budget overrides", 148 ) 149 150 151class ChannelConfig(BaseModel): 152 """ 153 Per-channel configuration with rate limiting, security, and message settings. 154 155 Channels can override global settings with their own configuration. 156 Use `get_channel_config()` on TiphysConfig to get merged configuration. 157 158 Example: 159 config = load_config() 160 webchat_config = config.get_channel_config("webchat") 161 rate_limit = webchat_config.rate_limit_requests_per_minute 162 """ 163 164 enabled: bool = Field(default=True, description="Whether channel is enabled") 165 166 # Rate limiting 167 rate_limit_requests_per_minute: int = Field( 168 default=20, 169 ge=1, 170 le=1000, 171 description="Maximum requests per minute from a single peer", 172 ) 173 rate_limit_burst: int = Field( 174 default=5, 175 ge=1, 176 le=100, 177 description="Maximum burst size for rate limiting", 178 ) 179 180 # Message limits 181 max_message_length: int = Field( 182 default=10000, 183 ge=1, 184 le=100000, 185 description="Maximum inbound message length in characters", 186 ) 187 chunk_size: int = Field( 188 default=2000, 189 ge=100, 190 le=10000, 191 description="Maximum outbound chunk size for long messages", 192 ) 193 194 # Security - Access control 195 allowed_peer_ids: list[str] | None = Field( 196 default=None, 197 description="Whitelist of allowed peer IDs (None = all allowed)", 198 ) 199 blocked_peer_ids: list[str] = Field( 200 default_factory=list, 201 description="Blacklist of blocked peer IDs", 202 ) 203 204 # Tool restrictions (override global) 205 tools_allow: list[str] | None = Field( 206 default=None, 207 description="Allowed tools for this channel (None = use global)", 208 ) 209 tools_deny: list[str] = Field( 210 default_factory=list, 211 description="Denied tools for this channel", 212 ) 213 214 # Custom settings for channel-specific behavior 215 extra: dict[str, Any] = Field( 216 default_factory=dict, 217 description="Extra channel-specific settings", 218 ) 219 220 221class AgentConfig(BaseModel): 222 """Configuration for a single agent (used in config file).""" 223 224 id: str = Field(..., description="Unique agent identifier") 225 name: str = Field(..., description="Display name") 226 description: str | None = Field(default=None, description="Agent description") 227 228 # LLM settings (override global) 229 model: str | None = Field(default=None, description="LLM model override") 230 api_base: str | None = Field(default=None, description="API base override") 231 api_key: str | None = Field(default=None, description="API key override") 232 temperature: float = Field(default=0.7, ge=0.0, le=2.0, description="LLM temperature") 233 max_tokens: int | None = Field(default=None, description="Max response tokens") 234 235 # Session settings 236 dm_scope: DmScope = Field(default=DmScope.MAIN, description="Session scope mode") 237 238 # Customization 239 system_prompt: str | None = Field(default=None, description="Custom system prompt") 240 custom_instructions: str | None = Field(default=None, description="Extra instructions") 241 242 # Tool restrictions 243 tools_allow: list[str] = Field(default_factory=list, description="Allowed tools") 244 tools_deny: list[str] = Field(default_factory=list, description="Denied tools") 245 246 # Skills 247 skills_dir: Path | None = Field(default=None, description="Agent-specific skills") 248 preload_skills: list[str] = Field(default_factory=list, description="Preloaded skills") 249 250 # Behavior 251 max_steps: int = Field(default=10, ge=1, le=100, description="Max tool steps") 252 enabled: bool = Field(default=True, description="Whether agent is active") 253 254 # Metadata 255 metadata: dict[str, Any] = Field(default_factory=dict, description="Extra metadata") 256 257 258class TiphysConfig(BaseSettings): 259 """Main configuration for Tiphys.""" 260 261 model_config = SettingsConfigDict( 262 env_prefix="TIPHYS_", 263 env_file=".env", 264 env_file_encoding="utf-8", 265 extra="ignore", 266 ) 267 268 # Server 269 host: str = Field(default="127.0.0.1", description="Server bind address") 270 port: int = Field(default=8000, ge=1, le=65535, description="Server port") 271 cors_origins: list[str] = Field( 272 default_factory=lambda: ["http://localhost:8000", "http://127.0.0.1:8000"], 273 description="Allowed CORS origins (use ['*'] for development only)", 274 ) 275 276 # LLM (new structured settings) 277 llm: LLMSettings = Field( 278 default_factory=LLMSettings, 279 description="LLM provider configuration", 280 ) 281 282 # Legacy LLM fields (for backwards compatibility) 283 model: str = Field( 284 default="anthropic/claude-3.5-sonnet", 285 description="LLM model identifier (LiteLLM format)", 286 ) 287 api_base: str | None = Field( 288 default=None, 289 description="Custom API base URL for LLM provider", 290 ) 291 api_key: str | None = Field( 292 default=None, 293 description="API key for LLM provider", 294 ) 295 296 # Agent 297 agent_id: str = Field(default="default", description="Default agent identifier") 298 agent_name: str = Field(default="Tiphys", description="Agent display name") 299 dm_scope: DmScope = Field( 300 default=DmScope.MAIN, 301 description="Session scoping mode for DMs", 302 ) 303 304 # Paths 305 data_dir: Path = Field( 306 default_factory=lambda: Path.home() / ".tiphys", 307 description="Data storage directory", 308 ) 309 plugins_dir: Path = Field( 310 default=Path("./plugins"), 311 description="Plugins directory", 312 ) 313 skills_dir: Path = Field( 314 default=Path("./skills"), 315 description="Skills directory", 316 ) 317 318 # Security 319 exec_mode: Literal["deny", "allowlist", "full"] = Field( 320 default="allowlist", 321 description="Command execution mode", 322 ) 323 allowed_commands: list[str] = Field( 324 default_factory=lambda: ["ls", "cat", "grep", "find", "git", "python", "pip", "uv"], 325 description="Allowed commands in allowlist mode", 326 ) 327 328 # Logging 329 log_level: LogLevel = Field( 330 default=LogLevel.INFO, 331 description="Logging level", 332 ) 333 log_json: bool = Field( 334 default=False, 335 description="Output logs as JSON", 336 ) 337 338 # Channels 339 telegram_token: str | None = Field( 340 default=None, 341 description="Telegram bot token", 342 ) 343 discord_token: str | None = Field( 344 default=None, 345 description="Discord bot token", 346 ) 347 348 # Sandbox 349 sandbox: SandboxSettings = Field( 350 default_factory=SandboxSettings, 351 description="Sandbox configuration for secure execution", 352 ) 353 354 # Multi-agent configuration 355 agents: list[AgentConfig] = Field( 356 default_factory=list, 357 description="Additional agent configurations", 358 ) 359 default_agent_id: str | None = Field( 360 default=None, 361 description="Default agent ID (uses agent_id if not set)", 362 ) 363 364 # Castor kernel budget configuration 365 budget: BudgetConfig = Field( 366 default_factory=BudgetConfig, 367 description="Castor budget configuration", 368 ) 369 370 # Per-channel configuration (Phase R6) 371 default_channel_config: ChannelConfig = Field( 372 default_factory=ChannelConfig, 373 description="Default configuration for all channels", 374 ) 375 channels: dict[str, ChannelConfig] = Field( 376 default_factory=dict, 377 description="Per-channel configuration overrides", 378 ) 379 380 def ensure_dirs(self) -> None: 381 """Create required directories if they don't exist.""" 382 self.data_dir.mkdir(parents=True, exist_ok=True) 383 (self.data_dir / "sessions").mkdir(exist_ok=True) 384 (self.data_dir / "logs").mkdir(exist_ok=True) 385 386 def get_channel_config(self, channel_name: str) -> ChannelConfig: 387 """ 388 Get merged configuration for a specific channel. 389 390 If the channel has specific configuration, it is merged with 391 the default configuration (channel settings override defaults). 392 393 Args: 394 channel_name: The channel name (e.g., "webchat", "telegram"). 395 396 Returns: 397 Merged ChannelConfig with channel-specific overrides applied. 398 399 Example: 400 config = load_config() 401 webchat = config.get_channel_config("webchat") 402 print(f"Rate limit: {webchat.rate_limit_requests_per_minute}/min") 403 """ 404 # If no channel-specific config, return defaults 405 if channel_name not in self.channels: 406 return self.default_channel_config 407 408 # Merge channel config with defaults 409 # Channel settings override default settings 410 defaults = self.default_channel_config.model_dump() 411 channel_overrides = self.channels[channel_name].model_dump(exclude_unset=True) 412 merged = {**defaults, **channel_overrides} 413 return ChannelConfig(**merged) 414 415 416def load_config() -> TiphysConfig: 417 """Load configuration from environment and files.""" 418 return TiphysConfig()
19class LLMProvider(str, Enum): 20 """Supported LLM providers.""" 21 22 OPENROUTER = "openrouter" # OpenRouter (multi-model gateway) 23 OPENAI = "openai" # OpenAI direct 24 ANTHROPIC = "anthropic" # Anthropic direct 25 OLLAMA = "ollama" # Local Ollama 26 CUSTOM = "custom" # Custom API endpoint
Supported LLM providers.
29class DmScope(str, Enum): 30 """DM session scoping modes.""" 31 32 MAIN = "main" # All DMs share context 33 PER_PEER = "per_peer" # Each user gets own context 34 PER_CHANNEL_PEER = "per_channel_peer" 35 FULL = "full" # Maximum granularity
DM session scoping modes.
38class LogLevel(str, Enum): 39 """Logging levels.""" 40 41 DEBUG = "DEBUG" 42 INFO = "INFO" 43 WARNING = "WARNING" 44 ERROR = "ERROR"
Logging levels.
47class LLMSettings(BaseModel): 48 """LLM provider configuration.""" 49 50 provider: LLMProvider = Field( 51 default=LLMProvider.OPENROUTER, 52 description="LLM provider to use", 53 ) 54 model: str = Field( 55 default="anthropic/claude-3.5-sonnet", 56 description="Model identifier (provider-specific format)", 57 ) 58 api_key: str | None = Field( 59 default=None, 60 description="API key for the provider (or use env vars)", 61 ) 62 api_base: str | None = Field( 63 default=None, 64 description="Custom API base URL (auto-set for known providers)", 65 ) 66 temperature: float = Field( 67 default=0.7, 68 ge=0.0, 69 le=2.0, 70 description="Default sampling temperature", 71 ) 72 max_tokens: int | None = Field( 73 default=None, 74 description="Default max tokens to generate", 75 ) 76 77 # OpenRouter-specific settings 78 openrouter_site_url: str | None = Field( 79 default=None, 80 description="Your site URL for OpenRouter rankings", 81 ) 82 openrouter_app_name: str | None = Field( 83 default="Tiphys", 84 description="Your app name for OpenRouter rankings", 85 ) 86 87 def get_api_base(self) -> str | None: 88 """Get the API base URL for the configured provider.""" 89 if self.api_base: 90 return self.api_base 91 92 match self.provider: 93 case LLMProvider.OPENROUTER: 94 return "https://openrouter.ai/api/v1" 95 case LLMProvider.OLLAMA: 96 return "http://localhost:11434" 97 case _: 98 return None 99 100 def get_model_id(self) -> str: 101 """Get the full model ID for LiteLLM.""" 102 # For OpenRouter, LiteLLM expects "openrouter/<model>" 103 if self.provider == LLMProvider.OPENROUTER: 104 if not self.model.startswith("openrouter/"): 105 return f"openrouter/{self.model}" 106 elif self.provider == LLMProvider.OLLAMA and not self.model.startswith("ollama/"): 107 return f"ollama/{self.model}" 108 return self.model 109 110 def get_extra_headers(self) -> dict[str, str]: 111 """Get extra headers for API requests.""" 112 headers: dict[str, str] = {} 113 114 if self.provider == LLMProvider.OPENROUTER: 115 if self.openrouter_site_url: 116 headers["HTTP-Referer"] = self.openrouter_site_url 117 if self.openrouter_app_name: 118 headers["X-Title"] = self.openrouter_app_name 119 120 return headers
LLM provider configuration.
87 def get_api_base(self) -> str | None: 88 """Get the API base URL for the configured provider.""" 89 if self.api_base: 90 return self.api_base 91 92 match self.provider: 93 case LLMProvider.OPENROUTER: 94 return "https://openrouter.ai/api/v1" 95 case LLMProvider.OLLAMA: 96 return "http://localhost:11434" 97 case _: 98 return None
Get the API base URL for the configured provider.
100 def get_model_id(self) -> str: 101 """Get the full model ID for LiteLLM.""" 102 # For OpenRouter, LiteLLM expects "openrouter/<model>" 103 if self.provider == LLMProvider.OPENROUTER: 104 if not self.model.startswith("openrouter/"): 105 return f"openrouter/{self.model}" 106 elif self.provider == LLMProvider.OLLAMA and not self.model.startswith("ollama/"): 107 return f"ollama/{self.model}" 108 return self.model
Get the full model ID for LiteLLM.
110 def get_extra_headers(self) -> dict[str, str]: 111 """Get extra headers for API requests.""" 112 headers: dict[str, str] = {} 113 114 if self.provider == LLMProvider.OPENROUTER: 115 if self.openrouter_site_url: 116 headers["HTTP-Referer"] = self.openrouter_site_url 117 if self.openrouter_app_name: 118 headers["X-Title"] = self.openrouter_app_name 119 120 return headers
Get extra headers for API requests.
123class SandboxSettings(BaseModel): 124 """Sandbox configuration for secure execution.""" 125 126 enabled: bool = Field(default=False, description="Enable sandboxed execution") 127 image: str = Field(default="python:3.12-slim", description="Docker image") 128 memory_limit: str = Field(default="512m", description="Memory limit") 129 cpu_limit: float = Field(default=1.0, ge=0.1, le=16.0, description="CPU limit") 130 timeout_seconds: int = Field(default=300, ge=10, le=3600, description="Timeout") 131 network_enabled: bool = Field(default=False, description="Allow network access") 132 sandboxed_tools: list[str] = Field( 133 default_factory=lambda: ["run_command"], 134 description="Tools to run in sandbox", 135 )
Sandbox configuration for secure execution.
138class BudgetConfig(BaseModel): 139 """Castor budget configuration for resource tracking.""" 140 141 enabled: bool = Field(default=True, description="Enable Castor budget tracking") 142 default_budgets: dict[str, float] = Field( 143 default_factory=lambda: {"api": 100.0, "compute": 50.0}, 144 description="Default resource budgets per session", 145 ) 146 per_channel_overrides: dict[str, dict[str, float]] = Field( 147 default_factory=dict, 148 description="Per-channel budget overrides", 149 )
Castor budget configuration for resource tracking.
152class ChannelConfig(BaseModel): 153 """ 154 Per-channel configuration with rate limiting, security, and message settings. 155 156 Channels can override global settings with their own configuration. 157 Use `get_channel_config()` on TiphysConfig to get merged configuration. 158 159 Example: 160 config = load_config() 161 webchat_config = config.get_channel_config("webchat") 162 rate_limit = webchat_config.rate_limit_requests_per_minute 163 """ 164 165 enabled: bool = Field(default=True, description="Whether channel is enabled") 166 167 # Rate limiting 168 rate_limit_requests_per_minute: int = Field( 169 default=20, 170 ge=1, 171 le=1000, 172 description="Maximum requests per minute from a single peer", 173 ) 174 rate_limit_burst: int = Field( 175 default=5, 176 ge=1, 177 le=100, 178 description="Maximum burst size for rate limiting", 179 ) 180 181 # Message limits 182 max_message_length: int = Field( 183 default=10000, 184 ge=1, 185 le=100000, 186 description="Maximum inbound message length in characters", 187 ) 188 chunk_size: int = Field( 189 default=2000, 190 ge=100, 191 le=10000, 192 description="Maximum outbound chunk size for long messages", 193 ) 194 195 # Security - Access control 196 allowed_peer_ids: list[str] | None = Field( 197 default=None, 198 description="Whitelist of allowed peer IDs (None = all allowed)", 199 ) 200 blocked_peer_ids: list[str] = Field( 201 default_factory=list, 202 description="Blacklist of blocked peer IDs", 203 ) 204 205 # Tool restrictions (override global) 206 tools_allow: list[str] | None = Field( 207 default=None, 208 description="Allowed tools for this channel (None = use global)", 209 ) 210 tools_deny: list[str] = Field( 211 default_factory=list, 212 description="Denied tools for this channel", 213 ) 214 215 # Custom settings for channel-specific behavior 216 extra: dict[str, Any] = Field( 217 default_factory=dict, 218 description="Extra channel-specific settings", 219 )
Per-channel configuration with rate limiting, security, and message settings.
Channels can override global settings with their own configuration.
Use get_channel_config() on TiphysConfig to get merged configuration.
Example: config = load_config() webchat_config = config.get_channel_config("webchat") rate_limit = webchat_config.rate_limit_requests_per_minute
222class AgentConfig(BaseModel): 223 """Configuration for a single agent (used in config file).""" 224 225 id: str = Field(..., description="Unique agent identifier") 226 name: str = Field(..., description="Display name") 227 description: str | None = Field(default=None, description="Agent description") 228 229 # LLM settings (override global) 230 model: str | None = Field(default=None, description="LLM model override") 231 api_base: str | None = Field(default=None, description="API base override") 232 api_key: str | None = Field(default=None, description="API key override") 233 temperature: float = Field(default=0.7, ge=0.0, le=2.0, description="LLM temperature") 234 max_tokens: int | None = Field(default=None, description="Max response tokens") 235 236 # Session settings 237 dm_scope: DmScope = Field(default=DmScope.MAIN, description="Session scope mode") 238 239 # Customization 240 system_prompt: str | None = Field(default=None, description="Custom system prompt") 241 custom_instructions: str | None = Field(default=None, description="Extra instructions") 242 243 # Tool restrictions 244 tools_allow: list[str] = Field(default_factory=list, description="Allowed tools") 245 tools_deny: list[str] = Field(default_factory=list, description="Denied tools") 246 247 # Skills 248 skills_dir: Path | None = Field(default=None, description="Agent-specific skills") 249 preload_skills: list[str] = Field(default_factory=list, description="Preloaded skills") 250 251 # Behavior 252 max_steps: int = Field(default=10, ge=1, le=100, description="Max tool steps") 253 enabled: bool = Field(default=True, description="Whether agent is active") 254 255 # Metadata 256 metadata: dict[str, Any] = Field(default_factory=dict, description="Extra metadata")
Configuration for a single agent (used in config file).
259class TiphysConfig(BaseSettings): 260 """Main configuration for Tiphys.""" 261 262 model_config = SettingsConfigDict( 263 env_prefix="TIPHYS_", 264 env_file=".env", 265 env_file_encoding="utf-8", 266 extra="ignore", 267 ) 268 269 # Server 270 host: str = Field(default="127.0.0.1", description="Server bind address") 271 port: int = Field(default=8000, ge=1, le=65535, description="Server port") 272 cors_origins: list[str] = Field( 273 default_factory=lambda: ["http://localhost:8000", "http://127.0.0.1:8000"], 274 description="Allowed CORS origins (use ['*'] for development only)", 275 ) 276 277 # LLM (new structured settings) 278 llm: LLMSettings = Field( 279 default_factory=LLMSettings, 280 description="LLM provider configuration", 281 ) 282 283 # Legacy LLM fields (for backwards compatibility) 284 model: str = Field( 285 default="anthropic/claude-3.5-sonnet", 286 description="LLM model identifier (LiteLLM format)", 287 ) 288 api_base: str | None = Field( 289 default=None, 290 description="Custom API base URL for LLM provider", 291 ) 292 api_key: str | None = Field( 293 default=None, 294 description="API key for LLM provider", 295 ) 296 297 # Agent 298 agent_id: str = Field(default="default", description="Default agent identifier") 299 agent_name: str = Field(default="Tiphys", description="Agent display name") 300 dm_scope: DmScope = Field( 301 default=DmScope.MAIN, 302 description="Session scoping mode for DMs", 303 ) 304 305 # Paths 306 data_dir: Path = Field( 307 default_factory=lambda: Path.home() / ".tiphys", 308 description="Data storage directory", 309 ) 310 plugins_dir: Path = Field( 311 default=Path("./plugins"), 312 description="Plugins directory", 313 ) 314 skills_dir: Path = Field( 315 default=Path("./skills"), 316 description="Skills directory", 317 ) 318 319 # Security 320 exec_mode: Literal["deny", "allowlist", "full"] = Field( 321 default="allowlist", 322 description="Command execution mode", 323 ) 324 allowed_commands: list[str] = Field( 325 default_factory=lambda: ["ls", "cat", "grep", "find", "git", "python", "pip", "uv"], 326 description="Allowed commands in allowlist mode", 327 ) 328 329 # Logging 330 log_level: LogLevel = Field( 331 default=LogLevel.INFO, 332 description="Logging level", 333 ) 334 log_json: bool = Field( 335 default=False, 336 description="Output logs as JSON", 337 ) 338 339 # Channels 340 telegram_token: str | None = Field( 341 default=None, 342 description="Telegram bot token", 343 ) 344 discord_token: str | None = Field( 345 default=None, 346 description="Discord bot token", 347 ) 348 349 # Sandbox 350 sandbox: SandboxSettings = Field( 351 default_factory=SandboxSettings, 352 description="Sandbox configuration for secure execution", 353 ) 354 355 # Multi-agent configuration 356 agents: list[AgentConfig] = Field( 357 default_factory=list, 358 description="Additional agent configurations", 359 ) 360 default_agent_id: str | None = Field( 361 default=None, 362 description="Default agent ID (uses agent_id if not set)", 363 ) 364 365 # Castor kernel budget configuration 366 budget: BudgetConfig = Field( 367 default_factory=BudgetConfig, 368 description="Castor budget configuration", 369 ) 370 371 # Per-channel configuration (Phase R6) 372 default_channel_config: ChannelConfig = Field( 373 default_factory=ChannelConfig, 374 description="Default configuration for all channels", 375 ) 376 channels: dict[str, ChannelConfig] = Field( 377 default_factory=dict, 378 description="Per-channel configuration overrides", 379 ) 380 381 def ensure_dirs(self) -> None: 382 """Create required directories if they don't exist.""" 383 self.data_dir.mkdir(parents=True, exist_ok=True) 384 (self.data_dir / "sessions").mkdir(exist_ok=True) 385 (self.data_dir / "logs").mkdir(exist_ok=True) 386 387 def get_channel_config(self, channel_name: str) -> ChannelConfig: 388 """ 389 Get merged configuration for a specific channel. 390 391 If the channel has specific configuration, it is merged with 392 the default configuration (channel settings override defaults). 393 394 Args: 395 channel_name: The channel name (e.g., "webchat", "telegram"). 396 397 Returns: 398 Merged ChannelConfig with channel-specific overrides applied. 399 400 Example: 401 config = load_config() 402 webchat = config.get_channel_config("webchat") 403 print(f"Rate limit: {webchat.rate_limit_requests_per_minute}/min") 404 """ 405 # If no channel-specific config, return defaults 406 if channel_name not in self.channels: 407 return self.default_channel_config 408 409 # Merge channel config with defaults 410 # Channel settings override default settings 411 defaults = self.default_channel_config.model_dump() 412 channel_overrides = self.channels[channel_name].model_dump(exclude_unset=True) 413 merged = {**defaults, **channel_overrides} 414 return ChannelConfig(**merged)
Main configuration for Tiphys.
381 def ensure_dirs(self) -> None: 382 """Create required directories if they don't exist.""" 383 self.data_dir.mkdir(parents=True, exist_ok=True) 384 (self.data_dir / "sessions").mkdir(exist_ok=True) 385 (self.data_dir / "logs").mkdir(exist_ok=True)
Create required directories if they don't exist.
387 def get_channel_config(self, channel_name: str) -> ChannelConfig: 388 """ 389 Get merged configuration for a specific channel. 390 391 If the channel has specific configuration, it is merged with 392 the default configuration (channel settings override defaults). 393 394 Args: 395 channel_name: The channel name (e.g., "webchat", "telegram"). 396 397 Returns: 398 Merged ChannelConfig with channel-specific overrides applied. 399 400 Example: 401 config = load_config() 402 webchat = config.get_channel_config("webchat") 403 print(f"Rate limit: {webchat.rate_limit_requests_per_minute}/min") 404 """ 405 # If no channel-specific config, return defaults 406 if channel_name not in self.channels: 407 return self.default_channel_config 408 409 # Merge channel config with defaults 410 # Channel settings override default settings 411 defaults = self.default_channel_config.model_dump() 412 channel_overrides = self.channels[channel_name].model_dump(exclude_unset=True) 413 merged = {**defaults, **channel_overrides} 414 return ChannelConfig(**merged)
Get merged configuration for a specific channel.
If the channel has specific configuration, it is merged with the default configuration (channel settings override defaults).
Args: channel_name: The channel name (e.g., "webchat", "telegram").
Returns: Merged ChannelConfig with channel-specific overrides applied.
Example: config = load_config() webchat = config.get_channel_config("webchat") print(f"Rate limit: {webchat.rate_limit_requests_per_minute}/min")
417def load_config() -> TiphysConfig: 418 """Load configuration from environment and files.""" 419 return TiphysConfig()
Load configuration from environment and files.