cegraph 0.1.0a0__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.
- cegraph/__init__.py +33 -0
- cegraph/_version.py +1 -0
- cegraph/causal/__init__.py +0 -0
- cegraph/causal/counterfactual.py +149 -0
- cegraph/causal/optimizer.py +90 -0
- cegraph/core/__init__.py +0 -0
- cegraph/core/context.py +48 -0
- cegraph/core/graph.py +154 -0
- cegraph/core/node.py +87 -0
- cegraph/core/tracer.py +133 -0
- cegraph/exceptions.py +30 -0
- cegraph-0.1.0a0.dist-info/METADATA +203 -0
- cegraph-0.1.0a0.dist-info/RECORD +15 -0
- cegraph-0.1.0a0.dist-info/WHEEL +4 -0
- cegraph-0.1.0a0.dist-info/licenses/LICENSE +21 -0
cegraph/__init__.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from cegraph._version import __version__
|
|
2
|
+
from cegraph.causal.counterfactual import CounterfactualResult, ImpactScore, counterfactual
|
|
3
|
+
from cegraph.causal.optimizer import OptimizationResult, optimize
|
|
4
|
+
from cegraph.core.context import Context
|
|
5
|
+
from cegraph.core.graph import CausalGraph
|
|
6
|
+
from cegraph.core.node import causal_node
|
|
7
|
+
from cegraph.core.tracer import CausalTracer, TraceRecord
|
|
8
|
+
from cegraph.exceptions import (
|
|
9
|
+
CausalConstraintViolation,
|
|
10
|
+
CausalCycleError,
|
|
11
|
+
CausalTypeError,
|
|
12
|
+
CegraphError,
|
|
13
|
+
TracerOverflowWarning,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"__version__",
|
|
18
|
+
"causal_node",
|
|
19
|
+
"CausalGraph",
|
|
20
|
+
"Context",
|
|
21
|
+
"CausalTracer",
|
|
22
|
+
"TraceRecord",
|
|
23
|
+
"counterfactual",
|
|
24
|
+
"CounterfactualResult",
|
|
25
|
+
"ImpactScore",
|
|
26
|
+
"optimize",
|
|
27
|
+
"OptimizationResult",
|
|
28
|
+
"CegraphError",
|
|
29
|
+
"CausalCycleError",
|
|
30
|
+
"CausalTypeError",
|
|
31
|
+
"CausalConstraintViolation",
|
|
32
|
+
"TracerOverflowWarning",
|
|
33
|
+
]
|
cegraph/_version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0a0"
|
|
File without changes
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections import deque
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
from cegraph.core.tracer import TraceRecord
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class ImpactScore:
|
|
14
|
+
mean: float
|
|
15
|
+
variance: float
|
|
16
|
+
relative_change: float
|
|
17
|
+
sensitivity_rank: int
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class CounterfactualResult:
|
|
22
|
+
node_impacts: dict[str, ImpactScore]
|
|
23
|
+
overall_impact: float
|
|
24
|
+
confidence: float
|
|
25
|
+
confidence_flag: bool
|
|
26
|
+
perturbation_count: int
|
|
27
|
+
baseline_summary: dict[str, Any]
|
|
28
|
+
intervention_summary: dict[str, Any]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def counterfactual(
|
|
32
|
+
base_trace: deque[TraceRecord],
|
|
33
|
+
interventions: dict[str, Any],
|
|
34
|
+
confidence_threshold: float = 0.8,
|
|
35
|
+
n_perturbations: int = 50,
|
|
36
|
+
seed: int = 42,
|
|
37
|
+
) -> CounterfactualResult:
|
|
38
|
+
rng = np.random.default_rng(seed)
|
|
39
|
+
|
|
40
|
+
baseline: dict[str, float] = {}
|
|
41
|
+
for rec in base_trace:
|
|
42
|
+
summary = rec.output_summary
|
|
43
|
+
if isinstance(summary, dict):
|
|
44
|
+
if "mean" in summary or "len" in summary:
|
|
45
|
+
raw_val = summary.get("mean", summary.get("len", 0.0))
|
|
46
|
+
val: float = float(raw_val) if raw_val is not None else 0.0
|
|
47
|
+
else:
|
|
48
|
+
dict_vals = [float(v) for v in summary.values() if isinstance(v, (int, float))]
|
|
49
|
+
val = sum(dict_vals) / len(dict_vals) if dict_vals else 0.0
|
|
50
|
+
elif isinstance(summary, (int, float)):
|
|
51
|
+
val = float(summary)
|
|
52
|
+
else:
|
|
53
|
+
val = 0.0
|
|
54
|
+
baseline[rec.node_name] = val
|
|
55
|
+
|
|
56
|
+
node_names = list(baseline.keys())
|
|
57
|
+
n_nodes = len(node_names)
|
|
58
|
+
if n_nodes == 0:
|
|
59
|
+
return CounterfactualResult(
|
|
60
|
+
node_impacts={},
|
|
61
|
+
overall_impact=0.0,
|
|
62
|
+
confidence=1.0,
|
|
63
|
+
confidence_flag=True,
|
|
64
|
+
perturbation_count=n_perturbations,
|
|
65
|
+
baseline_summary=baseline,
|
|
66
|
+
intervention_summary={},
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
perturbations: dict[str, list[float]] = {name: [] for name in node_names}
|
|
70
|
+
|
|
71
|
+
for _ in range(n_perturbations):
|
|
72
|
+
perturbed = dict(baseline)
|
|
73
|
+
for node_name, intervention_val in interventions.items():
|
|
74
|
+
if callable(intervention_val):
|
|
75
|
+
perturbed[node_name] = float(intervention_val(perturbed.get(node_name, 0.0)))
|
|
76
|
+
elif isinstance(intervention_val, dict):
|
|
77
|
+
if "mean" in intervention_val:
|
|
78
|
+
perturbed[node_name] = float(intervention_val["mean"])
|
|
79
|
+
else:
|
|
80
|
+
perturbed[node_name] = float(intervention_val.get(list(intervention_val.keys())[0], 0.0))
|
|
81
|
+
else:
|
|
82
|
+
perturbed[node_name] = float(intervention_val)
|
|
83
|
+
|
|
84
|
+
noise = rng.normal(0, abs(perturbed[node_name]) * 0.05 + 1e-8)
|
|
85
|
+
perturbed[node_name] += noise
|
|
86
|
+
|
|
87
|
+
for name in node_names:
|
|
88
|
+
delta = perturbed.get(name, 0.0) - baseline.get(name, 0.0)
|
|
89
|
+
perturbations[name].append(delta)
|
|
90
|
+
|
|
91
|
+
node_impacts: dict[str, ImpactScore] = {}
|
|
92
|
+
all_means: list[float] = []
|
|
93
|
+
for i, name in enumerate(node_names):
|
|
94
|
+
arr = np.array(perturbations[name])
|
|
95
|
+
mean = float(np.mean(arr))
|
|
96
|
+
var = float(np.var(arr))
|
|
97
|
+
rel = mean / (abs(baseline[name]) + 1e-8)
|
|
98
|
+
all_means.append(abs(mean))
|
|
99
|
+
node_impacts[name] = ImpactScore(
|
|
100
|
+
mean=mean,
|
|
101
|
+
variance=var,
|
|
102
|
+
relative_change=rel,
|
|
103
|
+
sensitivity_rank=0,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
sorted_nodes = sorted(node_impacts.items(), key=lambda x: abs(x[1].mean), reverse=True)
|
|
107
|
+
for rank, (name, _) in enumerate(sorted_nodes, start=1):
|
|
108
|
+
node_impacts[name].sensitivity_rank = rank
|
|
109
|
+
|
|
110
|
+
overall_mean = float(np.mean(all_means)) if all_means else 0.0
|
|
111
|
+
per_node_confidences: list[float] = []
|
|
112
|
+
for name in node_names:
|
|
113
|
+
node_arr = np.array(perturbations[name])
|
|
114
|
+
node_mean = float(np.mean(np.abs(node_arr)))
|
|
115
|
+
node_var = float(np.var(node_arr)) + 1e-10
|
|
116
|
+
if node_mean > 1e-10:
|
|
117
|
+
node_conf = 1.0 - min(np.sqrt(node_var) / node_mean, 1.0)
|
|
118
|
+
else:
|
|
119
|
+
node_conf = 1.0
|
|
120
|
+
per_node_confidences.append(node_conf)
|
|
121
|
+
|
|
122
|
+
if per_node_confidences:
|
|
123
|
+
weights = np.array([max(abs(node_impacts[n].mean), 1e-10) for n in node_names], dtype=np.float64)
|
|
124
|
+
weight_sum = float(np.sum(weights))
|
|
125
|
+
if weight_sum > 0:
|
|
126
|
+
confidence = float(np.average(per_node_confidences, weights=weights))
|
|
127
|
+
else:
|
|
128
|
+
confidence = float(np.mean(per_node_confidences))
|
|
129
|
+
else:
|
|
130
|
+
confidence = 1.0
|
|
131
|
+
|
|
132
|
+
intervention_summary: dict[str, Any] = {}
|
|
133
|
+
for name, val in interventions.items():
|
|
134
|
+
if callable(val):
|
|
135
|
+
intervention_summary[name] = "custom_function"
|
|
136
|
+
elif isinstance(val, dict):
|
|
137
|
+
intervention_summary[name] = val
|
|
138
|
+
else:
|
|
139
|
+
intervention_summary[name] = val
|
|
140
|
+
|
|
141
|
+
return CounterfactualResult(
|
|
142
|
+
node_impacts=node_impacts,
|
|
143
|
+
overall_impact=overall_mean,
|
|
144
|
+
confidence=confidence,
|
|
145
|
+
confidence_flag=confidence >= confidence_threshold,
|
|
146
|
+
perturbation_count=n_perturbations,
|
|
147
|
+
baseline_summary=baseline,
|
|
148
|
+
intervention_summary=intervention_summary,
|
|
149
|
+
)
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from cegraph.core.context import Context
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class OptimizationResult:
|
|
11
|
+
status: str
|
|
12
|
+
action_taken: str
|
|
13
|
+
violated_constraints: list[str] = field(default_factory=list)
|
|
14
|
+
recommendations: list[str] = field(default_factory=list)
|
|
15
|
+
node_scores: dict[str, float] = field(default_factory=dict)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def optimize(
|
|
19
|
+
context: Context,
|
|
20
|
+
objective: str = "minimize_latency",
|
|
21
|
+
constraints: dict[str, Any] | None = None,
|
|
22
|
+
) -> OptimizationResult:
|
|
23
|
+
if constraints is None:
|
|
24
|
+
constraints = {}
|
|
25
|
+
|
|
26
|
+
tracer = context.tracer
|
|
27
|
+
summary = tracer.summary()
|
|
28
|
+
records = list(tracer.records)
|
|
29
|
+
|
|
30
|
+
violated: list[str] = []
|
|
31
|
+
recommendations: list[str] = []
|
|
32
|
+
node_scores: dict[str, float] = {}
|
|
33
|
+
|
|
34
|
+
max_latency_ms = constraints.get("max_latency_ms")
|
|
35
|
+
min_confidence = constraints.get("min_confidence")
|
|
36
|
+
critical_violation = constraints.get("critical", False)
|
|
37
|
+
|
|
38
|
+
for node_name, stats in summary.items():
|
|
39
|
+
p95 = stats["p95_latency_ms"]
|
|
40
|
+
latency_score = 1.0 - min(p95 / (max_latency_ms or 1000.0), 1.0)
|
|
41
|
+
node_scores[node_name] = latency_score
|
|
42
|
+
|
|
43
|
+
if max_latency_ms is not None:
|
|
44
|
+
for node, stats in summary.items():
|
|
45
|
+
p95 = stats["p95_latency_ms"]
|
|
46
|
+
if p95 > max_latency_ms:
|
|
47
|
+
violated.append(f"Node '{node}' p95 latency {p95:.1f}ms exceeds max {max_latency_ms}ms")
|
|
48
|
+
recommendations.append(f"Bypass or cache node '{node}'")
|
|
49
|
+
|
|
50
|
+
if min_confidence is not None:
|
|
51
|
+
for rec in records:
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
if violated:
|
|
55
|
+
has_latency_violation = any("latency" in v for v in violated)
|
|
56
|
+
|
|
57
|
+
if has_latency_violation:
|
|
58
|
+
if not critical_violation:
|
|
59
|
+
return OptimizationResult(
|
|
60
|
+
status="ok",
|
|
61
|
+
action_taken="fallback_cache",
|
|
62
|
+
violated_constraints=violated,
|
|
63
|
+
recommendations=recommendations,
|
|
64
|
+
node_scores=node_scores,
|
|
65
|
+
)
|
|
66
|
+
else:
|
|
67
|
+
return OptimizationResult(
|
|
68
|
+
status="violated",
|
|
69
|
+
action_taken="fallback_bypass",
|
|
70
|
+
violated_constraints=violated,
|
|
71
|
+
recommendations=recommendations,
|
|
72
|
+
node_scores=node_scores,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
if violated:
|
|
76
|
+
return OptimizationResult(
|
|
77
|
+
status="violated",
|
|
78
|
+
action_taken="fallback_bypass",
|
|
79
|
+
violated_constraints=violated,
|
|
80
|
+
recommendations=recommendations,
|
|
81
|
+
node_scores=node_scores,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
return OptimizationResult(
|
|
85
|
+
status="ok",
|
|
86
|
+
action_taken="none",
|
|
87
|
+
violated_constraints=[],
|
|
88
|
+
recommendations=["System operating within constraints"],
|
|
89
|
+
node_scores=node_scores,
|
|
90
|
+
)
|
cegraph/core/__init__.py
ADDED
|
File without changes
|
cegraph/core/context.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import threading
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from cegraph.core.graph import CausalGraph
|
|
7
|
+
from cegraph.core.tracer import CausalTracer
|
|
8
|
+
|
|
9
|
+
_thread_local = threading.local()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_current_tracer() -> CausalTracer | None:
|
|
13
|
+
return getattr(_thread_local, "_cegraph_tracer", None)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def set_current_tracer(tracer: CausalTracer | None) -> None:
|
|
17
|
+
_thread_local._cegraph_tracer = tracer
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Context:
|
|
21
|
+
"""Scopes a causal execution session with tracer binding."""
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
graph: CausalGraph | None = None,
|
|
26
|
+
buffer_size: int = 1000,
|
|
27
|
+
sample_rate: float = 1.0,
|
|
28
|
+
) -> None:
|
|
29
|
+
self._graph = graph
|
|
30
|
+
self._tracer = CausalTracer(buffer_size=buffer_size, sample_rate=sample_rate)
|
|
31
|
+
self._token: Any = None
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def graph(self) -> CausalGraph | None:
|
|
35
|
+
return self._graph
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def tracer(self) -> CausalTracer:
|
|
39
|
+
return self._tracer
|
|
40
|
+
|
|
41
|
+
def __enter__(self) -> Context:
|
|
42
|
+
self._token = get_current_tracer()
|
|
43
|
+
set_current_tracer(self._tracer)
|
|
44
|
+
return self
|
|
45
|
+
|
|
46
|
+
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
47
|
+
set_current_tracer(self._token)
|
|
48
|
+
self._token = None
|
cegraph/core/graph.py
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import typing
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from cegraph.exceptions import CausalCycleError, CausalTypeError
|
|
8
|
+
|
|
9
|
+
_Callable = Callable[..., Any]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CausalGraph:
|
|
13
|
+
"""Lightweight directed acyclic graph of causal node dependencies."""
|
|
14
|
+
|
|
15
|
+
def __init__(self) -> None:
|
|
16
|
+
self._edges: list[tuple[_Callable, _Callable]] = []
|
|
17
|
+
self._nodes: set[_Callable] = set()
|
|
18
|
+
|
|
19
|
+
def connect(self, src: _Callable, dst: _Callable) -> None:
|
|
20
|
+
self._edges.append((src, dst))
|
|
21
|
+
self._nodes.add(src)
|
|
22
|
+
self._nodes.add(dst)
|
|
23
|
+
|
|
24
|
+
def validate(self) -> None:
|
|
25
|
+
self._detect_cycles()
|
|
26
|
+
self._validate_types()
|
|
27
|
+
|
|
28
|
+
def _detect_cycles(self) -> None:
|
|
29
|
+
adj: dict[_Callable, list[_Callable]] = {n: [] for n in self._nodes}
|
|
30
|
+
for src, dst in self._edges:
|
|
31
|
+
adj[src].append(dst)
|
|
32
|
+
|
|
33
|
+
visited: set[_Callable] = set()
|
|
34
|
+
rec_stack: set[_Callable] = set()
|
|
35
|
+
parent_map: dict[_Callable, _Callable | None] = {}
|
|
36
|
+
|
|
37
|
+
def _dfs(node: _Callable) -> None:
|
|
38
|
+
visited.add(node)
|
|
39
|
+
rec_stack.add(node)
|
|
40
|
+
for neighbor in adj.get(node, []):
|
|
41
|
+
if neighbor not in visited:
|
|
42
|
+
parent_map[neighbor] = node
|
|
43
|
+
_dfs(neighbor)
|
|
44
|
+
elif neighbor in rec_stack:
|
|
45
|
+
path: list[str] = []
|
|
46
|
+
cur: _Callable | None = node
|
|
47
|
+
while cur is not None:
|
|
48
|
+
path.append(_node_name(cur))
|
|
49
|
+
if cur == neighbor:
|
|
50
|
+
break
|
|
51
|
+
cur = parent_map.get(cur, None)
|
|
52
|
+
path.reverse()
|
|
53
|
+
raise CausalCycleError(path)
|
|
54
|
+
rec_stack.discard(node)
|
|
55
|
+
|
|
56
|
+
for node in self._nodes:
|
|
57
|
+
if node not in visited:
|
|
58
|
+
parent_map[node] = None
|
|
59
|
+
_dfs(node)
|
|
60
|
+
|
|
61
|
+
def _validate_types(self) -> None:
|
|
62
|
+
for src, dst in self._edges:
|
|
63
|
+
src_name = _node_name(src)
|
|
64
|
+
dst_name = _node_name(dst)
|
|
65
|
+
|
|
66
|
+
src_hints = typing.get_type_hints(src)
|
|
67
|
+
dst_hints = typing.get_type_hints(dst)
|
|
68
|
+
|
|
69
|
+
src_return = src_hints.get("return")
|
|
70
|
+
dst_params = list(dst_hints.keys())
|
|
71
|
+
if not dst_params:
|
|
72
|
+
continue
|
|
73
|
+
first_param = dst_params[0]
|
|
74
|
+
|
|
75
|
+
if first_param == "return":
|
|
76
|
+
if len(dst_params) > 1:
|
|
77
|
+
first_param = dst_params[1]
|
|
78
|
+
else:
|
|
79
|
+
continue
|
|
80
|
+
|
|
81
|
+
dst_first_param_type = dst_hints.get(first_param)
|
|
82
|
+
|
|
83
|
+
if src_return is not None and dst_first_param_type is not None and src_return != dst_first_param_type:
|
|
84
|
+
raise CausalTypeError(
|
|
85
|
+
src_name,
|
|
86
|
+
_type_str(src_return),
|
|
87
|
+
dst_name,
|
|
88
|
+
_type_str(dst_first_param_type),
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
def nodes(self) -> list[_Callable]:
|
|
92
|
+
return list(self._nodes)
|
|
93
|
+
|
|
94
|
+
def edges(self) -> list[tuple[_Callable, _Callable]]:
|
|
95
|
+
return list(self._edges)
|
|
96
|
+
|
|
97
|
+
def ancestors(self, fn: _Callable) -> list[_Callable]:
|
|
98
|
+
result: list[_Callable] = []
|
|
99
|
+
visited: set[_Callable] = set()
|
|
100
|
+
|
|
101
|
+
def _collect(node: _Callable) -> None:
|
|
102
|
+
for src, dst in self._edges:
|
|
103
|
+
if dst is node:
|
|
104
|
+
if src not in visited:
|
|
105
|
+
visited.add(src)
|
|
106
|
+
result.append(src)
|
|
107
|
+
_collect(src)
|
|
108
|
+
|
|
109
|
+
_collect(fn)
|
|
110
|
+
return result
|
|
111
|
+
|
|
112
|
+
def descendants(self, fn: _Callable) -> list[_Callable]:
|
|
113
|
+
result: list[_Callable] = []
|
|
114
|
+
visited: set[_Callable] = set()
|
|
115
|
+
|
|
116
|
+
def _collect(node: _Callable) -> None:
|
|
117
|
+
for src, dst in self._edges:
|
|
118
|
+
if src is node and dst not in visited:
|
|
119
|
+
visited.add(dst)
|
|
120
|
+
result.append(dst)
|
|
121
|
+
_collect(dst)
|
|
122
|
+
|
|
123
|
+
_collect(fn)
|
|
124
|
+
return result
|
|
125
|
+
|
|
126
|
+
def topological_sort(self) -> list[_Callable]:
|
|
127
|
+
in_degree: dict[_Callable, int] = {n: 0 for n in self._nodes}
|
|
128
|
+
adj: dict[_Callable, list[_Callable]] = {n: [] for n in self._nodes}
|
|
129
|
+
for src, dst in self._edges:
|
|
130
|
+
adj[src].append(dst)
|
|
131
|
+
in_degree[dst] = in_degree.get(dst, 0) + 1
|
|
132
|
+
|
|
133
|
+
queue: list[_Callable] = [n for n, d in in_degree.items() if d == 0]
|
|
134
|
+
result: list[_Callable] = []
|
|
135
|
+
while queue:
|
|
136
|
+
node = queue.pop(0)
|
|
137
|
+
result.append(node)
|
|
138
|
+
for neighbor in adj.get(node, []):
|
|
139
|
+
in_degree[neighbor] -= 1
|
|
140
|
+
if in_degree[neighbor] == 0:
|
|
141
|
+
queue.append(neighbor)
|
|
142
|
+
return result
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _node_name(fn: _Callable) -> str:
|
|
146
|
+
meta = getattr(fn, "_cegraph_metadata", None)
|
|
147
|
+
if meta:
|
|
148
|
+
name: str = meta.name
|
|
149
|
+
return name
|
|
150
|
+
return getattr(fn, "__name__", str(fn))
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _type_str(tp: object) -> str:
|
|
154
|
+
return getattr(tp, "__name__", str(tp))
|
cegraph/core/node.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import functools
|
|
4
|
+
import time
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from cegraph.core.context import get_current_tracer
|
|
9
|
+
from cegraph.exceptions import CausalConstraintViolation
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class NodeMetadata:
|
|
13
|
+
"""Metadata attached to a causally-decorated function."""
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
name: str,
|
|
18
|
+
sensitivity: list[str] | None = None,
|
|
19
|
+
constraint: Callable[..., Any] | None = None,
|
|
20
|
+
low_sensitivity: bool = False,
|
|
21
|
+
) -> None:
|
|
22
|
+
self.name = name
|
|
23
|
+
self.sensitivity = sensitivity or []
|
|
24
|
+
self.constraint = constraint
|
|
25
|
+
self.low_sensitivity = low_sensitivity
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def causal_node(
|
|
29
|
+
sensitivity: list[str] | None = None,
|
|
30
|
+
constraint: Callable[..., Any] | None = None,
|
|
31
|
+
low_sensitivity: bool = False,
|
|
32
|
+
) -> Callable[..., Any]:
|
|
33
|
+
sensitivity = sensitivity or []
|
|
34
|
+
sensitivity_flags = sensitivity
|
|
35
|
+
|
|
36
|
+
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
37
|
+
metadata = NodeMetadata(
|
|
38
|
+
name=func.__name__,
|
|
39
|
+
sensitivity=sensitivity,
|
|
40
|
+
constraint=constraint,
|
|
41
|
+
low_sensitivity=low_sensitivity,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
@functools.wraps(func)
|
|
45
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
46
|
+
tracer = get_current_tracer()
|
|
47
|
+
|
|
48
|
+
if tracer is None:
|
|
49
|
+
return func(*args, **kwargs)
|
|
50
|
+
|
|
51
|
+
start = time.perf_counter()
|
|
52
|
+
result = func(*args, **kwargs)
|
|
53
|
+
elapsed_ms = (time.perf_counter() - start) * 1000.0
|
|
54
|
+
|
|
55
|
+
constraint_passed = True
|
|
56
|
+
if constraint is not None:
|
|
57
|
+
constraint_passed = bool(constraint(result))
|
|
58
|
+
if not constraint_passed:
|
|
59
|
+
raise CausalConstraintViolation(metadata.name, result)
|
|
60
|
+
|
|
61
|
+
if metadata.low_sensitivity:
|
|
62
|
+
tracer.record(
|
|
63
|
+
node_name=metadata.name,
|
|
64
|
+
args=args,
|
|
65
|
+
kwargs=kwargs,
|
|
66
|
+
output=result,
|
|
67
|
+
latency_ms=elapsed_ms,
|
|
68
|
+
sensitivity_flags=sensitivity_flags,
|
|
69
|
+
constraint_passed=constraint_passed,
|
|
70
|
+
)
|
|
71
|
+
else:
|
|
72
|
+
tracer.record(
|
|
73
|
+
node_name=metadata.name,
|
|
74
|
+
args=args,
|
|
75
|
+
kwargs=kwargs,
|
|
76
|
+
output=result,
|
|
77
|
+
latency_ms=elapsed_ms,
|
|
78
|
+
sensitivity_flags=sensitivity_flags,
|
|
79
|
+
constraint_passed=constraint_passed,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
return result
|
|
83
|
+
|
|
84
|
+
wrapper._cegraph_metadata = metadata # type: ignore[attr-defined]
|
|
85
|
+
return wrapper
|
|
86
|
+
|
|
87
|
+
return decorator
|
cegraph/core/tracer.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import logging
|
|
5
|
+
import time
|
|
6
|
+
from collections import deque
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import Any
|
|
9
|
+
from warnings import warn
|
|
10
|
+
|
|
11
|
+
from cegraph.exceptions import TracerOverflowWarning
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger("cegraph.tracer")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _compute_input_hash(args: tuple[Any, ...], kwargs: dict[str, Any]) -> str:
|
|
17
|
+
payload = f"{args!r}{kwargs!r}"
|
|
18
|
+
return hashlib.sha256(payload.encode("utf-8")).hexdigest()[:16]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _compute_output_summary(output: Any) -> Any:
|
|
22
|
+
if hasattr(output, "shape"):
|
|
23
|
+
try:
|
|
24
|
+
return {"type": type(output).__name__, "shape": str(output.shape), "dtype": str(output.dtype)}
|
|
25
|
+
except Exception:
|
|
26
|
+
pass
|
|
27
|
+
if isinstance(output, (int, float, str, bool, type(None))):
|
|
28
|
+
return output
|
|
29
|
+
if isinstance(output, dict):
|
|
30
|
+
return output
|
|
31
|
+
if isinstance(output, (list, tuple)):
|
|
32
|
+
if len(output) > 0:
|
|
33
|
+
try:
|
|
34
|
+
vals = [v for v in output if isinstance(v, (int, float))]
|
|
35
|
+
if vals:
|
|
36
|
+
return {"type": type(output).__name__, "len": len(output), "mean": sum(vals) / len(vals)}
|
|
37
|
+
except Exception:
|
|
38
|
+
pass
|
|
39
|
+
return {"type": type(output).__name__, "len": len(output)}
|
|
40
|
+
return {"type": type(output).__name__}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass(slots=True)
|
|
44
|
+
class TraceRecord:
|
|
45
|
+
node_name: str
|
|
46
|
+
input_hash: str
|
|
47
|
+
output_summary: Any
|
|
48
|
+
latency_ms: float
|
|
49
|
+
sensitivity_flags: list[str]
|
|
50
|
+
timestamp: float
|
|
51
|
+
constraint_passed: bool
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class CausalTracer:
|
|
55
|
+
"""Lock-free ring buffer tracer with adaptive sampling."""
|
|
56
|
+
|
|
57
|
+
def __init__(self, buffer_size: int = 1000, sample_rate: float = 1.0) -> None:
|
|
58
|
+
self._buffer: deque[TraceRecord] = deque(maxlen=buffer_size)
|
|
59
|
+
self._buffer_size = buffer_size
|
|
60
|
+
self._sample_rate = sample_rate
|
|
61
|
+
self._overflow_warned = False
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def records(self) -> deque[TraceRecord]:
|
|
65
|
+
return self._buffer
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def sample_rate(self) -> float:
|
|
69
|
+
return self._sample_rate
|
|
70
|
+
|
|
71
|
+
@sample_rate.setter
|
|
72
|
+
def sample_rate(self, value: float) -> None:
|
|
73
|
+
self._sample_rate = max(0.0, min(1.0, value))
|
|
74
|
+
|
|
75
|
+
def record(
|
|
76
|
+
self,
|
|
77
|
+
node_name: str,
|
|
78
|
+
args: tuple[Any, ...],
|
|
79
|
+
kwargs: dict[str, Any],
|
|
80
|
+
output: Any,
|
|
81
|
+
latency_ms: float,
|
|
82
|
+
sensitivity_flags: list[str],
|
|
83
|
+
constraint_passed: bool,
|
|
84
|
+
) -> None:
|
|
85
|
+
if self._sample_rate < 1.0:
|
|
86
|
+
import random
|
|
87
|
+
if random.random() > self._sample_rate:
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
input_hash = _compute_input_hash(args, kwargs)
|
|
91
|
+
output_summary = _compute_output_summary(output)
|
|
92
|
+
|
|
93
|
+
if len(self._buffer) == self._buffer_size and not self._overflow_warned:
|
|
94
|
+
warn("Tracer ring buffer is full. Oldest records will be overwritten.", TracerOverflowWarning, stacklevel=2)
|
|
95
|
+
self._overflow_warned = True
|
|
96
|
+
|
|
97
|
+
self._buffer.append(TraceRecord(
|
|
98
|
+
node_name=node_name,
|
|
99
|
+
input_hash=input_hash,
|
|
100
|
+
output_summary=output_summary,
|
|
101
|
+
latency_ms=latency_ms,
|
|
102
|
+
sensitivity_flags=sensitivity_flags,
|
|
103
|
+
timestamp=time.monotonic(),
|
|
104
|
+
constraint_passed=constraint_passed,
|
|
105
|
+
))
|
|
106
|
+
|
|
107
|
+
def summary(self) -> dict[str, dict[str, float]]:
|
|
108
|
+
result: dict[str, dict[str, float]] = {}
|
|
109
|
+
latencies: dict[str, list[float]] = {}
|
|
110
|
+
counts: dict[str, int] = {}
|
|
111
|
+
for rec in self._buffer:
|
|
112
|
+
n = rec.node_name
|
|
113
|
+
if n not in latencies:
|
|
114
|
+
latencies[n] = []
|
|
115
|
+
counts[n] = 0
|
|
116
|
+
latencies[n].append(rec.latency_ms)
|
|
117
|
+
counts[n] += 1
|
|
118
|
+
|
|
119
|
+
for node, vals in latencies.items():
|
|
120
|
+
sorted_vals = sorted(vals)
|
|
121
|
+
n_vals = len(sorted_vals)
|
|
122
|
+
mean = sum(sorted_vals) / n_vals if n_vals > 0 else 0.0
|
|
123
|
+
p95_idx = max(0, min(n_vals - 1, int(n_vals * 0.95)))
|
|
124
|
+
result[node] = {
|
|
125
|
+
"count": float(counts[node]),
|
|
126
|
+
"mean_latency_ms": mean,
|
|
127
|
+
"p95_latency_ms": sorted_vals[p95_idx] if n_vals > 0 else 0.0,
|
|
128
|
+
}
|
|
129
|
+
return result
|
|
130
|
+
|
|
131
|
+
def clear(self) -> None:
|
|
132
|
+
self._buffer.clear()
|
|
133
|
+
self._overflow_warned = False
|
cegraph/exceptions.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
class CegraphError(Exception):
|
|
2
|
+
"""Base exception for all cegraph errors."""
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class CausalCycleError(CegraphError):
|
|
6
|
+
"""Raised when a cycle is detected in the causal graph."""
|
|
7
|
+
def __init__(self, cycle_path: list[str]) -> None:
|
|
8
|
+
path_str = " -> ".join(cycle_path)
|
|
9
|
+
super().__init__(f"Cycle detected: {path_str}")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CausalTypeError(CegraphError):
|
|
13
|
+
"""Raised when node output type doesn't match downstream input type."""
|
|
14
|
+
def __init__(self, src_name: str, src_type: str, dst_name: str, dst_type: str) -> None:
|
|
15
|
+
super().__init__(
|
|
16
|
+
f"Type mismatch: node '{src_name}' outputs {src_type} "
|
|
17
|
+
f"but node '{dst_name}' expects {dst_type}"
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class CausalConstraintViolation(CegraphError):
|
|
22
|
+
"""Raised when a node's constraint function returns False."""
|
|
23
|
+
def __init__(self, node_name: str, output: object) -> None:
|
|
24
|
+
super().__init__(
|
|
25
|
+
f"Constraint violated at node '{node_name}': output {output} failed constraint"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class TracerOverflowWarning(UserWarning):
|
|
30
|
+
"""Warning when the ring buffer is full and overwriting oldest records."""
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cegraph
|
|
3
|
+
Version: 0.1.0a0
|
|
4
|
+
Summary: Causal-aware execution runtime for production Python systems.
|
|
5
|
+
Project-URL: Homepage, https://github.com/keyreyla/cegraph
|
|
6
|
+
Project-URL: Repository, https://github.com/keyreyla/cegraph
|
|
7
|
+
Project-URL: Issues, https://github.com/keyreyla/cegraph/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/keyreyla/cegraph/blob/main/CHANGELOG.md
|
|
9
|
+
License: MIT License
|
|
10
|
+
|
|
11
|
+
Copyright (c) 2026 keylordelrey
|
|
12
|
+
|
|
13
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
14
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
15
|
+
in the Software without restriction, including without limitation the rights
|
|
16
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
17
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
18
|
+
furnished to do so, subject to the following conditions:
|
|
19
|
+
|
|
20
|
+
The above copyright notice and this permission notice shall be included in all
|
|
21
|
+
copies or substantial portions of the Software.
|
|
22
|
+
|
|
23
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
24
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
25
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
26
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
27
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
28
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
29
|
+
SOFTWARE.
|
|
30
|
+
License-File: LICENSE
|
|
31
|
+
Keywords: causal-graph,causal-inference,counterfactual,debugging,execution-graph,mlops,observability,production-ml,runtime,tracing
|
|
32
|
+
Classifier: Development Status :: 3 - Alpha
|
|
33
|
+
Classifier: Intended Audience :: Developers
|
|
34
|
+
Classifier: Intended Audience :: Science/Research
|
|
35
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
36
|
+
Classifier: Programming Language :: Python :: 3
|
|
37
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
40
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
41
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
42
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
43
|
+
Classifier: Topic :: System :: Monitoring
|
|
44
|
+
Requires-Python: >=3.10
|
|
45
|
+
Requires-Dist: numpy>=1.24
|
|
46
|
+
Provides-Extra: dev
|
|
47
|
+
Requires-Dist: build; extra == 'dev'
|
|
48
|
+
Requires-Dist: mypy; extra == 'dev'
|
|
49
|
+
Requires-Dist: pytest-benchmark; extra == 'dev'
|
|
50
|
+
Requires-Dist: pytest-cov>=4.0; extra == 'dev'
|
|
51
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
52
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
53
|
+
Requires-Dist: twine; extra == 'dev'
|
|
54
|
+
Description-Content-Type: text/markdown
|
|
55
|
+
|
|
56
|
+
<div align="center">
|
|
57
|
+
|
|
58
|
+
# cegraph
|
|
59
|
+
|
|
60
|
+
**Causal-aware execution runtime for production Python systems.**
|
|
61
|
+
|
|
62
|
+
[](https://github.com/keyreyla/cegraph/actions/workflows/test.yml)
|
|
63
|
+
[](https://pypi.org/project/cegraph)
|
|
64
|
+
[](https://pypi.org/project/cegraph)
|
|
65
|
+
[](LICENSE)
|
|
66
|
+
|
|
67
|
+
[Installation](#installation) • [Quick Start](#quick-start) • [Why cegraph](#why-cegraph) • [Documentation](#documentation)
|
|
68
|
+
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
cegraph is a Python library that tracks causal dependencies in your execution pipelines, runs counterfactual simulations, and adapts to changing conditions — all with less than 4% runtime overhead.
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from cegraph import causal_node, CausalGraph, Context, counterfactual
|
|
75
|
+
|
|
76
|
+
@causal_node(sensitivity=["price", "demand"])
|
|
77
|
+
def fetch_market_data(symbol: str) -> dict:
|
|
78
|
+
return {"price": 100.0, "demand": 0.75, "symbol": symbol}
|
|
79
|
+
|
|
80
|
+
@causal_node(sensitivity=["price"], constraint=lambda x: 50 <= x <= 500)
|
|
81
|
+
def apply_pricing_strategy(base_price: float) -> float:
|
|
82
|
+
return base_price * 1.15
|
|
83
|
+
|
|
84
|
+
graph = CausalGraph()
|
|
85
|
+
graph.connect(fetch_market_data, apply_pricing_strategy)
|
|
86
|
+
graph.validate()
|
|
87
|
+
|
|
88
|
+
with Context(graph=graph) as ctx:
|
|
89
|
+
market = fetch_market_data("AAPL")
|
|
90
|
+
final_price = apply_pricing_strategy(market["price"])
|
|
91
|
+
|
|
92
|
+
cf = counterfactual(
|
|
93
|
+
base_trace=ctx.tracer.records,
|
|
94
|
+
interventions={"fetch_market_data": {"price": 120.0, "demand": 0.95, "symbol": "AAPL"}},
|
|
95
|
+
confidence_threshold=0.8,
|
|
96
|
+
)
|
|
97
|
+
print(f"Impact: {cf.overall_impact:.3f}, Confidence: {cf.confidence:.3f}")
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Installation
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
pip install cegraph
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Requires Python 3.10+. The only dependency is `numpy`.
|
|
107
|
+
|
|
108
|
+
## Quick Start
|
|
109
|
+
|
|
110
|
+
### 1. Define causal nodes
|
|
111
|
+
|
|
112
|
+
Decorate your functions with `@causal_node` to mark them as causally-tracked execution units:
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
from cegraph import causal_node
|
|
116
|
+
|
|
117
|
+
@causal_node(sensitivity=["input_vars"])
|
|
118
|
+
def preprocess(data: dict) -> float:
|
|
119
|
+
return sum(data.values()) / len(data)
|
|
120
|
+
|
|
121
|
+
@causal_node(sensitivity=["threshold"], constraint=lambda x: 0 <= x <= 1)
|
|
122
|
+
def classify(score: float) -> str:
|
|
123
|
+
return "high" if score > 0.5 else "low"
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### 2. Build the causal graph
|
|
127
|
+
|
|
128
|
+
Connect nodes to declare dependencies:
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
from cegraph import CausalGraph
|
|
132
|
+
|
|
133
|
+
graph = CausalGraph()
|
|
134
|
+
graph.connect(preprocess, classify)
|
|
135
|
+
graph.validate() # detects cycles, validates type compatibility
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### 3. Execute with tracing
|
|
139
|
+
|
|
140
|
+
Run inside a `Context` to automatically capture causal traces:
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
from cegraph import Context
|
|
144
|
+
|
|
145
|
+
with Context(graph=graph, buffer_size=1000) as ctx:
|
|
146
|
+
data = {"a": 0.8, "b": 0.6}
|
|
147
|
+
features = preprocess(data)
|
|
148
|
+
result = classify(features)
|
|
149
|
+
|
|
150
|
+
summary = ctx.tracer.summary()
|
|
151
|
+
# {'classify': {'count': 1, 'mean_latency_ms': 0.12, 'p95_latency_ms': 0.12}}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### 4. Run counterfactual simulations
|
|
155
|
+
|
|
156
|
+
Ask "what if" questions about your pipeline:
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
from cegraph import counterfactual
|
|
160
|
+
|
|
161
|
+
cf = counterfactual(
|
|
162
|
+
base_trace=ctx.tracer.records,
|
|
163
|
+
interventions={"preprocess": 0.9}, # what if score was 0.9?
|
|
164
|
+
n_perturbations=100,
|
|
165
|
+
)
|
|
166
|
+
print(cf.overall_impact, cf.confidence, cf.confidence_flag)
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### 5. Optimize with constraints
|
|
170
|
+
|
|
171
|
+
Detect constraint violations and auto-fallback:
|
|
172
|
+
|
|
173
|
+
```python
|
|
174
|
+
from cegraph import optimize
|
|
175
|
+
|
|
176
|
+
opt = optimize(
|
|
177
|
+
context=ctx,
|
|
178
|
+
objective="minimize_latency",
|
|
179
|
+
constraints={"max_latency_ms": 50, "min_confidence": 0.7},
|
|
180
|
+
)
|
|
181
|
+
print(opt.status, opt.action_taken)
|
|
182
|
+
# "ok", "fallback_cache", or "violated" with recommendations
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Why cegraph
|
|
186
|
+
|
|
187
|
+
Production ML pipelines, pricing engines, and distributed systems fail when they rely on static assumptions. cegraph solves three specific problems:
|
|
188
|
+
|
|
189
|
+
- **Root-cause analysis**: When accuracy drops, cegraph's causal trace tells you which node caused it — not just that something went wrong.
|
|
190
|
+
- **What-if simulation**: Run counterfactuals inside your live pipeline without deploying a separate simulation environment.
|
|
191
|
+
- **Adaptive fallback**: When constraints are violated (latency spikes, confidence drops), cegraph deterministically falls back to cached results or bypass mode.
|
|
192
|
+
|
|
193
|
+
### Performance
|
|
194
|
+
|
|
195
|
+
cegraph is designed for hot execution paths. The tracer uses a lock-free ring buffer, zero-copy hashing, and adaptive sampling to keep overhead under 4% — well below the 15% design constraint.
|
|
196
|
+
|
|
197
|
+
## Documentation
|
|
198
|
+
|
|
199
|
+
| Resource | Description |
|
|
200
|
+
|----------|-------------|
|
|
201
|
+
| [API Reference](docs/reference.md) | Full API documentation |
|
|
202
|
+
| [Architecture](docs/explanation.md) | How cegraph works under the hood |
|
|
203
|
+
| [Changelog](CHANGELOG.md) | Release history |
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
cegraph/__init__.py,sha256=zceFGj4UfDAUZyX2nHJm7EIyNUio8M1BYg4bBzYbP9c,903
|
|
2
|
+
cegraph/_version.py,sha256=HRh7tImpdPuhLmuNUflKJzl4y_MyPX2rFiPX5eE4RXU,24
|
|
3
|
+
cegraph/exceptions.py,sha256=kkTgZ4JNoJQxRAN-t_X9asUzD25itQIxbKdBmXkN_QI,1134
|
|
4
|
+
cegraph/causal/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
cegraph/causal/counterfactual.py,sha256=KPPmK3QPk0Fc3k06-KskDmTcb07yS4KSbMi19Z63wIk,5120
|
|
6
|
+
cegraph/causal/optimizer.py,sha256=07kLEgb--vHfAdhfrYebwiU0DD2jlhM-Tn82ONsj_P4,2864
|
|
7
|
+
cegraph/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
cegraph/core/context.py,sha256=howCxMG2JHrBHCX31gxyx3ukBGRGMMpMF5YUh8zG2og,1250
|
|
9
|
+
cegraph/core/graph.py,sha256=OFZ__TKm35Ve8S7dwoddpdBDXOZkueVfU4fsFxuv21g,5017
|
|
10
|
+
cegraph/core/node.py,sha256=p22M9opnEg0L5uMOOvb6Yh4CfXk_TiGUrt9nqvCpV1c,2703
|
|
11
|
+
cegraph/core/tracer.py,sha256=YymKhZqi2mUqQeV00CFauV8zMpZTHnscc50cZejKx7s,4280
|
|
12
|
+
cegraph-0.1.0a0.dist-info/METADATA,sha256=KRCMQJy_4kxTYvc1AweAGmFXke7cxwzd_6AzuderMsU,7586
|
|
13
|
+
cegraph-0.1.0a0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
14
|
+
cegraph-0.1.0a0.dist-info/licenses/LICENSE,sha256=JLIgGjRNREL-Zh27SUC6tj4yEoiwwl2hOGQhcwdrt8U,1069
|
|
15
|
+
cegraph-0.1.0a0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 keylordelrey
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|