aeval-framework 0.3.0__tar.gz → 0.3.1__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.
Files changed (125) hide show
  1. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/PKG-INFO +1 -1
  2. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/pyproject.toml +6 -1
  3. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/src/agent_eval/__init__.py +1 -1
  4. aeval_framework-0.3.0/src/agent_eval/cli.py → aeval_framework-0.3.1/src/agent_eval/_cli_app.py +5 -42
  5. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/src/agent_eval/api/app.py +1 -1
  6. aeval_framework-0.3.1/src/agent_eval/cli.py +103 -0
  7. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/tests/test_cli.py +3 -3
  8. aeval_framework-0.3.1/tests/test_cli_entry_guard.py +193 -0
  9. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/tests/test_discovery.py +2 -2
  10. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/tests/test_holdout_and_canary.py +1 -1
  11. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/tests/test_packaging.py +2 -2
  12. aeval_framework-0.3.1/tests/test_version_single_source.py +131 -0
  13. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/.gitignore +0 -0
  14. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/README.md +0 -0
  15. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/src/agent_eval/api/__init__.py +0 -0
  16. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/src/agent_eval/api/events.py +0 -0
  17. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/src/agent_eval/api/routes/__init__.py +0 -0
  18. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/src/agent_eval/api/routes/datasets.py +0 -0
  19. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/src/agent_eval/api/routes/graders.py +0 -0
  20. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/src/agent_eval/api/routes/metrics.py +0 -0
  21. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/src/agent_eval/api/routes/runs.py +0 -0
  22. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/src/agent_eval/api/routes/suites.py +0 -0
  23. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/src/agent_eval/api/routes/tasks.py +0 -0
  24. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/src/agent_eval/api/standalone.py +0 -0
  25. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/src/agent_eval/core/__init__.py +0 -0
  26. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/src/agent_eval/core/comparison.py +0 -0
  27. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/src/agent_eval/core/contract.py +0 -0
  28. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/src/agent_eval/core/discovery.py +0 -0
  29. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/src/agent_eval/core/metrics.py +0 -0
  30. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/src/agent_eval/core/packaging.py +0 -0
  31. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/src/agent_eval/core/pricing.py +0 -0
  32. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/src/agent_eval/core/redaction.py +0 -0
  33. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/src/agent_eval/core/runner.py +0 -0
  34. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/src/agent_eval/core/simulator.py +0 -0
  35. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/src/agent_eval/core/suite.py +0 -0
  36. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/src/agent_eval/core/types.py +0 -0
  37. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/src/agent_eval/dataset/__init__.py +0 -0
  38. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/src/agent_eval/dataset/models.py +0 -0
  39. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/src/agent_eval/dataset/quality.py +0 -0
  40. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/src/agent_eval/dataset/sources/__init__.py +0 -0
  41. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/src/agent_eval/dataset/sources/llm_generator.py +0 -0
  42. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/src/agent_eval/dataset/sources/manual.py +0 -0
  43. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/src/agent_eval/dataset/sources/regression.py +0 -0
  44. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/src/agent_eval/dataset/sources/trace_mining.py +0 -0
  45. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/src/agent_eval/dataset/storage.py +0 -0
  46. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/src/agent_eval/dataset/version.py +0 -0
  47. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/src/agent_eval/examples/__init__.py +0 -0
  48. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/src/agent_eval/examples/basic_usage.py +0 -0
  49. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/src/agent_eval/examples/mock_runner.py +0 -0
  50. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/src/agent_eval/graders/__init__.py +0 -0
  51. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/src/agent_eval/graders/_evidence.py +0 -0
  52. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/src/agent_eval/graders/_verdicts.py +0 -0
  53. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/src/agent_eval/graders/artifact_check.py +0 -0
  54. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/src/agent_eval/graders/code_based.py +0 -0
  55. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/src/agent_eval/graders/human.py +0 -0
  56. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/src/agent_eval/graders/metric.py +0 -0
  57. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/src/agent_eval/graders/model_based.py +0 -0
  58. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/src/agent_eval/graders/presentation_probes.py +0 -0
  59. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/src/agent_eval/graders/state_check.py +0 -0
  60. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/src/agent_eval/graders/step_level.py +0 -0
  61. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/src/agent_eval/graders/tool_calls.py +0 -0
  62. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/src/agent_eval/graders/transcript.py +0 -0
  63. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/src/agent_eval/metrics/__init__.py +0 -0
  64. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/src/agent_eval/metrics/answer_relevancy.py +0 -0
  65. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/src/agent_eval/metrics/base.py +0 -0
  66. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/src/agent_eval/metrics/batch_evaluation.py +0 -0
  67. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/src/agent_eval/metrics/context_precision.py +0 -0
  68. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/src/agent_eval/metrics/context_recall.py +0 -0
  69. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/src/agent_eval/metrics/faithfulness.py +0 -0
  70. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/src/agent_eval/metrics/llm_judge.py +0 -0
  71. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/src/agent_eval/metrics/prompt_metric.py +0 -0
  72. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/src/agent_eval/metrics/pytest_plugin.py +0 -0
  73. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/src/agent_eval/metrics/report.py +0 -0
  74. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/src/agent_eval/metrics/synthetic_data.py +0 -0
  75. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/src/agent_eval/packs/__init__.py +0 -0
  76. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/src/agent_eval/packs/starter/README.md +0 -0
  77. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/src/agent_eval/packs/starter/manifest.json +0 -0
  78. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/src/agent_eval/packs/starter/suite.yaml +0 -0
  79. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/src/agent_eval/storage/__init__.py +0 -0
  80. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/src/agent_eval/storage/memory.py +0 -0
  81. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/src/agent_eval/storage/sqlite.py +0 -0
  82. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/src/agent_eval/trace/__init__.py +0 -0
  83. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/src/agent_eval/trace/mapping.py +0 -0
  84. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/src/agent_eval/trace/normalize.py +0 -0
  85. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/src/agent_eval/trace/observations.py +0 -0
  86. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/src/agent_eval/trace/phoenix.py +0 -0
  87. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/tests/conftest.py +0 -0
  88. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/tests/smoke_achat_runner.py +0 -0
  89. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/tests/test_agent_metric_catalog.py +0 -0
  90. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/tests/test_api.py +0 -0
  91. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/tests/test_baseline_comparison.py +0 -0
  92. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/tests/test_builtin_graders.py +0 -0
  93. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/tests/test_collection_phases.py +0 -0
  94. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/tests/test_conversation.py +0 -0
  95. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/tests/test_e2e.py +0 -0
  96. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/tests/test_environment_identity.py +0 -0
  97. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/tests/test_eval_batch_evaluation.py +0 -0
  98. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/tests/test_eval_dataset_api.py +0 -0
  99. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/tests/test_eval_dataset_sources.py +0 -0
  100. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/tests/test_eval_dataset_storage.py +0 -0
  101. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/tests/test_eval_metric_pipeline.py +0 -0
  102. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/tests/test_eval_metrics_api.py +0 -0
  103. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/tests/test_eval_metrics_module.py +0 -0
  104. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/tests/test_eval_prompt_metric.py +0 -0
  105. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/tests/test_eval_pytest_plugin.py +0 -0
  106. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/tests/test_eval_report.py +0 -0
  107. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/tests/test_eval_task_history.py +0 -0
  108. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/tests/test_evidence_capture_and_storage.py +0 -0
  109. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/tests/test_graders.py +0 -0
  110. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/tests/test_import_isolation.py +0 -0
  111. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/tests/test_metric_roles_gates_agreement.py +0 -0
  112. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/tests/test_metrics.py +0 -0
  113. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/tests/test_power_analysis.py +0 -0
  114. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/tests/test_presentation_probes.py +0 -0
  115. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/tests/test_prompt_determinism.py +0 -0
  116. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/tests/test_regrade_and_archive.py +0 -0
  117. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/tests/test_run_storage_fields.py +0 -0
  118. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/tests/test_runner.py +0 -0
  119. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/tests/test_sse.py +0 -0
  120. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/tests/test_standalone_api.py +0 -0
  121. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/tests/test_suite.py +0 -0
  122. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/tests/test_termination_and_budgets.py +0 -0
  123. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/tests/test_trace_normalization.py +0 -0
  124. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/tests/test_trial_evidence.py +0 -0
  125. {aeval_framework-0.3.0 → aeval_framework-0.3.1}/tests/test_vocabulary_isolation.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: aeval-framework
