deriva-dev 0.8.2__tar.gz → 0.8.4__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.
- {deriva_dev-0.8.2 → deriva_dev-0.8.4}/PKG-INFO +2 -2
- {deriva_dev-0.8.2 → deriva_dev-0.8.4}/pyproject.toml +2 -2
- {deriva_dev-0.8.2 → deriva_dev-0.8.4}/pyproject.toml.orig +2 -2
- {deriva_dev-0.8.2 → deriva_dev-0.8.4}/src/deriva/bench/benchmarking.py +141 -18
- {deriva_dev-0.8.2 → deriva_dev-0.8.4}/src/deriva/bench/cli.py +71 -13
- {deriva_dev-0.8.2 → deriva_dev-0.8.4}/src/deriva/bench/config_deviation.py +1 -1
- deriva_dev-0.8.4/src/deriva/bench/gate.py +305 -0
- {deriva_dev-0.8.2 → deriva_dev-0.8.4}/src/deriva/bench/reference_eval.py +60 -4
- {deriva_dev-0.8.2 → deriva_dev-0.8.4}/src/deriva/bench/router.py +38 -4
- {deriva_dev-0.8.2 → deriva_dev-0.8.4}/src/deriva/bench/session.py +26 -3
- {deriva_dev-0.8.2 → deriva_dev-0.8.4}/src/deriva/bench/session_metrics.py +79 -10
- deriva_dev-0.8.2/src/deriva/bench/gate.py +0 -216
- {deriva_dev-0.8.2 → deriva_dev-0.8.4}/LICENSE +0 -0
- {deriva_dev-0.8.2 → deriva_dev-0.8.4}/README.md +0 -0
- {deriva_dev-0.8.2 → deriva_dev-0.8.4}/src/deriva/bench/__init__.py +0 -0
- {deriva_dev-0.8.2 → deriva_dev-0.8.4}/src/deriva/bench/analysis.py +0 -0
- {deriva_dev-0.8.2 → deriva_dev-0.8.4}/src/deriva/bench/baseline_b1.py +0 -0
- {deriva_dev-0.8.2 → deriva_dev-0.8.4}/src/deriva/bench/benchmark_views.py +0 -0
- {deriva_dev-0.8.2 → deriva_dev-0.8.4}/src/deriva/bench/conventions_report.py +0 -0
- {deriva_dev-0.8.2 → deriva_dev-0.8.4}/src/deriva/bench/data/conventions.json +0 -0
- {deriva_dev-0.8.2 → deriva_dev-0.8.4}/src/deriva/bench/step_benchmark.py +0 -0
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: deriva-dev
|
|
3
|
-
Version: 0.8.
|
|
3
|
+
Version: 0.8.4
|
|
4
4
|
Summary: Deriva with benchmarking: the studio, the CLI and multi-model consistency benchmarks.
|
|
5
5
|
Author: S. T. Grond
|
|
6
6
|
Author-email: S. T. Grond <steven@deriva.dev>
|
|
7
7
|
License-Expression: AGPL-3.0-or-later
|
|
8
8
|
License-File: LICENSE
|
|
9
|
-
Requires-Dist: deriva-studio==0.8.
|
|
9
|
+
Requires-Dist: deriva-studio==0.8.4
|
|
10
10
|
Requires-Python: >=3.14
|
|
11
11
|
Project-URL: Homepage, https://deriva.dev
|
|
12
12
|
Project-URL: Documentation, https://docs.deriva.dev
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "deriva-dev"
|
|
3
|
-
version = "0.8.
|
|
3
|
+
version = "0.8.4"
|
|
4
4
|
description = "Deriva with benchmarking: the studio, the CLI and multi-model consistency benchmarks."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.14"
|
|
7
7
|
license = "AGPL-3.0-or-later"
|
|
8
8
|
license-files = ["LICENSE"]
|
|
9
|
-
dependencies = ["deriva-studio==0.8.
|
|
9
|
+
dependencies = ["deriva-studio==0.8.4"]
|
|
10
10
|
|
|
11
11
|
[[project.authors]]
|
|
12
12
|
name = "S. T. Grond"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "deriva-dev"
|
|
3
|
-
version = "0.8.
|
|
3
|
+
version = "0.8.4"
|
|
4
4
|
description = "Deriva with benchmarking: the studio, the CLI and multi-model consistency benchmarks."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.14"
|
|
@@ -8,7 +8,7 @@ license = "AGPL-3.0-or-later"
|
|
|
8
8
|
# A copy of the repository LICENSE (a package cannot point outside its folder); tests/test_packaging.py keeps it identical
|
|
9
9
|
license-files = ["LICENSE"]
|
|
10
10
|
authors = [{ name = "S. T. Grond", email = "steven@deriva.dev" }]
|
|
11
|
-
dependencies = ["deriva-studio==0.8.
|
|
11
|
+
dependencies = ["deriva-studio==0.8.4"]
|
|
12
12
|
|
|
13
13
|
[project.urls]
|
|
14
14
|
Homepage = "https://deriva.dev"
|
|
@@ -297,13 +297,14 @@ class OCELStepContext:
|
|
|
297
297
|
self._created_relationships.append(relationship_id)
|
|
298
298
|
|
|
299
299
|
def _elapsed_seconds(self) -> float:
|
|
300
|
-
|
|
300
|
+
"""The step's duration to the microsecond: the case log derives the start from it, so steps must not overlap."""
|
|
301
|
+
return round(time.perf_counter() - self._start_time, 6)
|
|
301
302
|
|
|
302
303
|
def complete(self, message: str = "") -> None:
|
|
303
304
|
"""Mark step as completed and log to OCEL."""
|
|
304
305
|
self._completed = True
|
|
305
306
|
if self.logger.verbose:
|
|
306
|
-
print(f" {self.step}: {self._elapsed_seconds()}s, {self.items_created} created")
|
|
307
|
+
print(f" {self.step}: {self._elapsed_seconds():.2f}s, {self.items_created} created")
|
|
307
308
|
config_type = "extraction" if self.logger._current_phase == "extraction" else "derivation"
|
|
308
309
|
self.logger.log_config_result(
|
|
309
310
|
config_type=config_type,
|
|
@@ -319,7 +320,7 @@ class OCELStepContext:
|
|
|
319
320
|
"""Mark step as errored."""
|
|
320
321
|
self._completed = True
|
|
321
322
|
if self.logger.verbose:
|
|
322
|
-
print(f" {self.step}: {self._elapsed_seconds()}s, ERROR {error[:120]}")
|
|
323
|
+
print(f" {self.step}: {self._elapsed_seconds():.2f}s, ERROR {error[:120]}")
|
|
323
324
|
config_type = "extraction" if self.logger._current_phase == "extraction" else "derivation"
|
|
324
325
|
self.logger.log_config_result(
|
|
325
326
|
config_type=config_type,
|
|
@@ -506,6 +507,8 @@ class BenchmarkOrchestrator:
|
|
|
506
507
|
self._call_logs: dict[str, LlmCallLog] = {}
|
|
507
508
|
# LLM answers per decision per step, recorded in the session metadata
|
|
508
509
|
self._llm_samples: dict[str, int] = {}
|
|
510
|
+
# The code commit and dirty flag when the session started, recorded in the session metadata
|
|
511
|
+
self._code: dict[str, Any] = {"commit": None, "dirty": None}
|
|
509
512
|
self._current_model: str | None = None
|
|
510
513
|
self._current_repo: str | None = None
|
|
511
514
|
|
|
@@ -1344,7 +1347,7 @@ class BenchmarkOrchestrator:
|
|
|
1344
1347
|
return response
|
|
1345
1348
|
|
|
1346
1349
|
# Attach cache keys list to function for retrieval after run
|
|
1347
|
-
query_fn.used_cache_keys = used_cache_keys # type: ignore[attr-defined]
|
|
1350
|
+
query_fn.used_cache_keys = used_cache_keys # type: ignore[attr-defined] # ty: ignore[unresolved-attribute]
|
|
1348
1351
|
return query_fn
|
|
1349
1352
|
|
|
1350
1353
|
def _record_llm_call(self, llm: Any, step: str | None, prompt: str, schema: dict | None, system_prompt: str | None, response: Any, error: str | None) -> None:
|
|
@@ -1457,6 +1460,7 @@ class BenchmarkOrchestrator:
|
|
|
1457
1460
|
model_name=model_display_name,
|
|
1458
1461
|
properties=leveled.properties,
|
|
1459
1462
|
views=leveled.views,
|
|
1463
|
+
relationship_properties=leveled.relationship_properties,
|
|
1460
1464
|
)
|
|
1461
1465
|
|
|
1462
1466
|
# JSON snapshot for consistency analysis: the XML carries no properties,
|
|
@@ -1478,11 +1482,18 @@ class BenchmarkOrchestrator:
|
|
|
1478
1482
|
"name": e.name,
|
|
1479
1483
|
"source": (e.properties or {}).get("source"),
|
|
1480
1484
|
"level": "architect",
|
|
1481
|
-
# The name came from an LLM choice among several candidates (origin split)
|
|
1482
|
-
|
|
1485
|
+
# The name came from an LLM choice among several candidates (origin split): as the naming
|
|
1486
|
+
# step recorded it, else (steps that record none) several candidates mean a choice
|
|
1487
|
+
"name_choice": bool((e.properties or {})["name_choice"])
|
|
1488
|
+
if "name_choice" in (e.properties or {})
|
|
1489
|
+
else len((e.properties or {}).get("name_candidates") or []) > 1,
|
|
1483
1490
|
# The structural name (first candidate) next to the chosen one: what LLM naming adds
|
|
1484
1491
|
# (no naming call: the name is the structural one)
|
|
1485
1492
|
"structure_name": (e.properties or {}).get("structure_name") or next(iter((e.properties or {}).get("name_candidates") or []), None) or e.name,
|
|
1493
|
+
# A closed-set name's candidates in order (None for a name that is no choice)
|
|
1494
|
+
"name_candidates": list((e.properties or {}).get("name_candidates") or []) or None,
|
|
1495
|
+
# The abstraction levels the abstraction_levels step wrote (None: not tagged, enterprise)
|
|
1496
|
+
"levels": list((e.properties or {})["levels"]) if isinstance((e.properties or {}).get("levels"), list) else None,
|
|
1486
1497
|
}
|
|
1487
1498
|
for e in elements
|
|
1488
1499
|
],
|
|
@@ -1493,6 +1504,9 @@ class BenchmarkOrchestrator:
|
|
|
1493
1504
|
"type": r.relationship_type,
|
|
1494
1505
|
"derived_from": (r.properties or {}).get("derived_from"),
|
|
1495
1506
|
"confidence": (r.properties or {}).get("confidence"),
|
|
1507
|
+
# Its levels (those both ends share); a cross-level link (no shared level) counts in the union only
|
|
1508
|
+
"levels": list((r.properties or {})["levels"]) if isinstance((r.properties or {}).get("levels"), list) else None,
|
|
1509
|
+
"cross_level": bool((r.properties or {}).get("cross_level")),
|
|
1496
1510
|
}
|
|
1497
1511
|
for r in relationships
|
|
1498
1512
|
],
|
|
@@ -1569,12 +1583,20 @@ class BenchmarkOrchestrator:
|
|
|
1569
1583
|
self.session_start.isoformat(),
|
|
1570
1584
|
],
|
|
1571
1585
|
)
|
|
1586
|
+
self._capture_code()
|
|
1572
1587
|
try:
|
|
1573
1588
|
self._write_session_inputs()
|
|
1574
1589
|
except Exception as exc:
|
|
1575
1590
|
# A record that cannot be written does not stop the session (as a failed model export does not fail its run)
|
|
1576
1591
|
logger.warning("Could not record the inputs of session %s: %s", self.session_id, exc)
|
|
1577
1592
|
|
|
1593
|
+
def _capture_code(self) -> None:
|
|
1594
|
+
"""The commit the session starts on, with a dirty flag (the code may change while the session runs)."""
|
|
1595
|
+
try:
|
|
1596
|
+
self._code = export_bundle.code_commit()
|
|
1597
|
+
except Exception as exc: # A commit that cannot be read does not stop the session
|
|
1598
|
+
logger.warning("Could not read the code commit of session %s: %s", self.session_id, exc)
|
|
1599
|
+
|
|
1578
1600
|
def _write_session_inputs(self) -> None:
|
|
1579
1601
|
"""Everything that changes results, as it is when the session starts, into the session folder.
|
|
1580
1602
|
|
|
@@ -1703,6 +1725,8 @@ class BenchmarkOrchestrator:
|
|
|
1703
1725
|
summary = {
|
|
1704
1726
|
"session_id": self.session_id,
|
|
1705
1727
|
"config": self.config.to_dict(),
|
|
1728
|
+
# The code the session ran: its commit and whether the working tree had changes at the start
|
|
1729
|
+
"code": self._code,
|
|
1706
1730
|
"started_at": self.session_start.isoformat() if self.session_start else None,
|
|
1707
1731
|
"completed_at": datetime.now().isoformat(),
|
|
1708
1732
|
"total_events": len(self.ocel_log.events),
|
|
@@ -1860,27 +1884,66 @@ def export_process_logs(benchmarks_dir: str | Path, session_ids: list[str], out_
|
|
|
1860
1884
|
return paths
|
|
1861
1885
|
|
|
1862
1886
|
|
|
1863
|
-
def list_benchmark_sessions(
|
|
1887
|
+
def list_benchmark_sessions(
|
|
1888
|
+
engine: Any,
|
|
1889
|
+
limit: int = 10,
|
|
1890
|
+
offset: int = 0,
|
|
1891
|
+
search: str | None = None,
|
|
1892
|
+
repository: str | None = None,
|
|
1893
|
+
model: str | None = None,
|
|
1894
|
+
status: str | None = None,
|
|
1895
|
+
) -> list[dict[str, Any]]:
|
|
1864
1896
|
"""
|
|
1865
|
-
List
|
|
1897
|
+
List benchmark sessions, newest first, with the repositories, models and runs they hold.
|
|
1866
1898
|
|
|
1867
1899
|
Args:
|
|
1868
1900
|
engine: DuckDB connection
|
|
1869
1901
|
limit: Maximum number of sessions to return
|
|
1902
|
+
offset: Sessions to skip (a page at a time)
|
|
1903
|
+
search: Text in the session id or description (any case)
|
|
1904
|
+
repository: Only sessions with a run of this repository
|
|
1905
|
+
model: Only sessions with a run of this model config
|
|
1906
|
+
status: Only sessions with this status (completed, failed, running)
|
|
1870
1907
|
|
|
1871
1908
|
Returns:
|
|
1872
1909
|
List of session dictionaries
|
|
1873
1910
|
"""
|
|
1911
|
+
where: list[str] = []
|
|
1912
|
+
params: list[Any] = []
|
|
1913
|
+
if search:
|
|
1914
|
+
where.append("(s.session_id ILIKE ? OR coalesce(s.description, '') ILIKE ?)")
|
|
1915
|
+
params += [f"%{search}%", f"%{search}%"]
|
|
1916
|
+
if repository:
|
|
1917
|
+
where.append("EXISTS (SELECT 1 FROM benchmark_runs r WHERE r.session_id = s.session_id AND r.repository = ?)")
|
|
1918
|
+
params.append(repository)
|
|
1919
|
+
if model:
|
|
1920
|
+
where.append("EXISTS (SELECT 1 FROM benchmark_runs r WHERE r.session_id = s.session_id AND r.model_name = ?)")
|
|
1921
|
+
params.append(model)
|
|
1922
|
+
if status:
|
|
1923
|
+
where.append("s.status = ?")
|
|
1924
|
+
params.append(status)
|
|
1925
|
+
clause = f"WHERE {' AND '.join(where)}" if where else ""
|
|
1874
1926
|
rows = engine.execute(
|
|
1875
|
-
"""
|
|
1876
|
-
SELECT session_id, description, status, started_at, completed_at
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1927
|
+
f"""
|
|
1928
|
+
SELECT s.session_id, s.description, s.status, s.started_at, s.completed_at, s.config,
|
|
1929
|
+
count(r.run_id), list(DISTINCT r.repository), list(DISTINCT r.model_name)
|
|
1930
|
+
FROM benchmark_sessions s
|
|
1931
|
+
LEFT JOIN benchmark_runs r ON r.session_id = s.session_id
|
|
1932
|
+
{clause}
|
|
1933
|
+
GROUP BY s.session_id, s.description, s.status, s.started_at, s.completed_at, s.config
|
|
1934
|
+
ORDER BY s.started_at DESC, s.session_id DESC
|
|
1935
|
+
LIMIT ? OFFSET ?
|
|
1880
1936
|
""",
|
|
1881
|
-
[limit],
|
|
1937
|
+
[*params, limit, offset],
|
|
1882
1938
|
).fetchall()
|
|
1883
1939
|
|
|
1940
|
+
def stages(config: Any) -> list[str]:
|
|
1941
|
+
try:
|
|
1942
|
+
found = json.loads(config) if isinstance(config, str) else (config or {})
|
|
1943
|
+
except ValueError:
|
|
1944
|
+
return []
|
|
1945
|
+
return [str(stage) for stage in found.get("stages") or []] if isinstance(found, dict) else []
|
|
1946
|
+
|
|
1884
1947
|
return [
|
|
1885
1948
|
{
|
|
1886
1949
|
"session_id": row[0],
|
|
@@ -1888,11 +1951,59 @@ def list_benchmark_sessions(engine: Any, limit: int = 10) -> list[dict[str, Any]
|
|
|
1888
1951
|
"status": row[2],
|
|
1889
1952
|
"started_at": row[3],
|
|
1890
1953
|
"completed_at": row[4],
|
|
1954
|
+
"stages": stages(row[5]),
|
|
1955
|
+
"runs": int(row[6]),
|
|
1956
|
+
"repositories": sorted(x for x in row[7] or [] if x is not None),
|
|
1957
|
+
"models": sorted(x for x in row[8] or [] if x is not None),
|
|
1891
1958
|
}
|
|
1892
1959
|
for row in rows
|
|
1893
1960
|
]
|
|
1894
1961
|
|
|
1895
1962
|
|
|
1963
|
+
def folder_bytes(folder: Path) -> int:
|
|
1964
|
+
"""Bytes the files under a folder take (0 when it does not exist)."""
|
|
1965
|
+
return sum(f.stat().st_size for f in folder.rglob("*") if f.is_file()) if folder.is_dir() else 0
|
|
1966
|
+
|
|
1967
|
+
|
|
1968
|
+
def delete_benchmark_session(engine: Any, session_id: str, benchmarks_dir: str | Path) -> dict[str, Any]:
|
|
1969
|
+
"""Delete a session: its rows (session, runs, metrics) and its folder (event logs, snapshots, inputs).
|
|
1970
|
+
|
|
1971
|
+
Raises:
|
|
1972
|
+
KeyError: no such session
|
|
1973
|
+
ValueError: the session is still running (mark it failed first when its process stopped)
|
|
1974
|
+
"""
|
|
1975
|
+
info = get_benchmark_session(engine, session_id)
|
|
1976
|
+
if info is None:
|
|
1977
|
+
raise KeyError(session_id)
|
|
1978
|
+
if info["status"] == "running":
|
|
1979
|
+
raise ValueError(f"Session {session_id} is running; mark it failed first when its process has stopped")
|
|
1980
|
+
(runs,) = engine.execute("SELECT count(*) FROM benchmark_runs WHERE session_id = ?", [session_id]).fetchone()
|
|
1981
|
+
for table in ("benchmark_metrics", "benchmark_runs", "benchmark_sessions"):
|
|
1982
|
+
engine.execute(f"DELETE FROM {table} WHERE session_id = ?", [session_id])
|
|
1983
|
+
folder = Path(benchmarks_dir) / session_id
|
|
1984
|
+
size = folder_bytes(folder)
|
|
1985
|
+
if folder.is_dir():
|
|
1986
|
+
shutil.rmtree(folder)
|
|
1987
|
+
return {"session_id": session_id, "runs": int(runs), "bytes": size}
|
|
1988
|
+
|
|
1989
|
+
|
|
1990
|
+
def prune_benchmark_sessions(engine: Any, benchmarks_dir: str | Path, keep: int | None = None, failed: bool = False) -> list[str]:
|
|
1991
|
+
"""Delete the failed sessions, or every session past the latest ``keep``; running sessions always stay.
|
|
1992
|
+
|
|
1993
|
+
Returns the deleted session ids, newest first.
|
|
1994
|
+
"""
|
|
1995
|
+
rows = engine.execute("SELECT session_id, status FROM benchmark_sessions ORDER BY started_at DESC, session_id DESC").fetchall()
|
|
1996
|
+
if failed:
|
|
1997
|
+
victims = [sid for sid, status in rows if status == "failed"]
|
|
1998
|
+
elif keep is not None:
|
|
1999
|
+
victims = [sid for sid, status in rows[max(keep, 0) :] if status != "running"]
|
|
2000
|
+
else:
|
|
2001
|
+
return []
|
|
2002
|
+
for session_id in victims:
|
|
2003
|
+
delete_benchmark_session(engine, session_id, benchmarks_dir)
|
|
2004
|
+
return victims
|
|
2005
|
+
|
|
2006
|
+
|
|
1896
2007
|
def get_benchmark_session(engine: Any, session_id: str) -> dict[str, Any] | None:
|
|
1897
2008
|
"""
|
|
1898
2009
|
Get details for a specific benchmark session.
|
|
@@ -2176,13 +2287,25 @@ def model_quality_for_session(
|
|
|
2176
2287
|
row: dict[str, Any] = {"repository": repo, "model": model, "run": int(run), **compute_model_quality(elements, relationships).to_dict()}
|
|
2177
2288
|
snapshot_path = path.with_suffix(".json")
|
|
2178
2289
|
try:
|
|
2179
|
-
|
|
2290
|
+
snapshot = json.loads(snapshot_path.read_text(encoding="utf-8")) if snapshot_path.is_file() else {}
|
|
2180
2291
|
except OSError, ValueError:
|
|
2181
|
-
|
|
2292
|
+
snapshot = {}
|
|
2293
|
+
row["solve"] = snapshot.get("solve")
|
|
2182
2294
|
row["reference"] = None
|
|
2183
2295
|
if reference:
|
|
2184
|
-
#
|
|
2185
|
-
|
|
2296
|
+
# Name + type, the user's accepted names and structural refs (sources and lineage from the run's
|
|
2297
|
+
# snapshot: the XML carries none) and derivable labels: the gate's matching (golden_mapping)
|
|
2298
|
+
structure = {str(e.get("identifier")): e for e in snapshot.get("elements", []) if isinstance(e, dict)}
|
|
2299
|
+
derived = [
|
|
2300
|
+
{
|
|
2301
|
+
"id": e.identifier,
|
|
2302
|
+
"name": e.name,
|
|
2303
|
+
"type": e.element_type,
|
|
2304
|
+
"source": structure.get(e.identifier, {}).get("source"),
|
|
2305
|
+
"lineage": structure.get(e.identifier, {}).get("lineage") or [],
|
|
2306
|
+
}
|
|
2307
|
+
for e in elements
|
|
2308
|
+
]
|
|
2186
2309
|
aliases, derivable = session_metrics.manual_inputs(reference_models[repo])
|
|
2187
2310
|
row["reference"] = session_metrics.golden_comparison(derived, reference, aliases=aliases, derivable=derivable)
|
|
2188
2311
|
rows.append(row)
|
|
@@ -7,7 +7,7 @@ Provides commands for multi-model benchmarking.
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
9
|
import json
|
|
10
|
-
from collections.abc import
|
|
10
|
+
from collections.abc import Generator
|
|
11
11
|
from contextlib import contextmanager
|
|
12
12
|
from pathlib import Path
|
|
13
13
|
from typing import TYPE_CHECKING, Annotated
|
|
@@ -26,7 +26,7 @@ app = typer.Typer(name="benchmark", help="Multi-model benchmarking")
|
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
@contextmanager
|
|
29
|
-
def _benchmarks() ->
|
|
29
|
+
def _benchmarks() -> Generator[BenchmarkSession]:
|
|
30
30
|
"""A connected PipelineSession with the benchmark operations."""
|
|
31
31
|
with PipelineSession() as session:
|
|
32
32
|
yield BenchmarkSession(session)
|
|
@@ -238,7 +238,7 @@ def benchmark_run(
|
|
|
238
238
|
typer.echo(f" ... and {len(result.errors) - 5} more")
|
|
239
239
|
|
|
240
240
|
typer.echo("\nTo analyze results:")
|
|
241
|
-
typer.echo(f" deriva benchmark analyze {result.session_id}")
|
|
241
|
+
typer.echo(f" deriva-cli benchmark analyze {result.session_id}")
|
|
242
242
|
|
|
243
243
|
if not result.success:
|
|
244
244
|
raise typer.Exit(1)
|
|
@@ -328,6 +328,54 @@ def benchmark_list(
|
|
|
328
328
|
typer.echo("")
|
|
329
329
|
|
|
330
330
|
|
|
331
|
+
def _megabytes(size: int) -> str:
|
|
332
|
+
return f"{size / 1024 / 1024:.1f} MB"
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
@app.command("delete")
|
|
336
|
+
def benchmark_delete(
|
|
337
|
+
session_ids: Annotated[list[str], typer.Argument(help="Benchmark sessions to delete")],
|
|
338
|
+
) -> None:
|
|
339
|
+
"""Delete benchmark sessions with their runs, metrics and folders (a running session is refused)."""
|
|
340
|
+
failed = False
|
|
341
|
+
with _benchmarks() as session:
|
|
342
|
+
for session_id in session_ids:
|
|
343
|
+
try:
|
|
344
|
+
result = session.delete_benchmark(session_id)
|
|
345
|
+
except KeyError:
|
|
346
|
+
typer.echo(f"Error: Benchmark session not found: {session_id}", err=True)
|
|
347
|
+
failed = True
|
|
348
|
+
continue
|
|
349
|
+
except ValueError as e:
|
|
350
|
+
typer.echo(f"Error: {e}", err=True)
|
|
351
|
+
failed = True
|
|
352
|
+
continue
|
|
353
|
+
typer.echo(f"Deleted {session_id} ({result['runs']} runs, {_megabytes(result['bytes'])})")
|
|
354
|
+
if failed:
|
|
355
|
+
raise typer.Exit(1)
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
@app.command("prune")
|
|
359
|
+
def benchmark_prune(
|
|
360
|
+
keep: Annotated[int | None, typer.Option("--keep", min=0, help="Keep the latest N sessions, delete the older ones")] = None,
|
|
361
|
+
failed: Annotated[bool, typer.Option("--failed", help="Delete the failed sessions")] = False,
|
|
362
|
+
yes: Annotated[bool, typer.Option("-y", "--yes", help="Do not ask first")] = False,
|
|
363
|
+
) -> None:
|
|
364
|
+
"""Delete the failed sessions, or every session past the latest N, with their runs, metrics and folders; running sessions stay."""
|
|
365
|
+
if keep is None and not failed:
|
|
366
|
+
typer.echo("Error: say which sessions to delete: --keep N or --failed", err=True)
|
|
367
|
+
raise typer.Exit(1)
|
|
368
|
+
which = "the failed sessions" if failed else f"every session past the latest {keep}"
|
|
369
|
+
if not yes and not typer.confirm(f"Delete {which}, with their runs, metrics and folders?"):
|
|
370
|
+
raise typer.Exit(1)
|
|
371
|
+
with _benchmarks() as session:
|
|
372
|
+
deleted = session.prune_benchmarks(keep=None if failed else keep, failed=failed)
|
|
373
|
+
if not deleted:
|
|
374
|
+
typer.echo("No sessions to delete.")
|
|
375
|
+
return
|
|
376
|
+
typer.echo(f"Deleted {len(deleted)} session{'' if len(deleted) == 1 else 's'}: {', '.join(deleted)}")
|
|
377
|
+
|
|
378
|
+
|
|
331
379
|
@app.command("conventions")
|
|
332
380
|
def benchmark_conventions(
|
|
333
381
|
session_ids: Annotated[list[str], typer.Argument(help="Benchmark sessions whose run snapshots are checked")],
|
|
@@ -383,10 +431,10 @@ def benchmark_gate(
|
|
|
383
431
|
reference: Annotated[str | None, typer.Option("--reference", help="Golden model folder (default: workspace/reference)")] = None,
|
|
384
432
|
benchmarks: Annotated[str | None, typer.Option("--benchmarks", help="Benchmark folder (default: workspace/benchmarks)")] = None,
|
|
385
433
|
) -> None:
|
|
386
|
-
"""
|
|
434
|
+
"""Element and relationship recall and precision and consistency by id per repository, and the verdict of a candidate.
|
|
387
435
|
|
|
388
|
-
Accepted when
|
|
389
|
-
than 2 points
|
|
436
|
+
Accepted when one of the four measures rises by 1 point or more on at least two thirds of the measured repositories,
|
|
437
|
+
none of them drops by more than 2 points anywhere, and element and relationship consistency by id stay at 95% or more.
|
|
390
438
|
"""
|
|
391
439
|
from deriva.bench import gate
|
|
392
440
|
|
|
@@ -399,17 +447,27 @@ def benchmark_gate(
|
|
|
399
447
|
typer.echo(f"Error: no benchmark session {', '.join(missing)} in {root}", err=True)
|
|
400
448
|
raise typer.Exit(1)
|
|
401
449
|
try:
|
|
402
|
-
measures = {
|
|
450
|
+
measures = {
|
|
451
|
+
level: {name: gate.measure([root / s for s in side], goldens_dir, reference_dir, model=model, level=level) for name, side in sides.items() if side}
|
|
452
|
+
for level in gate.GATE_LEVELS
|
|
453
|
+
}
|
|
403
454
|
except ValueError as exc:
|
|
404
455
|
typer.echo(f"Error: {exc}", err=True)
|
|
405
456
|
raise typer.Exit(1) from exc
|
|
406
457
|
lines = [f"# Gate: {', '.join(sides['baseline'])}" + (f" vs {', '.join(sides['candidate'])}" if sides["candidate"] else ""), ""]
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
lines += gate.table_markdown(
|
|
411
|
-
payload
|
|
412
|
-
|
|
458
|
+
payload: dict = {"sessions": sides, "levels": {}}
|
|
459
|
+
for level, by_side in measures.items():
|
|
460
|
+
lines += [f"## {level.capitalize()} level", ""]
|
|
461
|
+
lines += gate.table_markdown(by_side["baseline"], f"Baseline ({', '.join(sides['baseline'])})")
|
|
462
|
+
payload["levels"][level] = {"baseline": by_side["baseline"]}
|
|
463
|
+
if "candidate" in by_side:
|
|
464
|
+
lines += gate.table_markdown(by_side["candidate"], f"Candidate ({', '.join(sides['candidate'])})")
|
|
465
|
+
payload["levels"][level] |= {"candidate": by_side["candidate"], "verdict": gate.verdict(by_side["baseline"], by_side["candidate"])}
|
|
466
|
+
lines += gate.verdict_markdown(payload["levels"][level]["verdict"])
|
|
467
|
+
if sides["candidate"]:
|
|
468
|
+
result, voting = gate.overall_verdict({level: entry["verdict"] for level, entry in payload["levels"].items()})
|
|
469
|
+
payload["verdict"] = {"verdict": result, "levels": voting}
|
|
470
|
+
lines += [f"**Overall: {result}** (levels that voted: {', '.join(voting) or 'none'})", ""]
|
|
413
471
|
for line in lines:
|
|
414
472
|
typer.echo(line)
|
|
415
473
|
name = "_".join(sides["baseline"]) + (f"_vs_{'_'.join(sides['candidate'])}" if sides["candidate"] else "")
|
|
@@ -198,7 +198,7 @@ class ConfigDeviationAnalyzer:
|
|
|
198
198
|
return {"config_id": config_id, "error": "No objects found for config"}
|
|
199
199
|
|
|
200
200
|
# Build per-object breakdown
|
|
201
|
-
object_details = []
|
|
201
|
+
object_details: list[dict[str, Any]] = []
|
|
202
202
|
objects_by_run = {run_id: nodes_by_run.get(run_id, set()) | elements_by_run.get(run_id, set()) for run_id in all_runs}
|
|
203
203
|
|
|
204
204
|
for obj_id in config_objects:
|