flintai-cli 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- flintai/__init__.py +0 -0
- flintai/cli/__init__.py +0 -0
- flintai/cli/__main__.py +3 -0
- flintai/cli/builtin_config.json +911 -0
- flintai/cli/console.py +116 -0
- flintai/cli/eval_cli.py +891 -0
- flintai/cli/init_cli.py +144 -0
- flintai/cli/main.py +262 -0
- flintai/cli/rich_observer.py +89 -0
- flintai/cli/runner.py +305 -0
- flintai/cli/scan_cli.py +238 -0
- flintai/cli/utils.py +37 -0
- flintai/cli/version.py +1 -0
- flintai/data/detector_prompts/llm01_adversarial.txt +55 -0
- flintai/data/detector_prompts/llm01_fixed.txt +8 -0
- flintai/data/detector_prompts/llm02_adversarial.txt +75 -0
- flintai/data/detector_prompts/llm02_fixed.txt +8 -0
- flintai/data/detector_prompts/llm05_adversarial.txt +63 -0
- flintai/data/detector_prompts/llm05_fixed.txt +16 -0
- flintai/data/detector_prompts/llm06_adversarial.txt +135 -0
- flintai/data/detector_prompts/llm06_fixed.txt +15 -0
- flintai/data/detector_prompts/llm07_adversarial.txt +185 -0
- flintai/data/detector_prompts/llm07_fixed.txt +14 -0
- flintai/data/detector_prompts/llm09_adversarial.txt +77 -0
- flintai/data/detector_prompts/llm09_fixed.txt +15 -0
- flintai/data/llm01_prompt_injection.csv +6708 -0
- flintai/data/llm02_sensitive_information.csv +1069 -0
- flintai/data/llm05_unsafe_output.csv +3882 -0
- flintai/data/llm06_excessive_agency.csv +778 -0
- flintai/data/llm07_system_prompt_leakage.csv +971 -0
- flintai/data/llm09_hallucination-goals.csv +32419 -0
- flintai/data/llm09_hallucination.csv +599 -0
- flintai/data/pii_leakage.csv +706 -0
- flintai/data/secret_leakage.csv +1380 -0
- flintai/eval/__init__.py +0 -0
- flintai/eval/common/converter_anthropic.py +151 -0
- flintai/eval/common/converter_genai.py +169 -0
- flintai/eval/common/converter_openai.py +165 -0
- flintai/eval/common/log.py +96 -0
- flintai/eval/common/reference.py +25 -0
- flintai/eval/common/schema.py +192 -0
- flintai/eval/common/utils.py +74 -0
- flintai/eval/core/__init__.py +0 -0
- flintai/eval/core/detectors/__init__.py +1 -0
- flintai/eval/core/detectors/detector.py +25 -0
- flintai/eval/core/detectors/detector_garak.py +80 -0
- flintai/eval/core/detectors/detector_model.py +76 -0
- flintai/eval/core/detectors/detector_pii.py +64 -0
- flintai/eval/core/detectors/detector_secret.py +82 -0
- flintai/eval/core/detectors/detector_topic_guard.py +70 -0
- flintai/eval/core/detectors/detector_toxicity.py +70 -0
- flintai/eval/core/eval/__init__.py +0 -0
- flintai/eval/core/eval/eval_creator.py +197 -0
- flintai/eval/core/eval/evaluation.py +100 -0
- flintai/eval/core/eval/evaluation_adversarial.py +523 -0
- flintai/eval/core/eval/evaluation_garak_module.py +52 -0
- flintai/eval/core/eval/evaluation_garak_probe.py +274 -0
- flintai/eval/core/eval/evaluation_message_list.py +44 -0
- flintai/eval/core/eval/evaluation_multi.py +207 -0
- flintai/eval/core/eval/evaluation_single.py +79 -0
- flintai/eval/core/eval/evaluation_single_prompt.py +58 -0
- flintai/eval/core/eval/evaluation_topic_guard.py +318 -0
- flintai/eval/core/eval/metric_conciseness.py +181 -0
- flintai/eval/core/eval/metric_factual_accuracy.py +294 -0
- flintai/eval/core/eval/metric_instruction_adherence.py +180 -0
- flintai/eval/core/eval/metric_tone.py +183 -0
- flintai/eval/core/eval/metric_toxicity.py +147 -0
- flintai/eval/core/eval/observer.py +136 -0
- flintai/eval/core/eval/probe_adversarial.py +14 -0
- flintai/eval/core/message/__init__.py +0 -0
- flintai/eval/core/message/message_collection.py +37 -0
- flintai/eval/core/message/message_collection_csv.py +46 -0
- flintai/eval/core/message/message_collection_garak.py +52 -0
- flintai/eval/core/message/message_collection_memory.py +36 -0
- flintai/eval/core/models/__init__.py +0 -0
- flintai/eval/core/models/generator_model.py +88 -0
- flintai/eval/core/models/model.py +111 -0
- flintai/eval/core/models/model_adk.py +158 -0
- flintai/eval/core/models/model_anthropic.py +51 -0
- flintai/eval/core/models/model_anthropic_agent.py +92 -0
- flintai/eval/core/models/model_gemini.py +107 -0
- flintai/eval/core/models/model_generic_http.py +117 -0
- flintai/eval/core/models/model_huggingface.py +61 -0
- flintai/eval/core/models/model_langserve.py +94 -0
- flintai/eval/core/models/model_litellm.py +38 -0
- flintai/eval/core/models/model_ollama.py +50 -0
- flintai/eval/core/models/model_openai.py +35 -0
- flintai/eval/core/models/model_openai_agent.py +80 -0
- flintai/eval/core/models/model_openai_compatible.py +57 -0
- flintai/eval/core/models/model_retry.py +134 -0
- flintai/eval/core/models/model_sync_wrapper.py +35 -0
- flintai/eval/db/__init__.py +0 -0
- flintai/eval/db/base/__init__.py +0 -0
- flintai/eval/db/base/detectors/__init__.py +1 -0
- flintai/eval/db/base/detectors/detector_helpers.py +78 -0
- flintai/eval/db/base/detectors/detector_repository.py +91 -0
- flintai/eval/db/base/detectors/detector_types.py +77 -0
- flintai/eval/db/base/eval/__init__.py +1 -0
- flintai/eval/db/base/eval/eval_helpers.py +227 -0
- flintai/eval/db/base/eval/eval_repository.py +115 -0
- flintai/eval/db/base/eval/eval_run.py +139 -0
- flintai/eval/db/base/eval/eval_types.py +129 -0
- flintai/eval/db/base/eval/model_eval_repository.py +78 -0
- flintai/eval/db/base/eval/model_eval_run_repository.py +70 -0
- flintai/eval/db/base/eval/model_eval_run_result_repository.py +40 -0
- flintai/eval/db/base/eval/model_eval_run_result_types.py +21 -0
- flintai/eval/db/base/eval/model_eval_run_types.py +84 -0
- flintai/eval/db/base/eval/model_eval_types.py +71 -0
- flintai/eval/db/base/message/__init__.py +0 -0
- flintai/eval/db/base/message/message_collection_helpers.py +61 -0
- flintai/eval/db/base/message/message_collection_repository.py +92 -0
- flintai/eval/db/base/message/message_collection_types.py +89 -0
- flintai/eval/db/base/models/__init__.py +0 -0
- flintai/eval/db/base/models/model_helpers.py +172 -0
- flintai/eval/db/base/models/model_repository.py +95 -0
- flintai/eval/db/base/models/model_types.py +156 -0
- flintai/eval/db/json/__init__.py +0 -0
- flintai/eval/db/json/repository_json.py +603 -0
- flintai/scan/__init__.py +0 -0
- flintai/scan/agent_scanner.py +797 -0
- flintai/scan/config/agent_opengrep_rules.yaml +533 -0
- flintai/scan/config/agent_taxonomy.json +481 -0
- flintai/scan/config/agentic_cvss_mapping.yaml +136 -0
- flintai/scan/config/compliance_mappings.json +110 -0
- flintai/scan/config/triage_prompt.txt +335 -0
- flintai/scan/constants.py +15 -0
- flintai/scan/file_filter.py +156 -0
- flintai/scan/llm_provider.py +197 -0
- flintai/scan/opengrep_resolver.py +12 -0
- flintai/scan/reasoner.py +552 -0
- flintai/scan/schema.py +337 -0
- flintai/scan/scorer.py +371 -0
- flintai/scan/secret_anonymizer.py +72 -0
- flintai/scan/static_scanner.py +542 -0
- flintai/scan/taxonomy.py +75 -0
- flintai/scan/tool_dispatcher.py +754 -0
- flintai/scan/trace_logger.py +118 -0
- flintai/scan/trace_logger_file.py +143 -0
- flintai/scan/trace_logger_log.py +100 -0
- flintai/scan/triage.py +496 -0
- flintai/schema.py +58 -0
- flintai_cli-1.0.0.dist-info/METADATA +442 -0
- flintai_cli-1.0.0.dist-info/RECORD +147 -0
- flintai_cli-1.0.0.dist-info/WHEEL +5 -0
- flintai_cli-1.0.0.dist-info/entry_points.txt +2 -0
- flintai_cli-1.0.0.dist-info/licenses/LICENSE +210 -0
- flintai_cli-1.0.0.dist-info/top_level.txt +1 -0
flintai/cli/eval_cli.py
ADDED
|
@@ -0,0 +1,891 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Subcommands for `flintai eval`.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import argparse
|
|
8
|
+
import sys
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
|
|
11
|
+
from rich.panel import Panel
|
|
12
|
+
from rich.table import Table
|
|
13
|
+
from rich.text import Text
|
|
14
|
+
|
|
15
|
+
from flintai.cli.console import CLI_WIDTH, console
|
|
16
|
+
from flintai.cli.utils import get_flintai_config_path
|
|
17
|
+
from flintai.eval.core.eval.evaluation import EvaluationStatus
|
|
18
|
+
from flintai.eval.db.base.eval.model_eval_types import DbModelEvaluation
|
|
19
|
+
from flintai.eval.db.json.repository_json import JsonRepository
|
|
20
|
+
from flintai.cli.rich_observer import create_progress
|
|
21
|
+
from flintai.cli.runner import (
|
|
22
|
+
MAX_CONSECUTIVE_RUN_FAILURES,
|
|
23
|
+
CliRunResult,
|
|
24
|
+
log_run_summary,
|
|
25
|
+
print_results,
|
|
26
|
+
run_cli_evaluation,
|
|
27
|
+
write_output,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
_DEFAULT_CONFIG = str(get_flintai_config_path())
|
|
31
|
+
|
|
32
|
+
_DEFAULT_LOG = f"flintai_{datetime.now().strftime('%Y%m%dT%H%M%S')}.log"
|
|
33
|
+
|
|
34
|
+
_config_parent = argparse.ArgumentParser(add_help=False)
|
|
35
|
+
_config_parent.add_argument(
|
|
36
|
+
"--config",
|
|
37
|
+
default=_DEFAULT_CONFIG,
|
|
38
|
+
help="Path to JSON config file "
|
|
39
|
+
f"(default: {_DEFAULT_CONFIG})",
|
|
40
|
+
)
|
|
41
|
+
_config_parent.add_argument(
|
|
42
|
+
"--log",
|
|
43
|
+
help="Log file path "
|
|
44
|
+
f"(default: {_DEFAULT_LOG})",
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
_tag_parent = argparse.ArgumentParser(add_help=False)
|
|
49
|
+
_tag_parent.add_argument(
|
|
50
|
+
"--tag", action="append", default=[],
|
|
51
|
+
metavar="KEY=VALUE",
|
|
52
|
+
help="Filter by tag (repeatable)",
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _parse_tags(raw: list[str]) -> dict[str, str]:
|
|
57
|
+
tags: dict[str, str] = {}
|
|
58
|
+
for item in raw:
|
|
59
|
+
if "=" not in item:
|
|
60
|
+
console.print(
|
|
61
|
+
f"[red]Invalid tag format: {item!r} "
|
|
62
|
+
f"(expected KEY=VALUE)[/red]",
|
|
63
|
+
)
|
|
64
|
+
sys.exit(1)
|
|
65
|
+
k, v = item.split("=", 1)
|
|
66
|
+
tags[k] = v
|
|
67
|
+
return tags
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _matches_tags(
|
|
71
|
+
entity_tags: dict[str, str],
|
|
72
|
+
filter_tags: dict[str, str],
|
|
73
|
+
) -> bool:
|
|
74
|
+
return all(
|
|
75
|
+
entity_tags.get(k) == v
|
|
76
|
+
for k, v in filter_tags.items()
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def register(subparsers: argparse._SubParsersAction) -> None:
|
|
81
|
+
eval_parser = subparsers.add_parser(
|
|
82
|
+
"eval", help="Evaluation commands",
|
|
83
|
+
)
|
|
84
|
+
eval_sub = eval_parser.add_subparsers(dest="eval_cmd")
|
|
85
|
+
eval_sub.required = True
|
|
86
|
+
|
|
87
|
+
_register_models(eval_sub)
|
|
88
|
+
_register_evaluations(eval_sub)
|
|
89
|
+
_register_model_evaluations(eval_sub)
|
|
90
|
+
_register_run(eval_sub)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# -- models -------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
def _register_models(
|
|
96
|
+
subparsers: argparse._SubParsersAction,
|
|
97
|
+
) -> None:
|
|
98
|
+
models_parser = subparsers.add_parser(
|
|
99
|
+
"models", help="Model commands",
|
|
100
|
+
)
|
|
101
|
+
models_sub = models_parser.add_subparsers(
|
|
102
|
+
dest="models_cmd",
|
|
103
|
+
)
|
|
104
|
+
models_sub.required = True
|
|
105
|
+
|
|
106
|
+
models_sub.add_parser(
|
|
107
|
+
"list", help="List all models",
|
|
108
|
+
parents=[_config_parent, _tag_parent],
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
show_parser = models_sub.add_parser(
|
|
112
|
+
"show", help="Show model details",
|
|
113
|
+
parents=[_config_parent],
|
|
114
|
+
)
|
|
115
|
+
show_parser.add_argument("id", help="Model ID")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def handle_models(
|
|
119
|
+
args: argparse.Namespace, store: JsonRepository,
|
|
120
|
+
) -> None:
|
|
121
|
+
if args.models_cmd == "list":
|
|
122
|
+
_models_list(store, _parse_tags(args.tag))
|
|
123
|
+
elif args.models_cmd == "show":
|
|
124
|
+
_models_show(store, args.id)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _models_list(
|
|
128
|
+
store: JsonRepository,
|
|
129
|
+
filter_tags: dict[str, str] | None = None,
|
|
130
|
+
) -> None:
|
|
131
|
+
models = store.models.list()
|
|
132
|
+
if filter_tags:
|
|
133
|
+
models = [
|
|
134
|
+
m for m in models
|
|
135
|
+
if _matches_tags(m.tags, filter_tags)
|
|
136
|
+
]
|
|
137
|
+
if not models:
|
|
138
|
+
console.print("[dim]No models configured.[/dim]")
|
|
139
|
+
return
|
|
140
|
+
table = Table(title="Models", show_lines=False, width=CLI_WIDTH)
|
|
141
|
+
table.add_column("ID", style="dim", ratio=2, overflow="ellipsis", no_wrap=True)
|
|
142
|
+
table.add_column("Name", style="bold", ratio=3, overflow="ellipsis", no_wrap=True)
|
|
143
|
+
table.add_column("Type", ratio=1, overflow="ellipsis", no_wrap=True)
|
|
144
|
+
for m in models:
|
|
145
|
+
table.add_row(m.id, m.name, m.type.value)
|
|
146
|
+
console.print(table)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _models_show(store: JsonRepository, model_id: str) -> None:
|
|
150
|
+
model_id = _resolve_id(
|
|
151
|
+
model_id,
|
|
152
|
+
[m.id for m in store.models.list()],
|
|
153
|
+
)
|
|
154
|
+
model = store.models.get(model_id)
|
|
155
|
+
|
|
156
|
+
detail = Table.grid(padding=(0, 2))
|
|
157
|
+
detail.add_column(style="key", justify="right")
|
|
158
|
+
detail.add_column()
|
|
159
|
+
detail.add_row("ID", model.id)
|
|
160
|
+
detail.add_row("Name", model.name)
|
|
161
|
+
detail.add_row("Type", model.type.value)
|
|
162
|
+
detail.add_row("Model name", model.model_name)
|
|
163
|
+
if model.host:
|
|
164
|
+
detail.add_row("Host", model.host)
|
|
165
|
+
if model.endpoint:
|
|
166
|
+
detail.add_row("Endpoint", model.endpoint)
|
|
167
|
+
if model.tags:
|
|
168
|
+
detail.add_row("Tags", _fmt_tags(model.tags))
|
|
169
|
+
if model.description:
|
|
170
|
+
detail.add_row("Description", model.description)
|
|
171
|
+
|
|
172
|
+
console.print(Panel(detail, title=f"[bold]{model.name}[/bold]", expand=False))
|
|
173
|
+
|
|
174
|
+
me_view = store.model_evaluations.list_by_model(
|
|
175
|
+
model.id, limit=100,
|
|
176
|
+
)
|
|
177
|
+
if me_view.items:
|
|
178
|
+
table = Table(
|
|
179
|
+
title=f"Connected evaluations ({me_view.total})",
|
|
180
|
+
width=CLI_WIDTH,
|
|
181
|
+
)
|
|
182
|
+
table.add_column("ID", style="dim", ratio=2, overflow="ellipsis", no_wrap=True)
|
|
183
|
+
table.add_column("Name", style="bold", ratio=3, overflow="ellipsis", no_wrap=True)
|
|
184
|
+
table.add_column("Evaluation", ratio=3, overflow="ellipsis", no_wrap=True)
|
|
185
|
+
for item in me_view.items:
|
|
186
|
+
eval_name = item.evaluation_ref.name if item.evaluation_ref else "?"
|
|
187
|
+
eval_cell = Text(eval_name)
|
|
188
|
+
eval_cell.append("\n")
|
|
189
|
+
eval_cell.append(item.config.evaluation_id, style="dim")
|
|
190
|
+
table.add_row(item.config.id, item.config.name, eval_cell)
|
|
191
|
+
console.print(table)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
# -- evaluations ---------------------------------------------------
|
|
195
|
+
|
|
196
|
+
def _register_evaluations(
|
|
197
|
+
subparsers: argparse._SubParsersAction,
|
|
198
|
+
) -> None:
|
|
199
|
+
evals_parser = subparsers.add_parser(
|
|
200
|
+
"evaluations", help="Evaluation commands",
|
|
201
|
+
)
|
|
202
|
+
evals_sub = evals_parser.add_subparsers(
|
|
203
|
+
dest="evals_cmd",
|
|
204
|
+
)
|
|
205
|
+
evals_sub.required = True
|
|
206
|
+
|
|
207
|
+
evals_sub.add_parser(
|
|
208
|
+
"list", help="List all evaluations",
|
|
209
|
+
parents=[_config_parent, _tag_parent],
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
show_parser = evals_sub.add_parser(
|
|
213
|
+
"show", help="Show evaluation details",
|
|
214
|
+
parents=[_config_parent],
|
|
215
|
+
)
|
|
216
|
+
show_parser.add_argument("id", help="Evaluation ID")
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def handle_evaluations(
|
|
220
|
+
args: argparse.Namespace, store: JsonRepository,
|
|
221
|
+
) -> None:
|
|
222
|
+
if args.evals_cmd == "list":
|
|
223
|
+
_evaluations_list(store, _parse_tags(args.tag))
|
|
224
|
+
elif args.evals_cmd == "show":
|
|
225
|
+
_evaluations_show(store, args.id)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _evaluations_list(
|
|
229
|
+
store: JsonRepository,
|
|
230
|
+
filter_tags: dict[str, str] | None = None,
|
|
231
|
+
) -> None:
|
|
232
|
+
evaluations = store.evaluations.list()
|
|
233
|
+
if filter_tags:
|
|
234
|
+
evaluations = [
|
|
235
|
+
e for e in evaluations
|
|
236
|
+
if _matches_tags(e.tags, filter_tags)
|
|
237
|
+
]
|
|
238
|
+
if not evaluations:
|
|
239
|
+
console.print("[dim]No evaluations configured.[/dim]")
|
|
240
|
+
return
|
|
241
|
+
table = Table(title="Evaluations", show_lines=False, width=CLI_WIDTH)
|
|
242
|
+
table.add_column("ID", style="dim", ratio=2)
|
|
243
|
+
table.add_column("Name", style="bold", ratio=3, overflow="ellipsis", no_wrap=True)
|
|
244
|
+
table.add_column("Type", ratio=2, overflow="ellipsis", no_wrap=True)
|
|
245
|
+
for e in evaluations:
|
|
246
|
+
table.add_row(e.id, e.name, e.type.value)
|
|
247
|
+
console.print(table)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _evaluations_show(
|
|
251
|
+
store: JsonRepository, eval_id: str,
|
|
252
|
+
) -> None:
|
|
253
|
+
eval_id = _resolve_id(
|
|
254
|
+
eval_id,
|
|
255
|
+
[e.id for e in store.evaluations.list()],
|
|
256
|
+
)
|
|
257
|
+
evaluation = store.evaluations.get(eval_id)
|
|
258
|
+
|
|
259
|
+
detail = Table.grid(padding=(0, 2))
|
|
260
|
+
detail.add_column(style="key", justify="right")
|
|
261
|
+
detail.add_column()
|
|
262
|
+
detail.add_row("ID", evaluation.id)
|
|
263
|
+
detail.add_row("Name", evaluation.name)
|
|
264
|
+
detail.add_row("Type", evaluation.type.value)
|
|
265
|
+
detail.add_row("Approach", evaluation.approach.value)
|
|
266
|
+
if evaluation.tags:
|
|
267
|
+
detail.add_row("Tags", _fmt_tags(evaluation.tags))
|
|
268
|
+
if evaluation.description:
|
|
269
|
+
detail.add_row("Description", evaluation.description)
|
|
270
|
+
if evaluation.adversarial_goals:
|
|
271
|
+
prompts = evaluation.num_prompts or 5
|
|
272
|
+
turns = evaluation.max_turns or 5
|
|
273
|
+
detail.add_row("Prompts", str(prompts))
|
|
274
|
+
detail.add_row("Max turns", str(turns))
|
|
275
|
+
|
|
276
|
+
console.print(Panel(detail, title=f"[bold]{evaluation.name}[/bold]", expand=False))
|
|
277
|
+
|
|
278
|
+
me_view = store.model_evaluations.list_by_evaluation(
|
|
279
|
+
evaluation.id, limit=100,
|
|
280
|
+
)
|
|
281
|
+
if me_view.items:
|
|
282
|
+
table = Table(
|
|
283
|
+
title=f"Connected models ({me_view.total})",
|
|
284
|
+
width=CLI_WIDTH,
|
|
285
|
+
)
|
|
286
|
+
table.add_column("ID", style="dim", ratio=2, overflow="ellipsis", no_wrap=True)
|
|
287
|
+
table.add_column("Name", style="bold", ratio=3, overflow="ellipsis", no_wrap=True)
|
|
288
|
+
table.add_column("Model", ratio=3, overflow="ellipsis", no_wrap=True)
|
|
289
|
+
for item in me_view.items:
|
|
290
|
+
model_name = item.model_ref.name if item.model_ref else "?"
|
|
291
|
+
model_cell = Text(model_name)
|
|
292
|
+
model_cell.append("\n")
|
|
293
|
+
model_cell.append(item.config.model_id, style="dim")
|
|
294
|
+
table.add_row(item.config.id, item.config.name, model_cell)
|
|
295
|
+
console.print(table)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
# -- model-evaluations --------------------------------------------
|
|
299
|
+
|
|
300
|
+
_model_selector_parent = argparse.ArgumentParser(
|
|
301
|
+
add_help=False,
|
|
302
|
+
)
|
|
303
|
+
_model_selector_parent.add_argument(
|
|
304
|
+
"--model", action="append", default=[],
|
|
305
|
+
metavar="ID",
|
|
306
|
+
help="Model ID (repeatable)",
|
|
307
|
+
)
|
|
308
|
+
_model_selector_parent.add_argument(
|
|
309
|
+
"--model-tag", action="append", default=[],
|
|
310
|
+
metavar="KEY=VALUE",
|
|
311
|
+
help="Match models by tag (repeatable)",
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
_eval_selector_parent = argparse.ArgumentParser(
|
|
315
|
+
add_help=False,
|
|
316
|
+
)
|
|
317
|
+
_eval_selector_parent.add_argument(
|
|
318
|
+
"--eval", action="append", default=[],
|
|
319
|
+
metavar="ID",
|
|
320
|
+
help="Evaluation ID (repeatable)",
|
|
321
|
+
)
|
|
322
|
+
_eval_selector_parent.add_argument(
|
|
323
|
+
"--eval-tag", action="append", default=[],
|
|
324
|
+
metavar="KEY=VALUE",
|
|
325
|
+
help="Match evaluations by tag (repeatable)",
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _register_model_evaluations(
|
|
330
|
+
subparsers: argparse._SubParsersAction,
|
|
331
|
+
) -> None:
|
|
332
|
+
me_parser = subparsers.add_parser(
|
|
333
|
+
"model-evaluations",
|
|
334
|
+
help="Model-evaluation assignment commands",
|
|
335
|
+
)
|
|
336
|
+
me_sub = me_parser.add_subparsers(dest="me_cmd")
|
|
337
|
+
me_sub.required = True
|
|
338
|
+
me_sub.add_parser(
|
|
339
|
+
"list",
|
|
340
|
+
help="List all model-evaluation assignments",
|
|
341
|
+
parents=[
|
|
342
|
+
_config_parent,
|
|
343
|
+
_tag_parent,
|
|
344
|
+
_model_selector_parent,
|
|
345
|
+
_eval_selector_parent,
|
|
346
|
+
],
|
|
347
|
+
)
|
|
348
|
+
me_sub.add_parser(
|
|
349
|
+
"attach",
|
|
350
|
+
help="Assign evaluations to models",
|
|
351
|
+
parents=[
|
|
352
|
+
_config_parent,
|
|
353
|
+
_model_selector_parent,
|
|
354
|
+
_eval_selector_parent,
|
|
355
|
+
],
|
|
356
|
+
)
|
|
357
|
+
me_sub.add_parser(
|
|
358
|
+
"detach",
|
|
359
|
+
help="Remove evaluation assignments from models",
|
|
360
|
+
parents=[
|
|
361
|
+
_config_parent,
|
|
362
|
+
_model_selector_parent,
|
|
363
|
+
_eval_selector_parent,
|
|
364
|
+
],
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def handle_model_evaluations(
|
|
369
|
+
args: argparse.Namespace,
|
|
370
|
+
store: JsonRepository,
|
|
371
|
+
user_store: JsonRepository,
|
|
372
|
+
) -> None:
|
|
373
|
+
if args.me_cmd == "list":
|
|
374
|
+
_model_evaluations_list(
|
|
375
|
+
store,
|
|
376
|
+
filter_tags=_parse_tags(args.tag),
|
|
377
|
+
models=_resolve_models(args, store),
|
|
378
|
+
evaluations=_resolve_evaluations(args, store),
|
|
379
|
+
)
|
|
380
|
+
elif args.me_cmd == "attach":
|
|
381
|
+
_model_evaluations_attach(args, store, user_store)
|
|
382
|
+
elif args.me_cmd == "detach":
|
|
383
|
+
_model_evaluations_detach(args, store, user_store)
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def _model_evaluations_list(
|
|
387
|
+
store: JsonRepository,
|
|
388
|
+
filter_tags: dict[str, str] | None = None,
|
|
389
|
+
models: list | None = None,
|
|
390
|
+
evaluations: list | None = None,
|
|
391
|
+
) -> None:
|
|
392
|
+
items = store.model_evaluations.list_all()
|
|
393
|
+
if filter_tags:
|
|
394
|
+
items = [
|
|
395
|
+
me for me in items
|
|
396
|
+
if _matches_tags(me.tags, filter_tags)
|
|
397
|
+
]
|
|
398
|
+
if models:
|
|
399
|
+
model_ids = {m.id for m in models}
|
|
400
|
+
items = [me for me in items if me.model_id in model_ids]
|
|
401
|
+
if evaluations:
|
|
402
|
+
eval_ids = {e.id for e in evaluations}
|
|
403
|
+
items = [me for me in items if me.evaluation_id in eval_ids]
|
|
404
|
+
if not items:
|
|
405
|
+
console.print("[dim]No model-evaluation assignments configured.[/dim]")
|
|
406
|
+
return
|
|
407
|
+
|
|
408
|
+
table = Table(
|
|
409
|
+
title="Model-Evaluation Assignments",
|
|
410
|
+
show_lines=False, width=CLI_WIDTH,
|
|
411
|
+
)
|
|
412
|
+
table.add_column("ID", style="dim", ratio=2, overflow="ellipsis", no_wrap=True)
|
|
413
|
+
table.add_column("Name", style="bold", ratio=2, overflow="ellipsis", no_wrap=True)
|
|
414
|
+
table.add_column("Model", ratio=2, overflow="ellipsis", no_wrap=True)
|
|
415
|
+
table.add_column("Evaluation", ratio=3, overflow="ellipsis", no_wrap=True)
|
|
416
|
+
|
|
417
|
+
for me in items:
|
|
418
|
+
model_name = "?"
|
|
419
|
+
eval_name = "?"
|
|
420
|
+
try:
|
|
421
|
+
model_name = store.models.get(
|
|
422
|
+
me.model_id,
|
|
423
|
+
).name
|
|
424
|
+
except KeyError:
|
|
425
|
+
pass
|
|
426
|
+
try:
|
|
427
|
+
eval_name = store.evaluations.get(
|
|
428
|
+
me.evaluation_id,
|
|
429
|
+
).name
|
|
430
|
+
except KeyError:
|
|
431
|
+
pass
|
|
432
|
+
|
|
433
|
+
model_cell = Text(model_name)
|
|
434
|
+
model_cell.append("\n")
|
|
435
|
+
model_cell.append(me.model_id, style="dim")
|
|
436
|
+
|
|
437
|
+
eval_cell = Text(eval_name)
|
|
438
|
+
eval_cell.append("\n")
|
|
439
|
+
eval_cell.append(me.evaluation_id, style="dim")
|
|
440
|
+
|
|
441
|
+
table.add_row(me.id, me.name, model_cell, eval_cell)
|
|
442
|
+
|
|
443
|
+
console.print(table)
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def _resolve_models(
|
|
447
|
+
args: argparse.Namespace,
|
|
448
|
+
store: JsonRepository,
|
|
449
|
+
) -> list:
|
|
450
|
+
model_ids = args.model
|
|
451
|
+
model_tags = _parse_tags(args.model_tag)
|
|
452
|
+
|
|
453
|
+
if not model_ids and not model_tags:
|
|
454
|
+
return []
|
|
455
|
+
|
|
456
|
+
models = []
|
|
457
|
+
seen_ids: set[str] = set()
|
|
458
|
+
|
|
459
|
+
for mid in model_ids:
|
|
460
|
+
resolved = _resolve_id(
|
|
461
|
+
mid, [m.id for m in store.models.list()],
|
|
462
|
+
)
|
|
463
|
+
try:
|
|
464
|
+
m = store.models.get(resolved)
|
|
465
|
+
if m.id not in seen_ids:
|
|
466
|
+
models.append(m)
|
|
467
|
+
seen_ids.add(m.id)
|
|
468
|
+
except KeyError:
|
|
469
|
+
console.print(
|
|
470
|
+
f"[red]Model {mid!r} not found.[/red]",
|
|
471
|
+
)
|
|
472
|
+
sys.exit(1)
|
|
473
|
+
|
|
474
|
+
if model_tags:
|
|
475
|
+
for m in store.models.list():
|
|
476
|
+
if (
|
|
477
|
+
m.id not in seen_ids
|
|
478
|
+
and _matches_tags(m.tags, model_tags)
|
|
479
|
+
):
|
|
480
|
+
models.append(m)
|
|
481
|
+
seen_ids.add(m.id)
|
|
482
|
+
|
|
483
|
+
return models
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def _resolve_evaluations(
|
|
487
|
+
args: argparse.Namespace,
|
|
488
|
+
store: JsonRepository,
|
|
489
|
+
) -> list:
|
|
490
|
+
eval_ids = getattr(args, "eval", [])
|
|
491
|
+
eval_tags = _parse_tags(args.eval_tag)
|
|
492
|
+
|
|
493
|
+
if not eval_ids and not eval_tags:
|
|
494
|
+
return []
|
|
495
|
+
|
|
496
|
+
evaluations = []
|
|
497
|
+
seen_ids: set[str] = set()
|
|
498
|
+
|
|
499
|
+
for eid in eval_ids:
|
|
500
|
+
resolved = _resolve_id(
|
|
501
|
+
eid, [e.id for e in store.evaluations.list()],
|
|
502
|
+
)
|
|
503
|
+
try:
|
|
504
|
+
e = store.evaluations.get(resolved)
|
|
505
|
+
if e.id not in seen_ids:
|
|
506
|
+
evaluations.append(e)
|
|
507
|
+
seen_ids.add(e.id)
|
|
508
|
+
except KeyError:
|
|
509
|
+
console.print(
|
|
510
|
+
f"[red]Evaluation {eid!r} not found.[/red]",
|
|
511
|
+
)
|
|
512
|
+
sys.exit(1)
|
|
513
|
+
|
|
514
|
+
if eval_tags:
|
|
515
|
+
for e in store.evaluations.list():
|
|
516
|
+
if (
|
|
517
|
+
e.id not in seen_ids
|
|
518
|
+
and _matches_tags(e.tags, eval_tags)
|
|
519
|
+
):
|
|
520
|
+
evaluations.append(e)
|
|
521
|
+
seen_ids.add(e.id)
|
|
522
|
+
|
|
523
|
+
return evaluations
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
def _model_evaluations_attach(
|
|
527
|
+
args: argparse.Namespace,
|
|
528
|
+
store: JsonRepository,
|
|
529
|
+
user_store: JsonRepository,
|
|
530
|
+
) -> None:
|
|
531
|
+
models = _resolve_models(args, store)
|
|
532
|
+
evaluations = _resolve_evaluations(args, store)
|
|
533
|
+
|
|
534
|
+
if not models:
|
|
535
|
+
console.print(
|
|
536
|
+
"[red]No models matched. Specify --model "
|
|
537
|
+
"or --model-tag.[/red]",
|
|
538
|
+
)
|
|
539
|
+
return
|
|
540
|
+
if not evaluations:
|
|
541
|
+
console.print(
|
|
542
|
+
"[red]No evaluations matched. Specify --eval "
|
|
543
|
+
"or --eval-tag.[/red]",
|
|
544
|
+
)
|
|
545
|
+
return
|
|
546
|
+
|
|
547
|
+
added: list[DbModelEvaluation] = []
|
|
548
|
+
skipped = 0
|
|
549
|
+
|
|
550
|
+
for m in models:
|
|
551
|
+
for e in evaluations:
|
|
552
|
+
me = DbModelEvaluation(
|
|
553
|
+
model_id=m.id,
|
|
554
|
+
evaluation_id=e.id,
|
|
555
|
+
name=f"{m.name} / {e.name}",
|
|
556
|
+
)
|
|
557
|
+
if user_store.model_evaluations.add(me):
|
|
558
|
+
added.append(me)
|
|
559
|
+
else:
|
|
560
|
+
skipped += 1
|
|
561
|
+
|
|
562
|
+
if added:
|
|
563
|
+
user_store.save()
|
|
564
|
+
table = Table(
|
|
565
|
+
title=f"Attached ({len(added)} new)",
|
|
566
|
+
show_lines=False, width=CLI_WIDTH,
|
|
567
|
+
)
|
|
568
|
+
table.add_column("Model", ratio=2)
|
|
569
|
+
table.add_column("Evaluation", ratio=3)
|
|
570
|
+
for me in added:
|
|
571
|
+
table.add_row(
|
|
572
|
+
_name_for(store, "model", me.model_id),
|
|
573
|
+
_name_for(store, "eval", me.evaluation_id),
|
|
574
|
+
)
|
|
575
|
+
console.print(table)
|
|
576
|
+
|
|
577
|
+
if skipped:
|
|
578
|
+
console.print(
|
|
579
|
+
f"[dim]{skipped} already attached, skipped.[/dim]",
|
|
580
|
+
)
|
|
581
|
+
if not added and not skipped:
|
|
582
|
+
console.print("[dim]Nothing to attach.[/dim]")
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
def _model_evaluations_detach(
|
|
586
|
+
args: argparse.Namespace,
|
|
587
|
+
store: JsonRepository,
|
|
588
|
+
user_store: JsonRepository,
|
|
589
|
+
) -> None:
|
|
590
|
+
models = _resolve_models(args, store)
|
|
591
|
+
evaluations = _resolve_evaluations(args, store)
|
|
592
|
+
|
|
593
|
+
if not models and not evaluations:
|
|
594
|
+
console.print(
|
|
595
|
+
"[red]Specify at least --model/--model-tag "
|
|
596
|
+
"or --eval/--eval-tag.[/red]",
|
|
597
|
+
)
|
|
598
|
+
return
|
|
599
|
+
|
|
600
|
+
model_ids = {m.id for m in models} if models else None
|
|
601
|
+
eval_ids = (
|
|
602
|
+
{e.id for e in evaluations} if evaluations else None
|
|
603
|
+
)
|
|
604
|
+
|
|
605
|
+
removed: list[DbModelEvaluation] = []
|
|
606
|
+
|
|
607
|
+
if model_ids and eval_ids:
|
|
608
|
+
for mid in model_ids:
|
|
609
|
+
for eid in eval_ids:
|
|
610
|
+
removed.extend(
|
|
611
|
+
user_store.model_evaluations.remove(
|
|
612
|
+
model_id=mid, evaluation_id=eid,
|
|
613
|
+
),
|
|
614
|
+
)
|
|
615
|
+
elif model_ids:
|
|
616
|
+
for mid in model_ids:
|
|
617
|
+
removed.extend(
|
|
618
|
+
user_store.model_evaluations.remove(
|
|
619
|
+
model_id=mid,
|
|
620
|
+
),
|
|
621
|
+
)
|
|
622
|
+
elif eval_ids:
|
|
623
|
+
for eid in eval_ids:
|
|
624
|
+
removed.extend(
|
|
625
|
+
user_store.model_evaluations.remove(
|
|
626
|
+
evaluation_id=eid,
|
|
627
|
+
),
|
|
628
|
+
)
|
|
629
|
+
|
|
630
|
+
if removed:
|
|
631
|
+
user_store.save()
|
|
632
|
+
table = Table(
|
|
633
|
+
title=f"Detached ({len(removed)} removed)",
|
|
634
|
+
show_lines=False, width=CLI_WIDTH,
|
|
635
|
+
)
|
|
636
|
+
table.add_column("Model", ratio=2)
|
|
637
|
+
table.add_column("Evaluation", ratio=3)
|
|
638
|
+
for me in removed:
|
|
639
|
+
table.add_row(
|
|
640
|
+
_name_for(store, "model", me.model_id),
|
|
641
|
+
_name_for(store, "eval", me.evaluation_id),
|
|
642
|
+
)
|
|
643
|
+
console.print(table)
|
|
644
|
+
else:
|
|
645
|
+
console.print("[dim]No matching assignments found.[/dim]")
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
def _name_for(
|
|
649
|
+
store: JsonRepository, kind: str, id: str,
|
|
650
|
+
) -> str:
|
|
651
|
+
try:
|
|
652
|
+
if kind == "model":
|
|
653
|
+
return store.models.get(id).name
|
|
654
|
+
return store.evaluations.get(id).name
|
|
655
|
+
except KeyError:
|
|
656
|
+
return id
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
# -- run -----------------------------------------------------------
|
|
660
|
+
|
|
661
|
+
def _register_run(
|
|
662
|
+
subparsers: argparse._SubParsersAction,
|
|
663
|
+
) -> None:
|
|
664
|
+
run_parser = subparsers.add_parser(
|
|
665
|
+
"run", help="Run evaluations",
|
|
666
|
+
parents=[_config_parent],
|
|
667
|
+
)
|
|
668
|
+
run_parser.add_argument(
|
|
669
|
+
"model_evaluation_id",
|
|
670
|
+
nargs="?",
|
|
671
|
+
help="Model-evaluation ID to run",
|
|
672
|
+
)
|
|
673
|
+
run_parser.add_argument(
|
|
674
|
+
"--model",
|
|
675
|
+
help="Run all evaluations for this model ID",
|
|
676
|
+
)
|
|
677
|
+
run_parser.add_argument(
|
|
678
|
+
"--output", "-o",
|
|
679
|
+
help="Output JSON file path "
|
|
680
|
+
"(default: eval_<timestamp>.json)",
|
|
681
|
+
)
|
|
682
|
+
run_parser.add_argument(
|
|
683
|
+
"--concurrency", "-c",
|
|
684
|
+
type=int, default=20,
|
|
685
|
+
help="Max concurrent tasks (default: 20)",
|
|
686
|
+
)
|
|
687
|
+
run_parser.add_argument(
|
|
688
|
+
"--model-tag", action="append", default=[],
|
|
689
|
+
metavar="KEY=VALUE",
|
|
690
|
+
help="Filter by model tag (repeatable)",
|
|
691
|
+
)
|
|
692
|
+
run_parser.add_argument(
|
|
693
|
+
"--eval-tag", action="append", default=[],
|
|
694
|
+
metavar="KEY=VALUE",
|
|
695
|
+
help="Filter by evaluation tag (repeatable)",
|
|
696
|
+
)
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+
async def handle_run(
|
|
700
|
+
args: argparse.Namespace, store: JsonRepository,
|
|
701
|
+
) -> str | None:
|
|
702
|
+
model_tag_filter = _parse_tags(args.model_tag)
|
|
703
|
+
eval_tag_filter = _parse_tags(args.eval_tag)
|
|
704
|
+
|
|
705
|
+
model_evaluations = _resolve_run_targets(
|
|
706
|
+
store, args, model_tag_filter, eval_tag_filter,
|
|
707
|
+
)
|
|
708
|
+
if not model_evaluations:
|
|
709
|
+
return None
|
|
710
|
+
|
|
711
|
+
eval_names = [
|
|
712
|
+
store.evaluations.get(me.evaluation_id).name
|
|
713
|
+
for me in model_evaluations
|
|
714
|
+
]
|
|
715
|
+
desc_width = min(max((len(n) for n in eval_names), default=20), 40)
|
|
716
|
+
|
|
717
|
+
progress = create_progress()
|
|
718
|
+
|
|
719
|
+
results: list[CliRunResult] = []
|
|
720
|
+
consecutive_failures = 0
|
|
721
|
+
try:
|
|
722
|
+
progress.start()
|
|
723
|
+
for me in model_evaluations:
|
|
724
|
+
result = await run_cli_evaluation(
|
|
725
|
+
me, store, args.concurrency,
|
|
726
|
+
progress=progress,
|
|
727
|
+
desc_width=desc_width,
|
|
728
|
+
)
|
|
729
|
+
results.append(result)
|
|
730
|
+
if result.summary and result.summary.status == EvaluationStatus.ERROR:
|
|
731
|
+
consecutive_failures += 1
|
|
732
|
+
if consecutive_failures >= MAX_CONSECUTIVE_RUN_FAILURES:
|
|
733
|
+
console.print(
|
|
734
|
+
f"[red]Aborting: {consecutive_failures} consecutive"
|
|
735
|
+
f" evaluations failed. Is the target reachable?[/red]",
|
|
736
|
+
)
|
|
737
|
+
break
|
|
738
|
+
else:
|
|
739
|
+
consecutive_failures = 0
|
|
740
|
+
finally:
|
|
741
|
+
progress.stop()
|
|
742
|
+
|
|
743
|
+
if not results:
|
|
744
|
+
return None
|
|
745
|
+
|
|
746
|
+
print_results(results)
|
|
747
|
+
log_run_summary(results)
|
|
748
|
+
|
|
749
|
+
output_path = args.output or (
|
|
750
|
+
f"eval_"
|
|
751
|
+
f"{datetime.now().strftime('%Y%m%dT%H%M%S')}"
|
|
752
|
+
f".json"
|
|
753
|
+
)
|
|
754
|
+
write_output(results, args.config, output_path)
|
|
755
|
+
return output_path
|
|
756
|
+
|
|
757
|
+
|
|
758
|
+
def _filter_by_tags(
|
|
759
|
+
items: list,
|
|
760
|
+
store: JsonRepository,
|
|
761
|
+
model_tags: dict[str, str],
|
|
762
|
+
eval_tags: dict[str, str],
|
|
763
|
+
) -> list:
|
|
764
|
+
filtered = []
|
|
765
|
+
for me in items:
|
|
766
|
+
if model_tags:
|
|
767
|
+
try:
|
|
768
|
+
model = store.models.get(me.model_id)
|
|
769
|
+
if not _matches_tags(model.tags, model_tags):
|
|
770
|
+
continue
|
|
771
|
+
except KeyError:
|
|
772
|
+
continue
|
|
773
|
+
if eval_tags:
|
|
774
|
+
try:
|
|
775
|
+
ev = store.evaluations.get(
|
|
776
|
+
me.evaluation_id,
|
|
777
|
+
)
|
|
778
|
+
if not _matches_tags(ev.tags, eval_tags):
|
|
779
|
+
continue
|
|
780
|
+
except KeyError:
|
|
781
|
+
continue
|
|
782
|
+
filtered.append(me)
|
|
783
|
+
return filtered
|
|
784
|
+
|
|
785
|
+
|
|
786
|
+
def _resolve_run_targets(
|
|
787
|
+
store: JsonRepository,
|
|
788
|
+
args: argparse.Namespace,
|
|
789
|
+
model_tags: dict[str, str],
|
|
790
|
+
eval_tags: dict[str, str],
|
|
791
|
+
) -> list:
|
|
792
|
+
if args.model_evaluation_id and args.model:
|
|
793
|
+
console.print(
|
|
794
|
+
"[red]Error: specify either a model-evaluation ID "
|
|
795
|
+
"or --model, not both.[/red]",
|
|
796
|
+
)
|
|
797
|
+
return []
|
|
798
|
+
|
|
799
|
+
if not args.model_evaluation_id and not args.model:
|
|
800
|
+
console.print(
|
|
801
|
+
"[red]Error: specify a model-evaluation ID "
|
|
802
|
+
"or --model <model-id>.[/red]",
|
|
803
|
+
)
|
|
804
|
+
return []
|
|
805
|
+
|
|
806
|
+
if args.model_evaluation_id:
|
|
807
|
+
me_id = _resolve_id(
|
|
808
|
+
args.model_evaluation_id,
|
|
809
|
+
[me.id for me in
|
|
810
|
+
store.model_evaluations.list_all()],
|
|
811
|
+
)
|
|
812
|
+
try:
|
|
813
|
+
me = store.model_evaluations.get(me_id)
|
|
814
|
+
items = _filter_by_tags(
|
|
815
|
+
[me], store, model_tags, eval_tags,
|
|
816
|
+
)
|
|
817
|
+
if not items:
|
|
818
|
+
console.print(
|
|
819
|
+
"[yellow]Model-evaluation filtered "
|
|
820
|
+
"out by tag filters.[/yellow]",
|
|
821
|
+
)
|
|
822
|
+
return items
|
|
823
|
+
except KeyError:
|
|
824
|
+
console.print(
|
|
825
|
+
f"[red]Error: model-evaluation "
|
|
826
|
+
f"{args.model_evaluation_id!r} not found.[/red]",
|
|
827
|
+
)
|
|
828
|
+
return []
|
|
829
|
+
|
|
830
|
+
model_id = _resolve_id(
|
|
831
|
+
args.model,
|
|
832
|
+
[m.id for m in store.models.list()],
|
|
833
|
+
)
|
|
834
|
+
try:
|
|
835
|
+
model = store.models.get(model_id)
|
|
836
|
+
except KeyError:
|
|
837
|
+
console.print(
|
|
838
|
+
f"[red]Error: model {args.model!r} not found.[/red]",
|
|
839
|
+
)
|
|
840
|
+
return []
|
|
841
|
+
|
|
842
|
+
if model_tags and not _matches_tags(
|
|
843
|
+
model.tags, model_tags,
|
|
844
|
+
):
|
|
845
|
+
console.print(
|
|
846
|
+
f"[yellow]Model {model_id!r} does not match "
|
|
847
|
+
f"--model-tag filters.[/yellow]",
|
|
848
|
+
)
|
|
849
|
+
return []
|
|
850
|
+
|
|
851
|
+
me_view = store.model_evaluations.list_by_model(
|
|
852
|
+
model_id, limit=1000,
|
|
853
|
+
)
|
|
854
|
+
items = [item.config for item in me_view.items]
|
|
855
|
+
if eval_tags:
|
|
856
|
+
items = _filter_by_tags(
|
|
857
|
+
items, store, {}, eval_tags,
|
|
858
|
+
)
|
|
859
|
+
if not items:
|
|
860
|
+
console.print(
|
|
861
|
+
f"[yellow]No evaluations assigned to model "
|
|
862
|
+
f"{model_id!r} (after tag filtering).[/yellow]",
|
|
863
|
+
)
|
|
864
|
+
return items
|
|
865
|
+
|
|
866
|
+
|
|
867
|
+
# -- formatting helpers -------------------------------------------
|
|
868
|
+
|
|
869
|
+
def _resolve_id(
|
|
870
|
+
prefix: str, all_ids: list[str],
|
|
871
|
+
) -> str:
|
|
872
|
+
if prefix in all_ids:
|
|
873
|
+
return prefix
|
|
874
|
+
matches = [
|
|
875
|
+
full_id for full_id in all_ids
|
|
876
|
+
if full_id.startswith(prefix)
|
|
877
|
+
]
|
|
878
|
+
if len(matches) == 1:
|
|
879
|
+
return matches[0]
|
|
880
|
+
if len(matches) > 1:
|
|
881
|
+
console.print(
|
|
882
|
+
f"[yellow]Ambiguous ID prefix {prefix!r}, "
|
|
883
|
+
f"matches: {[m[:12] for m in matches]}[/yellow]",
|
|
884
|
+
)
|
|
885
|
+
return prefix
|
|
886
|
+
|
|
887
|
+
|
|
888
|
+
def _fmt_tags(tags: dict[str, str]) -> str:
|
|
889
|
+
return ", ".join(
|
|
890
|
+
f"{k}={v}" for k, v in tags.items()
|
|
891
|
+
)
|