dagc 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.
- dagc/__init__.py +47 -0
- dagc/adapters.py +93 -0
- dagc/baselines.py +82 -0
- dagc/cli.py +112 -0
- dagc/compressor.py +968 -0
- dagc/config.py +119 -0
- dagc/extraction.py +662 -0
- dagc/formats.py +206 -0
- dagc/graph.py +263 -0
- dagc/schema.py +194 -0
- dagc/utils.py +250 -0
- dagc-0.1.0.dist-info/METADATA +225 -0
- dagc-0.1.0.dist-info/RECORD +29 -0
- dagc-0.1.0.dist-info/WHEEL +5 -0
- dagc-0.1.0.dist-info/entry_points.txt +3 -0
- dagc-0.1.0.dist-info/licenses/LICENSE +21 -0
- dagc-0.1.0.dist-info/top_level.txt +2 -0
- dagc_eval/__init__.py +42 -0
- dagc_eval/adversarial.py +214 -0
- dagc_eval/benchmark.py +375 -0
- dagc_eval/diagnostics.py +139 -0
- dagc_eval/export.py +80 -0
- dagc_eval/interfaces.py +73 -0
- dagc_eval/leaderboard.py +77 -0
- dagc_eval/match.py +238 -0
- dagc_eval/normalize.py +23 -0
- dagc_eval/reproduce.py +318 -0
- dagc_eval/server.py +201 -0
- dagc_eval/stats.py +93 -0
dagc/__init__.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""
|
|
2
|
+
dagc — Decision-Anchored Graph Compression.
|
|
3
|
+
|
|
4
|
+
Compress long agent/chat message histories while guaranteeing that every
|
|
5
|
+
artifact a decision depends on (tool-call arguments, confirmed IDs, cited
|
|
6
|
+
metrics) survives compression. Zero required dependencies, zero LLM calls.
|
|
7
|
+
|
|
8
|
+
from dagc import compress
|
|
9
|
+
compressed = compress(messages, target_reduction=0.85)
|
|
10
|
+
response = client.chat.completions.create(model="gpt-4", messages=compressed)
|
|
11
|
+
|
|
12
|
+
Bring your own tokenizer/embedder for production-grade quality:
|
|
13
|
+
|
|
14
|
+
import dagc
|
|
15
|
+
from dagc.adapters import TiktokenTokenizer, SentenceTransformerEmbedder
|
|
16
|
+
dagc.configure(
|
|
17
|
+
tokenizer=TiktokenTokenizer("cl100k_base"),
|
|
18
|
+
embedder=SentenceTransformerEmbedder("all-MiniLM-L6-v2"),
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
"""
|
|
22
|
+
from .compressor import compress, compress_any, compress_dagc, DAGCConfig, DAGC_CFG
|
|
23
|
+
from .extraction import extract_decisions
|
|
24
|
+
from .graph import (
|
|
25
|
+
build_dependency_graph, attach_dependencies, compute_rci, compute_chain_rci,
|
|
26
|
+
CausalMessageGraph, CausalGraphConfig, SpectralCompressor,
|
|
27
|
+
)
|
|
28
|
+
from .config import configure, Runtime, Tokenizer, Embedder, runtime
|
|
29
|
+
from .formats import (
|
|
30
|
+
normalize_message, normalize_trace,
|
|
31
|
+
denormalize_message, denormalize_trace,
|
|
32
|
+
register_adapter as register_format_adapter,
|
|
33
|
+
)
|
|
34
|
+
from .schema import register_adapter, to_dagc_format
|
|
35
|
+
|
|
36
|
+
__version__ = "0.1.0"
|
|
37
|
+
|
|
38
|
+
__all__ = [
|
|
39
|
+
"compress", "compress_any", "compress_dagc", "DAGCConfig", "DAGC_CFG",
|
|
40
|
+
"extract_decisions",
|
|
41
|
+
"build_dependency_graph", "attach_dependencies", "compute_rci", "compute_chain_rci",
|
|
42
|
+
"CausalMessageGraph", "CausalGraphConfig", "SpectralCompressor",
|
|
43
|
+
"configure", "Runtime", "Tokenizer", "Embedder", "runtime",
|
|
44
|
+
"normalize_message", "normalize_trace",
|
|
45
|
+
"denormalize_message", "denormalize_trace",
|
|
46
|
+
"register_adapter", "register_format_adapter", "to_dagc_format",
|
|
47
|
+
]
|
dagc/adapters.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Optional, real-quality Tokenizer/Embedder adapters. Each is lazily
|
|
3
|
+
imported so that importing `dagc.adapters` never requires having every
|
|
4
|
+
vendor SDK installed -- only the one you actually instantiate.
|
|
5
|
+
|
|
6
|
+
Install what you need:
|
|
7
|
+
pip install dagc[tiktoken]
|
|
8
|
+
pip install dagc[sentence-transformers]
|
|
9
|
+
pip install dagc[openai]
|
|
10
|
+
|
|
11
|
+
Or write your own -- any object with .encode()/.decode() (Tokenizer) or
|
|
12
|
+
.encode() -> np.ndarray (Embedder) works, see dagc.config.Tokenizer /
|
|
13
|
+
dagc.config.Embedder for the exact protocol.
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
from typing import List
|
|
17
|
+
import numpy as np
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TiktokenTokenizer:
|
|
21
|
+
"""Real BPE token counts via OpenAI's tiktoken. pip install tiktoken."""
|
|
22
|
+
def __init__(self, encoding_name: str = "cl100k_base"):
|
|
23
|
+
try:
|
|
24
|
+
import tiktoken
|
|
25
|
+
except ImportError as e:
|
|
26
|
+
raise ImportError(
|
|
27
|
+
"TiktokenTokenizer needs tiktoken. Install it with "
|
|
28
|
+
"pip install -e '.[tiktoken]'"
|
|
29
|
+
) from e
|
|
30
|
+
self._enc = tiktoken.get_encoding(encoding_name)
|
|
31
|
+
|
|
32
|
+
def encode(self, text: str) -> List[int]:
|
|
33
|
+
return self._enc.encode(str(text))
|
|
34
|
+
|
|
35
|
+
def decode(self, tokens: List[int]) -> str:
|
|
36
|
+
return self._enc.decode(tokens)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class SentenceTransformerEmbedder:
|
|
40
|
+
"""Local, free, good-quality embeddings. pip install sentence-transformers."""
|
|
41
|
+
def __init__(self, model_name: str = "all-MiniLM-L6-v2"):
|
|
42
|
+
try:
|
|
43
|
+
from sentence_transformers import SentenceTransformer
|
|
44
|
+
except ImportError as e:
|
|
45
|
+
raise ImportError(
|
|
46
|
+
"SentenceTransformerEmbedder needs sentence-transformers. "
|
|
47
|
+
"Install it with pip install -e '.[sentence-transformers]'"
|
|
48
|
+
) from e
|
|
49
|
+
self._model = SentenceTransformer(model_name)
|
|
50
|
+
|
|
51
|
+
def encode(self, texts: List[str], normalize_embeddings: bool = False) -> np.ndarray:
|
|
52
|
+
return np.asarray(
|
|
53
|
+
self._model.encode(list(texts), normalize_embeddings=normalize_embeddings)
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class OpenAIEmbedder:
|
|
58
|
+
"""
|
|
59
|
+
BYOK wrapper around any OpenAI-compatible embeddings endpoint.
|
|
60
|
+
Pass your own already-configured client (openai.OpenAI(...)) --
|
|
61
|
+
dagc never sees or stores your API key.
|
|
62
|
+
"""
|
|
63
|
+
def __init__(self, client, model: str = "text-embedding-3-small"):
|
|
64
|
+
self._client = client
|
|
65
|
+
self._model = model
|
|
66
|
+
|
|
67
|
+
def encode(self, texts: List[str], normalize_embeddings: bool = False) -> np.ndarray:
|
|
68
|
+
resp = self._client.embeddings.create(model=self._model, input=list(texts))
|
|
69
|
+
vecs = np.array([d.embedding for d in resp.data], dtype=np.float32)
|
|
70
|
+
if normalize_embeddings:
|
|
71
|
+
norms = np.linalg.norm(vecs, axis=1, keepdims=True)
|
|
72
|
+
norms[norms == 0] = 1.0
|
|
73
|
+
vecs = vecs / norms
|
|
74
|
+
return vecs
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class CohereEmbedder:
|
|
78
|
+
"""BYOK wrapper around a Cohere client (cohere.Client(...))."""
|
|
79
|
+
def __init__(self, client, model: str = "embed-english-v3.0",
|
|
80
|
+
input_type: str = "search_document"):
|
|
81
|
+
self._client = client
|
|
82
|
+
self._model = model
|
|
83
|
+
self._input_type = input_type
|
|
84
|
+
|
|
85
|
+
def encode(self, texts: List[str], normalize_embeddings: bool = False) -> np.ndarray:
|
|
86
|
+
resp = self._client.embed(texts=list(texts), model=self._model,
|
|
87
|
+
input_type=self._input_type)
|
|
88
|
+
vecs = np.array(resp.embeddings, dtype=np.float32)
|
|
89
|
+
if normalize_embeddings:
|
|
90
|
+
norms = np.linalg.norm(vecs, axis=1, keepdims=True)
|
|
91
|
+
norms[norms == 0] = 1.0
|
|
92
|
+
vecs = vecs / norms
|
|
93
|
+
return vecs
|
dagc/baselines.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Optional baseline compressors for comparison against compress_dagc().
|
|
3
|
+
Not imported by dagc/__init__.py automatically -- pull in what you need:
|
|
4
|
+
|
|
5
|
+
from dagc.baselines import compress_tail_truncation, compress_random_drop
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
import random
|
|
9
|
+
from typing import Dict, List
|
|
10
|
+
|
|
11
|
+
from .utils import _get_text, _split_sents, _tok
|
|
12
|
+
from .compressor import DAGCConfig, DAGC_CFG
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def compress_tail_truncation(messages: List[Dict], cfg: DAGCConfig = DAGC_CFG, **kw) -> List[Dict]:
|
|
16
|
+
"""Naive baseline: keep system prompt + as many trailing messages as fit."""
|
|
17
|
+
orig_toks = sum(_tok(_get_text(m)) for m in messages)
|
|
18
|
+
budget = max(1, int(orig_toks * (1 - cfg.TARGET_REDUCTION)))
|
|
19
|
+
sys_msgs = [(i, m) for i, m in enumerate(messages) if m.get('role') == 'system']
|
|
20
|
+
rest = [(i, m) for i, m in enumerate(messages) if m.get('role') != 'system']
|
|
21
|
+
kept, used = {}, 0
|
|
22
|
+
for i, m in sys_msgs:
|
|
23
|
+
mc = dict(m)
|
|
24
|
+
toks = _tok(m.get('content', ''))
|
|
25
|
+
if toks > cfg.SYSTEM_MAX_TOKENS:
|
|
26
|
+
mc['content'] = m.get('content', '')[: cfg.SYSTEM_MAX_TOKENS * 4] + '[…]'
|
|
27
|
+
mc['_orig_idx'] = i
|
|
28
|
+
kept[i] = mc
|
|
29
|
+
used += _tok(mc.get('content', ''))
|
|
30
|
+
for i, m in reversed(rest):
|
|
31
|
+
t = _tok(_get_text(m))
|
|
32
|
+
if used + t > budget and kept:
|
|
33
|
+
continue
|
|
34
|
+
kept[i] = dict(m, _orig_idx=i)
|
|
35
|
+
used += t
|
|
36
|
+
return [kept[i] for i in sorted(kept)]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def compress_random_drop(messages: List[Dict], cfg: DAGCConfig = DAGC_CFG, seed: int = 0, **kw) -> List[Dict]:
|
|
40
|
+
"""Naive baseline: randomly keep sentences until the budget is spent."""
|
|
41
|
+
rng = random.Random(seed)
|
|
42
|
+
orig_toks = sum(_tok(_get_text(m)) for m in messages)
|
|
43
|
+
budget = max(1, int(orig_toks * (1 - cfg.TARGET_REDUCTION)))
|
|
44
|
+
n = len(messages)
|
|
45
|
+
protected = {i for i, m in enumerate(messages)
|
|
46
|
+
if m.get('role') == 'system' or i >= n - cfg.KEEP_LAST_K}
|
|
47
|
+
protected_toks = sum(_tok(_get_text(messages[i])) for i in protected)
|
|
48
|
+
remaining = max(0, budget - protected_toks)
|
|
49
|
+
pool = [(s, i) for i, m in enumerate(messages) if i not in protected
|
|
50
|
+
for s in _split_sents(_get_text(m), cfg.MIN_SENT_TOKENS)]
|
|
51
|
+
rng.shuffle(pool)
|
|
52
|
+
chosen, used = [], 0
|
|
53
|
+
for s, i in pool:
|
|
54
|
+
t = _tok(s)
|
|
55
|
+
if used + t <= remaining:
|
|
56
|
+
chosen.append((s, i))
|
|
57
|
+
used += t
|
|
58
|
+
msg_sents: Dict[int, List[str]] = {}
|
|
59
|
+
for s, i in chosen:
|
|
60
|
+
msg_sents.setdefault(i, []).append(s)
|
|
61
|
+
out = []
|
|
62
|
+
for i, m in enumerate(messages):
|
|
63
|
+
if i in protected:
|
|
64
|
+
out.append(dict(m, _orig_idx=i))
|
|
65
|
+
elif i in msg_sents:
|
|
66
|
+
mc = dict(m)
|
|
67
|
+
mc['content'] = ' '.join(msg_sents[i])
|
|
68
|
+
mc['_orig_idx'] = i
|
|
69
|
+
out.append(mc)
|
|
70
|
+
return out
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def compress_identity(messages: List[Dict], **kw) -> List[Dict]:
|
|
74
|
+
"""No-op baseline: returns the trace unchanged (for measuring the ceiling)."""
|
|
75
|
+
return [dict(m, _orig_idx=i) for i, m in enumerate(messages)]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
BASELINES = {
|
|
79
|
+
'tail_truncation': compress_tail_truncation,
|
|
80
|
+
'random_drop': compress_random_drop,
|
|
81
|
+
'identity': compress_identity,
|
|
82
|
+
}
|
dagc/cli.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""
|
|
2
|
+
dagc CLI: dagc compress | benchmark | compare | evaluate | stats
|
|
3
|
+
|
|
4
|
+
Wraps the existing Python API -- no logic duplicated here, just argument
|
|
5
|
+
parsing and dispatch, so behavior always matches the library functions.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
import argparse
|
|
9
|
+
import json
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _load_trace(path: str):
|
|
14
|
+
try:
|
|
15
|
+
with open(path) as f:
|
|
16
|
+
return json.load(f)
|
|
17
|
+
except FileNotFoundError as exc:
|
|
18
|
+
raise SystemExit(f"Trace file not found: {path}\nProvide a JSON trace file, for example: dagc evaluate your_trace.json") from exc
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def cmd_compress(args):
|
|
22
|
+
import dagc
|
|
23
|
+
messages = _load_trace(args.input)
|
|
24
|
+
result = dagc.compress(messages, target_reduction=args.target_reduction)
|
|
25
|
+
out = json.dumps(result, indent=2, default=str)
|
|
26
|
+
if args.output:
|
|
27
|
+
with open(args.output, 'w') as f:
|
|
28
|
+
f.write(out)
|
|
29
|
+
print(f"Wrote compressed trace to {args.output}")
|
|
30
|
+
else:
|
|
31
|
+
print(out)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def cmd_evaluate(args):
|
|
35
|
+
from dagc_eval import compute_drr
|
|
36
|
+
messages = _load_trace(args.input)
|
|
37
|
+
result = compute_drr(messages, verbose=not args.quiet,
|
|
38
|
+
decision_roles=tuple(args.decision_roles.split(',')))
|
|
39
|
+
print(f"\nDRR_soft={result['DRR_soft']} DRR_binary={result['DRR_binary']} "
|
|
40
|
+
f"RCI={result['RCI']} reduction={result.get('reduction', 0):.1f}%")
|
|
41
|
+
if args.output:
|
|
42
|
+
from dagc_eval.export import to_json, to_csv, to_markdown, to_html
|
|
43
|
+
ext = args.output.rsplit('.', 1)[-1].lower()
|
|
44
|
+
{'json': to_json, 'csv': to_csv, 'md': to_markdown, 'html': to_html}.get(
|
|
45
|
+
ext, to_json)(result, args.output)
|
|
46
|
+
print(f"Wrote report to {args.output}")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def cmd_benchmark(args):
|
|
50
|
+
from dagc_eval import run_benchmark
|
|
51
|
+
results = run_benchmark(n_traces_per_task=args.n_traces,
|
|
52
|
+
noise_levels=[int(x) for x in args.noise_levels.split(',')])
|
|
53
|
+
if args.output:
|
|
54
|
+
from dagc_eval.export import to_json, to_csv, to_markdown, to_html
|
|
55
|
+
ext = args.output.rsplit('.', 1)[-1].lower()
|
|
56
|
+
{'json': to_json, 'csv': to_csv, 'md': to_markdown, 'html': to_html}.get(
|
|
57
|
+
ext, to_json)(results, args.output)
|
|
58
|
+
print(f"Wrote report to {args.output}")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def cmd_compare(args):
|
|
62
|
+
from dagc_eval import run_method_comparison
|
|
63
|
+
run_method_comparison(n_traces_per_task=args.n_traces,
|
|
64
|
+
noise_levels=[int(x) for x in args.noise_levels.split(',')])
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def cmd_stats(args):
|
|
68
|
+
from dagc_eval import bootstrap_drr
|
|
69
|
+
with open(args.input) as f:
|
|
70
|
+
scores = json.load(f)
|
|
71
|
+
ci = bootstrap_drr([{'DRR_soft': s} for s in scores])
|
|
72
|
+
print(f"mean={ci['mean']:.4f} 95% CI=[{ci['ci_lo']:.4f}, {ci['ci_hi']:.4f}] n={ci['n']}")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def main():
|
|
76
|
+
parser = argparse.ArgumentParser(prog='dagc', description='DAGC — Decision-Anchored Graph Compression CLI')
|
|
77
|
+
sub = parser.add_subparsers(dest='command', required=True)
|
|
78
|
+
|
|
79
|
+
p_compress = sub.add_parser('compress', help='Compress a trace (JSON list of messages)')
|
|
80
|
+
p_compress.add_argument('input', help='Path to input trace JSON')
|
|
81
|
+
p_compress.add_argument('-o', '--output', help='Path to write compressed trace JSON')
|
|
82
|
+
p_compress.add_argument('--target-reduction', type=float, default=0.87)
|
|
83
|
+
p_compress.set_defaults(func=cmd_compress)
|
|
84
|
+
|
|
85
|
+
p_eval = sub.add_parser('evaluate', help='Score a trace with compute_drr')
|
|
86
|
+
p_eval.add_argument('input', help='Path to input trace JSON')
|
|
87
|
+
p_eval.add_argument('-o', '--output', help='Path to write report (.json/.csv/.md/.html)')
|
|
88
|
+
p_eval.add_argument('--decision-roles', default='assistant')
|
|
89
|
+
p_eval.add_argument('--quiet', action='store_true')
|
|
90
|
+
p_eval.set_defaults(func=cmd_evaluate)
|
|
91
|
+
|
|
92
|
+
p_bench = sub.add_parser('benchmark', help='Run the synthetic-task DRR benchmark sweep')
|
|
93
|
+
p_bench.add_argument('-o', '--output', help='Path to write report (.json/.csv/.md/.html)')
|
|
94
|
+
p_bench.add_argument('--n-traces', type=int, default=3)
|
|
95
|
+
p_bench.add_argument('--noise-levels', default='1,2,3,4,5')
|
|
96
|
+
p_bench.set_defaults(func=cmd_benchmark)
|
|
97
|
+
|
|
98
|
+
p_compare = sub.add_parser('compare', help='Compare DAGC vs BASELINES')
|
|
99
|
+
p_compare.add_argument('--n-traces', type=int, default=2)
|
|
100
|
+
p_compare.add_argument('--noise-levels', default='3')
|
|
101
|
+
p_compare.set_defaults(func=cmd_compare)
|
|
102
|
+
|
|
103
|
+
p_stats = sub.add_parser('stats', help='Bootstrap CI over a JSON list of DRR_soft scores')
|
|
104
|
+
p_stats.add_argument('input', help='Path to JSON list of floats')
|
|
105
|
+
p_stats.set_defaults(func=cmd_stats)
|
|
106
|
+
|
|
107
|
+
args = parser.parse_args()
|
|
108
|
+
args.func(args)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
if __name__ == '__main__':
|
|
112
|
+
sys.exit(main())
|