rtsa 0.1.0__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 (53) hide show
  1. rtsa/__init__.py +26 -0
  2. rtsa/__main__.py +5 -0
  3. rtsa/analysis/__init__.py +59 -0
  4. rtsa/analysis/benchmark.py +358 -0
  5. rtsa/analysis/fingerprint.py +325 -0
  6. rtsa/analysis/performance_correlation.py +382 -0
  7. rtsa/analysis/prune.py +657 -0
  8. rtsa/analysis/signal_adapters.py +146 -0
  9. rtsa/analysis/step_classifier.py +274 -0
  10. rtsa/analysis/step_clustering.py +193 -0
  11. rtsa/cli.py +434 -0
  12. rtsa/config/__init__.py +34 -0
  13. rtsa/core/__init__.py +31 -0
  14. rtsa/core/metrics.py +525 -0
  15. rtsa/core/motif_matcher.py +586 -0
  16. rtsa/core/ngs_validator.py +628 -0
  17. rtsa/core/robust_tsi.py +488 -0
  18. rtsa/core/types.py +443 -0
  19. rtsa/demo.py +99 -0
  20. rtsa/experiments/__init__.py +1 -0
  21. rtsa/experiments/annotate_steps.py +105 -0
  22. rtsa/experiments/calibrate_thresholds.py +331 -0
  23. rtsa/experiments/correlation_analysis.py +207 -0
  24. rtsa/experiments/data_analysis.py +242 -0
  25. rtsa/experiments/end_to_end_prune.py +391 -0
  26. rtsa/experiments/pilot.py +354 -0
  27. rtsa/experiments/run.py +456 -0
  28. rtsa/experiments/synthetic_redundant_cots.py +221 -0
  29. rtsa/extractors/__init__.py +30 -0
  30. rtsa/extractors/agreement.py +634 -0
  31. rtsa/extractors/analysis.py +139 -0
  32. rtsa/extractors/baselines.py +221 -0
  33. rtsa/extractors/experiments.py +264 -0
  34. rtsa/extractors/gcp_validator.py +498 -0
  35. rtsa/extractors/inter_annotator.py +134 -0
  36. rtsa/extractors/llm_extractor.py +380 -0
  37. rtsa/extractors/random_baseline.py +121 -0
  38. rtsa/extractors/rule_based.py +578 -0
  39. rtsa/extractors/syntax_based.py +448 -0
  40. rtsa/extractors/synthetic_validation.py +198 -0
  41. rtsa/utils/__init__.py +15 -0
  42. rtsa/utils/data_loader.py +287 -0
  43. rtsa/utils/gsm8k_loader.py +151 -0
  44. rtsa/utils/hf_adapter.py +260 -0
  45. rtsa/utils/math_loader.py +257 -0
  46. rtsa/utils/trace_exporters.py +197 -0
  47. rtsa/utils/visualize.py +95 -0
  48. rtsa-0.1.0.dist-info/METADATA +220 -0
  49. rtsa-0.1.0.dist-info/RECORD +53 -0
  50. rtsa-0.1.0.dist-info/WHEEL +5 -0
  51. rtsa-0.1.0.dist-info/entry_points.txt +2 -0
  52. rtsa-0.1.0.dist-info/licenses/LICENSE +21 -0
  53. rtsa-0.1.0.dist-info/top_level.txt +1 -0
