extract-tracker 0.2.2__tar.gz → 0.2.3__tar.gz
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.
- {extract_tracker-0.2.2 → extract_tracker-0.2.3}/PKG-INFO +1 -1
- {extract_tracker-0.2.2 → extract_tracker-0.2.3}/manual/mcp.md +24 -0
- {extract_tracker-0.2.2 → extract_tracker-0.2.3}/pyproject.toml +1 -1
- {extract_tracker-0.2.2 → extract_tracker-0.2.3}/python/src/extract/__main__.py +38 -13
- extract_tracker-0.2.3/python/src/extract/cli_query.py +223 -0
- {extract_tracker-0.2.2 → extract_tracker-0.2.3}/python/src/extract/mcp.py +165 -87
- {extract_tracker-0.2.2 → extract_tracker-0.2.3}/python/src/extract_tracker.egg-info/PKG-INFO +1 -1
- {extract_tracker-0.2.2 → extract_tracker-0.2.3}/python/src/extract_tracker.egg-info/SOURCES.txt +1 -0
- {extract_tracker-0.2.2 → extract_tracker-0.2.3}/rust/Cargo.lock +1 -1
- {extract_tracker-0.2.2 → extract_tracker-0.2.3}/rust/Cargo.toml +1 -1
- {extract_tracker-0.2.2 → extract_tracker-0.2.3}/MANIFEST.in +0 -0
- {extract_tracker-0.2.2 → extract_tracker-0.2.3}/README.md +0 -0
- {extract_tracker-0.2.2 → extract_tracker-0.2.3}/manual/config.md +0 -0
- {extract_tracker-0.2.2 → extract_tracker-0.2.3}/manual/packaging.md +0 -0
- {extract_tracker-0.2.2 → extract_tracker-0.2.3}/manual/sync.md +0 -0
- {extract_tracker-0.2.2 → extract_tracker-0.2.3}/manual/tui.md +0 -0
- {extract_tracker-0.2.2 → extract_tracker-0.2.3}/manual/usage.md +0 -0
- {extract_tracker-0.2.2 → extract_tracker-0.2.3}/python/src/extract/__init__.py +0 -0
- {extract_tracker-0.2.2 → extract_tracker-0.2.3}/python/src/extract/experiment.py +0 -0
- {extract_tracker-0.2.2 → extract_tracker-0.2.3}/python/src/extract/init.py +0 -0
- {extract_tracker-0.2.2 → extract_tracker-0.2.3}/python/src/extract/metrics.py +0 -0
- {extract_tracker-0.2.2 → extract_tracker-0.2.3}/python/src/extract/release_versioning.py +0 -0
- {extract_tracker-0.2.2 → extract_tracker-0.2.3}/python/src/extract/run.py +0 -0
- {extract_tracker-0.2.2 → extract_tracker-0.2.3}/python/src/extract/store.py +0 -0
- {extract_tracker-0.2.2 → extract_tracker-0.2.3}/python/src/extract/sync.py +0 -0
- {extract_tracker-0.2.2 → extract_tracker-0.2.3}/python/src/extract_tracker.egg-info/dependency_links.txt +0 -0
- {extract_tracker-0.2.2 → extract_tracker-0.2.3}/python/src/extract_tracker.egg-info/entry_points.txt +0 -0
- {extract_tracker-0.2.2 → extract_tracker-0.2.3}/python/src/extract_tracker.egg-info/requires.txt +0 -0
- {extract_tracker-0.2.2 → extract_tracker-0.2.3}/python/src/extract_tracker.egg-info/top_level.txt +0 -0
- {extract_tracker-0.2.2 → extract_tracker-0.2.3}/rust/src/app.rs +0 -0
- {extract_tracker-0.2.2 → extract_tracker-0.2.3}/rust/src/artifact.rs +0 -0
- {extract_tracker-0.2.2 → extract_tracker-0.2.3}/rust/src/config.rs +0 -0
- {extract_tracker-0.2.2 → extract_tracker-0.2.3}/rust/src/db.rs +0 -0
- {extract_tracker-0.2.2 → extract_tracker-0.2.3}/rust/src/event.rs +0 -0
- {extract_tracker-0.2.2 → extract_tracker-0.2.3}/rust/src/keys.rs +0 -0
- {extract_tracker-0.2.2 → extract_tracker-0.2.3}/rust/src/main.rs +0 -0
- {extract_tracker-0.2.2 → extract_tracker-0.2.3}/rust/src/model.rs +0 -0
- {extract_tracker-0.2.2 → extract_tracker-0.2.3}/rust/src/ui/chart.rs +0 -0
- {extract_tracker-0.2.2 → extract_tracker-0.2.3}/rust/src/ui/compare.rs +0 -0
- {extract_tracker-0.2.2 → extract_tracker-0.2.3}/rust/src/ui/dashboard.rs +0 -0
- {extract_tracker-0.2.2 → extract_tracker-0.2.3}/rust/src/ui/detail.rs +0 -0
- {extract_tracker-0.2.2 → extract_tracker-0.2.3}/rust/src/ui/diff.rs +0 -0
- {extract_tracker-0.2.2 → extract_tracker-0.2.3}/rust/src/ui/heatmap.rs +0 -0
- {extract_tracker-0.2.2 → extract_tracker-0.2.3}/rust/src/ui/help.rs +0 -0
- {extract_tracker-0.2.2 → extract_tracker-0.2.3}/rust/src/ui/layout.rs +0 -0
- {extract_tracker-0.2.2 → extract_tracker-0.2.3}/rust/src/ui/lineage.rs +0 -0
- {extract_tracker-0.2.2 → extract_tracker-0.2.3}/rust/src/ui/mod.rs +0 -0
- {extract_tracker-0.2.2 → extract_tracker-0.2.3}/rust/src/ui/popup.rs +0 -0
- {extract_tracker-0.2.2 → extract_tracker-0.2.3}/rust/src/ui/registry.rs +0 -0
- {extract_tracker-0.2.2 → extract_tracker-0.2.3}/rust/src/ui/search.rs +0 -0
- {extract_tracker-0.2.2 → extract_tracker-0.2.3}/rust/src/ui/selection.rs +0 -0
- {extract_tracker-0.2.2 → extract_tracker-0.2.3}/rust/src/ui/statusbar.rs +0 -0
- {extract_tracker-0.2.2 → extract_tracker-0.2.3}/rust/src/ui/summary.rs +0 -0
- {extract_tracker-0.2.2 → extract_tracker-0.2.3}/rust/src/ui/theme.rs +0 -0
- {extract_tracker-0.2.2 → extract_tracker-0.2.3}/rust/src/ui/todo.rs +0 -0
- {extract_tracker-0.2.2 → extract_tracker-0.2.3}/rust/src/ui/tree.rs +0 -0
- {extract_tracker-0.2.2 → extract_tracker-0.2.3}/setup.cfg +0 -0
- {extract_tracker-0.2.2 → extract_tracker-0.2.3}/setup.py +0 -0
|
@@ -47,3 +47,27 @@ All tools are read-only.
|
|
|
47
47
|
| `list_models` | Registered models with metadata |
|
|
48
48
|
|
|
49
49
|
Full schemas, response shapes, and error catalog live in [`DOC.md`](../DOC.md#mcp-server).
|
|
50
|
+
|
|
51
|
+
## Agent CLI surface
|
|
52
|
+
|
|
53
|
+
The same read-only operations are also exposed as JSON CLI commands for agents or hosts that cannot attach the MCP server:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
extract experiments list --store .extract --format json
|
|
57
|
+
extract runs list --limit 20 --store .extract --format json
|
|
58
|
+
extract runs get RUN_ID --store .extract --format json
|
|
59
|
+
extract runs compare RUN_A RUN_B --store .extract --format json
|
|
60
|
+
extract search --query resnet --tag production-candidate --store .extract --format json
|
|
61
|
+
extract todos list --scope-type run --scope-id RUN_ID --store .extract --format json
|
|
62
|
+
extract lineage get run RUN_ID --direction both --store .extract --format json
|
|
63
|
+
extract models list --store .extract --format json
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
An installable Pi package in `pi/extract-pi-tools/` wraps these commands as native Pi tools such as `extract_list_runs`, `extract_get_run`, and `extract_compare_runs`:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
pi install ./pi/extract-pi-tools # user-scoped
|
|
70
|
+
pi install -l ./pi/extract-pi-tools # project-scoped
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Reload or restart Pi after installation. These native tools are context-safe summaries, not raw dumps: list tools cap rows, `extract_get_run` omits full config unless specific `configKeys` are requested, and large raw JSON is written to a temp file path. For full configs, curves, or bulk aggregation, rerun the returned `meta.command` inside `ctx_execute` and print only the derived answer.
|
|
@@ -29,7 +29,9 @@ def _find_tui_binary() -> str | None:
|
|
|
29
29
|
return None
|
|
30
30
|
|
|
31
31
|
|
|
32
|
-
def _print_merge_stats(
|
|
32
|
+
def _print_merge_stats(
|
|
33
|
+
verb: str, src: object, dst: object, stats: dict[str, int]
|
|
34
|
+
) -> None:
|
|
33
35
|
print(f"{verb} {src} → {dst}")
|
|
34
36
|
exps = stats.get("experiments", 0)
|
|
35
37
|
runs = stats.get("runs", 0)
|
|
@@ -45,29 +47,39 @@ def _print_merge_stats(verb: str, src: object, dst: object, stats: dict[str, int
|
|
|
45
47
|
|
|
46
48
|
|
|
47
49
|
def main(argv: list[str] | None = None) -> None:
|
|
48
|
-
parser = argparse.ArgumentParser(
|
|
50
|
+
parser = argparse.ArgumentParser(
|
|
51
|
+
prog="extract", description="Extract experiment tracker"
|
|
52
|
+
)
|
|
49
53
|
sub = parser.add_subparsers(dest="command")
|
|
50
54
|
|
|
55
|
+
from extract.cli_query import add_query_parsers, run_query_command
|
|
56
|
+
|
|
57
|
+
add_query_parsers(sub)
|
|
58
|
+
|
|
51
59
|
# --- tui ---
|
|
52
60
|
tui_parser = sub.add_parser("tui", help="Launch the TUI explorer")
|
|
53
|
-
tui_parser.add_argument(
|
|
61
|
+
tui_parser.add_argument(
|
|
62
|
+
"--store", default=".extract", help="Path to .extract/ directory"
|
|
63
|
+
)
|
|
54
64
|
|
|
55
65
|
# --- init ---
|
|
56
66
|
init_parser = sub.add_parser(
|
|
57
67
|
"init", help="Bootstrap a .extract/ store with a hierarchy"
|
|
58
68
|
)
|
|
59
69
|
init_parser.add_argument(
|
|
60
|
-
"path",
|
|
61
|
-
|
|
70
|
+
"path",
|
|
71
|
+
nargs="?",
|
|
72
|
+
default=".extract",
|
|
73
|
+
help="Path to create the store at (default: .extract)",
|
|
62
74
|
)
|
|
63
75
|
init_parser.add_argument(
|
|
64
|
-
"--hierarchy",
|
|
76
|
+
"--hierarchy",
|
|
77
|
+
default=None,
|
|
65
78
|
help="Skip the interactive picker; use this hierarchy "
|
|
66
|
-
|
|
79
|
+
"(e.g. 'benchmark > model > variant')",
|
|
67
80
|
)
|
|
68
81
|
init_parser.add_argument(
|
|
69
|
-
"--no-gitignore", action="store_true",
|
|
70
|
-
help="Do not add .extract/ to .gitignore"
|
|
82
|
+
"--no-gitignore", action="store_true", help="Do not add .extract/ to .gitignore"
|
|
71
83
|
)
|
|
72
84
|
|
|
73
85
|
# --- sync ---
|
|
@@ -84,14 +96,24 @@ def main(argv: list[str] | None = None) -> None:
|
|
|
84
96
|
|
|
85
97
|
export_p = sync_sub.add_parser("export", help="Archive .extract/ to tar.gz")
|
|
86
98
|
export_p.add_argument("output", help="Output archive path, e.g. experiments.tar.gz")
|
|
87
|
-
export_p.add_argument(
|
|
99
|
+
export_p.add_argument(
|
|
100
|
+
"--root", default=".extract", help="Local .extract/ directory"
|
|
101
|
+
)
|
|
88
102
|
|
|
89
|
-
import_p = sync_sub.add_parser(
|
|
103
|
+
import_p = sync_sub.add_parser(
|
|
104
|
+
"import", help="Import tar.gz archive into .extract/"
|
|
105
|
+
)
|
|
90
106
|
import_p.add_argument("archive", help="Archive path to import")
|
|
91
|
-
import_p.add_argument(
|
|
107
|
+
import_p.add_argument(
|
|
108
|
+
"--root", default=".extract", help="Target .extract/ directory"
|
|
109
|
+
)
|
|
92
110
|
|
|
93
111
|
args = parser.parse_args(argv)
|
|
94
112
|
|
|
113
|
+
query_exit = run_query_command(args)
|
|
114
|
+
if query_exit is not None:
|
|
115
|
+
sys.exit(query_exit)
|
|
116
|
+
|
|
95
117
|
if args.command == "tui":
|
|
96
118
|
binary = _find_tui_binary()
|
|
97
119
|
if binary is None:
|
|
@@ -105,7 +127,10 @@ def main(argv: list[str] | None = None) -> None:
|
|
|
105
127
|
os.execvp(binary, [binary, "--store", args.store])
|
|
106
128
|
|
|
107
129
|
elif args.command == "init":
|
|
108
|
-
from extract import
|
|
130
|
+
from extract import (
|
|
131
|
+
init,
|
|
132
|
+
) # lazy import — pulls in rich/questionary only when needed
|
|
133
|
+
|
|
109
134
|
sys.exit(init.run(args))
|
|
110
135
|
|
|
111
136
|
elif args.command == "sync":
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"""JSON CLI adapter for Extract read-only query tools.
|
|
2
|
+
|
|
3
|
+
This module reuses the existing MCP tool implementations so the command-line
|
|
4
|
+
surface and MCP surface share one result contract while the query core is
|
|
5
|
+
factored out in a later cleanup.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import json
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from extract.store import Store
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _add_common(parser: argparse.ArgumentParser) -> None:
|
|
20
|
+
parser.add_argument(
|
|
21
|
+
"--store", default=".extract", help="Path to .extract/ directory"
|
|
22
|
+
)
|
|
23
|
+
parser.add_argument(
|
|
24
|
+
"--format",
|
|
25
|
+
choices=("json",),
|
|
26
|
+
default="json",
|
|
27
|
+
help="Output format (default: json)",
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def add_query_parsers(sub: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
|
|
32
|
+
"""Register read-only query subcommands on the top-level CLI parser."""
|
|
33
|
+
experiments = sub.add_parser("experiments", help="Read experiments from a store")
|
|
34
|
+
exp_sub = experiments.add_subparsers(dest="query_action")
|
|
35
|
+
exp_list = exp_sub.add_parser("list", help="List experiments")
|
|
36
|
+
_add_common(exp_list)
|
|
37
|
+
exp_list.add_argument("--prefix", default="", help="Experiment path prefix")
|
|
38
|
+
exp_list.add_argument("--limit", type=int, default=50, help="Max rows (max 500)")
|
|
39
|
+
exp_list.add_argument("--include-archived", action="store_true")
|
|
40
|
+
exp_list.set_defaults(_extract_query="list_experiments")
|
|
41
|
+
|
|
42
|
+
runs = sub.add_parser("runs", help="Read runs from a store")
|
|
43
|
+
runs_sub = runs.add_subparsers(dest="query_action")
|
|
44
|
+
runs_list = runs_sub.add_parser("list", help="List runs")
|
|
45
|
+
_add_common(runs_list)
|
|
46
|
+
runs_list.add_argument(
|
|
47
|
+
"--experiment-id", default=None, help="Scope to one experiment id"
|
|
48
|
+
)
|
|
49
|
+
runs_list.add_argument("--limit", type=int, default=50, help="Max rows (max 500)")
|
|
50
|
+
runs_list.add_argument("--include-archived", action="store_true")
|
|
51
|
+
runs_list.set_defaults(_extract_query="list_runs")
|
|
52
|
+
|
|
53
|
+
runs_get = runs_sub.add_parser("get", help="Get full run detail")
|
|
54
|
+
_add_common(runs_get)
|
|
55
|
+
runs_get.add_argument("run_id", help="Run ULID")
|
|
56
|
+
runs_get.set_defaults(_extract_query="get_run")
|
|
57
|
+
|
|
58
|
+
runs_compare = runs_sub.add_parser("compare", help="Compare 2-10 runs")
|
|
59
|
+
_add_common(runs_compare)
|
|
60
|
+
runs_compare.add_argument("run_ids", nargs="+", help="Run ULIDs")
|
|
61
|
+
runs_compare.add_argument("--include-curves", action="store_true")
|
|
62
|
+
runs_compare.set_defaults(_extract_query="compare_runs")
|
|
63
|
+
|
|
64
|
+
search = sub.add_parser("search", help="Search runs")
|
|
65
|
+
_add_common(search)
|
|
66
|
+
search.add_argument("--query", default="", help="Case-insensitive text search")
|
|
67
|
+
search.add_argument("--tag", default=None, help="Require tag")
|
|
68
|
+
search.add_argument("--status", default=None, help="Require status")
|
|
69
|
+
search.add_argument(
|
|
70
|
+
"--experiment-prefix", default=None, help="Experiment path prefix"
|
|
71
|
+
)
|
|
72
|
+
search.add_argument(
|
|
73
|
+
"--started-after", default=None, help="ISO timestamp lower bound"
|
|
74
|
+
)
|
|
75
|
+
search.add_argument(
|
|
76
|
+
"--started-before", default=None, help="ISO timestamp upper bound"
|
|
77
|
+
)
|
|
78
|
+
search.add_argument("--limit", type=int, default=50, help="Max rows (max 500)")
|
|
79
|
+
search.add_argument("--include-archived", action="store_true")
|
|
80
|
+
search.set_defaults(_extract_query="search")
|
|
81
|
+
|
|
82
|
+
todos = sub.add_parser("todos", help="Read TODOs from a store")
|
|
83
|
+
todos_sub = todos.add_subparsers(dest="query_action")
|
|
84
|
+
todos_list = todos_sub.add_parser("list", help="List TODOs")
|
|
85
|
+
_add_common(todos_list)
|
|
86
|
+
todos_list.add_argument(
|
|
87
|
+
"--scope-type",
|
|
88
|
+
default="global",
|
|
89
|
+
choices=("global", "experiment", "run"),
|
|
90
|
+
help="TODO scope",
|
|
91
|
+
)
|
|
92
|
+
todos_list.add_argument(
|
|
93
|
+
"--scope-id", default=None, help="Required for experiment/run scope"
|
|
94
|
+
)
|
|
95
|
+
todos_list.add_argument("--include-done", action="store_true")
|
|
96
|
+
todos_list.add_argument("--limit", type=int, default=50, help="Max rows (max 500)")
|
|
97
|
+
todos_list.set_defaults(_extract_query="list_todos")
|
|
98
|
+
|
|
99
|
+
lineage = sub.add_parser("lineage", help="Read lineage graph from a store")
|
|
100
|
+
lineage_sub = lineage.add_subparsers(dest="query_action")
|
|
101
|
+
lineage_get = lineage_sub.add_parser("get", help="Walk lineage DAG")
|
|
102
|
+
_add_common(lineage_get)
|
|
103
|
+
lineage_get.add_argument("node_type", choices=("experiment", "run", "model"))
|
|
104
|
+
lineage_get.add_argument("node_id")
|
|
105
|
+
lineage_get.add_argument(
|
|
106
|
+
"--direction",
|
|
107
|
+
default="both",
|
|
108
|
+
choices=("ancestors", "descendants", "both"),
|
|
109
|
+
)
|
|
110
|
+
lineage_get.add_argument("--depth", type=int, default=2)
|
|
111
|
+
lineage_get.set_defaults(_extract_query="get_lineage")
|
|
112
|
+
|
|
113
|
+
models = sub.add_parser("models", help="Read model registry from a store")
|
|
114
|
+
models_sub = models.add_subparsers(dest="query_action")
|
|
115
|
+
models_list = models_sub.add_parser("list", help="List registered models")
|
|
116
|
+
_add_common(models_list)
|
|
117
|
+
models_list.add_argument("--name-prefix", default="", help="Model name prefix")
|
|
118
|
+
models_list.add_argument("--limit", type=int, default=50, help="Max rows (max 500)")
|
|
119
|
+
models_list.set_defaults(_extract_query="list_models")
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _json_error(code: str, message: str) -> str:
|
|
123
|
+
return json.dumps(
|
|
124
|
+
{"error": {"code": code, "message": message}}, indent=2, sort_keys=True
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _build_filters(args: argparse.Namespace) -> dict[str, str]:
|
|
129
|
+
filters: dict[str, str] = {}
|
|
130
|
+
for attr, key in (
|
|
131
|
+
("tag", "tag"),
|
|
132
|
+
("status", "status"),
|
|
133
|
+
("experiment_prefix", "experiment_prefix"),
|
|
134
|
+
("started_after", "started_after"),
|
|
135
|
+
("started_before", "started_before"),
|
|
136
|
+
):
|
|
137
|
+
value = getattr(args, attr, None)
|
|
138
|
+
if value is not None:
|
|
139
|
+
filters[key] = value
|
|
140
|
+
return filters
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _call_query(args: argparse.Namespace) -> Any:
|
|
144
|
+
import extract.mcp as mcp_mod
|
|
145
|
+
|
|
146
|
+
action = args._extract_query
|
|
147
|
+
if action == "list_experiments":
|
|
148
|
+
return mcp_mod.list_experiments(
|
|
149
|
+
prefix=args.prefix,
|
|
150
|
+
limit=args.limit,
|
|
151
|
+
include_archived=args.include_archived,
|
|
152
|
+
)
|
|
153
|
+
if action == "list_runs":
|
|
154
|
+
return mcp_mod.list_runs(
|
|
155
|
+
experiment_id=args.experiment_id,
|
|
156
|
+
limit=args.limit,
|
|
157
|
+
include_archived=args.include_archived,
|
|
158
|
+
)
|
|
159
|
+
if action == "get_run":
|
|
160
|
+
return mcp_mod.get_run(args.run_id)
|
|
161
|
+
if action == "compare_runs":
|
|
162
|
+
return mcp_mod.compare_runs(args.run_ids, include_curves=args.include_curves)
|
|
163
|
+
if action == "search":
|
|
164
|
+
return mcp_mod.search(
|
|
165
|
+
query=args.query,
|
|
166
|
+
filters=_build_filters(args),
|
|
167
|
+
limit=args.limit,
|
|
168
|
+
include_archived=args.include_archived,
|
|
169
|
+
)
|
|
170
|
+
if action == "list_todos":
|
|
171
|
+
return mcp_mod.list_todos(
|
|
172
|
+
scope_type=args.scope_type,
|
|
173
|
+
scope_id=args.scope_id,
|
|
174
|
+
include_done=args.include_done,
|
|
175
|
+
limit=args.limit,
|
|
176
|
+
)
|
|
177
|
+
if action == "get_lineage":
|
|
178
|
+
return mcp_mod.get_lineage(
|
|
179
|
+
node_type=args.node_type,
|
|
180
|
+
node_id=args.node_id,
|
|
181
|
+
direction=args.direction,
|
|
182
|
+
depth=args.depth,
|
|
183
|
+
)
|
|
184
|
+
if action == "list_models":
|
|
185
|
+
return mcp_mod.list_models(name_prefix=args.name_prefix, limit=args.limit)
|
|
186
|
+
raise RuntimeError(f"unknown query action: {action}")
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def run_query_command(args: argparse.Namespace) -> int | None:
|
|
190
|
+
"""Run a query command if args selects one; else return None."""
|
|
191
|
+
if not hasattr(args, "_extract_query"):
|
|
192
|
+
return None
|
|
193
|
+
|
|
194
|
+
store_path = Path(args.store)
|
|
195
|
+
if not store_path.exists():
|
|
196
|
+
print(
|
|
197
|
+
_json_error(
|
|
198
|
+
"store_not_found",
|
|
199
|
+
f"store not found: {store_path} — run training with extract-tracker first, or pass --store",
|
|
200
|
+
),
|
|
201
|
+
file=sys.stdout,
|
|
202
|
+
)
|
|
203
|
+
return 2
|
|
204
|
+
|
|
205
|
+
import extract.mcp as mcp_mod
|
|
206
|
+
|
|
207
|
+
store: Store | None = None
|
|
208
|
+
try:
|
|
209
|
+
store = Store(store_path)
|
|
210
|
+
mcp_mod._store = store
|
|
211
|
+
result = _call_query(args)
|
|
212
|
+
print(json.dumps(result, indent=2, sort_keys=True))
|
|
213
|
+
return 0
|
|
214
|
+
except ValueError as exc:
|
|
215
|
+
print(_json_error("invalid_request", str(exc)), file=sys.stdout)
|
|
216
|
+
return 1
|
|
217
|
+
except Exception as exc: # pragma: no cover - defensive CLI boundary
|
|
218
|
+
print(_json_error("internal_error", str(exc)), file=sys.stdout)
|
|
219
|
+
return 70
|
|
220
|
+
finally:
|
|
221
|
+
mcp_mod._store = None
|
|
222
|
+
if store is not None:
|
|
223
|
+
store.close()
|
|
@@ -12,9 +12,9 @@ import json
|
|
|
12
12
|
import sys
|
|
13
13
|
from pathlib import Path
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
import tomli
|
|
17
16
|
from extract.store import Store
|
|
17
|
+
from mcp.server.fastmcp import FastMCP
|
|
18
18
|
|
|
19
19
|
# Module-level state. Set by main() at startup; monkey-patched by tests.
|
|
20
20
|
_store: Store | None = None
|
|
@@ -31,8 +31,17 @@ def _tool(fn):
|
|
|
31
31
|
# ----------------------------------------------------------------------
|
|
32
32
|
|
|
33
33
|
_MIN_METRIC_PATTERNS = (
|
|
34
|
-
"loss",
|
|
35
|
-
"
|
|
34
|
+
"loss",
|
|
35
|
+
"error",
|
|
36
|
+
"perplexity",
|
|
37
|
+
"mse",
|
|
38
|
+
"mae",
|
|
39
|
+
"rmse",
|
|
40
|
+
"nll",
|
|
41
|
+
"cer",
|
|
42
|
+
"wer",
|
|
43
|
+
"fid",
|
|
44
|
+
"divergence",
|
|
36
45
|
)
|
|
37
46
|
|
|
38
47
|
|
|
@@ -70,8 +79,18 @@ def _flatten_config(config: dict, prefix: str = "") -> dict:
|
|
|
70
79
|
return result
|
|
71
80
|
|
|
72
81
|
|
|
73
|
-
def _metric_direction(
|
|
74
|
-
|
|
82
|
+
def _metric_direction(
|
|
83
|
+
name: str,
|
|
84
|
+
overrides: tuple[set[str], set[str]] | None = None,
|
|
85
|
+
) -> str:
|
|
86
|
+
"""Return 'min' or 'max' using config overrides, then heuristics."""
|
|
87
|
+
if overrides is not None:
|
|
88
|
+
minimize, maximize = overrides
|
|
89
|
+
if name in minimize:
|
|
90
|
+
return "min"
|
|
91
|
+
if name in maximize:
|
|
92
|
+
return "max"
|
|
93
|
+
|
|
75
94
|
lowered = name.lower()
|
|
76
95
|
for pat in _MIN_METRIC_PATTERNS:
|
|
77
96
|
if pat in lowered:
|
|
@@ -79,6 +98,35 @@ def _metric_direction(name: str) -> str:
|
|
|
79
98
|
return "max"
|
|
80
99
|
|
|
81
100
|
|
|
101
|
+
def _metric_direction_overrides(store: Store) -> tuple[set[str], set[str]]:
|
|
102
|
+
"""Read [metrics] minimize/maximize overrides from config.toml."""
|
|
103
|
+
config_path = store.root / "config.toml"
|
|
104
|
+
if not config_path.exists():
|
|
105
|
+
return set(), set()
|
|
106
|
+
|
|
107
|
+
with config_path.open("rb") as f:
|
|
108
|
+
data = tomli.load(f)
|
|
109
|
+
|
|
110
|
+
metrics = data.get("metrics", {})
|
|
111
|
+
if not isinstance(metrics, dict):
|
|
112
|
+
return set(), set()
|
|
113
|
+
|
|
114
|
+
minimize = metrics.get("minimize", [])
|
|
115
|
+
maximize = metrics.get("maximize", [])
|
|
116
|
+
return (
|
|
117
|
+
(
|
|
118
|
+
{m for m in minimize if isinstance(m, str)}
|
|
119
|
+
if isinstance(minimize, list)
|
|
120
|
+
else set()
|
|
121
|
+
),
|
|
122
|
+
(
|
|
123
|
+
{m for m in maximize if isinstance(m, str)}
|
|
124
|
+
if isinstance(maximize, list)
|
|
125
|
+
else set()
|
|
126
|
+
),
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
82
130
|
def _config_diffs(runs_configs: list[tuple[str, dict]]) -> dict:
|
|
83
131
|
"""Return {flat_key: {run_id: value}} for keys that differ across runs.
|
|
84
132
|
|
|
@@ -96,8 +144,9 @@ def _config_diffs(runs_configs: list[tuple[str, dict]]) -> dict:
|
|
|
96
144
|
result: dict = {}
|
|
97
145
|
for key in all_keys:
|
|
98
146
|
values = [(rid, flat.get(key, _MISSING)) for rid, flat in flattened]
|
|
99
|
-
distinct = {
|
|
100
|
-
|
|
147
|
+
distinct = {
|
|
148
|
+
id(v) if v is _MISSING else (type(v).__name__, repr(v)) for _, v in values
|
|
149
|
+
}
|
|
101
150
|
if len(distinct) > 1:
|
|
102
151
|
result[key] = {rid: v for rid, v in values if v is not _MISSING}
|
|
103
152
|
return result
|
|
@@ -193,14 +242,16 @@ def list_experiments(
|
|
|
193
242
|
f"SELECT COUNT(*) FROM runs WHERE experiment_id = ?{run_filter}",
|
|
194
243
|
(row["id"],),
|
|
195
244
|
).fetchone()
|
|
196
|
-
items.append(
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
245
|
+
items.append(
|
|
246
|
+
{
|
|
247
|
+
"id": row["id"],
|
|
248
|
+
"path": row["path"],
|
|
249
|
+
"name": row["name"],
|
|
250
|
+
"node_type": row["node_type"],
|
|
251
|
+
"parent_id": row["parent_id"],
|
|
252
|
+
"n_runs": n_runs_row[0],
|
|
253
|
+
}
|
|
254
|
+
)
|
|
204
255
|
|
|
205
256
|
return _listing(items, total=len(items), limit=limit, limit_clamped=clamped)
|
|
206
257
|
|
|
@@ -257,23 +308,25 @@ def list_runs(
|
|
|
257
308
|
for row in rows:
|
|
258
309
|
config_dict = json.loads(row["config"]) if row["config"] else {}
|
|
259
310
|
top_keys = list(config_dict.keys())
|
|
260
|
-
items.append(
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
"
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
311
|
+
items.append(
|
|
312
|
+
{
|
|
313
|
+
"id": row["id"],
|
|
314
|
+
"label": _label(row["experiment_path"], row["name"], row["id"]),
|
|
315
|
+
"experiment_id": row["experiment_id"],
|
|
316
|
+
"experiment_path": row["experiment_path"],
|
|
317
|
+
"name": row["name"],
|
|
318
|
+
"status": row["status"],
|
|
319
|
+
"started_at": row["started_at"],
|
|
320
|
+
"ended_at": row["ended_at"],
|
|
321
|
+
"tags": json.loads(row["tags"]) if row["tags"] else [],
|
|
322
|
+
"git_sha": row["git_sha"],
|
|
323
|
+
"hostname": row["hostname"],
|
|
324
|
+
"config_summary": {
|
|
325
|
+
"n_keys": len(top_keys),
|
|
326
|
+
"top_level_keys": top_keys,
|
|
327
|
+
},
|
|
328
|
+
}
|
|
329
|
+
)
|
|
277
330
|
|
|
278
331
|
return _listing(items, total=len(items), limit=limit, limit_clamped=clamped)
|
|
279
332
|
|
|
@@ -308,16 +361,18 @@ def list_models(name_prefix: str = "", limit: int = 50) -> dict:
|
|
|
308
361
|
|
|
309
362
|
items: list[dict] = []
|
|
310
363
|
for row in rows:
|
|
311
|
-
items.append(
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
364
|
+
items.append(
|
|
365
|
+
{
|
|
366
|
+
"id": row["id"],
|
|
367
|
+
"name": row["name"],
|
|
368
|
+
"version": row["version"],
|
|
369
|
+
"run_id": row["run_id"],
|
|
370
|
+
"framework": row["framework"],
|
|
371
|
+
"artifact_path": row["artifact_path"],
|
|
372
|
+
"metadata": json.loads(row["metadata"]) if row["metadata"] else None,
|
|
373
|
+
"created_at": row["created_at"],
|
|
374
|
+
}
|
|
375
|
+
)
|
|
321
376
|
|
|
322
377
|
return _listing(items, total=len(items), limit=limit, limit_clamped=clamped)
|
|
323
378
|
|
|
@@ -429,14 +484,16 @@ def get_run(run_id: str) -> dict:
|
|
|
429
484
|
).fetchall()
|
|
430
485
|
artifacts: list[dict] = []
|
|
431
486
|
for a in art_rows:
|
|
432
|
-
artifacts.append(
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
487
|
+
artifacts.append(
|
|
488
|
+
{
|
|
489
|
+
"name": a["name"],
|
|
490
|
+
"kind": a["kind"],
|
|
491
|
+
"step": a["step"],
|
|
492
|
+
"rel_path": a["rel_path"],
|
|
493
|
+
"shape": json.loads(a["shape"]) if a["shape"] else None,
|
|
494
|
+
"dtype": a["dtype"],
|
|
495
|
+
}
|
|
496
|
+
)
|
|
440
497
|
|
|
441
498
|
todo_rows = _store._conn.execute(
|
|
442
499
|
"SELECT id, content, priority, done, created_at, completed_at "
|
|
@@ -470,7 +527,11 @@ def get_run(run_id: str) -> dict:
|
|
|
470
527
|
|
|
471
528
|
_VALID_STATUS = {"running", "completed", "failed", "archived"}
|
|
472
529
|
_VALID_FILTERS = {
|
|
473
|
-
"tag",
|
|
530
|
+
"tag",
|
|
531
|
+
"status",
|
|
532
|
+
"experiment_prefix",
|
|
533
|
+
"started_after",
|
|
534
|
+
"started_before",
|
|
474
535
|
}
|
|
475
536
|
|
|
476
537
|
|
|
@@ -568,23 +629,25 @@ def search(
|
|
|
568
629
|
for row in rows:
|
|
569
630
|
config_dict = json.loads(row["config"]) if row["config"] else {}
|
|
570
631
|
top_keys = list(config_dict.keys())
|
|
571
|
-
items.append(
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
"
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
632
|
+
items.append(
|
|
633
|
+
{
|
|
634
|
+
"id": row["id"],
|
|
635
|
+
"label": _label(row["experiment_path"], row["name"], row["id"]),
|
|
636
|
+
"experiment_id": row["experiment_id"],
|
|
637
|
+
"experiment_path": row["experiment_path"],
|
|
638
|
+
"name": row["name"],
|
|
639
|
+
"status": row["status"],
|
|
640
|
+
"started_at": row["started_at"],
|
|
641
|
+
"ended_at": row["ended_at"],
|
|
642
|
+
"tags": json.loads(row["tags"]) if row["tags"] else [],
|
|
643
|
+
"git_sha": row["git_sha"],
|
|
644
|
+
"hostname": row["hostname"],
|
|
645
|
+
"config_summary": {
|
|
646
|
+
"n_keys": len(top_keys),
|
|
647
|
+
"top_level_keys": top_keys,
|
|
648
|
+
},
|
|
649
|
+
}
|
|
650
|
+
)
|
|
588
651
|
|
|
589
652
|
return _listing(items, total=len(items), limit=limit, limit_clamped=clamped)
|
|
590
653
|
|
|
@@ -617,7 +680,9 @@ def compare_runs(run_ids: list[str], include_curves: bool = False) -> dict:
|
|
|
617
680
|
}
|
|
618
681
|
"""
|
|
619
682
|
if len(run_ids) < 2:
|
|
620
|
-
raise ValueError(
|
|
683
|
+
raise ValueError(
|
|
684
|
+
f"compare_runs requires at least 2 run_ids (got {len(run_ids)})"
|
|
685
|
+
)
|
|
621
686
|
if len(run_ids) > 10:
|
|
622
687
|
raise ValueError(
|
|
623
688
|
f"compare_runs supports at most 10 runs per call (got {len(run_ids)})"
|
|
@@ -627,7 +692,9 @@ def compare_runs(run_ids: list[str], include_curves: bool = False) -> dict:
|
|
|
627
692
|
runs_out: list[dict] = []
|
|
628
693
|
configs: list[tuple[str, dict]] = []
|
|
629
694
|
metric_values: dict[str, dict[str, float]] = {} # name -> {run_id: final_val}
|
|
630
|
-
curves_out: dict[str, dict[str, list[list]]] =
|
|
695
|
+
curves_out: dict[str, dict[str, list[list]]] = (
|
|
696
|
+
{}
|
|
697
|
+
) # name -> {run_id: [[step, value], ...]}
|
|
631
698
|
|
|
632
699
|
with _store.lock:
|
|
633
700
|
for rid in run_ids:
|
|
@@ -640,12 +707,14 @@ def compare_runs(run_ids: list[str], include_curves: bool = False) -> dict:
|
|
|
640
707
|
if row is None:
|
|
641
708
|
raise ValueError(f"Run not found: {rid!r}")
|
|
642
709
|
|
|
643
|
-
runs_out.append(
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
710
|
+
runs_out.append(
|
|
711
|
+
{
|
|
712
|
+
"id": row["id"],
|
|
713
|
+
"label": _label(row["experiment_path"], row["name"], row["id"]),
|
|
714
|
+
"experiment_path": row["experiment_path"],
|
|
715
|
+
"status": row["status"],
|
|
716
|
+
}
|
|
717
|
+
)
|
|
649
718
|
cfg = json.loads(row["config"]) if row["config"] else {}
|
|
650
719
|
configs.append((rid, cfg))
|
|
651
720
|
|
|
@@ -671,13 +740,15 @@ def compare_runs(run_ids: list[str], include_curves: bool = False) -> dict:
|
|
|
671
740
|
)
|
|
672
741
|
|
|
673
742
|
# Build the metrics dict.
|
|
743
|
+
direction_overrides = _metric_direction_overrides(_store)
|
|
674
744
|
metrics_out: dict[str, dict] = {}
|
|
675
745
|
for name, vals in metric_values.items():
|
|
676
|
-
direction = _metric_direction(name)
|
|
677
|
-
reverse =
|
|
678
|
-
ranking = [
|
|
679
|
-
|
|
680
|
-
|
|
746
|
+
direction = _metric_direction(name, direction_overrides)
|
|
747
|
+
reverse = direction == "max"
|
|
748
|
+
ranking = [
|
|
749
|
+
rid
|
|
750
|
+
for rid, _ in sorted(vals.items(), key=lambda kv: kv[1], reverse=reverse)
|
|
751
|
+
]
|
|
681
752
|
metrics_out[name] = {
|
|
682
753
|
"direction": direction,
|
|
683
754
|
"values": vals,
|
|
@@ -767,7 +838,9 @@ def get_lineage(
|
|
|
767
838
|
with _store.lock:
|
|
768
839
|
root_label = _lookup_node_label(_store._conn, node_type, node_id)
|
|
769
840
|
if root_label is None:
|
|
770
|
-
pretty = {"run": "Run", "experiment": "Experiment", "model": "Model"}[
|
|
841
|
+
pretty = {"run": "Run", "experiment": "Experiment", "model": "Model"}[
|
|
842
|
+
node_type
|
|
843
|
+
]
|
|
771
844
|
raise ValueError(f"{pretty} not found: {node_id!r}")
|
|
772
845
|
|
|
773
846
|
visited: set[tuple[str, str]] = {(node_type, node_id)}
|
|
@@ -776,7 +849,7 @@ def get_lineage(
|
|
|
776
849
|
|
|
777
850
|
for _hop in range(depth):
|
|
778
851
|
next_frontier: list[tuple[str, str]] = []
|
|
779
|
-
for
|
|
852
|
+
for nt, nid in frontier:
|
|
780
853
|
# Descendants: I am the parent.
|
|
781
854
|
if direction in ("descendants", "both"):
|
|
782
855
|
rows = _store._conn.execute(
|
|
@@ -811,14 +884,19 @@ def get_lineage(
|
|
|
811
884
|
seen_edges: set[tuple] = set()
|
|
812
885
|
unique_edges: list[dict] = []
|
|
813
886
|
for e in edges_out:
|
|
814
|
-
key = (
|
|
815
|
-
|
|
887
|
+
key = (
|
|
888
|
+
e["parent_type"],
|
|
889
|
+
e["parent_id"],
|
|
890
|
+
e["child_type"],
|
|
891
|
+
e["child_id"],
|
|
892
|
+
e["relation"],
|
|
893
|
+
)
|
|
816
894
|
if key not in seen_edges:
|
|
817
895
|
seen_edges.add(key)
|
|
818
896
|
unique_edges.append(e)
|
|
819
897
|
|
|
820
898
|
nodes_out: list[dict] = []
|
|
821
|
-
for
|
|
899
|
+
for nt, nid in visited:
|
|
822
900
|
if (nt, nid) == (node_type, node_id):
|
|
823
901
|
continue # root is emitted separately
|
|
824
902
|
label = _lookup_node_label(_store._conn, nt, nid)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{extract_tracker-0.2.2 → extract_tracker-0.2.3}/python/src/extract_tracker.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{extract_tracker-0.2.2 → extract_tracker-0.2.3}/python/src/extract_tracker.egg-info/requires.txt
RENAMED
|
File without changes
|
{extract_tracker-0.2.2 → extract_tracker-0.2.3}/python/src/extract_tracker.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|