runtime-memory 3.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. runtime_memory/__init__.py +28 -0
  2. runtime_memory/claude_code/__init__.py +48 -0
  3. runtime_memory/claude_code/commands.py +698 -0
  4. runtime_memory/claude_code/daemon.py +852 -0
  5. runtime_memory/claude_code/hooks.py +722 -0
  6. runtime_memory/cli/__init__.py +8 -0
  7. runtime_memory/cli/main.py +1936 -0
  8. runtime_memory/core/__init__.py +216 -0
  9. runtime_memory/core/config.py +473 -0
  10. runtime_memory/core/embeddings.py +908 -0
  11. runtime_memory/core/engine.py +1007 -0
  12. runtime_memory/core/exceptions.py +547 -0
  13. runtime_memory/core/legacy_env.py +39 -0
  14. runtime_memory/core/logging.py +160 -0
  15. runtime_memory/core/models.py +1051 -0
  16. runtime_memory/core/observability.py +725 -0
  17. runtime_memory/core/paths.py +30 -0
  18. runtime_memory/core/resilience.py +511 -0
  19. runtime_memory/core/retrieval.py +819 -0
  20. runtime_memory/core/storage.py +1105 -0
  21. runtime_memory/extraction/__init__.py +36 -0
  22. runtime_memory/extraction/extractor.py +1143 -0
  23. runtime_memory/hermes/__init__.py +39 -0
  24. runtime_memory/hermes/_base.py +154 -0
  25. runtime_memory/hermes/bridge.py +119 -0
  26. runtime_memory/hermes/plugin.yaml +13 -0
  27. runtime_memory/hermes/provider.py +536 -0
  28. runtime_memory/hermes/tools.py +230 -0
  29. runtime_memory/hermes/trace.py +177 -0
  30. runtime_memory/plugin/__init__.py +646 -0
  31. runtime_memory/sdk/__init__.py +97 -0
  32. runtime_memory/sdk/client.py +1577 -0
  33. runtime_memory/server/__init__.py +75 -0
  34. runtime_memory/server/api.py +1665 -0
  35. runtime_memory/server/mcp.py +1574 -0
  36. runtime_memory/server/static/css/styles.css +1110 -0
  37. runtime_memory/server/static/index.html +264 -0
  38. runtime_memory/server/static/js/api.js +294 -0
  39. runtime_memory/server/static/js/app.js +771 -0
  40. runtime_memory/tasks/__init__.py +114 -0
  41. runtime_memory/tasks/adapter.py +501 -0
  42. runtime_memory/tasks/claude_code_adapter.py +495 -0
  43. runtime_memory/tasks/claude_code_parser.py +339 -0
  44. runtime_memory/tasks/cli_bridge.py +415 -0
  45. runtime_memory/tasks/linking.py +397 -0
  46. runtime_memory/tasks/models.py +520 -0
  47. runtime_memory/tasks/outcomes.py +320 -0
  48. runtime_memory/tasks/parser.py +305 -0
  49. runtime_memory/tasks/unified_adapter.py +661 -0
  50. runtime_memory-3.0.0.dist-info/METADATA +497 -0
  51. runtime_memory-3.0.0.dist-info/RECORD +54 -0
  52. runtime_memory-3.0.0.dist-info/WHEEL +4 -0
  53. runtime_memory-3.0.0.dist-info/entry_points.txt +6 -0
  54. runtime_memory-3.0.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,216 @@
