langchain-breakwire 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.
- langchain_breakwire-0.1.0/LICENSE +21 -0
- langchain_breakwire-0.1.0/PKG-INFO +49 -0
- langchain_breakwire-0.1.0/README.md +32 -0
- langchain_breakwire-0.1.0/pyproject.toml +23 -0
- langchain_breakwire-0.1.0/setup.cfg +4 -0
- langchain_breakwire-0.1.0/src/langchain_breakwire/__init__.py +126 -0
- langchain_breakwire-0.1.0/src/langchain_breakwire.egg-info/PKG-INFO +49 -0
- langchain_breakwire-0.1.0/src/langchain_breakwire.egg-info/SOURCES.txt +10 -0
- langchain_breakwire-0.1.0/src/langchain_breakwire.egg-info/dependency_links.txt +1 -0
- langchain_breakwire-0.1.0/src/langchain_breakwire.egg-info/requires.txt +2 -0
- langchain_breakwire-0.1.0/src/langchain_breakwire.egg-info/top_level.txt +1 -0
- langchain_breakwire-0.1.0/tests/test_unit.py +72 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ethan Hawkes
|
|
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,49 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: langchain-breakwire
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: LangChain tools and callback handler for Breakwire: look up API/SDK errors and get the source-linked change that explains them, with the fix.
|
|
5
|
+
Author: Breakwire
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://breakwire.org
|
|
8
|
+
Project-URL: Source, https://github.com/breakwire-org/breakwire/tree/main/packages/langchain-breakwire
|
|
9
|
+
Project-URL: Issues, https://github.com/breakwire-org/breakwire/issues
|
|
10
|
+
Keywords: langchain,breakwire,errors,breaking-changes,deprecations,ai-agents
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: breakwire>=0.1.0
|
|
15
|
+
Requires-Dist: langchain-core>=0.3
|
|
16
|
+
Dynamic: license-file
|
|
17
|
+
|
|
18
|
+
# langchain-breakwire
|
|
19
|
+
|
|
20
|
+
LangChain integration for [Breakwire](https://breakwire.org): tools an agent can call to look up an API/SDK error or
|
|
21
|
+
check upcoming deadlines, and a callback handler that looks up errors raised in your chains, LLM calls and tools.
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install langchain-breakwire
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
from langchain_breakwire import BreakwireLookupTool, BreakwireDeadlinesTool, BreakwireCallbackHandler
|
|
29
|
+
|
|
30
|
+
tools = [BreakwireLookupTool(), BreakwireDeadlinesTool()] # give them to your agent
|
|
31
|
+
|
|
32
|
+
handler = BreakwireCallbackHandler(service="openai-api")
|
|
33
|
+
try:
|
|
34
|
+
chain.invoke(inputs, config={"callbacks": [handler]})
|
|
35
|
+
except Exception as e:
|
|
36
|
+
print(e.breakwire.id, e.breakwire.fix) # same exception, explanation attached
|
|
37
|
+
raise
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
- `BreakwireLookupTool` (`breakwire_lookup_error`): error text (+ optional service, sdk, sdk_version, model) → compact
|
|
41
|
+
matches with id, confidence, fix, source URL. Unmatched errors are not logged unless `log_miss=True`.
|
|
42
|
+
- `BreakwireDeadlinesTool` (`breakwire_deadlines`): upcoming act-by dates for a service.
|
|
43
|
+
- `BreakwireCallbackHandler`: `on_llm_error` / `on_tool_error` / `on_chain_error`; each exception is looked up once even
|
|
44
|
+
though LangChain reports it at every nesting level. Never raises, never changes the error. `on_match(info, error)` hook.
|
|
45
|
+
|
|
46
|
+
Built on the stdlib-only [`breakwire`](https://pypi.org/project/breakwire/) client, which strips secrets before sending.
|
|
47
|
+
Breakwire's community verification has just started; claims link primary sources, prefer those.
|
|
48
|
+
|
|
49
|
+
MIT.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# langchain-breakwire
|
|
2
|
+
|
|
3
|
+
LangChain integration for [Breakwire](https://breakwire.org): tools an agent can call to look up an API/SDK error or
|
|
4
|
+
check upcoming deadlines, and a callback handler that looks up errors raised in your chains, LLM calls and tools.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
pip install langchain-breakwire
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
```python
|
|
11
|
+
from langchain_breakwire import BreakwireLookupTool, BreakwireDeadlinesTool, BreakwireCallbackHandler
|
|
12
|
+
|
|
13
|
+
tools = [BreakwireLookupTool(), BreakwireDeadlinesTool()] # give them to your agent
|
|
14
|
+
|
|
15
|
+
handler = BreakwireCallbackHandler(service="openai-api")
|
|
16
|
+
try:
|
|
17
|
+
chain.invoke(inputs, config={"callbacks": [handler]})
|
|
18
|
+
except Exception as e:
|
|
19
|
+
print(e.breakwire.id, e.breakwire.fix) # same exception, explanation attached
|
|
20
|
+
raise
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
- `BreakwireLookupTool` (`breakwire_lookup_error`): error text (+ optional service, sdk, sdk_version, model) → compact
|
|
24
|
+
matches with id, confidence, fix, source URL. Unmatched errors are not logged unless `log_miss=True`.
|
|
25
|
+
- `BreakwireDeadlinesTool` (`breakwire_deadlines`): upcoming act-by dates for a service.
|
|
26
|
+
- `BreakwireCallbackHandler`: `on_llm_error` / `on_tool_error` / `on_chain_error`; each exception is looked up once even
|
|
27
|
+
though LangChain reports it at every nesting level. Never raises, never changes the error. `on_match(info, error)` hook.
|
|
28
|
+
|
|
29
|
+
Built on the stdlib-only [`breakwire`](https://pypi.org/project/breakwire/) client, which strips secrets before sending.
|
|
30
|
+
Breakwire's community verification has just started; claims link primary sources, prefer those.
|
|
31
|
+
|
|
32
|
+
MIT.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "langchain-breakwire"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "LangChain tools and callback handler for Breakwire: look up API/SDK errors and get the source-linked change that explains them, with the fix."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
requires-python = ">=3.10"
|
|
13
|
+
authors = [{ name = "Breakwire" }]
|
|
14
|
+
keywords = ["langchain", "breakwire", "errors", "breaking-changes", "deprecations", "ai-agents"]
|
|
15
|
+
dependencies = ["breakwire>=0.1.0", "langchain-core>=0.3"]
|
|
16
|
+
|
|
17
|
+
[project.urls]
|
|
18
|
+
Homepage = "https://breakwire.org"
|
|
19
|
+
Source = "https://github.com/breakwire-org/breakwire/tree/main/packages/langchain-breakwire"
|
|
20
|
+
Issues = "https://github.com/breakwire-org/breakwire/issues"
|
|
21
|
+
|
|
22
|
+
[tool.setuptools.packages.find]
|
|
23
|
+
where = ["src"]
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""LangChain integration for Breakwire: tools the agent can call, and a callback handler that looks up
|
|
2
|
+
errors raised inside chains, LLM calls and tools.
|
|
3
|
+
|
|
4
|
+
from langchain_breakwire import BreakwireLookupTool, BreakwireDeadlinesTool, BreakwireCallbackHandler
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import Any, Dict, List, Optional, Type
|
|
9
|
+
from uuid import UUID
|
|
10
|
+
|
|
11
|
+
from breakwire import Breakwire, explain
|
|
12
|
+
from langchain_core.callbacks import BaseCallbackHandler
|
|
13
|
+
from langchain_core.tools import BaseTool
|
|
14
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
15
|
+
|
|
16
|
+
__version__ = "0.1.0"
|
|
17
|
+
__all__ = ["BreakwireLookupTool", "BreakwireDeadlinesTool", "BreakwireCallbackHandler", "__version__"]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _compact(r: Dict[str, Any], max_matches: int = 3) -> Dict[str, Any]:
|
|
21
|
+
cov = r.get("coverage") or {}
|
|
22
|
+
return {
|
|
23
|
+
"lookup_id": r.get("lookup_id"),
|
|
24
|
+
"explained": r.get("explained"),
|
|
25
|
+
"coverage_note": cov.get("note") if cov and not cov.get("covered", True) else None,
|
|
26
|
+
"matches": [
|
|
27
|
+
{"id": m.get("cid"), "title": m.get("title"), "confidence": m.get("confidence"), "fix": m.get("fix"),
|
|
28
|
+
"deadline": m.get("deadline"), "url": m.get("url"), "source_url": m.get("source_url")}
|
|
29
|
+
for m in (r.get("matches") or [])[:max_matches]
|
|
30
|
+
],
|
|
31
|
+
"note": 'Community data: "signature" = vendor-documented error text matched; "related" = a lead only. Prefer source_url.',
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class _LookupInput(BaseModel):
|
|
36
|
+
error: str = Field(description="The error message text. Secrets are stripped before sending.")
|
|
37
|
+
service: Optional[str] = Field(default=None, description="Service slug, e.g. anthropic-api, openai-api, stripe, github-api, mcp")
|
|
38
|
+
sdk: Optional[str] = Field(default=None)
|
|
39
|
+
sdk_version: Optional[str] = Field(default=None)
|
|
40
|
+
model: Optional[str] = Field(default=None)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class BreakwireLookupTool(BaseTool):
|
|
44
|
+
"""Look up an API/SDK/platform error in Breakwire and get the claim that explains it, with the fix."""
|
|
45
|
+
|
|
46
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
47
|
+
name: str = "breakwire_lookup_error"
|
|
48
|
+
description: str = (
|
|
49
|
+
"Look up an API/SDK/platform error message in Breakwire, a source-linked registry of breaking changes, "
|
|
50
|
+
"deprecations and advisories. Returns the matching claim and its fix. Use when a call fails with an error you do not recognise."
|
|
51
|
+
)
|
|
52
|
+
args_schema: Type[BaseModel] = _LookupInput
|
|
53
|
+
client: Breakwire = Field(default_factory=Breakwire)
|
|
54
|
+
log_miss: bool = False
|
|
55
|
+
max_matches: int = 3
|
|
56
|
+
|
|
57
|
+
def _run(self, error: str, service: Optional[str] = None, sdk: Optional[str] = None,
|
|
58
|
+
sdk_version: Optional[str] = None, model: Optional[str] = None, run_manager: Any = None) -> Dict[str, Any]:
|
|
59
|
+
try:
|
|
60
|
+
r = self.client.lookup(error, service=service, sdk=sdk, version=sdk_version, model=model, log_miss=self.log_miss)
|
|
61
|
+
return _compact(r, self.max_matches)
|
|
62
|
+
except Exception as e: # noqa: BLE001
|
|
63
|
+
return {"error": f"Breakwire unavailable: {e}"}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class _DeadlinesInput(BaseModel):
|
|
67
|
+
service: Optional[str] = Field(default=None, description="Service slug; omit for all services")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class BreakwireDeadlinesTool(BaseTool):
|
|
71
|
+
"""Upcoming act-by dates (model retirements, API shutdowns, required migrations), soonest first."""
|
|
72
|
+
|
|
73
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
74
|
+
name: str = "breakwire_deadlines"
|
|
75
|
+
description: str = "Upcoming act-by dates (model retirements, API shutdowns, required migrations) for a service, soonest first."
|
|
76
|
+
args_schema: Type[BaseModel] = _DeadlinesInput
|
|
77
|
+
client: Breakwire = Field(default_factory=Breakwire)
|
|
78
|
+
|
|
79
|
+
def _run(self, service: Optional[str] = None, run_manager: Any = None) -> Any:
|
|
80
|
+
try:
|
|
81
|
+
return [{"id": c.get("cid"), "title": c.get("title"), "deadline": c.get("deadline"), "fix": c.get("fix"), "url": c.get("url")}
|
|
82
|
+
for c in self.client.deadlines(service, limit=10)]
|
|
83
|
+
except Exception as e: # noqa: BLE001
|
|
84
|
+
return {"error": f"Breakwire unavailable: {e}"}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class BreakwireCallbackHandler(BaseCallbackHandler):
|
|
88
|
+
"""Looks up errors raised in LLM calls, tools and chains; sets ``error.breakwire`` and calls ``on_match``.
|
|
89
|
+
|
|
90
|
+
Each exception is looked up once even though LangChain reports it at every level of nesting.
|
|
91
|
+
Never raises (``raise_error`` is False), never changes the error.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
raise_error: bool = False
|
|
95
|
+
|
|
96
|
+
def __init__(self, service: Optional[str] = None, client: Optional[Breakwire] = None, timeout: float = 2.5,
|
|
97
|
+
log_miss: bool = True, on_match: Optional[Any] = None, **lookup: Any):
|
|
98
|
+
self.client = client or Breakwire(timeout=timeout)
|
|
99
|
+
self.timeout = timeout
|
|
100
|
+
self.lookup = {"service": service, "log_miss": log_miss, **lookup}
|
|
101
|
+
self.on_match = on_match
|
|
102
|
+
self.results: List[Any] = []
|
|
103
|
+
|
|
104
|
+
def _handle(self, error: BaseException) -> None:
|
|
105
|
+
if not isinstance(error, Exception) or getattr(error, "breakwire", None) is not None:
|
|
106
|
+
return
|
|
107
|
+
info = explain(error, client=self.client, timeout=self.timeout, **{k: v for k, v in self.lookup.items() if v is not None})
|
|
108
|
+
try:
|
|
109
|
+
error.breakwire = info # type: ignore[attr-defined]
|
|
110
|
+
except Exception:
|
|
111
|
+
pass
|
|
112
|
+
self.results.append(info)
|
|
113
|
+
if self.on_match:
|
|
114
|
+
try:
|
|
115
|
+
self.on_match(info, error)
|
|
116
|
+
except Exception:
|
|
117
|
+
pass
|
|
118
|
+
|
|
119
|
+
def on_llm_error(self, error: BaseException, *, run_id: UUID, parent_run_id: Optional[UUID] = None, **kwargs: Any) -> None:
|
|
120
|
+
self._handle(error)
|
|
121
|
+
|
|
122
|
+
def on_tool_error(self, error: BaseException, *, run_id: UUID, parent_run_id: Optional[UUID] = None, **kwargs: Any) -> None:
|
|
123
|
+
self._handle(error)
|
|
124
|
+
|
|
125
|
+
def on_chain_error(self, error: BaseException, *, run_id: UUID, parent_run_id: Optional[UUID] = None, **kwargs: Any) -> None:
|
|
126
|
+
self._handle(error)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: langchain-breakwire
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: LangChain tools and callback handler for Breakwire: look up API/SDK errors and get the source-linked change that explains them, with the fix.
|
|
5
|
+
Author: Breakwire
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://breakwire.org
|
|
8
|
+
Project-URL: Source, https://github.com/breakwire-org/breakwire/tree/main/packages/langchain-breakwire
|
|
9
|
+
Project-URL: Issues, https://github.com/breakwire-org/breakwire/issues
|
|
10
|
+
Keywords: langchain,breakwire,errors,breaking-changes,deprecations,ai-agents
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: breakwire>=0.1.0
|
|
15
|
+
Requires-Dist: langchain-core>=0.3
|
|
16
|
+
Dynamic: license-file
|
|
17
|
+
|
|
18
|
+
# langchain-breakwire
|
|
19
|
+
|
|
20
|
+
LangChain integration for [Breakwire](https://breakwire.org): tools an agent can call to look up an API/SDK error or
|
|
21
|
+
check upcoming deadlines, and a callback handler that looks up errors raised in your chains, LLM calls and tools.
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install langchain-breakwire
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
from langchain_breakwire import BreakwireLookupTool, BreakwireDeadlinesTool, BreakwireCallbackHandler
|
|
29
|
+
|
|
30
|
+
tools = [BreakwireLookupTool(), BreakwireDeadlinesTool()] # give them to your agent
|
|
31
|
+
|
|
32
|
+
handler = BreakwireCallbackHandler(service="openai-api")
|
|
33
|
+
try:
|
|
34
|
+
chain.invoke(inputs, config={"callbacks": [handler]})
|
|
35
|
+
except Exception as e:
|
|
36
|
+
print(e.breakwire.id, e.breakwire.fix) # same exception, explanation attached
|
|
37
|
+
raise
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
- `BreakwireLookupTool` (`breakwire_lookup_error`): error text (+ optional service, sdk, sdk_version, model) → compact
|
|
41
|
+
matches with id, confidence, fix, source URL. Unmatched errors are not logged unless `log_miss=True`.
|
|
42
|
+
- `BreakwireDeadlinesTool` (`breakwire_deadlines`): upcoming act-by dates for a service.
|
|
43
|
+
- `BreakwireCallbackHandler`: `on_llm_error` / `on_tool_error` / `on_chain_error`; each exception is looked up once even
|
|
44
|
+
though LangChain reports it at every nesting level. Never raises, never changes the error. `on_match(info, error)` hook.
|
|
45
|
+
|
|
46
|
+
Built on the stdlib-only [`breakwire`](https://pypi.org/project/breakwire/) client, which strips secrets before sending.
|
|
47
|
+
Breakwire's community verification has just started; claims link primary sources, prefer those.
|
|
48
|
+
|
|
49
|
+
MIT.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/langchain_breakwire/__init__.py
|
|
5
|
+
src/langchain_breakwire.egg-info/PKG-INFO
|
|
6
|
+
src/langchain_breakwire.egg-info/SOURCES.txt
|
|
7
|
+
src/langchain_breakwire.egg-info/dependency_links.txt
|
|
8
|
+
src/langchain_breakwire.egg-info/requires.txt
|
|
9
|
+
src/langchain_breakwire.egg-info/top_level.txt
|
|
10
|
+
tests/test_unit.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
langchain_breakwire
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Offline tests with a fake Breakwire client. Run: python -m unittest discover -s tests"""
|
|
2
|
+
import pathlib
|
|
3
|
+
import sys
|
|
4
|
+
import unittest
|
|
5
|
+
|
|
6
|
+
ROOT = pathlib.Path(__file__).resolve().parents[1]
|
|
7
|
+
sys.path.insert(0, str(ROOT / "src"))
|
|
8
|
+
sys.path.insert(0, str(ROOT.parent / "python" / "src"))
|
|
9
|
+
|
|
10
|
+
from langchain_core.runnables import RunnableLambda # noqa: E402
|
|
11
|
+
from breakwire import Breakwire # noqa: E402
|
|
12
|
+
from langchain_breakwire import BreakwireCallbackHandler, BreakwireDeadlinesTool, BreakwireLookupTool # noqa: E402
|
|
13
|
+
|
|
14
|
+
LOOKUP = {"lookup_id": "lk_x", "explained": True, "coverage": None,
|
|
15
|
+
"matches": [{"cid": "AN-2026-0025", "title": "T", "confidence": "signature", "fix": "F", "url": "U", "source_url": "S", "deadline": None}]}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Fake(Breakwire):
|
|
19
|
+
def __init__(self, fail=False):
|
|
20
|
+
super().__init__(base_url="http://fake.invalid")
|
|
21
|
+
self.calls, self.fail = [], fail
|
|
22
|
+
|
|
23
|
+
def lookup(self, error, **kw):
|
|
24
|
+
self.calls.append(("lookup", str(error), kw))
|
|
25
|
+
if self.fail:
|
|
26
|
+
raise RuntimeError("down")
|
|
27
|
+
return LOOKUP
|
|
28
|
+
|
|
29
|
+
def deadlines(self, service=None, **kw):
|
|
30
|
+
self.calls.append(("deadlines", service, kw))
|
|
31
|
+
return [{"cid": "AN-2026-0004", "title": "D", "deadline": "2026-11-30", "fix": "F", "url": "U"}]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class T(unittest.TestCase):
|
|
35
|
+
def test_lookup_tool(self):
|
|
36
|
+
f = Fake()
|
|
37
|
+
t = BreakwireLookupTool(client=f)
|
|
38
|
+
out = t.invoke({"error": "boom", "service": "anthropic-api"})
|
|
39
|
+
self.assertEqual(out["matches"][0]["id"], "AN-2026-0025")
|
|
40
|
+
self.assertEqual(f.calls[0][2]["service"], "anthropic-api")
|
|
41
|
+
self.assertFalse(f.calls[0][2]["log_miss"])
|
|
42
|
+
self.assertIn("error", t.args)
|
|
43
|
+
|
|
44
|
+
def test_lookup_tool_never_raises(self):
|
|
45
|
+
out = BreakwireLookupTool(client=Fake(fail=True)).invoke({"error": "boom"})
|
|
46
|
+
self.assertIn("unavailable", out["error"])
|
|
47
|
+
|
|
48
|
+
def test_deadlines_tool(self):
|
|
49
|
+
out = BreakwireDeadlinesTool(client=Fake()).invoke({"service": "anthropic-api"})
|
|
50
|
+
self.assertEqual(out[0]["deadline"], "2026-11-30")
|
|
51
|
+
|
|
52
|
+
def test_callback_handler_once_per_exception(self):
|
|
53
|
+
f = Fake()
|
|
54
|
+
h = BreakwireCallbackHandler(service="openai-api", client=f)
|
|
55
|
+
inner = RunnableLambda(lambda x: (_ for _ in ()).throw(ValueError("Error code: 429 organization_spend_limit_exceeded")))
|
|
56
|
+
chain = RunnableLambda(lambda x: x) | inner
|
|
57
|
+
with self.assertRaises(ValueError) as cm:
|
|
58
|
+
chain.invoke("hi", config={"callbacks": [h]})
|
|
59
|
+
self.assertEqual(cm.exception.breakwire.id, "AN-2026-0025")
|
|
60
|
+
self.assertEqual(len([c for c in f.calls if c[0] == "lookup"]), 1)
|
|
61
|
+
|
|
62
|
+
def test_callback_handler_never_masks(self):
|
|
63
|
+
h = BreakwireCallbackHandler(client=Fake(fail=True))
|
|
64
|
+
err = KeyError("x")
|
|
65
|
+
with self.assertRaises(KeyError) as cm:
|
|
66
|
+
RunnableLambda(lambda x: (_ for _ in ()).throw(err)).invoke(1, config={"callbacks": [h]})
|
|
67
|
+
self.assertIs(cm.exception, err)
|
|
68
|
+
self.assertIsNone(err.breakwire.id)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
if __name__ == "__main__":
|
|
72
|
+
unittest.main()
|