3
- Version: 0.3.0
3
+ Version: 0.3.1
4
4
  Summary: Aeval — open-source agent evaluation framework driven by OTel traces (suites, graders, metrics, storage, API, CLI)
5
5
  Project-URL: Homepage, https://github.com/QiYuyyds/Aeval
6
6
  Project-URL: Repository, https://github.com/QiYuyyds/Aeval
@@ -2,7 +2,9 @@
2
2
  # Distribution name: aeval-framework (PyPI). Python module: agent_eval, CLI: eval-suite.
3
3
  # (`agent-eval` on PyPI belongs to an unrelated project — UK AISI agenteval.)
4
4
  name = "aeval-framework"
5
- version = "0.3.0"
5
+ # 版本只有一个真相源: src/agent_eval/__init__.py 的 __version__ (design D1)。
6
+ # 手工写第二份 (旧的 pyproject `version =`) 才是缺陷 —— dynamic 让它无法被写出来。
7
+ dynamic = ["version"]
6
8
  description = "Aeval — open-source agent evaluation framework driven by OTel traces (suites, graders, metrics, storage, API, CLI)"
7
9
  readme = "README.md"
8
10
  requires-python = ">=3.11"
@@ -58,6 +60,9 @@ Changelog = "https://github.com/QiYuyyds/Aeval/blob/main/CHANGELOG.md"
58
60
  requires = ["hatchling"]
59
61
  build-backend = "hatchling.build"
60
62
 
