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 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())