warmwinter 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,49 @@
1
+ Metadata-Version: 2.4
2
+ Name: warmwinter
3
+ Version: 0.1.0
4
+ Summary: Python SDK for the Warm Winter trust gate — is this AI/agent action trustworthy enough to act on, or escalate?
5
+ Author: Warm Winter
6
+ License: MIT
7
+ Project-URL: Homepage, https://warmwinter.io
8
+ Project-URL: Documentation, https://warmwinter.io/gate
9
+ Project-URL: Source, https://github.com/ricovigil1/warmwinter-sdk/tree/main/python
10
+ Keywords: llm,agents,ai,routing,calibration,trust,gateway
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
15
+ Requires-Python: >=3.8
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Dynamic: license-file
19
+
20
+ # warmwinter (Python SDK)
21
+
22
+ The smallest wrapper around an AI/agent call that asks the one question the Warm
23
+ Winter gate exists to answer: **is this cheap answer trustworthy enough to act on
24
+ — or should we escalate, or abstain?** You keep executing; we only judge. Zero
25
+ dependencies (stdlib `urllib`).
26
+
27
+ ```bash
28
+ pip install warmwinter
29
+ ```
30
+
31
+ ```python
32
+ from warmwinter import WarmWinter
33
+
34
+ ww = WarmWinter(api_key="ww_...") # mint at /api/v1/gate/keys
35
+
36
+ # one call gates, runs the chosen path, and auto-reports the outcome
37
+ answer = ww.guard(
38
+ domain="compute", decision_type="model_route", stated_confidence=0.82,
39
+ cheap=lambda: small_model(prompt),
40
+ escalate=lambda: big_model(prompt),
41
+ verify=lambda out: out is not None, # your success test
42
+ )
43
+ ```
44
+
45
+ Or drive the two halves yourself with `decide(...)` and `outcome(...)`. See the
46
+ module docstring for the full surface.
47
+
48
+ Seamless alternative: point your existing OpenAI/Anthropic client's `base_url`
49
+ at the gateway and change nothing else — see https://warmwinter.io/gate.
@@ -0,0 +1,6 @@
1
+ warmwinter.py,sha256=sMEfE6TKtOXhp8s6iLihMBhksgclJpwDoKsuFlBzqqA,10070
2
+ warmwinter-0.1.0.dist-info/licenses/LICENSE,sha256=9o-yDeQU_7ay5OIc5K6OxxXazPA2bPqlxbhRVPwX18M,1068
3
+ warmwinter-0.1.0.dist-info/METADATA,sha256=hOAlkNDextt0yxNicyix-Hef1q_6LA__S-SLK-ca09A,1865
4
+ warmwinter-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
5
+ warmwinter-0.1.0.dist-info/top_level.txt,sha256=2W6dapz00mRnBafD81lLhCOYSo26Swu61sjh0uTtFHk,11
6
+ warmwinter-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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Warm Winter
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.
@@ -0,0 +1 @@
1
+ warmwinter
warmwinter.py ADDED
@@ -0,0 +1,216 @@
1
+ """
2
+ Warm Winter — Python SDK for the live trust gate.
3
+
4
+ The smallest wrapper around an AI/agent call that asks the one question the gate
5
+ exists to answer: *is this cheap answer trustworthy enough to act on — or should
6
+ we escalate, or abstain?* You keep executing; we only judge.
7
+
8
+ Zero dependencies (stdlib `urllib`), so it drops into any environment.
9
+
10
+ Quickstart
11
+ ----------
12
+ from warmwinter import WarmWinter
13
+
14
+ ww = WarmWinter(api_key="ww_...") # mint at /api/v1/gate/keys
15
+
16
+ # 1) ask the gate before spending on the expensive path
17
+ d = ww.decide(domain="compute", decision_type="model_route",
18
+ stated_confidence=0.82, stakes="medium")
19
+
20
+ if d.verdict == "act":
21
+ answer = cheap_model(prompt) # trust the cheap path
22
+ else: # "escalate" / "abstain"
23
+ answer = strong_model(prompt) # or a human
24
+
25
+ # 2) when you learn whether it held, close the loop so the cell sharpens
26
+ ww.outcome(d.decision_id, "success" if answer_was_good else "failure")
27
+
28
+ The `guard()` helper does both ends in one call when you can supply both paths
29
+ and a verifier — see its docstring.
30
+ """
31
+ from __future__ import annotations
32
+
33
+ import json
34
+ import urllib.error
35
+ import urllib.request
36
+ from dataclasses import dataclass
37
+ from typing import Any, Callable, Optional
38
+
39
+ __version__ = "0.1.0"
40
+ __all__ = ["WarmWinter", "Decision", "WarmWinterError", "DEFAULT_BASE_URL", "__version__"]
41
+
42
+ DEFAULT_BASE_URL = "https://api.warmwinter.io"
43
+
44
+
45
+ class WarmWinterError(RuntimeError):
46
+ def __init__(self, status: int, detail: str):
47
+ super().__init__(f"[{status}] {detail}")
48
+ self.status = status
49
+ self.detail = detail
50
+
51
+
52
+ @dataclass
53
+ class Decision:
54
+ decision_id: str
55
+ verdict: str # "act" | "escalate" | "abstain"
56
+ cell_state: str # "verified" | "provisional" | "ungrounded"
57
+ calibrated_confidence: Optional[float]
58
+ stated_confidence: float
59
+ stakes: str
60
+ reasons: list
61
+ cell: dict
62
+ replayed: bool = False
63
+
64
+ @property
65
+ def act(self) -> bool:
66
+ """True when the gate says the cheap/proposed path is trustworthy."""
67
+ return self.verdict == "act"
68
+
69
+
70
+ class WarmWinter:
71
+ def __init__(self, api_key: str, base_url: str = DEFAULT_BASE_URL, timeout: float = 10.0):
72
+ if not api_key:
73
+ raise ValueError("api_key is required (mint one at /api/v1/gate/keys)")
74
+ self.api_key = api_key
75
+ self.base_url = base_url.rstrip("/")
76
+ self.timeout = timeout
77
+ self._policy: Optional[dict] = None # cached decision boundary (local mode)
78
+ self._buffer: list = [] # outcomes pending a flush
79
+
80
+ # ── low-level ──────────────────────────────────────────────────────────────
81
+ def _post(self, path: str, body: dict) -> dict:
82
+ return self._request("POST", path, body)
83
+
84
+ def _request(self, method: str, path: str, body: Optional[dict]) -> dict:
85
+ data = json.dumps(body).encode() if body is not None else None
86
+ req = urllib.request.Request(
87
+ self.base_url + path, data=data, method=method,
88
+ headers={"X-Api-Key": self.api_key, "Content-Type": "application/json"},
89
+ )
90
+ try:
91
+ with urllib.request.urlopen(req, timeout=self.timeout) as resp:
92
+ return json.loads(resp.read().decode() or "{}")
93
+ except urllib.error.HTTPError as e:
94
+ detail = e.read().decode(errors="replace")
95
+ try:
96
+ detail = json.loads(detail).get("detail", detail)
97
+ except Exception:
98
+ pass
99
+ raise WarmWinterError(e.code, detail) from None
100
+
101
+ # ── the gate ───────────────────────────────────────────────────────────────
102
+ def decide(self, *, domain: str, decision_type: str, stated_confidence: float,
103
+ context: Optional[dict] = None, stakes: Optional[str] = None,
104
+ candidate_action: Optional[str] = None,
105
+ idempotency_key: Optional[str] = None,
106
+ on_ungrounded: Optional[str] = None) -> Decision:
107
+ """Ask whether a proposed action is trustworthy enough to act on."""
108
+ body = {"domain": domain, "decision_type": decision_type,
109
+ "stated_confidence": stated_confidence}
110
+ for k, v in (("context", context), ("stakes", stakes),
111
+ ("candidate_action", candidate_action),
112
+ ("idempotency_key", idempotency_key),
113
+ ("on_ungrounded", on_ungrounded)):
114
+ if v is not None:
115
+ body[k] = v
116
+ r = self._post("/api/v1/gate/decide", body)
117
+ return Decision(
118
+ decision_id=r["decision_id"], verdict=r["verdict"], cell_state=r["cell_state"],
119
+ calibrated_confidence=r.get("calibrated_confidence"),
120
+ stated_confidence=r.get("stated_confidence", stated_confidence),
121
+ stakes=r.get("stakes", stakes or "medium"),
122
+ reasons=r.get("reasons", []), cell=r.get("cell", {}),
123
+ replayed=r.get("replayed", False),
124
+ )
125
+
126
+ def outcome(self, decision_id: str, outcome: str,
127
+ observed: Optional[dict] = None) -> dict:
128
+ """Close the loop. `outcome` ∈ {success, failure, abstained}."""
129
+ body = {"decision_id": decision_id, "outcome": outcome}
130
+ if observed is not None:
131
+ body["observed"] = observed
132
+ return self._post("/api/v1/gate/outcome", body)
133
+
134
+ def frontier(self) -> dict:
135
+ """This account's live competence map (+ the cold-start backtest seed)."""
136
+ return self._request("GET", "/api/v1/gate/frontier", None)
137
+
138
+ # ── local-decide mode (Phase 3 — decide at the edge, learn in batches) ───────
139
+ # Pull the policy ONCE, then decide() in-process at ~microsecond latency with
140
+ # no per-call round-trip (serves billions of agents); buffer outcomes and flush
141
+ # them in one batch. The policy ships states only, never calibration (G6).
142
+ def pull_policy(self) -> dict:
143
+ """Fetch + cache the signed decision boundary. Call once, then periodically."""
144
+ self._policy = self._request("GET", "/api/v1/gate/policy", None)
145
+ return self._policy
146
+
147
+ def decide_local(self, *, domain: str, decision_type: str,
148
+ stakes: str = "medium", on_ungrounded: str = "escalate") -> str:
149
+ """Reproduce the gate verdict from the cached policy alone — no network.
150
+ Auto-pulls the policy on first use. Returns act | escalate | abstain."""
151
+ if self._policy is None:
152
+ self.pull_policy()
153
+ p = self._policy
154
+ state = p.get("default_state", "ungrounded")
155
+ for c in p.get("cells", []):
156
+ if c["domain"] == domain and c["decision_type"] == decision_type:
157
+ state = c["state"]
158
+ break
159
+ if state == "ungrounded" and on_ungrounded == "abstain":
160
+ return "abstain"
161
+ return p["stakes_rule"][state][stakes]
162
+
163
+ def report_local(self, *, domain: str, decision_type: str,
164
+ stated_confidence: float, outcome: str,
165
+ stakes: str = "medium") -> None:
166
+ """Buffer a resolved outcome locally (no network). Flush in a batch."""
167
+ self._buffer.append({"domain": domain, "decision_type": decision_type,
168
+ "stated_confidence": stated_confidence,
169
+ "outcome": outcome, "stakes": stakes})
170
+
171
+ def flush(self) -> dict:
172
+ """Send buffered outcomes in one call. If the server reports a newer policy
173
+ version, refresh the cache automatically (self-healing edge)."""
174
+ if not self._buffer:
175
+ return {"ingested": 0, "stale": False}
176
+ version = (self._policy or {}).get("version")
177
+ body = {"rows": self._buffer, "policy_version": version}
178
+ res = self._post("/api/v1/gate/outcome/bulk", body)
179
+ self._buffer = []
180
+ if res.get("stale"):
181
+ self.pull_policy()
182
+ return res
183
+
184
+ # ── one-call convenience ────────────────────────────────────────────────────
185
+ def guard(self, *, domain: str, decision_type: str, stated_confidence: float,
186
+ cheap: Callable[[], Any], escalate: Callable[[], Any],
187
+ verify: Optional[Callable[[Any], bool]] = None,
188
+ stakes: Optional[str] = None, context: Optional[dict] = None) -> Any:
189
+ """Gate, run the chosen path, and (if `verify` is given) auto-report the
190
+ outcome — the whole loop in one call.
191
+
192
+ result = ww.guard(
193
+ domain="compute", decision_type="model_route", stated_confidence=0.82,
194
+ cheap=lambda: small_model(prompt),
195
+ escalate=lambda: big_model(prompt),
196
+ verify=lambda out: out is not None, # your success test
197
+ )
198
+
199
+ `act` → run `cheap`; anything else → run `escalate`. When `verify` is
200
+ supplied we report success/failure so the cell learns; otherwise resolve
201
+ it yourself with `outcome(...)`.
202
+ """
203
+ d = self.decide(domain=domain, decision_type=decision_type,
204
+ stated_confidence=stated_confidence, stakes=stakes,
205
+ context=context)
206
+ took_cheap = d.act
207
+ result = (cheap if took_cheap else escalate)()
208
+ if verify is not None:
209
+ ok = bool(verify(result))
210
+ # Only the cheap path's correctness scores the cell — escalation is the
211
+ # safe fallback, not a prediction we're grading.
212
+ if took_cheap:
213
+ self.outcome(d.decision_id, "success" if ok else "failure")
214
+ else:
215
+ self.outcome(d.decision_id, "abstained")
216
+ return result