lbagents-eval 0.19.0__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.
- lbagents_eval-0.19.0/.gitignore +118 -0
- lbagents_eval-0.19.0/PKG-INFO +18 -0
- lbagents_eval-0.19.0/pyproject.toml +49 -0
- lbagents_eval-0.19.0/scripts/cases_to_scenarios.py +417 -0
- lbagents_eval-0.19.0/scripts/threads_to_scenarios.py +256 -0
- lbagents_eval-0.19.0/src/lbagents_eval/__init__.py +42 -0
- lbagents_eval-0.19.0/src/lbagents_eval/cli.py +379 -0
- lbagents_eval-0.19.0/src/lbagents_eval/config.py +43 -0
- lbagents_eval-0.19.0/src/lbagents_eval/execution/__init__.py +17 -0
- lbagents_eval-0.19.0/src/lbagents_eval/execution/live_tools.py +195 -0
- lbagents_eval-0.19.0/src/lbagents_eval/execution/mock_llm.py +60 -0
- lbagents_eval-0.19.0/src/lbagents_eval/execution/replay.py +328 -0
- lbagents_eval-0.19.0/src/lbagents_eval/execution/results.py +47 -0
- lbagents_eval-0.19.0/src/lbagents_eval/execution/runner.py +142 -0
- lbagents_eval-0.19.0/src/lbagents_eval/execution/suite.py +522 -0
- lbagents_eval-0.19.0/src/lbagents_eval/execution/transient.py +42 -0
- lbagents_eval-0.19.0/src/lbagents_eval/manifest.py +188 -0
- lbagents_eval-0.19.0/src/lbagents_eval/observability/__init__.py +13 -0
- lbagents_eval-0.19.0/src/lbagents_eval/observability/common.py +41 -0
- lbagents_eval-0.19.0/src/lbagents_eval/observability/export.py +435 -0
- lbagents_eval-0.19.0/src/lbagents_eval/observability/reviews.py +164 -0
- lbagents_eval-0.19.0/src/lbagents_eval/profiles/__init__.py +197 -0
- lbagents_eval-0.19.0/src/lbagents_eval/profiles/ci_failure/__init__.py +200 -0
- lbagents_eval-0.19.0/src/lbagents_eval/profiles/ci_failure/replay.py +352 -0
- lbagents_eval-0.19.0/src/lbagents_eval/report.py +510 -0
- lbagents_eval-0.19.0/src/lbagents_eval/scenarios/__init__.py +17 -0
- lbagents_eval-0.19.0/src/lbagents_eval/scenarios/datasets/scenarios.jsonl +51 -0
- lbagents_eval-0.19.0/src/lbagents_eval/scenarios/harness/ap_ci_mr4950.yaml +392 -0
- lbagents_eval-0.19.0/src/lbagents_eval/scenarios/harness/ap_ci_mr6488_39255.yaml +2225 -0
- lbagents_eval-0.19.0/src/lbagents_eval/scenarios/harness/ap_ci_mr6488_40894.yaml +311 -0
- lbagents_eval-0.19.0/src/lbagents_eval/scenarios/harness/ap_ci_mr6756_40661.yaml +579 -0
- lbagents_eval-0.19.0/src/lbagents_eval/scenarios/harness/ap_ci_mr6860.yaml +1339 -0
- lbagents_eval-0.19.0/src/lbagents_eval/scenarios/harness/ap_ci_mr6965_40576.yaml +741 -0
- lbagents_eval-0.19.0/src/lbagents_eval/scenarios/harness/ap_ci_mr7025_39809.yaml +421 -0
- lbagents_eval-0.19.0/src/lbagents_eval/scenarios/harness/ap_ci_mr7036_39281.yaml +405 -0
- lbagents_eval-0.19.0/src/lbagents_eval/scenarios/harness/ap_ci_mr7103_39501.yaml +420 -0
- lbagents_eval-0.19.0/src/lbagents_eval/scenarios/harness/ap_ci_mr7125_39716.yaml +458 -0
- lbagents_eval-0.19.0/src/lbagents_eval/scenarios/harness/ap_ci_mr7137_39844.yaml +688 -0
- lbagents_eval-0.19.0/src/lbagents_eval/scenarios/harness/ap_ci_mr7179_40267.yaml +522 -0
- lbagents_eval-0.19.0/src/lbagents_eval/scenarios/harness/ap_ci_mr7219_40093.yaml +739 -0
- lbagents_eval-0.19.0/src/lbagents_eval/scenarios/harness/ap_ci_mr7258_40226.yaml +1843 -0
- lbagents_eval-0.19.0/src/lbagents_eval/scenarios/harness/ap_ci_mr7258_40294.yaml +1089 -0
- lbagents_eval-0.19.0/src/lbagents_eval/scenarios/harness/ap_ci_mr7260_40224.yaml +436 -0
- lbagents_eval-0.19.0/src/lbagents_eval/scenarios/harness/ap_ci_mr7263_40702.yaml +1525 -0
- lbagents_eval-0.19.0/src/lbagents_eval/scenarios/harness/ap_ci_mr7276_40327.yaml +236 -0
- lbagents_eval-0.19.0/src/lbagents_eval/scenarios/harness/ap_ci_mr7288_40362.yaml +1007 -0
- lbagents_eval-0.19.0/src/lbagents_eval/scenarios/harness/ap_ci_mr7298_40397.yaml +1314 -0
- lbagents_eval-0.19.0/src/lbagents_eval/scenarios/harness/ap_ci_mr7306_40444.yaml +454 -0
- lbagents_eval-0.19.0/src/lbagents_eval/scenarios/harness/ap_ci_mr7343_40747.yaml +1779 -0
- lbagents_eval-0.19.0/src/lbagents_eval/scenarios/harness/ap_ci_mr7349_40643.yaml +464 -0
- lbagents_eval-0.19.0/src/lbagents_eval/scenarios/harness/ap_ci_mr7350_40652.yaml +874 -0
- lbagents_eval-0.19.0/src/lbagents_eval/scenarios/harness/ap_ci_mr7352_40654.yaml +399 -0
- lbagents_eval-0.19.0/src/lbagents_eval/scenarios/harness/ap_ci_mr7370_40783.yaml +1373 -0
- lbagents_eval-0.19.0/src/lbagents_eval/scenarios/harness/ap_ci_mr7379_40815.yaml +139 -0
- lbagents_eval-0.19.0/src/lbagents_eval/scenarios/harness/ap_ci_mr7395_40884.yaml +437 -0
- lbagents_eval-0.19.0/src/lbagents_eval/scenarios/harness/ap_ci_mr7398_41072.yaml +725 -0
- lbagents_eval-0.19.0/src/lbagents_eval/scenarios/harness/ap_ci_mr7403_40923.yaml +1382 -0
- lbagents_eval-0.19.0/src/lbagents_eval/scenarios/harness/ap_ci_mr7404_40924.yaml +662 -0
- lbagents_eval-0.19.0/src/lbagents_eval/scenarios/harness/ap_ci_mr7407.yaml +984 -0
- lbagents_eval-0.19.0/src/lbagents_eval/scenarios/harness/ap_ci_mr7416_41006.yaml +375 -0
- lbagents_eval-0.19.0/src/lbagents_eval/scenarios/harness/ap_ci_mr7422_41060.yaml +365 -0
- lbagents_eval-0.19.0/src/lbagents_eval/scenarios/harness/ap_ci_mr7428_41086.yaml +263 -0
- lbagents_eval-0.19.0/src/lbagents_eval/scenarios/harness/ap_ci_mr7440_41131.yaml +855 -0
- lbagents_eval-0.19.0/src/lbagents_eval/scenarios/harness/ap_ci_mr7449.yaml +537 -0
- lbagents_eval-0.19.0/src/lbagents_eval/scenarios/harness/ap_ci_mr7454_41253.yaml +547 -0
- lbagents_eval-0.19.0/src/lbagents_eval/scenarios/harness/ap_ci_mr7462.yaml +1155 -0
- lbagents_eval-0.19.0/src/lbagents_eval/scenarios/harness/ap_ci_mr7463.yaml +1125 -0
- lbagents_eval-0.19.0/src/lbagents_eval/scenarios/harness/ap_ci_mr7466.yaml +826 -0
- lbagents_eval-0.19.0/src/lbagents_eval/scenarios/harness/ap_ci_mr7471_41311.yaml +467 -0
- lbagents_eval-0.19.0/src/lbagents_eval/scenarios/harness/ap_pg_needs_download_input_data.yaml +240 -0
- lbagents_eval-0.19.0/src/lbagents_eval/scenarios/harness/ap_pg_ownpv_fatal_gaudi.yaml +271 -0
- lbagents_eval-0.19.0/src/lbagents_eval/scenarios/harness/ap_pg_pilot_not_running.yaml +198 -0
- lbagents_eval-0.19.0/src/lbagents_eval/scenarios/harness/ap_pg_xrd_redirect_limit.yaml +205 -0
- lbagents_eval-0.19.0/src/lbagents_eval/scenarios/harness/ap_stuck_235832.yaml +389 -0
- lbagents_eval-0.19.0/src/lbagents_eval/scenarios/harness/ap_stuck_235838.yaml +170 -0
- lbagents_eval-0.19.0/src/lbagents_eval/scenarios/harness/ap_stuck_236121.yaml +345 -0
- lbagents_eval-0.19.0/src/lbagents_eval/scenarios/harness/ap_stuck_236517.yaml +192 -0
- lbagents_eval-0.19.0/src/lbagents_eval/scenarios/harness/ap_stuck_236988.yaml +277 -0
- lbagents_eval-0.19.0/src/lbagents_eval/scenarios/loader.py +125 -0
- lbagents_eval-0.19.0/src/lbagents_eval/scenarios/models.py +85 -0
- lbagents_eval-0.19.0/src/lbagents_eval/scenarios/recordings/.gitkeep +0 -0
- lbagents_eval-0.19.0/src/lbagents_eval/scenarios/recordings.py +454 -0
- lbagents_eval-0.19.0/src/lbagents_eval/scenarios/suites/ci.txt +45 -0
- lbagents_eval-0.19.0/src/lbagents_eval/scenarios/suites/mr.txt +21 -0
- lbagents_eval-0.19.0/src/lbagents_eval/scenarios/suites/triage.txt +13 -0
- lbagents_eval-0.19.0/src/lbagents_eval/scenarios/tool_contract.py +84 -0
- lbagents_eval-0.19.0/src/lbagents_eval/scenarios/tool_specs.json +1030 -0
- lbagents_eval-0.19.0/src/lbagents_eval/scoring/__init__.py +12 -0
- lbagents_eval-0.19.0/src/lbagents_eval/scoring/grounding.py +170 -0
- lbagents_eval-0.19.0/src/lbagents_eval/scoring/verdict.py +327 -0
- lbagents_eval-0.19.0/tests/test_cli.py +49 -0
- lbagents_eval-0.19.0/tests/test_export.py +156 -0
- lbagents_eval-0.19.0/tests/test_grounding.py +53 -0
- lbagents_eval-0.19.0/tests/test_profiles.py +108 -0
- lbagents_eval-0.19.0/tests/test_replay.py +288 -0
- lbagents_eval-0.19.0/tests/test_report.py +380 -0
- lbagents_eval-0.19.0/tests/test_scenarios.py +98 -0
- lbagents_eval-0.19.0/tests/test_scenarios_contract.py +115 -0
- lbagents_eval-0.19.0/tests/test_triage_scenarios.py +45 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
share/python-wheels/
|
|
24
|
+
*.egg-info/
|
|
25
|
+
.installed.cfg
|
|
26
|
+
*.egg
|
|
27
|
+
MANIFEST
|
|
28
|
+
|
|
29
|
+
# Installer logs
|
|
30
|
+
pip-log.txt
|
|
31
|
+
pip-delete-this-directory.txt
|
|
32
|
+
|
|
33
|
+
# Unit test / coverage reports
|
|
34
|
+
htmlcov/
|
|
35
|
+
.tox/
|
|
36
|
+
.nox/
|
|
37
|
+
.coverage
|
|
38
|
+
.coverage.*
|
|
39
|
+
.cache
|
|
40
|
+
nosetests.xml
|
|
41
|
+
coverage.xml
|
|
42
|
+
cov.xml
|
|
43
|
+
*.cover
|
|
44
|
+
*.py,cover
|
|
45
|
+
.hypothesis/
|
|
46
|
+
.pytest_cache/
|
|
47
|
+
cover/
|
|
48
|
+
|
|
49
|
+
# Translations
|
|
50
|
+
*.mo
|
|
51
|
+
*.pot
|
|
52
|
+
|
|
53
|
+
# Sphinx documentation
|
|
54
|
+
docs/_build/
|
|
55
|
+
|
|
56
|
+
# PyBuilder
|
|
57
|
+
.pybuilder/
|
|
58
|
+
target/
|
|
59
|
+
|
|
60
|
+
# Jupyter Notebook
|
|
61
|
+
.ipynb_checkpoints
|
|
62
|
+
|
|
63
|
+
# IPython
|
|
64
|
+
profile_default/
|
|
65
|
+
ipython_config.py
|
|
66
|
+
|
|
67
|
+
# pyenv
|
|
68
|
+
# .python-version
|
|
69
|
+
|
|
70
|
+
# PEP 582
|
|
71
|
+
__pypackages__/
|
|
72
|
+
|
|
73
|
+
# Environments
|
|
74
|
+
.env
|
|
75
|
+
.venv
|
|
76
|
+
env/
|
|
77
|
+
venv/
|
|
78
|
+
ENV/
|
|
79
|
+
env.bak/
|
|
80
|
+
venv.bak/
|
|
81
|
+
|
|
82
|
+
# Pixi
|
|
83
|
+
.pixi/
|
|
84
|
+
|
|
85
|
+
# Rope project settings
|
|
86
|
+
.ropeproject
|
|
87
|
+
|
|
88
|
+
# mypy
|
|
89
|
+
.mypy_cache/
|
|
90
|
+
.dmypy.json
|
|
91
|
+
dmypy.json
|
|
92
|
+
|
|
93
|
+
# Pyre type checker
|
|
94
|
+
.pyre/
|
|
95
|
+
|
|
96
|
+
# pytype static type analyzer
|
|
97
|
+
.pytype/
|
|
98
|
+
|
|
99
|
+
# Cython debug symbols
|
|
100
|
+
cython_debug/
|
|
101
|
+
|
|
102
|
+
# Ruff
|
|
103
|
+
.ruff_cache/
|
|
104
|
+
|
|
105
|
+
# OS
|
|
106
|
+
.DS_Store
|
|
107
|
+
|
|
108
|
+
# IDE
|
|
109
|
+
*.code-workspace
|
|
110
|
+
.idea/
|
|
111
|
+
.vscode/
|
|
112
|
+
|
|
113
|
+
# Eval results (runtime artifacts)
|
|
114
|
+
lbagents-eval/results.json
|
|
115
|
+
eval_results.xml
|
|
116
|
+
|
|
117
|
+
# threads from local script
|
|
118
|
+
.threads/
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: lbagents-eval
|
|
3
|
+
Version: 0.19.0
|
|
4
|
+
Summary: Evaluation of LbAgents workflows on replayed real failures
|
|
5
|
+
Project-URL: Homepage, https://gitlab.cern.ch/lhcb-dpa/analysis-productions/lbagents
|
|
6
|
+
Classifier: Intended Audience :: Science/Research
|
|
7
|
+
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
10
|
+
Classifier: Topic :: Scientific/Engineering
|
|
11
|
+
Requires-Python: >=3.12
|
|
12
|
+
Requires-Dist: langfuse<6.0,>=5.0.3
|
|
13
|
+
Requires-Dist: lbagents
|
|
14
|
+
Requires-Dist: mcp>=1.0
|
|
15
|
+
Requires-Dist: pyyaml>=6.0
|
|
16
|
+
Requires-Dist: rich>=13.0
|
|
17
|
+
Requires-Dist: scipy>=1.15
|
|
18
|
+
Requires-Dist: typer>=0.12
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling", "hatch-vcs"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "lbagents-eval"
|
|
7
|
+
description = "Evaluation of LbAgents workflows on replayed real failures"
|
|
8
|
+
requires-python = ">=3.12"
|
|
9
|
+
dynamic = ["version"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Intended Audience :: Science/Research",
|
|
12
|
+
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
|
|
13
|
+
"Programming Language :: Python :: 3",
|
|
14
|
+
"Programming Language :: Python :: 3.12",
|
|
15
|
+
"Topic :: Scientific/Engineering",
|
|
16
|
+
]
|
|
17
|
+
# One-directional dependency: the eval harness depends on the product, never the
|
|
18
|
+
# reverse. In the pixi workspace `lbagents` resolves to the local editable copy.
|
|
19
|
+
dependencies = [
|
|
20
|
+
"lbagents",
|
|
21
|
+
# `export` reads production traces with the v2 observations API (SDK 5).
|
|
22
|
+
"langfuse>=5.0.3,<6.0",
|
|
23
|
+
# Live MCP tools (live-fallback replay, snapshot, check-tools).
|
|
24
|
+
"mcp>=1.0",
|
|
25
|
+
"typer>=0.12",
|
|
26
|
+
"rich>=13.0",
|
|
27
|
+
"pyyaml>=6.0",
|
|
28
|
+
# Exact sign test and the bootstrap confidence interval of the report.
|
|
29
|
+
"scipy>=1.15",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.scripts]
|
|
33
|
+
lbagents-eval = "lbagents_eval.cli:app"
|
|
34
|
+
|
|
35
|
+
[project.urls]
|
|
36
|
+
Homepage = "https://gitlab.cern.ch/lhcb-dpa/analysis-productions/lbagents"
|
|
37
|
+
|
|
38
|
+
# Same git-tag-derived version as `lbagents` (one tag → identical versions).
|
|
39
|
+
[tool.hatch.version]
|
|
40
|
+
source = "vcs"
|
|
41
|
+
raw-options = { root = "..", git_describe_command = ["git", "describe", "--dirty", "--tags", "--long", "--match", "*[0-9]*", "--exclude", "v[0-9]r*", "--exclude", "v[0-9][0-9]r*"] }
|
|
42
|
+
|
|
43
|
+
[tool.hatch.build.targets.wheel]
|
|
44
|
+
packages = ["src/lbagents_eval"]
|
|
45
|
+
|
|
46
|
+
[tool.pytest.ini_options]
|
|
47
|
+
testpaths = ["tests"]
|
|
48
|
+
addopts = "-v --cov=lbagents_eval --cov-report=term-missing"
|
|
49
|
+
junit_family = "xunit1"
|
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
###############################################################################
|
|
2
|
+
# (c) Copyright 2026 CERN for the benefit of the LHCb Collaboration #
|
|
3
|
+
# #
|
|
4
|
+
# This software is distributed under the terms of the GNU General Public #
|
|
5
|
+
# Licence version 3 (GPL Version 3), copied verbatim in the file "COPYING". #
|
|
6
|
+
# #
|
|
7
|
+
# In applying this licence, CERN does not waive the privileges and immunities #
|
|
8
|
+
# granted to it by virtue of its status as an Intergovernmental Organization #
|
|
9
|
+
# or submit itself to any jurisdiction. #
|
|
10
|
+
###############################################################################
|
|
11
|
+
"""Turn recorded real CI failures into lbagents-eval scenarios.
|
|
12
|
+
|
|
13
|
+
python cases_to_scenarios.py CASES.json CACHE_DIR --tag harvest-2026-10
|
|
14
|
+
|
|
15
|
+
CASES.json: a list of cases ({"id", "mr_iid", "pipeline_id", "ci_run_name", "sha",
|
|
16
|
+
"reference", "evidence", optional "kind", "confidence", "draft", "reviewed_by",
|
|
17
|
+
"expected_response_contains"}), as produced by the benchmark harness. CACHE_DIR/<id>.json:
|
|
18
|
+
the tool outputs recorded when the case was run live ({key: {"tool", "args", "output"}};
|
|
19
|
+
GitLab tools pinned to the failing commit).
|
|
20
|
+
|
|
21
|
+
For each case it writes scenarios/harness/ap_ci_<id>.yaml (every recorded call as a
|
|
22
|
+
mock response matched on its arguments, a "no output recorded" fallback per tool, and a
|
|
23
|
+
scripted gold path: pipeline -> CI run -> first failed job -> post the expected answer)
|
|
24
|
+
and adds or replaces the case's entry in scenarios/datasets/scenarios.jsonl.
|
|
25
|
+
|
|
26
|
+
Draft cases (expected answer not yet checked by an operator: every case without a
|
|
27
|
+
"reviewed_by", whatever its "draft" says) get no expected_response_contains: those
|
|
28
|
+
keywords are chosen when the answer is confirmed. Proposed keywords are kept in
|
|
29
|
+
metadata.draft_expected_response_contains, where nothing gates on them.
|
|
30
|
+
|
|
31
|
+
Each entry carries metadata: draft, confidence, kind, side (request / outside / either,
|
|
32
|
+
the side the expected cause is on; see SIDE) and needs_review.
|
|
33
|
+
|
|
34
|
+
get_mr_info is recorded live, so it returns the MR as it is now, not as it was when the
|
|
35
|
+
pipeline failed: the fields that describe what happened after the failure (state, merge
|
|
36
|
+
and close data, the current head, labels...) are removed, head_sha is set to the failing
|
|
37
|
+
commit, and sentences of the description that state the fix are cut (REDACT). The title
|
|
38
|
+
and description are still the current ones (GitLab keeps no history of them).
|
|
39
|
+
|
|
40
|
+
python cases_to_scenarios.py CASES.json --update-existing
|
|
41
|
+
|
|
42
|
+
rewrites only the scenarios.jsonl entries of CASES.json (metadata, tags, keywords,
|
|
43
|
+
expected_tool_calls from the gold path of the existing harness) and strips get_mr_info
|
|
44
|
+
in the existing harness YAMLs, without re-reading the recordings.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
from __future__ import annotations
|
|
48
|
+
|
|
49
|
+
import argparse
|
|
50
|
+
import json
|
|
51
|
+
import pathlib
|
|
52
|
+
|
|
53
|
+
import yaml
|
|
54
|
+
|
|
55
|
+
# Shared with the live-fallback replay, so a recording made later is cleaned the same way.
|
|
56
|
+
from lbagents_eval.scenarios.recordings import FALLBACK, REDACT, at_failure_mr_info, commits_before, trimmed
|
|
57
|
+
|
|
58
|
+
HERE = pathlib.Path(__file__).resolve().parent
|
|
59
|
+
SCENARIOS = HERE.parent / "src" / "lbagents_eval" / "scenarios"
|
|
60
|
+
# References that no benchmark report matched: possibly too narrow or wrong
|
|
61
|
+
# (dataset-eval-review.md, section 1.2). Low confidence until an operator reviews them.
|
|
62
|
+
TO_REVIEW = {"mr7370_40783", "mr7276_40327", "mr7298_40397", "mr7403_40923", "mr7471_41311"}
|
|
63
|
+
|
|
64
|
+
# Side of the expected cause: "request" (the author changes the MR), "outside" (site,
|
|
65
|
+
# DIRAC, storage, CI service, permissions: an owner adds the author, so the 2 process
|
|
66
|
+
# cases are outside), "either" (both remedies are valid: the "insufficient disk replicas"
|
|
67
|
+
# cases, where the data can be staged or the request changed).
|
|
68
|
+
EITHER = {"mr7395_40884", "mr7471_41311", "mr6756_40661"}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
MR_INFO_NOTE = (
|
|
72
|
+
"# get_mr_info: post-failure fields removed, head_sha = the failing commit; title and description\n"
|
|
73
|
+
"# are the MR's current text (recorded after the failure), not the text at the failure time.\n"
|
|
74
|
+
)
|
|
75
|
+
REDACT_NOTE = "# The sentence of the current description that states the fix is removed (REDACT).\n"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# Accepted / rejected fixes that the single prose reference does not spell out
|
|
79
|
+
# (dataset-eval-review.md, section 1.2), for the goal judge (metadata.grading_notes).
|
|
80
|
+
# Not reviewed by an operator. Kept for reviewers; the bench judges (scoring/verdict.py) do not read them.
|
|
81
|
+
GRADING_NOTES = {
|
|
82
|
+
"mr7463": "Also accept removing the END_V*/end-vertex variables, as in the similar case mr7466.",
|
|
83
|
+
"mr7462": "Accept any unpacking that takes the last two returned values.",
|
|
84
|
+
"mr7407": "Rejected fix: removing the input_pvs argument (create_dtf requires pvs).",
|
|
85
|
+
"mr7398_41072": "Also accept fixing the import so that it works with the Moore version used.",
|
|
86
|
+
"mr7258_40226": (
|
|
87
|
+
"Accept any change that reduces the combinatorics (tighter cuts, fewer combinations). "
|
|
88
|
+
"Rejected fix: raising the memory limit."
|
|
89
|
+
),
|
|
90
|
+
"mr7276_40327": (
|
|
91
|
+
"Also accept setting input_process/input_stream/geometry_version in extra_options by hand, "
|
|
92
|
+
"as the error message suggests. Rejected fix: waiting for a SprucingConfig release."
|
|
93
|
+
),
|
|
94
|
+
"mr7403_40923": "Also accept using other input data or adding a re-merge step.",
|
|
95
|
+
"mr7440_41131": "Also accept initialising D_data or guarding the tuples.",
|
|
96
|
+
"mr7343_40747": "Also accept dropping the cut or applying the track-type cut on the pion.",
|
|
97
|
+
"mr7288_40362": "Accept removing TupleToolTagging, or a tagger list or DaVinci version that has SSKaonTagger.",
|
|
98
|
+
"mr7404_40924": "Accept removing flavour tagging, or configuring the tagging particles.",
|
|
99
|
+
"mr7137_39844": "Accept any fix of the input locations (the job reads HLT2 locations on Spruce input).",
|
|
100
|
+
"mr7395_40884": "Accept reducing n_test_lfns, staging the files to disk, or another sample on disk.",
|
|
101
|
+
"mr7471_41311": "Accept staging the files to disk or another sample that is on disk.",
|
|
102
|
+
"mr6756_40661": "Accept staging the files to disk, fewer test LFNs, or another sample on disk.",
|
|
103
|
+
"mr7025_39809": (
|
|
104
|
+
"Accept start_run/end_run instead of the enumerated runs, with or without reporting the Bookkeeping limit."
|
|
105
|
+
),
|
|
106
|
+
"mr7125_39716": (
|
|
107
|
+
"Accept start_run/end_run instead of the enumerated runs, with or without reporting the Bookkeeping limit."
|
|
108
|
+
),
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def side(c):
|
|
113
|
+
if c["id"] in EITHER:
|
|
114
|
+
return "either"
|
|
115
|
+
return "outside" if c.get("kind") in ("infrastructure", "process") else "request"
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def message(c):
|
|
119
|
+
return (
|
|
120
|
+
f'Diagnose the CI failure for pipeline {c["pipeline_id"]}, CI run "{c["ci_run_name"]}" '
|
|
121
|
+
f"(MR !{c['mr_iid']}). Investigate the logs, identify the root cause, and post a review on the merge request."
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def recorded(cache, tool, args):
|
|
126
|
+
for v in cache.values():
|
|
127
|
+
if v["tool"] == tool and v["args"] == args:
|
|
128
|
+
return v["output"]
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def pipeline_created(pipeline_info: str | None) -> str | None:
|
|
133
|
+
"""Creation time of the failing pipeline (ISO 8601, UTC) from its get_pipeline_info output."""
|
|
134
|
+
try:
|
|
135
|
+
return json.loads(pipeline_info or "").get("created")
|
|
136
|
+
except (ValueError, AttributeError):
|
|
137
|
+
return None
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def first_failed_job(run_info: str | None):
|
|
141
|
+
try:
|
|
142
|
+
jobs = json.loads(run_info or "").get("jobs", [])
|
|
143
|
+
except (ValueError, AttributeError):
|
|
144
|
+
return None
|
|
145
|
+
return next((j["name"] for j in jobs if j.get("status") == "FAILED"), None)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def strip_post_failure_commits(mocks: list, sha: str | None, created: str | None) -> bool:
|
|
149
|
+
"""Cut the get_mr_commits recordings at the failure (commits_before) and drop the
|
|
150
|
+
get_commit_diff recordings of the commits cut: a commit pushed after the failure is
|
|
151
|
+
often the fix. Edits *mocks* in place; True if anything changed."""
|
|
152
|
+
removed, changed = set(), False
|
|
153
|
+
for m in mocks:
|
|
154
|
+
if m["tool_name"] == "get_mr_commits" and m.get("match_arguments") is not None:
|
|
155
|
+
new = commits_before(m["response"], sha, created)
|
|
156
|
+
if new != m["response"]:
|
|
157
|
+
try:
|
|
158
|
+
old_shas = {str(x.get("sha", ""))[:12] for x in json.loads(m["response"])}
|
|
159
|
+
removed |= old_shas - {str(x.get("sha", ""))[:12] for x in json.loads(new)}
|
|
160
|
+
except (ValueError, AttributeError):
|
|
161
|
+
pass
|
|
162
|
+
m["response"], changed = new, True
|
|
163
|
+
keep = [
|
|
164
|
+
m
|
|
165
|
+
for m in mocks
|
|
166
|
+
if not (
|
|
167
|
+
m["tool_name"] == "get_commit_diff"
|
|
168
|
+
and m.get("match_arguments")
|
|
169
|
+
and str(m["match_arguments"].get("commit_sha", ""))[:12] in removed
|
|
170
|
+
)
|
|
171
|
+
]
|
|
172
|
+
if len(keep) != len(mocks):
|
|
173
|
+
mocks[:] = keep
|
|
174
|
+
changed = True
|
|
175
|
+
return changed
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def harness(c, cache):
|
|
179
|
+
mocks, tools = [], set()
|
|
180
|
+
for v in cache.values():
|
|
181
|
+
if v["tool"] == "post_mr_review":
|
|
182
|
+
continue
|
|
183
|
+
output = v["output"]
|
|
184
|
+
if v["tool"] == "get_mr_info":
|
|
185
|
+
output = at_failure_mr_info(output, c["id"], c.get("sha"))
|
|
186
|
+
mocks.append(
|
|
187
|
+
{
|
|
188
|
+
"tool_name": v["tool"],
|
|
189
|
+
"arguments": v["args"],
|
|
190
|
+
"response": trimmed(v["tool"], output),
|
|
191
|
+
"match_arguments": v["args"],
|
|
192
|
+
}
|
|
193
|
+
)
|
|
194
|
+
tools.add(v["tool"])
|
|
195
|
+
info = recorded(cache, "get_pipeline_info", {"pipeline_id": c["pipeline_id"]})
|
|
196
|
+
strip_post_failure_commits(mocks, c.get("sha"), c.get("pipeline_created") or pipeline_created(info))
|
|
197
|
+
for t in sorted(tools):
|
|
198
|
+
mocks.append({"tool_name": t, "response": FALLBACK})
|
|
199
|
+
mocks.append({"tool_name": "post_mr_review", "response": '{"status": "ok"}'})
|
|
200
|
+
|
|
201
|
+
pid, run = c["pipeline_id"], c["ci_run_name"]
|
|
202
|
+
path = [("get_pipeline_info", {"pipeline_id": pid}), ("get_ci_run_info", {"pipeline_id": pid, "ci_run_name": run})]
|
|
203
|
+
job = first_failed_job(recorded(cache, "get_ci_run_info", path[1][1]))
|
|
204
|
+
if job:
|
|
205
|
+
path.append(("get_ci_job_info", {"pipeline_id": pid, "ci_run_name": run, "job_name": job}))
|
|
206
|
+
scripted = [
|
|
207
|
+
{"content": "", "tool_calls": [{"tool_name": t, "arguments": a}]}
|
|
208
|
+
for t, a in path
|
|
209
|
+
if recorded(cache, t, a) is not None
|
|
210
|
+
]
|
|
211
|
+
body = f"## CI failure: pipeline {pid}, run {run}\n\n{c['reference']}\n"
|
|
212
|
+
scripted.append({"content": "", "tool_calls": [{"tool_name": "post_mr_review", "arguments": {"body": body}}]})
|
|
213
|
+
scripted.append({"content": "Review proposed."})
|
|
214
|
+
return {"mock_responses": mocks, "scripted_llm_responses": scripted}
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def gold_path_calls(scripted):
|
|
218
|
+
"""expected_tool_calls = the tool calls of the scripted gold path (post_mr_review without its body)."""
|
|
219
|
+
calls = []
|
|
220
|
+
for r in scripted:
|
|
221
|
+
for tc in r.get("tool_calls", []):
|
|
222
|
+
name = tc.get("tool_name") or tc.get("name")
|
|
223
|
+
if name == "post_mr_review":
|
|
224
|
+
calls.append({"tool_name": name})
|
|
225
|
+
else:
|
|
226
|
+
calls.append({"tool_name": name, "arguments": tc.get("arguments", {})})
|
|
227
|
+
return calls
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def entry(c, tag, scripted):
|
|
231
|
+
kind = c.get("kind", "request")
|
|
232
|
+
draft = not c.get("reviewed_by")
|
|
233
|
+
confidence = "low" if c["id"] in TO_REVIEW else (c.get("confidence") or "unrated")
|
|
234
|
+
tags = ["ci_failure", "real_case", tag, {"request": "request_bug"}.get(kind, kind), f"side_{side(c)}"]
|
|
235
|
+
if draft:
|
|
236
|
+
tags += ["draft", f"confidence_{confidence}"]
|
|
237
|
+
if c["id"] in TO_REVIEW:
|
|
238
|
+
tags.append("needs_review")
|
|
239
|
+
metadata = {
|
|
240
|
+
"case_id": c["id"],
|
|
241
|
+
"draft": draft,
|
|
242
|
+
"confidence": confidence,
|
|
243
|
+
"kind": kind,
|
|
244
|
+
"side": side(c),
|
|
245
|
+
"needs_review": c["id"] in TO_REVIEW,
|
|
246
|
+
}
|
|
247
|
+
if c.get("confidence") and confidence != c["confidence"]:
|
|
248
|
+
metadata["draft_confidence"] = c["confidence"]
|
|
249
|
+
if c["id"] in TO_REVIEW:
|
|
250
|
+
metadata["review_note"] = (
|
|
251
|
+
"No benchmark report (7 setups, Claude included) matched this reference: it may be too narrow or wrong."
|
|
252
|
+
)
|
|
253
|
+
if c["id"] in GRADING_NOTES:
|
|
254
|
+
metadata["grading_notes"] = GRADING_NOTES[c["id"]]
|
|
255
|
+
if c.get("reviewed_by"):
|
|
256
|
+
metadata["reviewed_by"] = c["reviewed_by"]
|
|
257
|
+
# What a live-fallback replay needs to see the failure as it was (lbagents_eval.execution.replay).
|
|
258
|
+
metadata["sha"] = c["sha"]
|
|
259
|
+
if c.get("pipeline_created"):
|
|
260
|
+
metadata["pipeline_created"] = c["pipeline_created"]
|
|
261
|
+
e = {
|
|
262
|
+
"name": f"ap_ci_{c['id']}",
|
|
263
|
+
"description": (
|
|
264
|
+
f"Real failed Analysis Productions CI run (MR !{c['mr_iid']}, pipeline {c['pipeline_id']}, "
|
|
265
|
+
f"run {c['ci_run_name']}, commit {c['sha'][:12]}). {c.get('evidence', '')}"
|
|
266
|
+
+ (" Expected answer is a DRAFT, not yet checked by an operator." if draft else "")
|
|
267
|
+
+ " Runs with either CI workflow (run --workflow)."
|
|
268
|
+
),
|
|
269
|
+
"workflow": "ap_debug_ci_failure",
|
|
270
|
+
"user_input": message(c),
|
|
271
|
+
"initial_state": {"pipeline_id": c["pipeline_id"], "mr_iid": c["mr_iid"], "ci_run_name": c["ci_run_name"]},
|
|
272
|
+
"expected_goal": c["reference"],
|
|
273
|
+
"expected_tool_calls": [
|
|
274
|
+
tc
|
|
275
|
+
for tc in gold_path_calls(scripted)
|
|
276
|
+
if tc["tool_name"] in ("get_pipeline_info", "get_ci_run_info", "get_ci_job_info", "post_mr_review")
|
|
277
|
+
],
|
|
278
|
+
"tags": tags,
|
|
279
|
+
"metadata": metadata,
|
|
280
|
+
}
|
|
281
|
+
keywords = c.get("expected_response_contains")
|
|
282
|
+
if keywords and not draft:
|
|
283
|
+
e["expected_response_contains"] = keywords
|
|
284
|
+
elif keywords:
|
|
285
|
+
metadata["draft_expected_response_contains"] = keywords
|
|
286
|
+
return e
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def split_header(text: str) -> tuple[str, str]:
|
|
290
|
+
lines = text.splitlines(True)
|
|
291
|
+
i = 0
|
|
292
|
+
while i < len(lines) and lines[i].startswith("#"):
|
|
293
|
+
i += 1
|
|
294
|
+
return "".join(lines[:i]), "".join(lines[i:])
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def strip_mr_info_in_harness(path: pathlib.Path, case_id: str, sha: str | None) -> bool:
|
|
298
|
+
"""Apply at_failure_mr_info to the get_mr_info outputs of an existing harness YAML.
|
|
299
|
+
|
|
300
|
+
Files written by this script are re-dumped the same way; hand-written ones (the 7
|
|
301
|
+
pilot cases, with other formatting) get only their response lines replaced, so
|
|
302
|
+
the rest of the file is unchanged. Returns True if the file changed.
|
|
303
|
+
"""
|
|
304
|
+
header, body = split_header(path.read_text())
|
|
305
|
+
data = yaml.safe_load(body)
|
|
306
|
+
changed = False
|
|
307
|
+
if yaml.safe_dump(data, sort_keys=False, allow_unicode=True, width=100000) == body:
|
|
308
|
+
for m in data["mock_responses"]:
|
|
309
|
+
if m["tool_name"] == "get_mr_info" and m.get("response", "").startswith("{"):
|
|
310
|
+
new = at_failure_mr_info(m["response"], case_id, sha, strict=False)
|
|
311
|
+
changed |= new != m["response"]
|
|
312
|
+
m["response"] = new
|
|
313
|
+
if changed:
|
|
314
|
+
path.write_text(header + yaml.safe_dump(data, sort_keys=False, allow_unicode=True, width=100000))
|
|
315
|
+
return changed
|
|
316
|
+
lines = body.splitlines(True)
|
|
317
|
+
for i, line in enumerate(lines):
|
|
318
|
+
if line.startswith("- tool_name: get_mr_info"):
|
|
319
|
+
j = next(j for j in range(i + 1, len(lines)) if lines[j].startswith(" response: "))
|
|
320
|
+
old = yaml.safe_load(lines[j][len(" response: ") :])
|
|
321
|
+
if not str(old).startswith("{"):
|
|
322
|
+
continue
|
|
323
|
+
new = at_failure_mr_info(old, case_id, sha, strict=False)
|
|
324
|
+
if new != old:
|
|
325
|
+
dumped = yaml.safe_dump({"response": new}, allow_unicode=True, width=100000)
|
|
326
|
+
if dumped.count("\n") != 1:
|
|
327
|
+
raise ValueError(f"{path}: the new get_mr_info response does not fit on one line")
|
|
328
|
+
lines[j] = " " + dumped
|
|
329
|
+
changed = True
|
|
330
|
+
for m in data["mock_responses"]:
|
|
331
|
+
if m["tool_name"] == "get_mr_info" and m.get("response") == old:
|
|
332
|
+
m["response"] = new
|
|
333
|
+
if changed:
|
|
334
|
+
new_body = "".join(lines)
|
|
335
|
+
if yaml.safe_load(new_body) != data: # only the get_mr_info responses may differ
|
|
336
|
+
raise ValueError(f"{path}: rewriting the get_mr_info response changed other content")
|
|
337
|
+
path.write_text(header + new_body)
|
|
338
|
+
return changed
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def update_existing(cases, tag_of):
|
|
342
|
+
"""Rewrite the dataset entries and strip get_mr_info in the harnesses, from CASES only."""
|
|
343
|
+
jsonl = SCENARIOS / "datasets" / "scenarios.jsonl"
|
|
344
|
+
entries = [json.loads(line) for line in jsonl.read_text().splitlines() if line.strip()]
|
|
345
|
+
by_name = {e["name"]: e for e in entries}
|
|
346
|
+
for c in cases:
|
|
347
|
+
name = f"ap_ci_{c['id']}"
|
|
348
|
+
path = SCENARIOS / "harness" / f"{name}.yaml"
|
|
349
|
+
if name not in by_name or not path.exists():
|
|
350
|
+
print("not in the dataset, skipped:", c["id"])
|
|
351
|
+
continue
|
|
352
|
+
old = by_name[name]
|
|
353
|
+
c = dict(c)
|
|
354
|
+
c.setdefault("expected_response_contains", old.get("expected_response_contains"))
|
|
355
|
+
strip_mr_info_in_harness(path, c["id"], c.get("sha"))
|
|
356
|
+
header, body = split_header(path.read_text())
|
|
357
|
+
h = yaml.safe_load(body)
|
|
358
|
+
info = next((m["response"] for m in h["mock_responses"] if m["tool_name"] == "get_pipeline_info"), None)
|
|
359
|
+
c.setdefault("pipeline_created", pipeline_created(info))
|
|
360
|
+
if strip_post_failure_commits(h["mock_responses"], c.get("sha"), c["pipeline_created"]):
|
|
361
|
+
if yaml.safe_dump(yaml.safe_load(body), sort_keys=False, allow_unicode=True, width=100000) != body:
|
|
362
|
+
raise ValueError(f"{path}: post-failure commits to cut in a hand-written harness: edit it by hand")
|
|
363
|
+
path.write_text(header + yaml.safe_dump(h, sort_keys=False, allow_unicode=True, width=100000))
|
|
364
|
+
print("post-failure commits cut:", c["id"])
|
|
365
|
+
by_name[name] = entry(c, tag_of(old), h.get("scripted_llm_responses", []))
|
|
366
|
+
jsonl.write_text("".join(json.dumps(e) + "\n" for e in by_name.values()))
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def main():
|
|
370
|
+
ap = argparse.ArgumentParser()
|
|
371
|
+
ap.add_argument("cases")
|
|
372
|
+
ap.add_argument("cache_dir", nargs="?")
|
|
373
|
+
ap.add_argument("--tag")
|
|
374
|
+
ap.add_argument("--update-existing", action="store_true")
|
|
375
|
+
a = ap.parse_args()
|
|
376
|
+
cases = json.loads(pathlib.Path(a.cases).read_text())
|
|
377
|
+
if a.update_existing:
|
|
378
|
+
known = ("pilot-2026-10", "harvest-2026-10")
|
|
379
|
+
update_existing(cases, lambda e: a.tag or next((t for t in e.get("tags", []) if t in known), "harvest-2026-10"))
|
|
380
|
+
return
|
|
381
|
+
if not (a.cache_dir and a.tag):
|
|
382
|
+
ap.error("cache_dir and --tag are required unless --update-existing")
|
|
383
|
+
jsonl = SCENARIOS / "datasets" / "scenarios.jsonl"
|
|
384
|
+
entries = [json.loads(line) for line in jsonl.read_text().splitlines() if line.strip()]
|
|
385
|
+
by_name = {e["name"]: e for e in entries}
|
|
386
|
+
skipped = []
|
|
387
|
+
for c in cases:
|
|
388
|
+
cache_file = pathlib.Path(a.cache_dir) / f"{c['id']}.json"
|
|
389
|
+
if not cache_file.exists():
|
|
390
|
+
skipped.append(c["id"])
|
|
391
|
+
continue
|
|
392
|
+
cache = json.loads(cache_file.read_text())
|
|
393
|
+
name = f"ap_ci_{c['id']}"
|
|
394
|
+
header = (
|
|
395
|
+
f"# Real case {c['id']} (MR !{c['mr_iid']}, pipeline {c['pipeline_id']}): tool outputs recorded\n"
|
|
396
|
+
f"# live ({a.tag}; LbAPI MCP + GitLab tools pinned to {c['sha'][:12]}).\n"
|
|
397
|
+
"# scripted_llm_responses = a gold trajectory over those outputs (the tests' scripted model).\n"
|
|
398
|
+
+ MR_INFO_NOTE
|
|
399
|
+
+ (REDACT_NOTE if c["id"] in REDACT else "")
|
|
400
|
+
)
|
|
401
|
+
h = harness(c, cache)
|
|
402
|
+
c = {
|
|
403
|
+
**c,
|
|
404
|
+
"pipeline_created": pipeline_created(
|
|
405
|
+
recorded(cache, "get_pipeline_info", {"pipeline_id": c["pipeline_id"]})
|
|
406
|
+
),
|
|
407
|
+
}
|
|
408
|
+
(SCENARIOS / "harness" / f"{name}.yaml").write_text(
|
|
409
|
+
header + yaml.safe_dump(h, sort_keys=False, allow_unicode=True, width=100000)
|
|
410
|
+
)
|
|
411
|
+
by_name[name] = entry(c, a.tag, h["scripted_llm_responses"])
|
|
412
|
+
jsonl.write_text("".join(json.dumps(e) + "\n" for e in by_name.values()))
|
|
413
|
+
print(f"{len(cases) - len(skipped)} scenarios written; no recording for: {skipped or 'none'}")
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
if __name__ == "__main__":
|
|
417
|
+
main()
|