Skip to content

Commit 3447dfb

Browse files
[v2.0.1] finalize AI client, lease API, and canonical examples
1 parent b1846e9 commit 3447dfb

14 files changed

Lines changed: 532 additions & 150 deletions

examples/README.md

Lines changed: 58 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
# OnceOnly Python SDK — Examples
22

33
This folder contains **small, runnable examples** demonstrating how to use the OnceOnly Python SDK.
4+
45
Examples are organized into two groups:
56

67
- `examples/general/` — core idempotency use cases (webhooks, workers, automations)
7-
- `examples/ai/` — AI agent and tool-calling integrations
8+
- `examples/ai/` — AI agents, long-running jobs, and tool-calling integrations
89

910
---
1011

1112
## Prerequisites
1213

13-
1) Install the SDK (from repo root):
14+
1) Install the SDK (from repository root):
1415

1516
```bash
1617
pip install -e .
@@ -28,7 +29,7 @@ All examples read `ONCEONLY_API_KEY` from the environment.
2829

2930
## Running examples
3031

31-
From the repository root:
32+
Always run examples from the repository root:
3233

3334
```bash
3435
python examples/general/basic_check_lock.py
@@ -42,7 +43,7 @@ python examples/general/basic_check_lock.py
4243
Minimal idempotency primitive.
4344

4445
- First call → `locked=True`
45-
- Second call with same key → `duplicate=True`
46+
- Second call with the same key → `duplicate=True`
4647

4748
Use this pattern for:
4849
- Webhooks
@@ -64,10 +65,10 @@ Demonstrates TTL behavior.
6465
Attaches metadata to an idempotency key.
6566

6667
Typical metadata:
67-
- user_id
68-
- scenario_id
69-
- webhook_event_id
70-
- trace_id
68+
- `user_id`
69+
- `scenario_id`
70+
- `webhook_event_id`
71+
- `trace_id`
7172

7273
Metadata is useful for debugging and analytics.
7374
If the server does not echo it back, the SDK preserves it in `result.raw`.
@@ -98,7 +99,7 @@ Designed for:
9899
---
99100

100101
### `decorator_pydantic.py`
101-
Advanced decorator example with **Pydantic models**.
102+
Advanced decorator example using **Pydantic models**.
102103

103104
Highlights:
104105
- Stable hashing for complex objects
@@ -113,7 +114,7 @@ This solves a common pain point for AI and API developers.
113114
Inspect account and usage information.
114115

115116
Endpoints used:
116-
- `/v1/me` — API key & plan info
117+
- `/v1/me` — API key and plan info
117118
- `/v1/usage` — current usage vs limits
118119

119120
Useful for:
@@ -123,84 +124,92 @@ Useful for:
123124

124125
---
125126

126-
## AI / Agent examples (`examples/ai/`)
127+
## AI examples (`examples/ai/`)
127128

128-
### `agent_guard_manual.py`
129-
Manual guard pattern for AI agents.
129+
### `run_and_wait.py`
130+
Canonical long-running AI job example.
130131

131-
- Call `check_lock()` before performing a tool action
132-
- Abort immediately if duplicate
132+
- Uses `/v1/ai/run`
133+
- Polls status until completion
134+
- Charged **once per key**, polling is free
133135

134136
Best for:
135-
- Custom agent loops
136-
- Tool routing
137-
- Expensive API calls
137+
- Background AI jobs
138+
- Batch processing
139+
- Server-side agents
138140

139141
---
140142

141-
### `agent_retry_safe.py`
142-
Agent restart / retry safety example.
143+
### `agent_action_local.py`
144+
Local side-effect guard using the AI Lease API.
143145

144-
- Run the script once: performs the side-effect
145-
- Run it again: duplicate is detected and the side-effect is skipped
146+
- Uses `/v1/ai/lease`
147+
- Executes the side-effect locally
148+
- Ensures exactly-once execution across retries and crashes
146149

147-
This is a core pattern for autonomous agents that can crash, retry, or resume.
150+
Best for:
151+
- Payments
152+
- Emails
153+
- Webhooks triggered by agents
148154

149155
---
150156

151-
### `langchain_tool.py`
152-
LangChain integration example.
157+
### `poll_status.py`
158+
Polling example for AI jobs.
153159

154-
- Wraps a standard LangChain `Tool`
155-
- Automatically enforces idempotency
156-
- Prevents double execution of side-effects
160+
- Uses `/v1/ai/status`
161+
- Demonstrates `retry_after_sec` and adaptive polling
157162

158-
Optional dependency:
163+
---
159164

160-
```bash
161-
pip install langchain-core
162-
```
165+
### `get_result.py`
166+
Fetches the final result of a completed AI job.
163167

164-
Works well with:
165-
- LangChain agents
166-
- Tool-calling LLMs
167-
- Multi-step plans
168+
- Uses `/v1/ai/result`
169+
- Safe to call multiple times
168170

169171
---
170172

171-
### `long_running_job.py`
172-
Long-running AI / backend job example.
173+
### `langchain_tool_ai_lease.py`
174+
LangChain integration example using the AI Lease API.
173175

