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.
Files changed (47) hide show
  1. dpaa/__init__.py +10 -0
  2. dpaa/analyze.py +27 -0
  3. dpaa/core/__init__.py +1 -0
  4. dpaa/core/llm_usage.py +196 -0
  5. dpaa/core/metadata.py +97 -0
  6. dpaa/core/metric_spec.py +137 -0
  7. dpaa/core/tool_representation/__init__.py +88 -0
  8. dpaa/core/tool_representation/canonicalizer.py +96 -0
  9. dpaa/core/tool_representation/context_tracker.py +117 -0
  10. dpaa/core/tool_representation/equivalence.py +170 -0
  11. dpaa/core/tool_representation/extractor.py +35 -0
  12. dpaa/core/tool_representation/features.py +19 -0
  13. dpaa/core/tool_representation/functional_groups.py +57 -0
  14. dpaa/core/tool_representation/intent_labeler.py +129 -0
  15. dpaa/core/tool_representation/representation.py +72 -0
  16. dpaa/core/tool_representation/state_assembler.py +17 -0
  17. dpaa/core/tool_representation/states.py +32 -0
  18. dpaa/core/tool_representation/terminal_command_classifier.py +158 -0
  19. dpaa/core/tool_representation/tools_config.py +33 -0
  20. dpaa/core/traces.py +183 -0
  21. dpaa/core/traces_scored.py +104 -0
  22. dpaa/distribution_inspection/__init__.py +1 -0
  23. dpaa/distribution_inspection/distribution_builders.py +80 -0
  24. dpaa/metrics/__init__.py +3 -0
  25. dpaa/metrics/consistency/consistency_scorer.py +177 -0
  26. dpaa/metrics/consistency/consistency_scorer_spec.py +116 -0
  27. dpaa/metrics/consistency/implementation/__init__.py +1 -0
  28. dpaa/metrics/consistency/implementation/outcome_consistency.py +105 -0
  29. dpaa/metrics/consistency/implementation/resource_consistency.py +203 -0
  30. dpaa/metrics/consistency/implementation/trajectory_distribution_consistency.py +128 -0
  31. dpaa/metrics/consistency/implementation/trajectory_sequence_consistency.py +115 -0
  32. dpaa/metrics/consistency/outcome.py +47 -0
  33. dpaa/metrics/consistency/overall.py +114 -0
  34. dpaa/metrics/consistency/resource.py +57 -0
  35. dpaa/metrics/consistency/trajectory_distribution.py +63 -0
  36. dpaa/metrics/consistency/trajectory_sequence.py +58 -0
  37. dpaa/metrics/consistency/utils/__init__.py +1 -0
  38. dpaa/metrics/consistency/utils/representation.py +47 -0
  39. dpaa/metrics/consistency/utils/tool_distribution.py +89 -0
  40. dpaa/py.typed +0 -0
  41. dpaa/scorer.py +20 -0
  42. dpaa/utils/__init__.py +1 -0
  43. dpaa/utils/token_counter.py +9 -0
  44. dpaa-0.0.5.dist-info/METADATA +22 -0
  45. dpaa-0.0.5.dist-info/RECORD +47 -0
  46. dpaa-0.0.5.dist-info/WHEEL +4 -0
  47. 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
@@ -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