cc-transcript 4.1.0__tar.gz → 5.0.0__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.
- {cc_transcript-4.1.0 → cc_transcript-5.0.0}/Cargo.lock +1 -1
- {cc_transcript-4.1.0 → cc_transcript-5.0.0}/PKG-INFO +2 -2
- {cc_transcript-4.1.0 → cc_transcript-5.0.0}/cc_transcript/__init__.py +66 -0
- {cc_transcript-4.1.0 → cc_transcript-5.0.0}/cc_transcript/__main__.py +2 -0
- {cc_transcript-4.1.0 → cc_transcript-5.0.0}/cc_transcript/_parser_rs.pyi +4 -1
- {cc_transcript-4.1.0 → cc_transcript-5.0.0}/cc_transcript/corrections_cli.py +2 -3
- cc_transcript-5.0.0/cc_transcript/cost.py +138 -0
- {cc_transcript-4.1.0 → cc_transcript-5.0.0}/cc_transcript/judge/llm.py +20 -27
- {cc_transcript-4.1.0 → cc_transcript-5.0.0}/cc_transcript/judge/verdicts.py +4 -8
- {cc_transcript-4.1.0 → cc_transcript-5.0.0}/cc_transcript/mining/__init__.py +12 -1
- cc_transcript-5.0.0/cc_transcript/mining/formats.py +172 -0
- {cc_transcript-4.1.0 → cc_transcript-5.0.0}/cc_transcript/mining/signals.py +79 -24
- {cc_transcript-4.1.0 → cc_transcript-5.0.0}/cc_transcript/mining/store.py +2 -2
- {cc_transcript-4.1.0 → cc_transcript-5.0.0}/cc_transcript/models.py +190 -0
- {cc_transcript-4.1.0 → cc_transcript-5.0.0}/cc_transcript/parser.py +112 -0
- {cc_transcript-4.1.0 → cc_transcript-5.0.0}/cc_transcript/render.py +2 -2
- {cc_transcript-4.1.0 → cc_transcript-5.0.0}/cc_transcript/sentiment/engine.py +3 -1
- {cc_transcript-4.1.0 → cc_transcript-5.0.0}/cc_transcript/store.py +2 -9
- {cc_transcript-4.1.0 → cc_transcript-5.0.0}/cc_transcript/tools.py +4 -7
- {cc_transcript-4.1.0 → cc_transcript-5.0.0}/pyproject.toml +2 -2
- {cc_transcript-4.1.0 → cc_transcript-5.0.0}/rust/Cargo.toml +1 -1
- {cc_transcript-4.1.0 → cc_transcript-5.0.0}/rust/src/event.rs +195 -3
- {cc_transcript-4.1.0 → cc_transcript-5.0.0}/rust/src/lib.rs +9 -1
- {cc_transcript-4.1.0 → cc_transcript-5.0.0}/rust/src/model.rs +9 -0
- cc_transcript-4.1.0/cc_transcript/mining/formats.py +0 -64
- {cc_transcript-4.1.0 → cc_transcript-5.0.0}/Cargo.toml +0 -0
- {cc_transcript-4.1.0 → cc_transcript-5.0.0}/LICENSE +0 -0
- {cc_transcript-4.1.0 → cc_transcript-5.0.0}/README.md +0 -0
- {cc_transcript-4.1.0 → cc_transcript-5.0.0}/cc_transcript/activity.py +0 -0
- {cc_transcript-4.1.0 → cc_transcript-5.0.0}/cc_transcript/backend.py +0 -0
- {cc_transcript-4.1.0 → cc_transcript-5.0.0}/cc_transcript/builders.py +0 -0
- {cc_transcript-4.1.0 → cc_transcript-5.0.0}/cc_transcript/cli.py +0 -0
- {cc_transcript-4.1.0 → cc_transcript-5.0.0}/cc_transcript/context.py +0 -0
- {cc_transcript-4.1.0 → cc_transcript-5.0.0}/cc_transcript/corrections.py +0 -0
- {cc_transcript-4.1.0 → cc_transcript-5.0.0}/cc_transcript/decisions.py +0 -0
- {cc_transcript-4.1.0 → cc_transcript-5.0.0}/cc_transcript/discovery.py +0 -0
- {cc_transcript-4.1.0 → cc_transcript-5.0.0}/cc_transcript/disktruth.py +0 -0
- {cc_transcript-4.1.0 → cc_transcript-5.0.0}/cc_transcript/evidence.py +0 -0
- {cc_transcript-4.1.0 → cc_transcript-5.0.0}/cc_transcript/extract/__init__.py +0 -0
- {cc_transcript-4.1.0 → cc_transcript-5.0.0}/cc_transcript/extract/correct.py +0 -0
- {cc_transcript-4.1.0 → cc_transcript-5.0.0}/cc_transcript/filterspec.py +0 -0
- {cc_transcript-4.1.0 → cc_transcript-5.0.0}/cc_transcript/ids.py +2 -2
- {cc_transcript-4.1.0 → cc_transcript-5.0.0}/cc_transcript/judge/__init__.py +0 -0
- {cc_transcript-4.1.0 → cc_transcript-5.0.0}/cc_transcript/messages.py +0 -0
- {cc_transcript-4.1.0 → cc_transcript-5.0.0}/cc_transcript/mining/candidates.py +0 -0
- {cc_transcript-4.1.0 → cc_transcript-5.0.0}/cc_transcript/mining/confidence.py +0 -0
- {cc_transcript-4.1.0 → cc_transcript-5.0.0}/cc_transcript/mining/filterspec.py +0 -0
- {cc_transcript-4.1.0 → cc_transcript-5.0.0}/cc_transcript/mining/sourcekind.py +0 -0
- {cc_transcript-4.1.0 → cc_transcript-5.0.0}/cc_transcript/py.typed +0 -0
- {cc_transcript-4.1.0 → cc_transcript-5.0.0}/cc_transcript/query.py +0 -0
- {cc_transcript-4.1.0 → cc_transcript-5.0.0}/cc_transcript/rust.py +0 -0
- {cc_transcript-4.1.0 → cc_transcript-5.0.0}/cc_transcript/sentiment/__init__.py +0 -0
- {cc_transcript-4.1.0 → cc_transcript-5.0.0}/cc_transcript/sentiment/buckets.py +0 -0
- {cc_transcript-4.1.0 → cc_transcript-5.0.0}/cc_transcript/sentiment/lexicon.py +0 -0
- {cc_transcript-4.1.0 → cc_transcript-5.0.0}/cc_transcript/sentiment/scorespec.py +0 -0
- {cc_transcript-4.1.0 → cc_transcript-5.0.0}/rust/data/afinn-en-165.tsv +0 -0
- {cc_transcript-4.1.0 → cc_transcript-5.0.0}/rust/data/domain_overrides.tsv +0 -0
- {cc_transcript-4.1.0 → cc_transcript-5.0.0}/rust/src/filter.rs +0 -0
- {cc_transcript-4.1.0 → cc_transcript-5.0.0}/rust/src/lexicon.rs +0 -0
- {cc_transcript-4.1.0 → cc_transcript-5.0.0}/rust/src/score.rs +0 -0
- {cc_transcript-4.1.0 → cc_transcript-5.0.0}/rust/src/value.rs +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cc-transcript
|
|
3
|
-
Version:
|
|
3
|
+
Version: 5.0.0
|
|
4
4
|
Classifier: Development Status :: 3 - Alpha
|
|
5
5
|
Classifier: Environment :: Console
|
|
6
6
|
Classifier: Intended Audience :: Developers
|
|
@@ -16,7 +16,7 @@ Requires-Dist: orjson>=3.10
|
|
|
16
16
|
Requires-Dist: pytest>=8.0 ; extra == 'dev'
|
|
17
17
|
Requires-Dist: ty>=0.0.44 ; extra == 'dev'
|
|
18
18
|
Requires-Dist: ruff>=0.8 ; extra == 'dev'
|
|
19
|
-
Requires-Dist: spawnllm>=0.
|
|
19
|
+
Requires-Dist: spawnllm>=0.4.0 ; extra == 'llm'
|
|
20
20
|
Requires-Dist: spacy>=3.8 ; extra == 'sentiment'
|
|
21
21
|
Requires-Dist: afinn>=0.1 ; extra == 'sentiment'
|
|
22
22
|
Provides-Extra: dev
|
|
@@ -57,19 +57,28 @@ EXPORTS: dict[str, str] = {
|
|
|
57
57
|
),
|
|
58
58
|
"cc_transcript.models": (
|
|
59
59
|
"AssistantEvent",
|
|
60
|
+
"CacheCreation",
|
|
60
61
|
"CcVersion",
|
|
61
62
|
"ContentBlock",
|
|
62
63
|
"EntryMeta",
|
|
63
64
|
"FallbackBlock",
|
|
65
|
+
"InitInfo",
|
|
66
|
+
"McpServer",
|
|
64
67
|
"ModeEvent",
|
|
68
|
+
"ModelUsage",
|
|
65
69
|
"OtherBlock",
|
|
66
70
|
"OtherEvent",
|
|
71
|
+
"Plugin",
|
|
72
|
+
"PrintMessage",
|
|
73
|
+
"PrintResult",
|
|
74
|
+
"ServerToolUse",
|
|
67
75
|
"SystemEvent",
|
|
68
76
|
"TextBlock",
|
|
69
77
|
"ThinkingBlock",
|
|
70
78
|
"ToolResultBlock",
|
|
71
79
|
"ToolUseBlock",
|
|
72
80
|
"TranscriptEvent",
|
|
81
|
+
"Usage",
|
|
73
82
|
"UserEvent",
|
|
74
83
|
),
|
|
75
84
|
"cc_transcript.parser": (
|
|
@@ -77,6 +86,15 @@ EXPORTS: dict[str, str] = {
|
|
|
77
86
|
"parse_event",
|
|
78
87
|
"parse_events_async",
|
|
79
88
|
"parse_events_from_bytes",
|
|
89
|
+
"parse_print_result",
|
|
90
|
+
),
|
|
91
|
+
"cc_transcript.cost": (
|
|
92
|
+
"PRICING",
|
|
93
|
+
"CostBreakdown",
|
|
94
|
+
"ModelPricing",
|
|
95
|
+
"cost_of",
|
|
96
|
+
"cost_of_assistant",
|
|
97
|
+
"resolve_pricing",
|
|
80
98
|
),
|
|
81
99
|
"cc_transcript.backend": ("Backend", "ParsedTranscript"),
|
|
82
100
|
"cc_transcript.discovery": (
|
|
@@ -271,6 +289,24 @@ if TYPE_CHECKING:
|
|
|
271
289
|
from cc_transcript.corrections import (
|
|
272
290
|
Origin as Origin,
|
|
273
291
|
)
|
|
292
|
+
from cc_transcript.cost import (
|
|
293
|
+
PRICING as PRICING,
|
|
294
|
+
)
|
|
295
|
+
from cc_transcript.cost import (
|
|
296
|
+
CostBreakdown as CostBreakdown,
|
|
297
|
+
)
|
|
298
|
+
from cc_transcript.cost import (
|
|
299
|
+
ModelPricing as ModelPricing,
|
|
300
|
+
)
|
|
301
|
+
from cc_transcript.cost import (
|
|
302
|
+
cost_of as cost_of,
|
|
303
|
+
)
|
|
304
|
+
from cc_transcript.cost import (
|
|
305
|
+
cost_of_assistant as cost_of_assistant,
|
|
306
|
+
)
|
|
307
|
+
from cc_transcript.cost import (
|
|
308
|
+
resolve_pricing as resolve_pricing,
|
|
309
|
+
)
|
|
274
310
|
from cc_transcript.decisions import (
|
|
275
311
|
DECISIONS_DDL as DECISIONS_DDL,
|
|
276
312
|
)
|
|
@@ -415,6 +451,9 @@ if TYPE_CHECKING:
|
|
|
415
451
|
from cc_transcript.models import (
|
|
416
452
|
AssistantEvent as AssistantEvent,
|
|
417
453
|
)
|
|
454
|
+
from cc_transcript.models import (
|
|
455
|
+
CacheCreation as CacheCreation,
|
|
456
|
+
)
|
|
418
457
|
from cc_transcript.models import (
|
|
419
458
|
CcVersion as CcVersion,
|
|
420
459
|
)
|
|
@@ -427,15 +466,36 @@ if TYPE_CHECKING:
|
|
|
427
466
|
from cc_transcript.models import (
|
|
428
467
|
FallbackBlock as FallbackBlock,
|
|
429
468
|
)
|
|
469
|
+
from cc_transcript.models import (
|
|
470
|
+
InitInfo as InitInfo,
|
|
471
|
+
)
|
|
472
|
+
from cc_transcript.models import (
|
|
473
|
+
McpServer as McpServer,
|
|
474
|
+
)
|
|
430
475
|
from cc_transcript.models import (
|
|
431
476
|
ModeEvent as ModeEvent,
|
|
432
477
|
)
|
|
478
|
+
from cc_transcript.models import (
|
|
479
|
+
ModelUsage as ModelUsage,
|
|
480
|
+
)
|
|
433
481
|
from cc_transcript.models import (
|
|
434
482
|
OtherBlock as OtherBlock,
|
|
435
483
|
)
|
|
436
484
|
from cc_transcript.models import (
|
|
437
485
|
OtherEvent as OtherEvent,
|
|
438
486
|
)
|
|
487
|
+
from cc_transcript.models import (
|
|
488
|
+
Plugin as Plugin,
|
|
489
|
+
)
|
|
490
|
+
from cc_transcript.models import (
|
|
491
|
+
PrintMessage as PrintMessage,
|
|
492
|
+
)
|
|
493
|
+
from cc_transcript.models import (
|
|
494
|
+
PrintResult as PrintResult,
|
|
495
|
+
)
|
|
496
|
+
from cc_transcript.models import (
|
|
497
|
+
ServerToolUse as ServerToolUse,
|
|
498
|
+
)
|
|
439
499
|
from cc_transcript.models import (
|
|
440
500
|
SystemEvent as SystemEvent,
|
|
441
501
|
)
|
|
@@ -454,6 +514,9 @@ if TYPE_CHECKING:
|
|
|
454
514
|
from cc_transcript.models import (
|
|
455
515
|
TranscriptEvent as TranscriptEvent,
|
|
456
516
|
)
|
|
517
|
+
from cc_transcript.models import (
|
|
518
|
+
Usage as Usage,
|
|
519
|
+
)
|
|
457
520
|
from cc_transcript.models import (
|
|
458
521
|
UserEvent as UserEvent,
|
|
459
522
|
)
|
|
@@ -469,6 +532,9 @@ if TYPE_CHECKING:
|
|
|
469
532
|
from cc_transcript.parser import (
|
|
470
533
|
parse_events_from_bytes as parse_events_from_bytes,
|
|
471
534
|
)
|
|
535
|
+
from cc_transcript.parser import (
|
|
536
|
+
parse_print_result as parse_print_result,
|
|
537
|
+
)
|
|
472
538
|
from cc_transcript.query import (
|
|
473
539
|
FileRef as FileRef,
|
|
474
540
|
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from cc_transcript.models import TranscriptEvent
|
|
3
|
+
from cc_transcript.models import PrintResult, TranscriptEvent
|
|
4
4
|
|
|
5
5
|
class ParseStream:
|
|
6
6
|
"""A streaming handle over a rayon-parsed batch of transcript files."""
|
|
@@ -18,6 +18,9 @@ def stream_parse(paths: list[tuple[str, float]], prefetch: int, spec_json: str |
|
|
|
18
18
|
are dropped during parsing, before any Python object is built.
|
|
19
19
|
"""
|
|
20
20
|
|
|
21
|
+
def parse_print_result(raw: bytes, /) -> PrintResult:
|
|
22
|
+
"""Parses a 'claude -p --output-format json' result from raw JSON bytes."""
|
|
23
|
+
|
|
21
24
|
def lexicon_available() -> bool:
|
|
22
25
|
"""Whether the UDPipe English model loaded (downloading + caching on first call)."""
|
|
23
26
|
|
|
@@ -11,7 +11,6 @@ from __future__ import annotations
|
|
|
11
11
|
import json
|
|
12
12
|
import time
|
|
13
13
|
from dataclasses import asdict
|
|
14
|
-
from typing import cast
|
|
15
14
|
|
|
16
15
|
import click
|
|
17
16
|
import orjson
|
|
@@ -54,7 +53,7 @@ def add(
|
|
|
54
53
|
anchor: str,
|
|
55
54
|
incorrect_file: str,
|
|
56
55
|
ts_ms: int | None,
|
|
57
|
-
origin:
|
|
56
|
+
origin: Origin | None,
|
|
58
57
|
incorrect_old: str,
|
|
59
58
|
incorrect_new: str,
|
|
60
59
|
incorrect_digest: str | None,
|
|
@@ -79,7 +78,7 @@ def add(
|
|
|
79
78
|
incorrect_file=incorrect_file,
|
|
80
79
|
incorrect_old=incorrect_old,
|
|
81
80
|
incorrect_new=incorrect_new,
|
|
82
|
-
correction_origin=
|
|
81
|
+
correction_origin=origin,
|
|
83
82
|
correction_file=correction_file,
|
|
84
83
|
correction_old=correction_old,
|
|
85
84
|
correction_new=correction_new,
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from collections.abc import Mapping
|
|
8
|
+
|
|
9
|
+
from cc_transcript.models import AssistantEvent, Usage
|
|
10
|
+
|
|
11
|
+
MTOK = 1_000_000
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True, slots=True)
|
|
15
|
+
class ModelPricing:
|
|
16
|
+
"""USD-per-million-token rates for a model family.
|
|
17
|
+
|
|
18
|
+
Standard service tier, standard (non-long-context) pricing — current Claude
|
|
19
|
+
4.x models serve their 1M context at standard rates with no long-context
|
|
20
|
+
premium. Override via the `pricing` argument to cost_of for other tiers.
|
|
21
|
+
|
|
22
|
+
Attributes:
|
|
23
|
+
input: USD per million uncached input tokens.
|
|
24
|
+
output: USD per million output tokens.
|
|
25
|
+
cache_read: USD per million tokens served from the cache.
|
|
26
|
+
cache_write_5m: USD per million tokens written to the 5-minute cache.
|
|
27
|
+
cache_write_1h: USD per million tokens written to the 1-hour cache.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
input: float
|
|
31
|
+
output: float
|
|
32
|
+
cache_read: float
|
|
33
|
+
cache_write_5m: float
|
|
34
|
+
cache_write_1h: float
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True, slots=True)
|
|
38
|
+
class CostBreakdown:
|
|
39
|
+
"""The per-component and total USD cost of a single turn's token usage.
|
|
40
|
+
|
|
41
|
+
Attributes:
|
|
42
|
+
input_cost: USD cost of the uncached input tokens.
|
|
43
|
+
output_cost: USD cost of the output tokens.
|
|
44
|
+
cache_read_cost: USD cost of the cache-read tokens.
|
|
45
|
+
cache_write_cost: USD cost of the cache-creation tokens, summed across TTL buckets.
|
|
46
|
+
total: The sum of the four component costs.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
input_cost: float
|
|
50
|
+
output_cost: float
|
|
51
|
+
cache_read_cost: float
|
|
52
|
+
cache_write_cost: float
|
|
53
|
+
total: float
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
PRICING: Mapping[str, ModelPricing] = {
|
|
57
|
+
"fable": ModelPricing(input=10.0, output=50.0, cache_read=1.0, cache_write_5m=12.5, cache_write_1h=20.0),
|
|
58
|
+
"opus": ModelPricing(input=5.0, output=25.0, cache_read=0.5, cache_write_5m=6.25, cache_write_1h=10.0),
|
|
59
|
+
"sonnet": ModelPricing(input=3.0, output=15.0, cache_read=0.3, cache_write_5m=3.75, cache_write_1h=6.0),
|
|
60
|
+
"haiku": ModelPricing(input=1.0, output=5.0, cache_read=0.1, cache_write_5m=1.25, cache_write_1h=2.0),
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def resolve_pricing(model: str, *, pricing: Mapping[str, ModelPricing] = PRICING) -> ModelPricing:
|
|
65
|
+
"""Return the pricing row whose family key is a substring of `model`.
|
|
66
|
+
|
|
67
|
+
Family keys are mutually exclusive substrings, so at most one matches —
|
|
68
|
+
``"claude-haiku-4-5-20251001"`` resolves to haiku, ``"claude-opus-4-8"`` to
|
|
69
|
+
opus, and a bare ``"sonnet"`` to sonnet.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
model: The model id to resolve.
|
|
73
|
+
pricing: The family-keyed pricing table to search.
|
|
74
|
+
|
|
75
|
+
Raises:
|
|
76
|
+
KeyError: If no family key is a substring of `model`.
|
|
77
|
+
|
|
78
|
+
Example:
|
|
79
|
+
>>> resolve_pricing("claude-opus-4-8").input
|
|
80
|
+
5.0
|
|
81
|
+
"""
|
|
82
|
+
for family, row in pricing.items():
|
|
83
|
+
if family in model:
|
|
84
|
+
return row
|
|
85
|
+
raise KeyError(f"no pricing for model: {model}")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def cost_of(usage: Usage, model: str, *, pricing: Mapping[str, ModelPricing] = PRICING) -> CostBreakdown:
|
|
89
|
+
"""Compute the USD cost of a turn's token usage under a model's rates.
|
|
90
|
+
|
|
91
|
+
The cache-creation split prefers the per-TTL ``usage.cache_creation`` when
|
|
92
|
+
present, falling back to the flat ``usage.cache_creation_input_tokens`` as
|
|
93
|
+
5-minute writes with no 1-hour share.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
usage: The turn's token usage and cache accounting.
|
|
97
|
+
model: The model id whose rates apply.
|
|
98
|
+
pricing: The family-keyed pricing table to resolve against.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
The per-component and total cost.
|
|
102
|
+
|
|
103
|
+
Example:
|
|
104
|
+
>>> cost_of(usage, "claude-haiku-4-5-20251001").total
|
|
105
|
+
0.05759
|
|
106
|
+
"""
|
|
107
|
+
row = resolve_pricing(model, pricing=pricing)
|
|
108
|
+
write_5m, write_1h = (
|
|
109
|
+
(usage.cache_creation.ephemeral_5m_input_tokens, usage.cache_creation.ephemeral_1h_input_tokens)
|
|
110
|
+
if usage.cache_creation is not None
|
|
111
|
+
else (usage.cache_creation_input_tokens, 0)
|
|
112
|
+
)
|
|
113
|
+
input_cost = usage.input_tokens / MTOK * row.input
|
|
114
|
+
output_cost = usage.output_tokens / MTOK * row.output
|
|
115
|
+
cache_read_cost = usage.cache_read_input_tokens / MTOK * row.cache_read
|
|
116
|
+
cache_write_cost = write_5m / MTOK * row.cache_write_5m + write_1h / MTOK * row.cache_write_1h
|
|
117
|
+
return CostBreakdown(
|
|
118
|
+
input_cost=input_cost,
|
|
119
|
+
output_cost=output_cost,
|
|
120
|
+
cache_read_cost=cache_read_cost,
|
|
121
|
+
cache_write_cost=cache_write_cost,
|
|
122
|
+
total=input_cost + output_cost + cache_read_cost + cache_write_cost,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def cost_of_assistant(event: AssistantEvent, *, pricing: Mapping[str, ModelPricing] = PRICING) -> CostBreakdown | None:
|
|
127
|
+
"""Compute the cost of an assistant turn, or None when it carries no usage.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
event: The assistant turn whose usage is priced.
|
|
131
|
+
pricing: The family-keyed pricing table to resolve against.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
The cost breakdown, or None when ``event.usage`` is None.
|
|
135
|
+
"""
|
|
136
|
+
if event.usage is None:
|
|
137
|
+
return None
|
|
138
|
+
return cost_of(event.usage, event.model, pricing=pricing)
|
|
@@ -1,22 +1,18 @@
|
|
|
1
1
|
"""Headless structured completions via spawnllm's first ready CLI backend.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
the spawn
|
|
5
|
-
spawnllm's CLIs is installed and authenticated
|
|
6
|
-
using the caller's existing CLI auth — no API
|
|
7
|
-
``spawnllm`` and ``pydantic`` load lazily inside each
|
|
8
|
-
judge package needs no extra installed.
|
|
3
|
+
Run dispatch and envelope parsing come from the shared ``spawnllm`` library;
|
|
4
|
+
:func:`spawnllm.run` drives the spawn and built-in transient retry. The backend
|
|
5
|
+
is whichever of spawnllm's CLIs is installed and authenticated
|
|
6
|
+
(:func:`spawnllm.select_backend`), using the caller's existing CLI auth — no API
|
|
7
|
+
key, no pinned provider. ``spawnllm`` and ``pydantic`` load lazily inside each
|
|
8
|
+
function, so importing the judge package needs no extra installed.
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
11
|
from __future__ import annotations
|
|
12
12
|
|
|
13
|
-
import os
|
|
14
|
-
import subprocess
|
|
15
13
|
from functools import cache
|
|
16
14
|
from typing import TYPE_CHECKING, cast
|
|
17
15
|
|
|
18
|
-
import anyio
|
|
19
|
-
|
|
20
16
|
if TYPE_CHECKING:
|
|
21
17
|
from collections.abc import Awaitable, Callable
|
|
22
18
|
|
|
@@ -55,27 +51,24 @@ async def run_structured_on[M: BaseModel](
|
|
|
55
51
|
|
|
56
52
|
A caller that resolves a backend once per pass passes it here, so a large pass
|
|
57
53
|
probes auth once; :func:`run_structured` is the select-and-run convenience.
|
|
54
|
+
:func:`spawnllm.run` retries transient failures with backoff before returning.
|
|
58
55
|
|
|
59
56
|
Raises:
|
|
60
|
-
subprocess.
|
|
57
|
+
subprocess.TimeoutExpired: If the backend CLI outlives ``timeout``.
|
|
61
58
|
pydantic.ValidationError: If the response does not match the schema.
|
|
62
59
|
"""
|
|
63
|
-
from spawnllm import
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
60
|
+
from spawnllm import RunSpec, run
|
|
61
|
+
|
|
62
|
+
rr = await run(
|
|
63
|
+
RunSpec(
|
|
64
|
+
prompt=prompt,
|
|
65
|
+
model=backend.models[tier],
|
|
66
|
+
schema=backend.schema_for(response_model),
|
|
67
|
+
timeout=timeout,
|
|
68
|
+
),
|
|
69
|
+
backend=backend,
|
|
70
70
|
)
|
|
71
|
-
|
|
72
|
-
with anyio.fail_after(timeout):
|
|
73
|
-
result = await anyio.run_process(
|
|
74
|
-
argv, input=stdin.encode() if stdin is not None else None, check=True, env=os.environ | backend.env()
|
|
75
|
-
)
|
|
76
|
-
except TimeoutError as exc:
|
|
77
|
-
raise subprocess.TimeoutExpired(argv, timeout) from exc
|
|
78
|
-
return cast(response_model, backend.parse_response(result.stdout.decode(), response_model))
|
|
71
|
+
return cast(M, backend.parse_response(rr.stdout, response_model))
|
|
79
72
|
|
|
80
73
|
|
|
81
74
|
async def run_structured[M: BaseModel](
|
|
@@ -88,7 +81,7 @@ async def run_structured[M: BaseModel](
|
|
|
88
81
|
|
|
89
82
|
Raises:
|
|
90
83
|
spawnllm.BackendUnavailable: If no backend is installed and authenticated.
|
|
91
|
-
subprocess.
|
|
84
|
+
subprocess.TimeoutExpired: If the backend CLI outlives ``timeout``.
|
|
92
85
|
pydantic.ValidationError: If the response does not match the schema.
|
|
93
86
|
"""
|
|
94
87
|
return await run_structured_on(default_backend(), prompt, response_model=response_model, tier=tier, timeout=timeout)
|
|
@@ -421,7 +421,7 @@ class FlipReport:
|
|
|
421
421
|
|
|
422
422
|
async def run_verdicts[V](
|
|
423
423
|
rows: Sequence[Mapping[str, object]],
|
|
424
|
-
prompt_for: Callable[[Mapping[str, object]],
|
|
424
|
+
prompt_for: Callable[[Mapping[str, object]], Awaitable[str]],
|
|
425
425
|
judge: Callable[[str], Awaitable[V]],
|
|
426
426
|
persist: Callable[[Mapping[str, object], V], Awaitable[None]],
|
|
427
427
|
*,
|
|
@@ -436,8 +436,8 @@ async def run_verdicts[V](
|
|
|
436
436
|
|
|
437
437
|
Args:
|
|
438
438
|
rows: The rows to judge.
|
|
439
|
-
prompt_for: Builds one row's prompt;
|
|
440
|
-
|
|
439
|
+
prompt_for: Builds one row's prompt; async, so prompts may hydrate their
|
|
440
|
+
context window first.
|
|
441
441
|
judge: Turns one prompt into a verdict payload, e.g. ``structured_judge(...)``.
|
|
442
442
|
persist: Persists one row's verdict, e.g. ``store.record_verdict`` applied.
|
|
443
443
|
concurrency: The maximum number of concurrent judge calls.
|
|
@@ -451,11 +451,7 @@ async def run_verdicts[V](
|
|
|
451
451
|
async def worker(row: Mapping[str, object]) -> None:
|
|
452
452
|
async with limiter:
|
|
453
453
|
try:
|
|
454
|
-
|
|
455
|
-
case str() as prompt:
|
|
456
|
-
verdict = await judge(prompt)
|
|
457
|
-
case awaitable:
|
|
458
|
-
verdict = await judge(await awaitable)
|
|
454
|
+
verdict = await judge(await prompt_for(row))
|
|
459
455
|
except Exception:
|
|
460
456
|
counts["failed"] += 1
|
|
461
457
|
return
|
|
@@ -44,15 +44,25 @@ from cc_transcript.mining.filterspec import (
|
|
|
44
44
|
keep_candidate,
|
|
45
45
|
only_kinds,
|
|
46
46
|
)
|
|
47
|
-
from cc_transcript.mining.formats import
|
|
47
|
+
from cc_transcript.mining.formats import (
|
|
48
|
+
ReviewComment,
|
|
49
|
+
ReviewFormat,
|
|
50
|
+
StructuredFormat,
|
|
51
|
+
extract_all,
|
|
52
|
+
extract_structured,
|
|
53
|
+
)
|
|
48
54
|
from cc_transcript.mining.signals import (
|
|
49
55
|
DEFAULT_DETECTORS,
|
|
50
56
|
DENIAL_PREFIX,
|
|
51
57
|
EDIT_TOOLS,
|
|
52
58
|
REENTRY_LOOKBACK,
|
|
59
|
+
SUBAGENT_TOOLS,
|
|
53
60
|
USER_SAID_MARKER,
|
|
54
61
|
USER_SAID_TRAILER,
|
|
55
62
|
MiningSignal,
|
|
63
|
+
Provenance,
|
|
64
|
+
ScanText,
|
|
65
|
+
classify_provenance,
|
|
56
66
|
correction_text,
|
|
57
67
|
denial_results,
|
|
58
68
|
denied_tool_payload,
|
|
@@ -69,6 +79,7 @@ from cc_transcript.mining.signals import (
|
|
|
69
79
|
marker_in,
|
|
70
80
|
nearest_assistant_index,
|
|
71
81
|
next_user_message,
|
|
82
|
+
review_scan_texts,
|
|
72
83
|
)
|
|
73
84
|
from cc_transcript.mining.sourcekind import (
|
|
74
85
|
INTERRUPT_REJECTION,
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""Generic infrastructure for parsing structured code-review messages.
|
|
2
|
+
|
|
3
|
+
The concrete review formats are app policy; an app injects its own
|
|
4
|
+
:class:`ReviewFormat` sequence into :func:`extract_all`.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from typing import TYPE_CHECKING
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
import re
|
|
15
|
+
from collections.abc import Callable, Iterator, Mapping, Sequence
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
FINDING_KEYS: tuple[str, ...] = ("findings", "bugs", "improvements", "issues", "items", "verdicts")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True, slots=True)
|
|
22
|
+
class ReviewComment:
|
|
23
|
+
"""A single inline review comment parsed from a code-review message.
|
|
24
|
+
|
|
25
|
+
Attributes:
|
|
26
|
+
file: The file the comment targets, when cited.
|
|
27
|
+
line_start: The first line the comment targets, when cited.
|
|
28
|
+
line_end: The last line the comment targets, when a range is cited.
|
|
29
|
+
comment: The comment's text.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
file: str | None
|
|
33
|
+
line_start: int | None
|
|
34
|
+
line_end: int | None
|
|
35
|
+
comment: str
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True, slots=True)
|
|
39
|
+
class ReviewFormat:
|
|
40
|
+
"""A named code-review text format with a detector and extractor.
|
|
41
|
+
|
|
42
|
+
Attributes:
|
|
43
|
+
name: The format's identifier.
|
|
44
|
+
pattern: A pattern that matches when the format is present in a text.
|
|
45
|
+
extract: Parses a matching text into its review comments.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
name: str
|
|
49
|
+
pattern: re.Pattern[str]
|
|
50
|
+
extract: Callable[[str], tuple[ReviewComment, ...]]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def extract_all(text: str, formats: Sequence[ReviewFormat]) -> Iterator[tuple[ReviewFormat, ReviewComment]]:
|
|
54
|
+
"""Yields every ``(format, comment)`` extracted by any matching format.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
text: The raw review message text.
|
|
58
|
+
formats: The review formats to try, in order.
|
|
59
|
+
|
|
60
|
+
Yields:
|
|
61
|
+
One pair per extracted comment, across all formats whose pattern matches.
|
|
62
|
+
"""
|
|
63
|
+
return (
|
|
64
|
+
(fmt, comment)
|
|
65
|
+
for fmt in formats
|
|
66
|
+
if fmt.pattern.search(text)
|
|
67
|
+
for comment in fmt.extract(text)
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass(frozen=True, slots=True)
|
|
72
|
+
class StructuredFormat:
|
|
73
|
+
"""A named JSON code-review format keyed by caller-supplied field aliases.
|
|
74
|
+
|
|
75
|
+
The aliases let an app map its own finding schema onto :class:`ReviewComment`
|
|
76
|
+
without hardcoding key names here. Each alias tuple is tried in order against
|
|
77
|
+
every finding object.
|
|
78
|
+
|
|
79
|
+
Attributes:
|
|
80
|
+
name: The format's identifier.
|
|
81
|
+
file_keys: Aliases for the cited file path.
|
|
82
|
+
line_keys: Aliases for the cited line (int, ``"96"``, or ``"24-51"``).
|
|
83
|
+
comment_keys: Aliases for the comment text.
|
|
84
|
+
fix_keys: Aliases for a suggested fix appended to the comment, when present.
|
|
85
|
+
finding_keys: Extra aliases for the finding array, beyond the defaults.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
name: str
|
|
89
|
+
file_keys: tuple[str, ...] = ("file", "path", "file_path")
|
|
90
|
+
line_keys: tuple[str, ...] = ("line", "line_start", "lines")
|
|
91
|
+
comment_keys: tuple[str, ...] = ("comment", "message", "text", "description")
|
|
92
|
+
fix_keys: tuple[str, ...] = field(default=())
|
|
93
|
+
finding_keys: tuple[str, ...] = field(default=())
|
|
94
|
+
|
|
95
|
+
def extract(self, payload: Any) -> tuple[ReviewComment, ...]:
|
|
96
|
+
return tuple(
|
|
97
|
+
review_comment(obj, self.file_keys, self.line_keys, self.comment_keys, self.fix_keys)
|
|
98
|
+
for obj in findings(payload, FINDING_KEYS + self.finding_keys)
|
|
99
|
+
if isinstance(obj, dict)
|
|
100
|
+
if first(obj, self.comment_keys) is not None
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def first(obj: Mapping[str, Any], keys: tuple[str, ...]) -> Any:
|
|
105
|
+
return next((obj[key] for key in keys if key in obj and obj[key] is not None), None)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def line_bounds(value: Any) -> tuple[int | None, int | None]:
|
|
109
|
+
match value:
|
|
110
|
+
case int():
|
|
111
|
+
return value, value
|
|
112
|
+
case str() if "-" in value:
|
|
113
|
+
start, _, end = value.partition("-")
|
|
114
|
+
return int(start.strip()), int(end.strip())
|
|
115
|
+
case str() if value.strip().isdigit():
|
|
116
|
+
return (n := int(value.strip())), n
|
|
117
|
+
case _:
|
|
118
|
+
return None, None
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def review_comment(
|
|
122
|
+
obj: Mapping[str, Any],
|
|
123
|
+
file_keys: tuple[str, ...],
|
|
124
|
+
line_keys: tuple[str, ...],
|
|
125
|
+
comment_keys: tuple[str, ...],
|
|
126
|
+
fix_keys: tuple[str, ...] = (),
|
|
127
|
+
) -> ReviewComment:
|
|
128
|
+
start, end = line_bounds(first(obj, line_keys))
|
|
129
|
+
return ReviewComment(
|
|
130
|
+
file=(file := first(obj, file_keys)) and str(file),
|
|
131
|
+
line_start=start,
|
|
132
|
+
line_end=end,
|
|
133
|
+
comment=" ".join(str(part) for part in (first(obj, comment_keys), first(obj, fix_keys)) if part is not None),
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def findings(payload: Any, keys: tuple[str, ...]) -> Iterator[Any]:
|
|
138
|
+
match payload:
|
|
139
|
+
case list():
|
|
140
|
+
yield from payload
|
|
141
|
+
case dict():
|
|
142
|
+
if (nested := first(payload, keys)) is not None and isinstance(nested, list):
|
|
143
|
+
yield from nested
|
|
144
|
+
elif isinstance(result := payload.get("result"), dict):
|
|
145
|
+
yield from findings(result, keys)
|
|
146
|
+
else:
|
|
147
|
+
yield from (
|
|
148
|
+
item
|
|
149
|
+
for key, value in payload.items()
|
|
150
|
+
if key.startswith("confirmed")
|
|
151
|
+
if isinstance(value, list)
|
|
152
|
+
for item in value
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def extract_structured(
|
|
157
|
+
text: str, structured_formats: Sequence[StructuredFormat]
|
|
158
|
+
) -> Iterator[tuple[StructuredFormat, ReviewComment]]:
|
|
159
|
+
"""Yields every ``(format, comment)`` JSON-parsed from a finding payload.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
text: The raw message text, expected to be a JSON document.
|
|
163
|
+
structured_formats: The structured formats to apply, in order.
|
|
164
|
+
|
|
165
|
+
Yields:
|
|
166
|
+
One pair per finding, across all formats. Non-JSON text yields nothing.
|
|
167
|
+
"""
|
|
168
|
+
try:
|
|
169
|
+
payload = json.loads(text)
|
|
170
|
+
except (ValueError, TypeError):
|
|
171
|
+
return
|
|
172
|
+
yield from ((fmt, comment) for fmt in structured_formats for comment in fmt.extract(payload))
|