rtsa/__init__.py ADDED
@@ -0,0 +1,26 @@
1
+ """
2
+ RTSA: Reasoning Trace Structure Analysis Toolkit.
3
+
4
+ Quick start:
5
+ from rtsa.extractors import create_extractor_deepseek
6
+ ext = create_extractor_deepseek()
7
+ graph = ext.extract("Calculate 5+3=8. Check the result.")
8
+ print(graph.nodes)
9
+ """
10
+
11
+ __version__ = "0.1.0"
12
+
13
+ # Convenience re-exports when running from the project directory
14
+ from rtsa.core.types import ReasoningTraceGraph, GraphNode, NodeType, MotifEntry
15
+ from rtsa.core.metrics import compute_graph_features, compute_tsi, compute_pairwise_tsi
16
+ from rtsa.core.motif_matcher import MotifMatcher
17
+ from rtsa.core.ngs_validator import NGSValidator
18
+ from rtsa.core.robust_tsi import UnsupervisedTSI
19
+ from rtsa.extractors import create_extractor_deepseek, RuleBasedExtractor, SyntaxBasedExtractor
20
+
21
+ __all__ = [
22
+ "ReasoningTraceGraph", "GraphNode", "NodeType", "MotifEntry",
23
+ "compute_graph_features", "compute_tsi", "compute_pairwise_tsi",
24
+ "MotifMatcher", "NGSValidator", "UnsupervisedTSI",
25
+ "create_extractor_deepseek", "RuleBasedExtractor", "SyntaxBasedExtractor",
26
+ ]
rtsa/__main__.py ADDED
@@ -0,0 +1,5 @@
1
+ """Allow `python -m rtsa` to invoke the CLI."""
2
+ from rtsa.cli import main
3
+
4
+ if __name__ == "__main__":
5
+ main()
@@ -0,0 +1,59 @@
1
+ """RTSA analysis layer — value-added modules built on core graph primitives.
2
+
3
+ Provides:
4
+ - prune : redundancy detection & CoT optimization
5
+ - fingerprint: LLM authorship attribution via graph structure
6
+ - benchmark : extractor reliability benchmarking (GCP + NGS + TSI)
7
+ """
8
+ from rtsa.extractors.analysis import (
9
+ CostEstimator, CostBreakdown, estimate_project_cost,
10
+ kruskal_wallis_test, bootstrap_ci, cohens_d, partial_correlation,
11
+ )
12
+
13
+ from .prune import (
14
+ PruneConfig,
15
+ RedundancyRegion,
16
+ PruningReport,
17
+ RedundancyAnalyzer,
18
+ prune_graph,
19
+ )
20
+
21
+ from .fingerprint import (
22
+ ModelSignature,
23
+ FingerprintMatchResult,
24
+ ModelFingerprint,
25
+ enroll_model,
26
+ identify_author,
27
+ )
28
+
29
+ from .benchmark import (
30
+ ExtractorScore,
31
+ ExtractorBenchmarkResult,
32
+ BenchmarkReport,
33
+ ExtractorBenchmark,
34
+ benchmark_extractors,
35
+ )
36
+
37
+ __all__ = [
38
+ # Re-exports from rtsa.extractors.analysis (legacy)
39
+ "CostEstimator", "CostBreakdown", "estimate_project_cost",
40
+ "kruskal_wallis_test", "bootstrap_ci", "cohens_d", "partial_correlation",
41
+ # Prune module
42
+ "PruneConfig",
43
+ "RedundancyRegion",
44
+ "PruningReport",
45
+ "RedundancyAnalyzer",
46
+ "prune_graph",
47
+ # Fingerprint module
48
+ "ModelSignature",
49
+ "FingerprintMatchResult",
50
+ "ModelFingerprint",
51
+ "enroll_model",
52
+ "identify_author",
53
+ # Benchmark module
54
+ "ExtractorScore",
55
+ "ExtractorBenchmarkResult",
56
+ "BenchmarkReport",
57
+ "ExtractorBenchmark",
58
+ "benchmark_extractors",
59
+ ]
@@ -0,0 +1,358 @@
1
+ """
2
+ Extractor Reliability Benchmark — Direction A (unique differentiator).
3
+
4
+ Provides a unified benchmarking suite that calibrates and compares
5
+ reasoning-trace extractors across three dimensions:
6
+
7
+ 1. Granularity Calibration Protocol (GCP) — precision at sentence level
8
+ 2. NGS Structural Compliance — validity of produced graphs
9
+ 3. Cross-Extractor Consistency — Topological Similarity Index (TSI)
10
+
11
+ This is the feature ReasoningFlow will never build (they don't evaluate
12
+ themselves), making it the ideal flagship command for RTSA-as-library.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ import logging
19
+ from dataclasses import dataclass, field
20
+ from typing import Callable, Dict, List, Optional, Tuple
21
+
22
+ import numpy as np
23
+
24
+ from rtsa.core.types import NodeType, ReasoningTraceGraph
25
+ from rtsa.core.metrics import compute_tsi
26
+ from rtsa.core.ngs_validator import NGSValidator, NGSViolation
27
+ from rtsa.extractors.gcp_validator import GCPValidator, GCPResult, make_gcp_adapter
28
+ from rtsa.extractors.inter_annotator import InterAnnotatorAgreement
29
+
30
+ logger = logging.getLogger(__name__)
31
+
32
+ # ---------------------------------------------------------------------------
33
+ # Data structures
34
+ # ---------------------------------------------------------------------------
35
+
36
+ @dataclass
37
+ class ExtractorScore:
38
+ """Scores for a single extractor on a single benchmark dimension."""
39
+ dimension: str
40
+ score: float
41
+ passed: bool
42
+ details: Dict = field(default_factory=dict)
43
+
44
+
45
+ @dataclass
46
+ class ExtractorBenchmarkResult:
47
+ """Complete benchmark result for one extractor."""
48
+ extractor_name: str
49
+ n_samples: int
50
+ gcp: Optional[GCPResult] = None
51
+ ngs_pass_rate: float = 0.0
52
+ ngs_violations: List[NGSViolation] = field(default_factory=list)
53
+ mean_tsi_vs_others: float = 0.0
54
+ overall_score: float = 0.0
55
+ passed: bool = False
56
+ plan_redundancy_score: float = 0.0 # reasonplan insight: wasteful planning structure
57
+
58
+ def summary(self) -> str:
59
+ lines = [
60
+ f"BenchmarkResult: {self.extractor_name}",
61
+ f" Samples: {self.n_samples}",
62
+ ]
63
+ if self.gcp:
64
+ lines.append(
65
+ f" GCP: mean={self.gcp.mean_gcs:.3f} min={self.gcp.min_gcs:.3f} "
66
+ f"passed={self.gcp.passed}"
67
+ )
68
+ lines.append(f" NGS pass rate: {self.ngs_pass_rate:.1%}")
69
+ lines.append(f" Mean TSI vs peers: {self.mean_tsi_vs_others:.3f}")
70
+ lines.append(f" Overall score: {self.overall_score:.3f} passed={self.passed}")
71
+ if self.plan_redundancy_score > 0.05:
72
+ lines.append(
73
+ f" Plan redundancy: {self.plan_redundancy_score:.3f} "
74
+ f"(reasonplan insight: excessive planning structure hurts efficiency)"
75
+ )
76
+ return "\n".join(lines)
77
+
78
+
79
+ @dataclass
80
+ class BenchmarkReport:
81
+ """Aggregate report across all extractors."""
82
+ results: Dict[str, ExtractorBenchmarkResult]
83
+ winner: Optional[str] = None
84
+ ranking: List[Tuple[str, float]] = field(default_factory=list)
85
+
86
+ def to_json(self, path: str) -> None:
87
+ serializable = {
88
+ "winner": self.winner,
89
+ "ranking": self.ranking,
90
+ "extractors": {
91
+ name: {
92
+ "n_samples": r.n_samples,
93
+ "gcp_mean": r.gcp.mean_gcs if r.gcp else None,
94
+ "gcp_passed": r.gcp.passed if r.gcp else None,
95
+ "ngs_pass_rate": r.ngs_pass_rate,
96
+ "mean_tsi": r.mean_tsi_vs_others,
97
+ "overall_score": r.overall_score,
98
+ "passed": r.passed,
99
+ }
100
+ for name, r in self.results.items()
101
+ },
102
+ }
103
+ with open(path, "w", encoding="utf-8") as f:
104
+ json.dump(serializable, f, indent=2, ensure_ascii=False)
105
+ logger.info(f"Benchmark report saved to {path}")
106
+
107
+ def summary(self) -> str:
108
+ lines = [
109
+ "=" * 60,
110
+ "RTSA Extractor Reliability Benchmark Report",
111
+ "=" * 60,
112
+ ]
113
+ for rank, (name, score) in enumerate(self.ranking, 1):
114
+ marker = " ★" if name == self.winner else ""
115
+ lines.append(f"#{rank} {name}: {score:.3f}{marker}")
116
+ lines.append("=" * 60)
117
+ return "\n".join(lines)
118
+
119
+
120
+ # ---------------------------------------------------------------------------
121
+ # Benchmark engine
122
+ # ---------------------------------------------------------------------------
123
+
124
+ class ExtractorBenchmark:
125
+ """Run the full RTSA extractor reliability benchmark.
126
+
127
+ Usage:
128
+ bench = ExtractorBenchmark()
129
+ report = bench.run({
130
+ "rbe": rule_based_extractor,
131
+ "sbe": syntax_based_extractor,
132
+ "llm": llm_extractor,
133
+ }, graphs={...})
134
+ print(report.summary())
135
+ """
136
+
137
+ def __init__(
138
+ self,
139
+ gcp_validator: Optional[GCPValidator] = None,
140
+ ngs_validator: Optional[NGSValidator] = None,
141
+ ):
142
+ self.gcp = gcp_validator or GCPValidator()
143
+ self.ngs = ngs_validator or NGSValidator(strict=True)
144
+
145
+ # ------------------------------------------------------------------
146
+ # Main entry point
147
+ # ------------------------------------------------------------------
148
+
149
+ def run(
150
+ self,
151
+ extractors: Dict[str, Callable[[str], ReasoningTraceGraph]],
152
+ sample_texts: Optional[List[str]] = None,
153
+ graphs: Optional[Dict[str, List[ReasoningTraceGraph]]] = None,
154
+ run_gcp: bool = True,
155
+ run_ngs: bool = True,
156
+ run_tsi: bool = True,
157
+ ) -> BenchmarkReport:
158
+ """Run benchmark suite.
159
+
160
+ Args:
161
+ extractors: mapping name → extractor function
162
+ sample_texts: texts for GCP calibration (sentence-level)
163
+ graphs: pre-extracted graphs per extractor for NGS/TSI
164
+ run_gcp: whether to run Granularity Calibration Protocol
165
+ run_ngs: whether to run NGS structural validation
166
+ run_tsi: whether to compute cross-extractor TSI
167
+
168
+ Returns:
169
+ BenchmarkReport with aggregated scores and ranking
170
+ """
171
+ results: Dict[str, ExtractorBenchmarkResult] = {}
172
+
173
+ for name, extractor in extractors.items():
174
+ logger.info(f"Benchmarking extractor: {name}")
175
+ result = ExtractorBenchmarkResult(extractor_name=name, n_samples=0)
176
+
177
+ # --- GCP (sentence-level calibration) ---
178
+ if run_gcp and sample_texts:
179
+ # Wrap extractor for GCP (sentence → List[NodeType])
180
+ def sentence_adapter(sentence: str) -> List[NodeType]:
181
+ # Try to extract a mini-graph from a single sentence
182
+ # and return its node-type sequence
183
+ try:
184
+ mini_graph = extractor(sentence)
185
+ return [n.type for n in sorted(mini_graph.nodes, key=lambda n: n.id)]
186
+ except Exception:
187
+ return []
188
+
189
+ gcp_result = self.gcp.calibrate_extractor(
190
+ make_gcp_adapter(sentence_adapter),
191
+ extractor_name=name,
192
+ )
193
+ result.gcp = gcp_result
194
+
195
+ # --- NGS (graph-level structural compliance) ---
196
+ if run_ngs and graphs and name in graphs:
197
+ extractor_graphs = graphs[name]
198
+ result.n_samples = len(extractor_graphs)
199
+ valid_count = 0
200
+ all_violations: List[NGSViolation] = []
201
+ for g in extractor_graphs:
202
+ is_valid, violations = self.ngs.validate(g)
203
+ if is_valid:
204
+ valid_count += 1
205
+ all_violations.extend(violations)
206
+ result.ngs_pass_rate = valid_count / max(len(extractor_graphs), 1)
207
+ result.ngs_violations = all_violations
208
+ result.plan_redundancy_score = self._compute_plan_redundancy(extractor_graphs)
209
+
210
+ results[name] = result
211
+
212
+ # --- TSI (cross-extractor consistency) ---
213
+ if run_tsi and graphs:
214
+ self._compute_tsi_matrix(results, graphs)
215
+
216
+ # --- Aggregate scoring ---
217
+ self._compute_overall_scores(results)
218
+
219
+ # --- Ranking ---
220
+ ranking = sorted(
221
+ ((name, r.overall_score) for name, r in results.items()),
222
+ key=lambda x: x[1],
223
+ reverse=True,
224
+ )
225
+ winner = ranking[0][0] if ranking else None
226
+
227
+ report = BenchmarkReport(results=results, winner=winner, ranking=ranking)
228
+ logger.info(report.summary())
229
+ return report
230
+
231
+ # ------------------------------------------------------------------
232
+ # TSI computation
233
+ # ------------------------------------------------------------------
234
+
235
+ def _compute_tsi_matrix(
236
+ self,
237
+ results: Dict[str, ExtractorBenchmarkResult],
238
+ graphs: Dict[str, List[ReasoningTraceGraph]],
239
+ ) -> None:
240
+ """Compute pairwise mean TSI between extractors."""
241
+ names = list(results.keys())
242
+ if len(names) < 2:
243
+ return
244
+
245
+ # Build per-extractor graph lists (must be same-length for fair comparison)
246
+ lens = [len(graphs[n]) for n in names if n in graphs]
247
+ if not lens:
248
+ return
249
+ min_len = min(lens)
250
+ if min_len == 0:
251
+ return
252
+
253
+ for name in names:
254
+ if name not in graphs:
255
+ continue
256
+ my_graphs = graphs[name][:min_len]
257
+ tsi_scores: List[float] = []
258
+ for other in names:
259
+ if other == name or other not in graphs:
260
+ continue
261
+ other_graphs = graphs[other][:min_len]
262
+ for g1, g2 in zip(my_graphs, other_graphs):
263
+ try:
264
+ tsi = compute_tsi(g1.to_networkx(), g2.to_networkx())
265
+ tsi_scores.append(tsi.tsi_value)
266
+ except Exception:
267
+ pass
268
+ if tsi_scores:
269
+ results[name].mean_tsi_vs_others = float(np.mean(tsi_scores))
270
+
271
+ # ------------------------------------------------------------------
272
+ # Plan-redundancy insight (reasonplan negative result)
273
+ # ------------------------------------------------------------------
274
+
275
+ @staticmethod
276
+ def _compute_plan_redundancy(graphs: List[ReasoningTraceGraph]) -> float:
277
+ """Measure planning-structure bloat (Branch/Backtrack without productive children).
278
+
279
+ Inspired by reasonplan negative results: excessive planning overhead
280
+ in reasoning traces degrades effective output quality.
281
+ """
282
+ if not graphs:
283
+ return 0.0
284
+ scores = []
285
+ for g in graphs:
286
+ n = len(g.nodes)
287
+ if n == 0:
288
+ continue
289
+ plan_nodes = [node for node in g.nodes if node.type.value in ("Branch", "Backtrack")]
290
+ if not plan_nodes:
291
+ scores.append(0.0)
292
+ continue
293
+ wasteful = 0
294
+ for node in plan_nodes:
295
+ children = [e[1] for e in g.edges if e[0] == node.id]
296
+ child_types = {c.type.value for c in g.nodes if c.id in children}
297
+ if not child_types.intersection({"Transform", "Retrieve", "Compare"}):
298
+ wasteful += 1
299
+ scores.append(wasteful / n)
300
+ return float(np.mean(scores))
301
+
302
+ # ------------------------------------------------------------------
303
+ # Overall scoring
304
+ # ------------------------------------------------------------------
305
+
306
+ @staticmethod
307
+ def _compute_overall_scores(
308
+ results: Dict[str, ExtractorBenchmarkResult],
309
+ ) -> None:
310
+ """Aggregate per-extractor scores into a single 0-1 score.
311
+
312
+ Weights:
313
+ GCP passed & mean → 40%
314
+ NGS pass rate → 35%
315
+ TSI consistency → 25%
316
+ """
317
+ for r in results.values():
318
+ scores = []
319
+ weights = []
320
+
321
+ if r.gcp is not None:
322
+ gcp_score = r.gcp.mean_gcs if r.gcp.passed else r.gcp.mean_gcs * 0.5
323
+ scores.append(gcp_score)
324
+ weights.append(0.40)
325
+
326
+ if r.n_samples > 0:
327
+ scores.append(r.ngs_pass_rate)
328
+ weights.append(0.35)
329
+
330
+ if r.mean_tsi_vs_others > 0:
331
+ scores.append(r.mean_tsi_vs_others)
332
+ weights.append(0.25)
333
+
334
+ if scores:
335
+ r.overall_score = float(np.average(scores, weights=weights))
336
+ else:
337
+ r.overall_score = 0.0
338
+
339
+ # Pass threshold: overall >= 0.70 AND NGS pass rate >= 0.60
340
+ r.passed = r.overall_score >= 0.70 and r.ngs_pass_rate >= 0.60
341
+
342
+
343
+ # ---------------------------------------------------------------------------
344
+ # Convenience one-liner
345
+ # ---------------------------------------------------------------------------
346
+
347
+ def benchmark_extractors(
348
+ extractors: Dict[str, Callable[[str], ReasoningTraceGraph]],
349
+ sample_texts: Optional[List[str]] = None,
350
+ graphs: Optional[Dict[str, List[ReasoningTraceGraph]]] = None,
351
+ output_path: Optional[str] = None,
352
+ ) -> BenchmarkReport:
353
+ """Run full benchmark and optionally save report to JSON."""
354
+ bench = ExtractorBenchmark()
355
+ report = bench.run(extractors, sample_texts=sample_texts, graphs=graphs)
356
+ if output_path:
357
+ report.to_json(output_path)
358
+ return report