cheqpoint 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.
cheqpoint/__init__.py ADDED
@@ -0,0 +1,25 @@
1
+ from .client import (
2
+ CheqpointClient,
3
+ CheqpointError,
4
+ RejectedError,
5
+ TimeoutError,
6
+ CheckpointResult,
7
+ RequestStatus,
8
+ )
9
+
10
+ __all__ = [
11
+ "CheqpointClient",
12
+ "CheqpointError",
13
+ "RejectedError",
14
+ "TimeoutError",
15
+ "CheckpointResult",
16
+ "RequestStatus",
17
+ ]
18
+
19
+ __version__ = "0.1.0"
20
+
21
+ # Framework-specific tools are importable directly but not auto-imported
22
+ # to avoid requiring optional dependencies at package load time.
23
+ # Usage:
24
+ # from cheqpoint.langchain_tool import CheqpointApprovalTool
25
+ # from cheqpoint.crewai_tool import CheqpointApprovalTool
cheqpoint/client.py ADDED
@@ -0,0 +1,437 @@
1
+ """Cheqpoint Python SDK — human-in-the-loop approval queues for AI agents."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+ from dataclasses import dataclass
7
+ from typing import Any, Optional
8
+
9
+ import requests as _requests
10
+
11
+ __version__ = "0.1.0"
12
+
13
+ DEFAULT_BASE_URL = "https://app.cheqpoint.io"
14
+ DEFAULT_POLL_INTERVAL = 3 # seconds
15
+ DEFAULT_TIMEOUT = 300 # seconds (5 minutes)
16
+
17
+
18
+ # ---------------------------------------------------------------------------
19
+ # Result types
20
+ # ---------------------------------------------------------------------------
21
+
22
+
23
+ @dataclass
24
+ class CheckpointResult:
25
+ id: str
26
+ status: str # "APPROVED" | "REJECTED"
27
+ details: dict[str, Any]
28
+ modified_details: Optional[dict[str, Any]]
29
+ response_notes: Optional[str]
30
+
31
+ @property
32
+ def effective_details(self) -> dict[str, Any]:
33
+ """Return modified_details if set, otherwise the original details."""
34
+ return self.modified_details if self.modified_details is not None else self.details
35
+
36
+
37
+ @dataclass
38
+ class RequestStatus:
39
+ id: str
40
+ status: str # "PENDING" | "APPROVED" | "REJECTED"
41
+ details: dict[str, Any]
42
+ modified_details: Optional[dict[str, Any]]
43
+ response_notes: Optional[str]
44
+ decision_reason_code: Optional[str]
45
+ decision_note: Optional[str]
46
+ auto_decided: bool
47
+ auto_decision_rule_name: Optional[str]
48
+ decided_at: Optional[str]
49
+
50
+
51
+ # ---------------------------------------------------------------------------
52
+ # Errors
53
+ # ---------------------------------------------------------------------------
54
+
55
+
56
+ class CheqpointError(Exception):
57
+ """Raised when the Cheqpoint API returns an error response."""
58
+
59
+ def __init__(self, message: str, status_code: int, response_body: Any = None) -> None:
60
+ super().__init__(message)
61
+ self.status_code = status_code
62
+ self.response_body = response_body
63
+
64
+
65
+ class RejectedError(Exception):
66
+ """Raised when a checkpoint request is rejected by a reviewer."""
67
+
68
+ def __init__(self, request_id: str, response_notes: Optional[str]) -> None:
69
+ note = f": {response_notes}" if response_notes else ""
70
+ super().__init__(f"Cheqpoint request {request_id} was rejected{note}")
71
+ self.request_id = request_id
72
+ self.response_notes = response_notes
73
+
74
+
75
+ class TimeoutError(Exception):
76
+ """Raised when polling exceeds the configured timeout."""
77
+
78
+ def __init__(self, request_id: str, timeout: float) -> None:
79
+ super().__init__(
80
+ f"Cheqpoint request {request_id} timed out after {timeout}s waiting for a decision"
81
+ )
82
+ self.request_id = request_id
83
+ self.timeout = timeout
84
+
85
+
86
+ # ---------------------------------------------------------------------------
87
+ # Client
88
+ # ---------------------------------------------------------------------------
89
+
90
+
91
+ class CheqpointClient:
92
+ """Client for the Cheqpoint human-in-the-loop approval API."""
93
+
94
+ def __init__(
95
+ self,
96
+ api_key: str,
97
+ base_url: str = DEFAULT_BASE_URL,
98
+ timeout: float = DEFAULT_TIMEOUT,
99
+ ) -> None:
100
+ self.api_key = api_key
101
+ self.base_url = base_url.rstrip("/")
102
+ self.default_timeout = timeout
103
+ self._session = _requests.Session()
104
+ self._session.headers.update(
105
+ {
106
+ "Content-Type": "application/json",
107
+ "User-Agent": f"cheqpoint-python/{__version__}",
108
+ }
109
+ )
110
+
111
+ # ------------------------------------------------------------------
112
+ # Public methods
113
+ # ------------------------------------------------------------------
114
+
115
+ def checkpoint(
116
+ self,
117
+ *,
118
+ action: str,
119
+ risk_score: Optional[float] = None,
120
+ summary: str,
121
+ details: dict[str, Any],
122
+ justification: Optional[str] = None,
123
+ webhook_url: Optional[str] = None,
124
+ trace: Optional[dict[str, Any]] = None,
125
+ poll_interval: float = DEFAULT_POLL_INTERVAL,
126
+ timeout: Optional[float] = None,
127
+ ) -> CheckpointResult:
128
+ """Submit an action for review and block until a human decides.
129
+
130
+ Args:
131
+ action: Action type (e.g. "send_email", "process_refund", "deploy_code").
132
+ risk_score: Optional float 0.0–1.0. Default: 0.5 (medium).
133
+ summary: One-sentence description shown to reviewers.
134
+ details: Structured payload for the action.
135
+ justification: Optional agent reasoning shown to reviewers.
136
+ webhook_url: URL for Cheqpoint to POST the decision to.
137
+ poll_interval: Seconds between status polls. Default 3.
138
+ timeout: Max seconds to wait. Default is client-level default (300).
139
+
140
+ Returns:
141
+ CheckpointResult with status "APPROVED".
142
+
143
+ Raises:
144
+ RejectedError: If the request is rejected.
145
+ TimeoutError: If no decision within `timeout` seconds.
146
+ CheqpointError: On API errors.
147
+ """
148
+ effective_timeout = timeout if timeout is not None else self.default_timeout
149
+
150
+ result = self.create_request(
151
+ action=action,
152
+ risk_score=risk_score,
153
+ summary=summary,
154
+ details=details,
155
+ justification=justification,
156
+ webhook_url=webhook_url,
157
+ trace=trace,
158
+ )
159
+ request_id = result["id"]
160
+ deadline = time.monotonic() + effective_timeout
161
+
162
+ while time.monotonic() < deadline:
163
+ status = self.get_request(request_id)
164
+
165
+ if status.status == "APPROVED":
166
+ return CheckpointResult(
167
+ id=status.id,
168
+ status="APPROVED",
169
+ details=status.details,
170
+ modified_details=status.modified_details,
171
+ response_notes=status.response_notes,
172
+ )
173
+
174
+ if status.status == "REJECTED":
175
+ raise RejectedError(request_id, status.response_notes)
176
+
177
+ remaining = deadline - time.monotonic()
178
+ if remaining <= 0:
179
+ break
180
+ time.sleep(min(poll_interval, remaining))
181
+
182
+ raise TimeoutError(request_id, effective_timeout)
183
+
184
+ def create_request(
185
+ self,
186
+ *,
187
+ action: str,
188
+ risk_score: Optional[float] = None,
189
+ summary: str,
190
+ details: dict[str, Any],
191
+ justification: Optional[str] = None,
192
+ webhook_url: Optional[str] = None,
193
+ trace: Optional[dict[str, Any]] = None,
194
+ ) -> dict[str, str]:
195
+ """Fire-and-forget: submit a request and return immediately with its ID.
196
+
197
+ Returns:
198
+ dict with "id" key containing the request ID.
199
+ """
200
+ body: dict[str, Any] = {
201
+ "apiKey": self.api_key,
202
+ "action": action,
203
+ "riskScore": risk_score,
204
+ "summary": summary,
205
+ "details": details,
206
+ }
207
+ if justification is not None:
208
+ body["justification"] = justification
209
+ if webhook_url is not None:
210
+ body["webhookUrl"] = webhook_url
211
+ if trace is not None:
212
+ body["trace"] = trace
213
+
214
+ data = self._post("/api/approvals/request", body)
215
+ return {"id": data["id"]}
216
+
217
+ def get_request(self, request_id: str) -> RequestStatus:
218
+ """Fetch the current status of a request by its ID."""
219
+ data = self._get(f"/api/approvals/{request_id}")
220
+ return RequestStatus(
221
+ id=data["id"],
222
+ status=data["status"],
223
+ details=data["details"],
224
+ modified_details=data.get("modifiedDetails"),
225
+ response_notes=data.get("responseNotes"),
226
+ decision_reason_code=data.get("decisionReasonCode"),
227
+ decision_note=data.get("decisionNote"),
228
+ auto_decided=data.get("autoDecided", False),
229
+ auto_decision_rule_name=data.get("autoDecisionRuleName"),
230
+ decided_at=data.get("decidedAt"),
231
+ )
232
+
233
+ # ------------------------------------------------------------------
234
+ # Instrumentation wrappers
235
+ # ------------------------------------------------------------------
236
+
237
+ def wrap_openai(self, openai_client: Any) -> tuple[Any, "TraceCollector"]:
238
+ """Wrap an OpenAI client to auto-capture traces.
239
+
240
+ Usage::
241
+
242
+ client, collector = cheq.wrap_openai(OpenAI(api_key=...))
243
+ response = client.chat.completions.create(model="gpt-4o", messages=[...])
244
+ cheq.checkpoint(..., trace=collector.get_trace())
245
+ """
246
+ return _wrap_openai(openai_client)
247
+
248
+ def wrap_anthropic(self, anthropic_client: Any) -> tuple[Any, "TraceCollector"]:
249
+ """Wrap an Anthropic client to auto-capture traces.
250
+
251
+ Usage::
252
+
253
+ client, collector = cheq.wrap_anthropic(Anthropic(api_key=...))
254
+ response = client.messages.create(model="claude-opus-4-6", messages=[...])
255
+ cheq.checkpoint(..., trace=collector.get_trace())
256
+ """
257
+ return _wrap_anthropic(anthropic_client)
258
+
259
+ # ------------------------------------------------------------------
260
+ # Internal helpers
261
+ # ------------------------------------------------------------------
262
+
263
+ def _post(self, path: str, body: dict[str, Any]) -> Any:
264
+ url = f"{self.base_url}{path}"
265
+ resp = self._session.post(url, json=body)
266
+ return self._handle_response(resp)
267
+
268
+ def _get(self, path: str) -> Any:
269
+ url = f"{self.base_url}{path}"
270
+ resp = self._session.get(url)
271
+ return self._handle_response(resp)
272
+
273
+ def _handle_response(self, resp: _requests.Response) -> Any:
274
+ if not resp.ok:
275
+ body: Any = None
276
+ message = f"HTTP {resp.status_code}"
277
+ try:
278
+ body = resp.json()
279
+ if isinstance(body, dict) and "error" in body:
280
+ message = body["error"]
281
+ except Exception:
282
+ pass
283
+ raise CheqpointError(message, resp.status_code, body)
284
+ return resp.json()
285
+
286
+
287
+ # ---------------------------------------------------------------------------
288
+ # Trace instrumentation helpers
289
+ # ---------------------------------------------------------------------------
290
+
291
+
292
+ from datetime import datetime, timezone
293
+
294
+
295
+ class TraceCollector:
296
+ """Accumulates trace steps; call get_trace() to build the TraceData dict."""
297
+
298
+ def __init__(self) -> None:
299
+ self._steps: list[dict[str, str]] = []
300
+ self._model: str = ""
301
+ self._total_tokens: int = 0
302
+
303
+ def _now(self) -> str:
304
+ return datetime.now(tz=timezone.utc).isoformat()
305
+
306
+ def add_step(self, type: str, content: str) -> None:
307
+ self._steps.append({"type": type, "content": content, "timestamp": self._now()})
308
+
309
+ def get_trace(self) -> dict[str, Any]:
310
+ trace: dict[str, Any] = {"steps": list(self._steps)}
311
+ if self._model:
312
+ trace["model"] = self._model
313
+ if self._total_tokens:
314
+ trace["totalTokens"] = self._total_tokens
315
+ return trace
316
+
317
+
318
+ class _OpenAIWrapper:
319
+ """Thin proxy around an OpenAI client that records chat completions."""
320
+
321
+ def __init__(self, client: Any, collector: TraceCollector) -> None:
322
+ self._client = client
323
+ self._collector = collector
324
+
325
+ def __getattr__(self, name: str) -> Any:
326
+ if name == "chat":
327
+ return _OpenAIChatProxy(self._client.chat, self._collector)
328
+ return getattr(self._client, name)
329
+
330
+
331
+ class _OpenAIChatProxy:
332
+ def __init__(self, chat: Any, collector: TraceCollector) -> None:
333
+ self._chat = chat
334
+ self._collector = collector
335
+
336
+ @property
337
+ def completions(self) -> "_OpenAICompletionsProxy":
338
+ return _OpenAICompletionsProxy(self._chat.completions, self._collector)
339
+
340
+
341
+ class _OpenAICompletionsProxy:
342
+ def __init__(self, completions: Any, collector: TraceCollector) -> None:
343
+ self._completions = completions
344
+ self._collector = collector
345
+
346
+ def create(self, **kwargs: Any) -> Any:
347
+ if "model" in kwargs:
348
+ self._collector._model = kwargs["model"]
349
+ for msg in kwargs.get("messages", []):
350
+ role = msg.get("role", "")
351
+ content = msg.get("content", "")
352
+ if role == "assistant":
353
+ self._collector.add_step("thought", str(content))
354
+ elif role == "tool":
355
+ self._collector.add_step("observation", str(content))
356
+ if "tools" in kwargs:
357
+ self._collector.add_step("tool_call", str(kwargs["tools"])[:200])
358
+
359
+ result = self._completions.create(**kwargs)
360
+
361
+ usage = getattr(result, "usage", None)
362
+ if usage and hasattr(usage, "total_tokens") and usage.total_tokens:
363
+ self._collector._total_tokens += usage.total_tokens
364
+
365
+ choices = getattr(result, "choices", []) or []
366
+ if choices:
367
+ msg = getattr(choices[0], "message", None)
368
+ if msg:
369
+ if getattr(msg, "content", None):
370
+ self._collector.add_step("thought", msg.content)
371
+ tool_calls = getattr(msg, "tool_calls", None) or []
372
+ for tc in tool_calls:
373
+ fn = getattr(tc, "function", None)
374
+ name = getattr(fn, "name", "tool") if fn else "tool"
375
+ args = getattr(fn, "arguments", "") if fn else ""
376
+ self._collector.add_step("tool_call", f"{name}({args})")
377
+
378
+ return result
379
+
380
+
381
+ class _AnthropicWrapper:
382
+ """Thin proxy around an Anthropic client that records message calls."""
383
+
384
+ def __init__(self, client: Any, collector: TraceCollector) -> None:
385
+ self._client = client
386
+ self._collector = collector
387
+
388
+ def __getattr__(self, name: str) -> Any:
389
+ if name == "messages":
390
+ return _AnthropicMessagesProxy(self._client.messages, self._collector)
391
+ return getattr(self._client, name)
392
+
393
+
394
+ class _AnthropicMessagesProxy:
395
+ def __init__(self, messages: Any, collector: TraceCollector) -> None:
396
+ self._messages = messages
397
+ self._collector = collector
398
+
399
+ def create(self, **kwargs: Any) -> Any:
400
+ if "model" in kwargs:
401
+ self._collector._model = kwargs["model"]
402
+ for msg in kwargs.get("messages", []):
403
+ role = msg.get("role", "")
404
+ content = msg.get("content", "")
405
+ if role == "assistant":
406
+ self._collector.add_step("thought", str(content))
407
+ elif role == "user":
408
+ self._collector.add_step("observation", str(content))
409
+
410
+ result = self._messages.create(**kwargs)
411
+
412
+ usage = getattr(result, "usage", None)
413
+ if usage:
414
+ inp = getattr(usage, "input_tokens", 0) or 0
415
+ out = getattr(usage, "output_tokens", 0) or 0
416
+ self._collector._total_tokens += inp + out
417
+
418
+ for block in getattr(result, "content", []) or []:
419
+ btype = getattr(block, "type", "")
420
+ if btype == "text":
421
+ self._collector.add_step("thought", getattr(block, "text", ""))
422
+ elif btype == "tool_use":
423
+ name = getattr(block, "name", "tool")
424
+ inp = getattr(block, "input", {})
425
+ self._collector.add_step("tool_call", f"{name}({inp})")
426
+
427
+ return result
428
+
429
+
430
+ def _wrap_openai(client: Any) -> tuple[Any, TraceCollector]:
431
+ collector = TraceCollector()
432
+ return _OpenAIWrapper(client, collector), collector
433
+
434
+
435
+ def _wrap_anthropic(client: Any) -> tuple[Any, TraceCollector]:
436
+ collector = TraceCollector()
437
+ return _AnthropicWrapper(client, collector), collector
@@ -0,0 +1,86 @@
1
+ """Cheqpoint CrewAI integration — human-in-the-loop approval tool."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Optional
6
+
7
+ from .client import CheqpointClient
8
+
9
+ try:
10
+ from crewai.tools import BaseTool
11
+ from pydantic import BaseModel, Field
12
+ except ImportError as e: # pragma: no cover
13
+ raise ImportError(
14
+ "crewai is required for CheqpointApprovalTool. "
15
+ "Install it with: pip install cheqpoint[crewai]"
16
+ ) from e
17
+
18
+
19
+ class _ApprovalInput(BaseModel):
20
+ action: str = Field(
21
+ description="Category of the action (e.g. 'send_email', 'process_refund', 'deploy_code')"
22
+ )
23
+ summary: str = Field(description="One-sentence description shown to the reviewer")
24
+ details: dict[str, Any] = Field(description="Full structured payload for the action")
25
+ risk_score: float = Field(
26
+ default=0.5, ge=0.0, le=1.0, description="Risk score 0.0–1.0"
27
+ )
28
+ justification: Optional[str] = Field(
29
+ default=None, description="Optional agent reasoning for the reviewer"
30
+ )
31
+
32
+
33
+ class CheqpointApprovalTool(BaseTool):
34
+ """CrewAI tool that pauses agent execution and waits for human approval.
35
+
36
+ Usage::
37
+
38
+ from crewai import Agent, Task, Crew
39
+ from cheqpoint.crewai_tool import CheqpointApprovalTool
40
+
41
+ approval_tool = CheqpointApprovalTool(api_key="ck_...")
42
+
43
+ agent = Agent(
44
+ role="Financial Analyst",
45
+ goal="Process refunds with human oversight",
46
+ backstory="You process refunds and escalate risky ones for approval.",
47
+ tools=[approval_tool],
48
+ )
49
+ """
50
+
51
+ name: str = "request_human_approval"
52
+ description: str = (
53
+ "Request human approval before taking a consequential action. "
54
+ "Call this tool when an action needs human review before execution. "
55
+ "Provide action type, a clear summary, and full details. "
56
+ "The tool blocks until a human approves or rejects the request."
57
+ )
58
+ args_schema: type[BaseModel] = _ApprovalInput
59
+
60
+ _client: CheqpointClient
61
+
62
+ def __init__(self, api_key: str, base_url: str = "https://app.cheqpoint.io", **kwargs: Any) -> None:
63
+ super().__init__(**kwargs)
64
+ object.__setattr__(self, "_client", CheqpointClient(api_key=api_key, base_url=base_url))
65
+
66
+ def _run(
67
+ self,
68
+ action: str,
69
+ summary: str,
70
+ details: dict[str, Any],
71
+ risk_score: float = 0.5,
72
+ justification: Optional[str] = None,
73
+ ) -> str:
74
+ """Execute the approval request and return the decision as a string."""
75
+ try:
76
+ result = self._client.checkpoint(
77
+ action=action,
78
+ summary=summary,
79
+ details=details,
80
+ risk_score=risk_score,
81
+ justification=justification,
82
+ )
83
+ notes = result.response_notes or "Approved by reviewer"
84
+ return f"APPROVED: {notes}"
85
+ except Exception as exc:
86
+ return f"REJECTED: {exc}"
@@ -0,0 +1,109 @@
1
+ """Cheqpoint LangChain integration — human-in-the-loop approval tool."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Optional, Type
6
+
7
+ from .client import CheqpointClient
8
+
9
+ try:
10
+ from langchain.tools import BaseTool
11
+ from pydantic import BaseModel, Field
12
+ except ImportError as e: # pragma: no cover
13
+ raise ImportError(
14
+ "langchain is required for CheqpointApprovalTool. "
15
+ "Install it with: pip install cheqpoint[langchain]"
16
+ ) from e
17
+
18
+
19
+ class _CheqpointInput(BaseModel):
20
+ action: str = Field(
21
+ description="Category of the action requiring approval (e.g. 'send_email', 'process_refund', 'deploy_code')"
22
+ )
23
+ summary: str = Field(
24
+ description="One-sentence description shown to the human reviewer"
25
+ )
26
+ details: dict[str, Any] = Field(
27
+ description="Structured payload describing the action in detail"
28
+ )
29
+ risk_score: float = Field(
30
+ default=0.5,
31
+ ge=0.0,
32
+ le=1.0,
33
+ description="Risk score from 0.0 (low) to 1.0 (critical). Default: 0.5",
34
+ )
35
+ justification: Optional[str] = Field(
36
+ default=None,
37
+ description="Optional agent reasoning shown to the reviewer",
38
+ )
39
+
40
+
41
+ class CheqpointApprovalTool(BaseTool):
42
+ """LangChain tool that pauses agent execution and waits for human approval.
43
+
44
+ Usage::
45
+
46
+ from langchain_openai import ChatOpenAI
47
+ from langchain.agents import create_tool_calling_agent, AgentExecutor
48
+ from cheqpoint.langchain_tool import CheqpointApprovalTool
49
+
50
+ tool = CheqpointApprovalTool(api_key="ck_...")
51
+ llm = ChatOpenAI(model="gpt-4o")
52
+ agent = create_tool_calling_agent(llm, [tool], prompt)
53
+ executor = AgentExecutor(agent=agent, tools=[tool])
54
+ """
55
+
56
+ name: str = "request_human_approval"
57
+ description: str = (
58
+ "Request human approval before taking a consequential action. "
59
+ "Use this tool when the action could have significant impact and requires "
60
+ "a human reviewer to approve before you proceed. "
61
+ "The tool blocks until a decision is made."
62
+ )
63
+ args_schema: Type[BaseModel] = _CheqpointInput
64
+
65
+ # Pydantic excludes the client field from serialisation
66
+ _client: CheqpointClient
67
+
68
+ def __init__(self, api_key: str, base_url: str = "https://app.cheqpoint.io", **kwargs: Any) -> None:
69
+ super().__init__(**kwargs)
70
+ object.__setattr__(self, "_client", CheqpointClient(api_key=api_key, base_url=base_url))
71
+
72
+ def _run(
73
+ self,
74
+ action: str,
75
+ summary: str,
76
+ details: dict[str, Any],
77
+ risk_score: float = 0.5,
78
+ justification: Optional[str] = None,
79
+ ) -> str:
80
+ """Synchronous execution — submits the request and polls for decision."""
81
+ try:
82
+ result = self._client.checkpoint(
83
+ action=action,
84
+ summary=summary,
85
+ details=details,
86
+ risk_score=risk_score,
87
+ justification=justification,
88
+ )
89
+ notes = result.response_notes or "Approved by reviewer"
90
+ return f"APPROVED: {notes}"
91
+ except Exception as exc:
92
+ return f"REJECTED: {exc}"
93
+
94
+ async def _arun(
95
+ self,
96
+ action: str,
97
+ summary: str,
98
+ details: dict[str, Any],
99
+ risk_score: float = 0.5,
100
+ justification: Optional[str] = None,
101
+ ) -> str:
102
+ """Async execution delegates to the sync implementation (polling is blocking)."""
103
+ return self._run(
104
+ action=action,
105
+ summary=summary,
106
+ details=details,
107
+ risk_score=risk_score,
108
+ justification=justification,
109
+ )
@@ -0,0 +1,237 @@
1
+ Metadata-Version: 2.4
2
+ Name: cheqpoint
3
+ Version: 0.1.0
4
+ Summary: Human-in-the-loop approval SDK for AI agents
5
+ Home-page: https://cheqpoint.io
6
+ Author: Cheqpoint
7
+ Keywords: cheqpoint,human-in-the-loop,ai,agents,approval
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.9
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
+ Requires-Python: >=3.9
18
+ Description-Content-Type: text/markdown
19
+ Requires-Dist: requests>=2.28.0
20
+ Dynamic: author
21
+ Dynamic: classifier
22
+ Dynamic: description
23
+ Dynamic: description-content-type
24
+ Dynamic: home-page
25
+ Dynamic: keywords
26
+ Dynamic: requires-dist
27
+ Dynamic: requires-python
28
+ Dynamic: summary
29
+
30
+ # cheqpoint
31
+
32
+ Official Python SDK for [Cheqpoint](https://cheqpoint.co) — human-in-the-loop approval queues for AI agents.
33
+
34
+ ## Installation
35
+
36
+ ```bash
37
+ pip install cheqpoint
38
+ ```
39
+
40
+ ## Quick start
41
+
42
+ ```python
43
+ import os
44
+ from cheqpoint import CheqpointClient
45
+
46
+ client = CheqpointClient(api_key=os.environ["CHEQPOINT_API_KEY"])
47
+
48
+ # Your agent calls this instead of executing directly.
49
+ # checkpoint() submits the request and waits for a human decision.
50
+ result = client.checkpoint(
51
+ type="refund",
52
+ risk_level="high",
53
+ summary="Refund $149 to sarah@example.com",
54
+ details={"userId": "usr_123", "amount": 149, "currency": "USD"},
55
+ justification="Double-charged on invoice #1821",
56
+ )
57
+
58
+ # effective_details returns modifiedDetails if set, else original details
59
+ stripe.refunds.create(**result.effective_details)
60
+ ```
61
+
62
+ ## Constructor
63
+
64
+ ```python
65
+ CheqpointClient(
66
+ api_key: str, # Required. Your workspace API key.
67
+ base_url: str = "https://app.cheqpoint.co",
68
+ timeout: float = 300, # Default poll timeout in seconds
69
+ )
70
+ ```
71
+
72
+ ## Methods
73
+
74
+ ### `checkpoint(...)` → `CheckpointResult`
75
+
76
+ Submit an action for review and **block** until a human decides.
77
+
78
+ | Parameter | Type | Default | Description |
79
+ |---|---|---|---|
80
+ | `type` | `str` | required | Short label for the action (e.g. `"refund"`, `"email"`) |
81
+ | `risk_level` | `"low" \| "medium" \| "high"` | required | Risk level of the action |
82
+ | `summary` | `str` | required | One-sentence description shown to reviewers |
83
+ | `details` | `dict` | required | Structured payload. Returned as-is (or modified) on approval |
84
+ | `justification` | `str \| None` | `None` | Agent's reasoning shown to reviewers |
85
+ | `webhook_url` | `str \| None` | `None` | URL for Cheqpoint to POST the decision to |
86
+ | `poll_interval` | `float` | `3` | Seconds between status polls |
87
+ | `timeout` | `float \| None` | client default | Max seconds to wait |
88
+
89
+ **Returns** `CheckpointResult` when approved.
90
+ **Raises** `RejectedError` if rejected.
91
+ **Raises** `TimeoutError` if no decision within timeout.
92
+
93
+ ```python
94
+ @dataclass
95
+ class CheckpointResult:
96
+ id: str
97
+ status: str # "APPROVED"
98
+ details: dict # original payload
99
+ modified_details: dict | None # reviewer edits, if any
100
+ response_notes: str | None # reviewer's note
101
+
102
+ def effective_details(self) -> dict:
103
+ """Returns modified_details if set, else original details."""
104
+ ```
105
+
106
+ ### `create_request(...)` → `dict`
107
+
108
+ Fire-and-forget. Submits the request and returns immediately with `{"id": "..."}`. Pair with a `webhook_url` or poll manually with `get_request`.
109
+
110
+ ### `get_request(request_id)` → `RequestStatus`
111
+
112
+ Fetch the current status of a request.
113
+
114
+ ```python
115
+ @dataclass
116
+ class RequestStatus:
117
+ id: str
118
+ status: str # "PENDING" | "APPROVED" | "REJECTED"
119
+ type: str
120
+ risk_level: str
121
+ summary: str
122
+ details: dict
123
+ modified_details: dict | None
124
+ response_notes: str | None
125
+ webhook_delivered: bool
126
+ created_at: str
127
+ decided_at: str | None
128
+ ```
129
+
130
+ ## Error handling
131
+
132
+ ```python
133
+ from cheqpoint import CheqpointClient, CheqpointError, RejectedError, TimeoutError
134
+
135
+ try:
136
+ result = client.checkpoint(...)
137
+ except RejectedError as e:
138
+ print(f"Rejected: {e.response_notes}")
139
+ except TimeoutError as e:
140
+ print(f"Timed out for request {e.request_id}")
141
+ except CheqpointError as e:
142
+ print(f"API error {e.status_code}: {e}")
143
+ ```
144
+
145
+ ## Examples
146
+
147
+ ### With webhook (recommended for production)
148
+
149
+ ```python
150
+ result = client.create_request(
151
+ type="db-write",
152
+ risk_level="medium",
153
+ summary="Delete user account usr_456",
154
+ details={"userId": "usr_456", "reason": "GDPR deletion request"},
155
+ webhook_url="https://yourapp.com/webhook/cheqpoint",
156
+ )
157
+ request_id = result["id"]
158
+ # Store request_id — your Flask/Django webhook handler receives the decision
159
+ ```
160
+
161
+ ### Manual polling
162
+
163
+ ```python
164
+ result = client.create_request(type="email", risk_level="low", ...)
165
+ request_id = result["id"]
166
+
167
+ # Check later
168
+ status = client.get_request(request_id)
169
+ if status.status == "APPROVED":
170
+ payload = status.modified_details or status.details
171
+ send_email(**payload)
172
+ ```
173
+
174
+ ### LangChain agent tool
175
+
176
+ ```python
177
+ from langchain_core.tools import tool
178
+
179
+ @tool
180
+ def issue_refund(user_id: str, amount: float, reason: str) -> str:
181
+ """Issue a refund to a customer. Requires human approval."""
182
+ result = client.checkpoint(
183
+ type="refund",
184
+ risk_level="high",
185
+ summary=f"Refund ${amount} to user {user_id}",
186
+ details={"userId": user_id, "amount": amount, "reason": reason},
187
+ )
188
+ payload = result.effective_details
189
+ return f"Refund approved for ${payload['amount']}"
190
+ ```
191
+
192
+ ### CrewAI tool
193
+
194
+ ```python
195
+ from crewai_tools import BaseTool
196
+
197
+ class CheqpointApprovalTool(BaseTool):
198
+ name: str = "Request Human Approval"
199
+ description: str = "Submit a risky action for human approval before executing."
200
+
201
+ def _run(self, action_type: str, summary: str, details: dict) -> str:
202
+ result = client.checkpoint(
203
+ type=action_type,
204
+ risk_level="high",
205
+ summary=summary,
206
+ details=details,
207
+ )
208
+ return f"Approved. Effective details: {result.effective_details}"
209
+ ```
210
+
211
+ ### Flask webhook handler
212
+
213
+ ```python
214
+ from flask import Flask, request, jsonify
215
+
216
+ app = Flask(__name__)
217
+
218
+ @app.route("/webhook/cheqpoint", methods=["POST"])
219
+ def cheqpoint_webhook():
220
+ data = request.get_json()
221
+ status = data["status"]
222
+ payload = data.get("modifiedDetails") or data["details"]
223
+
224
+ if status == "APPROVED":
225
+ process_action(payload)
226
+
227
+ return jsonify({"ok": True}), 200
228
+ ```
229
+
230
+ ## Requirements
231
+
232
+ - Python 3.9+
233
+ - `requests` library
234
+
235
+ ## License
236
+
237
+ MIT
@@ -0,0 +1,8 @@
1
+ cheqpoint/__init__.py,sha256=GfWwQ84iw4v7hpzLvpJPr6TnTxlr3EPzDWxtBe7yicE,583
2
+ cheqpoint/client.py,sha256=vaHhkCb7M4TnRs2MKeRYrUTkUWRQUJLQarOW4tu3nQU,15457
3
+ cheqpoint/crewai_tool.py,sha256=0h73Uo-OAz7fhDYLJfgYKDRBDN0irI2jf7SdLzncETQ,2988
4
+ cheqpoint/langchain_tool.py,sha256=IHFd8-uMbUQIpMtwi9MivynFFKcCsVSNsF4wI6g2XbU,3676
5
+ cheqpoint-0.1.0.dist-info/METADATA,sha256=l2h-6yv1ekmgWvxTh5TRUcX1gNw8CR4SqmQPH3VsZTc,6693
6
+ cheqpoint-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
7
+ cheqpoint-0.1.0.dist-info/top_level.txt,sha256=V5bGw0B5vcbbkuBwrokEG1qt7clJRlIZewSLdDYaysk,10
8
+ cheqpoint-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ cheqpoint