trace2eval-cli 0.2.1__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.
- trace2eval/__init__.py +65 -0
- trace2eval/__main__.py +10 -0
- trace2eval/checks.py +149 -0
- trace2eval/cli.py +307 -0
- trace2eval/matchers.py +101 -0
- trace2eval/report.py +221 -0
- trace2eval/runner.py +251 -0
- trace2eval/schema.py +233 -0
- trace2eval/select.py +709 -0
- trace2eval/signals.py +207 -0
- trace2eval_cli-0.2.1.dist-info/METADATA +227 -0
- trace2eval_cli-0.2.1.dist-info/RECORD +16 -0
- trace2eval_cli-0.2.1.dist-info/WHEEL +5 -0
- trace2eval_cli-0.2.1.dist-info/entry_points.txt +2 -0
- trace2eval_cli-0.2.1.dist-info/licenses/LICENSE +21 -0
- trace2eval_cli-0.2.1.dist-info/top_level.txt +1 -0
trace2eval/signals.py
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"""Signals: deciding which traces are worth turning into test cases.
|
|
2
|
+
|
|
3
|
+
A production log is mostly boring. If you dump ten thousand calls into a test
|
|
4
|
+
suite you get a suite that is slow, redundant, and ignored. What you want is the
|
|
5
|
+
handful of calls that carry information -- the ones where something already went
|
|
6
|
+
wrong, or where the shape of the answer matters.
|
|
7
|
+
|
|
8
|
+
Each signal below is a cheap, deterministic observation about a single row plus
|
|
9
|
+
a little global context (percentiles across the whole log). Signals carry
|
|
10
|
+
weights, and weights add up into a score. The score decides what gets promoted.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import math
|
|
17
|
+
import statistics
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
from typing import Any, Iterable
|
|
20
|
+
|
|
21
|
+
from .checks import detect_fallback
|
|
22
|
+
from .schema import Trace
|
|
23
|
+
|
|
24
|
+
#: Default weights. Tuned so that a single strong signal outranks several weak
|
|
25
|
+
#: ones: an explicit thumbs-down should always beat "this call was a bit slow".
|
|
26
|
+
DEFAULT_WEIGHTS: dict[str, float] = {
|
|
27
|
+
"negative_feedback": 3.0,
|
|
28
|
+
"user_retried": 2.5,
|
|
29
|
+
"empty_output": 3.0,
|
|
30
|
+
"expected_shape_violated": 3.0,
|
|
31
|
+
"fallback_phrase": 2.0,
|
|
32
|
+
"explicit_expectation": 2.0,
|
|
33
|
+
"output_much_shorter": 1.5,
|
|
34
|
+
"output_much_longer": 1.0,
|
|
35
|
+
"slow_response": 1.0,
|
|
36
|
+
"expensive_call": 1.0,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
#: An output shorter than this is treated as suspicious on its own.
|
|
40
|
+
MIN_PLAUSIBLE_CHARS = 12
|
|
41
|
+
|
|
42
|
+
#: A response is "much shorter/longer" past these ratios against the log median.
|
|
43
|
+
SHORT_RATIO = 0.5
|
|
44
|
+
LONG_RATIO = 3.0
|
|
45
|
+
|
|
46
|
+
# Backwards-compatible private aliases; the underscore names are historical.
|
|
47
|
+
_MIN_PLAUSIBLE_CHARS = MIN_PLAUSIBLE_CHARS
|
|
48
|
+
_SHORT_RATIO = SHORT_RATIO
|
|
49
|
+
_LONG_RATIO = LONG_RATIO
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def is_suspiciously_short(chars: int, median_output_chars: float) -> bool:
|
|
53
|
+
"""Whether a response this long counts as too short for this log."""
|
|
54
|
+
if median_output_chars <= 0:
|
|
55
|
+
return False
|
|
56
|
+
return (
|
|
57
|
+
chars < median_output_chars * SHORT_RATIO
|
|
58
|
+
and chars < MIN_PLAUSIBLE_CHARS * 2
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def suspiciously_short_boundary(median_output_chars: float) -> int | None:
|
|
63
|
+
"""The length a response had to fall below to be flagged as too short.
|
|
64
|
+
|
|
65
|
+
Exposed because the generated *checks* need the same number the *signal* used.
|
|
66
|
+
Deriving both from one function is the only way to stop them drifting apart:
|
|
67
|
+
if the threshold moved and the checks did not follow, a case built from a
|
|
68
|
+
length failure would stop reproducing its own failure, silently.
|
|
69
|
+
"""
|
|
70
|
+
if median_output_chars <= 0:
|
|
71
|
+
return None
|
|
72
|
+
return int(math.ceil(min(median_output_chars * SHORT_RATIO, MIN_PLAUSIBLE_CHARS * 2)))
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass
|
|
76
|
+
class Signal:
|
|
77
|
+
name: str
|
|
78
|
+
weight: float
|
|
79
|
+
detail: str
|
|
80
|
+
|
|
81
|
+
def to_dict(self) -> dict[str, Any]:
|
|
82
|
+
return {"name": self.name, "weight": self.weight, "detail": self.detail}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@dataclass
|
|
86
|
+
class TraceContext:
|
|
87
|
+
"""Log-wide statistics that individual signals are judged against."""
|
|
88
|
+
|
|
89
|
+
p95_latency_ms: float | None = None
|
|
90
|
+
p95_cost_usd: float | None = None
|
|
91
|
+
median_output_chars: float = 0.0
|
|
92
|
+
|
|
93
|
+
def to_dict(self) -> dict[str, Any]:
|
|
94
|
+
return {
|
|
95
|
+
"p95_latency_ms": self.p95_latency_ms,
|
|
96
|
+
"p95_cost_usd": self.p95_cost_usd,
|
|
97
|
+
"median_output_chars": round(self.median_output_chars, 1),
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def percentile(values: list[float], fraction: float) -> float | None:
|
|
102
|
+
"""Linear-interpolation percentile. Avoids depending on numpy for one number."""
|
|
103
|
+
clean = sorted(value for value in values if value is not None)
|
|
104
|
+
if not clean:
|
|
105
|
+
return None
|
|
106
|
+
if len(clean) == 1:
|
|
107
|
+
return clean[0]
|
|
108
|
+
position = fraction * (len(clean) - 1)
|
|
109
|
+
lower = int(position)
|
|
110
|
+
upper = min(lower + 1, len(clean) - 1)
|
|
111
|
+
weight = position - lower
|
|
112
|
+
return clean[lower] * (1 - weight) + clean[upper] * weight
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def build_context(traces: Iterable[Trace]) -> TraceContext:
|
|
116
|
+
trace_list = list(traces)
|
|
117
|
+
latencies = [t.latency_ms for t in trace_list if t.latency_ms is not None]
|
|
118
|
+
costs = [t.cost_usd for t in trace_list if t.cost_usd is not None]
|
|
119
|
+
char_counts = [t.output_chars for t in trace_list if t.output_chars > 0]
|
|
120
|
+
return TraceContext(
|
|
121
|
+
p95_latency_ms=percentile(latencies, 0.95),
|
|
122
|
+
p95_cost_usd=percentile(costs, 0.95),
|
|
123
|
+
median_output_chars=statistics.median(char_counts) if char_counts else 0.0,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _looks_like_json(text: str) -> bool:
|
|
128
|
+
stripped = text.strip()
|
|
129
|
+
if not stripped or stripped[0] not in "[{":
|
|
130
|
+
return False
|
|
131
|
+
try:
|
|
132
|
+
json.loads(stripped)
|
|
133
|
+
except (json.JSONDecodeError, TypeError):
|
|
134
|
+
return False
|
|
135
|
+
return True
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def compute_signals(
|
|
139
|
+
trace: Trace,
|
|
140
|
+
context: TraceContext,
|
|
141
|
+
weights: dict[str, float] | None = None,
|
|
142
|
+
) -> list[Signal]:
|
|
143
|
+
"""Collect every signal that fires for this trace."""
|
|
144
|
+
active = {**DEFAULT_WEIGHTS, **(weights or {})}
|
|
145
|
+
signals: list[Signal] = []
|
|
146
|
+
|
|
147
|
+
def add(name: str, detail: str) -> None:
|
|
148
|
+
weight = active.get(name, 0.0)
|
|
149
|
+
if weight > 0:
|
|
150
|
+
signals.append(Signal(name=name, weight=weight, detail=detail))
|
|
151
|
+
|
|
152
|
+
if trace.is_negative:
|
|
153
|
+
add("negative_feedback", "user gave negative feedback")
|
|
154
|
+
|
|
155
|
+
if trace.retried:
|
|
156
|
+
add("user_retried", "the same request was retried")
|
|
157
|
+
|
|
158
|
+
chars = trace.output_chars
|
|
159
|
+
if chars == 0:
|
|
160
|
+
add("empty_output", "model returned an empty response")
|
|
161
|
+
else:
|
|
162
|
+
fallback = detect_fallback(trace.output)
|
|
163
|
+
if fallback:
|
|
164
|
+
add("fallback_phrase", f"output contains {fallback!r}")
|
|
165
|
+
|
|
166
|
+
median = context.median_output_chars
|
|
167
|
+
if is_suspiciously_short(chars, median):
|
|
168
|
+
add(
|
|
169
|
+
"output_much_shorter",
|
|
170
|
+
f"{chars} chars vs log median {median:.0f}",
|
|
171
|
+
)
|
|
172
|
+
elif median > 0 and chars > median * LONG_RATIO:
|
|
173
|
+
add(
|
|
174
|
+
"output_much_longer",
|
|
175
|
+
f"{chars} chars vs log median {median:.0f}",
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
if trace.expect:
|
|
179
|
+
add("explicit_expectation", "trace already carried an expectation block")
|
|
180
|
+
if trace.expect.get("json") and not _looks_like_json(trace.output):
|
|
181
|
+
add("expected_shape_violated", "expected JSON output, got something else")
|
|
182
|
+
|
|
183
|
+
if (
|
|
184
|
+
context.p95_latency_ms is not None
|
|
185
|
+
and trace.latency_ms is not None
|
|
186
|
+
and trace.latency_ms > context.p95_latency_ms
|
|
187
|
+
):
|
|
188
|
+
add(
|
|
189
|
+
"slow_response",
|
|
190
|
+
f"{trace.latency_ms:.0f}ms above p95 {context.p95_latency_ms:.0f}ms",
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
if (
|
|
194
|
+
context.p95_cost_usd is not None
|
|
195
|
+
and trace.cost_usd is not None
|
|
196
|
+
and trace.cost_usd > context.p95_cost_usd
|
|
197
|
+
):
|
|
198
|
+
add(
|
|
199
|
+
"expensive_call",
|
|
200
|
+
f"${trace.cost_usd:.6f} above p95 ${context.p95_cost_usd:.6f}",
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
return signals
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def score(signals: list[Signal]) -> float:
|
|
207
|
+
return round(sum(signal.weight for signal in signals), 3)
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: trace2eval-cli
|
|
3
|
+
Version: 0.2.1
|
|
4
|
+
Summary: Turn production LLM traces into a regression eval set. Zero dependencies.
|
|
5
|
+
Author: rfioly
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/rfioly/trace2eval
|
|
8
|
+
Project-URL: Source, https://github.com/rfioly/trace2eval
|
|
9
|
+
Project-URL: Issues, https://github.com/rfioly/trace2eval/issues
|
|
10
|
+
Keywords: llm,evaluation,evals,observability,regression-testing,trace,ci
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
[English](README.en.md) | **简体中文**
|
|
30
|
+
|
|
31
|
+
# trace2eval
|
|
32
|
+
|
|
33
|
+
**把生产环境的 LLM 调用日志,变成一套回归测试集。**
|
|
34
|
+
|
|
35
|
+
它是 Pytest 那个路子,区别在于用例不用手写——从你已经发生的线上调用里挖出来。
|
|
36
|
+
|
|
37
|
+
零依赖、不用 API key、不调模型。同样的日志进,同样的用例出。
|
|
38
|
+
|
|
39
|
+
[](https://github.com/rfioly/trace2eval/actions/workflows/ci.yml)
|
|
40
|
+
[](https://www.python.org/)
|
|
41
|
+
[](#)
|
|
42
|
+
[](LICENSE)
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## 快速开始
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install -e .
|
|
50
|
+
|
|
51
|
+
# 1. 把日志变成用例集
|
|
52
|
+
trace2eval build traces.jsonl -o evalset/
|
|
53
|
+
|
|
54
|
+
# 2. 拿用例集给一批输出打分
|
|
55
|
+
trace2eval run --cases evalset/cases.jsonl --outputs outputs.jsonl -o runs/current.json
|
|
56
|
+
|
|
57
|
+
# 3. 有回归就失败(退出码 1),可直接挂 CI
|
|
58
|
+
trace2eval check --baseline runs/baseline.json --current runs/current.json
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
需要 Python 3.10+。
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## 三个命令
|
|
66
|
+
|
|
67
|
+
| 命令 | 做什么 |
|
|
68
|
+
| --- | --- |
|
|
69
|
+
| `build` | JSONL 调用日志 → 用例集(`cases.jsonl` + `report.md`) |
|
|
70
|
+
| `run` | 用例集 + 一批输出 → `metrics.json` |
|
|
71
|
+
| `check` | 两份指标对比 → 通过 / 失败 |
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## 特性
|
|
76
|
+
|
|
77
|
+
- **零依赖,不用 API key。** 纯标准库,不调任何模型,完全离线可跑。
|
|
78
|
+
- **确定性。** 同一份日志产出同一套用例,逐字节一致——这条在 CI 里有断言。
|
|
79
|
+
- **一个问题一个用例。** 被问了 200 次的问题只变成一个用例,并且老实记录它代表 200 次调用。
|
|
80
|
+
- **形状从干净答案学,不从失败那条学。** 失败种子的长度下限来自同问题的干净答案中位数。
|
|
81
|
+
- **每个用例自检。** 报告里会标明哪些用例的检查**抓不住**它当初为什么坏,不混进"已覆盖"里。
|
|
82
|
+
- **相似度可换。** `--similarity module:function` 换成你自己的实现,包本身不引依赖。
|
|
83
|
+
- **天生适合 CI。** `check` 检出回归返回非零退出码。
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## 跑一遍看看
|
|
88
|
+
|
|
89
|
+
```console
|
|
90
|
+
$ trace2eval build examples/sample_traces.jsonl -o evalset
|
|
91
|
+
read 42 traces from examples/sample_traces.jsonl
|
|
92
|
+
generated 16 cases -> evalset/cases.jsonl
|
|
93
|
+
8 collapsed as near-duplicates
|
|
94
|
+
3 case(s) carry checks that cannot detect the failure they came from
|
|
95
|
+
report -> evalset/report.md
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
42 次调用 → 16 个用例。每个用例都记了为什么被挑中、形状从哪来、自检结论:
|
|
99
|
+
|
|
100
|
+
```markdown
|
|
101
|
+
| Case | Score | In log | Shape from | Self-check | Input |
|
|
102
|
+
| -------- | ----- | ------ | ------------------------------ | ---------------------- | ------------------------ |
|
|
103
|
+
| case-001 | 8.0 | 1 | 日志的短答线(弱参考) | ok | 订单一直显示处理中,已经三天了 |
|
|
104
|
+
| case-007 | 4.5 | 6 | 同一问题的 5 个干净答案 | ok | 你们的退款政策是什么? |
|
|
105
|
+
| case-011 | 2.5 | 4 | 同一问题的 3 个干净答案 | failure_not_reproduced | 修改手机号 |
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
再看门禁。改一次提示词,修好一处、弄坏五处:
|
|
109
|
+
|
|
110
|
+
```console
|
|
111
|
+
$ trace2eval check --baseline runs/baseline.json --current runs/current.json
|
|
112
|
+
| Metric | Baseline | Current | Rule |
|
|
113
|
+
| ----------------------- | -------- | ------- | ----------------------------------- |
|
|
114
|
+
| pass_rate | 1.0000 | 0.6875 | dropped by 0.3125 (allowed 0.0200) |
|
|
115
|
+
| format_compliance_rate | 1.0000 | 0.5000 | dropped by 0.5000 (allowed 0.0200) |
|
|
116
|
+
| fallback_rate | 0 | 0.0625 | rose by 0.0625 (allowed 0.0200) |
|
|
117
|
+
|
|
118
|
+
FAIL: 3 regression(s) detected
|
|
119
|
+
$ echo $?
|
|
120
|
+
1
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
五个失败横跨三种失败模式:
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
case-001: min_chars <- 答案被截断
|
|
127
|
+
case-002: not_fallback, min_chars <- 直接放弃作答
|
|
128
|
+
case-004: json <- 声明过的 JSON 契约没兑现
|
|
129
|
+
case-007: min_chars
|
|
130
|
+
case-011: min_chars
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## 它怎么挑用例
|
|
136
|
+
|
|
137
|
+
```mermaid
|
|
138
|
+
flowchart LR
|
|
139
|
+
A[JSONL 日志] --> B[解析与归一化]
|
|
140
|
+
B --> C[计算信号得分]
|
|
141
|
+
C --> D[近重复聚类]
|
|
142
|
+
D --> E{分数高于<br/>最低分?}
|
|
143
|
+
E -- 否 --> F[丢弃]
|
|
144
|
+
E -- 是 --> G[构建用例]
|
|
145
|
+
G --> H[cases.jsonl<br/>report.md]
|
|
146
|
+
H --> I[对输出评分]
|
|
147
|
+
I --> J[metrics.json]
|
|
148
|
+
J --> K{与基线对比}
|
|
149
|
+
K -- 有回归 --> L[退出码 1]
|
|
150
|
+
K -- 干净 --> M[退出码 0]
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
每条 trace 按信号加权重打分,分数决定谁被提拔。信号和权重就在 `signals.py` 顶部:
|
|
154
|
+
|
|
155
|
+
| 信号 | 权重 | 为什么它算信号 |
|
|
156
|
+
| --- | --- | --- |
|
|
157
|
+
| `negative_feedback` | 3.0 | 用户已经告诉你答错了。 |
|
|
158
|
+
| `empty_output` | 3.0 | 什么都没返回。 |
|
|
159
|
+
| `expected_shape_violated` | 3.0 | 声明过的契约(比如 JSON)没被满足。 |
|
|
160
|
+
| `user_retried` | 2.5 | 他问了两遍,说明第一次没解决。 |
|
|
161
|
+
| `fallback_phrase` | 2.0 | 模型放弃回答,而不是作答。 |
|
|
162
|
+
| `explicit_expectation` | 2.0 | 这条 trace 自己就带了断言。 |
|
|
163
|
+
| `output_much_shorter` | 1.5 | 大概率被截断,和日志中位数比。 |
|
|
164
|
+
| `output_much_longer` | 1.0 | 通常是不受控的长篇大论。 |
|
|
165
|
+
| `slow_response` | 1.0 | 超过整份日志的 p95 延迟。 |
|
|
166
|
+
| `expensive_call` | 1.0 | 超过整份日志的 p95 成本。 |
|
|
167
|
+
|
|
168
|
+
设计取舍的完整论证在 [`DECISIONS.md`](DECISIONS.md)。
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## 输入格式
|
|
173
|
+
|
|
174
|
+
每行一个 JSON 对象,只有 `input` 和 `output` 必填,常见别名(`prompt`/`response`、`question`/`completion` 等)也认。
|
|
175
|
+
|
|
176
|
+
```json
|
|
177
|
+
{
|
|
178
|
+
"id": "req-0042",
|
|
179
|
+
"input": "账单为什么变多了",
|
|
180
|
+
"output": "账单增加通常是因为套餐在到期后自动续费……",
|
|
181
|
+
"latency_ms": 830,
|
|
182
|
+
"cost_usd": 0.00027,
|
|
183
|
+
"feedback": "negative",
|
|
184
|
+
"retried": true,
|
|
185
|
+
"expect": { "json": true, "required": ["status"], "contains": ["已受理"] }
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
格式坏掉的行、没有输入的行会跳过并计数。空的 `output` 不算坏行——它是日志里最有价值的行之一。
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## 已知边界
|
|
194
|
+
|
|
195
|
+
- **相似度是字符 n-gram**,不含相同字符的改写会被当成两个问题。可用 `--similarity` 换成你自己的实现。
|
|
196
|
+
- **行为型失败抓不住。** 样本日志 16 个用例里有 3 个,失败原因只是"用户又问了一遍",输出本身没毛病——任何对输出文本的确定性检查都抓不到。报告里已单独标出。
|
|
197
|
+
- **比较成本是 O(用例数 × 每簇不同问法数)**,不是 O(日志大小):同一问题问 5000 次只留 1 个待比对指纹。`MAX_DISTINCT_FINGERPRINTS = 512` 是拍的保护值,没压测过。
|
|
198
|
+
- **用例仍是提案**,但会自检:`report.md` 里单列"需要人看一眼"的用例。
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## 仓库结构
|
|
203
|
+
|
|
204
|
+
```
|
|
205
|
+
src/trace2eval/
|
|
206
|
+
schema.py trace 加载、字段别名、容错解析
|
|
207
|
+
signals.py 一条 trace 为什么值得测
|
|
208
|
+
select.py 相似度、聚类、用例构建
|
|
209
|
+
checks.py 确定性输出检查
|
|
210
|
+
runner.py 评分与回归对比
|
|
211
|
+
report.py markdown 渲染
|
|
212
|
+
matchers.py 可替换的相似度实现
|
|
213
|
+
tests/ 42 个测试
|
|
214
|
+
examples/ 一份 42 行样本日志 + 一次基线 / 一次回归运行
|
|
215
|
+
evalset/ 提交进仓库的构建产物
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## 开发
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
pip install -e ".[dev]"
|
|
222
|
+
pytest
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## 许可证
|
|
226
|
+
|
|
227
|
+
MIT
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
trace2eval/__init__.py,sha256=U4FZFVE_8PlvsvDu36hNam03SP4eshEV3V6QIHR5cL8,1833
|
|
2
|
+
trace2eval/__main__.py,sha256=3-8jUqJ56SMv1AO_k9zL-n4m2thfa83lqjYyFfglvbE,198
|
|
3
|
+
trace2eval/checks.py,sha256=u2ZuguIZa5pQXnmwtpxNtGIyCHyFOZZuKeDPWgsRuSc,5456
|
|
4
|
+
trace2eval/cli.py,sha256=8qe378FOkzqVo6c8G48c4Il9ypY5c4Lyn5oXjZ6rrWo,10627
|
|
5
|
+
trace2eval/matchers.py,sha256=mvYM7D1hSd7MjlYya97KpVX8BEF58JbmN5-gDmJ7Clg,4079
|
|
6
|
+
trace2eval/report.py,sha256=sMWIZOS9Yx-4X5vF__0LSnP35JI02SYR9Jx8GkdPr8g,9207
|
|
7
|
+
trace2eval/runner.py,sha256=W33eWThu_Ray0xh7vf8TX_K31CE9FRmjlkIlwWa4N74,8216
|
|
8
|
+
trace2eval/schema.py,sha256=AFBDtOVjoe98Dtg8PvGAVXXJxByz54OzVhc5LpKmnYE,7028
|
|
9
|
+
trace2eval/select.py,sha256=8tuMFu6kxE9VP3TCATCyEWCK0m3tK-Zyf6EqP9U-acA,27403
|
|
10
|
+
trace2eval/signals.py,sha256=wUWr8JYHblx3lRz2ugfPb8Mgat3wEB8ck45XYi95B10,6857
|
|
11
|
+
trace2eval_cli-0.2.1.dist-info/licenses/LICENSE,sha256=V3e-V4qN_C-iuTVsa-1pRUrxkrPLWTLpqFuhS896Z_Q,1063
|
|
12
|
+
trace2eval_cli-0.2.1.dist-info/METADATA,sha256=YVbZJT1pYBQlZdYK7jzwVQ2Ntoz4YNCRNE89Tz4kndc,8652
|
|
13
|
+
trace2eval_cli-0.2.1.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
14
|
+
trace2eval_cli-0.2.1.dist-info/entry_points.txt,sha256=KDIbfvCzPM3uUMSFZF1-JJhlyrOZHO3SLut02AFY9gQ,51
|
|
15
|
+
trace2eval_cli-0.2.1.dist-info/top_level.txt,sha256=UC0TN9YklI4pHkU46JR2YZLrBqzkWXkx6njiHyxdSls,11
|
|
16
|
+
trace2eval_cli-0.2.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 rfioly
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
trace2eval
|