langchain-brewpage 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,7 @@
1
+ __pycache__/
2
+ *.pyc
3
+ .venv/
4
+ dist/
5
+ build/
6
+ *.egg-info/
7
+ .pytest_cache/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Maksim Kochetkov
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,100 @@
1
+ Metadata-Version: 2.5
2
+ Name: langchain-brewpage
3
+ Version: 0.1.0
4
+ Summary: LangChain tools for BrewPage: publish HTML, Markdown, JSON, KV stores and files to brewpage.app and get a shareable URL.
5
+ Project-URL: Homepage, https://brewpage.app
6
+ Project-URL: Repository, https://github.com/kochetkov-ma/langchain-brewpage
7
+ Project-URL: Issues, https://github.com/kochetkov-ma/langchain-brewpage/issues
8
+ Project-URL: Documentation, https://github.com/kochetkov-ma/langchain-brewpage#readme
9
+ Author-email: Maksim Kochetkov <apmatypa88@gmail.com>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: agent,brewpage,hosting,langchain,publish,tools
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Topic :: Software Development :: Libraries
16
+ Requires-Python: <4.0,>=3.10
17
+ Requires-Dist: httpx==0.28.1
18
+ Requires-Dist: langchain-core==1.7.6
19
+ Description-Content-Type: text/markdown
20
+
21
+ # langchain-brewpage
22
+
23
+ LangChain tools for [BrewPage](https://brewpage.app): let an agent publish an HTML page, a Markdown document, a JSON document, a key-value store or a file and get back a shareable URL. No account and no API key are needed.
24
+
25
+ Each call returns the URL plus an owner token. The toolkit reuses that token for later publications, so everything the agent creates belongs to one owner. Publications are private and unlisted by default and expire after 15 days unless you set `ttl_days` (1-30).
26
+
27
+ ## Install
28
+
29
+ ```bash
30
+ pip install langchain-brewpage
31
+ ```
32
+
33
+ ## Tools
34
+
35
+ | Tool | API | Use |
36
+ |---|---|---|
37
+ | `BrewPagePublishPageTool` (`brewpage_publish_page`) | `POST /api/html` | HTML or Markdown page -> URL |
38
+ | `BrewPagePublishJsonTool` (`brewpage_publish_json`) | `POST /api/json` | JSON document -> URL |
39
+ | `BrewPageCreateKvStoreTool` (`brewpage_create_kv_store`) | `POST /api/kv` | Key-value store with one initial key -> URL |
40
+ | `BrewPageUploadFileTool` (`brewpage_upload_file`) | `POST /api/files` | Local file -> download or page URL |
41
+
42
+ `BrewPageToolkit` returns all four tools, sharing one client.
43
+
44
+ Every tool also accepts `ttl_days`, `password` and `namespace`. Leave `namespace` empty for a private link. `public` lists pages in the public gallery and search engines, so never put secrets or personal data there.
45
+
46
+ ## Usage
47
+
48
+ ```python
49
+ from langchain_brewpage import BrewPageToolkit
50
+
51
+ tools = BrewPageToolkit().get_tools()
52
+ page = tools[0].invoke({"content": "# Weekly report\n\nAll green.", "format": "markdown"})
53
+ print(page["url"], page["owner_token"])
54
+ ```
55
+
56
+ With an agent:
57
+
58
+ ```python
59
+ from langchain.agents import create_agent
60
+ from langchain_brewpage import BrewPageToolkit
61
+
62
+ agent = create_agent(model, tools=BrewPageToolkit().get_tools()) # any tool-calling chat model
63
+ agent.invoke({"messages": [{"role": "user", "content": "Write a one-page HTML changelog and publish it."}]})
64
+ ```
65
+
66
+ The result of each tool is a dict: `url`, `id`, `namespace`, `owner_token`, `management_link`, `expires_at`.
67
+
68
+ ## Configuration
69
+
70
+ | Setting | Constructor | Environment |
71
+ |---|---|---|
72
+ | API base URL | `BrewPageClient(base_url=...)` | `BREWPAGE_BASE_URL` (default `https://brewpage.app`) |
73
+ | Owner token to reuse | `BrewPageClient(owner_token=...)` | `BREWPAGE_OWNER_TOKEN` |
74
+ | Default namespace | `BrewPageClient(namespace=...)` | `BREWPAGE_NAMESPACE` |
75
+
76
+ ```python
77
+ from langchain_brewpage import BrewPageClient, BrewPageToolkit
78
+
79
+ toolkit = BrewPageToolkit(client=BrewPageClient(owner_token="..."))
80
+ ```
81
+
82
+ API errors (rate limits, refused content) come back to the agent as tool messages instead of exceptions.
83
+
84
+ ## Links
85
+
86
+ - API reference: https://kochetkov-ma.github.io/brewpage-openapi/
87
+ - OpenAPI contract: https://github.com/kochetkov-ma/brewpage-openapi
88
+ - Limits and terms: https://brewpage.app
89
+
90
+ ## Development
91
+
92
+ ```bash
93
+ uv sync --group test
94
+ uv run pytest
95
+ BREWPAGE_LIVE=1 uv run pytest -m live # one private page on production
96
+ ```
97
+
98
+ ## License
99
+
100
+ MIT
@@ -0,0 +1,80 @@
1
+ # langchain-brewpage
2
+
3
+ LangChain tools for [BrewPage](https://brewpage.app): let an agent publish an HTML page, a Markdown document, a JSON document, a key-value store or a file and get back a shareable URL. No account and no API key are needed.
4
+
5
+ Each call returns the URL plus an owner token. The toolkit reuses that token for later publications, so everything the agent creates belongs to one owner. Publications are private and unlisted by default and expire after 15 days unless you set `ttl_days` (1-30).
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pip install langchain-brewpage
11
+ ```
12
+
13
+ ## Tools
14
+
15
+ | Tool | API | Use |
16
+ |---|---|---|
17
+ | `BrewPagePublishPageTool` (`brewpage_publish_page`) | `POST /api/html` | HTML or Markdown page -> URL |
18
+ | `BrewPagePublishJsonTool` (`brewpage_publish_json`) | `POST /api/json` | JSON document -> URL |
19
+ | `BrewPageCreateKvStoreTool` (`brewpage_create_kv_store`) | `POST /api/kv` | Key-value store with one initial key -> URL |
20
+ | `BrewPageUploadFileTool` (`brewpage_upload_file`) | `POST /api/files` | Local file -> download or page URL |
21
+
22
+ `BrewPageToolkit` returns all four tools, sharing one client.
23
+
24
+ Every tool also accepts `ttl_days`, `password` and `namespace`. Leave `namespace` empty for a private link. `public` lists pages in the public gallery and search engines, so never put secrets or personal data there.
25
+
26
+ ## Usage
27
+
28
+ ```python
29
+ from langchain_brewpage import BrewPageToolkit
30
+
31
+ tools = BrewPageToolkit().get_tools()
32
+ page = tools[0].invoke({"content": "# Weekly report\n\nAll green.", "format": "markdown"})
33
+ print(page["url"], page["owner_token"])
34
+ ```
35
+
36
+ With an agent:
37
+
38
+ ```python
39
+ from langchain.agents import create_agent
40
+ from langchain_brewpage import BrewPageToolkit
41
+
42
+ agent = create_agent(model, tools=BrewPageToolkit().get_tools()) # any tool-calling chat model
43
+ agent.invoke({"messages": [{"role": "user", "content": "Write a one-page HTML changelog and publish it."}]})
44
+ ```
45
+
46
+ The result of each tool is a dict: `url`, `id`, `namespace`, `owner_token`, `management_link`, `expires_at`.
47
+
48
+ ## Configuration
49
+
50
+ | Setting | Constructor | Environment |
51
+ |---|---|---|
52
+ | API base URL | `BrewPageClient(base_url=...)` | `BREWPAGE_BASE_URL` (default `https://brewpage.app`) |
53
+ | Owner token to reuse | `BrewPageClient(owner_token=...)` | `BREWPAGE_OWNER_TOKEN` |
54
+ | Default namespace | `BrewPageClient(namespace=...)` | `BREWPAGE_NAMESPACE` |
55
+
56
+ ```python
57
+ from langchain_brewpage import BrewPageClient, BrewPageToolkit
58
+
59
+ toolkit = BrewPageToolkit(client=BrewPageClient(owner_token="..."))
60
+ ```
61
+
62
+ API errors (rate limits, refused content) come back to the agent as tool messages instead of exceptions.
63
+
64
+ ## Links
65
+
66
+ - API reference: https://kochetkov-ma.github.io/brewpage-openapi/
67
+ - OpenAPI contract: https://github.com/kochetkov-ma/brewpage-openapi
68
+ - Limits and terms: https://brewpage.app
69
+
70
+ ## Development
71
+
72
+ ```bash
73
+ uv sync --group test
74
+ uv run pytest
75
+ BREWPAGE_LIVE=1 uv run pytest -m live # one private page on production
76
+ ```
77
+
78
+ ## License
79
+
80
+ MIT
@@ -0,0 +1,20 @@
1
+ """LangChain integration for BrewPage: publish HTML, Markdown, JSON, KV and files, get a URL."""
2
+
3
+ from langchain_brewpage.client import BrewPageClient, BrewPageError
4
+ from langchain_brewpage.tools import (
5
+ BrewPageCreateKvStoreTool,
6
+ BrewPagePublishJsonTool,
7
+ BrewPagePublishPageTool,
8
+ BrewPageToolkit,
9
+ BrewPageUploadFileTool,
10
+ )
11
+
12
+ __all__ = [
13
+ "BrewPageClient",
14
+ "BrewPageError",
15
+ "BrewPageCreateKvStoreTool",
16
+ "BrewPagePublishJsonTool",
17
+ "BrewPagePublishPageTool",
18
+ "BrewPageToolkit",
19
+ "BrewPageUploadFileTool",
20
+ ]
@@ -0,0 +1,88 @@
1
+ """Minimal synchronous client for the public BrewPage API (https://brewpage.app)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import mimetypes
7
+ import os
8
+ from pathlib import Path
9
+ from typing import Any, Dict, Optional
10
+
11
+ import httpx
12
+
13
+ DEFAULT_BASE_URL = "https://brewpage.app"
14
+ USER_AGENT = "langchain-brewpage/0.1.0"
15
+
16
+
17
+ class BrewPageError(RuntimeError):
18
+ """Raised when the BrewPage API answers with a non-2xx status."""
19
+
20
+
21
+ class BrewPageClient:
22
+ """Publishes pages, JSON, KV stores and files; reuses one owner token for all of them."""
23
+
24
+ def __init__(
25
+ self,
26
+ base_url: Optional[str] = None,
27
+ owner_token: Optional[str] = None,
28
+ namespace: Optional[str] = None,
29
+ timeout: float = 60.0,
30
+ transport: Optional[httpx.BaseTransport] = None,
31
+ ) -> None:
32
+ self.base_url = (base_url or os.environ.get("BREWPAGE_BASE_URL") or DEFAULT_BASE_URL).rstrip("/")
33
+ self.owner_token = owner_token or os.environ.get("BREWPAGE_OWNER_TOKEN") or None
34
+ self.namespace = namespace or os.environ.get("BREWPAGE_NAMESPACE") or None
35
+ self._http = httpx.Client(timeout=timeout, transport=transport, headers={"User-Agent": USER_AGENT})
36
+
37
+ def publish_page(self, content: str, format: str = "html", ttl_days: Optional[int] = None,
38
+ password: Optional[str] = None, namespace: Optional[str] = None) -> Dict[str, Any]:
39
+ body = {"content": content, "format": format}
40
+ return self._post("/api/html", namespace, ttl_days, password, json_body=body)
41
+
42
+ def publish_json(self, data: Any, ttl_days: Optional[int] = None, password: Optional[str] = None,
43
+ namespace: Optional[str] = None) -> Dict[str, Any]:
44
+ return self._post("/api/json", namespace, ttl_days, password, json_body=data)
45
+
46
+ def create_kv_store(self, key: str, value: str, ttl_days: Optional[int] = None,
47
+ password: Optional[str] = None, namespace: Optional[str] = None) -> Dict[str, Any]:
48
+ return self._post("/api/kv", namespace, ttl_days, password, json_body={"key": key, "value": value})
49
+
50
+ def upload_file(self, path: str, ttl_days: Optional[int] = None, password: Optional[str] = None,
51
+ namespace: Optional[str] = None) -> Dict[str, Any]:
52
+ file = Path(path)
53
+ mime = mimetypes.guess_type(file.name)[0] or "application/octet-stream"
54
+ files = {"file": (file.name, file.read_bytes(), mime)}
55
+ return self._post("/api/files", namespace, ttl_days, password, files=files)
56
+
57
+ def _post(self, path: str, namespace: Optional[str], ttl_days: Optional[int], password: Optional[str],
58
+ json_body: Any = None, files: Any = None) -> Dict[str, Any]:
59
+ params = {}
60
+ ns = namespace or self.namespace
61
+ if ns:
62
+ params["ns"] = ns
63
+ if ttl_days is not None:
64
+ params["ttl"] = str(ttl_days)
65
+ headers = {}
66
+ if self.owner_token:
67
+ headers["X-Owner-Token"] = self.owner_token
68
+ if password:
69
+ headers["X-Password"] = password
70
+ if files is None:
71
+ headers["Content-Type"] = "application/json"
72
+ response = self._http.post(self.base_url + path, params=params, headers=headers,
73
+ content=json.dumps(json_body).encode("utf-8"))
74
+ else:
75
+ response = self._http.post(self.base_url + path, params=params, headers=headers, files=files)
76
+ if response.status_code >= 300:
77
+ raise BrewPageError(f"BrewPage {path} failed with HTTP {response.status_code}: {response.text[:500]}")
78
+ payload = response.json()
79
+ if not self.owner_token and payload.get("ownerToken"):
80
+ self.owner_token = payload["ownerToken"]
81
+ return {
82
+ "url": payload.get("link"),
83
+ "id": payload.get("id"),
84
+ "namespace": payload.get("namespace"),
85
+ "owner_token": payload.get("ownerToken") or self.owner_token,
86
+ "management_link": payload.get("managementLink"),
87
+ "expires_at": payload.get("expiresAt"),
88
+ }
File without changes
@@ -0,0 +1,115 @@
1
+ """LangChain tools and toolkit for BrewPage publishing."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from typing import Any, Dict, List, Literal, Optional, Type
7
+
8
+ from langchain_core.tools import BaseTool, BaseToolkit, ToolException
9
+ from pydantic import BaseModel, ConfigDict, Field
10
+
11
+ from langchain_brewpage.client import BrewPageClient, BrewPageError
12
+
13
+ _NS = ("Namespace: omit for a private unlisted link; 'public' lists pages in the public gallery and search. "
14
+ "Never put secrets or personal data in 'public'.")
15
+ _TTL = "Lifetime in days (1-30); omit for the 15-day default."
16
+ _PASSWORD = "Optional password readers must enter."
17
+
18
+
19
+ class _CommonInput(BaseModel):
20
+ ttl_days: Optional[int] = Field(default=None, description=_TTL)
21
+ password: Optional[str] = Field(default=None, description=_PASSWORD)
22
+ namespace: Optional[str] = Field(default=None, description=_NS)
23
+
24
+
25
+ class PublishPageInput(_CommonInput):
26
+ content: str = Field(description="HTML document or Markdown text to publish.")
27
+ format: Literal["html", "markdown"] = Field(default="html", description="'html' or 'markdown'.")
28
+
29
+
30
+ class PublishJsonInput(_CommonInput):
31
+ json_text: str = Field(description="A valid JSON document as text.")
32
+
33
+
34
+ class CreateKvStoreInput(_CommonInput):
35
+ key: str = Field(description="Initial key name.")
36
+ value: str = Field(description="Value for the initial key (max 1 MB).")
37
+
38
+
39
+ class UploadFileInput(_CommonInput):
40
+ path: str = Field(description="Local path of the file to upload.")
41
+
42
+
43
+ class _BrewPageTool(BaseTool):
44
+ model_config = ConfigDict(arbitrary_types_allowed=True)
45
+ client: BrewPageClient = Field(default_factory=BrewPageClient, exclude=True)
46
+ handle_tool_error: bool = True
47
+
48
+ def _call(self, method: str, **kwargs: Any) -> Dict[str, Any]:
49
+ try:
50
+ return getattr(self.client, method)(**kwargs)
51
+ except (BrewPageError, OSError, ValueError) as error:
52
+ raise ToolException(str(error)) from error
53
+
54
+
55
+ class BrewPagePublishPageTool(_BrewPageTool):
56
+ name: str = "brewpage_publish_page"
57
+ description: str = ("Publish an HTML page or Markdown document on brewpage.app and get a shareable URL "
58
+ "plus an owner token for later edits. No account needed.")
59
+ args_schema: Type[BaseModel] = PublishPageInput
60
+
61
+ def _run(self, content: str, format: str = "html", ttl_days: Optional[int] = None,
62
+ password: Optional[str] = None, namespace: Optional[str] = None, **_: Any) -> Dict[str, Any]:
63
+ return self._call("publish_page", content=content, format=format, ttl_days=ttl_days,
64
+ password=password, namespace=namespace)
65
+
66
+
67
+ class BrewPagePublishJsonTool(_BrewPageTool):
68
+ name: str = "brewpage_publish_json"
69
+ description: str = "Host a JSON document on brewpage.app and get a URL that serves it."
70
+ args_schema: Type[BaseModel] = PublishJsonInput
71
+
72
+ def _run(self, json_text: str, ttl_days: Optional[int] = None, password: Optional[str] = None,
73
+ namespace: Optional[str] = None, **_: Any) -> Dict[str, Any]:
74
+ try:
75
+ data = json.loads(json_text)
76
+ except ValueError as error:
77
+ raise ToolException(f"json_text is not valid JSON: {error}") from error
78
+ return self._call("publish_json", data=data, ttl_days=ttl_days, password=password, namespace=namespace)
79
+
80
+
81
+ class BrewPageCreateKvStoreTool(_BrewPageTool):
82
+ name: str = "brewpage_create_kv_store"
83
+ description: str = "Create a key-value store on brewpage.app with one initial key; returns its URL."
84
+ args_schema: Type[BaseModel] = CreateKvStoreInput
85
+
86
+ def _run(self, key: str, value: str, ttl_days: Optional[int] = None, password: Optional[str] = None,
87
+ namespace: Optional[str] = None, **_: Any) -> Dict[str, Any]:
88
+ return self._call("create_kv_store", key=key, value=value, ttl_days=ttl_days,
89
+ password=password, namespace=namespace)
90
+
91
+
92
+ class BrewPageUploadFileTool(_BrewPageTool):
93
+ name: str = "brewpage_upload_file"
94
+ description: str = ("Upload a local file (image, PDF, archive, document, HTML or Markdown) to brewpage.app "
95
+ "and get a download or page URL.")
96
+ args_schema: Type[BaseModel] = UploadFileInput
97
+
98
+ def _run(self, path: str, ttl_days: Optional[int] = None, password: Optional[str] = None,
99
+ namespace: Optional[str] = None, **_: Any) -> Dict[str, Any]:
100
+ return self._call("upload_file", path=path, ttl_days=ttl_days, password=password, namespace=namespace)
101
+
102
+
103
+ class BrewPageToolkit(BaseToolkit):
104
+ """All BrewPage tools sharing one client, so every publication groups under one owner token."""
105
+
106
+ model_config = ConfigDict(arbitrary_types_allowed=True)
107
+ client: BrewPageClient = Field(default_factory=BrewPageClient)
108
+
109
+ def get_tools(self) -> List[BaseTool]:
110
+ return [
111
+ BrewPagePublishPageTool(client=self.client),
112
+ BrewPagePublishJsonTool(client=self.client),
113
+ BrewPageCreateKvStoreTool(client=self.client),
114
+ BrewPageUploadFileTool(client=self.client),
115
+ ]
@@ -0,0 +1,41 @@
1
+ [build-system]
2
+ requires = ["hatchling==1.33.0"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "langchain-brewpage"
7
+ version = "0.1.0"
8
+ description = "LangChain tools for BrewPage: publish HTML, Markdown, JSON, KV stores and files to brewpage.app and get a shareable URL."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ authors = [{ name = "Maksim Kochetkov", email = "apmatypa88@gmail.com" }]
13
+ requires-python = ">=3.10,<4.0"
14
+ keywords = ["langchain", "brewpage", "hosting", "publish", "agent", "tools"]
15
+ classifiers = [
16
+ "Programming Language :: Python :: 3",
17
+ "Topic :: Software Development :: Libraries",
18
+ "Intended Audience :: Developers",
19
+ ]
20
+ dependencies = [
21
+ "langchain-core==1.7.6",
22
+ "httpx==0.28.1",
23
+ ]
24
+
25
+ [project.urls]
26
+ Homepage = "https://brewpage.app"
27
+ Repository = "https://github.com/kochetkov-ma/langchain-brewpage"
28
+ Issues = "https://github.com/kochetkov-ma/langchain-brewpage/issues"
29
+ Documentation = "https://github.com/kochetkov-ma/langchain-brewpage#readme"
30
+
31
+ [dependency-groups]
32
+ test = ["pytest==9.2.0"]
33
+
34
+ [tool.hatch.build.targets.wheel]
35
+ packages = ["langchain_brewpage"]
36
+
37
+ [tool.hatch.build.targets.sdist]
38
+ include = ["langchain_brewpage", "tests", "README.md", "LICENSE"]
39
+
40
+ [tool.pytest.ini_options]
41
+ markers = ["live: hits production brewpage.app (set BREWPAGE_LIVE=1)"]
@@ -0,0 +1,105 @@
1
+ import json
2
+ import os
3
+ import re
4
+
5
+ import httpx
6
+ import pytest
7
+
8
+ from langchain_brewpage import BrewPageClient, BrewPageToolkit
9
+
10
+ API_REPLY = {
11
+ "link": "https://brewpage.app/my/abc123",
12
+ "id": "abc123",
13
+ "namespace": "my",
14
+ "ownerToken": "tok-1",
15
+ "managementLink": "https://brewpage.app/my/abc123?manage=k1",
16
+ "expiresAt": "2026-11-20T00:00:00Z",
17
+ }
18
+ BOUNDARY = re.compile(r"[0-9a-f]{32}")
19
+ RESULT = {
20
+ "url": "https://brewpage.app/my/abc123",
21
+ "id": "abc123",
22
+ "namespace": "my",
23
+ "owner_token": "tok-1",
24
+ "management_link": "https://brewpage.app/my/abc123?manage=k1",
25
+ "expires_at": "2026-11-20T00:00:00Z",
26
+ }
27
+
28
+
29
+ def _recording_client(status, reply, seen):
30
+ def handler(request):
31
+ seen.append({
32
+ "method": request.method,
33
+ "url": str(request.url),
34
+ "content_type": BOUNDARY.sub("B", request.headers["content-type"]),
35
+ "owner_token": request.headers.get("x-owner-token"),
36
+ "password": request.headers.get("x-password"),
37
+ "body": BOUNDARY.sub("B", request.content.decode("utf-8")),
38
+ })
39
+ return httpx.Response(status, json=reply)
40
+
41
+ return BrewPageClient(owner_token="tok-1", namespace="my", transport=httpx.MockTransport(handler))
42
+
43
+
44
+ @pytest.mark.parametrize(
45
+ ("tool_index", "schema", "args", "expected_request"),
46
+ [
47
+ (0, ("brewpage_publish_page", ["content"]), {"content": "# Hi", "format": "markdown", "ttl_days": 3},
48
+ {"method": "POST", "url": "https://brewpage.app/api/html?ns=my&ttl=3", "content_type": "application/json",
49
+ "owner_token": "tok-1", "password": None, "body": '{"content": "# Hi", "format": "markdown"}'}),
50
+ (1, ("brewpage_publish_json", ["json_text"]), {"json_text": '{"a": [1, 2]}', "password": "pw"},
51
+ {"method": "POST", "url": "https://brewpage.app/api/json?ns=my", "content_type": "application/json",
52
+ "owner_token": "tok-1", "password": "pw", "body": '{"a": [1, 2]}'}),
53
+ (2, ("brewpage_create_kv_store", ["key", "value"]), {"key": "status", "value": "green"},
54
+ {"method": "POST", "url": "https://brewpage.app/api/kv?ns=my", "content_type": "application/json",
55
+ "owner_token": "tok-1", "password": None, "body": '{"key": "status", "value": "green"}'}),
56
+ (3, ("brewpage_upload_file", ["path"]), {"path": "{tmp}/notes.txt"},
57
+ {"method": "POST", "url": "https://brewpage.app/api/files?ns=my", "content_type": "multipart/form-data; boundary=B",
58
+ "owner_token": "tok-1", "password": None,
59
+ "body": '--B\r\nContent-Disposition: form-data; name="file"; filename="notes.txt"\r\n'
60
+ 'Content-Type: text/plain\r\n\r\nhello\r\n--B--\r\n'}),
61
+ ],
62
+ ids=["page", "json", "kv", "file"],
63
+ )
64
+ def test_tool_posts_exact_request_and_returns_link(tmp_path, tool_index, schema, args, expected_request):
65
+ # GIVEN a toolkit whose client answers every POST with a fixed API reply
66
+ (tmp_path / "notes.txt").write_text("hello")
67
+ seen = []
68
+ tool = BrewPageToolkit(client=_recording_client(201, API_REPLY, seen)).get_tools()[tool_index]
69
+ resolved = json.loads(json.dumps(args).replace("{tmp}", str(tmp_path)))
70
+
71
+ # WHEN the agent invokes the tool
72
+ result = tool.invoke(resolved)
73
+
74
+ # THEN the tool schema, the single request and the mapped result match exactly
75
+ assert ((tool.name, tool.args_schema.model_json_schema()["required"]), seen, result) == (schema, [expected_request], RESULT), f"tool {tool.name} sent {seen} and returned {result}"
76
+
77
+
78
+ def test_api_error_becomes_tool_message():
79
+ # GIVEN an API that rate-limits the caller
80
+ seen = []
81
+ tool = BrewPageToolkit(client=_recording_client(429, {"message": "slow down"}, seen)).get_tools()[2]
82
+
83
+ # WHEN the tool is invoked
84
+ result = tool.invoke({"key": "k", "value": "v"})
85
+
86
+ # THEN the agent receives the error text instead of an exception
87
+ assert result == 'BrewPage /api/kv failed with HTTP 429: {"message":"slow down"}', f"unexpected tool output {result!r}"
88
+
89
+
90
+ @pytest.mark.live
91
+ @pytest.mark.skipif(os.environ.get("BREWPAGE_LIVE") != "1", reason="set BREWPAGE_LIVE=1 to hit production")
92
+ def test_live_private_markdown_page():
93
+ # GIVEN the real API and the private 'my' namespace
94
+ tool = BrewPageToolkit(client=BrewPageClient(namespace="my")).get_tools()[0]
95
+
96
+ # WHEN a short Markdown page is published for one day
97
+ result = tool.invoke({"content": "# langchain-brewpage smoke\n\nPrivate test page.", "format": "markdown", "ttl_days": 1})
98
+
99
+ # THEN a private brewpage.app link and an owner token come back
100
+ shape = {
101
+ "namespace": result["namespace"],
102
+ "url": bool(re.fullmatch(r"https://([a-z0-9-]+\.)*brewpage\.app/\S+", result["url"])),
103
+ "owner_token": bool(re.fullmatch(r"\S{16,}", result["owner_token"])),
104
+ }
105
+ assert shape == {"namespace": "my", "url": True, "owner_token": True}, f"live result {json.dumps(result)}"