63
+ [tool.hatch.version]
64
+ path = "src/agent_eval/__init__.py"
65
+
61
66
  [tool.hatch.build.targets.wheel]
62
67
  packages = ["src/agent_eval"]
63
68
  # src/agent_eval/packs/** (含 starter pack 的 suite.yaml/manifest.json) 作为
@@ -11,4 +11,4 @@ Usage:
11
11
  result = await runner.run_suite(suite)
12
12
  """
13
13
 
14
- __version__ = "0.3.0"
14
+ __version__ = "0.3.1"
@@ -1,46 +1,9 @@
1
1
  """
2
- eval-suite — the Aeval command line.
3
-
4
- Commands:
5
- run Execute a suite (default runner: built-in MockAgentRunner).
6
- Exit 0 all passed / 1 failed tasks or baseline regression /
7
- 3 evaluation untrustworthy. --baseline adds a relative
8
- regression gate against a stored run
9
- validate Validate a suite without running it
10
- list List runs or suites from the storage DB
11
- show Show one run's details (--task drills into a single task);
12
- reports valid/invalid/pending counts, pass@1 95% CI and
13
- extrapolation markers
14
- compare A/B compare two runs; overlapping 95% intervals read as
15
- "not significant" with no directional verdict, and differing
16
- statistics versions or evidence boundaries read as not comparable
17
- power Sample-size planning: trials needed for a target resolution
18
- (--delta with an assumed or measured baseline pass rate)
19
- serve Serve the standalone API (/v1) via uvicorn
20
-
21
- Source forms (run/validate, spec: suite-distribution): a single suite.yaml
22
- file (status quo); a pack directory or .tar.gz/.zip archive (integrity-checked
23
- against manifest.json); a git URL (shallow clone into a temp dir — needs the
24
- git executable); or the literal "demo" (the built-in starter pack shipped
25
- inside the wheel; a local file/dir named demo wins with a hint). Temp dirs are
26
- cleaned up when the command ends.
27
-
28
- Holdout (run/validate): tasks marked holdout: true are excluded by default
29
- and the skip count is reported; --include-holdout runs them. validate reports
30
- the holdout count without running anything.
31
-
32
- Runner selection (run): --runner option > AEVAL_RUNNER env var > "mock"
33
- (forced "mock" for the built-in demo pack unless --runner is explicit).
34
- Custom runners register via the "agent_eval.runners" entry-point group
35
- (name → zero-arg factory returning an AgentRunner).
36
-
37
- Trace vocabulary (run): --vocabulary option > AEVAL_TRACE_VOCABULARY env var >
38
- "otel-genai". The value selects a built-in public-convention preset
39
- (agent_eval.trace.known_vocabularies()); host-private attribute names still go
40
- in through default_mapping(extra_entries) in library code, never through the CLI.
41
-
42
- Storage (list/show/compare and run persistence): SQLite, ./aeval.db by
43
- default; override with --db or the AEVAL_DB environment variable.
2
+ eval-suite 的实现体 —— 依赖 typer (发行包 `cli` 可选依赖组里的命令行依赖)。
3
+
4
+ 本模块**不是**入口点: 入口是 `agent_eval/cli.py:main()`, 它必须能在缺少命令行
5
+ 依赖时被正常导入并给出指引 (spec: cli)。所以本模块只在入口点确认依赖齐备之后
6
+ 才被导入。命令语义与退出码见 `agent_eval.cli` 的模块文档。
44
7
  """
45
8
 
46
9
  from __future__ import annotations
@@ -184,7 +184,7 @@ def create_app(runner: EvalRunner | None = None) -> FastAPI:
184
184
  """
185
185
  app = FastAPI(
186
186
  title="Aeval API",
187
- version="0.1.0",
187
+ version=package_version(),
188
188
  description="Agent Evaluation Framework API",
189
189
  )
190
190
 
