overwing 0.1.0__tar.gz

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,19 @@
1
+ name: CI
2
+ on:
3
+ push:
4
+ branches: [main]
5
+ pull_request:
6
+ permissions:
7
+ contents: read
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ matrix:
13
+ python: ["3.10", "3.11", "3.12", "3.13"]
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ - uses: astral-sh/setup-uv@v5
17
+ - run: uv python install ${{ matrix.python }}
18
+ - run: uv sync --python ${{ matrix.python }}
19
+ - run: uv run pytest -q
@@ -0,0 +1,16 @@
1
+ name: Release to PyPI
2
+ on:
3
+ push:
4
+ tags: ["v*"]
5
+ permissions:
6
+ contents: read
7
+ id-token: write # PyPI trusted publishing, no token stored anywhere
8
+ jobs:
9
+ publish:
10
+ runs-on: ubuntu-latest
11
+ environment: pypi
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ - uses: astral-sh/setup-uv@v5
15
+ - run: uv build
16
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,7 @@
1
+ .venv
2
+ dist
3
+ __pycache__
4
+ *.egg-info
5
+ .pytest_cache
6
+ .mypy_cache
7
+ .DS_Store
overwing-0.1.0/LICENSE ADDED
@@ -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.
@@ -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,106 @@
1
+ <p align="center">
2
+ <a href="https://overwing.ai">
3
+ <picture>
4
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/frod27/overwing-python/main/assets/wordmark-dark.svg">
5
+ <img src="https://raw.githubusercontent.com/frod27/overwing-python/main/assets/wordmark.svg" alt="Overwing" width="220">
6
+ </picture>
7
+ </a>
8
+ </p>
9
+
10
+ <p align="center"><strong>Guardrails for LLM output, in one line.</strong><br>
11
+ 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>
12
+
13
+ <p align="center">
14
+ <a href="https://pypi.org/project/overwing/"><img alt="PyPI" src="https://img.shields.io/pypi/v/overwing?color=0B1220&label=overwing"></a>
15
+ <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>
16
+ <a href="https://overwing.ai/docs"><img alt="API reference" src="https://img.shields.io/badge/API-reference-0B1220"></a>
17
+ <a href="https://overwing.ai"><img alt="agents welcome" src="https://overwing.ai/badge.svg"></a>
18
+ </p>
19
+
20
+ ---
21
+
22
+ ```bash
23
+ pip install "overwing[agents]" # OpenAI Agents SDK guardrails
24
+ pip install "overwing[langchain]" # LangChain guard runnable + callbacks
25
+ ```
26
+
27
+ 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).
28
+
29
+ ## OpenAI Agents SDK guardrails
30
+
31
+ ```python
32
+ from agents import Agent, Runner, InputGuardrailTripwireTriggered, OutputGuardrailTripwireTriggered
33
+ from overwing.openai_agents import overwing_input_guardrail, overwing_output_guardrail
34
+
35
+ agent = Agent(
36
+ name="Support",
37
+ instructions="Help the customer.",
38
+ input_guardrails=[overwing_input_guardrail()], # scores the user's message
39
+ output_guardrails=[overwing_output_guardrail()], # scores the agent's final answer
40
+ )
41
+
42
+ try:
43
+ result = await Runner.run(agent, "Reach me at dana@example.com to sort out the refund.")
44
+ except (InputGuardrailTripwireTriggered, OutputGuardrailTripwireTriggered) as exc:
45
+ evaluation = exc.guardrail_result.output.output_info["evaluation"]
46
+ print(evaluation.verdict, evaluation.failed_rules) # "fail" ["pii_detected"]
47
+ ```
48
+
49
+ 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=...)`.
50
+
51
+ ## LangChain
52
+
53
+ Pipe a guard after your model. It scores the answer and acts on the verdict before anything downstream sees it.
54
+
55
+ ```python
56
+ from overwing.langchain import overwing_guard, OverwingGuardrailError
57
+
58
+ chain = prompt | llm | overwing_guard(on_fail="replace") # or on_fail="raise" (default) / "annotate"
59
+ msg = chain.invoke({"question": "..."})
60
+ msg.response_metadata["overwing"] # {"verdict": "pass", "confidence": 0.97, "failed_rules": [], ...}
61
+ ```
62
+
63
+ Or observe every LLM call with a callback handler, which aborts the run on `fail`:
64
+
65
+ ```python
66
+ from overwing.langchain import OverwingCallbackHandler
67
+
68
+ handler = OverwingCallbackHandler(check_input=True) # also scores the user's prompt
69
+ llm.invoke("...", config={"callbacks": [handler]})
70
+ handler.verdicts # [("input", Evaluation), ("output", Evaluation), ...]
71
+ ```
72
+
73
+ Both accept `rule_set`, `metadata`, `on_verdict`, and `fail_open`. There is an `AsyncOverwingCallbackHandler` too.
74
+
75
+ ## Client
76
+
77
+ ```python
78
+ from overwing import Overwing, AsyncOverwing
79
+
80
+ ow = Overwing() # or Overwing(api_key="ow_live_...")
81
+
82
+ e = ow.evaluate("Reach me at dana@example.com to sort out the refund.")
83
+ e.verdict # "fail"
84
+ e.failed_rules # ["pii_detected"]
85
+ e.results[1] # RuleResult(rule="pii_detected", answer=True, confidence=0.98, verdict="fail", ...)
86
+
87
+ batch = ow.evaluate_batch([{"id": "a", "input": "..."}, {"id": "b", "input": "..."}])
88
+ ow.create_rule_set(name="Support tone", slug="support-tone", rules=[...])
89
+ ow.usage()
90
+
91
+ async with AsyncOverwing() as aow:
92
+ e = await aow.evaluate("...")
93
+ ```
94
+
95
+ `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+.
96
+
97
+ ## How verdicts work
98
+
99
+ 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).
100
+
101
+ ## Also from Overwing
102
+
103
+ - [`overwing`](https://github.com/frod27/overwing-js) on npm: the same client, a Vercel AI SDK middleware, and Agents SDK guardrails for JavaScript.
104
+ - [`overwing-mcp`](https://github.com/frod27/overwing-mcp): the guardrails as MCP tools for Claude, Cursor, and any MCP client.
105
+
106
+ MIT © Overwing. Verdicts are produced by TypeSafe's Jev System One model; Overwing is not affiliated with TypeSafe.
@@ -0,0 +1,6 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="220" height="44" viewBox="0 0 220 44" role="img" aria-label="Overwing">
2
+ <g fill="#E8EDF4" transform="translate(4,6)">
3
+ <path d="M4 8h24l-6 5H4z"/><path d="M4 15h16l-5 5H4z"/><path d="M4 22h8l-3 4H4z"/><rect x="4" y="28" width="24" height="2" rx="1"/>
4
+ </g>
5
+ <text x="46" y="30" font-family="ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif" font-size="24" font-weight="700" letter-spacing="-0.5" fill="#E8EDF4">Overwing</text>
6
+ </svg>
@@ -0,0 +1,6 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="220" height="44" viewBox="0 0 220 44" role="img" aria-label="Overwing">
2
+ <g fill="#0B1220" transform="translate(4,6)">
3
+ <path d="M4 8h24l-6 5H4z"/><path d="M4 15h16l-5 5H4z"/><path d="M4 22h8l-3 4H4z"/><rect x="4" y="28" width="24" height="2" rx="1"/>
4
+ </g>
5
+ <text x="46" y="30" font-family="ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif" font-size="24" font-weight="700" letter-spacing="-0.5" fill="#0B1220">Overwing</text>
6
+ </svg>
@@ -0,0 +1,52 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.25"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "overwing"
7
+ version = "0.1.0"
8
+ description = "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."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.10"
12
+ authors = [{ name = "Overwing", email = "support@overwing.ai" }]
13
+ keywords = ["overwing", "guardrails", "llm", "ai-safety", "content-moderation", "pii", "toxicity", "openai-agents", "langchain", "agents"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.10",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Programming Language :: Python :: 3.13",
23
+ "Topic :: Software Development :: Libraries",
24
+ "Typing :: Typed",
25
+ ]
26
+ dependencies = ["httpx>=0.27"]
27
+
28
+ [project.optional-dependencies]
29
+ agents = ["openai-agents>=0.1"]
30
+ langchain = ["langchain-core>=0.3"]
31
+
32
+ [project.urls]
33
+ Homepage = "https://overwing.ai"
34
+ Documentation = "https://overwing.ai/docs"
35
+ Repository = "https://github.com/frod27/overwing-python"
36
+ Issues = "https://github.com/frod27/overwing-python/issues"
37
+
38
+ [dependency-groups]
39
+ dev = [
40
+ "pytest>=8",
41
+ "pytest-asyncio>=0.24",
42
+ "openai-agents>=0.1",
43
+ "mypy>=1.11",
44
+ "langchain-core>=1.6.4",
45
+ ]
46
+
47
+ [tool.hatch.build.targets.wheel]
48
+ packages = ["src/overwing"]
49
+
50
+ [tool.pytest.ini_options]
51
+ asyncio_mode = "auto"
52
+ testpaths = ["tests"]
@@ -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"
@@ -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")
@@ -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
@@ -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
+ )