dpaa 0.0.5__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.
- dpaa/__init__.py +10 -0
- dpaa/analyze.py +27 -0
- dpaa/core/__init__.py +1 -0
- dpaa/core/llm_usage.py +196 -0
- dpaa/core/metadata.py +97 -0
- dpaa/core/metric_spec.py +137 -0
- dpaa/core/tool_representation/__init__.py +88 -0
- dpaa/core/tool_representation/canonicalizer.py +96 -0
- dpaa/core/tool_representation/context_tracker.py +117 -0
- dpaa/core/tool_representation/equivalence.py +170 -0
- dpaa/core/tool_representation/extractor.py +35 -0
- dpaa/core/tool_representation/features.py +19 -0
- dpaa/core/tool_representation/functional_groups.py +57 -0
- dpaa/core/tool_representation/intent_labeler.py +129 -0
- dpaa/core/tool_representation/representation.py +72 -0
- dpaa/core/tool_representation/state_assembler.py +17 -0
- dpaa/core/tool_representation/states.py +32 -0
- dpaa/core/tool_representation/terminal_command_classifier.py +158 -0
- dpaa/core/tool_representation/tools_config.py +33 -0
- dpaa/core/traces.py +183 -0
- dpaa/core/traces_scored.py +104 -0
- dpaa/distribution_inspection/__init__.py +1 -0
- dpaa/distribution_inspection/distribution_builders.py +80 -0
- dpaa/metrics/__init__.py +3 -0
- dpaa/metrics/consistency/consistency_scorer.py +177 -0
- dpaa/metrics/consistency/consistency_scorer_spec.py +116 -0
- dpaa/metrics/consistency/implementation/__init__.py +1 -0
- dpaa/metrics/consistency/implementation/outcome_consistency.py +105 -0
- dpaa/metrics/consistency/implementation/resource_consistency.py +203 -0
- dpaa/metrics/consistency/implementation/trajectory_distribution_consistency.py +128 -0
- dpaa/metrics/consistency/implementation/trajectory_sequence_consistency.py +115 -0
- dpaa/metrics/consistency/outcome.py +47 -0
- dpaa/metrics/consistency/overall.py +114 -0
- dpaa/metrics/consistency/resource.py +57 -0
- dpaa/metrics/consistency/trajectory_distribution.py +63 -0
- dpaa/metrics/consistency/trajectory_sequence.py +58 -0
- dpaa/metrics/consistency/utils/__init__.py +1 -0
- dpaa/metrics/consistency/utils/representation.py +47 -0
- dpaa/metrics/consistency/utils/tool_distribution.py +89 -0
- dpaa/py.typed +0 -0
- dpaa/scorer.py +20 -0
- dpaa/utils/__init__.py +1 -0
- dpaa/utils/token_counter.py +9 -0
- dpaa-0.0.5.dist-info/METADATA +22 -0
- dpaa-0.0.5.dist-info/RECORD +47 -0
- dpaa-0.0.5.dist-info/WHEEL +4 -0
- dpaa-0.0.5.dist-info/licenses/LICENSE +21 -0
dpaa/__init__.py
ADDED
|
@@ -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
|
dpaa/analyze.py
ADDED
|
@@ -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)
|
dpaa/core/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
dpaa/core/llm_usage.py
ADDED
|
@@ -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
|
dpaa/core/metadata.py
ADDED
|
@@ -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
|
dpaa/core/metric_spec.py
ADDED
|
@@ -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
|
+
]
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
from dataclasses import dataclass, replace
|
|
6
|
+
from typing import Protocol
|
|
7
|
+
|
|
8
|
+
from dpaa.core.tool_representation.features import StepFeatures
|
|
9
|
+
from dpaa.core.tool_representation.functional_groups import canonical_verb
|
|
10
|
+
from dpaa.core.tool_representation.terminal_command_classifier import (
|
|
11
|
+
_first_exe,
|
|
12
|
+
_strip_command_prefix,
|
|
13
|
+
)
|
|
14
|
+
from dpaa.core.tool_representation.tools_config import ToolsConfig
|
|
15
|
+
|
|
16
|
+
_FILE_OPERAND_RE = re.compile(
|
|
17
|
+
"\\.(?:py|kt|java|ts|tsx|js|jsx|rb|go|rs|cpp|cc|c|h|hpp|cs|sh|md|rst|txt|json|ya?ml|toml|cfg|ini|xml|html|css|sql)$",
|
|
18
|
+
re.IGNORECASE,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _normalize_operand(token: str) -> str:
|
|
23
|
+
token = token.rstrip("/")
|
|
24
|
+
if not token:
|
|
25
|
+
return ""
|
|
26
|
+
return os.path.abspath(os.path.normpath(token))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _shell_target_files(command: str) -> list[str] | None:
|
|
30
|
+
tokens = _strip_command_prefix(command).split()
|
|
31
|
+
targets: list[str] = []
|
|
32
|
+
seen: set[str] = set()
|
|
33
|
+
for tok in tokens[1:]:
|
|
34
|
+
if tok.startswith("-") or not _FILE_OPERAND_RE.search(tok):
|
|
35
|
+
continue
|
|
36
|
+
name = _normalize_operand(tok)
|
|
37
|
+
if name and name not in seen:
|
|
38
|
+
seen.add(name)
|
|
39
|
+
targets.append(name)
|
|
40
|
+
return targets or None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _canonical_action(
|
|
44
|
+
tool_name: str, command: str, target_files: list[str] | None, tool_aliases: dict[str, str]
|
|
45
|
+
) -> str:
|
|
46
|
+
if command:
|
|
47
|
+
verb = canonical_verb(_first_exe(command))
|
|
48
|
+
operands = _shell_target_files(command) or target_files or []
|
|
49
|
+
else:
|
|
50
|
+
verb = tool_aliases.get(tool_name, tool_name)
|
|
51
|
+
operands = target_files or []
|
|
52
|
+
normalized = sorted({_normalize_operand(o) for o in operands})
|
|
53
|
+
return verb + "|" + " ".join(normalized)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class Canonicalizer(Protocol):
|
|
57
|
+
def canonicalize(self, features: StepFeatures) -> StepFeatures: ...
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass(frozen=True)
|
|
61
|
+
class ContentHashCanonicalizer(Canonicalizer):
|
|
62
|
+
tool_registry: ToolsConfig
|
|
63
|
+
|
|
64
|
+
def canonicalize(self, features: StepFeatures) -> StepFeatures:
|
|
65
|
+
stage_token = str(self.tool_registry.tool_to_stage.get(features.tool_name, ""))
|
|
66
|
+
return features.to_canonical(stage_token + features.raw_input + features.raw_output)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass(frozen=True)
|
|
70
|
+
class ActionCanonicalizer(Canonicalizer):
|
|
71
|
+
tool_aliases: dict[str, str]
|
|
72
|
+
|
|
73
|
+
def canonicalize(self, features: StepFeatures) -> StepFeatures:
|
|
74
|
+
return features.to_canonical(
|
|
75
|
+
_canonical_action(
|
|
76
|
+
features.tool_name, features.command, features.target_files, self.tool_aliases
|
|
77
|
+
)
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass(frozen=True)
|
|
82
|
+
class ShellTargetCanonicalizer(Canonicalizer):
|
|
83
|
+
base: Canonicalizer
|
|
84
|
+
execute_tools: frozenset[str]
|
|
85
|
+
|
|
86
|
+
def canonicalize(self, features: StepFeatures) -> StepFeatures:
|
|
87
|
+
canonical = self.base.canonicalize(features)
|
|
88
|
+
if (
|
|
89
|
+
features.tool_name in self.execute_tools
|
|
90
|
+
and (not canonical.target_files)
|
|
91
|
+
and features.command
|
|
92
|
+
):
|
|
93
|
+
targets = _shell_target_files(features.command)
|
|
94
|
+
if targets is not None:
|
|
95
|
+
return replace(canonical, target_files=targets)
|
|
96
|
+
return canonical
|