174-
- Uses OnceOnly AI client helpers
175-
- Demonstrates run → poll → wait pattern
176-
- Suitable for batch AI jobs or background processing
176+
- Wraps a LangChain tool
177+
- Guarantees exactly-once tool execution
178+
- Prevents double side-effects
179+
- Protects against LLM 'hallucinations' causing multiple tool calls
180+
181+
Optional dependency:
182+
183+
```bash
184+
pip install langchain-core
185+
```
177186

178187
---
179188

180189
## Design tips
181190

182-
- **Key design:** Model keys after real-world actions
191+
- **Key design:** model keys after real-world actions
183192
Example:
184193
```
185194
agent:email:user42:welcome
186-
make:scenario:123:event:evt_abc
195+
ai:job:daily_summary:2026-01-09
187196
```
188197

189-
- **TTL:** Choose TTL based on how long a duplicate would be dangerous
198+
- **TTL:** choose TTL based on how long a duplicate would be dangerous
190199

191-
- **Fail-open:**
192-
- `fail_open=True` (default): safer for production workflows
200+
- **Fail-open behavior:**
201+
- `fail_open=True` (default): safer for production workflows
193202
- `fail_open=False`: strict correctness for critical paths
194203

195204
---
196205

197206
## Troubleshooting
198207

199208
- **401 / 403** — invalid or missing API key
200-
- **402** — plan limit reached
201-
- **429** — rate limit exceeded (backoff & retry)
209+
- **402** — plan or usage limit reached
210+
- **429** — rate limit exceeded (retry with backoff)
202211
- **5xx** — server error (fail-open may apply)
203212

204213
---
205214

206-
If you need an additional example, open an issue or PR.
215+
If you need additional examples, open an issue or submit a PR.

examples/ai/agent_action_local.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
"""
2+
Local agent/tool side effect guard using the AI Lease API.
3+
4+
This pattern is for cases when YOUR CODE performs the side-effect locally,
5+
but you still want:
6+
- AI pricing/limits (AI usage)
7+
- exactly-once execution across retries/crashes
8+
9+
Flow:
10+
1) POST /ai/lease (charged only if acquired)
11+
2) If acquired -> do side effect locally
12+
3) POST /ai/complete or /ai/fail
13+
14+
Run this file twice: second run should NOT do the side effect again.
15+
"""
16+
17+
import os
18+
from onceonly import OnceOnly
19+
20+
API_KEY = os.getenv("ONCEONLY_API_KEY")
21+
if not API_KEY:
22+
raise SystemExit("Set ONCEONLY_API_KEY env var")
23+
24+
client = OnceOnly(api_key=API_KEY)
25+
26+
KEY = "ai:agent:charge:user_42:invoice_101"
27+
28+
29+
def do_side_effect() -> dict:
30+
# (charge, refund, email, etc.)
31+
print(">>> Charging...")
32+
return {"ok": True}
33+
34+
35+
def main() -> None:
36+
try:
37+
lease = client.ai.lease(key=KEY, ttl=300, metadata={"kind": "charge", "user": "user_42", "invoice": "100"})
38+
status = (lease.get("status") or "").lower()
39+
40+
if status == "acquired":
41+
lease_id = lease.get("lease_id")
42+
if not lease_id:
43+
raise RuntimeError(f"Missing lease_id in response: {lease}")
44+
45+
try:
46+
result = do_side_effect()
47+
except Exception:
48+
# mark failed so you can retry later deterministically
49+
client.ai.fail(key=KEY, lease_id=lease_id, error_code="charge_failed")
50+
raise
51+
else:
52+
client.ai.complete(key=KEY, lease_id=lease_id, result=result)
53+
print("Done.")
54+
return
55+
56+
if status == "completed":
57+
res = client.ai.result(KEY)
58+
# res.result may be dict or None depending on your backend model
59+
print(f"Already done previously: {getattr(res, 'result', None)}")
60+
return
61+
62+
if status == "failed":
63+
# Optional: fetch final result to show error_code / details if backend stores it
64+
res = client.ai.result(KEY)
65+
print(f"Previously failed: {getattr(res, 'error_code', None)}")
66+
return
67+
68+
# in_progress / locked / running / etc.
69+
print(f"Action in progress by another worker. Status: {status}")
70+
71+
except Exception as e:
72+
print(f"SDK or Network error: {e}")
73+
74+
75+
if __name__ == "__main__":
76+
main()

examples/ai/agent_guard_manual.py

Lines changed: 0 additions & 40 deletions
This file was deleted.

examples/ai/agent_retry_safe.py

Lines changed: 0 additions & 29 deletions
This file was deleted.

examples/ai/get_result.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import os
2+
from onceonly import OnceOnly
3+
4+
API_KEY = os.getenv("ONCEONLY_API_KEY")
5+
if not API_KEY:
6+
raise SystemExit("Set ONCEONLY_API_KEY env var")
7+
8+
client = OnceOnly(api_key=API_KEY)
9+
10+
key = "ai:job:daily-summary:2026-01-09"
11+
12+
# If you already know the job is done, fetch result directly:
13+
res = client.ai.result(key)
14+
print("status:", res.status)
15+
print("result:", res.result)
16+
print("error:", res.error_code)

0 commit comments

Comments
 (0)