@@ -0,0 +1,103 @@
1
+ """
2
+ eval-suite — the Aeval command line.
3
+
4
+ 本模块是命令行入口点 (`pyproject [project.scripts]`),**不得在模块期导入
5
+ typer**:发行包无条件生成 `eval-suite` 可执行文件(PEP 621 的 `[project.scripts]`
6
+ 是项目级表,无法按可选依赖声明,见 change 的 design D2),所以只装了 core 的人
7
+ 手上也有这条命令。它必须在这里被说清楚缺什么,而不是抛一个指向内部依赖的
8
+ traceback (spec: cli)。真正的实现体在 `agent_eval/_cli_app.py`。
9
+
10
+ Commands:
11
+ run Execute a suite (default runner: built-in MockAgentRunner).
12
+ Exit 0 all passed / 1 failed tasks or baseline regression /
13
+ 3 evaluation untrustworthy. --baseline adds a relative
14
+ regression gate against a stored run
15
+ validate Validate a suite without running it
16
+ list List runs or suites from the storage DB
17
+ show Show one run's details (--task drills into a single task);
18
+ reports valid/invalid/pending counts, pass@1 95% CI and
19
+ extrapolation markers
20
+ compare A/B compare two runs; overlapping 95% intervals read as
21
+ "not significant" with no directional verdict, and differing
22
+ statistics versions or evidence boundaries read as not comparable
23
+ power Sample-size planning: trials needed for a target resolution
24
+ (--delta with an assumed or measured baseline pass rate)
25
+ serve Serve the standalone API (/v1) via uvicorn
26
+
27
+ Exit codes: 0 放行 / 1 agent 表现未达标 / 2 用法错误 / 3 评测本身不可信 /
28
+ 4 命令行依赖缺失(安装形态不对,见 `MISSING_CLI_DEPENDENCY_EXIT_CODE`)
29
+
30
+ Source forms (run/validate, spec: suite-distribution): a single suite.yaml
31
+ file (status quo); a pack directory or .tar.gz/.zip archive (integrity-checked
32
+ against manifest.json); a git URL (shallow clone into a temp dir — needs the
33
+ git executable); or the literal "demo" (the built-in starter pack shipped
34
+ inside the wheel; a local file/dir named demo wins with a hint). Temp dirs are
35
+ cleaned up when the command ends.
36
+
37
+ Holdout (run/validate): tasks marked holdout: true are excluded by default
38
+ and the skip count is reported; --include-holdout runs them. validate reports
39
+ the holdout count without running anything.
40
+
41
+ Runner selection (run): --runner option > AEVAL_RUNNER env var > "mock"
42
+ (forced "mock" for the built-in demo pack unless --runner is explicit).
43
+ Custom runners register via the "agent_eval.runners" entry-point group
44
+ (name → zero-arg factory returning an AgentRunner).
45
+
46
+ Trace vocabulary (run): --vocabulary option > AEVAL_TRACE_VOCABULARY env var >
47
+ "otel-genai". The value selects a built-in public-convention preset
48
+ (agent_eval.trace.known_vocabularies()); host-private attribute names still go
49
+ in through default_mapping(extra_entries) in library code, never through the CLI.
50
+
51
+ Storage (list/show/compare and run persistence): SQLite, ./aeval.db by
52
+ default; override with --db or the AEVAL_DB environment variable.
53
+ """
54
+
55
+ from __future__ import annotations
56
+
57
+ import sys
58
+
59
+ # 与评测结果 (1/3) 和用法错误 (2) 都区分开,脚本可以只凭退出码判定"安装形态不对"
60
+ MISSING_CLI_DEPENDENCY_EXIT_CODE = 4
61
+ # 缺依赖指引的稳定标记:文本可判别,不需要去匹配 traceback
62
+ MISSING_CLI_DEPENDENCY_TOKEN = "error: missing-cli-dependency"
63
+
64
+ # `cli` 可选依赖组实际提供的顶层模块 —— 只有缺这些才给安装指引
65
+ _CLI_EXTRA_MODULES = frozenset({"typer", "rich"})
66
+
67
+ _DISTRIBUTION_NAME = "aeval-framework"
68
+
69
+
70
+ def missing_dependency_guidance() -> str:
71
+ """缺命令行依赖时的可行动指引 (spec: cli「未安装 CLI 依赖时命令行入口必须给出可行动指引」)。"""
72
+ return (
73
+ f"{MISSING_CLI_DEPENDENCY_TOKEN}\n"
74
+ "eval-suite needs the command-line dependencies, which are not installed\n"
75
+ f"in the {_DISTRIBUTION_NAME} environment. The library itself is fine — only\n"
76
+ "this command is missing its extras.\n"
77
+ "\n"
78
+ f" pip install \"{_DISTRIBUTION_NAME}[cli]\"\n"
79
+ "\n"
80
+ f"(from a source checkout: pip install -e \".[cli]\")\n"
81
+ )
82
+
83
+
84
+ def _is_missing_cli_dependency(exc: BaseException) -> bool:
85
+ """区分"命令行依赖没装"与"安装坏了/代码本身有 bug"——后者不能被指引用盖住。"""
86
+ missing = getattr(exc, "name", None) or ""
87
+ return missing.partition(".")[0] in _CLI_EXTRA_MODULES
88
+
89
+
90
+ def main() -> int | None:
91
+ """Console-script entry point (pyproject [project.scripts])."""
92
+ try:
93
+ from agent_eval._cli_app import main as _cli_main
94
+ except ImportError as exc:
95
+ if not _is_missing_cli_dependency(exc):
96
+ raise
97
+ sys.stderr.write(missing_dependency_guidance())
98
+ return MISSING_CLI_DEPENDENCY_EXIT_CODE
99
+ return _cli_main()
100
+
101
+
102
+ if __name__ == "__main__":
103
+ sys.exit(main())
@@ -10,7 +10,7 @@ import re
10
10
 
11
11
  from typer.testing import CliRunner
12
12
 
13
- from agent_eval.cli import app
13
+ from agent_eval._cli_app import app
14
14
 
15
15
  runner = CliRunner()
16
16
 
@@ -699,8 +699,8 @@ class TestServeSmoke:
699
699
  def test_serve_defaults_are_loopback(self):
700
700
  import inspect
