langchain-tabstack 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,66 @@
1
+ """Tabstack tools for LangChain.
2
+
3
+ Schema-enforced web extraction and research, backed by the official Tabstack SDK.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from .client import (
9
+ create_async_tabstack_client,
10
+ create_tabstack_client,
11
+ get_default_async_client,
12
+ get_default_client,
13
+ )
14
+ from .errors import TabstackToolError, normalize_error
15
+ from .schemas import (
16
+ AutomateBrowserTaskInput,
17
+ ExtractPageContentInput,
18
+ ExtractStructuredDataInput,
19
+ GenerateStructuredDataInput,
20
+ ResearchQuestionInput,
21
+ )
22
+ from .tools import (
23
+ TOOL_DESCRIPTIONS,
24
+ TOOL_NAMES,
25
+ create_tabstack_tools,
26
+ )
27
+
28
+ # Default-client tools. The client is resolved lazily on first tool call, so
29
+ # importing this module never requires TABSTACK_API_KEY to be set.
30
+ TABSTACK_TOOLS = create_tabstack_tools()
31
+ (
32
+ extract_structured_data_tool,
33
+ extract_page_content_tool,
34
+ research_question_tool,
35
+ generate_structured_data_tool,
36
+ automate_browser_task_tool,
37
+ ) = TABSTACK_TOOLS
38
+
39
+ # Alias matching the common LangChain naming convention.
40
+ get_tabstack_tools = create_tabstack_tools
41
+
42
+ __all__ = [
43
+ "create_tabstack_tools",
44
+ "get_tabstack_tools",
45
+ "TABSTACK_TOOLS",
46
+ "extract_structured_data_tool",
47
+ "extract_page_content_tool",
48
+ "research_question_tool",
49
+ "generate_structured_data_tool",
50
+ "automate_browser_task_tool",
51
+ "create_tabstack_client",
52
+ "create_async_tabstack_client",
53
+ "get_default_client",
54
+ "get_default_async_client",
55
+ "TabstackToolError",
56
+ "normalize_error",
57
+ "TOOL_NAMES",
58
+ "TOOL_DESCRIPTIONS",
59
+ "ExtractStructuredDataInput",
60
+ "ExtractPageContentInput",
61
+ "ResearchQuestionInput",
62
+ "GenerateStructuredDataInput",
63
+ "AutomateBrowserTaskInput",
64
+ ]
65
+
66
+ __version__ = "0.1.0"
@@ -0,0 +1,72 @@
1
+ """Tabstack client construction.
2
+
3
+ The API key is read from ``TABSTACK_API_KEY`` by default and can be overridden
4
+ explicitly. Every network call in this package goes through this client.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import os
10
+ from typing import Optional
11
+
12
+ from tabstack import AsyncTabstack, Tabstack
13
+
14
+ __all__ = [
15
+ "create_tabstack_client",
16
+ "get_default_client",
17
+ "create_async_tabstack_client",
18
+ "get_default_async_client",
19
+ ]
20
+
21
+
22
+ def _client_kwargs(api_key: Optional[str], base_url: Optional[str]) -> dict:
23
+ resolved_key = api_key if api_key is not None else os.environ.get("TABSTACK_API_KEY")
24
+ kwargs: dict = {"api_key": resolved_key}
25
+ if base_url is not None:
26
+ kwargs["base_url"] = base_url
27
+ return kwargs
28
+
29
+
30
+ def create_tabstack_client(
31
+ api_key: Optional[str] = None,
32
+ base_url: Optional[str] = None,
33
+ ) -> Tabstack:
34
+ """Construct a configured synchronous Tabstack client.
35
+
36
+ Args:
37
+ api_key: Tabstack API key. Falls back to ``TABSTACK_API_KEY`` when omitted.
38
+ base_url: Override the API base URL. Uses the SDK default when omitted.
39
+ """
40
+ return Tabstack(**_client_kwargs(api_key, base_url))
41
+
42
+
43
+ def create_async_tabstack_client(
44
+ api_key: Optional[str] = None,
45
+ base_url: Optional[str] = None,
46
+ ) -> AsyncTabstack:
47
+ """Construct a configured asynchronous Tabstack client."""
48
+ return AsyncTabstack(**_client_kwargs(api_key, base_url))
49
+
50
+
51
+ _default_client: Optional[Tabstack] = None
52
+ _default_async_client: Optional[AsyncTabstack] = None
53
+
54
+
55
+ def get_default_client() -> Tabstack:
56
+ """Lazily construct and reuse a single default sync client.
57
+
58
+ The client is only built on first use, so importing this package never
59
+ requires an API key to be set.
60
+ """
61
+ global _default_client
62
+ if _default_client is None:
63
+ _default_client = create_tabstack_client()
64
+ return _default_client
65
+
66
+
67
+ def get_default_async_client() -> AsyncTabstack:
68
+ """Lazily construct and reuse a single default async client."""
69
+ global _default_async_client
70
+ if _default_async_client is None:
71
+ _default_async_client = create_async_tabstack_client()
72
+ return _default_async_client
@@ -0,0 +1,35 @@
1
+ """Normalised error surfaced consistently by the Tabstack tools."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Optional
6
+
7
+ from tabstack import APIError, APIStatusError
8
+
9
+ __all__ = ["TabstackToolError", "normalize_error"]
10
+
11
+
12
+ class TabstackToolError(Exception):
13
+ """A small normalised error wrapping the SDK's exceptions.
14
+
15
+ Consumers can catch this without depending on SDK internals.
16
+ """
17
+
18
+ def __init__(self, message: str, *, status: Optional[int] = None, cause: Any = None) -> None:
19
+ super().__init__(message)
20
+ self.status = status
21
+ self.__cause__ = cause if isinstance(cause, BaseException) else None
22
+ self.cause = cause
23
+
24
+
25
+ def normalize_error(err: Any) -> TabstackToolError:
26
+ """Map any raised value into a :class:`TabstackToolError`."""
27
+ if isinstance(err, TabstackToolError):
28
+ return err
29
+ if isinstance(err, APIStatusError):
30
+ return TabstackToolError(str(err), status=err.status_code, cause=err)
31
+ if isinstance(err, APIError):
32
+ return TabstackToolError(str(err), cause=err)
33
+ if isinstance(err, Exception):
34
+ return TabstackToolError(str(err), cause=err)
35
+ return TabstackToolError("Unknown Tabstack error", cause=err)
File without changes
@@ -0,0 +1,100 @@
1
+ """Pydantic input schemas for the Tabstack tools.
2
+
3
+ These are the single source of truth for each tool's input surface and mirror
4
+ the TypeScript ``@tabstack/langchain`` and ``@tabstack/ai`` packages so the
5
+ cross-language story stays consistent.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Any, Dict, Optional
11
+
12
+ from pydantic import BaseModel, Field
13
+ from typing_extensions import Literal
14
+
15
+ __all__ = [
16
+ "ExtractStructuredDataInput",
17
+ "ExtractPageContentInput",
18
+ "ResearchQuestionInput",
19
+ "GenerateStructuredDataInput",
20
+ "AutomateBrowserTaskInput",
21
+ ]
22
+
23
+ _EFFORT_DESCRIPTION = (
24
+ 'Optional fetch effort: "min" (fastest), "standard" (default, balanced), '
25
+ '"max" (full browser rendering for JS-heavy pages).'
26
+ )
27
+ _NOCACHE_DESCRIPTION = "Optional: bypass cache and force fresh retrieval"
28
+ _COUNTRY_DESCRIPTION = 'Optional ISO 3166-1 alpha-2 country code for geotargeting (e.g. "US")'
29
+
30
+
31
+ class ExtractStructuredDataInput(BaseModel):
32
+ url: str = Field(description="URL to fetch and extract structured data from")
33
+ json_schema_json: str = Field(
34
+ description=(
35
+ "A JSON-encoded JSON Schema object with concrete properties and descriptions "
36
+ "for each field to extract."
37
+ )
38
+ )
39
+ effort: Optional[Literal["min", "standard", "max"]] = Field(
40
+ default=None, description=_EFFORT_DESCRIPTION
41
+ )
42
+ nocache: Optional[bool] = Field(default=None, description=_NOCACHE_DESCRIPTION)
43
+ country: Optional[str] = Field(default=None, description=_COUNTRY_DESCRIPTION)
44
+
45
+
46
+ class ExtractPageContentInput(BaseModel):
47
+ url: str = Field(description="URL to fetch and convert to clean markdown")
48
+ effort: Optional[Literal["min", "standard", "max"]] = Field(
49
+ default=None, description=_EFFORT_DESCRIPTION
50
+ )
51
+ nocache: Optional[bool] = Field(default=None, description=_NOCACHE_DESCRIPTION)
52
+ country: Optional[str] = Field(default=None, description=_COUNTRY_DESCRIPTION)
53
+
54
+
55
+ class ResearchQuestionInput(BaseModel):
56
+ query: str = Field(description="The research question to answer using multiple web sources")
57
+ mode: Optional[Literal["fast", "balanced"]] = Field(
58
+ default=None,
59
+ description='Optional research mode: "fast" (quick) or "balanced" (default, more thorough)',
60
+ )
61
+ nocache: Optional[bool] = Field(default=None, description=_NOCACHE_DESCRIPTION)
62
+
63
+
64
+ class GenerateStructuredDataInput(BaseModel):
65
+ url: str = Field(description="URL whose content should be fetched and transformed")
66
+ instructions: str = Field(
67
+ description="Natural-language instructions describing how to transform the page content"
68
+ )
69
+ json_schema_json: str = Field(
70
+ description=(
71
+ "A JSON-encoded JSON Schema object describing the shape of the generated output, "
72
+ "with concrete properties and descriptions for each field."
73
+ )
74
+ )
75
+ effort: Optional[Literal["min", "standard", "max"]] = Field(
76
+ default=None, description=_EFFORT_DESCRIPTION
77
+ )
78
+ nocache: Optional[bool] = Field(default=None, description=_NOCACHE_DESCRIPTION)
79
+ country: Optional[str] = Field(default=None, description=_COUNTRY_DESCRIPTION)
80
+
81
+
82
+ class AutomateBrowserTaskInput(BaseModel):
83
+ task: str = Field(description="The browser automation task to perform, in natural language")
84
+ url: Optional[str] = Field(default=None, description="Optional starting URL for the task")
85
+ guardrails: Optional[str] = Field(
86
+ default=None, description="Optional safety constraints on what the agent is allowed to do"
87
+ )
88
+ data: Optional[Dict[str, Any]] = Field(
89
+ default=None, description="Optional JSON context data for form filling or complex tasks"
90
+ )
91
+ country: Optional[str] = Field(
92
+ default=None,
93
+ description='Optional ISO 3166-1 alpha-2 country code for geotargeted browsing (e.g. "US")',
94
+ )
95
+ max_iterations: Optional[int] = Field(
96
+ default=None, description="Optional cap on the number of agent iterations"
97
+ )
98
+ max_validation_attempts: Optional[int] = Field(
99
+ default=None, description="Optional cap on the number of result validation attempts"
100
+ )
@@ -0,0 +1,386 @@
1
+ """Framework-agnostic Tabstack tool bodies plus the LangChain tool factory.
2
+
3
+ The tool names and descriptions are the single source of truth, kept in lockstep
4
+ with the TypeScript ``@tabstack/langchain`` and ``@tabstack/ai`` packages. Each
5
+ tool is exposed with both a synchronous implementation and a native async
6
+ coroutine, so ``invoke`` and ``ainvoke`` both run without a thread-pool bounce.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ from typing import Any, Dict, List, Optional
13
+
14
+ from langchain_core.tools import BaseTool, StructuredTool
15
+ from tabstack import AsyncTabstack, Tabstack
16
+
17
+ from .client import get_default_async_client, get_default_client
18
+ from .errors import normalize_error
19
+ from .schemas import (
20
+ AutomateBrowserTaskInput,
21
+ ExtractPageContentInput,
22
+ ExtractStructuredDataInput,
23
+ GenerateStructuredDataInput,
24
+ ResearchQuestionInput,
25
+ )
26
+
27
+ __all__ = [
28
+ "TOOL_NAMES",
29
+ "TOOL_DESCRIPTIONS",
30
+ "extract_structured_data",
31
+ "extract_page_content",
32
+ "research_question",
33
+ "generate_structured_data",
34
+ "automate_browser_task",
35
+ "create_tabstack_tools",
36
+ ]
37
+
38
+ TOOL_NAMES = {
39
+ "extract_structured_data": "extract_structured_data",
40
+ "extract_page_content": "extract_page_content",
41
+ "research_question": "research_question",
42
+ "generate_structured_data": "generate_structured_data",
43
+ "automate_browser_task": "automate_browser_task",
44
+ }
45
+
46
+ TOOL_DESCRIPTIONS = {
47
+ "extract_structured_data": (
48
+ "Extract structured JSON data from a URL. Use when you need specific fields from a web "
49
+ "page. 'json_schema_json' must be a JSON-encoded JSON Schema object with concrete "
50
+ "properties and descriptions for each field to extract. See the schema-design guide for "
51
+ "patterns that produce reliable results. Returns the extracted data as JSON."
52
+ ),
53
+ "extract_page_content": (
54
+ "Fetch a URL and return its content as clean markdown. Use when you need to read a page's "
55
+ "full content for summarization or when you don't know what specific fields to extract. "
56
+ "Returns clean markdown text."
57
+ ),
58
+ "research_question": (
59
+ "Research a question using multiple web sources. Use when you need a synthesized answer "
60
+ "from multiple sources, not just data from a single known page. Returns an answer with "
61
+ "cited sources."
62
+ ),
63
+ "generate_structured_data": (
64
+ "Fetch a URL, then transform and restructure its content with AI into structured JSON. "
65
+ "Use when you need derived or reshaped data (summaries, categorizations, computed "
66
+ "fields), not just verbatim extraction. 'instructions' tells the AI how to transform the "
67
+ "content; 'json_schema_json' must be a JSON-encoded JSON Schema object describing the "
68
+ "output shape. Returns the generated data as JSON."
69
+ ),
70
+ "automate_browser_task": (
71
+ "Run an AI browser automation task described in natural language. Use for multi-step web "
72
+ "workflows: navigating, filling forms, gathering, and extracting across pages. Provide a "
73
+ "clear 'task' and optionally a starting 'url', 'guardrails' to constrain the agent, "
74
+ "'data' as context for form filling, a 'country' code for geotargeted browsing, and caps "
75
+ "via 'max_iterations' and 'max_validation_attempts'. Runs non-interactively and returns "
76
+ "the final answer plus any extracted data and the pages visited."
77
+ ),
78
+ }
79
+
80
+
81
+ def _fetch_options(inp: Any) -> Dict[str, Any]:
82
+ """Shared fetch options (effort, cache bypass, geotargeting) for the extract
83
+ and generate endpoints. Omitted values fall back to SDK defaults."""
84
+ options: Dict[str, Any] = {}
85
+ if inp.effort is not None:
86
+ options["effort"] = inp.effort
87
+ if inp.nocache is not None:
88
+ options["nocache"] = inp.nocache
89
+ if inp.country is not None:
90
+ options["geo_target"] = {"country": inp.country}
91
+ return options
92
+
93
+
94
+ def _research_kwargs(inp: ResearchQuestionInput) -> Dict[str, Any]:
95
+ kwargs: Dict[str, Any] = {"query": inp.query, "mode": inp.mode or "balanced"}
96
+ if inp.nocache is not None:
97
+ kwargs["nocache"] = inp.nocache
98
+ return kwargs
99
+
100
+
101
+ def _automate_kwargs(inp: AutomateBrowserTaskInput) -> Dict[str, Any]:
102
+ # Interactive mode is intentionally left off: a one-shot tool cannot service
103
+ # the human-in-the-loop form-data callback (``agent.automate_input``).
104
+ kwargs: Dict[str, Any] = {"task": inp.task}
105
+ if inp.url is not None:
106
+ kwargs["url"] = inp.url
107
+ if inp.guardrails is not None:
108
+ kwargs["guardrails"] = inp.guardrails
109
+ if inp.data is not None:
110
+ kwargs["data"] = inp.data
111
+ if inp.country is not None:
112
+ kwargs["geo_target"] = {"country": inp.country}
113
+ if inp.max_iterations is not None:
114
+ kwargs["max_iterations"] = inp.max_iterations
115
+ if inp.max_validation_attempts is not None:
116
+ kwargs["max_validation_attempts"] = inp.max_validation_attempts
117
+ return kwargs
118
+
119
+
120
+ def _research_complete(event: Any) -> Dict[str, Any]:
121
+ cited = event.data.metadata.cited_pages or []
122
+ return {
123
+ "answer": event.data.report,
124
+ "sources": [{"title": page.title or "", "url": page.url} for page in cited],
125
+ }
126
+
127
+
128
+ def _automate_complete(
129
+ event: Any, extracted: List[str], pages_visited: List[Dict[str, str]]
130
+ ) -> Dict[str, Any]:
131
+ data = event.data
132
+ if not data.success and data.error:
133
+ raise RuntimeError(f"Automation failed: {data.error.message}")
134
+ return {
135
+ "answer": data.final_answer,
136
+ "success": data.success,
137
+ "iterations": data.stats.iterations,
138
+ "actions": data.stats.actions,
139
+ "duration_ms": data.stats.duration_ms,
140
+ "extracted": extracted,
141
+ "pages_visited": pages_visited,
142
+ }
143
+
144
+
145
+ def _automate_incomplete(
146
+ extracted: List[str], pages_visited: List[Dict[str, str]]
147
+ ) -> Dict[str, Any]:
148
+ return {
149
+ "answer": None,
150
+ "success": False,
151
+ "iterations": 0,
152
+ "actions": 0,
153
+ "duration_ms": 0,
154
+ "extracted": extracted,
155
+ "pages_visited": pages_visited,
156
+ }
157
+
158
+
159
+ # --- Synchronous tool bodies -------------------------------------------------
160
+
161
+
162
+ def extract_structured_data(inp: ExtractStructuredDataInput, client: Tabstack) -> Dict[str, Any]:
163
+ """Extract structured data from a URL via ``client.extract.json``."""
164
+ try:
165
+ schema = json.loads(inp.json_schema_json)
166
+ return client.extract.json(url=inp.url, json_schema=schema, **_fetch_options(inp))
167
+ except Exception as err:
168
+ raise normalize_error(err) from err
169
+
170
+
171
+ def extract_page_content(inp: ExtractPageContentInput, client: Tabstack) -> str:
172
+ """Fetch a URL and return clean markdown via ``client.extract.markdown``."""
173
+ try:
174
+ result = client.extract.markdown(url=inp.url, **_fetch_options(inp))
175
+ return result.content
176
+ except Exception as err:
177
+ raise normalize_error(err) from err
178
+
179
+
180
+ def research_question(inp: ResearchQuestionInput, client: Tabstack) -> Dict[str, Any]:
181
+ """Research a question across multiple web sources via ``client.agent.research``."""
182
+ try:
183
+ stream = client.agent.research(**_research_kwargs(inp))
184
+ for event in stream:
185
+ if event.event == "error":
186
+ message = getattr(event.data, "message", None) or "unknown error"
187
+ raise RuntimeError(f"Research failed: {message}")
188
+ if event.event == "complete":
189
+ return _research_complete(event)
190
+ return {"answer": "Research did not complete", "sources": []}
191
+ except Exception as err:
192
+ raise normalize_error(err) from err
193
+
194
+
195
+ def generate_structured_data(inp: GenerateStructuredDataInput, client: Tabstack) -> Dict[str, Any]:
196
+ """Fetch a URL and AI-transform its content via ``client.generate.json``."""
197
+ try:
198
+ schema = json.loads(inp.json_schema_json)
199
+ return client.generate.json(
200
+ url=inp.url,
201
+ json_schema=schema,
202
+ instructions=inp.instructions,
203
+ **_fetch_options(inp),
204
+ )
205
+ except Exception as err:
206
+ raise normalize_error(err) from err
207
+
208
+
209
+ def automate_browser_task(inp: AutomateBrowserTaskInput, client: Tabstack) -> Dict[str, Any]:
210
+ """Run a browser automation task via ``client.agent.automate``.
211
+
212
+ Consumes the SSE stream non-interactively and returns the final answer plus
213
+ any extracted data and the pages visited.
214
+ """
215
+ try:
216
+ stream = client.agent.automate(**_automate_kwargs(inp))
217
+ extracted: List[str] = []
218
+ pages_visited: List[Dict[str, str]] = []
219
+ for event in stream:
220
+ if event.event == "agent:extracted":
221
+ extracted.append(event.data.extracted_data)
222
+ elif event.event == "browser:navigated":
223
+ pages_visited.append({"url": event.data.url, "title": event.data.title})
224
+ elif event.event == "error":
225
+ raise RuntimeError(f"Automation failed: {event.data.error.message}")
226
+ elif event.event == "complete":
227
+ return _automate_complete(event, extracted, pages_visited)
228
+ return _automate_incomplete(extracted, pages_visited)
229
+ except Exception as err:
230
+ raise normalize_error(err) from err
231
+
232
+
233
+ # --- Asynchronous tool bodies ------------------------------------------------
234
+
235
+
236
+ async def aextract_structured_data(
237
+ inp: ExtractStructuredDataInput, client: AsyncTabstack
238
+ ) -> Dict[str, Any]:
239
+ try:
240
+ schema = json.loads(inp.json_schema_json)
241
+ return await client.extract.json(url=inp.url, json_schema=schema, **_fetch_options(inp))
242
+ except Exception as err:
243
+ raise normalize_error(err) from err
244
+
245
+
246
+ async def aextract_page_content(inp: ExtractPageContentInput, client: AsyncTabstack) -> str:
247
+ try:
248
+ result = await client.extract.markdown(url=inp.url, **_fetch_options(inp))
249
+ return result.content
250
+ except Exception as err:
251
+ raise normalize_error(err) from err
252
+
253
+
254
+ async def aresearch_question(inp: ResearchQuestionInput, client: AsyncTabstack) -> Dict[str, Any]:
255
+ try:
256
+ stream = await client.agent.research(**_research_kwargs(inp))
257
+ async for event in stream:
258
+ if event.event == "error":
259
+ message = getattr(event.data, "message", None) or "unknown error"
260
+ raise RuntimeError(f"Research failed: {message}")
261
+ if event.event == "complete":
262
+ return _research_complete(event)
263
+ return {"answer": "Research did not complete", "sources": []}
264
+ except Exception as err:
265
+ raise normalize_error(err) from err
266
+
267
+
268
+ async def agenerate_structured_data(
269
+ inp: GenerateStructuredDataInput, client: AsyncTabstack
270
+ ) -> Dict[str, Any]:
271
+ try:
272
+ schema = json.loads(inp.json_schema_json)
273
+ return await client.generate.json(
274
+ url=inp.url,
275
+ json_schema=schema,
276
+ instructions=inp.instructions,
277
+ **_fetch_options(inp),
278
+ )
279
+ except Exception as err:
280
+ raise normalize_error(err) from err
281
+
282
+
283
+ async def aautomate_browser_task(
284
+ inp: AutomateBrowserTaskInput, client: AsyncTabstack
285
+ ) -> Dict[str, Any]:
286
+ try:
287
+ stream = await client.agent.automate(**_automate_kwargs(inp))
288
+ extracted: List[str] = []
289
+ pages_visited: List[Dict[str, str]] = []
290
+ async for event in stream:
291
+ if event.event == "agent:extracted":
292
+ extracted.append(event.data.extracted_data)
293
+ elif event.event == "browser:navigated":
294
+ pages_visited.append({"url": event.data.url, "title": event.data.title})
295
+ elif event.event == "error":
296
+ raise RuntimeError(f"Automation failed: {event.data.error.message}")
297
+ elif event.event == "complete":
298
+ return _automate_complete(event, extracted, pages_visited)
299
+ return _automate_incomplete(extracted, pages_visited)
300
+ except Exception as err:
301
+ raise normalize_error(err) from err
302
+
303
+
304
+ def _dumps(value: Any) -> str:
305
+ return value if isinstance(value, str) else json.dumps(value)
306
+
307
+
308
+ def create_tabstack_tools(
309
+ client: Optional[Tabstack] = None,
310
+ *,
311
+ async_client: Optional[AsyncTabstack] = None,
312
+ api_key: Optional[str] = None,
313
+ base_url: Optional[str] = None,
314
+ ) -> List[BaseTool]:
315
+ """Build the Tabstack LangChain tools.
316
+
317
+ Pass a ready ``client`` / ``async_client``, or ``api_key`` / ``base_url`` to
318
+ construct them. When nothing is given, each tool resolves the lazily-built
319
+ default client at call time, so building the tools never requires an API key.
320
+ Both sync (``invoke``) and async (``ainvoke``) execution are supported.
321
+
322
+ Returns:
323
+ A list of LangChain tools ready to pass to ``create_agent`` /
324
+ ``AgentExecutor``.
325
+ """
326
+ if api_key is not None or base_url is not None:
327
+ from .client import create_async_tabstack_client, create_tabstack_client
328
+
329
+ if client is None:
330
+ client = create_tabstack_client(api_key=api_key, base_url=base_url)
331
+ if async_client is None:
332
+ async_client = create_async_tabstack_client(api_key=api_key, base_url=base_url)
333
+
334
+ def sync() -> Tabstack:
335
+ return client if client is not None else get_default_client()
336
+
337
+ def asyncc() -> AsyncTabstack:
338
+ return async_client if async_client is not None else get_default_async_client()
339
+
340
+ def _build(name: str, schema, body, abody) -> StructuredTool:
341
+ def func(**kwargs: Any) -> str:
342
+ return _dumps(body(schema(**kwargs), sync()))
343
+
344
+ async def coroutine(**kwargs: Any) -> str:
345
+ return _dumps(await abody(schema(**kwargs), asyncc()))
346
+
347
+ return StructuredTool.from_function(
348
+ func=func,
349
+ coroutine=coroutine,
350
+ name=TOOL_NAMES[name],
351
+ description=TOOL_DESCRIPTIONS[name],
352
+ args_schema=schema,
353
+ )
354
+
355
+ return [
356
+ _build(
357
+ "extract_structured_data",
358
+ ExtractStructuredDataInput,
359
+ extract_structured_data,
360
+ aextract_structured_data,
361
+ ),
362
+ _build(
363
+ "extract_page_content",
364
+ ExtractPageContentInput,
365
+ extract_page_content,
366
+ aextract_page_content,
367
+ ),
368
+ _build(
369
+ "research_question",
370
+ ResearchQuestionInput,
371
+ research_question,
372
+ aresearch_question,
373
+ ),
374
+ _build(
375
+ "generate_structured_data",
376
+ GenerateStructuredDataInput,
377
+ generate_structured_data,
378
+ agenerate_structured_data,
379
+ ),
380
+ _build(
381
+ "automate_browser_task",
382
+ AutomateBrowserTaskInput,
383
+ automate_browser_task,
384
+ aautomate_browser_task,
385
+ ),
386
+ ]
@@ -0,0 +1,284 @@
1
+ Metadata-Version: 2.4
2
+ Name: langchain-tabstack
3
+ Version: 0.1.0
4
+ Summary: Tabstack tools for LangChain. Schema-enforced web extraction and research, backed by the official Tabstack SDK.
5
+ Project-URL: Homepage, https://tabstack.ai
6
+ Project-URL: Documentation, https://docs.tabstack.ai
7
+ Project-URL: Source, https://github.com/Mozilla-Ocho/tabstack-langchain-python
8
+ Author: Tabstack
9
+ License: MIT
10
+ Keywords: agent,extraction,langchain,tabstack,tools,web-scraping
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Requires-Python: >=3.9
20
+ Requires-Dist: langchain-core<2,>=0.3
21
+ Requires-Dist: pydantic>=2
22
+ Requires-Dist: tabstack>=2.6.1
23
+ Provides-Extra: dev
24
+ Requires-Dist: ruff>=0.6; extra == 'dev'
25
+ Provides-Extra: test
26
+ Requires-Dist: pytest-mock>=3; extra == 'test'
27
+ Requires-Dist: pytest>=8; extra == 'test'
28
+ Description-Content-Type: text/markdown
29
+
30
+ # langchain-tabstack
31
+
32
+ Give your [LangChain](https://python.langchain.com) agents reliable web access. Schema-enforced
33
+ extraction, multi-source research, AI transformation, and browser automation, all as native
34
+ LangChain tools backed by the official [Tabstack SDK](https://pypi.org/project/tabstack/).
35
+
36
+ ## Why Tabstack
37
+
38
+ LangChain's built-in loaders (`WebBaseLoader`, `PlaywrightURLLoader`) are fine for prototypes and
39
+ brittle in production: unpredictable parsing, Playwright binaries to install and maintain, and
40
+ behaviour that drifts across LangChain releases.
41
+
42
+ Tabstack replaces them with a hosted API:
43
+
44
+ - **Schema-enforced output.** You define the shape, you get that shape back. No prompt-engineering
45
+ the JSON out of raw text.
46
+ - **No browser to run.** JS-heavy pages render server-side. No Playwright, no binaries.
47
+ - **Version-independent.** It is an SDK call, not a loader coupled to your LangChain version.
48
+ - **Sync and async.** Every tool supports `invoke` and native `ainvoke`.
49
+
50
+ ## Install
51
+
52
+ ```bash
53
+ pip install langchain-tabstack
54
+ ```
55
+
56
+ Requires Python 3.9+. For the agent example below you also need a chat model, for example
57
+ `pip install langchain langchain-openai`. Set your API key (get one at
58
+ [console.tabstack.ai](https://console.tabstack.ai)):
59
+
60
+ ```bash
61
+ export TABSTACK_API_KEY="your-key-here"
62
+ ```
63
+
64
+ ## Quickstart
65
+
66
+ ```python
67
+ from langchain.agents import create_agent
68
+ from langchain_tabstack import TABSTACK_TOOLS
69
+
70
+ agent = create_agent(
71
+ "openai:gpt-4o",
72
+ tools=TABSTACK_TOOLS,
73
+ system_prompt=(
74
+ "You are a research assistant with web intelligence tools. "
75
+ "Use extract_structured_data for specific fields from a URL, "
76
+ "extract_page_content for full page text, and research_question for multi-source research."
77
+ ),
78
+ )
79
+
80
+ result = agent.invoke(
81
+ {"messages": [{"role": "user", "content": "What are Vercel's pricing plans?"}]}
82
+ )
83
+ print(result["messages"][-1].content)
84
+ ```
85
+
86
+ `TABSTACK_TOOLS` reads `TABSTACK_API_KEY` from the environment and is ready to drop into any agent.
87
+
88
+ > Using LangChain 0.x? `create_agent` is the LangChain 1.x replacement for `AgentExecutor` +
89
+ > `create_tool_calling_agent`. The tools themselves work with either.
90
+
91
+ ## The tools
92
+
93
+ | Tool | What it does |
94
+ | --- | --- |
95
+ | `extract_structured_data` | Pull specific fields from a URL into a JSON shape you define. |
96
+ | `extract_page_content` | Fetch a page as clean markdown. |
97
+ | `research_question` | Synthesised answer with cited sources across multiple pages. |
98
+ | `generate_structured_data` | Fetch a page, then AI-transform it into derived or reshaped JSON. |
99
+ | `automate_browser_task` | Run a multi-step, natural-language browser task. |
100
+
101
+ Every tool is exported individually too, so you can use one directly without an agent. Tools return
102
+ a JSON string (use `json.loads`); `extract_page_content` returns markdown text directly.
103
+
104
+ ### extract_structured_data
105
+
106
+ ```python
107
+ import json
108
+ from langchain_tabstack import extract_structured_data_tool
109
+
110
+ raw = extract_structured_data_tool.invoke(
111
+ {
112
+ "url": "https://news.ycombinator.com",
113
+ # json_schema_json is a JSON-encoded JSON Schema describing the fields you want.
114
+ "json_schema_json": json.dumps(
115
+ {
116
+ "type": "object",
117
+ "properties": {
118
+ "stories": {
119
+ "type": "array",
120
+ "items": {
121
+ "type": "object",
122
+ "properties": {
123
+ "title": {"type": "string"},
124
+ "points": {"type": "number", "description": "Story points"},
125
+ },
126
+ },
127
+ }
128
+ },
129
+ }
130
+ ),
131
+ }
132
+ )
133
+ data = json.loads(raw)
134
+ print(data["stories"])
135
+ ```
136
+
137
+ ### extract_page_content
138
+
139
+ ```python
140
+ from langchain_tabstack import extract_page_content_tool
141
+
142
+ markdown = extract_page_content_tool.invoke({"url": "https://example.com/blog/article"})
143
+ ```
144
+
145
+ ### research_question
146
+
147
+ ```python
148
+ import json
149
+ from langchain_tabstack import research_question_tool
150
+
151
+ result = json.loads(
152
+ research_question_tool.invoke(
153
+ {"query": "What are the latest developments in quantum error correction?"}
154
+ )
155
+ )
156
+ print(result["answer"])
157
+ for source in result["sources"]: # [{"title": ..., "url": ...}, ...]
158
+ print(source["url"])
159
+ ```
160
+
161
+ ### generate_structured_data
162
+
163
+ ```python
164
+ import json
165
+ from langchain_tabstack import generate_structured_data_tool
166
+
167
+ raw = generate_structured_data_tool.invoke(
168
+ {
169
+ "url": "https://news.ycombinator.com",
170
+ "instructions": "For each story, categorize it and write a one-sentence summary.",
171
+ "json_schema_json": json.dumps(
172
+ {
173
+ "type": "object",
174
+ "properties": {
175
+ "summaries": {
176
+ "type": "array",
177
+ "items": {
178
+ "type": "object",
179
+ "properties": {
180
+ "title": {"type": "string"},
181
+ "category": {"type": "string"},
182
+ "summary": {"type": "string"},
183
+ },
184
+ },
185
+ }
186
+ },
187
+ }
188
+ ),
189
+ }
190
+ )
191
+ ```
192
+
193
+ ### automate_browser_task
194
+
195
+ ```python
196
+ import json
197
+ from langchain_tabstack import automate_browser_task_tool
198
+
199
+ result = json.loads(
200
+ automate_browser_task_tool.invoke(
201
+ {
202
+ "task": "Find the top 3 trending repositories and their star counts",
203
+ "url": "https://github.com/trending",
204
+ "guardrails": "browse and extract only, do not submit forms",
205
+ }
206
+ )
207
+ )
208
+ # {"answer", "success", "iterations", "actions", "duration_ms", "extracted", "pages_visited"}
209
+ ```
210
+
211
+ ## Optional inputs
212
+
213
+ Pass these alongside the required inputs for finer control. They are sent only when provided, so
214
+ omitting them keeps Tabstack's defaults.
215
+
216
+ - `extract_structured_data`, `extract_page_content`, `generate_structured_data`:
217
+ - `effort`: `"min"` | `"standard"` | `"max"`. Use `"max"` for JS-heavy pages (full server-side
218
+ browser rendering, the `PlaywrightURLLoader` replacement).
219
+ - `nocache`: `True` to bypass the cache.
220
+ - `country`: ISO 3166-1 alpha-2 code (for example `"US"`) for geotargeted fetches.
221
+ - `research_question`: `mode` (`"fast"` | `"balanced"`), `nocache`.
222
+ - `automate_browser_task`: `data` (context for form filling), `country`, `max_iterations`,
223
+ `max_validation_attempts`.
224
+
225
+ ```python
226
+ # Render a JS-heavy SPA, fresh, from the UK:
227
+ extract_page_content_tool.invoke(
228
+ {"url": url, "effort": "max", "nocache": True, "country": "GB"}
229
+ )
230
+ ```
231
+
232
+ ## Async
233
+
234
+ Every tool supports `ainvoke` natively, backed by `AsyncTabstack`, so it runs in your event loop
235
+ without a thread-pool bounce:
236
+
237
+ ```python
238
+ markdown = await extract_page_content_tool.ainvoke({"url": "https://example.com"})
239
+ ```
240
+
241
+ ## Configuration
242
+
243
+ For a custom API key or base URL, or to reuse one client, build the tools explicitly:
244
+
245
+ ```python
246
+ from langchain_tabstack import create_tabstack_tools
247
+
248
+ tools = create_tabstack_tools(api_key="...")
249
+ # or pass clients you already have:
250
+ # create_tabstack_tools(client=..., async_client=...)
251
+ ```
252
+
253
+ ## Error handling
254
+
255
+ Tool calls raise `TabstackToolError` with a normalised message and, for API failures, an HTTP
256
+ `status`:
257
+
258
+ ```python
259
+ from langchain_tabstack import TabstackToolError, extract_page_content_tool
260
+
261
+ try:
262
+ extract_page_content_tool.invoke({"url": "https://example.com"})
263
+ except TabstackToolError as err:
264
+ print(f"Tabstack failed ({err.status or 'no status'}): {err}")
265
+ ```
266
+
267
+ ## Good to know
268
+
269
+ - **`automate_browser_task` runs non-interactively.** It does not pause for human-in-the-loop form
270
+ input, so it never blocks. It returns the final answer plus the data it extracted and the pages it
271
+ visited.
272
+ - The client is created lazily, so importing the package never requires an API key to be set.
273
+ - The tool names, descriptions, and inputs match the
274
+ [`@tabstack/langchain`](https://www.npmjs.com/package/@tabstack/langchain) (LangChain.js) and
275
+ [`@tabstack/ai`](https://www.npmjs.com/package/@tabstack/ai) (Vercel AI SDK) packages, so
276
+ behaviour is consistent across languages and frameworks. Those return native objects with
277
+ camelCase keys; this package returns JSON strings with snake_case keys, the LangChain-Python
278
+ convention. The data is the same.
279
+
280
+ ## Links
281
+
282
+ - [Tabstack docs](https://docs.tabstack.ai)
283
+ - [Tabstack SDK (`tabstack`)](https://pypi.org/project/tabstack/)
284
+ - [Get an API key](https://console.tabstack.ai)
@@ -0,0 +1,9 @@
1
+ langchain_tabstack/__init__.py,sha256=Vag5MQOP_d2ZZ7vFklSw4PSRFdaUF5FXCei-s9m1anU,1746
2
+ langchain_tabstack/client.py,sha256=9pGF7m1mwM1T6m3HVjocAkL7_IzOXXSXMdNS0lHFolA,2161
3
+ langchain_tabstack/errors.py,sha256=2A3H5-9TGALCct17CLhUGSDi98FJRa07VGZJYIoTHJk,1221
4
+ langchain_tabstack/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ langchain_tabstack/schemas.py,sha256=BRRS12w6bL8hfCEZy4FYKALWHQXvtSz3YzvqIeaMd5g,4137
6
+ langchain_tabstack/tools.py,sha256=4UhMdU5NE7nmfEjmrb71esY3mJrRm_NWEB5BzuwvGtc,14758
7
+ langchain_tabstack-0.1.0.dist-info/METADATA,sha256=Uj4xrivZeqnelsDQG4fC0NWbEv4byd1MTC9ZslMmLec,9750
8
+ langchain_tabstack-0.1.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
9
+ langchain_tabstack-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.31.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any