dpaa 0.0.5__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.
- dpaa-0.0.5/.gitignore +13 -0
- dpaa-0.0.5/LICENSE +21 -0
- dpaa-0.0.5/PKG-INFO +22 -0
- dpaa-0.0.5/README.md +3 -0
- dpaa-0.0.5/pyproject.toml +62 -0
- dpaa-0.0.5/src/dpaa/__init__.py +10 -0
- dpaa-0.0.5/src/dpaa/analyze.py +27 -0
- dpaa-0.0.5/src/dpaa/core/__init__.py +1 -0
- dpaa-0.0.5/src/dpaa/core/llm_usage.py +196 -0
- dpaa-0.0.5/src/dpaa/core/metadata.py +97 -0
- dpaa-0.0.5/src/dpaa/core/metric_spec.py +137 -0
- dpaa-0.0.5/src/dpaa/core/tool_representation/__init__.py +88 -0
- dpaa-0.0.5/src/dpaa/core/tool_representation/canonicalizer.py +96 -0
- dpaa-0.0.5/src/dpaa/core/tool_representation/context_tracker.py +117 -0
- dpaa-0.0.5/src/dpaa/core/tool_representation/equivalence.py +170 -0
- dpaa-0.0.5/src/dpaa/core/tool_representation/extractor.py +35 -0
- dpaa-0.0.5/src/dpaa/core/tool_representation/features.py +19 -0
- dpaa-0.0.5/src/dpaa/core/tool_representation/functional_groups.py +57 -0
- dpaa-0.0.5/src/dpaa/core/tool_representation/intent_labeler.py +129 -0
- dpaa-0.0.5/src/dpaa/core/tool_representation/representation.py +72 -0
- dpaa-0.0.5/src/dpaa/core/tool_representation/state_assembler.py +17 -0
- dpaa-0.0.5/src/dpaa/core/tool_representation/states.py +32 -0
- dpaa-0.0.5/src/dpaa/core/tool_representation/terminal_command_classifier.py +158 -0
- dpaa-0.0.5/src/dpaa/core/tool_representation/tools_config.py +33 -0
- dpaa-0.0.5/src/dpaa/core/traces.py +183 -0
- dpaa-0.0.5/src/dpaa/core/traces_scored.py +104 -0
- dpaa-0.0.5/src/dpaa/distribution_inspection/__init__.py +1 -0
- dpaa-0.0.5/src/dpaa/distribution_inspection/distribution_builders.py +80 -0
- dpaa-0.0.5/src/dpaa/metrics/__init__.py +3 -0
- dpaa-0.0.5/src/dpaa/metrics/consistency/consistency_scorer.py +177 -0
- dpaa-0.0.5/src/dpaa/metrics/consistency/consistency_scorer_spec.py +116 -0
- dpaa-0.0.5/src/dpaa/metrics/consistency/implementation/__init__.py +1 -0
- dpaa-0.0.5/src/dpaa/metrics/consistency/implementation/outcome_consistency.py +105 -0
- dpaa-0.0.5/src/dpaa/metrics/consistency/implementation/resource_consistency.py +203 -0
- dpaa-0.0.5/src/dpaa/metrics/consistency/implementation/trajectory_distribution_consistency.py +128 -0
- dpaa-0.0.5/src/dpaa/metrics/consistency/implementation/trajectory_sequence_consistency.py +115 -0
- dpaa-0.0.5/src/dpaa/metrics/consistency/outcome.py +47 -0
- dpaa-0.0.5/src/dpaa/metrics/consistency/overall.py +114 -0
- dpaa-0.0.5/src/dpaa/metrics/consistency/resource.py +57 -0
- dpaa-0.0.5/src/dpaa/metrics/consistency/trajectory_distribution.py +63 -0
- dpaa-0.0.5/src/dpaa/metrics/consistency/trajectory_sequence.py +58 -0
- dpaa-0.0.5/src/dpaa/metrics/consistency/utils/__init__.py +1 -0
- dpaa-0.0.5/src/dpaa/metrics/consistency/utils/representation.py +47 -0
- dpaa-0.0.5/src/dpaa/metrics/consistency/utils/tool_distribution.py +89 -0
- dpaa-0.0.5/src/dpaa/py.typed +0 -0
- dpaa-0.0.5/src/dpaa/scorer.py +20 -0
- dpaa-0.0.5/src/dpaa/utils/__init__.py +1 -0
- dpaa-0.0.5/src/dpaa/utils/token_counter.py +9 -0
dpaa-0.0.5/.gitignore
ADDED
dpaa-0.0.5/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 JetBrains s.r.o.
|
|
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.
|
dpaa-0.0.5/PKG-INFO
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: dpaa
|
|
3
|
+
Version: 0.0.5
|
|
4
|
+
Summary: Consistency metrics for repeated AI-agent attempts
|
|
5
|
+
Author: Nikita Dukin, Egor Klimov, Saga Rut Sunnevudóttir, Vladimir Muzhichenko
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Requires-Python: >=3.11
|
|
13
|
+
Requires-Dist: numpy<3,>=2.4
|
|
14
|
+
Requires-Dist: pandas<3,>=2.3
|
|
15
|
+
Requires-Dist: rapidfuzz<4,>=3.14
|
|
16
|
+
Requires-Dist: scipy<2,>=1.16
|
|
17
|
+
Requires-Dist: typing-extensions<5,>=4.14
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# dpaa
|
|
21
|
+
|
|
22
|
+
Experimental Python library for AI-agent analysis.
|
dpaa-0.0.5/README.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.26,<2"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "dpaa"
|
|
7
|
+
version = "0.0.5"
|
|
8
|
+
description = "Consistency metrics for repeated AI-agent attempts"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
authors = [
|
|
12
|
+
{ name = "Nikita Dukin" },
|
|
13
|
+
{ name = "Egor Klimov" },
|
|
14
|
+
{ name = "Saga Rut Sunnevudóttir" },
|
|
15
|
+
{ name = "Vladimir Muzhichenko" },
|
|
16
|
+
]
|
|
17
|
+
license-files = ["LICENSE"]
|
|
18
|
+
classifiers = [
|
|
19
|
+
"Development Status :: 3 - Alpha",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Operating System :: OS Independent",
|
|
24
|
+
]
|
|
25
|
+
dependencies = [
|
|
26
|
+
"scipy>=1.16,<2",
|
|
27
|
+
"pandas>=2.3,<3",
|
|
28
|
+
"numpy>=2.4,<3",
|
|
29
|
+
"rapidfuzz>=3.14,<4",
|
|
30
|
+
"typing-extensions>=4.14,<5",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[tool.hatch.build.targets.wheel]
|
|
34
|
+
packages = ["src/dpaa"]
|
|
35
|
+
|
|
36
|
+
[tool.hatch.build.targets.sdist]
|
|
37
|
+
include = ["/src/dpaa", "/LICENSE", "/README.md", "/pyproject.toml"]
|
|
38
|
+
|
|
39
|
+
[dependency-groups]
|
|
40
|
+
dev = ["pytest>=9,<10", "ruff>=0.15,<0.16"]
|
|
41
|
+
|
|
42
|
+
[tool.pytest.ini_options]
|
|
43
|
+
testpaths = ["tests"]
|
|
44
|
+
pythonpath = ["."]
|
|
45
|
+
|
|
46
|
+
[tool.ruff]
|
|
47
|
+
line-length = 100
|
|
48
|
+
target-version = "py311"
|
|
49
|
+
|
|
50
|
+
[tool.ruff.lint]
|
|
51
|
+
extend-select = ["I", "F", "E", "W", "UP", "B", "SIM", "PL"]
|
|
52
|
+
|
|
53
|
+
[tool.ruff.lint.per-file-ignores]
|
|
54
|
+
"tests/**" = ["PLR2004"]
|
|
55
|
+
"src/dpaa/**" = ["E501"]
|
|
56
|
+
"src/dpaa/core/tool_representation/equivalence.py" = ["PLR0911"]
|
|
57
|
+
"src/dpaa/core/tool_representation/intent_labeler.py" = ["PLR0911", "PLR0913"]
|
|
58
|
+
"src/dpaa/core/tool_representation/representation.py" = ["PLR0913"]
|
|
59
|
+
"src/dpaa/core/tool_representation/terminal_command_classifier.py" = ["PLR0911", "PLR0912"]
|
|
60
|
+
|
|
61
|
+
[tool.ruff.lint.isort]
|
|
62
|
+
known-first-party = ["dpaa", "scripts"]
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from dpaa.analyze import analyze as analyze
|
|
2
|
+
from dpaa.core.metric_spec import MetricAnchors as MetricAnchors
|
|
3
|
+
from dpaa.core.metric_spec import MetricId as MetricId
|
|
4
|
+
from dpaa.core.metric_spec import MetricSpec as MetricSpec
|
|
5
|
+
from dpaa.core.metric_spec import ProvidesMetricValues as ProvidesMetricValues
|
|
6
|
+
from dpaa.core.metric_spec import ScorerSpec as ScorerSpec
|
|
7
|
+
from dpaa.core.traces import AgentStep as AgentStep
|
|
8
|
+
from dpaa.core.traces import AgentStepKind as AgentStepKind
|
|
9
|
+
from dpaa.core.traces import AgentTrace as AgentTrace
|
|
10
|
+
from dpaa.core.traces import AgentTraces as AgentTraces
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
4
|
+
from typing import TYPE_CHECKING, overload
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from dpaa.core.traces import AgentTraces
|
|
8
|
+
from dpaa.core.traces_scored import ScoredAgentTraces
|
|
9
|
+
from dpaa.scorer import Scorer
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@overload
|
|
13
|
+
def analyze(runs: AgentTraces, scorer: Scorer) -> ScoredAgentTraces: ...
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@overload
|
|
17
|
+
def analyze(runs: AgentTraces, scorer: dict[str, Scorer]) -> dict[str, ScoredAgentTraces]: ...
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def analyze(
|
|
21
|
+
runs: AgentTraces, scorer: Scorer | dict[str, Scorer]
|
|
22
|
+
) -> ScoredAgentTraces | dict[str, ScoredAgentTraces]:
|
|
23
|
+
if isinstance(scorer, dict):
|
|
24
|
+
with ThreadPoolExecutor(max_workers=len(scorer)) as executor:
|
|
25
|
+
futures = {name: executor.submit(s.score, runs) for name, s in scorer.items()}
|
|
26
|
+
return {name: future.result() for name, future in futures.items()}
|
|
27
|
+
return scorer.score(runs)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
from collections.abc import Mapping
|
|
5
|
+
from typing import Any, cast
|
|
6
|
+
|
|
7
|
+
from dpaa.core.metadata import (
|
|
8
|
+
CACHE_WRITE_TOKENS,
|
|
9
|
+
CACHED_INPUT_TOKENS,
|
|
10
|
+
COST_USD,
|
|
11
|
+
EST_INPUT_TOKENS,
|
|
12
|
+
EST_OUTPUT_TOKENS,
|
|
13
|
+
EST_TOTAL_TOKENS,
|
|
14
|
+
INPUT_TOKENS,
|
|
15
|
+
OUTPUT_TOKENS,
|
|
16
|
+
REASONING_TOKENS,
|
|
17
|
+
RUN_STEPS_N,
|
|
18
|
+
RUN_USAGE,
|
|
19
|
+
STEP_USAGE,
|
|
20
|
+
TOTAL_TOKENS,
|
|
21
|
+
)
|
|
22
|
+
from dpaa.core.traces import AgentStep, AgentStepKind, AgentTrace
|
|
23
|
+
from dpaa.utils.token_counter import TokenCounter
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def build_step_usage_meta(
|
|
27
|
+
payload: object, *, attributes: Mapping[str, Any] | None = None
|
|
28
|
+
) -> dict[str, Any]:
|
|
29
|
+
raw: Mapping[str, Any] = (
|
|
30
|
+
cast(Mapping[str, Any], payload) if isinstance(payload, Mapping) else {}
|
|
31
|
+
)
|
|
32
|
+
usage = {**_reported_usage(attributes or {}), **_normalize_usage(raw)}
|
|
33
|
+
return {STEP_USAGE: usage} if any(usage.values()) else {}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def get_or_compute_step_usage(step: AgentStep | None) -> Mapping[str, float]:
|
|
37
|
+
if step is None:
|
|
38
|
+
return {}
|
|
39
|
+
payload = step.metadata.get(STEP_USAGE)
|
|
40
|
+
if isinstance(payload, Mapping):
|
|
41
|
+
return _normalize_usage(cast(Mapping[str, Any], payload))
|
|
42
|
+
return _reported_usage(step.metadata)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def enrich_metadata_with_usage(
|
|
46
|
+
trace: AgentTrace, counter: TokenCounter | None = None
|
|
47
|
+
) -> AgentTrace:
|
|
48
|
+
counter = counter or TokenCounter()
|
|
49
|
+
usage = _compute_steps_usage(trace.steps)
|
|
50
|
+
if TOTAL_TOKENS not in usage:
|
|
51
|
+
usage |= estimate_steps_usage(trace.steps, counter)
|
|
52
|
+
trace.metadata[RUN_USAGE] = usage
|
|
53
|
+
return trace
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def get_or_compute_trace_usage(trace: AgentTrace) -> Mapping[str, float]:
|
|
57
|
+
recorded = trace.metadata.get(RUN_USAGE)
|
|
58
|
+
if isinstance(recorded, Mapping):
|
|
59
|
+
return cast(Mapping[str, Any], recorded)
|
|
60
|
+
return _compute_steps_usage(trace.steps)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def step_total_tokens(step: AgentStep | None) -> float | None:
|
|
64
|
+
return usage_total_tokens(get_or_compute_step_usage(step))
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
_ALIASES: dict[str, tuple[str, ...]] = {
|
|
68
|
+
INPUT_TOKENS: (INPUT_TOKENS, "prompt_tokens", "promptTokens", "gen_ai.usage.input_tokens"),
|
|
69
|
+
OUTPUT_TOKENS: (
|
|
70
|
+
OUTPUT_TOKENS,
|
|
71
|
+
"completion_tokens",
|
|
72
|
+
"completionTokens",
|
|
73
|
+
"gen_ai.usage.output_tokens",
|
|
74
|
+
),
|
|
75
|
+
CACHED_INPUT_TOKENS: (
|
|
76
|
+
CACHED_INPUT_TOKENS,
|
|
77
|
+
"cached_tokens",
|
|
78
|
+
"cache_read_input_tokens",
|
|
79
|
+
"input_tokens_cache_read",
|
|
80
|
+
"gen_ai.usage.cache_read_input_tokens",
|
|
81
|
+
),
|
|
82
|
+
CACHE_WRITE_TOKENS: (
|
|
83
|
+
CACHE_WRITE_TOKENS,
|
|
84
|
+
"cache_creation_input_tokens",
|
|
85
|
+
"input_tokens_cache_write",
|
|
86
|
+
"gen_ai.usage.cache_creation_input_tokens",
|
|
87
|
+
),
|
|
88
|
+
REASONING_TOKENS: (REASONING_TOKENS, "gen_ai.usage.reasoning_tokens"),
|
|
89
|
+
TOTAL_TOKENS: (TOTAL_TOKENS, "totalTokens", "gen_ai.usage.total_tokens"),
|
|
90
|
+
COST_USD: (COST_USD, "gen_ai.usage.cost", "gen_ai.usage.cost_usd", "totalCost"),
|
|
91
|
+
}
|
|
92
|
+
_PAYLOAD_ALIASES: dict[str, tuple[str, ...]] = {
|
|
93
|
+
INPUT_TOKENS: ("input",),
|
|
94
|
+
OUTPUT_TOKENS: ("output",),
|
|
95
|
+
TOTAL_TOKENS: ("total",),
|
|
96
|
+
COST_USD: ("cost",),
|
|
97
|
+
}
|
|
98
|
+
_ANY_VALUE_FIELDS = ("intValue", "doubleValue", "stringValue")
|
|
99
|
+
_RUN_SUMMED = (
|
|
100
|
+
INPUT_TOKENS,
|
|
101
|
+
OUTPUT_TOKENS,
|
|
102
|
+
CACHED_INPUT_TOKENS,
|
|
103
|
+
CACHE_WRITE_TOKENS,
|
|
104
|
+
REASONING_TOKENS,
|
|
105
|
+
COST_USD,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _compute_steps_usage(steps: list[AgentStep]) -> dict[str, float]:
|
|
110
|
+
totals: dict[str, float] = {RUN_STEPS_N: len(steps)}
|
|
111
|
+
for step in steps:
|
|
112
|
+
if step.kind is not AgentStepKind.LLM:
|
|
113
|
+
continue
|
|
114
|
+
usage = get_or_compute_step_usage(step)
|
|
115
|
+
reported = {name: usage[name] for name in _RUN_SUMMED if name in usage}
|
|
116
|
+
total = usage_total_tokens(usage)
|
|
117
|
+
if total is not None:
|
|
118
|
+
reported[TOTAL_TOKENS] = total
|
|
119
|
+
for name, value in reported.items():
|
|
120
|
+
totals[name] = totals.get(name, 0.0) + value
|
|
121
|
+
return totals
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def estimate_steps_usage(
|
|
125
|
+
steps: list[AgentStep], counter: TokenCounter | None = None
|
|
126
|
+
) -> dict[str, float]:
|
|
127
|
+
counter = counter or TokenCounter()
|
|
128
|
+
inputs = float(sum(counter(step.input) for step in steps))
|
|
129
|
+
outputs = float(sum(counter(step.output) for step in steps))
|
|
130
|
+
estimated = {
|
|
131
|
+
EST_INPUT_TOKENS: inputs,
|
|
132
|
+
EST_OUTPUT_TOKENS: outputs,
|
|
133
|
+
EST_TOTAL_TOKENS: inputs + outputs,
|
|
134
|
+
}
|
|
135
|
+
return {name: value for name, value in estimated.items() if value}
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def usage_total_tokens(usage: Mapping[str, float]) -> float | None:
|
|
139
|
+
total = usage.get(TOTAL_TOKENS)
|
|
140
|
+
if total is not None:
|
|
141
|
+
return total
|
|
142
|
+
parts = [usage[name] for name in (INPUT_TOKENS, OUTPUT_TOKENS) if name in usage]
|
|
143
|
+
return sum(parts) if parts else None
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _normalize_usage(payload: Mapping[str, Any]) -> dict[str, float]:
|
|
147
|
+
return _read(_flatten(payload), _ALIASES, _PAYLOAD_ALIASES)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _reported_usage(metadata: Mapping[str, Any]) -> dict[str, float]:
|
|
151
|
+
return _read(metadata, _ALIASES)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _read(raw: Mapping[str, Any], *tables: Mapping[str, tuple[str, ...]]) -> dict[str, float]:
|
|
155
|
+
usage: dict[str, float] = {}
|
|
156
|
+
for table in tables:
|
|
157
|
+
for name, aliases in table.items():
|
|
158
|
+
if name in usage:
|
|
159
|
+
continue
|
|
160
|
+
for alias in aliases:
|
|
161
|
+
value = _as_number(raw.get(alias))
|
|
162
|
+
if value is not None:
|
|
163
|
+
usage[name] = value
|
|
164
|
+
break
|
|
165
|
+
return usage
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _flatten(payload: Mapping[str, Any]) -> dict[str, Any]:
|
|
169
|
+
top: dict[str, Any] = {}
|
|
170
|
+
nested: dict[str, Any] = {}
|
|
171
|
+
for key, value in payload.items():
|
|
172
|
+
details = cast(Mapping[str, Any], value) if isinstance(value, Mapping) else None
|
|
173
|
+
if details is not None and _as_number(details) is None:
|
|
174
|
+
nested.update(details)
|
|
175
|
+
else:
|
|
176
|
+
top[key] = value
|
|
177
|
+
return {**nested, **top}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _as_number(value: object) -> float | None:
|
|
181
|
+
if value is None or isinstance(value, bool):
|
|
182
|
+
return None
|
|
183
|
+
if isinstance(value, Mapping):
|
|
184
|
+
wrapper = cast(Mapping[str, Any], value)
|
|
185
|
+
for field in _ANY_VALUE_FIELDS:
|
|
186
|
+
if field in wrapper:
|
|
187
|
+
return _as_number(wrapper[field])
|
|
188
|
+
return None
|
|
189
|
+
if isinstance(value, (int, float)):
|
|
190
|
+
number = float(value)
|
|
191
|
+
else:
|
|
192
|
+
try:
|
|
193
|
+
number = float(str(value))
|
|
194
|
+
except ValueError:
|
|
195
|
+
return None
|
|
196
|
+
return number if math.isfinite(number) else None
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import pandas as pd
|
|
8
|
+
|
|
9
|
+
DATASET = "dataset"
|
|
10
|
+
MODEL = "model"
|
|
11
|
+
TASK_ID = "task_id"
|
|
12
|
+
RUN_NAME = "run_name"
|
|
13
|
+
RESOLVED = "resolved"
|
|
14
|
+
RESOLVED_N = "resolved_n"
|
|
15
|
+
RESOLVED_RATE = "resolved_rate"
|
|
16
|
+
NUM_TOOL_CALLS = "num_tool_calls"
|
|
17
|
+
NUM_STEPS = "num_steps"
|
|
18
|
+
NUM_TOKENS = "num_tokens"
|
|
19
|
+
DURATION_S = "duration_s"
|
|
20
|
+
AVG_TOOL_CALLS = "avg_tool_calls"
|
|
21
|
+
AVG_TOOL_CALLS_RESOLVED = "avg_tool_calls_resolved"
|
|
22
|
+
AVG_TOOL_CALLS_UNRESOLVED = "avg_tool_calls_unresolved"
|
|
23
|
+
AVG_STEPS = "avg_steps"
|
|
24
|
+
AVG_TOKENS = "avg_tokens"
|
|
25
|
+
AVG_TOKENS_RESOLVED = "avg_tokens_resolved"
|
|
26
|
+
AVG_TOKENS_UNRESOLVED = "avg_tokens_unresolved"
|
|
27
|
+
UNIQUE_TOOLS_N = "unique_tools"
|
|
28
|
+
STEP_USAGE = "step_usage"
|
|
29
|
+
RUN_USAGE = "run_usage"
|
|
30
|
+
RUN_STEPS_N = "run_steps_n"
|
|
31
|
+
INPUT_TOKENS = "input_tokens"
|
|
32
|
+
OUTPUT_TOKENS = "output_tokens"
|
|
33
|
+
CACHED_INPUT_TOKENS = "cached_input_tokens"
|
|
34
|
+
CACHE_WRITE_TOKENS = "cache_write_tokens"
|
|
35
|
+
REASONING_TOKENS = "reasoning_tokens"
|
|
36
|
+
TOTAL_TOKENS = "total_tokens"
|
|
37
|
+
COST_USD = "cost_usd"
|
|
38
|
+
EST_INPUT_TOKENS = "est_input_tokens"
|
|
39
|
+
EST_OUTPUT_TOKENS = "est_output_tokens"
|
|
40
|
+
EST_TOTAL_TOKENS = "est_total_tokens"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def resolved_str(resolved: bool | None) -> str:
|
|
44
|
+
if resolved is None:
|
|
45
|
+
return ""
|
|
46
|
+
return str(int(resolved))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def resolved_series(series: pd.Series) -> pd.Series:
|
|
50
|
+
lowered = series.astype("string").str.lower().str.strip()
|
|
51
|
+
result = pd.Series(pd.NA, index=series.index, dtype="boolean")
|
|
52
|
+
result[lowered.isin(["true", "1", "1.0"])] = True
|
|
53
|
+
result[lowered.isin(["false", "0", "0.0"])] = False
|
|
54
|
+
return result
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass(frozen=True)
|
|
58
|
+
class TaskKey:
|
|
59
|
+
dataset: str
|
|
60
|
+
model: str
|
|
61
|
+
task_id: str
|
|
62
|
+
|
|
63
|
+
@classmethod
|
|
64
|
+
def of(cls, metadata: Mapping[str, Any], task_id: str = "") -> TaskKey:
|
|
65
|
+
return cls(
|
|
66
|
+
dataset=str(metadata.get(DATASET, "")),
|
|
67
|
+
model=str(metadata.get(MODEL, "")),
|
|
68
|
+
task_id=str(metadata.get(TASK_ID) or task_id),
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
def run(self, run_name: str) -> RunKey:
|
|
72
|
+
return RunKey(
|
|
73
|
+
dataset=self.dataset, model=self.model, task_id=self.task_id, run_name=run_name
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass(frozen=True)
|
|
78
|
+
class RunKey:
|
|
79
|
+
dataset: str
|
|
80
|
+
model: str
|
|
81
|
+
task_id: str
|
|
82
|
+
run_name: str
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def cell(self) -> TaskKey:
|
|
86
|
+
return TaskKey(self.dataset, self.model, self.task_id)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
CELL_KEY: tuple[str, str, str] = (DATASET, MODEL, TASK_ID)
|
|
90
|
+
RUN_KEY: tuple[str, str, str, str] = (DATASET, MODEL, TASK_ID, RUN_NAME)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def as_metadata(subject: RunKey | TaskKey) -> dict[str, str]:
|
|
94
|
+
keys = {DATASET: subject.dataset, MODEL: subject.model, TASK_ID: subject.task_id}
|
|
95
|
+
if isinstance(subject, RunKey):
|
|
96
|
+
keys[RUN_NAME] = subject.run_name
|
|
97
|
+
return keys
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
from collections.abc import Mapping
|
|
5
|
+
from dataclasses import dataclass, replace
|
|
6
|
+
from enum import StrEnum
|
|
7
|
+
from typing import NewType, Protocol, runtime_checkable
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class MetricDirection(StrEnum):
|
|
11
|
+
HIGHER_BETTER = "higher_better"
|
|
12
|
+
LOWER_BETTER = "lower_better"
|
|
13
|
+
NEUTRAL = "neutral"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class MetricGrain(StrEnum):
|
|
17
|
+
TRACE = "trace"
|
|
18
|
+
GROUP = "group"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class MetricPriority(StrEnum):
|
|
22
|
+
PRIMARY = "primary"
|
|
23
|
+
SECONDARY = "secondary"
|
|
24
|
+
TERTIARY = "tertiary"
|
|
25
|
+
DIAGNOSTIC = "diagnostic"
|
|
26
|
+
SPLIT = "split"
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def is_score(self) -> bool:
|
|
30
|
+
return self not in (MetricPriority.DIAGNOSTIC, MetricPriority.SPLIT)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True)
|
|
34
|
+
class MetricAnchors:
|
|
35
|
+
upper: float | None
|
|
36
|
+
lower: float | None
|
|
37
|
+
lower_label: str = "weak"
|
|
38
|
+
mid_label: str = "mixed"
|
|
39
|
+
upper_label: str = "strong"
|
|
40
|
+
inner_label: str | None = None
|
|
41
|
+
|
|
42
|
+
def __str__(self):
|
|
43
|
+
return self.anchor_line()
|
|
44
|
+
|
|
45
|
+
def anchor_line(self) -> str:
|
|
46
|
+
if self.inner_label and self.lower is not None and (self.upper is not None):
|
|
47
|
+
return f"`{self.lower:g}-{self.upper:g}` {self.inner_label}"
|
|
48
|
+
if self.lower is not None:
|
|
49
|
+
if self.upper is not None:
|
|
50
|
+
return f"`>= {self.upper}` {self.upper_label}, `{self.lower:g}-<{self.upper:g}` {self.mid_label}, `< {self.lower:g}` {self.lower_label}"
|
|
51
|
+
else:
|
|
52
|
+
return f"`< {self.lower:g}` {self.lower_label}"
|
|
53
|
+
elif self.upper is not None:
|
|
54
|
+
return f"`>= {self.upper:g}` {self.upper_label}"
|
|
55
|
+
return ""
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
Family = NewType("Family", str)
|
|
59
|
+
Variant = NewType("Variant", str)
|
|
60
|
+
MetricIdStr = NewType("MetricIdStr", str)
|
|
61
|
+
EMPTY_VARIANT = Variant("")
|
|
62
|
+
EMPTY_FAMILY = Family("")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass(frozen=True)
|
|
66
|
+
class MetricId:
|
|
67
|
+
family: Family
|
|
68
|
+
variant: Variant
|
|
69
|
+
key: str
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def id_str(self) -> MetricIdStr:
|
|
73
|
+
if not self.family and (not self.variant):
|
|
74
|
+
return MetricIdStr(self.key)
|
|
75
|
+
return MetricIdStr("/".join(dict.fromkeys((self.family, self.variant, self.key))))
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass(frozen=True)
|
|
79
|
+
class MetricSpec:
|
|
80
|
+
key: str
|
|
81
|
+
label: str
|
|
82
|
+
description: str
|
|
83
|
+
priority: MetricPriority
|
|
84
|
+
value_range: tuple[float, float] | None
|
|
85
|
+
direction: MetricDirection = MetricDirection.HIGHER_BETTER
|
|
86
|
+
grain: MetricGrain = MetricGrain.TRACE
|
|
87
|
+
anchors: MetricAnchors | None = None
|
|
88
|
+
family: Family = EMPTY_FAMILY
|
|
89
|
+
variant: Variant = EMPTY_VARIANT
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def id(self) -> MetricId:
|
|
93
|
+
return MetricId(family=self.family, variant=self.variant, key=self.key)
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def metric_id(self) -> MetricIdStr:
|
|
97
|
+
return self.id.id_str
|
|
98
|
+
|
|
99
|
+
def bind(self, family: str, *, variant: str) -> MetricSpec:
|
|
100
|
+
if self.family and self.family != family:
|
|
101
|
+
raise ValueError(f"metric {self.key} family {self.family} != {family} conflict")
|
|
102
|
+
if self.variant and self.variant != variant:
|
|
103
|
+
raise ValueError(f"metric {self.key} variant {self.variant} != {variant} conflict")
|
|
104
|
+
return replace(self, family=Family(family), variant=variant)
|
|
105
|
+
|
|
106
|
+
def value_range_finite(self) -> float | None:
|
|
107
|
+
v = self.value_range
|
|
108
|
+
if v and math.isfinite(v[0]) and math.isfinite(v[1]):
|
|
109
|
+
return math.fabs(v[1] - v[0])
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def is_score(self) -> bool:
|
|
114
|
+
return self.priority.is_score
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@dataclass(frozen=True, kw_only=True)
|
|
118
|
+
class ScorerSpec:
|
|
119
|
+
family: Family
|
|
120
|
+
metrics: dict[str, MetricSpec]
|
|
121
|
+
description: str = ""
|
|
122
|
+
variant: Variant = EMPTY_VARIANT
|
|
123
|
+
|
|
124
|
+
def __post_init__(self):
|
|
125
|
+
variant = self.variant or self.family
|
|
126
|
+
bound = {k: m.bind(self.family, variant=variant) for k, m in self.metrics.items()}
|
|
127
|
+
object.__setattr__(self, "variant", variant)
|
|
128
|
+
object.__setattr__(self, "metrics", bound)
|
|
129
|
+
|
|
130
|
+
@property
|
|
131
|
+
def id(self) -> str:
|
|
132
|
+
return "/".join(dict.fromkeys((self.family, self.variant)))
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@runtime_checkable
|
|
136
|
+
class ProvidesMetricValues(Protocol):
|
|
137
|
+
def metric_values(self, variant: str) -> Mapping[str, float | None]: ...
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dpaa.core.tool_representation.canonicalizer import (
|
|
4
|
+
ActionCanonicalizer,
|
|
5
|
+
Canonicalizer,
|
|
6
|
+
ContentHashCanonicalizer,
|
|
7
|
+
ShellTargetCanonicalizer,
|
|
8
|
+
)
|
|
9
|
+
from dpaa.core.tool_representation.context_tracker import (
|
|
10
|
+
ContextTracker,
|
|
11
|
+
EditSetContextTracker,
|
|
12
|
+
FileLifecycle,
|
|
13
|
+
LifecycleContextTracker,
|
|
14
|
+
StepCategory,
|
|
15
|
+
TrajectoryContext,
|
|
16
|
+
record_context,
|
|
17
|
+
)
|
|
18
|
+
from dpaa.core.tool_representation.equivalence import (
|
|
19
|
+
JACCARD_THRESHOLD,
|
|
20
|
+
LINE_OVERLAP_MIN,
|
|
21
|
+
AdaptiveJaccardThreshold,
|
|
22
|
+
EquivalenceEngine,
|
|
23
|
+
EquivalenceMargin,
|
|
24
|
+
EquivalenceResult,
|
|
25
|
+
equivalence_margin,
|
|
26
|
+
)
|
|
27
|
+
from dpaa.core.tool_representation.extractor import ToolCallFeatureExtractor
|
|
28
|
+
from dpaa.core.tool_representation.features import StepFeatures
|
|
29
|
+
from dpaa.core.tool_representation.functional_groups import (
|
|
30
|
+
DEFAULT_TERMINAL_FUNCTIONAL_GROUPS,
|
|
31
|
+
canonical_verb,
|
|
32
|
+
)
|
|
33
|
+
from dpaa.core.tool_representation.intent_labeler import (
|
|
34
|
+
DEFAULT_SEARCH_GROUPS,
|
|
35
|
+
IntentLabeler,
|
|
36
|
+
SplitReadSearchIntentLabeler,
|
|
37
|
+
StateDrivenIntentLabeler,
|
|
38
|
+
ToolIdentityIntentLabeler,
|
|
39
|
+
label_intent,
|
|
40
|
+
)
|
|
41
|
+
from dpaa.core.tool_representation.representation import ToolCallRepresenter, compose_representation
|
|
42
|
+
from dpaa.core.tool_representation.state_assembler import assemble_state
|
|
43
|
+
from dpaa.core.tool_representation.terminal_command_classifier import classify_terminal_command
|
|
44
|
+
from dpaa.core.tool_representation.tools_config import (
|
|
45
|
+
CommandExtractor,
|
|
46
|
+
TargetFilesExtractor,
|
|
47
|
+
TerminalCommandClassifier,
|
|
48
|
+
ToolsConfig,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
__all__ = [
|
|
52
|
+
"DEFAULT_SEARCH_GROUPS",
|
|
53
|
+
"DEFAULT_TERMINAL_FUNCTIONAL_GROUPS",
|
|
54
|
+
"JACCARD_THRESHOLD",
|
|
55
|
+
"LINE_OVERLAP_MIN",
|
|
56
|
+
"AdaptiveJaccardThreshold",
|
|
57
|
+
"ActionCanonicalizer",
|
|
58
|
+
"Canonicalizer",
|
|
59
|
+
"EquivalenceEngine",
|
|
60
|
+
"CommandExtractor",
|
|
61
|
+
"ToolCallFeatureExtractor",
|
|
62
|
+
"ContentHashCanonicalizer",
|
|
63
|
+
"ContextTracker",
|
|
64
|
+
"EditSetContextTracker",
|
|
65
|
+
"EquivalenceResult",
|
|
66
|
+
"FileLifecycle",
|
|
67
|
+
"IntentLabeler",
|
|
68
|
+
"LifecycleContextTracker",
|
|
69
|
+
"EquivalenceMargin",
|
|
70
|
+
"ShellTargetCanonicalizer",
|
|
71
|
+
"SplitReadSearchIntentLabeler",
|
|
72
|
+
"StateDrivenIntentLabeler",
|
|
73
|
+
"StepCategory",
|
|
74
|
+
"StepFeatures",
|
|
75
|
+
"TargetFilesExtractor",
|
|
76
|
+
"TerminalCommandClassifier",
|
|
77
|
+
"ToolCallRepresenter",
|
|
78
|
+
"ToolIdentityIntentLabeler",
|
|
79
|
+
"ToolsConfig",
|
|
80
|
+
"TrajectoryContext",
|
|
81
|
+
"assemble_state",
|
|
82
|
+
"canonical_verb",
|
|
83
|
+
"classify_terminal_command",
|
|
84
|
+
"compose_representation",
|
|
85
|
+
"label_intent",
|
|
86
|
+
"equivalence_margin",
|
|
87
|
+
"record_context",
|
|
88
|
+
]
|