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/init_cli.py
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""Subcommand for `flintai init`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import stat
|
|
9
|
+
|
|
10
|
+
from rich.prompt import Prompt
|
|
11
|
+
|
|
12
|
+
from flintai.cli.console import console, select
|
|
13
|
+
from flintai.cli.utils import get_flintai_config_path, get_flintai_env_path, get_flintai_dir
|
|
14
|
+
|
|
15
|
+
_PROVIDER_DEFAULTS: dict[str, dict[str, str | None]] = {
|
|
16
|
+
"gemini": {
|
|
17
|
+
"model": "gemini-3.5-flash",
|
|
18
|
+
"api_key_var": "GEMINI_API_KEY",
|
|
19
|
+
},
|
|
20
|
+
"openai": {
|
|
21
|
+
"model": "gpt-5.4",
|
|
22
|
+
"api_key_var": "OPENAI_API_KEY",
|
|
23
|
+
},
|
|
24
|
+
"anthropic": {
|
|
25
|
+
"model": "claude-sonnet-4-6",
|
|
26
|
+
"api_key_var": "ANTHROPIC_API_KEY",
|
|
27
|
+
},
|
|
28
|
+
"litellm": {
|
|
29
|
+
"model": "gpt-5.4",
|
|
30
|
+
"api_key_var": None,
|
|
31
|
+
},
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
_LITELLM_BACKENDS: dict[str, str] = {
|
|
35
|
+
"openai": "OPENAI_API_KEY",
|
|
36
|
+
"gemini": "GEMINI_API_KEY",
|
|
37
|
+
"anthropic": "ANTHROPIC_API_KEY",
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def register(
|
|
42
|
+
subparsers: argparse._SubParsersAction,
|
|
43
|
+
) -> None:
|
|
44
|
+
subparsers.add_parser(
|
|
45
|
+
"init",
|
|
46
|
+
help="Initialize flintai configuration",
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def run_init() -> None:
|
|
51
|
+
flintai_dir = get_flintai_dir()
|
|
52
|
+
|
|
53
|
+
console.print("[bold cyan]Flint AI Setup[/bold cyan]")
|
|
54
|
+
console.print()
|
|
55
|
+
|
|
56
|
+
if flintai_dir.exists():
|
|
57
|
+
proceed = select(
|
|
58
|
+
"Existing configuration will be overwritten."
|
|
59
|
+
" Do you want to proceed?",
|
|
60
|
+
options=["yes", "no"],
|
|
61
|
+
default_index=0,
|
|
62
|
+
)
|
|
63
|
+
if proceed.lower() == "no":
|
|
64
|
+
console.print("[dim]Aborted.[/dim]")
|
|
65
|
+
return
|
|
66
|
+
console.print()
|
|
67
|
+
|
|
68
|
+
provider = select(
|
|
69
|
+
"Flint AI CLI needs an LLM for certain functionality. Select your model provider:",
|
|
70
|
+
options=["gemini", "openai", "anthropic", "litellm"],
|
|
71
|
+
default_index=0,
|
|
72
|
+
)
|
|
73
|
+
console.print()
|
|
74
|
+
|
|
75
|
+
defaults = _PROVIDER_DEFAULTS[provider]
|
|
76
|
+
|
|
77
|
+
if provider == "litellm":
|
|
78
|
+
model_name = Prompt.ask(
|
|
79
|
+
"LiteLLM model string (e.g. openai/gpt-5.4, gemini/gemini-2.5-flash)",
|
|
80
|
+
default="openai/gpt-5.4",
|
|
81
|
+
)
|
|
82
|
+
console.print()
|
|
83
|
+
|
|
84
|
+
backend = model_name.split("/")[0] if "/" in model_name else None
|
|
85
|
+
api_key_var = _LITELLM_BACKENDS.get(backend, None) if backend else None
|
|
86
|
+
if not api_key_var:
|
|
87
|
+
api_key_var = Prompt.ask("API key env var name")
|
|
88
|
+
console.print()
|
|
89
|
+
|
|
90
|
+
generator_model = f"litellm:{model_name}"
|
|
91
|
+
else:
|
|
92
|
+
model_name = Prompt.ask(
|
|
93
|
+
"Model name",
|
|
94
|
+
default=defaults["model"],
|
|
95
|
+
)
|
|
96
|
+
console.print()
|
|
97
|
+
api_key_var = defaults["api_key_var"]
|
|
98
|
+
generator_model = f"{provider}:{model_name}"
|
|
99
|
+
|
|
100
|
+
assert api_key_var is not None
|
|
101
|
+
api_key = Prompt.ask(
|
|
102
|
+
f"Enter your {api_key_var}",
|
|
103
|
+
password=True,
|
|
104
|
+
)
|
|
105
|
+
console.print()
|
|
106
|
+
env_lines = [
|
|
107
|
+
f"GENERATOR_MODEL={generator_model}",
|
|
108
|
+
f"{api_key_var}={api_key}",
|
|
109
|
+
"EXECUTOR_MAX_WORKERS=20"
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
default_config = {
|
|
113
|
+
"models": [],
|
|
114
|
+
"evaluations": [],
|
|
115
|
+
"detectors": [],
|
|
116
|
+
"message_collections": [],
|
|
117
|
+
"model_evaluations": [],
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
old_umask = os.umask(0o077)
|
|
121
|
+
try:
|
|
122
|
+
flintai_dir.mkdir(parents=True, exist_ok=True)
|
|
123
|
+
|
|
124
|
+
env_path = get_flintai_env_path()
|
|
125
|
+
env_path.write_text("\n".join(env_lines) + "\n")
|
|
126
|
+
env_path.chmod(stat.S_IRUSR | stat.S_IWUSR)
|
|
127
|
+
|
|
128
|
+
config_path = get_flintai_config_path()
|
|
129
|
+
config_path.write_text(
|
|
130
|
+
json.dumps(default_config, indent=2) + "\n",
|
|
131
|
+
)
|
|
132
|
+
finally:
|
|
133
|
+
os.umask(old_umask)
|
|
134
|
+
|
|
135
|
+
console.print(f"[green]Created {env_path}[/green]")
|
|
136
|
+
console.print(
|
|
137
|
+
f"[green]Created {config_path}[/green]",
|
|
138
|
+
)
|
|
139
|
+
console.print()
|
|
140
|
+
console.print(
|
|
141
|
+
"[bold green]Flint AI initialized"
|
|
142
|
+
" successfully![/bold green]",
|
|
143
|
+
)
|
|
144
|
+
console.print()
|
flintai/cli/main.py
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CLI entry point for Flint AI CLI.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
python -m flintai.cli init
|
|
6
|
+
python -m flintai.cli eval models list
|
|
7
|
+
python -m flintai.cli eval run <model-evaluation-id>
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import asyncio
|
|
14
|
+
import logging
|
|
15
|
+
import os
|
|
16
|
+
import sys
|
|
17
|
+
import time
|
|
18
|
+
import traceback
|
|
19
|
+
from datetime import datetime
|
|
20
|
+
|
|
21
|
+
from dotenv import load_dotenv
|
|
22
|
+
from rich.panel import Panel
|
|
23
|
+
from rich.text import Text
|
|
24
|
+
|
|
25
|
+
from flintai.cli import eval_cli, scan_cli, init_cli
|
|
26
|
+
from flintai.cli.console import CLI_WIDTH, console
|
|
27
|
+
from flintai.cli.version import VERSION
|
|
28
|
+
from flintai.cli.utils import is_ci
|
|
29
|
+
from flintai.eval.common.log import setup_file_logging
|
|
30
|
+
from flintai.eval.db.json.repository_json import JsonRepository
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
_LOGO_LINES = [
|
|
35
|
+
"███████╗██╗ ██╗███╗ ██╗████████╗ █████╗ ██╗",
|
|
36
|
+
"██╔════╝██║ ██║████╗ ██║╚══██╔══╝ ██╔══██╗██║",
|
|
37
|
+
"█████╗ ██║ ██║██╔██╗ ██║ ██║ ███████║██║",
|
|
38
|
+
"██╔══╝ ██║ ██║██║╚██╗██║ ██║ ██╔══██║██║",
|
|
39
|
+
"██║ ███████╗██║██║ ╚████║ ██║ ██║ ██║██║",
|
|
40
|
+
"╚═╝ ╚══════╝╚═╝╚═╝ ╚═══╝ ╚═╝ ╚═╝ ╚═╝╚═╝",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
_TIMESTAMP = datetime.now().strftime("%Y%m%dT%H%M%S")
|
|
45
|
+
|
|
46
|
+
_BUILTIN_CONFIG = os.path.join(
|
|
47
|
+
os.path.dirname(os.path.abspath(__file__)),
|
|
48
|
+
"builtin_config.json",
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _count(repo: JsonRepository) -> dict[str, int]:
|
|
54
|
+
return {
|
|
55
|
+
"models": len(repo.models.list()),
|
|
56
|
+
"evaluations": len(repo.evaluations.list()),
|
|
57
|
+
"detectors": len(repo.detectors.list()),
|
|
58
|
+
"collections": len(repo.message_collections.list()),
|
|
59
|
+
"assignments": len(repo.model_evaluations.list_all()),
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _fmt_count(total: int, builtin: int, user: int) -> str:
|
|
64
|
+
return f"{total} [dim]({builtin} builtin, {user} user)[/dim]"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _print_logo() -> None:
|
|
68
|
+
console.print()
|
|
69
|
+
for line in _LOGO_LINES:
|
|
70
|
+
pad = max(0, (CLI_WIDTH - len(line)) // 2)
|
|
71
|
+
console.print(" " * pad + f"[bold cyan]{line}[/bold cyan]")
|
|
72
|
+
version_text = f"v{VERSION}"
|
|
73
|
+
pad = max(0, (CLI_WIDTH - len(version_text)) // 2)
|
|
74
|
+
console.print(" " * pad + f"[dim][bold cyan]{version_text}[/bold cyan][/dim]")
|
|
75
|
+
console.print()
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _print_config_info(
|
|
79
|
+
merged: JsonRepository,
|
|
80
|
+
builtin: JsonRepository,
|
|
81
|
+
user: JsonRepository,
|
|
82
|
+
config_path: str,
|
|
83
|
+
) -> None:
|
|
84
|
+
m = _count(merged)
|
|
85
|
+
b = _count(builtin)
|
|
86
|
+
u = _count(user)
|
|
87
|
+
|
|
88
|
+
console.print(f"[dim]Config: {config_path}[/dim]")
|
|
89
|
+
console.print(f"[dim]Models: {_fmt_count(m['models'], b['models'], u['models'])}[/dim]")
|
|
90
|
+
console.print(f"[dim]Evaluations: {_fmt_count(m['evaluations'], b['evaluations'], u['evaluations'])}[/dim]")
|
|
91
|
+
console.print(f"[dim]Detectors: {_fmt_count(m['detectors'], b['detectors'], u['detectors'])}[/dim]")
|
|
92
|
+
console.print(f"[dim]Collections: {_fmt_count(m['collections'], b['collections'], u['collections'])}[/dim]")
|
|
93
|
+
console.print(f"[dim]Assignments: {_fmt_count(m['assignments'], b['assignments'], u['assignments'])}[/dim]")
|
|
94
|
+
console.print()
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _print_path(
|
|
98
|
+
path: str,
|
|
99
|
+
) -> None:
|
|
100
|
+
console.print(f"[dim]Path: {path}[/dim]")
|
|
101
|
+
console.print()
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _print_shutdown(
|
|
105
|
+
elapsed: float,
|
|
106
|
+
output_path: str | None,
|
|
107
|
+
log_path: str | None,
|
|
108
|
+
) -> None:
|
|
109
|
+
console.print()
|
|
110
|
+
parts = [f"[dim]Completed in [bold cyan]{elapsed:.1f}s[/bold cyan][/dim]"]
|
|
111
|
+
if output_path:
|
|
112
|
+
parts.append(f"[dim]Results: [bold cyan]{output_path}[/bold cyan][/dim]")
|
|
113
|
+
if log_path:
|
|
114
|
+
parts.append(f"[dim]Logs: [bold cyan]{log_path}[/bold cyan][/dim]")
|
|
115
|
+
console.print(" ".join(parts))
|
|
116
|
+
console.print()
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _dispatch(args: argparse.Namespace) -> str | None:
|
|
120
|
+
logger.info("Command: %s", " ".join(sys.argv))
|
|
121
|
+
|
|
122
|
+
if args.command == "eval":
|
|
123
|
+
return _dispatch_eval(args)
|
|
124
|
+
elif args.command == "scan":
|
|
125
|
+
return _dispatch_scan(args)
|
|
126
|
+
elif args.command == "init":
|
|
127
|
+
return _dispatch_init(args)
|
|
128
|
+
else:
|
|
129
|
+
console.print(f"[red]Unknown command: {args.command}[/red]")
|
|
130
|
+
sys.exit(1)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _dispatch_init(args: argparse.Namespace) -> str | None:
|
|
134
|
+
init_cli.run_init()
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _dispatch_scan(args: argparse.Namespace) -> None | None:
|
|
139
|
+
logger.info("flintai v%s | scan mode", VERSION)
|
|
140
|
+
_print_path(args.path)
|
|
141
|
+
return scan_cli.handle_scan(args)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _dispatch_eval(args: argparse.Namespace) -> str | None:
|
|
145
|
+
"""Run the requested command. Returns output file path if any."""
|
|
146
|
+
if args.command != "eval":
|
|
147
|
+
console.print(f"[red]Unknown command: {args.command}[/red]")
|
|
148
|
+
sys.exit(1)
|
|
149
|
+
|
|
150
|
+
config_path = getattr(args, "config", None) or str(
|
|
151
|
+
init_cli.get_flintai_config_path(),
|
|
152
|
+
)
|
|
153
|
+
builtin = JsonRepository(_BUILTIN_CONFIG)
|
|
154
|
+
user = JsonRepository(config_path)
|
|
155
|
+
store = user.merge(builtin)
|
|
156
|
+
|
|
157
|
+
_print_config_info(store, builtin, user, config_path)
|
|
158
|
+
|
|
159
|
+
m = _count(store)
|
|
160
|
+
logger.info(
|
|
161
|
+
"flintai v%s | models=%d, evaluations=%d, assignments=%d",
|
|
162
|
+
VERSION, m["models"], m["evaluations"], m["assignments"],
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
cmd = args.eval_cmd
|
|
166
|
+
if cmd == "models":
|
|
167
|
+
eval_cli.handle_models(args, store)
|
|
168
|
+
elif cmd == "evaluations":
|
|
169
|
+
eval_cli.handle_evaluations(args, store)
|
|
170
|
+
elif cmd == "model-evaluations":
|
|
171
|
+
eval_cli.handle_model_evaluations(
|
|
172
|
+
args, store, user,
|
|
173
|
+
)
|
|
174
|
+
elif cmd == "run":
|
|
175
|
+
return asyncio.run(eval_cli.handle_run(args, store))
|
|
176
|
+
else:
|
|
177
|
+
console.print(f"[red]Unknown eval subcommand: {cmd}[/red]")
|
|
178
|
+
sys.exit(1)
|
|
179
|
+
return None
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _print_error(error: BaseException) -> None:
|
|
183
|
+
error_type = type(error).__name__
|
|
184
|
+
error_msg = str(error) or "(no message)"
|
|
185
|
+
|
|
186
|
+
body = Text()
|
|
187
|
+
body.append(f"{error_type}: ", style="bold red")
|
|
188
|
+
body.append(error_msg)
|
|
189
|
+
|
|
190
|
+
panel = Panel(
|
|
191
|
+
body,
|
|
192
|
+
title="[bold red]Error[/bold red]",
|
|
193
|
+
border_style="red",
|
|
194
|
+
width=min(CLI_WIDTH, max(60, len(error_msg) + 20)),
|
|
195
|
+
padding=(1, 2),
|
|
196
|
+
)
|
|
197
|
+
console.print()
|
|
198
|
+
console.print(panel)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def main(argv: list[str] | None = None) -> None:
|
|
202
|
+
load_dotenv()
|
|
203
|
+
|
|
204
|
+
parser = argparse.ArgumentParser(
|
|
205
|
+
prog="flintai",
|
|
206
|
+
description="Flint AI CLI — AI Agent Evaluation Framework",
|
|
207
|
+
)
|
|
208
|
+
parser.add_argument(
|
|
209
|
+
"--version", action="version", version=f"%(prog)s {VERSION}",
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
213
|
+
subparsers.required = True
|
|
214
|
+
eval_cli.register(subparsers)
|
|
215
|
+
scan_cli.register(subparsers)
|
|
216
|
+
init_cli.register(subparsers)
|
|
217
|
+
|
|
218
|
+
args = parser.parse_args(argv)
|
|
219
|
+
|
|
220
|
+
ci = is_ci()
|
|
221
|
+
flintai_env = init_cli.get_flintai_env_path()
|
|
222
|
+
|
|
223
|
+
if not flintai_env.exists() and args.command != "init":
|
|
224
|
+
if ci:
|
|
225
|
+
console.print(
|
|
226
|
+
"[dim]CI environment detected —"
|
|
227
|
+
" skipping interactive init.[/dim]",
|
|
228
|
+
)
|
|
229
|
+
else:
|
|
230
|
+
console.print(
|
|
231
|
+
"[yellow]First-time setup required."
|
|
232
|
+
" Running flintai init...[/yellow]",
|
|
233
|
+
)
|
|
234
|
+
console.print()
|
|
235
|
+
init_cli.run_init()
|
|
236
|
+
|
|
237
|
+
if flintai_env.exists():
|
|
238
|
+
load_dotenv(flintai_env)
|
|
239
|
+
|
|
240
|
+
log_path = getattr(args, "log", None) or f"flintai_{_TIMESTAMP}.log"
|
|
241
|
+
setup_file_logging(log_path)
|
|
242
|
+
_print_logo()
|
|
243
|
+
|
|
244
|
+
t0 = time.monotonic()
|
|
245
|
+
output_path: str | None = None
|
|
246
|
+
try:
|
|
247
|
+
output_path = _dispatch(args)
|
|
248
|
+
except KeyboardInterrupt:
|
|
249
|
+
logger.info("Interrupted by user")
|
|
250
|
+
console.print("\n[dim]Interrupted.[/dim]")
|
|
251
|
+
sys.exit(130)
|
|
252
|
+
except Exception as e:
|
|
253
|
+
logger.critical(
|
|
254
|
+
"Fatal error: %s: %s\n%s",
|
|
255
|
+
type(e).__name__, e, traceback.format_exc(),
|
|
256
|
+
)
|
|
257
|
+
_print_error(e)
|
|
258
|
+
elapsed = time.monotonic() - t0
|
|
259
|
+
_print_shutdown(elapsed, output_path, log_path)
|
|
260
|
+
sys.exit(1)
|
|
261
|
+
elapsed = time.monotonic() - t0
|
|
262
|
+
_print_shutdown(elapsed, output_path, log_path)
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from rich.progress import (
|
|
4
|
+
BarColumn,
|
|
5
|
+
Progress,
|
|
6
|
+
ProgressColumn,
|
|
7
|
+
TextColumn,
|
|
8
|
+
TimeElapsedColumn,
|
|
9
|
+
TaskID,
|
|
10
|
+
)
|
|
11
|
+
from rich.spinner import Spinner
|
|
12
|
+
from rich.text import Text
|
|
13
|
+
|
|
14
|
+
from flintai.cli.console import console, score_style
|
|
15
|
+
from flintai.eval.core.eval.evaluation import Evaluation
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class StatusColumn(ProgressColumn):
|
|
19
|
+
"""Spinner while running, green check when done, red X on error."""
|
|
20
|
+
|
|
21
|
+
def __init__(self) -> None:
|
|
22
|
+
super().__init__()
|
|
23
|
+
self._spinner = Spinner("dots")
|
|
24
|
+
|
|
25
|
+
def render(self, task: object) -> Text:
|
|
26
|
+
if task.finished:
|
|
27
|
+
status = task.fields.get("final_status", "finished")
|
|
28
|
+
if status == "error":
|
|
29
|
+
return Text("✗", style="bold red")
|
|
30
|
+
return Text("✔", style="bold green")
|
|
31
|
+
return self._spinner.render(task.get_time())
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ProgressOrScoreColumn(ProgressColumn):
|
|
35
|
+
"""Shows M/N while running, score percentage when finished."""
|
|
36
|
+
|
|
37
|
+
def render(self, task: object) -> Text:
|
|
38
|
+
if task.finished:
|
|
39
|
+
score = task.fields.get("score")
|
|
40
|
+
if score is not None:
|
|
41
|
+
pct = score * 100
|
|
42
|
+
return Text(f"{pct:.0f}%", style=score_style(score))
|
|
43
|
+
return Text("—", style="dim")
|
|
44
|
+
completed = int(task.completed)
|
|
45
|
+
if task.total is not None:
|
|
46
|
+
return Text(f"{completed}/{int(task.total)}")
|
|
47
|
+
return Text(f"{completed}/?")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def create_progress() -> Progress:
|
|
51
|
+
return Progress(
|
|
52
|
+
StatusColumn(),
|
|
53
|
+
TextColumn("{task.description}"),
|
|
54
|
+
BarColumn(),
|
|
55
|
+
ProgressOrScoreColumn(),
|
|
56
|
+
TextColumn("[dim]|[/dim]"),
|
|
57
|
+
TimeElapsedColumn(),
|
|
58
|
+
console=console,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def pad_description(name: str, width: int) -> str:
|
|
63
|
+
if len(name) > width:
|
|
64
|
+
return name[: width - 1] + "…"
|
|
65
|
+
return name.ljust(width)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class RichObserver:
|
|
69
|
+
"""Evaluation observer that updates a task in a shared Progress."""
|
|
70
|
+
|
|
71
|
+
def __init__(self, progress: Progress, task_id: TaskID):
|
|
72
|
+
self._progress = progress
|
|
73
|
+
self._task_id = task_id
|
|
74
|
+
|
|
75
|
+
def __call__(self, evaluation: Evaluation) -> None:
|
|
76
|
+
summary = evaluation.get_summary()
|
|
77
|
+
done = summary.finished_evaluations + summary.error_evaluations
|
|
78
|
+
self._progress.update(self._task_id, completed=done)
|
|
79
|
+
|
|
80
|
+
def finish(self, evaluation: Evaluation) -> None:
|
|
81
|
+
summary = evaluation.get_summary()
|
|
82
|
+
total = summary.total_evaluations or 1
|
|
83
|
+
self._progress.update(
|
|
84
|
+
self._task_id,
|
|
85
|
+
completed=total,
|
|
86
|
+
total=total,
|
|
87
|
+
final_status=summary.status.value,
|
|
88
|
+
score=summary.score,
|
|
89
|
+
)
|