gradia 1.0.0__py3-none-any.whl → 2.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.
- gradia/__init__.py +38 -1
- gradia/cli/main.py +1 -1
- gradia/core/config.py +71 -13
- gradia/core/migration.py +324 -0
- gradia/events/__init__.py +17 -0
- gradia/events/logger.py +215 -0
- gradia/events/models.py +170 -0
- gradia/events/tracker.py +337 -0
- gradia/trainer/engine.py +175 -3
- gradia/viz/server.py +153 -17
- gradia/viz/static/css/timeline.css +419 -0
- gradia/viz/static/js/timeline.js +471 -0
- gradia/viz/templates/configure.html +1 -1
- gradia/viz/templates/index.html +11 -9
- gradia/viz/templates/timeline.html +195 -0
- gradia-2.0.0.dist-info/METADATA +394 -0
- gradia-2.0.0.dist-info/RECORD +30 -0
- {gradia-1.0.0.dist-info → gradia-2.0.0.dist-info}/WHEEL +1 -1
- gradia-1.0.0.dist-info/METADATA +0 -143
- gradia-1.0.0.dist-info/RECORD +0 -22
- {gradia-1.0.0.dist-info → gradia-2.0.0.dist-info}/entry_points.txt +0 -0
- {gradia-1.0.0.dist-info → gradia-2.0.0.dist-info}/licenses/LICENSE +0 -0
- {gradia-1.0.0.dist-info → gradia-2.0.0.dist-info}/top_level.txt +0 -0
gradia/__init__.py
CHANGED
|
@@ -1 +1,38 @@
|
|
|
1
|
-
|
|
1
|
+
"""
|
|
2
|
+
Gradia - Local-first ML Training Visualization
|
|
3
|
+
|
|
4
|
+
v2.0.0: Learning Timeline Edition
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
__version__ = "2.0.0"
|
|
8
|
+
|
|
9
|
+
# Core exports
|
|
10
|
+
from .core.scenario import Scenario, ScenarioInferrer
|
|
11
|
+
from .core.config import ConfigManager
|
|
12
|
+
from .core.inspector import Inspector
|
|
13
|
+
from .core.migration import SchemaMigrator, ensure_v2_config
|
|
14
|
+
|
|
15
|
+
# Events module (v2.0)
|
|
16
|
+
from .events import LearningEvent, SampleTracker, TimelineLogger
|
|
17
|
+
|
|
18
|
+
# Trainer
|
|
19
|
+
from .trainer.engine import Trainer
|
|
20
|
+
from .trainer.callbacks import EventLogger
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"__version__",
|
|
24
|
+
# Core
|
|
25
|
+
"Scenario",
|
|
26
|
+
"ScenarioInferrer",
|
|
27
|
+
"ConfigManager",
|
|
28
|
+
"Inspector",
|
|
29
|
+
"SchemaMigrator",
|
|
30
|
+
"ensure_v2_config",
|
|
31
|
+
# Events (v2.0)
|
|
32
|
+
"LearningEvent",
|
|
33
|
+
"SampleTracker",
|
|
34
|
+
"TimelineLogger",
|
|
35
|
+
# Trainer
|
|
36
|
+
"Trainer",
|
|
37
|
+
"EventLogger",
|
|
38
|
+
]
|
gradia/cli/main.py
CHANGED
gradia/core/config.py
CHANGED
|
@@ -2,55 +2,113 @@ import yaml
|
|
|
2
2
|
from pathlib import Path
|
|
3
3
|
from typing import Any, Dict
|
|
4
4
|
|
|
5
|
+
from .migration import SchemaMigrator, SchemaVersion
|
|
6
|
+
|
|
7
|
+
|
|
5
8
|
class ConfigManager:
|
|
6
|
-
"""Manages gradia configuration."""
|
|
9
|
+
"""Manages gradia configuration with v2.0 migration support."""
|
|
7
10
|
|
|
11
|
+
# v2.0 Default Configuration
|
|
8
12
|
DEFAULT_CONFIG = {
|
|
13
|
+
'schema_version': SchemaVersion.V2_0.value,
|
|
9
14
|
'model': {
|
|
10
|
-
'type': 'auto',
|
|
15
|
+
'type': 'auto', # auto, linear, random_forest, sgd, mlp, cnn
|
|
11
16
|
'params': {}
|
|
12
17
|
},
|
|
13
18
|
'training': {
|
|
14
19
|
'test_split': 0.2,
|
|
15
20
|
'random_seed': 42,
|
|
16
|
-
'shuffle': True
|
|
21
|
+
'shuffle': True,
|
|
22
|
+
'epochs': 10
|
|
17
23
|
},
|
|
18
24
|
'scenario': {
|
|
19
|
-
'target': None,
|
|
20
|
-
'task': None
|
|
21
|
-
}
|
|
25
|
+
'target': None, # Auto-detect
|
|
26
|
+
'task': None # Auto-detect
|
|
27
|
+
},
|
|
28
|
+
# v2.0: Learning Timeline configuration
|
|
29
|
+
'timeline': {
|
|
30
|
+
'enabled': True,
|
|
31
|
+
'max_samples': 100,
|
|
32
|
+
'user_samples': None # List of sample indices to always track
|
|
33
|
+
},
|
|
34
|
+
'project_name': 'experiment',
|
|
35
|
+
'save_model': False
|
|
22
36
|
}
|
|
23
37
|
|
|
24
38
|
def __init__(self, run_dir: str = ".gradia_logs"):
|
|
25
39
|
self.run_dir = Path(run_dir)
|
|
26
40
|
self.config_path = self.run_dir / "config.yaml"
|
|
41
|
+
self._migrator = SchemaMigrator()
|
|
27
42
|
|
|
28
43
|
def load_or_create(self, user_overrides: Dict[str, Any] = None) -> Dict[str, Any]:
|
|
29
|
-
config = self.DEFAULT_CONFIG
|
|
44
|
+
config = self._deep_copy(self.DEFAULT_CONFIG)
|
|
30
45
|
|
|
31
|
-
# Load existing if
|
|
32
|
-
|
|
33
|
-
|
|
46
|
+
# Load existing config if present (for run continuation)
|
|
47
|
+
if self.config_path.exists():
|
|
48
|
+
with open(self.config_path, 'r') as f:
|
|
49
|
+
existing = yaml.safe_load(f) or {}
|
|
50
|
+
|
|
51
|
+
# Migrate to v2 if needed
|
|
52
|
+
result = self._migrator.migrate(existing)
|
|
53
|
+
if result.changes:
|
|
54
|
+
print(f"Config migrated: {', '.join(result.changes)}")
|
|
55
|
+
|
|
56
|
+
self._update_recursive(config, existing)
|
|
34
57
|
|
|
58
|
+
# Load root gradia.yaml overrides
|
|
35
59
|
root_config = Path("gradia.yaml")
|
|
36
60
|
if root_config.exists():
|
|
37
61
|
with open(root_config, 'r') as f:
|
|
38
|
-
user_config = yaml.safe_load(f)
|
|
62
|
+
user_config = yaml.safe_load(f) or {}
|
|
39
63
|
self._update_recursive(config, user_config)
|
|
40
64
|
|
|
65
|
+
# Apply explicit overrides
|
|
41
66
|
if user_overrides:
|
|
42
67
|
self._update_recursive(config, user_overrides)
|
|
68
|
+
|
|
69
|
+
# Ensure v2 fields exist
|
|
70
|
+
config = self._ensure_v2_fields(config)
|
|
43
71
|
|
|
44
72
|
return config
|
|
45
73
|
|
|
46
74
|
def save(self, config: Dict[str, Any]):
|
|
47
|
-
|
|
75
|
+
"""Save config with schema version marker."""
|
|
76
|
+
self.run_dir.mkdir(parents=True, exist_ok=True)
|
|
77
|
+
|
|
78
|
+
# Ensure schema version is set
|
|
79
|
+
config['schema_version'] = SchemaVersion.V2_0.value
|
|
80
|
+
|
|
48
81
|
with open(self.config_path, 'w') as f:
|
|
49
|
-
yaml.dump(config, f)
|
|
82
|
+
yaml.dump(config, f, default_flow_style=False)
|
|
50
83
|
|
|
51
84
|
def _update_recursive(self, base: Dict, update: Dict):
|
|
85
|
+
"""Recursively merge update into base."""
|
|
52
86
|
for k, v in update.items():
|
|
53
87
|
if k in base and isinstance(base[k], dict) and isinstance(v, dict):
|
|
54
88
|
self._update_recursive(base[k], v)
|
|
55
89
|
else:
|
|
56
90
|
base[k] = v
|
|
91
|
+
|
|
92
|
+
def _deep_copy(self, d: Dict) -> Dict:
|
|
93
|
+
"""Create a deep copy of nested dict."""
|
|
94
|
+
import copy
|
|
95
|
+
return copy.deepcopy(d)
|
|
96
|
+
|
|
97
|
+
def _ensure_v2_fields(self, config: Dict) -> Dict:
|
|
98
|
+
"""Ensure all v2.0 required fields exist."""
|
|
99
|
+
# Timeline config
|
|
100
|
+
if 'timeline' not in config:
|
|
101
|
+
config['timeline'] = {
|
|
102
|
+
'enabled': True,
|
|
103
|
+
'max_samples': 100,
|
|
104
|
+
'user_samples': None
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
# Training epochs
|
|
108
|
+
if 'epochs' not in config.get('training', {}):
|
|
109
|
+
config.setdefault('training', {})['epochs'] = 10
|
|
110
|
+
|
|
111
|
+
# Schema version
|
|
112
|
+
config['schema_version'] = SchemaVersion.V2_0.value
|
|
113
|
+
|
|
114
|
+
return config
|
gradia/core/migration.py
ADDED
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Migration Layer for Gradia v2.0.0
|
|
3
|
+
|
|
4
|
+
Handles backward compatibility with v1.x .gradia_logs runs.
|
|
5
|
+
Supports schema versioning and silent migration.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Dict, Any, Optional, List
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
import json
|
|
11
|
+
import yaml
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from enum import Enum
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SchemaVersion(str, Enum):
|
|
17
|
+
"""Gradia config/log schema versions."""
|
|
18
|
+
V1_0 = "1.0"
|
|
19
|
+
V1_1 = "1.1"
|
|
20
|
+
V1_2 = "1.2"
|
|
21
|
+
V1_3 = "1.3"
|
|
22
|
+
V2_0 = "2.0"
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def current(cls) -> "SchemaVersion":
|
|
26
|
+
return cls.V2_0
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class MigrationResult:
|
|
31
|
+
"""Result of a migration operation."""
|
|
32
|
+
success: bool
|
|
33
|
+
from_version: str
|
|
34
|
+
to_version: str
|
|
35
|
+
changes: List[str]
|
|
36
|
+
warnings: List[str]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class SchemaMigrator:
|
|
40
|
+
"""
|
|
41
|
+
Handles schema migration between Gradia versions.
|
|
42
|
+
|
|
43
|
+
Strategy:
|
|
44
|
+
- Silent upgrades (no user intervention required)
|
|
45
|
+
- Deprecate, don't remove fields
|
|
46
|
+
- Add new fields with sensible defaults
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(self):
|
|
50
|
+
self.migrations = {
|
|
51
|
+
("1.0", "1.1"): self._migrate_1_0_to_1_1,
|
|
52
|
+
("1.1", "1.2"): self._migrate_1_1_to_1_2,
|
|
53
|
+
("1.2", "1.3"): self._migrate_1_2_to_1_3,
|
|
54
|
+
("1.3", "2.0"): self._migrate_1_3_to_2_0,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
def detect_version(self, config: Dict[str, Any]) -> str:
|
|
58
|
+
"""Detect schema version from config structure."""
|
|
59
|
+
# v2.0 has explicit version field
|
|
60
|
+
if "schema_version" in config:
|
|
61
|
+
return config["schema_version"]
|
|
62
|
+
|
|
63
|
+
# v2.0 has timeline config
|
|
64
|
+
if "timeline" in config:
|
|
65
|
+
return "2.0"
|
|
66
|
+
|
|
67
|
+
# v1.3 has project_name and save_model
|
|
68
|
+
if "project_name" in config or "save_model" in config:
|
|
69
|
+
return "1.3"
|
|
70
|
+
|
|
71
|
+
# v1.2 has training.epochs
|
|
72
|
+
if config.get("training", {}).get("epochs"):
|
|
73
|
+
return "1.2"
|
|
74
|
+
|
|
75
|
+
# v1.1 has model.params
|
|
76
|
+
if config.get("model", {}).get("params"):
|
|
77
|
+
return "1.1"
|
|
78
|
+
|
|
79
|
+
# Default to 1.0
|
|
80
|
+
return "1.0"
|
|
81
|
+
|
|
82
|
+
def migrate(self, config: Dict[str, Any], target_version: str = None) -> MigrationResult:
|
|
83
|
+
"""
|
|
84
|
+
Migrate config to target version (default: current).
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
config: Configuration dictionary
|
|
88
|
+
target_version: Target version string (default: current)
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
MigrationResult with details
|
|
92
|
+
"""
|
|
93
|
+
if target_version is None:
|
|
94
|
+
target_version = SchemaVersion.current().value
|
|
95
|
+
|
|
96
|
+
from_version = self.detect_version(config)
|
|
97
|
+
changes = []
|
|
98
|
+
warnings = []
|
|
99
|
+
|
|
100
|
+
if from_version == target_version:
|
|
101
|
+
return MigrationResult(
|
|
102
|
+
success=True,
|
|
103
|
+
from_version=from_version,
|
|
104
|
+
to_version=target_version,
|
|
105
|
+
changes=["No migration needed"],
|
|
106
|
+
warnings=[]
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# Build migration path
|
|
110
|
+
current = from_version
|
|
111
|
+
version_order = ["1.0", "1.1", "1.2", "1.3", "2.0"]
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
start_idx = version_order.index(current)
|
|
115
|
+
end_idx = version_order.index(target_version)
|
|
116
|
+
except ValueError as e:
|
|
117
|
+
return MigrationResult(
|
|
118
|
+
success=False,
|
|
119
|
+
from_version=from_version,
|
|
120
|
+
to_version=target_version,
|
|
121
|
+
changes=[],
|
|
122
|
+
warnings=[f"Unknown version: {e}"]
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
if start_idx > end_idx:
|
|
126
|
+
# Downgrade not supported
|
|
127
|
+
return MigrationResult(
|
|
128
|
+
success=False,
|
|
129
|
+
from_version=from_version,
|
|
130
|
+
to_version=target_version,
|
|
131
|
+
changes=[],
|
|
132
|
+
warnings=["Downgrade not supported. Please use a compatible Gradia version."]
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
# Apply migrations sequentially
|
|
136
|
+
for i in range(start_idx, end_idx):
|
|
137
|
+
v_from = version_order[i]
|
|
138
|
+
v_to = version_order[i + 1]
|
|
139
|
+
key = (v_from, v_to)
|
|
140
|
+
|
|
141
|
+
if key in self.migrations:
|
|
142
|
+
migration_fn = self.migrations[key]
|
|
143
|
+
step_changes, step_warnings = migration_fn(config)
|
|
144
|
+
changes.extend(step_changes)
|
|
145
|
+
warnings.extend(step_warnings)
|
|
146
|
+
|
|
147
|
+
# Set version marker
|
|
148
|
+
config["schema_version"] = target_version
|
|
149
|
+
|
|
150
|
+
return MigrationResult(
|
|
151
|
+
success=True,
|
|
152
|
+
from_version=from_version,
|
|
153
|
+
to_version=target_version,
|
|
154
|
+
changes=changes,
|
|
155
|
+
warnings=warnings
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
def _migrate_1_0_to_1_1(self, config: Dict) -> tuple:
|
|
159
|
+
"""Add model.params if missing."""
|
|
160
|
+
changes = []
|
|
161
|
+
warnings = []
|
|
162
|
+
|
|
163
|
+
if "model" not in config:
|
|
164
|
+
config["model"] = {"type": "auto", "params": {}}
|
|
165
|
+
changes.append("Added model config with defaults")
|
|
166
|
+
elif "params" not in config["model"]:
|
|
167
|
+
config["model"]["params"] = {}
|
|
168
|
+
changes.append("Added model.params")
|
|
169
|
+
|
|
170
|
+
return changes, warnings
|
|
171
|
+
|
|
172
|
+
def _migrate_1_1_to_1_2(self, config: Dict) -> tuple:
|
|
173
|
+
"""Add training.epochs default."""
|
|
174
|
+
changes = []
|
|
175
|
+
warnings = []
|
|
176
|
+
|
|
177
|
+
if "training" not in config:
|
|
178
|
+
config["training"] = {
|
|
179
|
+
"test_split": 0.2,
|
|
180
|
+
"random_seed": 42,
|
|
181
|
+
"shuffle": True,
|
|
182
|
+
"epochs": 10
|
|
183
|
+
}
|
|
184
|
+
changes.append("Added training config with defaults")
|
|
185
|
+
elif "epochs" not in config["training"]:
|
|
186
|
+
config["training"]["epochs"] = 10
|
|
187
|
+
changes.append("Added training.epochs=10 default")
|
|
188
|
+
|
|
189
|
+
return changes, warnings
|
|
190
|
+
|
|
191
|
+
def _migrate_1_2_to_1_3(self, config: Dict) -> tuple:
|
|
192
|
+
"""Add project_name and save_model."""
|
|
193
|
+
changes = []
|
|
194
|
+
warnings = []
|
|
195
|
+
|
|
196
|
+
if "project_name" not in config:
|
|
197
|
+
config["project_name"] = "experiment"
|
|
198
|
+
changes.append("Added project_name default")
|
|
199
|
+
|
|
200
|
+
if "save_model" not in config:
|
|
201
|
+
config["save_model"] = False
|
|
202
|
+
changes.append("Added save_model=False default")
|
|
203
|
+
|
|
204
|
+
return changes, warnings
|
|
205
|
+
|
|
206
|
+
def _migrate_1_3_to_2_0(self, config: Dict) -> tuple:
|
|
207
|
+
"""Add v2.0 timeline configuration."""
|
|
208
|
+
changes = []
|
|
209
|
+
warnings = []
|
|
210
|
+
|
|
211
|
+
if "timeline" not in config:
|
|
212
|
+
config["timeline"] = {
|
|
213
|
+
"enabled": True,
|
|
214
|
+
"max_samples": 100,
|
|
215
|
+
"user_samples": None
|
|
216
|
+
}
|
|
217
|
+
changes.append("Added timeline config for Learning Timeline feature")
|
|
218
|
+
|
|
219
|
+
# Ensure schema version is set
|
|
220
|
+
config["schema_version"] = "2.0"
|
|
221
|
+
changes.append("Set schema_version to 2.0")
|
|
222
|
+
|
|
223
|
+
return changes, warnings
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class RunMigrator:
|
|
227
|
+
"""
|
|
228
|
+
Handles migration of existing .gradia_logs run directories.
|
|
229
|
+
"""
|
|
230
|
+
|
|
231
|
+
def __init__(self, run_dir: Path):
|
|
232
|
+
self.run_dir = Path(run_dir)
|
|
233
|
+
self.schema_migrator = SchemaMigrator()
|
|
234
|
+
|
|
235
|
+
def needs_migration(self) -> bool:
|
|
236
|
+
"""Check if run directory needs migration."""
|
|
237
|
+
config = self._load_config()
|
|
238
|
+
if config is None:
|
|
239
|
+
return False
|
|
240
|
+
|
|
241
|
+
version = self.schema_migrator.detect_version(config)
|
|
242
|
+
return version != SchemaVersion.current().value
|
|
243
|
+
|
|
244
|
+
def migrate(self) -> MigrationResult:
|
|
245
|
+
"""Migrate the run directory to current schema."""
|
|
246
|
+
config = self._load_config()
|
|
247
|
+
|
|
248
|
+
if config is None:
|
|
249
|
+
return MigrationResult(
|
|
250
|
+
success=False,
|
|
251
|
+
from_version="unknown",
|
|
252
|
+
to_version=SchemaVersion.current().value,
|
|
253
|
+
changes=[],
|
|
254
|
+
warnings=["No config.yaml found in run directory"]
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
result = self.schema_migrator.migrate(config)
|
|
258
|
+
|
|
259
|
+
if result.success:
|
|
260
|
+
self._save_config(config)
|
|
261
|
+
|
|
262
|
+
# Create migration marker file
|
|
263
|
+
marker_path = self.run_dir / ".migrated"
|
|
264
|
+
marker_path.write_text(f"Migrated from {result.from_version} to {result.to_version}")
|
|
265
|
+
|
|
266
|
+
return result
|
|
267
|
+
|
|
268
|
+
def _load_config(self) -> Optional[Dict]:
|
|
269
|
+
"""Load config.yaml from run directory."""
|
|
270
|
+
config_path = self.run_dir / "config.yaml"
|
|
271
|
+
|
|
272
|
+
if not config_path.exists():
|
|
273
|
+
return None
|
|
274
|
+
|
|
275
|
+
with open(config_path, 'r') as f:
|
|
276
|
+
return yaml.safe_load(f)
|
|
277
|
+
|
|
278
|
+
def _save_config(self, config: Dict):
|
|
279
|
+
"""Save config.yaml to run directory."""
|
|
280
|
+
config_path = self.run_dir / "config.yaml"
|
|
281
|
+
|
|
282
|
+
with open(config_path, 'w') as f:
|
|
283
|
+
yaml.dump(config, f)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def migrate_all_runs(base_dir: Path = None) -> List[MigrationResult]:
|
|
287
|
+
"""
|
|
288
|
+
Migrate all run directories in .gradia_logs.
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
base_dir: Base directory containing .gradia_logs (default: cwd)
|
|
292
|
+
|
|
293
|
+
Returns:
|
|
294
|
+
List of migration results
|
|
295
|
+
"""
|
|
296
|
+
if base_dir is None:
|
|
297
|
+
base_dir = Path.cwd()
|
|
298
|
+
|
|
299
|
+
logs_dir = base_dir / ".gradia_logs"
|
|
300
|
+
results = []
|
|
301
|
+
|
|
302
|
+
if not logs_dir.exists():
|
|
303
|
+
return results
|
|
304
|
+
|
|
305
|
+
for run_dir in logs_dir.iterdir():
|
|
306
|
+
if run_dir.is_dir() and run_dir.name.startswith("run_"):
|
|
307
|
+
migrator = RunMigrator(run_dir)
|
|
308
|
+
if migrator.needs_migration():
|
|
309
|
+
result = migrator.migrate()
|
|
310
|
+
results.append(result)
|
|
311
|
+
print(f"Migrated {run_dir.name}: {result.from_version} → {result.to_version}")
|
|
312
|
+
|
|
313
|
+
return results
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def ensure_v2_config(config: Dict[str, Any]) -> Dict[str, Any]:
|
|
317
|
+
"""
|
|
318
|
+
Ensure config has all v2.0 fields with sensible defaults.
|
|
319
|
+
|
|
320
|
+
Use this when loading configs to guarantee v2 compatibility.
|
|
321
|
+
"""
|
|
322
|
+
migrator = SchemaMigrator()
|
|
323
|
+
migrator.migrate(config)
|
|
324
|
+
return config
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Gradia Events Module (v2.0.0)
|
|
3
|
+
|
|
4
|
+
Core abstraction for sample-level learning events that power the Learning Timeline.
|
|
5
|
+
Decouples training logic from visualization and storage.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .models import LearningEvent, EventType
|
|
9
|
+
from .tracker import SampleTracker
|
|
10
|
+
from .logger import TimelineLogger
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"LearningEvent",
|
|
14
|
+
"EventType",
|
|
15
|
+
"SampleTracker",
|
|
16
|
+
"TimelineLogger",
|
|
17
|
+
]
|