701
701
 
702
- from agent_eval import cli
702
+ from agent_eval import _cli_app
703
703
 
704
- sig = inspect.signature(cli.serve)
704
+ sig = inspect.signature(_cli_app.serve)
705
705
  default = sig.parameters["host"].default
706
706
  assert getattr(default, "default", default) == "127.0.0.1"
@@ -0,0 +1,193 @@
1
+ """core-only 安装形态下命令行入口的指引 (change fix-version-drift-and-cli-entry-guard,
2
+ 组 4 / spec: cli「未安装 CLI 依赖时命令行入口必须给出可行动指引」).
3
+
4
+ 全部用子进程跑真入口点 —— 引导路径的成立与否取决于"模块导入期碰不到 typer",
5
+ 进程内 import 早就把模块缓存好了,测不到那一层。
6
+ """
7
+
8
+ import os
9
+ import re
10
+ import subprocess
11
+ import sys
12
+ from pathlib import Path
13
+
14
+ import pytest
15
+
16
+ import agent_eval
17
+ from agent_eval.cli import (
18
+ _CLI_EXTRA_MODULES,
19
+ MISSING_CLI_DEPENDENCY_EXIT_CODE,
20
+ MISSING_CLI_DEPENDENCY_TOKEN,
21
+ )
22
+
23
+ PACKAGE_ROOT = Path(agent_eval.__file__).parent
24
+ CHILD_ENV = {**os.environ, "PYTHONIOENCODING": "utf-8", "PYTHONPATH": str(PACKAGE_ROOT.parent)}
25
+
26
+ # 屏蔽指定顶层模块后再走真入口点,模拟"该依赖没装"
27
+ GUARD_PROBE = """
28
+ import sys
29
+
30
+
31
+ class _Blocker:
32
+ def __init__(self, names):
33
+ self.names = set(names)
34
+
35
+ def find_spec(self, name, path=None, target=None):
36
+ if name in self.names or name.partition(".")[0] in self.names:
37
+ raise ModuleNotFoundError(f"No module named {{name!r}}", name=name)
38
+ return None
39
+
40
+
41
+ sys.meta_path.insert(0, _Blocker([{blocked!r}]))
42
+ sys.argv = ["eval-suite", *{argv!r}]
43
+ from agent_eval.cli import main
44
+
45
+ sys.exit(main())
46
+ """
47
+
48
+ CLI_ARGS = ["--help", "run --help", "validate --help", "list --help", "show --help",
49
+ "compare --help", "power --help", "serve --help", "extensions"]
50
+
51
+
52
+ def _run_python(body: str, extra_env: dict | None = None, **fmt) -> subprocess.CompletedProcess:
53
+ env = {**CHILD_ENV, **(extra_env or {})}
54
+ return subprocess.run(
55
+ [sys.executable, "-c", body.format(**fmt)],
56
+ capture_output=True, text=True, encoding="utf-8", env=env,
57
+ )
58
+
59
+
60
+ def _module_entry(module: str, args: str, extra_env: dict | None = None) -> subprocess.CompletedProcess:
61
+ env = {**CHILD_ENV, **(extra_env or {})}
62
+ return subprocess.run(
63
+ [sys.executable, "-m", module, *args.split()],
64
+ capture_output=True, text=True, encoding="utf-8", env=env,
65
+ )
66
+
67
+
68
+ # 两条路径都在 -c 子进程里跑并把 argv[0] 钉成 eval-suite —— 与真 console script 同形,
69
+ # click 由此算出同一个 prog_name,剩下的字节差只可能来自引导层。
70
+ RUN_ENTRY = """
71
+ import sys
72
+ sys.argv = ["eval-suite", *{argv!r}]
73
+ from agent_eval.cli import main
74
+
75
+ sys.exit(main())
76
+ """
77
+ RUN_IMPL = """
78
+ import sys
79
+ sys.argv = ["eval-suite", *{argv!r}]
80
+ from agent_eval._cli_app import app
81
+
82
+ app()
83
+ """
84
+
85
+
86
+ class TestNormalPathIsUntouched:
87
+ """Scenario: 已装齐依赖时零影响 —— 引导路径完全不介入。"""
88
+
89
+ def test_entry_module_import_alone_never_touches_typer(self):
90
+ """改动前 `import agent_eval.cli` 就会 ModuleNotFoundError; 入口模块必须与依赖解耦。"""
91
+ body = """
92
+ import sys
93
+ import agent_eval.cli
94
+ print("typer" in sys.modules, "agent_eval._cli_app" in sys.modules)
95
+ """
96
+ resp = _run_python(body)
97
+ assert resp.returncode == 0, resp.stderr
98
+ assert resp.stdout.strip() == "False False", resp.stdout
99
+
100
+ def test_bootstrap_adds_zero_bytes_and_zero_exit_code_shift(self):
101
+ """主闸: 走入口点的输出与退出码, 和直接跑实现体逐字节相同。"""
102
+ for args in CLI_ARGS:
103
+ argv = args.split()
104
+ through_entry = _run_python(RUN_ENTRY, argv=argv)
105
+ direct = _run_python(RUN_IMPL, argv=argv)
106
+ label = f"[{args}]"
107
+ assert through_entry.stdout == direct.stdout, f"{label} stdout 变了"
108
+ assert through_entry.stderr == direct.stderr, f"{label} stderr 变了"
109
+ assert through_entry.returncode == direct.returncode, f"{label} 退出码变了"
110
+
111
+ def test_guidance_text_is_absent_when_dependencies_are_installed(self):
112
+ from agent_eval.cli import missing_dependency_guidance
113
+
114
+ guidance_word = "pip install"
115
+ for args in CLI_ARGS:
116
+ resp = _module_entry("agent_eval.cli", args)
117
+ assert guidance_word not in resp.stdout + resp.stderr, args
118
+ assert guidance_word in missing_dependency_guidance()
119
+
120
+ def test_run_demo_still_exits_zero_through_the_entry(self, tmp_path):
121
+ """正常路径不止 help: 真跑一次内置套件仍按既有语义给码 (0=放行)。"""
122
+ resp = _module_entry(
123
+ "agent_eval.cli", "run demo", extra_env={"AEVAL_DB": str(tmp_path / "demo.db")}
124
+ )
125
+ assert resp.returncode == 0, resp.stdout + resp.stderr
126
+ assert "Status: completed" in resp.stdout
127
+
128
+
129
+ class TestMissingCliDependencyGuidance:
130
+ """Scenario: 裸装后执行命令行 —— 得到的是指引,不是指向内部依赖的 traceback。"""
131
+
132
+ def test_missing_typer_yields_actionable_guidance(self, tmp_path):
133
+ resp = _run_python(GUARD_PROBE, blocked="typer", argv=["--help"])
134
+ out = resp.stdout + resp.stderr
135
+ assert resp.returncode != 0
136
+ assert "pip install" in out and "[cli]" in out, out
137
+ assert "eval-suite" in out
138
+
139
+ def test_output_is_not_a_typer_traceback(self):
140
+ resp = _run_python(GUARD_PROBE, blocked="typer", argv=["--help"])
141
+ out = resp.stdout + resp.stderr
142
+ assert "Traceback" not in out, out
143
+ assert "No module named 'typer'" not in out, out
144
+ assert "agent_eval/_cli_app.py" not in out.replace("\\", "/"), out
145
+
146
+ def test_handled_module_set_matches_the_declared_cli_extra(self):
147
+ """守护集合不许和发行声明漂移 —— `cli` 组改了这里就得改。"""
148
+ tomllib = pytest.importorskip("tomllib")
149
+ pyproject = PACKAGE_ROOT.parent.parent / "pyproject.toml"
150
+ if not pyproject.is_file():
151
+ pytest.skip("发行环境不随包带 pyproject.toml")
152
+ declared = tomllib.loads(pyproject.read_text(encoding="utf-8"))["project"][
153
+ "optional-dependencies"
154
+ ]["cli"]
155
+ tops = {re.match(r"\s*([A-Za-z0-9._-]+)", r).group(1) for r in declared}
156
+ assert tops == set(_CLI_EXTRA_MODULES), (tops, set(_CLI_EXTRA_MODULES))
157
+
158
+ def test_rich_is_not_an_import_time_dependency_of_the_entry(self):
159
+ """记录实测边界: 缺 typer 才是裸装形态的真实故障点 (rich 由 typer 在渲染期拉入)。"""
160
+ resp = _run_python(GUARD_PROBE, blocked="rich", argv=["--help"])
161
+ assert "No module named 'rich'" in resp.stderr or resp.returncode != 0
162
+ assert resp.returncode != MISSING_CLI_DEPENDENCY_EXIT_CODE
163
+
164
+ def test_unrelated_import_failure_is_not_masked_by_the_guidance(self):
165
+ """安装坏了/代码自身有 bug 时不许用"请装 [cli]"盖住 —— 那会把人引到错路上。"""
166
+ resp = _run_python(GUARD_PROBE, blocked="agent_eval.core.types", argv=["--help"])
167
+ out = resp.stdout + resp.stderr
168
+ assert resp.returncode != 0
169
+ assert "Traceback" in out, "非依赖缺失的导入失败必须照原样抛出来"
170
+ assert "pip install" not in out, out
171
+
172
+
173
+ class TestExitCodeIsScriptDiscriminable:
174
+ def test_missing_dependency_code_is_distinct_from_every_outcome(self):
175
+ """4.4: 成功与两种失败不共用一个码 (0 放行 / 1 表现 / 2 用法 / 3 不可信 / 4 缺依赖)。"""
176
+ assert MISSING_CLI_DEPENDENCY_EXIT_CODE == 4
177
+ assert len({MISSING_CLI_DEPENDENCY_EXIT_CODE, 0, 1, 2, 3}) == 5
178
+
179
+ def test_missing_dependency_run_reports_the_documented_code(self):
180
+ resp = _run_python(GUARD_PROBE, blocked="typer", argv=["--help"])
181
+ assert resp.returncode == MISSING_CLI_DEPENDENCY_EXIT_CODE
182
+
183
+ def test_guidance_carries_a_stable_machine_readable_token(self):
184
+ resp = _run_python(GUARD_PROBE, blocked="typer", argv=["--help"])
185
+ out = resp.stdout + resp.stderr
186
+ assert MISSING_CLI_DEPENDENCY_TOKEN in out
187
+ # 脚本只需匹配这一行,不需要读安装表达式的具体写法
188
+ assert out.strip().splitlines()[0].startswith(MISSING_CLI_DEPENDENCY_TOKEN)
189
+
190
+ def test_normal_path_never_emits_the_token(self):
191
+ for args in CLI_ARGS:
192
+ resp = _module_entry("agent_eval.cli", args)
193
+ assert MISSING_CLI_DEPENDENCY_TOKEN not in resp.stdout + resp.stderr, args
@@ -221,7 +221,7 @@ def test_same_name_conflict_raises_listing_both_sources(monkeypatch):
221
221
 