1
+ """Core memory engine components.
2
+
3
+ This module contains the foundational components for Runtime Memory:
4
+ - Data models and enums
5
+ - SQLite storage layer
6
+ - Vector embeddings
7
+ - Hybrid retrieval system
8
+ - Memory engine orchestrator
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from runtime_memory.core.embeddings import (
14
+ APIEmbeddingProvider,
15
+ APIError,
16
+ BatchEmbeddingResult,
17
+ Embedding,
18
+ EmbeddingCache,
19
+ EmbeddingConfig,
20
+ EmbeddingError,
21
+ EmbeddingProvider,
22
+ EmbeddingResult,
23
+ LocalEmbeddingProvider,
24
+ MockEmbeddingProvider,
25
+ ModelNotFoundError,
26
+ NullEmbeddingProvider,
27
+ get_embedding_provider,
28
+ )
29
+ from runtime_memory.core.engine import (
30
+ EngineConfig,
31
+ EngineError,
32
+ EngineNotInitializedError,
33
+ EngineStats,
34
+ LastSearchInfo,
35
+ MemoryEngine,
36
+ create_engine,
37
+ )
38
+ from runtime_memory.core.exceptions import (
39
+ AuthenticationError,
40
+ BeadsNotFoundError,
41
+ CircuitOpenError,
42
+ ConfigurationError,
43
+ DaemonAlreadyRunningError,
44
+ DaemonError,
45
+ DatabaseConnectionError,
46
+ DatabaseIntegrityError,
47
+ ErrorCode,
48
+ ExtractionError,
49
+ HookError,
50
+ HookNotInstalledError,
51
+ InitializationError,
52
+ InvalidResponseError,
53
+ LLMAPIError,
54
+ MCPError,
55
+ RuntimeMemoryError,
56
+ RateLimitError,
57
+ RetrievalError,
58
+ SDKError,
59
+ SearchTimeoutError,
60
+ ServerError,
61
+ TaskError,
62
+ TaskLinkError,
63
+ TaskSyncError,
64
+ ValidationError,
65
+ format_error,
66
+ is_recoverable,
67
+ )
68
+ from runtime_memory.core.logging import get_logger, setup_logging
69
+ from runtime_memory.core.models import (
70
+ OUTCOME_SCORE_ADJUSTMENTS,
71
+ ContextResponse,
72
+ ContextResponseModel,
73
+ Memory,
74
+ MemoryCategory,
75
+ MemoryCreate,
76
+ MemoryResponse,
77
+ MemoryScope,
78
+ MemorySource,
79
+ MemoryUpdate,
80
+ Outcome,
81
+ OutcomeRecord,
82
+ SearchQuery,
83
+ SearchResult,
84
+ SearchResultResponse,
85
+ StatsResponse,
86
+ )
87
+ from runtime_memory.core.resilience import (
88
+ CircuitBreaker,
89
+ CircuitBreakerConfig,
90
+ CircuitState,
91
+ RetryConfig,
92
+ calculate_backoff,
93
+ get_circuit_breaker,
94
+ reset_all_circuit_breakers,
95
+ retry,
96
+ retry_sync,
97
+ try_multiple,
98
+ with_fallback,
99
+ with_fallback_sync,
100
+ with_timeout,
101
+ )
102
+ from runtime_memory.core.retrieval import (
103
+ BM25Index,
104
+ CategoryRouter,
105
+ HybridRetriever,
106
+ RetrievalConfig,
107
+ )
108
+ from runtime_memory.core.storage import (
109
+ ConnectionError,
110
+ MemoryNotFoundError,
111
+ MemoryStorage,
112
+ StorageError,
113
+ StorageStats,
114
+ )
115
+
116
+ __all__ = [
117
+ # Logging
118
+ "get_logger",
119
+ "setup_logging",
120
+ # Enums
121
+ "MemoryCategory",
122
+ "MemoryScope",
123
+ "MemorySource",
124
+ "Outcome",
125
+ "OUTCOME_SCORE_ADJUSTMENTS",
126
+ # Dataclasses
127
+ "Memory",
128
+ "SearchResult",
129
+ "ContextResponse",
130
+ # Pydantic models
131
+ "MemoryCreate",
132
+ "MemoryUpdate",
133
+ "SearchQuery",
134
+ "OutcomeRecord",
135
+ "MemoryResponse",
136
+ "SearchResultResponse",
137
+ "ContextResponseModel",
138
+ "StatsResponse",
139
+ # Storage
140
+ "MemoryStorage",
141
+ "StorageStats",
142
+ "StorageError",
143
+ "MemoryNotFoundError",
144
+ "ConnectionError",
145
+ # Embeddings
146
+ "EmbeddingProvider",
147
+ "LocalEmbeddingProvider",
148
+ "APIEmbeddingProvider",
149
+ "MockEmbeddingProvider",
150
+ "NullEmbeddingProvider",
151
+ "EmbeddingConfig",
152
+ "EmbeddingResult",
153
+ "BatchEmbeddingResult",
154
+ "EmbeddingCache",
155
+ "Embedding",
156
+ "EmbeddingError",
157
+ "ModelNotFoundError",
158
+ "APIError",
159
+ "get_embedding_provider",
160
+ # Retrieval
161
+ "BM25Index",
162
+ "HybridRetriever",
163
+ "RetrievalConfig",
164
+ "CategoryRouter",
165
+ # Engine
166
+ "MemoryEngine",
167
+ "EngineConfig",
168
+ "EngineStats",
169
+ "EngineError",
170
+ "EngineNotInitializedError",
171
+ "LastSearchInfo",
172
+ "create_engine",
173
+ # Exceptions
174
+ "RuntimeMemoryError",
175
+ "ErrorCode",
176
+ "DatabaseConnectionError",
177
+ "DatabaseIntegrityError",
178
+ "RetrievalError",
179
+ "SearchTimeoutError",
180
+ "ExtractionError",
181
+ "LLMAPIError",
182
+ "RateLimitError",
183
+ "InvalidResponseError",
184
+ "ServerError",
185
+ "MCPError",
186
+ "HookError",
187
+ "HookNotInstalledError",
188
+ "DaemonError",
189
+ "DaemonAlreadyRunningError",
190
+ "SDKError",
191
+ "AuthenticationError",
192
+ "ValidationError",
193
+ "TaskError",
194
+ "BeadsNotFoundError",
195
+ "TaskSyncError",
196
+ "TaskLinkError",
197
+ "ConfigurationError",
198
+ "InitializationError",
199
+ "CircuitOpenError",
200
+ "format_error",
201
+ "is_recoverable",
202
+ # Resilience
203
+ "RetryConfig",
204
+ "calculate_backoff",
205
+ "retry",
206
+ "retry_sync",
207
+ "CircuitState",
208
+ "CircuitBreakerConfig",
209
+ "CircuitBreaker",
210
+ "get_circuit_breaker",
211
+ "reset_all_circuit_breakers",
212
+ "with_fallback",
213
+ "with_fallback_sync",
214
+ "try_multiple",
215
+ "with_timeout",
216
+ ]
@@ -0,0 +1,473 @@
1
+ """
2
+ Configuration management for Runtime Memory.
3
+
4
+ Provides:
5
+ - Environment-based configuration
6
+ - Validation with Pydantic
7
+ - Support for multiple environments (dev, test, prod)
8
+ - Secrets management via environment variables
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import os
14
+ from enum import Enum
15
+ from functools import lru_cache
16
+ from pathlib import Path
17
+ from typing import Any
18
+
19
+ from pydantic import BaseModel, Field, field_validator
20
+ from pydantic_settings import BaseSettings, SettingsConfigDict
21
+
22
+ from runtime_memory.core.paths import default_db_path, store_dir
23
+
24
+
25
+ class Environment(str, Enum):
26
+ """Application environment."""
27
+
28
+ DEVELOPMENT = "development"
29
+ TESTING = "testing"
30
+ PRODUCTION = "production"
31
+
32
+
33
+ class DatabaseConfig(BaseModel):
34
+ """Database configuration."""
35
+
36
+ path: Path = Field(
37
+ default_factory=default_db_path,
38
+ description="Path to SQLite database file",
39
+ )
40
+ echo: bool = Field(
41
+ default=False,
42
+ description="Echo SQL queries (for debugging)",
43
+ )
44
+ pool_size: int = Field(
45
+ default=5,
46
+ ge=1,
47
+ le=20,
48
+ description="Connection pool size",
49
+ )
50
+ timeout: float = Field(
51
+ default=30.0,
52
+ ge=1.0,
53
+ le=300.0,
54
+ description="Database operation timeout in seconds",
55
+ )
56
+
57
+ @field_validator("path", mode="before")
58
+ @classmethod
59
+ def expand_path(cls, v: Any) -> Path:
60
+ """Expand user home directory in path."""
61
+ if isinstance(v, str):
62
+ v = Path(v).expanduser()
63
+ return v
64
+
65
+
66
+ class EmbeddingConfig(BaseModel):
67
+ """Embedding model configuration."""
68
+
69
+ model_name: str = Field(
70
+ default="all-MiniLM-L6-v2",
71
+ description="Sentence transformer model name",
72
+ )
73
+ cache_enabled: bool = Field(
74
+ default=True,
75
+ description="Enable embedding cache",
76
+ )
77
+ cache_size: int = Field(
78
+ default=10000,
79
+ ge=100,
80
+ le=100000,
81
+ description="Maximum number of cached embeddings",
82
+ )
83
+ batch_size: int = Field(
84
+ default=32,
85
+ ge=1,
86
+ le=128,
87
+ description="Batch size for embedding generation",
88
+ )
89
+ device: str = Field(
90
+ default="cpu",
91
+ description="Device for embedding model (cpu/cuda/mps)",
92
+ )
93
+
94
+
95
+ class RetrievalConfig(BaseModel):
96
+ """Retrieval system configuration."""
97
+
98
+ semantic_weight: float = Field(
99
+ default=0.35,
100
+ ge=0.0,
101
+ le=1.0,
102
+ description="Weight for semantic similarity (0-1)",
103
+ )
104
+ outcome_weight: float = Field(
105
+ default=0.25,
106
+ ge=0.0,
107
+ le=1.0,
108
+ description="Weight for outcome score (0-1)",
109
+ )
110
+ recency_weight: float = Field(
111
+ default=0.15,
112
+ ge=0.0,
113
+ le=1.0,
114
+ description="Weight for recency (0-1)",
115
+ )
116
+ frequency_weight: float = Field(
117
+ default=0.15,
118
+ ge=0.0,
119
+ le=1.0,
120
+ description="Weight for usage frequency (0-1)",
121
+ )
122
+ confidence_weight: float = Field(
123
+ default=0.10,
124
+ ge=0.0,
125
+ le=1.0,
126
+ description="Weight for confidence score (0-1)",
127
+ )
128
+ recency_half_life_days: int = Field(
129
+ default=30,
130
+ ge=1,
131
+ le=365,
132
+ description="Half-life for recency decay in days",
133
+ )
134
+ default_limit: int = Field(
135
+ default=10,
136
+ ge=1,
137
+ le=100,
138
+ description="Default number of results to return",
139
+ )
140
+
141
+
142
+ class ServerConfig(BaseModel):
143
+ """Server configuration."""
144
+
145
+ host: str = Field(
146
+ default="127.0.0.1",
147
+ description="Server bind address",
148
+ )
149
+ port: int = Field(
150
+ default=8080,
151
+ ge=1024,
152
+ le=65535,
153
+ description="Server port number",
154
+ )
155
+ workers: int = Field(
156
+ default=1,
157
+ ge=1,
158
+ le=16,
159
+ description="Number of server workers",
160
+ )
161
+ cors_origins: list[str] = Field(
162
+ default_factory=lambda: ["http://localhost:*", "http://127.0.0.1:*"],
163
+ description="Allowed CORS origins",
164
+ )
165
+ request_timeout: float = Field(
166
+ default=30.0,
167
+ ge=1.0,
168
+ le=300.0,
169
+ description="Request timeout in seconds",
170
+ )
171
+
172
+
173
+ class ExtractionConfig(BaseModel):
174
+ """LLM extraction configuration."""
175
+
176
+ model: str = Field(
177
+ default="claude-3-haiku-20240307",
178
+ description="LLM model for extraction",
179
+ )
180
+ max_tokens: int = Field(
181
+ default=1000,
182
+ ge=100,
183
+ le=4000,
184
+ description="Maximum tokens for extraction response",
185
+ )
186
+ temperature: float = Field(
187
+ default=0.0,
188
+ ge=0.0,
189
+ le=1.0,
190
+ description="LLM temperature for extraction",
191
+ )
192
+ timeout: float = Field(
193
+ default=30.0,
194
+ ge=5.0,
195
+ le=120.0,
196
+ description="API call timeout in seconds",
197
+ )
198
+ max_retries: int = Field(
199
+ default=3,
200
+ ge=0,
201
+ le=5,
202
+ description="Maximum retry attempts for API calls",
203
+ )
204
+
205
+
206
+ class LoggingConfig(BaseModel):
207
+ """Logging configuration."""
208
+
209
+ level: str = Field(
210
+ default="WARNING",
211
+ description="Logging level (DEBUG, INFO, WARNING, ERROR)",
212
+ )
213
+ format: str = Field(
214
+ default="text",
215
+ description="Log format (text or json)",
216
+ )
217
+ include_timestamp: bool = Field(
218
+ default=True,
219
+ description="Include timestamp in log output",
220
+ )
221
+ log_file: Path | None = Field(
222
+ default=None,
223
+ description="Optional log file path",
224
+ )
225
+
226
+ @field_validator("level")
227
+ @classmethod
228
+ def validate_level(cls, v: str) -> str:
229
+ """Validate logging level."""
230
+ valid_levels = {"DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"}
231
+ v = v.upper()
232
+ if v not in valid_levels:
233
+ raise ValueError(f"Invalid log level: {v}. Must be one of {valid_levels}")
234
+ return v
235
+
236
+
237
+ class Settings(BaseSettings):
238
+ """Main application settings.
239
+
240
+ Configuration is loaded in priority order:
241
+ 1. Environment variables (highest priority)
242
+ 2. .env file in current directory
243
+ 3. Default values (lowest priority)
244
+
245
+ Environment variables use RUNTIME_MEMORY_ prefix. Nested settings use a
246
+ double-underscore delimiter:
247
+ - RUNTIME_MEMORY_ENV=production
248
+ - RUNTIME_MEMORY_DATABASE__PATH=/path/to/db
249
+ - RUNTIME_MEMORY_LOG_LEVEL=DEBUG
250
+
251
+ The ``mem`` CLI accepts either RUNTIME_MEMORY_DATABASE__PATH or the shorter
252
+ RUNTIME_MEMORY_DB. Note that RUNTIME_MEMORY_DB_PATH is *not* recognised; it was
253
+ documented here in error and silently fell through to the default database.
254
+ """
255
+
256
+ model_config = SettingsConfigDict(
257
+ env_prefix="RUNTIME_MEMORY_",
258
+ env_file=".env",
259
+ env_file_encoding="utf-8",
260
+ env_nested_delimiter="__",
261
+ extra="ignore",
262
+ )
263
+
264
+ # Environment
265
+ env: Environment = Field(
266
+ default=Environment.DEVELOPMENT,
267
+ description="Application environment",
268
+ )
269
+
270
+ # API Keys (from environment only, never stored)
271
+ anthropic_api_key: str | None = Field(
272
+ default=None,
273
+ description="Anthropic API key for extraction",
274
+ )
275
+ openai_api_key: str | None = Field(
276
+ default=None,
277
+ description="OpenAI API key (fallback)",
278
+ )
279
+
280
+ # Component configs
281
+ database: DatabaseConfig = Field(default_factory=DatabaseConfig)
282
+ embedding: EmbeddingConfig = Field(default_factory=EmbeddingConfig)
283
+ retrieval: RetrievalConfig = Field(default_factory=RetrievalConfig)
284
+ server: ServerConfig = Field(default_factory=ServerConfig)
285
+ extraction: ExtractionConfig = Field(default_factory=ExtractionConfig)
286
+ logging: LoggingConfig = Field(default_factory=LoggingConfig)
287
+
288
+ # Feature flags
289
+ enable_extraction: bool = Field(
290
+ default=True,
291
+ description="Enable LLM-based extraction",
292
+ )
293
+ enable_metrics: bool = Field(
294
+ default=False,
295
+ description="Enable Prometheus metrics",
296
+ )
297
+ enable_tracing: bool = Field(
298
+ default=False,
299
+ description="Enable distributed tracing",
300
+ )
301
+
302
+ # Project context
303
+ project_name: str | None = Field(
304
+ default=None,
305
+ description="Current project name (auto-detected if not set)",
306
+ )
307
+ project_path: Path | None = Field(
308
+ default=None,
309
+ description="Current project path",
310
+ )
311
+
312
+ @classmethod
313
+ def for_testing(cls) -> "Settings":
314
+ """Create settings optimized for testing."""
315
+ return cls(
316
+ env=Environment.TESTING,
317
+ database=DatabaseConfig(
318
+ path=Path(":memory:"),
319
+ echo=False,
320
+ ),
321
+ embedding=EmbeddingConfig(
322
+ cache_enabled=False,
323
+ ),
324
+ logging=LoggingConfig(
325
+ level="DEBUG",
326
+ ),
327
+ enable_extraction=False,
328
+ enable_metrics=False,
329
+ enable_tracing=False,
330
+ )
331
+
332
+ @classmethod
333
+ def for_production(cls) -> "Settings":
334
+ """Create settings optimized for production."""
335
+ return cls(
336
+ env=Environment.PRODUCTION,
337
+ logging=LoggingConfig(
338
+ level="WARNING",
339
+ format="json",
340
+ ),
341
+ enable_metrics=True,
342
+ )
343
+
344
+ def is_development(self) -> bool:
345
+ """Check if running in development mode."""
346
+ return self.env == Environment.DEVELOPMENT
347
+
348
+ def is_testing(self) -> bool:
349
+ """Check if running in testing mode."""
350
+ return self.env == Environment.TESTING
351
+
352
+ def is_production(self) -> bool:
353
+ """Check if running in production mode."""
354
+ return self.env == Environment.PRODUCTION
355
+
356
+ def get_db_path(self) -> Path:
357
+ """Get resolved database path."""
358
+ path = self.database.path.expanduser()
359
+ path.parent.mkdir(parents=True, exist_ok=True)
360
+ return path
361
+
362
+ def validate_for_extraction(self) -> bool:
363
+ """Check if extraction is properly configured."""
364
+ return bool(self.anthropic_api_key or self.openai_api_key)
365
+
366
+
367
+ @lru_cache()
368
+ def get_settings() -> Settings:
369
+ """Get cached application settings.
370
+
371
+ Settings are loaded once and cached for the lifetime of the application.
372
+ Use clear_settings_cache() to reload settings.
373
+
374
+ Returns:
375
+ The application settings
376
+ """
377
+ return Settings()
378
+
379
+
380
+ def clear_settings_cache() -> None:
381
+ """Clear the settings cache, forcing reload on next access."""
382
+ get_settings.cache_clear()
383
+
384
+
385
+ def get_config_path() -> Path:
386
+ """Get the configuration directory path."""
387
+ config_dir = store_dir()
388
+ config_dir.mkdir(parents=True, exist_ok=True)
389
+ return config_dir
390
+
391
+
392
+ def get_data_path() -> Path:
393
+ """Get the data directory path."""
394
+ data_dir = store_dir() / "data"
395
+ data_dir.mkdir(parents=True, exist_ok=True)
396
+ return data_dir
397
+
398
+
399
+ def get_cache_path() -> Path:
400
+ """Get the cache directory path."""
401
+ cache_dir = store_dir() / "cache"
402
+ cache_dir.mkdir(parents=True, exist_ok=True)
403
+ return cache_dir
404
+
405
+
406
+ # Environment variable helpers
407
+ def get_env(key: str, default: str | None = None) -> str | None:
408
+ """Get environment variable with RUNTIME_MEMORY_ prefix.
409
+
410
+ Args:
411
+ key: Variable name (without prefix)
412
+ default: Default value if not set
413
+
414
+ Returns:
415
+ The environment variable value or default
416
+ """
417
+ return os.getenv(f"RUNTIME_MEMORY_{key}", default)
418
+
419
+
420
+ def get_env_bool(key: str, default: bool = False) -> bool:
421
+ """Get boolean environment variable.
422
+
423
+ Recognizes: true, 1, yes, on (case-insensitive)
424
+
425
+ Args:
426
+ key: Variable name (without prefix)
427
+ default: Default value if not set
428
+
429
+ Returns:
430
+ Boolean value
431
+ """
432
+ value = get_env(key)
433
+ if value is None:
434
+ return default
435
+ return value.lower() in ("true", "1", "yes", "on")
436
+
437
+
438
+ def get_env_int(key: str, default: int = 0) -> int:
439
+ """Get integer environment variable.
440
+
441
+ Args:
442
+ key: Variable name (without prefix)
443
+ default: Default value if not set or invalid
444
+
445
+ Returns:
446
+ Integer value
447
+ """
448
+ value = get_env(key)
449
+ if value is None:
450
+ return default
451
+ try:
452
+ return int(value)
453
+ except ValueError:
454
+ return default
455
+
456
+
457
+ def get_env_float(key: str, default: float = 0.0) -> float:
458
+ """Get float environment variable.
459
+
460
+ Args:
461
+ key: Variable name (without prefix)
462
+ default: Default value if not set or invalid
463
+
464
+ Returns:
465
+ Float value
466
+ """
467
+ value = get_env(key)
468
+ if value is None:
469
+ return default
470
+ try:
471
+ return float(value)
472
+ except ValueError:
473
+ return default