consequence-gate 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.
@@ -0,0 +1,3 @@
1
+ """consequence-gate: speculative outcome-simulation layer for AI agent tool calls."""
2
+
3
+ __version__ = "0.1.0"
File without changes
@@ -0,0 +1,43 @@
1
+ """
2
+ Offline backtest harness: replays historical JSONL tool-call traces
3
+ through a simulator + evaluator, WITHOUT re-executing anything, to
4
+ measure the four-quadrant FP/FN/TN/steer-recovery breakdown against
5
+ the trace's recorded existing_gate_decision and actual_execution_status.
6
+ """
7
+
8
+ import json
9
+ from typing import Callable, Dict, Iterable, List
10
+
11
+
12
+ def load_traces(path: str) -> List[Dict]:
13
+ traces = []
14
+ with open(path) as f:
15
+ for line in f:
16
+ line = line.strip()
17
+ if line:
18
+ traces.append(json.loads(line))
19
+ return traces
20
+
21
+
22
+ def run_backtest(traces: Iterable[Dict], simulate_and_evaluate: Callable[[Dict], str]) -> List[Dict]:
23
+ """
24
+ simulate_and_evaluate: function(trace) -> decision string ("ALLOW"/"DENY"/"ASK"/"STEER")
25
+ Returns per-trace records annotated with quadrant classification.
26
+ """
27
+ results = []
28
+ for trace in traces:
29
+ new_decision = simulate_and_evaluate(trace)
30
+ old_decision = trace.get("existing_gate_decision", "ALLOW")
31
+ outcome = trace.get("actual_execution_status", "UNKNOWN")
32
+
33
+ if old_decision == "ALLOW" and new_decision in ("DENY", "STEER", "ASK") and outcome != "SUCCESS":
34
+ quadrant = "FALSE_NEGATIVE_CAUGHT"
35
+ elif old_decision in ("DENY", "ASK") and new_decision == "ALLOW":
36
+ quadrant = "FALSE_POSITIVE_RELIEVED"
37
+ elif old_decision == "ALLOW" and new_decision == "ALLOW":
38
+ quadrant = "TRUE_NEGATIVE"
39
+ else:
40
+ quadrant = "OTHER"
41
+
42
+ results.append({**trace, "simulated_decision": new_decision, "quadrant": quadrant})
43
+ return results
@@ -0,0 +1,20 @@
1
+ """
2
+ Generates the four-quadrant FP/FN breakdown report from backtest results.
3
+ """
4
+
5
+ from collections import Counter
6
+ from typing import Dict, List
7
+
8
+
9
+ def generate_report(results: List[Dict]) -> Dict:
10
+ counts = Counter(r["quadrant"] for r in results)
11
+ total = len(results)
12
+ return {
13
+ "total_traces": total,
14
+ "true_negative": counts.get("TRUE_NEGATIVE", 0),
15
+ "false_negative_caught": counts.get("FALSE_NEGATIVE_CAUGHT", 0),
16
+ "false_positive_relieved": counts.get("FALSE_POSITIVE_RELIEVED", 0),
17
+ "other": counts.get("OTHER", 0),
18
+ "false_negative_rate": counts.get("FALSE_NEGATIVE_CAUGHT", 0) / total if total else 0.0,
19
+ "false_positive_relief_rate": counts.get("FALSE_POSITIVE_RELIEVED", 0) / total if total else 0.0,
20
+ }
@@ -0,0 +1,75 @@
1
+ """
2
+ CLI entry point for consequence-gate.
3
+ """
4
+
5
+ import argparse
6
+ import json
7
+ import sys
8
+ from consequence_gate import __version__
9
+ from consequence_gate.backtest.harness import load_traces, run_backtest
10
+ from consequence_gate.backtest.reporter import generate_report
11
+
12
+
13
+ def default_evaluator(trace: dict) -> str:
14
+ """Heuristic evaluator for CLI demo and trace analysis."""
15
+ tool = trace.get("tool_name", "")
16
+ args = trace.get("tool_args", {})
17
+
18
+ tool_lower = tool.lower()
19
+ args_str = str(args).lower()
20
+
21
+ if any(k in tool_lower for k in ("delete", "drop", "purge", "truncate")) or any(
22
+ k in args_str for k in ("drop ", "delete from", "truncate ", "purge")
23
+ ):
24
+ return "DENY"
25
+ if "transfer" in tool.lower() or "pay" in tool.lower():
26
+ amount = args.get("amount", 0)
27
+ if isinstance(amount, (int, float)) and amount > 5000:
28
+ return "ASK"
29
+ return "ALLOW"
30
+
31
+
32
+ def main():
33
+ parser = argparse.ArgumentParser(
34
+ prog="consequence-gate",
35
+ description="Speculative outcome-simulation gate & trace backtesting for AI agent tool calls.",
36
+ )
37
+ parser.add_argument("-v", "--version", action="version", version=f"%(prog)s {__version__}")
38
+
39
+ subparsers = parser.add_subparsers(dest="command", help="Available subcommands")
40
+
41
+ # Backtest subcommand
42
+ bt_parser = subparsers.add_parser("backtest", help="Run offline backtesting on a JSONL trace file")
43
+ bt_parser.add_argument("file", help="Path to JSONL file containing recorded agent traces")
44
+ bt_parser.add_argument("--json", action="store_true", help="Output results in JSON format")
45
+
46
+ args = parser.parse_args()
47
+
48
+ if args.command == "backtest":
49
+ try:
50
+ traces = load_traces(args.file)
51
+ results = run_backtest(traces, default_evaluator)
52
+ report = generate_report(results)
53
+
54
+ if args.json:
55
+ print(json.dumps(report, indent=2))
56
+ else:
57
+ print("\n================ CONSEQUENCE GATE BACKTEST REPORT ================")
58
+ print(f"Total Traces Evaluated: {report['total_traces']}")
59
+ print(f"True Negatives: {report['true_negative']}")
60
+ print(f"False Negatives Caught: {report['false_negative_caught']}")
61
+ print(f"False Positives Relieved: {report['false_positive_relieved']}")
62
+ print(f"Other / Unclassified: {report['other']}")
63
+ print("-----------------------------------------------------------------")
64
+ print(f"False Negative Catch Rate: {report['false_negative_rate']:.2%}")
65
+ print(f"False Positive Relief Rate: {report['false_positive_relief_rate']:.2%}")
66
+ print("=================================================================\n")
67
+ except Exception as e:
68
+ print(f"Error executing backtest: {e}", file=sys.stderr)
69
+ sys.exit(1)
70
+ else:
71
+ parser.print_help()
72
+
73
+
74
+ if __name__ == "__main__":
75
+ main()
File without changes
@@ -0,0 +1,56 @@
1
+ """
2
+ SteerCircuitBreaker: idempotency-locked retry cap for STEER decisions.
3
+
4
+ Design contract (see project history / design notes):
5
+ - The idempotency token is derived ONCE from the transaction's own natural
6
+ key (e.g. claim_id, table+filter hash) -- never regenerated per retry.
7
+ A fresh UUID per attempt defeats duplicate-execution protection.
8
+ - Responses are cached per token, so a retry with the same natural key
9
+ returns the cached result instead of re-executing (Stripe-style contract).
10
+ - Retry count is tracked server-side per token, with a hard cap. Once
11
+ exceeded, the breaker forces ASK (human escalation) regardless of how
12
+ good the steering guidance is -- this is a backstop against
13
+ loop-thrashing, independent of guidance quality.
14
+ """
15
+
16
+ from typing import Any, Dict
17
+ from .models import GateDecision, EvaluationResult
18
+
19
+
20
+ class SteerCircuitBreaker:
21
+ def __init__(self, max_retries: int = 2):
22
+ self.max_retries = max_retries
23
+ self._attempts: Dict[str, int] = {}
24
+ self._responses: Dict[str, EvaluationResult] = {}
25
+
26
+ def token_for(self, natural_key: str) -> str:
27
+ return f"steer_{natural_key}"
28
+
29
+ def resolve(self, natural_key: str, confidence: float,
30
+ base_steer: Dict[str, Any]) -> EvaluationResult:
31
+ token = self.token_for(natural_key)
32
+
33
+ if token in self._responses:
34
+ return self._responses[token]
35
+
36
+ attempt = self._attempts.get(token, 0)
37
+
38
+ if attempt >= self.max_retries:
39
+ result = EvaluationResult(
40
+ decision=GateDecision.ASK,
41
+ confidence=confidence,
42
+ reason=f"Steer circuit breaker tripped ({attempt}/{self.max_retries}). Escalating to human.",
43
+ )
44
+ self._responses[token] = result
45
+ return result
46
+
47
+ self._attempts[token] = attempt + 1
48
+ base_steer.setdefault("suggested_args", {})["idempotency_key"] = token
49
+
50
+ result = EvaluationResult(
51
+ decision=GateDecision.STEER,
52
+ confidence=confidence,
53
+ reason=f"Steered to safer path (attempt {attempt + 1}/{self.max_retries}).",
54
+ steer_payload=base_steer,
55
+ )
56
+ return result
@@ -0,0 +1,27 @@
1
+ """
2
+ BlastRadiusEvaluator: generic threshold evaluation for simulated deltas.
3
+ Domain simulators typically implement their own evaluate() with
4
+ domain-specific rules, but this provides a reusable default.
5
+ """
6
+
7
+ from .models import SimulatedStateDelta, GateDecision
8
+
9
+
10
+ class BlastRadiusEvaluator:
11
+ def __init__(self, max_irreversible_value: float = 1000.0,
12
+ min_confidence_for_autopass: float = 0.85):
13
+ self.max_irreversible_value = max_irreversible_value
14
+ self.min_confidence_for_autopass = min_confidence_for_autopass
15
+
16
+ def evaluate(self, delta: SimulatedStateDelta) -> GateDecision:
17
+ if delta.confidence < self.min_confidence_for_autopass:
18
+ return GateDecision.ASK
19
+
20
+ breach = any(abs(v) > self.max_irreversible_value
21
+ for v in delta.numeric_deltas.values())
22
+
23
+ if delta.irreversibility_score >= 0.9 and breach:
24
+ return GateDecision.STEER
25
+ if breach:
26
+ return GateDecision.ASK
27
+ return GateDecision.ALLOW
@@ -0,0 +1,35 @@
1
+ """
2
+ Core data models shared across all domain simulators.
3
+ """
4
+
5
+ from dataclasses import dataclass, field
6
+ from enum import Enum
7
+ from typing import Any, Dict, List, Optional
8
+
9
+
10
+ class GateDecision(str, Enum):
11
+ ALLOW = "ALLOW"
12
+ DENY = "DENY"
13
+ ASK = "ASK"
14
+ STEER = "STEER"
15
+
16
+
17
+ @dataclass
18
+ class SimulatedStateDelta:
19
+ """Generic projected-outcome envelope. Domain simulators subclass or
20
+ populate this with their own numeric_deltas / side-effect semantics."""
21
+ tool_name: str
22
+ proposed_args: Dict[str, Any]
23
+ numeric_deltas: Dict[str, float] = field(default_factory=dict)
24
+ irreversibility_score: float = 0.0 # 0.0 fully reversible -> 1.0 irreversible
25
+ confidence: float = 0.0 # 0.0 -> 1.0, simulator's own confidence in this projection
26
+ simulated_side_effects: List[str] = field(default_factory=list)
27
+ natural_key: Optional[str] = None # stable identity for idempotency (NOT a random token)
28
+
29
+
30
+ @dataclass
31
+ class EvaluationResult:
32
+ decision: GateDecision
33
+ confidence: float
34
+ reason: str
35
+ steer_payload: Optional[Dict[str, Any]] = None
File without changes
@@ -0,0 +1,60 @@
1
+ """
2
+ Example: Use consequence-gate with LangGraph agents via middleware.
3
+
4
+ Usage:
5
+ python -m consequence_gate.integrations.examples.run_langgraph
6
+ """
7
+
8
+ import sys
9
+ from pathlib import Path
10
+
11
+ # Add parent directory to path for imports
12
+ sys.path.insert(0, str(Path(__file__).parent.parent.parent))
13
+
14
+ from langchain.agents import create_agent
15
+ from langchain_core.tools import tool
16
+
17
+ from consequence_gate.integrations.langgraph_hook import create_financial_gate_middleware
18
+
19
+
20
+ @tool
21
+ def process_claim(amount: float, claim_id: str, currency: str = "INR", payout_method: str = "standard_ach") -> str:
22
+ """Process a claim payout."""
23
+ return f"Processed claim {claim_id} for {amount} {currency} via {payout_method}"
24
+
25
+
26
+ def main():
27
+ # Create middleware
28
+ middleware = create_financial_gate_middleware(
29
+ daily_tier_limit_inr=25000.0,
30
+ instant_wire_threshold=10000.0,
31
+ max_retries=2,
32
+ context_provider=lambda state: {
33
+ "account_rolling_24h_spend": 0.0,
34
+ "kyc_verified": True,
35
+ },
36
+ )
37
+
38
+ # Create agent with middleware
39
+ agent = create_agent(
40
+ model="claude-sonnet-4",
41
+ tools=[process_claim],
42
+ middleware=[middleware],
43
+ )
44
+
45
+ print("LangGraph example:")
46
+ print("Agent created with consequence-gate middleware")
47
+ print()
48
+ print("Try this invocation:")
49
+ print('agent.invoke({"messages": [("user", "Process claim for 50,000 INR")]})')
50
+ print()
51
+ print("Expected behavior:")
52
+ print("- The agent will propose a tool call: process_claim(amount=50000, ...)")
53
+ print("- The middleware will intercept and simulate the outcome")
54
+ print("- Since 50,000 INR exceeds the 25,000 INR tier limit, the middleware will return:")
55
+ print(' ToolMessage(content="STEER_GUIDANCE: Cannot process full 50,000 INR...")')
56
+ print("- The agent will see this as the tool result and can retry with a safer alternative")
57
+
58
+
59
+ if __name__ == "__main__":
60
+ main()
@@ -0,0 +1,100 @@
1
+ """
2
+ Example: Run consequence-gate as an MCP proxy.
3
+
4
+ This script demonstrates how to run the MCP proxy in front of a downstream
5
+ MCP server. The proxy intercepts tools/call requests, runs consequence
6
+ simulation, and either allows, denies, asks, or steers the call.
7
+
8
+ Usage:
9
+ # Financial disbursement gate
10
+ python -m consequence_gate.integrations.examples.run_mcp_proxy financial \\
11
+ --downstream-command "npx -y @modelcontextprotocol/server-postgres postgresql://localhost/mydb" \\
12
+ --daily-tier-limit 25000 \\
13
+ --instant-wire-threshold 10000
14
+
15
+ # Database deletion gate
16
+ python -m consequence_gate.integrations.examples.run_mcp_proxy database \\
17
+ --downstream-command "npx -y @modelcontextprotocol/server-postgres postgresql://localhost/mydb" \\
18
+ --max-autonomous-delete-rows 100
19
+
20
+ # Communications blast gate
21
+ python -m consequence_gate.integrations.examples.run_mcp_proxy communications \\
22
+ --downstream-command "npx -y @modelcontextprotocol/server-sendgrid" \\
23
+ --max-autonomous-recipients 10000
24
+ """
25
+
26
+ import argparse
27
+ import sys
28
+ from pathlib import Path
29
+
30
+ from consequence_gate.integrations.mcp_proxy import (
31
+ create_financial_mcp_proxy,
32
+ create_database_mcp_proxy,
33
+ create_communications_mcp_proxy,
34
+ )
35
+
36
+
37
+ def parse_args():
38
+ parser = argparse.ArgumentParser(description="Run consequence-gate MCP proxy")
39
+ subparsers = parser.add_subparsers(dest="domain", required=True)
40
+
41
+ # Financial subcommand
42
+ financial_parser = subparsers.add_parser("financial", help="Financial disbursement gate")
43
+ financial_parser.add_argument("--downstream-command", required=True, help="Downstream MCP server command")
44
+ financial_parser.add_argument("--daily-tier-limit", type=float, default=25000.0)
45
+ financial_parser.add_argument("--instant-wire-threshold", type=float, default=10000.0)
46
+ financial_parser.add_argument("--max-retries", type=int, default=2)
47
+
48
+ # Database subcommand
49
+ db_parser = subparsers.add_parser("database", help="Database deletion gate")
50
+ db_parser.add_argument("--downstream-command", required=True, help="Downstream MCP server command")
51
+ db_parser.add_argument("--max-autonomous-delete-rows", type=int, default=100)
52
+ db_parser.add_argument("--max-retries", type=int, default=2)
53
+
54
+ # Communications subcommand
55
+ comm_parser = subparsers.add_parser("communications", help="Communications blast gate")
56
+ comm_parser.add_argument("--downstream-command", required=True, help="Downstream MCP server command")
57
+ comm_parser.add_argument("--max-autonomous-recipients", type=int, default=10000)
58
+ comm_parser.add_argument("--canary-min-size", type=int, default=100)
59
+ comm_parser.add_argument("--max-retries", type=int, default=2)
60
+
61
+ return parser.parse_args()
62
+
63
+
64
+ def main():
65
+ args = parse_args()
66
+
67
+ downstream_command = args.downstream_command.split()
68
+
69
+ if args.domain == "financial":
70
+ proxy = create_financial_mcp_proxy(
71
+ downstream_command=downstream_command,
72
+ daily_tier_limit_inr=args.daily_tier_limit,
73
+ instant_wire_threshold=args.instant_wire_threshold,
74
+ max_retries=args.max_retries,
75
+ )
76
+ elif args.domain == "database":
77
+ proxy = create_database_mcp_proxy(
78
+ downstream_command=downstream_command,
79
+ max_autonomous_delete_rows=args.max_autonomous_delete_rows,
80
+ max_retries=args.max_retries,
81
+ )
82
+ elif args.domain == "communications":
83
+ proxy = create_communications_mcp_proxy(
84
+ downstream_command=downstream_command,
85
+ max_autonomous_recipients=args.max_autonomous_recipients,
86
+ canary_min_size=args.canary_min_size,
87
+ max_retries=args.max_retries,
88
+ )
89
+ else:
90
+ raise ValueError(f"Unknown domain: {args.domain}")
91
+
92
+ print(f"Starting consequence-gate MCP proxy ({args.domain} domain)...", file=sys.stderr)
93
+ print(f"Downstream command: {' '.join(downstream_command)}", file=sys.stderr)
94
+ print("Reading from stdin, writing to stdout...", file=sys.stderr)
95
+
96
+ proxy.run()
97
+
98
+
99
+ if __name__ == "__main__":
100
+ main()
@@ -0,0 +1,197 @@
1
+ """
2
+ LangGraph integration: middleware that wraps tool calls with consequence gating.
3
+
4
+ Two patterns supported:
5
+ 1. @wrap_tool_call decorator - intercepts tool execution in LangGraph agents
6
+ 2. Custom ToolNode wrapper - for StateGraph workflows with explicit ToolNode
7
+
8
+ Reference:
9
+ - Agent Middleware: https://www.langchain.com/blog/agent-middleware
10
+ - wrap_tool_call: https://mcpservers.org/agent-skills/langchain-ai/langchain-middleware
11
+ - ToolNode: https://reference.langchain.com/python/langgraph.prebuilt/tool_node/ToolNode
12
+ """
13
+
14
+ from typing import Any, Callable, Dict, Optional
15
+ import json
16
+
17
+ try:
18
+ from langchain_core.messages import ToolMessage
19
+ from langchain.tools.tool_node import ToolCallRequest
20
+ except ImportError:
21
+ ToolMessage = None
22
+ ToolCallRequest = None
23
+
24
+ from ..core.models import GateDecision, EvaluationResult
25
+ from ..core.circuit_breaker import SteerCircuitBreaker
26
+ from ..simulators.financial import FinancialDeltaPredictor
27
+ from ..simulators.database import DataDeletionSimulator
28
+ from ..simulators.communications import OutboundCommunicationSimulator
29
+
30
+
31
+ def create_consequence_middleware(
32
+ simulator_fn: Callable[[str, Dict[str, Any], Dict[str, Any]], Any],
33
+ evaluator_fn: Callable[[Any, SteerCircuitBreaker], EvaluationResult],
34
+ circuit_breaker: Optional[SteerCircuitBreaker] = None,
35
+ context_provider: Optional[Callable[[Dict[str, Any]], Dict[str, Any]]] = None,
36
+ ):
37
+ """
38
+ Factory for creating a LangGraph middleware that wraps tool calls.
39
+
40
+ Usage:
41
+ from langchain.agents import create_agent
42
+ from consequence_gate.integrations.langgraph_hook import create_consequence_middleware
43
+
44
+ middleware = create_consequence_middleware(
45
+ simulator_fn=financial_simulator,
46
+ evaluator_fn=evaluator,
47
+ circuit_breaker=SteerCircuitBreaker(),
48
+ )
49
+
50
+ agent = create_agent(
51
+ model="claude-sonnet-4",
52
+ tools=[my_tool],
53
+ middleware=[middleware],
54
+ )
55
+ """
56
+ from langchain.agents.middleware import wrap_tool_call
57
+
58
+ breaker = circuit_breaker or SteerCircuitBreaker(max_retries=2)
59
+ context_fn = context_provider or (lambda state: {})
60
+
61
+ @wrap_tool_call
62
+ def consequence_gate_wrapper(request: ToolCallRequest, handler):
63
+ """
64
+ Wrap tool calls to run consequence simulation before execution.
65
+
66
+ Args:
67
+ request: ToolCallRequest with tool_call dict and state
68
+ handler: function to call to actually execute the tool
69
+
70
+ Returns:
71
+ Tool execution result, or raises exception for DENY,
72
+ or returns ToolMessage with guidance for STEER.
73
+ """
74
+ tool_call = request.tool_call
75
+ tool_name = tool_call.get("name", "unknown")
76
+ arguments = tool_call.get("args", {})
77
+ state = request.state or {}
78
+ context = context_fn(state)
79
+
80
+ # Extract natural key for idempotency
81
+ natural_key = arguments.get("claim_id") or arguments.get("transaction_ref") or f"{tool_name}:{json.dumps(arguments, sort_keys=True)}"
82
+
83
+ # Run simulation + evaluation
84
+ delta = simulator_fn(tool_name, arguments, context)
85
+ result = evaluator_fn(delta, breaker)
86
+
87
+ if result.decision == GateDecision.ALLOW:
88
+ # Pass through - tool executes normally
89
+ return handler(request)
90
+
91
+ if result.decision == GateDecision.DENY:
92
+ # Hard block - raise exception that becomes a ToolMessage error
93
+ raise ValueError(f"BLOCKED: {result.reason}")
94
+
95
+ if result.decision == GateDecision.ASK:
96
+ # Human approval required - for now, raise exception with escalation message
97
+ # In production, this would integrate with LangGraph's interrupt() or a human-in-the-loop system
98
+ raise ValueError(f"ESCALATION_REQUIRED: {result.reason}")
99
+
100
+ if result.decision == GateDecision.STEER:
101
+ # Return ToolMessage with guidance - agent sees this as the tool result
102
+ steer_payload = result.steer_payload or {}
103
+ guidance = steer_payload.get("guidance", result.reason)
104
+ suggested_tool = steer_payload.get("suggested_tool")
105
+ suggested_args = steer_payload.get("suggested_args", {})
106
+ idempotency_key = suggested_args.get("idempotency_key")
107
+ if idempotency_key:
108
+ guidance += f" [idempotency_key={idempotency_key}]"
109
+
110
+ error_text = (
111
+ f"STEER_GUIDANCE: {guidance}\\n"
112
+ f"Suggested alternative: {suggested_tool} with args {suggested_args}"
113
+ )
114
+ return ToolMessage(content=error_text, tool_call_id=tool_call.get("id", ""), name=tool_name, status="error")
115
+
116
+ # Should not reach here
117
+ return handler(request)
118
+
119
+ return consequence_gate_wrapper
120
+
121
+
122
+ # Convenience factory functions
123
+
124
+ def create_financial_gate_middleware(
125
+ daily_tier_limit_inr: float = 25000.0,
126
+ instant_wire_threshold: float = 10000.0,
127
+ max_retries: int = 2,
128
+ context_provider: Optional[Callable[[Dict[str, Any]], Dict[str, Any]]] = None,
129
+ ):
130
+ """Factory for financial gate middleware."""
131
+ predictor = FinancialDeltaPredictor(
132
+ daily_tier_limit_inr=daily_tier_limit_inr,
133
+ instant_wire_threshold=instant_wire_threshold,
134
+ )
135
+ breaker = SteerCircuitBreaker(max_retries=max_retries)
136
+
137
+ def evaluator(delta, circuit_breaker):
138
+ return predictor.evaluate(delta, circuit_breaker)
139
+
140
+ return create_consequence_middleware(
141
+ simulator_fn=predictor.simulate,
142
+ evaluator_fn=evaluator,
143
+ circuit_breaker=breaker,
144
+ context_provider=context_provider,
145
+ )
146
+
147
+
148
+ def create_database_gate_middleware(
149
+ max_autonomous_delete_rows: int = 100,
150
+ db_conn=None,
151
+ max_retries: int = 2,
152
+ context_provider: Optional[Callable[[Dict[str, Any]], Dict[str, Any]]] = None,
153
+ ):
154
+ """Factory for database gate middleware."""
155
+ simulator = DataDeletionSimulator(
156
+ max_autonomous_delete_rows=max_autonomous_delete_rows,
157
+ db_conn=db_conn,
158
+ )
159
+ breaker = SteerCircuitBreaker(max_retries=max_retries)
160
+
161
+ def evaluator(delta, circuit_breaker):
162
+ return simulator.evaluate(delta, circuit_breaker)
163
+
164
+ return create_consequence_middleware(
165
+ simulator_fn=simulator.simulate,
166
+ evaluator_fn=evaluator,
167
+ circuit_breaker=breaker,
168
+ context_provider=context_provider,
169
+ )
170
+
171
+
172
+ def create_communications_gate_middleware(
173
+ max_autonomous_recipients: int = 10000,
174
+ canary_min_size: int = 100,
175
+ canary_max_bounce_rate: float = 0.05,
176
+ canary_max_complaint_rate: float = 0.01,
177
+ max_retries: int = 2,
178
+ context_provider: Optional[Callable[[Dict[str, Any]], Dict[str, Any]]] = None,
179
+ ):
180
+ """Factory for communications gate middleware."""
181
+ simulator = OutboundCommunicationSimulator(
182
+ max_autonomous_recipients=max_autonomous_recipients,
183
+ canary_min_size=canary_min_size,
184
+ canary_max_bounce_rate=canary_max_bounce_rate,
185
+ canary_max_complaint_rate=canary_max_complaint_rate,
186
+ )
187
+ breaker = SteerCircuitBreaker(max_retries=max_retries)
188
+
189
+ def evaluator(delta, circuit_breaker):
190
+ return simulator.evaluate(delta, circuit_breaker)
191
+
192
+ return create_consequence_middleware(
193
+ simulator_fn=simulator.simulate,
194
+ evaluator_fn=evaluator,
195
+ circuit_breaker=breaker,
196
+ context_provider=context_provider,
197
+ )