222
222
  def test_cli_extensions_listing_shows_builtin_and_discovered(monkeypatch):
223
223
  """eval-suite extensions: 内置 + 被发现的 (标注来源包)。"""
224
- from agent_eval.cli import app
224
+ from agent_eval._cli_app import app
225
225
 
226
226
  patch_entry_points(
227
227
  monkeypatch,
@@ -240,8 +240,8 @@ def test_cli_extensions_listing_shows_builtin_and_discovered(monkeypatch):
240
240
 
241
241
  def test_cli_and_rest_meta_list_same_names(monkeypatch):
242
242
  """CLI 清单与 /meta 能力清单给出的自定义判据名字集合相同。"""
243
+ from agent_eval._cli_app import app
243
244
  from agent_eval.api.app import meta_payload
244
- from agent_eval.cli import app
245
245
 
246
246
  patch_entry_points(
247
247
  monkeypatch,
@@ -12,7 +12,7 @@ from pathlib import Path
12
12
  import pytest
13
13
  from typer.testing import CliRunner
14
14
 
15
- from agent_eval.cli import app
15
+ from agent_eval._cli_app import app
16
16
  from agent_eval.core.runner import EvalRunner, NoRunnableTasksError
17
17
  from agent_eval.core.suite import SuiteLoadError, load_suite
18
18
  from agent_eval.core.types import RunResult
@@ -401,7 +401,7 @@ class TestCliSourceForms:
401
401
  def test_run_tar_gz_source_completes(self, tmp_path):
402
402
  from typer.testing import CliRunner
403
403
 
404
- from agent_eval.cli import app
404
+ from agent_eval._cli_app import app
405
405
 
406
406
  root = _make_pack(tmp_path)
407
407
  archive = _tar_pack(tmp_path, root, name="suite-pack.tar.gz")
@@ -417,7 +417,7 @@ class TestCliSourceForms:
417
417
 
418
418
  from typer.testing import CliRunner
419
419
 
420
- from agent_eval.cli import app
420
+ from agent_eval._cli_app import app
421
421
 
422
422
  repo = tmp_path / "suite-repo"
423
423
  repo.mkdir()
@@ -0,0 +1,131 @@
1
+ """版本出口同源 (change fix-version-drift-and-cli-entry-guard, 组 2 / spec: rest-api).
2
+
3
+ 钉住三件事:
4
+ 1. 同一进程内接口文档版本 / 能力清单版本 / 版本响应头三处相等;
5
+ 2. 寄宿挂载形态与独立部署形态读到的接口文档版本相同;
6
+ 3. `api/` 下不存在第二处写死的版本号字面量 —— 静态扫描, 防的是"新增第二处"。
7
+ """
8
+
9
+ import ast
10
+ import re
11
+ from pathlib import Path
12
+
13
+ from starlette.testclient import TestClient
14
+
15
+ import agent_eval.api
16
+ from agent_eval.api.app import create_app, meta_payload, package_version
17
+ from agent_eval.api.standalone import create_standalone_app
18
+
19
+ VERSION_LITERAL = re.compile(r"\d+\.\d+\.\d+[\w.+-]*")
20
+ API_ROOT = Path(next(iter(agent_eval.api.__path__)))
21
+
22
+
23
+ def _doc_string_nodes(tree: ast.AST) -> set[int]:
24
+ """docstring 常量节点 id —— 叙述历史版本的文字不是版本声明。"""
25
+ out = set()
26
+ for node in ast.walk(tree):
27
+ if isinstance(node, ast.Module | ast.ClassDef | ast.FunctionDef | ast.AsyncFunctionDef):
28
+ first = node.body[0] if node.body else None
29
+ if (
30
+ isinstance(first, ast.Expr)
31
+ and isinstance(first.value, ast.Constant)
32
+ and isinstance(first.value.value, str)
33
+ ):
34
+ out.add(id(first.value))
35
+ return out
36
+
37
+
38
+ def _version_literals_in(source: str, path: Path) -> list[str]:
39
+ tree = ast.parse(source)
40
+ docstrings = _doc_string_nodes(tree)
41
+ offenders = []
42
+ for node in ast.walk(tree):
43
+ if not isinstance(node, ast.Constant) or id(node) in docstrings:
44
+ continue
45
+ if isinstance(node.value, str) and VERSION_LITERAL.fullmatch(node.value):
46
+ loc = path.relative_to(API_ROOT.parent).as_posix()
47
+ offenders.append(f"{loc}:{node.lineno} = {node.value!r}")
48
+ return offenders
49
+
50
+
51
+ class TestVersionOutletsSameSource:
52
+ def test_openapi_meta_and_header_agree_in_one_process(self):
53
+ """Scenario: 同一进程对外声明的版本只有一个来源 (独立部署形态三出口齐全)。"""
54
+ app = create_standalone_app()
55
+ doc_version = app.openapi()["info"]["version"]
56
+ with TestClient(app) as client:
57
+ meta_version = client.get("/v1/meta").json()["version"]
58
+ header = client.get("/v1/health").headers["X-Aeval-Version"]
59
+
60
+ resolved = package_version()
61
+ assert doc_version == resolved, doc_version
62
+ assert meta_version == resolved, meta_version
63
+ assert header == resolved, header
64
+
65
+ def test_hosted_openapi_and_meta_agree(self):
66
+ """寄宿形态自己那两处出口同样同源 (它没有版本头, 那是独立部署专属)。"""
67
+ app = create_app()
68
+ resolved = package_version()
69
+ assert app.openapi()["info"]["version"] == resolved
70
+ assert meta_payload()["version"] == resolved
71
+
72
+ def test_mounted_openapi_served_under_prefix_matches(self):
73
+ """独立部署里内层 app 被挂到 /v1, 它自己的接口文档也是同一版本。"""
74
+ with TestClient(create_standalone_app()) as client:
75
+ resp = client.get("/v1/openapi.json")
76
+ assert resp.status_code == 200
77
+ assert resp.json()["info"]["version"] == package_version()
78
+
79
+ def test_bumping_the_package_version_moves_every_outlet(self):
80
+ """Scenario: 升级包版本后所有出口同步 —— 不改任何接口层代码。"""
81
+ import agent_eval.api.app as app_module
82
+ import agent_eval.api.standalone as standalone_module
83
+
84
+ bumped = "9.9.9"
85
+ original = app_module.package_version
86
+ assert package_version() != bumped
87
+ app_module.package_version = lambda: bumped
88
+ standalone_module.package_version = lambda: bumped
89
+ try:
90
+ app = create_standalone_app()
91
+ with TestClient(app) as client:
92
+ assert app.openapi()["info"]["version"] == bumped
93
+ assert client.get("/v1/meta").json()["version"] == bumped
94
+ assert client.get("/v1/health").headers["X-Aeval-Version"] == bumped
95
+ assert create_app().openapi()["info"]["version"] == bumped
96
+ assert meta_payload()["version"] == bumped
97
+ finally:
98
+ app_module.package_version = original
99
+ standalone_module.package_version = original
100
+
101
+
102
+ class TestDeploymentFormsAgree:
103
+ def test_mounted_and_standalone_read_the_same_doc_version(self):
104
+ """Scenario: 两种部署形态给出同一版本 —— 挂载形态不得成为例外。"""
105
+ hosted = create_app().openapi()["info"]["version"]
106
+ standalone = create_standalone_app().openapi()["info"]["version"]
107
+ assert hosted == standalone == package_version()
108
+
109
+
110
+ class TestNoSecondVersionLiteral:
111
+ def test_api_source_is_found_by_the_scan(self):
112
+ """一条永远绿的守护测试比没有更糟 —— 先确认扫描真的覆盖了 api/ 源码。"""
113
+ scanned = sorted(p.name for p in API_ROOT.rglob("*.py"))
114
+ assert {"app.py", "standalone.py"} <= set(scanned), scanned
115
+
116
+ def test_guard_actually_catches_a_planted_literal(self):
117
+ """守护测试自己得是活的: 手工植入一处字面量必须被抓到并报出位置。"""
118
+ planted = 'app = FastAPI(title="Aeval API", version="0.1.0")\n'
119
+ offenders = _version_literals_in(planted, API_ROOT / "planted.py")
120
+ assert len(offenders) == 1, offenders
121
+ assert offenders[0].endswith("planted.py:1 = '0.1.0'"), offenders
122
+
123
+ def test_api_layer_has_no_hardcoded_version_string(self):
124
+ """Scenario: 出现第二处版本字面量时测试失败并报出该位置。"""
125
+ offenders = []
126
+ for path in sorted(API_ROOT.rglob("*.py")):
127
+ offenders += _version_literals_in(path.read_text(encoding="utf-8"), path)
128
+
129
+ assert not offenders, (
130
+ "api/ 下出现写死的版本号字面量, 必须改走 package_version(): " + "; ".join(offenders)
131
+ )