overwing 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.
- overwing/__init__.py +8 -0
- overwing/_client.py +181 -0
- overwing/_errors.py +10 -0
- overwing/_types.py +77 -0
- overwing/langchain.py +255 -0
- overwing/openai_agents.py +137 -0
- overwing/py.typed +0 -0
- overwing-0.1.0.dist-info/METADATA +136 -0
- overwing-0.1.0.dist-info/RECORD +11 -0
- overwing-0.1.0.dist-info/WHEEL +4 -0
- overwing-0.1.0.dist-info/licenses/LICENSE +21 -0
overwing/__init__.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""Overwing: guardrails for LLM output. https://overwing.ai"""
|
|
2
|
+
|
|
3
|
+
from ._client import AsyncOverwing, Overwing
|
|
4
|
+
from ._errors import OverwingError
|
|
5
|
+
from ._types import BatchResult, Evaluation, RuleResult, Verdict
|
|
6
|
+
|
|
7
|
+
__all__ = ["AsyncOverwing", "BatchResult", "Evaluation", "Overwing", "OverwingError", "RuleResult", "Verdict"]
|
|
8
|
+
__version__ = "0.1.0"
|
overwing/_client.py
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import time
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from ._errors import OverwingError
|
|
10
|
+
from ._types import BatchResult, Evaluation
|
|
11
|
+
|
|
12
|
+
DEFAULT_BASE_URL = "https://overwing.ai"
|
|
13
|
+
_USER_AGENT = "overwing-python/0.1.0"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _resolve(api_key: str | None, base_url: str | None) -> tuple[str, str]:
|
|
17
|
+
key = api_key or os.environ.get("OVERWING_API_KEY")
|
|
18
|
+
if not key:
|
|
19
|
+
raise OverwingError("Overwing API key missing. Pass api_key= or set OVERWING_API_KEY. Get one at https://overwing.ai/login")
|
|
20
|
+
return key, (base_url or os.environ.get("OVERWING_BASE_URL") or DEFAULT_BASE_URL).rstrip("/")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _error_from(res: httpx.Response) -> OverwingError:
|
|
24
|
+
try:
|
|
25
|
+
message = str(res.json().get("error", f"HTTP {res.status_code}"))
|
|
26
|
+
except ValueError:
|
|
27
|
+
message = f"HTTP {res.status_code}"
|
|
28
|
+
ra = res.headers.get("retry-after")
|
|
29
|
+
return OverwingError(message, res.status_code, float(ra) if ra else None)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _retryable(res: httpx.Response) -> bool:
|
|
33
|
+
if res.status_code == 429:
|
|
34
|
+
ra = res.headers.get("retry-after")
|
|
35
|
+
return ra is None or float(ra) <= 5
|
|
36
|
+
return res.status_code >= 500
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class _Base:
|
|
40
|
+
def __init__(self, api_key: str | None = None, *, base_url: str | None = None, timeout: float = 15.0, max_retries: int = 2) -> None:
|
|
41
|
+
self._api_key, self.base_url = _resolve(api_key, base_url)
|
|
42
|
+
self._timeout = timeout
|
|
43
|
+
self._max_retries = max_retries
|
|
44
|
+
|
|
45
|
+
def _headers(self, idempotency_key: str | None = None, json_body: bool = False) -> dict[str, str]:
|
|
46
|
+
h = {"Authorization": f"Bearer {self._api_key}", "Accept": "application/json", "User-Agent": _USER_AGENT}
|
|
47
|
+
if json_body:
|
|
48
|
+
h["Content-Type"] = "application/json"
|
|
49
|
+
if idempotency_key:
|
|
50
|
+
h["Idempotency-Key"] = idempotency_key
|
|
51
|
+
return h
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class Overwing(_Base):
|
|
55
|
+
"""Synchronous client for the Overwing API."""
|
|
56
|
+
|
|
57
|
+
def __init__(self, api_key: str | None = None, *, base_url: str | None = None, timeout: float = 15.0, max_retries: int = 2, transport: httpx.BaseTransport | None = None) -> None:
|
|
58
|
+
super().__init__(api_key, base_url=base_url, timeout=timeout, max_retries=max_retries)
|
|
59
|
+
self._http = httpx.Client(base_url=self.base_url, timeout=timeout, transport=transport)
|
|
60
|
+
|
|
61
|
+
def close(self) -> None:
|
|
62
|
+
self._http.close()
|
|
63
|
+
|
|
64
|
+
def __enter__(self) -> "Overwing":
|
|
65
|
+
return self
|
|
66
|
+
|
|
67
|
+
def __exit__(self, *exc: object) -> None:
|
|
68
|
+
self.close()
|
|
69
|
+
|
|
70
|
+
def _request(self, method: str, path: str, *, json: Any = None, idempotency_key: str | None = None, accept: tuple[int, ...] = ()) -> Any:
|
|
71
|
+
attempt = 0
|
|
72
|
+
while True:
|
|
73
|
+
try:
|
|
74
|
+
res = self._http.request(method, path, json=json, headers=self._headers(idempotency_key, json is not None))
|
|
75
|
+
except httpx.HTTPError as e:
|
|
76
|
+
if attempt < self._max_retries:
|
|
77
|
+
attempt += 1
|
|
78
|
+
time.sleep(0.25 * attempt)
|
|
79
|
+
continue
|
|
80
|
+
raise OverwingError(f"Overwing API unreachable: {e}") from e
|
|
81
|
+
if res.status_code in accept:
|
|
82
|
+
return res.json()
|
|
83
|
+
if _retryable(res) and attempt < self._max_retries:
|
|
84
|
+
attempt += 1
|
|
85
|
+
ra = res.headers.get("retry-after")
|
|
86
|
+
time.sleep(float(ra) if ra else 0.3 * attempt)
|
|
87
|
+
continue
|
|
88
|
+
if res.is_error:
|
|
89
|
+
raise _error_from(res)
|
|
90
|
+
return res.json() if res.content else None
|
|
91
|
+
|
|
92
|
+
def evaluate(self, text: str, *, rule_set: str = "content-safety", metadata: dict[str, Any] | None = None, idempotency_key: str | None = None) -> Evaluation:
|
|
93
|
+
"""Score one text. Raises OverwingError on any non-2xx."""
|
|
94
|
+
return Evaluation.from_dict(self._request("POST", "/api/v1/evaluate", json={"input": text, "rule_set": rule_set, "metadata": metadata}, idempotency_key=idempotency_key))
|
|
95
|
+
|
|
96
|
+
def evaluate_batch(self, items: list[dict[str, Any]], *, rule_set: str = "content-safety", idempotency_key: str | None = None) -> BatchResult:
|
|
97
|
+
"""Score up to 50 texts. Each item: {"input": str, "id"?: str, "metadata"?: dict}."""
|
|
98
|
+
return BatchResult.from_dict(self._request("POST", "/api/v1/evaluate/batch", json={"rule_set": rule_set, "items": items}, idempotency_key=idempotency_key, accept=(502,)))
|
|
99
|
+
|
|
100
|
+
def get_evaluation(self, evaluation_id: str) -> dict[str, Any]:
|
|
101
|
+
return self._request("GET", f"/api/v1/evaluations/{evaluation_id}")
|
|
102
|
+
|
|
103
|
+
def list_rule_sets(self, include_inactive: bool = False) -> list[dict[str, Any]]:
|
|
104
|
+
return self._request("GET", "/api/v1/rule-sets" + ("?include_inactive=true" if include_inactive else ""))["rule_sets"]
|
|
105
|
+
|
|
106
|
+
def get_rule_set(self, slug: str) -> dict[str, Any]:
|
|
107
|
+
return self._request("GET", f"/api/v1/rule-sets/{slug}")
|
|
108
|
+
|
|
109
|
+
def create_rule_set(self, *, name: str, slug: str, rules: list[dict[str, Any]], description: str | None = None) -> dict[str, Any]:
|
|
110
|
+
return self._request("POST", "/api/v1/rule-sets", json={"name": name, "slug": slug, "rules": rules, "description": description})
|
|
111
|
+
|
|
112
|
+
def usage(self, days: int | None = None) -> dict[str, Any]:
|
|
113
|
+
return self._request("GET", "/api/v1/usage" + (f"?days={days}" if days else ""))
|
|
114
|
+
|
|
115
|
+
def me(self) -> dict[str, Any]:
|
|
116
|
+
return self._request("GET", "/api/v1/me")
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class AsyncOverwing(_Base):
|
|
120
|
+
"""Asynchronous client for the Overwing API."""
|
|
121
|
+
|
|
122
|
+
def __init__(self, api_key: str | None = None, *, base_url: str | None = None, timeout: float = 15.0, max_retries: int = 2, transport: httpx.AsyncBaseTransport | None = None) -> None:
|
|
123
|
+
super().__init__(api_key, base_url=base_url, timeout=timeout, max_retries=max_retries)
|
|
124
|
+
self._http = httpx.AsyncClient(base_url=self.base_url, timeout=timeout, transport=transport)
|
|
125
|
+
|
|
126
|
+
async def aclose(self) -> None:
|
|
127
|
+
await self._http.aclose()
|
|
128
|
+
|
|
129
|
+
async def __aenter__(self) -> "AsyncOverwing":
|
|
130
|
+
return self
|
|
131
|
+
|
|
132
|
+
async def __aexit__(self, *exc: object) -> None:
|
|
133
|
+
await self.aclose()
|
|
134
|
+
|
|
135
|
+
async def _request(self, method: str, path: str, *, json: Any = None, idempotency_key: str | None = None, accept: tuple[int, ...] = ()) -> Any:
|
|
136
|
+
import asyncio
|
|
137
|
+
|
|
138
|
+
attempt = 0
|
|
139
|
+
while True:
|
|
140
|
+
try:
|
|
141
|
+
res = await self._http.request(method, path, json=json, headers=self._headers(idempotency_key, json is not None))
|
|
142
|
+
except httpx.HTTPError as e:
|
|
143
|
+
if attempt < self._max_retries:
|
|
144
|
+
attempt += 1
|
|
145
|
+
await asyncio.sleep(0.25 * attempt)
|
|
146
|
+
continue
|
|
147
|
+
raise OverwingError(f"Overwing API unreachable: {e}") from e
|
|
148
|
+
if res.status_code in accept:
|
|
149
|
+
return res.json()
|
|
150
|
+
if _retryable(res) and attempt < self._max_retries:
|
|
151
|
+
attempt += 1
|
|
152
|
+
ra = res.headers.get("retry-after")
|
|
153
|
+
await asyncio.sleep(float(ra) if ra else 0.3 * attempt)
|
|
154
|
+
continue
|
|
155
|
+
if res.is_error:
|
|
156
|
+
raise _error_from(res)
|
|
157
|
+
return res.json() if res.content else None
|
|
158
|
+
|
|
159
|
+
async def evaluate(self, text: str, *, rule_set: str = "content-safety", metadata: dict[str, Any] | None = None, idempotency_key: str | None = None) -> Evaluation:
|
|
160
|
+
return Evaluation.from_dict(await self._request("POST", "/api/v1/evaluate", json={"input": text, "rule_set": rule_set, "metadata": metadata}, idempotency_key=idempotency_key))
|
|
161
|
+
|
|
162
|
+
async def evaluate_batch(self, items: list[dict[str, Any]], *, rule_set: str = "content-safety", idempotency_key: str | None = None) -> BatchResult:
|
|
163
|
+
return BatchResult.from_dict(await self._request("POST", "/api/v1/evaluate/batch", json={"rule_set": rule_set, "items": items}, idempotency_key=idempotency_key, accept=(502,)))
|
|
164
|
+
|
|
165
|
+
async def get_evaluation(self, evaluation_id: str) -> dict[str, Any]:
|
|
166
|
+
return await self._request("GET", f"/api/v1/evaluations/{evaluation_id}")
|
|
167
|
+
|
|
168
|
+
async def list_rule_sets(self, include_inactive: bool = False) -> list[dict[str, Any]]:
|
|
169
|
+
return (await self._request("GET", "/api/v1/rule-sets" + ("?include_inactive=true" if include_inactive else "")))["rule_sets"]
|
|
170
|
+
|
|
171
|
+
async def get_rule_set(self, slug: str) -> dict[str, Any]:
|
|
172
|
+
return await self._request("GET", f"/api/v1/rule-sets/{slug}")
|
|
173
|
+
|
|
174
|
+
async def create_rule_set(self, *, name: str, slug: str, rules: list[dict[str, Any]], description: str | None = None) -> dict[str, Any]:
|
|
175
|
+
return await self._request("POST", "/api/v1/rule-sets", json={"name": name, "slug": slug, "rules": rules, "description": description})
|
|
176
|
+
|
|
177
|
+
async def usage(self, days: int | None = None) -> dict[str, Any]:
|
|
178
|
+
return await self._request("GET", "/api/v1/usage" + (f"?days={days}" if days else ""))
|
|
179
|
+
|
|
180
|
+
async def me(self) -> dict[str, Any]:
|
|
181
|
+
return await self._request("GET", "/api/v1/me")
|
overwing/_errors.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class OverwingError(Exception):
|
|
5
|
+
"""Any non-2xx answer from the Overwing API, or a transport failure."""
|
|
6
|
+
|
|
7
|
+
def __init__(self, message: str, status: int = 0, retry_after_seconds: float | None = None) -> None:
|
|
8
|
+
super().__init__(message)
|
|
9
|
+
self.status = status
|
|
10
|
+
self.retry_after_seconds = retry_after_seconds
|
overwing/_types.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Any, Literal
|
|
5
|
+
|
|
6
|
+
Verdict = Literal["pass", "fail", "review"]
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
|
+
class RuleResult:
|
|
11
|
+
rule: str
|
|
12
|
+
type: Literal["choice", "score", "noul"]
|
|
13
|
+
answer: str | float | bool
|
|
14
|
+
probability: float
|
|
15
|
+
confidence: float
|
|
16
|
+
verdict: Verdict
|
|
17
|
+
|
|
18
|
+
@classmethod
|
|
19
|
+
def from_dict(cls, d: dict[str, Any]) -> "RuleResult":
|
|
20
|
+
return cls(rule=d["rule"], type=d["type"], answer=d["answer"], probability=float(d["probability"]), confidence=float(d["confidence"]), verdict=d["verdict"])
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class Evaluation:
|
|
25
|
+
id: str
|
|
26
|
+
verdict: Verdict
|
|
27
|
+
aggregate_score: float
|
|
28
|
+
confidence: float
|
|
29
|
+
latency_ms: int
|
|
30
|
+
results: list[RuleResult] = field(default_factory=list)
|
|
31
|
+
raw: dict[str, Any] = field(default_factory=dict, repr=False, compare=False)
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def failed_rules(self) -> list[str]:
|
|
35
|
+
return [r.rule for r in self.results if r.verdict == "fail"]
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def review_rules(self) -> list[str]:
|
|
39
|
+
return [r.rule for r in self.results if r.verdict == "review"]
|
|
40
|
+
|
|
41
|
+
@classmethod
|
|
42
|
+
def from_dict(cls, d: dict[str, Any]) -> "Evaluation":
|
|
43
|
+
return cls(
|
|
44
|
+
id=d["id"],
|
|
45
|
+
verdict=d["verdict"],
|
|
46
|
+
aggregate_score=float(d["aggregate_score"]),
|
|
47
|
+
confidence=float(d["confidence"]),
|
|
48
|
+
latency_ms=int(d["latency_ms"]),
|
|
49
|
+
results=[RuleResult.from_dict(r) for r in d.get("results", [])],
|
|
50
|
+
raw=d,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass(frozen=True)
|
|
55
|
+
class BatchItemResult:
|
|
56
|
+
id: str | None
|
|
57
|
+
index: int
|
|
58
|
+
evaluation: Evaluation | None
|
|
59
|
+
error: str | None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass(frozen=True)
|
|
63
|
+
class BatchResult:
|
|
64
|
+
total: int
|
|
65
|
+
passed: int
|
|
66
|
+
failed: int
|
|
67
|
+
review: int
|
|
68
|
+
errors: int
|
|
69
|
+
results: list[BatchItemResult]
|
|
70
|
+
|
|
71
|
+
@classmethod
|
|
72
|
+
def from_dict(cls, d: dict[str, Any]) -> "BatchResult":
|
|
73
|
+
s = d["summary"]
|
|
74
|
+
return cls(
|
|
75
|
+
total=s["total"], passed=s["pass"], failed=s["fail"], review=s["review"], errors=s["errors"],
|
|
76
|
+
results=[BatchItemResult(id=r.get("id"), index=r["index"], evaluation=Evaluation.from_dict(r["evaluation"]) if r.get("evaluation") else None, error=r.get("error")) for r in d.get("results", [])],
|
|
77
|
+
)
|
overwing/langchain.py
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"""Overwing guardrails for LangChain.
|
|
2
|
+
|
|
3
|
+
Two ways to use it:
|
|
4
|
+
|
|
5
|
+
1. A runnable you pipe after your model. It scores the model's answer and,
|
|
6
|
+
on `fail`, raises or replaces it. This is the one to reach for.
|
|
7
|
+
|
|
8
|
+
from overwing.langchain import overwing_guard
|
|
9
|
+
chain = prompt | llm | overwing_guard(on_fail="replace")
|
|
10
|
+
|
|
11
|
+
2. A callback handler that scores every LLM output (and optionally every
|
|
12
|
+
prompt) as it happens. Use it to log verdicts across an application or to
|
|
13
|
+
abort a run on `fail` without changing the chain.
|
|
14
|
+
|
|
15
|
+
from overwing.langchain import OverwingCallbackHandler
|
|
16
|
+
llm.invoke("...", config={"callbacks": [OverwingCallbackHandler()]})
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import json
|
|
22
|
+
from typing import Any, Callable, Literal
|
|
23
|
+
from uuid import UUID
|
|
24
|
+
|
|
25
|
+
from langchain_core.callbacks import AsyncCallbackHandler, BaseCallbackHandler
|
|
26
|
+
from langchain_core.messages import AIMessage, BaseMessage
|
|
27
|
+
from langchain_core.outputs import LLMResult
|
|
28
|
+
from langchain_core.runnables import RunnableLambda
|
|
29
|
+
|
|
30
|
+
from ._client import AsyncOverwing, Overwing
|
|
31
|
+
from ._errors import OverwingError
|
|
32
|
+
from ._types import Evaluation
|
|
33
|
+
|
|
34
|
+
Action = Literal["raise", "replace", "annotate"]
|
|
35
|
+
DEFAULT_REPLACEMENT = "I can't share that response."
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class OverwingGuardrailError(Exception):
|
|
39
|
+
"""Raised when a verdict trips the configured action."""
|
|
40
|
+
|
|
41
|
+
def __init__(self, evaluation: Evaluation, phase: str) -> None:
|
|
42
|
+
detail = ", ".join(f"{r.rule}={r.verdict}" for r in evaluation.results if r.verdict != "pass") or "no rule detail"
|
|
43
|
+
super().__init__(f"Overwing {evaluation.verdict.upper()} on {phase} ({detail}) · {evaluation.id}")
|
|
44
|
+
self.evaluation = evaluation
|
|
45
|
+
self.phase = phase
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def text_of(value: Any) -> str:
|
|
49
|
+
"""Text from a string, a message, a list of messages, or anything else LangChain hands us."""
|
|
50
|
+
if isinstance(value, str):
|
|
51
|
+
return value.strip()
|
|
52
|
+
if isinstance(value, BaseMessage):
|
|
53
|
+
content = value.content
|
|
54
|
+
if isinstance(content, str):
|
|
55
|
+
return content.strip()
|
|
56
|
+
if isinstance(content, list):
|
|
57
|
+
return "\n".join(p.get("text", "") if isinstance(p, dict) else str(p) for p in content).strip()
|
|
58
|
+
return str(content).strip()
|
|
59
|
+
if isinstance(value, list):
|
|
60
|
+
return "\n".join(t for t in (text_of(v) for v in value) if t).strip()
|
|
61
|
+
if value is None:
|
|
62
|
+
return ""
|
|
63
|
+
try:
|
|
64
|
+
return json.dumps(value, default=str)
|
|
65
|
+
except Exception: # noqa: BLE001
|
|
66
|
+
return str(value)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _decide(evaluation: Evaluation, on_fail: Action, on_review: Action) -> Action | Literal["pass"]:
|
|
70
|
+
if evaluation.verdict == "fail":
|
|
71
|
+
return on_fail
|
|
72
|
+
if evaluation.verdict == "review":
|
|
73
|
+
return on_review
|
|
74
|
+
return "pass"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _annotate(value: Any, evaluation: Evaluation) -> Any:
|
|
78
|
+
meta = {
|
|
79
|
+
"id": evaluation.id,
|
|
80
|
+
"verdict": evaluation.verdict,
|
|
81
|
+
"aggregate_score": evaluation.aggregate_score,
|
|
82
|
+
"confidence": evaluation.confidence,
|
|
83
|
+
"failed_rules": evaluation.failed_rules,
|
|
84
|
+
"review_rules": evaluation.review_rules,
|
|
85
|
+
}
|
|
86
|
+
if isinstance(value, BaseMessage):
|
|
87
|
+
value.response_metadata = {**(value.response_metadata or {}), "overwing": meta}
|
|
88
|
+
return value
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _replace(value: Any, replacement: str) -> Any:
|
|
92
|
+
if isinstance(value, BaseMessage):
|
|
93
|
+
return AIMessage(content=replacement, response_metadata={**(value.response_metadata or {})})
|
|
94
|
+
return replacement
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def overwing_guard(
|
|
98
|
+
*,
|
|
99
|
+
client: Overwing | None = None,
|
|
100
|
+
rule_set: str = "content-safety",
|
|
101
|
+
on_fail: Action = "raise",
|
|
102
|
+
on_review: Action = "annotate",
|
|
103
|
+
replacement: str | Callable[[Evaluation], str] = DEFAULT_REPLACEMENT,
|
|
104
|
+
metadata: dict[str, Any] | None = None,
|
|
105
|
+
on_verdict: Callable[[Evaluation, str], None] | None = None,
|
|
106
|
+
fail_open: bool = False,
|
|
107
|
+
) -> RunnableLambda:
|
|
108
|
+
"""A runnable that scores whatever flows through it (string, AIMessage, or list) and acts on the verdict.
|
|
109
|
+
|
|
110
|
+
- fail -> `on_fail`: "raise" (default) raises OverwingGuardrailError; "replace" swaps the text; "annotate" passes it through with the verdict on `response_metadata["overwing"]`.
|
|
111
|
+
- review -> `on_review`: "annotate" (default) | "raise" | "replace".
|
|
112
|
+
- pass -> passed through, annotated when the value is a message.
|
|
113
|
+
"""
|
|
114
|
+
ow = client or Overwing()
|
|
115
|
+
|
|
116
|
+
def run(value: Any) -> Any:
|
|
117
|
+
text = text_of(value)
|
|
118
|
+
if not text:
|
|
119
|
+
return value
|
|
120
|
+
try:
|
|
121
|
+
evaluation = ow.evaluate(text, rule_set=rule_set, metadata={**(metadata or {}), "phase": "output", "source": "langchain"})
|
|
122
|
+
except OverwingError:
|
|
123
|
+
if fail_open:
|
|
124
|
+
return value
|
|
125
|
+
raise
|
|
126
|
+
if on_verdict:
|
|
127
|
+
on_verdict(evaluation, "output")
|
|
128
|
+
action = _decide(evaluation, on_fail, on_review)
|
|
129
|
+
if action == "raise":
|
|
130
|
+
raise OverwingGuardrailError(evaluation, "output")
|
|
131
|
+
if action == "replace":
|
|
132
|
+
return _annotate(_replace(value, replacement(evaluation) if callable(replacement) else replacement), evaluation)
|
|
133
|
+
return _annotate(value, evaluation)
|
|
134
|
+
|
|
135
|
+
return RunnableLambda(run, name="overwing_guard")
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class OverwingCallbackHandler(BaseCallbackHandler):
|
|
139
|
+
"""Scores every LLM output (and prompts when `check_input=True`).
|
|
140
|
+
|
|
141
|
+
On `fail` the handler raises OverwingGuardrailError, which aborts the run
|
|
142
|
+
because `raise_error` is True. Set `on_fail="log"` to only record verdicts.
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
raise_error = True
|
|
146
|
+
|
|
147
|
+
def __init__(
|
|
148
|
+
self,
|
|
149
|
+
*,
|
|
150
|
+
client: Overwing | None = None,
|
|
151
|
+
rule_set: str = "content-safety",
|
|
152
|
+
on_fail: Literal["raise", "log"] = "raise",
|
|
153
|
+
on_review: Literal["raise", "log"] = "log",
|
|
154
|
+
check_input: bool = False,
|
|
155
|
+
metadata: dict[str, Any] | None = None,
|
|
156
|
+
on_verdict: Callable[[Evaluation, str], None] | None = None,
|
|
157
|
+
fail_open: bool = False,
|
|
158
|
+
) -> None:
|
|
159
|
+
super().__init__()
|
|
160
|
+
self.client = client or Overwing()
|
|
161
|
+
self.rule_set = rule_set
|
|
162
|
+
self.on_fail = on_fail
|
|
163
|
+
self.on_review = on_review
|
|
164
|
+
self.check_input = check_input
|
|
165
|
+
self.metadata = metadata or {}
|
|
166
|
+
self.on_verdict = on_verdict
|
|
167
|
+
self.fail_open = fail_open
|
|
168
|
+
self.verdicts: list[tuple[str, Evaluation]] = []
|
|
169
|
+
|
|
170
|
+
def _score(self, text: str, phase: str) -> None:
|
|
171
|
+
if not text:
|
|
172
|
+
return
|
|
173
|
+
try:
|
|
174
|
+
evaluation = self.client.evaluate(text, rule_set=self.rule_set, metadata={**self.metadata, "phase": phase, "source": "langchain-callback"})
|
|
175
|
+
except OverwingError:
|
|
176
|
+
if self.fail_open:
|
|
177
|
+
return
|
|
178
|
+
raise
|
|
179
|
+
self.verdicts.append((phase, evaluation))
|
|
180
|
+
if self.on_verdict:
|
|
181
|
+
self.on_verdict(evaluation, phase)
|
|
182
|
+
if (evaluation.verdict == "fail" and self.on_fail == "raise") or (evaluation.verdict == "review" and self.on_review == "raise"):
|
|
183
|
+
raise OverwingGuardrailError(evaluation, phase)
|
|
184
|
+
|
|
185
|
+
def on_llm_start(self, serialized: dict[str, Any], prompts: list[str], *, run_id: UUID, **kwargs: Any) -> None:
|
|
186
|
+
if self.check_input:
|
|
187
|
+
self._score("\n".join(prompts).strip(), "input")
|
|
188
|
+
|
|
189
|
+
def on_chat_model_start(self, serialized: dict[str, Any], messages: list[list[BaseMessage]], *, run_id: UUID, **kwargs: Any) -> None:
|
|
190
|
+
if self.check_input:
|
|
191
|
+
last = [m for batch in messages for m in batch if getattr(m, "type", "") == "human"]
|
|
192
|
+
self._score(text_of(last[-1]) if last else "", "input")
|
|
193
|
+
|
|
194
|
+
def on_llm_end(self, response: LLMResult, *, run_id: UUID, **kwargs: Any) -> None:
|
|
195
|
+
for batch in response.generations:
|
|
196
|
+
for gen in batch:
|
|
197
|
+
self._score(gen.text.strip() if gen.text else text_of(getattr(gen, "message", None)), "output")
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class AsyncOverwingCallbackHandler(AsyncCallbackHandler):
|
|
201
|
+
"""Async twin of OverwingCallbackHandler."""
|
|
202
|
+
|
|
203
|
+
raise_error = True
|
|
204
|
+
|
|
205
|
+
def __init__(
|
|
206
|
+
self,
|
|
207
|
+
*,
|
|
208
|
+
client: AsyncOverwing | None = None,
|
|
209
|
+
rule_set: str = "content-safety",
|
|
210
|
+
on_fail: Literal["raise", "log"] = "raise",
|
|
211
|
+
on_review: Literal["raise", "log"] = "log",
|
|
212
|
+
check_input: bool = False,
|
|
213
|
+
metadata: dict[str, Any] | None = None,
|
|
214
|
+
on_verdict: Callable[[Evaluation, str], None] | None = None,
|
|
215
|
+
fail_open: bool = False,
|
|
216
|
+
) -> None:
|
|
217
|
+
super().__init__()
|
|
218
|
+
self.client = client or AsyncOverwing()
|
|
219
|
+
self.rule_set = rule_set
|
|
220
|
+
self.on_fail = on_fail
|
|
221
|
+
self.on_review = on_review
|
|
222
|
+
self.check_input = check_input
|
|
223
|
+
self.metadata = metadata or {}
|
|
224
|
+
self.on_verdict = on_verdict
|
|
225
|
+
self.fail_open = fail_open
|
|
226
|
+
self.verdicts: list[tuple[str, Evaluation]] = []
|
|
227
|
+
|
|
228
|
+
async def _score(self, text: str, phase: str) -> None:
|
|
229
|
+
if not text:
|
|
230
|
+
return
|
|
231
|
+
try:
|
|
232
|
+
evaluation = await self.client.evaluate(text, rule_set=self.rule_set, metadata={**self.metadata, "phase": phase, "source": "langchain-callback"})
|
|
233
|
+
except OverwingError:
|
|
234
|
+
if self.fail_open:
|
|
235
|
+
return
|
|
236
|
+
raise
|
|
237
|
+
self.verdicts.append((phase, evaluation))
|
|
238
|
+
if self.on_verdict:
|
|
239
|
+
self.on_verdict(evaluation, phase)
|
|
240
|
+
if (evaluation.verdict == "fail" and self.on_fail == "raise") or (evaluation.verdict == "review" and self.on_review == "raise"):
|
|
241
|
+
raise OverwingGuardrailError(evaluation, phase)
|
|
242
|
+
|
|
243
|
+
async def on_llm_start(self, serialized: dict[str, Any], prompts: list[str], *, run_id: UUID, **kwargs: Any) -> None:
|
|
244
|
+
if self.check_input:
|
|
245
|
+
await self._score("\n".join(prompts).strip(), "input")
|
|
246
|
+
|
|
247
|
+
async def on_chat_model_start(self, serialized: dict[str, Any], messages: list[list[BaseMessage]], *, run_id: UUID, **kwargs: Any) -> None:
|
|
248
|
+
if self.check_input:
|
|
249
|
+
last = [m for batch in messages for m in batch if getattr(m, "type", "") == "human"]
|
|
250
|
+
await self._score(text_of(last[-1]) if last else "", "input")
|
|
251
|
+
|
|
252
|
+
async def on_llm_end(self, response: LLMResult, *, run_id: UUID, **kwargs: Any) -> None:
|
|
253
|
+
for batch in response.generations:
|
|
254
|
+
for gen in batch:
|
|
255
|
+
await self._score(gen.text.strip() if gen.text else text_of(getattr(gen, "message", None)), "output")
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""Overwing guardrails for the OpenAI Agents SDK (Python).
|
|
2
|
+
|
|
3
|
+
from agents import Agent, Runner
|
|
4
|
+
from overwing.openai_agents import overwing_input_guardrail, overwing_output_guardrail
|
|
5
|
+
|
|
6
|
+
agent = Agent(
|
|
7
|
+
name="Support",
|
|
8
|
+
instructions="Help the customer.",
|
|
9
|
+
input_guardrails=[overwing_input_guardrail()],
|
|
10
|
+
output_guardrails=[overwing_output_guardrail()],
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
A tripped guardrail makes the SDK raise InputGuardrailTripwireTriggered or
|
|
14
|
+
OutputGuardrailTripwireTriggered; the Overwing evaluation is on
|
|
15
|
+
``exc.guardrail_result.output.output_info["evaluation"]``.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import json
|
|
21
|
+
from typing import Any, Callable, Literal
|
|
22
|
+
|
|
23
|
+
from agents import Agent, GuardrailFunctionOutput, InputGuardrail, OutputGuardrail, RunContextWrapper
|
|
24
|
+
|
|
25
|
+
from ._client import AsyncOverwing
|
|
26
|
+
from ._errors import OverwingError
|
|
27
|
+
from ._types import Evaluation
|
|
28
|
+
|
|
29
|
+
TripOn = Literal["fail", "fail-or-review"]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def text_from_input(value: Any) -> str:
|
|
33
|
+
"""Pull user-facing text out of the SDK's input shape (a string or a list of input items)."""
|
|
34
|
+
if isinstance(value, str):
|
|
35
|
+
return value.strip()
|
|
36
|
+
if not isinstance(value, list):
|
|
37
|
+
return ""
|
|
38
|
+
texts: list[str] = []
|
|
39
|
+
for item in value:
|
|
40
|
+
if not isinstance(item, dict):
|
|
41
|
+
continue
|
|
42
|
+
role = item.get("role")
|
|
43
|
+
if role is not None and role != "user":
|
|
44
|
+
continue
|
|
45
|
+
content = item.get("content")
|
|
46
|
+
if isinstance(content, str):
|
|
47
|
+
texts.append(content)
|
|
48
|
+
elif isinstance(content, list):
|
|
49
|
+
for part in content:
|
|
50
|
+
if isinstance(part, dict) and isinstance(part.get("text"), str) and part.get("type") in (None, "input_text", "text"):
|
|
51
|
+
texts.append(part["text"])
|
|
52
|
+
elif isinstance(item.get("text"), str):
|
|
53
|
+
texts.append(item["text"])
|
|
54
|
+
return "\n".join(texts).strip()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def text_from_output(value: Any) -> str:
|
|
58
|
+
if isinstance(value, str):
|
|
59
|
+
return value.strip()
|
|
60
|
+
if value is None:
|
|
61
|
+
return ""
|
|
62
|
+
for attr in ("model_dump", "dict"):
|
|
63
|
+
fn = getattr(value, attr, None)
|
|
64
|
+
if callable(fn):
|
|
65
|
+
try:
|
|
66
|
+
return json.dumps(fn(), default=str)
|
|
67
|
+
except Exception: # noqa: BLE001
|
|
68
|
+
break
|
|
69
|
+
try:
|
|
70
|
+
return json.dumps(value, default=str)
|
|
71
|
+
except Exception: # noqa: BLE001
|
|
72
|
+
return str(value)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class _Runner:
|
|
76
|
+
def __init__(self, *, client: AsyncOverwing | None, rule_set: str, trip_on: TripOn, metadata: dict[str, Any] | Callable[[], dict[str, Any]] | None, on_verdict: Callable[[Evaluation, str], None] | None, fail_open: bool) -> None:
|
|
77
|
+
self.client = client or AsyncOverwing()
|
|
78
|
+
self.rule_set = rule_set
|
|
79
|
+
self.trip_on = trip_on
|
|
80
|
+
self.metadata = metadata
|
|
81
|
+
self.on_verdict = on_verdict
|
|
82
|
+
self.fail_open = fail_open
|
|
83
|
+
|
|
84
|
+
async def run(self, text: str, phase: str) -> GuardrailFunctionOutput:
|
|
85
|
+
if not text:
|
|
86
|
+
return GuardrailFunctionOutput(output_info={"evaluation": None, "skipped": "empty"}, tripwire_triggered=False)
|
|
87
|
+
meta = self.metadata() if callable(self.metadata) else dict(self.metadata or {})
|
|
88
|
+
meta.update({"phase": phase, "source": "openai-agents"})
|
|
89
|
+
try:
|
|
90
|
+
evaluation = await self.client.evaluate(text, rule_set=self.rule_set, metadata=meta)
|
|
91
|
+
except OverwingError:
|
|
92
|
+
if self.fail_open:
|
|
93
|
+
return GuardrailFunctionOutput(output_info={"evaluation": None, "skipped": "unreachable"}, tripwire_triggered=False)
|
|
94
|
+
raise
|
|
95
|
+
if self.on_verdict:
|
|
96
|
+
self.on_verdict(evaluation, phase)
|
|
97
|
+
tripped = evaluation.verdict == "fail" or (self.trip_on == "fail-or-review" and evaluation.verdict == "review")
|
|
98
|
+
return GuardrailFunctionOutput(output_info={"evaluation": evaluation}, tripwire_triggered=tripped)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def overwing_input_guardrail(
|
|
102
|
+
*,
|
|
103
|
+
client: AsyncOverwing | None = None,
|
|
104
|
+
rule_set: str = "content-safety",
|
|
105
|
+
trip_on: TripOn = "fail",
|
|
106
|
+
name: str = "overwing-input",
|
|
107
|
+
metadata: dict[str, Any] | Callable[[], dict[str, Any]] | None = None,
|
|
108
|
+
on_verdict: Callable[[Evaluation, str], None] | None = None,
|
|
109
|
+
fail_open: bool = False,
|
|
110
|
+
run_in_parallel: bool = True,
|
|
111
|
+
) -> InputGuardrail[Any]:
|
|
112
|
+
"""Scores the user's input before (or alongside) the agent run."""
|
|
113
|
+
runner = _Runner(client=client, rule_set=rule_set, trip_on=trip_on, metadata=metadata, on_verdict=on_verdict, fail_open=fail_open)
|
|
114
|
+
|
|
115
|
+
async def guardrail(ctx: RunContextWrapper[Any], agent: Agent[Any], input: Any) -> GuardrailFunctionOutput: # noqa: A002
|
|
116
|
+
return await runner.run(text_from_input(input), "input")
|
|
117
|
+
|
|
118
|
+
return InputGuardrail(guardrail_function=guardrail, name=name, run_in_parallel=run_in_parallel)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def overwing_output_guardrail(
|
|
122
|
+
*,
|
|
123
|
+
client: AsyncOverwing | None = None,
|
|
124
|
+
rule_set: str = "content-safety",
|
|
125
|
+
trip_on: TripOn = "fail",
|
|
126
|
+
name: str = "overwing-output",
|
|
127
|
+
metadata: dict[str, Any] | Callable[[], dict[str, Any]] | None = None,
|
|
128
|
+
on_verdict: Callable[[Evaluation, str], None] | None = None,
|
|
129
|
+
fail_open: bool = False,
|
|
130
|
+
) -> OutputGuardrail[Any]:
|
|
131
|
+
"""Scores the agent's final output before it is returned."""
|
|
132
|
+
runner = _Runner(client=client, rule_set=rule_set, trip_on=trip_on, metadata=metadata, on_verdict=on_verdict, fail_open=fail_open)
|
|
133
|
+
|
|
134
|
+
async def guardrail(ctx: RunContextWrapper[Any], agent: Agent[Any], output: Any) -> GuardrailFunctionOutput:
|
|
135
|
+
return await runner.run(text_from_output(output), "output")
|
|
136
|
+
|
|
137
|
+
return OutputGuardrail(guardrail_function=guardrail, name=name)
|
overwing/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: overwing
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Overwing SDK: guardrails for LLM output. Typed client for the Overwing API plus OpenAI Agents SDK guardrails and LangChain runnables/callbacks that check every message before it ships.
|
|
5
|
+
Project-URL: Homepage, https://overwing.ai
|
|
6
|
+
Project-URL: Documentation, https://overwing.ai/docs
|
|
7
|
+
Project-URL: Repository, https://github.com/frod27/overwing-python
|
|
8
|
+
Project-URL: Issues, https://github.com/frod27/overwing-python/issues
|
|
9
|
+
Author-email: Overwing <support@overwing.ai>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: agents,ai-safety,content-moderation,guardrails,langchain,llm,openai-agents,overwing,pii,toxicity
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Requires-Dist: httpx>=0.27
|
|
25
|
+
Provides-Extra: agents
|
|
26
|
+
Requires-Dist: openai-agents>=0.1; extra == 'agents'
|
|
27
|
+
Provides-Extra: langchain
|
|
28
|
+
Requires-Dist: langchain-core>=0.3; extra == 'langchain'
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
<p align="center">
|
|
32
|
+
<a href="https://overwing.ai">
|
|
33
|
+
<picture>
|
|
34
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/frod27/overwing-python/main/assets/wordmark-dark.svg">
|
|
35
|
+
<img src="https://raw.githubusercontent.com/frod27/overwing-python/main/assets/wordmark.svg" alt="Overwing" width="220">
|
|
36
|
+
</picture>
|
|
37
|
+
</a>
|
|
38
|
+
</p>
|
|
39
|
+
|
|
40
|
+
<p align="center"><strong>Guardrails for LLM output, in one line.</strong><br>
|
|
41
|
+
OpenAI Agents SDK guardrails, LangChain runnables and callbacks, and a typed client. Every message gets a <code>pass</code> / <code>fail</code> / <code>review</code> verdict with calibrated confidence before it reaches your user.</p>
|
|
42
|
+
|
|
43
|
+
<p align="center">
|
|
44
|
+
<a href="https://pypi.org/project/overwing/"><img alt="PyPI" src="https://img.shields.io/pypi/v/overwing?color=0B1220&label=overwing"></a>
|
|
45
|
+
<a href="https://github.com/frod27/overwing-python/actions"><img alt="CI" src="https://github.com/frod27/overwing-python/actions/workflows/ci.yml/badge.svg"></a>
|
|
46
|
+
<a href="https://overwing.ai/docs"><img alt="API reference" src="https://img.shields.io/badge/API-reference-0B1220"></a>
|
|
47
|
+
<a href="https://overwing.ai"><img alt="agents welcome" src="https://overwing.ai/badge.svg"></a>
|
|
48
|
+
</p>
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pip install "overwing[agents]" # OpenAI Agents SDK guardrails
|
|
54
|
+
pip install "overwing[langchain]" # LangChain guard runnable + callbacks
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Get a free API key at [overwing.ai](https://overwing.ai/login) (250 evaluations a day), or let your agent sign itself up with one `POST` to `/api/v1/signup`. Try it first with no key: paste anything into the console at [overwing.ai](https://overwing.ai).
|
|
58
|
+
|
|
59
|
+
## OpenAI Agents SDK guardrails
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
from agents import Agent, Runner, InputGuardrailTripwireTriggered, OutputGuardrailTripwireTriggered
|
|
63
|
+
from overwing.openai_agents import overwing_input_guardrail, overwing_output_guardrail
|
|
64
|
+
|
|
65
|
+
agent = Agent(
|
|
66
|
+
name="Support",
|
|
67
|
+
instructions="Help the customer.",
|
|
68
|
+
input_guardrails=[overwing_input_guardrail()], # scores the user's message
|
|
69
|
+
output_guardrails=[overwing_output_guardrail()], # scores the agent's final answer
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
result = await Runner.run(agent, "Reach me at dana@example.com to sort out the refund.")
|
|
74
|
+
except (InputGuardrailTripwireTriggered, OutputGuardrailTripwireTriggered) as exc:
|
|
75
|
+
evaluation = exc.guardrail_result.output.output_info["evaluation"]
|
|
76
|
+
print(evaluation.verdict, evaluation.failed_rules) # "fail" ["pii_detected"]
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Both accept `rule_set`, `trip_on="fail" | "fail-or-review"`, `metadata`, `on_verdict`, and `fail_open`. Input guardrails run in parallel with the agent by default; pass `run_in_parallel=False` to block before the model is called. Reads `OVERWING_API_KEY` from the environment, or pass `client=AsyncOverwing(api_key=...)`.
|
|
80
|
+
|
|
81
|
+
## LangChain
|
|
82
|
+
|
|
83
|
+
Pipe a guard after your model. It scores the answer and acts on the verdict before anything downstream sees it.
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
from overwing.langchain import overwing_guard, OverwingGuardrailError
|
|
87
|
+
|
|
88
|
+
chain = prompt | llm | overwing_guard(on_fail="replace") # or on_fail="raise" (default) / "annotate"
|
|
89
|
+
msg = chain.invoke({"question": "..."})
|
|
90
|
+
msg.response_metadata["overwing"] # {"verdict": "pass", "confidence": 0.97, "failed_rules": [], ...}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Or observe every LLM call with a callback handler, which aborts the run on `fail`:
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
from overwing.langchain import OverwingCallbackHandler
|
|
97
|
+
|
|
98
|
+
handler = OverwingCallbackHandler(check_input=True) # also scores the user's prompt
|
|
99
|
+
llm.invoke("...", config={"callbacks": [handler]})
|
|
100
|
+
handler.verdicts # [("input", Evaluation), ("output", Evaluation), ...]
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Both accept `rule_set`, `metadata`, `on_verdict`, and `fail_open`. There is an `AsyncOverwingCallbackHandler` too.
|
|
104
|
+
|
|
105
|
+
## Client
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
from overwing import Overwing, AsyncOverwing
|
|
109
|
+
|
|
110
|
+
ow = Overwing() # or Overwing(api_key="ow_live_...")
|
|
111
|
+
|
|
112
|
+
e = ow.evaluate("Reach me at dana@example.com to sort out the refund.")
|
|
113
|
+
e.verdict # "fail"
|
|
114
|
+
e.failed_rules # ["pii_detected"]
|
|
115
|
+
e.results[1] # RuleResult(rule="pii_detected", answer=True, confidence=0.98, verdict="fail", ...)
|
|
116
|
+
|
|
117
|
+
batch = ow.evaluate_batch([{"id": "a", "input": "..."}, {"id": "b", "input": "..."}])
|
|
118
|
+
ow.create_rule_set(name="Support tone", slug="support-tone", rules=[...])
|
|
119
|
+
ow.usage()
|
|
120
|
+
|
|
121
|
+
async with AsyncOverwing() as aow:
|
|
122
|
+
e = await aow.evaluate("...")
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
`OverwingError` carries `status` and `retry_after_seconds`. 429s with a short `Retry-After` and 5xx are retried automatically. Pass `idempotency_key=` to make retries safe. Python 3.10+.
|
|
126
|
+
|
|
127
|
+
## How verdicts work
|
|
128
|
+
|
|
129
|
+
Each rule has a fail condition, an optional review threshold, and a weight. The prebuilt `content-safety` set checks toxicity, personal data, self-harm, sexual content, and severity. **fail** means a rule matched. **review** means a rule was unsure. **pass** is everything else. Full guide: [overwing.ai/llms.txt](https://overwing.ai/llms.txt). Reference: [overwing.ai/docs](https://overwing.ai/docs).
|
|
130
|
+
|
|
131
|
+
## Also from Overwing
|
|
132
|
+
|
|
133
|
+
- [`overwing`](https://github.com/frod27/overwing-js) on npm: the same client, a Vercel AI SDK middleware, and Agents SDK guardrails for JavaScript.
|
|
134
|
+
- [`overwing-mcp`](https://github.com/frod27/overwing-mcp): the guardrails as MCP tools for Claude, Cursor, and any MCP client.
|
|
135
|
+
|
|
136
|
+
MIT © Overwing. Verdicts are produced by TypeSafe's Jev System One model; Overwing is not affiliated with TypeSafe.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
overwing/__init__.py,sha256=yjoApvQWTDbKPu9nSRlqL-BU26Ah_HDvN5zLkPILiqE,343
|
|
2
|
+
overwing/_client.py,sha256=7irHyp_t6V7rQvYmwG9ynZLR9CSnaWYSL-gi8sTjdZA,8870
|
|
3
|
+
overwing/_errors.py,sha256=daQrWoYNGduhq2oSXItUwv4gCjKGKlHGHgMllb20JWE,369
|
|
4
|
+
overwing/_types.py,sha256=pEzHS4eVydUyDfyrt9XgKTMAVQg54ZnrFkLBIKUqTck,2290
|
|
5
|
+
overwing/langchain.py,sha256=XtDFgPJbjuhQmPXZrZMrV4fXCzShHJTTQ8QkOdIRS-I,10262
|
|
6
|
+
overwing/openai_agents.py,sha256=cTu3CJxfiUKgJbxGcrbYLDNZsRjBuwzaOfhmeOggVrs,5596
|
|
7
|
+
overwing/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
overwing-0.1.0.dist-info/METADATA,sha256=WXcgKaR-WnVhfrpWMtKdA-A90iSH8XfZBOVCQ7AzOxk,6785
|
|
9
|
+
overwing-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
10
|
+
overwing-0.1.0.dist-info/licenses/LICENSE,sha256=OrKI5tjJZ3skIwOkce1ypaRMFLKKYH7VfdfHi0NN9oY,1079
|
|
11
|
+
overwing-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Overwing (overwing.ai)
|
|
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.
|