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.
- langchain_tabstack/__init__.py +66 -0
- langchain_tabstack/client.py +72 -0
- langchain_tabstack/errors.py +35 -0
- langchain_tabstack/py.typed +0 -0
- langchain_tabstack/schemas.py +100 -0
- langchain_tabstack/tools.py +386 -0
- langchain_tabstack-0.1.0.dist-info/METADATA +284 -0
- langchain_tabstack-0.1.0.dist-info/RECORD +9 -0
- langchain_tabstack-0.1.0.dist-info/WHEEL +4 -0
|
@@ -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,,
|