langchain-stackresolve 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_stackresolve/__init__.py +36 -0
- langchain_stackresolve/py.typed +0 -0
- langchain_stackresolve/toolkit.py +45 -0
- langchain_stackresolve/tools.py +417 -0
- langchain_stackresolve-0.1.0.dist-info/METADATA +137 -0
- langchain_stackresolve-0.1.0.dist-info/RECORD +8 -0
- langchain_stackresolve-0.1.0.dist-info/WHEEL +4 -0
- langchain_stackresolve-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""LangChain integration for StackResolve.
|
|
2
|
+
|
|
3
|
+
Web intelligence for AI agents: find, compare, and audit software for a task, and pull
|
|
4
|
+
structured company research, in one tool call instead of a search-and-scrape loop.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .toolkit import StackResolveToolkit
|
|
8
|
+
from .tools import (
|
|
9
|
+
ALL_TOOL_CLASSES,
|
|
10
|
+
StackResolveAudit,
|
|
11
|
+
StackResolveBaseTool,
|
|
12
|
+
StackResolveCompareProducts,
|
|
13
|
+
StackResolveFindCompetitors,
|
|
14
|
+
StackResolveFindTools,
|
|
15
|
+
StackResolveGetCompany,
|
|
16
|
+
StackResolveGetPricing,
|
|
17
|
+
StackResolveResearchCompany,
|
|
18
|
+
StackResolveSearchTools,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
__version__ = "0.1.0"
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"StackResolveToolkit",
|
|
25
|
+
"StackResolveBaseTool",
|
|
26
|
+
"StackResolveFindTools",
|
|
27
|
+
"StackResolveSearchTools",
|
|
28
|
+
"StackResolveAudit",
|
|
29
|
+
"StackResolveCompareProducts",
|
|
30
|
+
"StackResolveGetCompany",
|
|
31
|
+
"StackResolveGetPricing",
|
|
32
|
+
"StackResolveFindCompetitors",
|
|
33
|
+
"StackResolveResearchCompany",
|
|
34
|
+
"ALL_TOOL_CLASSES",
|
|
35
|
+
"__version__",
|
|
36
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Toolkit that hands an agent every StackResolve tool with one shared client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, List, Optional
|
|
6
|
+
|
|
7
|
+
from langchain_core.tools import BaseTool
|
|
8
|
+
from pydantic import BaseModel, ConfigDict, PrivateAttr
|
|
9
|
+
from stackresolve import StackResolve
|
|
10
|
+
|
|
11
|
+
from .tools import ALL_TOOL_CLASSES, _build_client
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class StackResolveToolkit(BaseModel):
|
|
15
|
+
"""All StackResolve tools, sharing one HTTP client.
|
|
16
|
+
|
|
17
|
+
Registry reads work without a key. Metered tools (audit, research) need one, from
|
|
18
|
+
https://stackresolve.dev/developers, passed here or set as STACKRESOLVE_API_KEY.
|
|
19
|
+
|
|
20
|
+
from langchain_stackresolve import StackResolveToolkit
|
|
21
|
+
|
|
22
|
+
tools = StackResolveToolkit().get_tools()
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
26
|
+
|
|
27
|
+
_client: StackResolve = PrivateAttr()
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
client: Optional[StackResolve] = None,
|
|
32
|
+
api_key: Optional[str] = None,
|
|
33
|
+
base_url: Optional[str] = None,
|
|
34
|
+
**kwargs: Any,
|
|
35
|
+
) -> None:
|
|
36
|
+
super().__init__(**kwargs)
|
|
37
|
+
self._client = client if client is not None else _build_client(api_key, base_url)
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def client(self) -> StackResolve:
|
|
41
|
+
return self._client
|
|
42
|
+
|
|
43
|
+
def get_tools(self) -> List[BaseTool]:
|
|
44
|
+
"""Every tool in the toolkit, each bound to the shared client."""
|
|
45
|
+
return [cls(client=self._client) for cls in ALL_TOOL_CLASSES]
|
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
"""LangChain tools over the StackResolve API.
|
|
2
|
+
|
|
3
|
+
Each tool wraps one StackResolve SDK call and returns compact JSON, so an agent can
|
|
4
|
+
pick a tool for a task, compare vendors, check whether a product is agent-ready, and
|
|
5
|
+
pull structured company facts without running its own search-and-scrape loop.
|
|
6
|
+
|
|
7
|
+
from langchain_stackresolve import StackResolveToolkit
|
|
8
|
+
|
|
9
|
+
tools = StackResolveToolkit().get_tools()
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
from typing import Any, Dict, List, Optional, Type
|
|
17
|
+
|
|
18
|
+
from langchain_core.callbacks import (
|
|
19
|
+
AsyncCallbackManagerForToolRun,
|
|
20
|
+
CallbackManagerForToolRun,
|
|
21
|
+
)
|
|
22
|
+
from langchain_core.runnables.config import run_in_executor
|
|
23
|
+
from langchain_core.tools import BaseTool
|
|
24
|
+
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr
|
|
25
|
+
from stackresolve import StackResolve, StackResolveError
|
|
26
|
+
|
|
27
|
+
# Tool output goes straight into a prompt, so cap it. Registry and research payloads
|
|
28
|
+
# can run to tens of kilobytes and the tail is rarely what the model needs.
|
|
29
|
+
MAX_CHARS = 6000
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _dump(payload: Any, max_chars: int = MAX_CHARS) -> str:
|
|
33
|
+
"""Serialize a payload to JSON, truncating on a character budget.
|
|
34
|
+
|
|
35
|
+
Truncation stays valid JSON. Slicing the serialized text mid-token would leave
|
|
36
|
+
output that a caller re-parsing tool results cannot load, and that failure would
|
|
37
|
+
only show up on large payloads.
|
|
38
|
+
"""
|
|
39
|
+
text = json.dumps(payload, indent=2, default=str, ensure_ascii=False)
|
|
40
|
+
if len(text) <= max_chars:
|
|
41
|
+
return text
|
|
42
|
+
return json.dumps(
|
|
43
|
+
{
|
|
44
|
+
"truncated": True,
|
|
45
|
+
"omitted_characters": len(text) - max_chars,
|
|
46
|
+
"note": (
|
|
47
|
+
f"Result exceeded {max_chars} characters. 'partial' holds the start of "
|
|
48
|
+
"the JSON payload. Narrow the query for a complete result."
|
|
49
|
+
),
|
|
50
|
+
"partial": text[:max_chars],
|
|
51
|
+
},
|
|
52
|
+
indent=2,
|
|
53
|
+
ensure_ascii=False,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _build_client(
|
|
58
|
+
api_key: Optional[str] = None, base_url: Optional[str] = None
|
|
59
|
+
) -> StackResolve:
|
|
60
|
+
"""Construct an SDK client, falling back to the standard environment variables."""
|
|
61
|
+
opts: Dict[str, Any] = {}
|
|
62
|
+
key = api_key or os.environ.get("STACKRESOLVE_API_KEY")
|
|
63
|
+
if key:
|
|
64
|
+
opts["api_key"] = key
|
|
65
|
+
url = base_url or os.environ.get("STACKRESOLVE_BASE_URL")
|
|
66
|
+
if url:
|
|
67
|
+
opts["base_url"] = url
|
|
68
|
+
return StackResolve(**opts)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class StackResolveBaseTool(BaseTool):
|
|
72
|
+
"""Shared plumbing: one client, uniform error handling, async via executor.
|
|
73
|
+
|
|
74
|
+
Subclasses set ``name``, ``description``, and ``args_schema``, then call
|
|
75
|
+
``self._call(self.client.<method>, ...)`` from ``_run``.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
79
|
+
|
|
80
|
+
_client: Optional[StackResolve] = PrivateAttr(default=None)
|
|
81
|
+
|
|
82
|
+
def __init__(
|
|
83
|
+
self,
|
|
84
|
+
client: Optional[StackResolve] = None,
|
|
85
|
+
api_key: Optional[str] = None,
|
|
86
|
+
base_url: Optional[str] = None,
|
|
87
|
+
**kwargs: Any,
|
|
88
|
+
) -> None:
|
|
89
|
+
super().__init__(**kwargs)
|
|
90
|
+
self._client = client if client is not None else _build_client(api_key, base_url)
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def client(self) -> StackResolve:
|
|
94
|
+
if self._client is None: # pragma: no cover - defensive
|
|
95
|
+
self._client = _build_client()
|
|
96
|
+
return self._client
|
|
97
|
+
|
|
98
|
+
def _call(self, fn: Any, *args: Any, **kwargs: Any) -> str:
|
|
99
|
+
"""Run an SDK call and return JSON, converting any failure into readable text.
|
|
100
|
+
|
|
101
|
+
A tool that raises ends the agent's turn. A tool that explains what went wrong
|
|
102
|
+
lets the model recover, so a 402 becomes an instruction rather than a traceback.
|
|
103
|
+
|
|
104
|
+
The catch-all matters as much as the status branches: a read timeout, a refused
|
|
105
|
+
connection, or a DNS failure is an ordinary condition for a network tool, and
|
|
106
|
+
letting one escape would kill the turn just as surely as an API error.
|
|
107
|
+
"""
|
|
108
|
+
try:
|
|
109
|
+
return _dump(fn(*args, **kwargs))
|
|
110
|
+
except StackResolveError as err:
|
|
111
|
+
if err.status in (401, 403):
|
|
112
|
+
return (
|
|
113
|
+
"StackResolve rejected the credentials. Set STACKRESOLVE_API_KEY "
|
|
114
|
+
"to a key from https://stackresolve.dev/developers and retry."
|
|
115
|
+
)
|
|
116
|
+
if err.status == 402:
|
|
117
|
+
return (
|
|
118
|
+
"This StackResolve tool is metered and the free allowance is used "
|
|
119
|
+
"up. Registry reads (search, profiles, compare) still work."
|
|
120
|
+
)
|
|
121
|
+
if err.status == 429:
|
|
122
|
+
return "StackResolve rate limit reached. Wait a moment and retry."
|
|
123
|
+
return f"StackResolve API error {err.status}: {err.body}"
|
|
124
|
+
except Exception as err: # noqa: BLE001 - a tool must never end the agent's turn
|
|
125
|
+
return (
|
|
126
|
+
f"StackResolve request failed: {type(err).__name__}: {err}. "
|
|
127
|
+
"This is usually a timeout or a network problem. Retry, or check "
|
|
128
|
+
"STACKRESOLVE_BASE_URL if it is set."
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
async def _acall(self, fn: Any, *args: Any, **kwargs: Any) -> str:
|
|
132
|
+
return await run_in_executor(None, self._call, fn, *args, **kwargs)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# --------------------------------------------------------------------------------
|
|
136
|
+
# AgentReady: pick, compare, and vet software
|
|
137
|
+
# --------------------------------------------------------------------------------
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class FindToolsInput(BaseModel):
|
|
141
|
+
task: str = Field(
|
|
142
|
+
description=(
|
|
143
|
+
"The engineering task in plain English, e.g. 'scrape javascript-heavy "
|
|
144
|
+
"sites' or 'send transactional email from a Node service'."
|
|
145
|
+
)
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class StackResolveFindTools(StackResolveBaseTool):
|
|
150
|
+
"""Task in, ranked agent-ready tools out."""
|
|
151
|
+
|
|
152
|
+
name: str = "stackresolve_find_tools"
|
|
153
|
+
description: str = (
|
|
154
|
+
"Find the best software for an engineering task, ranked by how well it works "
|
|
155
|
+
"for AI agents. Returns candidate tools with an AgentReady score (0-100), what "
|
|
156
|
+
"each one does, and why it fits. Use this when you need to choose a library, "
|
|
157
|
+
"API, or service and want current, scored options rather than guessing from "
|
|
158
|
+
"memory. Input is a plain-English task description."
|
|
159
|
+
)
|
|
160
|
+
args_schema: Type[BaseModel] = FindToolsInput
|
|
161
|
+
|
|
162
|
+
def _run(
|
|
163
|
+
self, task: str, run_manager: Optional[CallbackManagerForToolRun] = None
|
|
164
|
+
) -> str:
|
|
165
|
+
return self._call(self.client.find_tools, task)
|
|
166
|
+
|
|
167
|
+
async def _arun(
|
|
168
|
+
self, task: str, run_manager: Optional[AsyncCallbackManagerForToolRun] = None
|
|
169
|
+
) -> str:
|
|
170
|
+
return await self._acall(self.client.find_tools, task)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class SearchToolsInput(BaseModel):
|
|
174
|
+
query: str = Field(description="Search query for the StackResolve registry.")
|
|
175
|
+
api: Optional[bool] = Field(default=None, description="Require a public API.")
|
|
176
|
+
mcp: Optional[bool] = Field(default=None, description="Require an MCP server.")
|
|
177
|
+
cli: Optional[bool] = Field(default=None, description="Require a CLI.")
|
|
178
|
+
openapi: Optional[bool] = Field(
|
|
179
|
+
default=None, description="Require a published OpenAPI spec."
|
|
180
|
+
)
|
|
181
|
+
self_serve: Optional[bool] = Field(
|
|
182
|
+
default=None, description="Require self-serve signup with no sales call."
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _requirements(**kwargs: Any) -> Optional[Dict[str, Any]]:
|
|
187
|
+
reqs = {k: v for k, v in kwargs.items() if v is not None}
|
|
188
|
+
return reqs or None
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class StackResolveSearchTools(StackResolveBaseTool):
|
|
192
|
+
"""Registry search with hard requirements."""
|
|
193
|
+
|
|
194
|
+
name: str = "stackresolve_search_tools"
|
|
195
|
+
description: str = (
|
|
196
|
+
"Search the StackResolve registry for software, filtering on capabilities an "
|
|
197
|
+
"agent needs: a public API, an MCP server, a CLI, an OpenAPI spec, or self-serve "
|
|
198
|
+
"signup. Use this when the requirement is concrete, e.g. 'a payments provider "
|
|
199
|
+
"with an MCP server and self-serve signup'. Free, no API key needed."
|
|
200
|
+
)
|
|
201
|
+
args_schema: Type[BaseModel] = SearchToolsInput
|
|
202
|
+
|
|
203
|
+
def _run(
|
|
204
|
+
self,
|
|
205
|
+
query: str,
|
|
206
|
+
api: Optional[bool] = None,
|
|
207
|
+
mcp: Optional[bool] = None,
|
|
208
|
+
cli: Optional[bool] = None,
|
|
209
|
+
openapi: Optional[bool] = None,
|
|
210
|
+
self_serve: Optional[bool] = None,
|
|
211
|
+
run_manager: Optional[CallbackManagerForToolRun] = None,
|
|
212
|
+
) -> str:
|
|
213
|
+
reqs = _requirements(
|
|
214
|
+
api=api, mcp=mcp, cli=cli, openapi=openapi, self_serve=self_serve
|
|
215
|
+
)
|
|
216
|
+
return self._call(self.client.search, query, reqs)
|
|
217
|
+
|
|
218
|
+
async def _arun(
|
|
219
|
+
self,
|
|
220
|
+
query: str,
|
|
221
|
+
api: Optional[bool] = None,
|
|
222
|
+
mcp: Optional[bool] = None,
|
|
223
|
+
cli: Optional[bool] = None,
|
|
224
|
+
openapi: Optional[bool] = None,
|
|
225
|
+
self_serve: Optional[bool] = None,
|
|
226
|
+
run_manager: Optional[AsyncCallbackManagerForToolRun] = None,
|
|
227
|
+
) -> str:
|
|
228
|
+
reqs = _requirements(
|
|
229
|
+
api=api, mcp=mcp, cli=cli, openapi=openapi, self_serve=self_serve
|
|
230
|
+
)
|
|
231
|
+
return await self._acall(self.client.search, query, reqs)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
class AuditInput(BaseModel):
|
|
235
|
+
domain: str = Field(description="Domain to audit, e.g. 'stripe.com'.")
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
class StackResolveAudit(StackResolveBaseTool):
|
|
239
|
+
"""Agent-readiness score and failing checks for a domain."""
|
|
240
|
+
|
|
241
|
+
name: str = "stackresolve_audit"
|
|
242
|
+
description: str = (
|
|
243
|
+
"Audit a domain for agent readiness. Returns a 0-100 AgentReady score, "
|
|
244
|
+
"sub-scores for discovery, understanding, adoption, and operability, plus the "
|
|
245
|
+
"specific checks that failed. Use this to judge whether an agent can actually "
|
|
246
|
+
"work with a vendor before recommending it, or to check a site you are building."
|
|
247
|
+
)
|
|
248
|
+
args_schema: Type[BaseModel] = AuditInput
|
|
249
|
+
|
|
250
|
+
def _run(
|
|
251
|
+
self, domain: str, run_manager: Optional[CallbackManagerForToolRun] = None
|
|
252
|
+
) -> str:
|
|
253
|
+
return self._call(self.client.audit, domain)
|
|
254
|
+
|
|
255
|
+
async def _arun(
|
|
256
|
+
self, domain: str, run_manager: Optional[AsyncCallbackManagerForToolRun] = None
|
|
257
|
+
) -> str:
|
|
258
|
+
return await self._acall(self.client.audit, domain)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
class CompareInput(BaseModel):
|
|
262
|
+
slugs: List[str] = Field(
|
|
263
|
+
description="Registry slugs to compare side by side, e.g. ['firecrawl', 'apify']."
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
class StackResolveCompareProducts(StackResolveBaseTool):
|
|
268
|
+
"""Side-by-side product comparison."""
|
|
269
|
+
|
|
270
|
+
name: str = "stackresolve_compare_products"
|
|
271
|
+
description: str = (
|
|
272
|
+
"Compare two or more products from the StackResolve registry side by side on "
|
|
273
|
+
"AgentReady scores, capabilities, and pricing. Use this after narrowing to a "
|
|
274
|
+
"shortlist. Input is a list of registry slugs, which you can get from "
|
|
275
|
+
"stackresolve_find_tools or stackresolve_search_tools."
|
|
276
|
+
)
|
|
277
|
+
args_schema: Type[BaseModel] = CompareInput
|
|
278
|
+
|
|
279
|
+
def _run(
|
|
280
|
+
self, slugs: List[str], run_manager: Optional[CallbackManagerForToolRun] = None
|
|
281
|
+
) -> str:
|
|
282
|
+
return self._call(self.client.compare, slugs)
|
|
283
|
+
|
|
284
|
+
async def _arun(
|
|
285
|
+
self,
|
|
286
|
+
slugs: List[str],
|
|
287
|
+
run_manager: Optional[AsyncCallbackManagerForToolRun] = None,
|
|
288
|
+
) -> str:
|
|
289
|
+
return await self._acall(self.client.compare, slugs)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
# --------------------------------------------------------------------------------
|
|
293
|
+
# CompanyData: structured research in one call
|
|
294
|
+
# --------------------------------------------------------------------------------
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
class DomainInput(BaseModel):
|
|
298
|
+
domain: str = Field(description="Company domain, e.g. 'vercel.com'.")
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
class StackResolveGetCompany(StackResolveBaseTool):
|
|
302
|
+
"""Structured company facts."""
|
|
303
|
+
|
|
304
|
+
name: str = "stackresolve_get_company"
|
|
305
|
+
description: str = (
|
|
306
|
+
"Get structured facts about a company from its domain: what it does, category, "
|
|
307
|
+
"size, funding, and location. Use this instead of searching the web and reading "
|
|
308
|
+
"pages, which costs many more tool calls and returns unstructured text."
|
|
309
|
+
)
|
|
310
|
+
args_schema: Type[BaseModel] = DomainInput
|
|
311
|
+
|
|
312
|
+
def _run(
|
|
313
|
+
self, domain: str, run_manager: Optional[CallbackManagerForToolRun] = None
|
|
314
|
+
) -> str:
|
|
315
|
+
return self._call(self.client.get_company, domain)
|
|
316
|
+
|
|
317
|
+
async def _arun(
|
|
318
|
+
self, domain: str, run_manager: Optional[AsyncCallbackManagerForToolRun] = None
|
|
319
|
+
) -> str:
|
|
320
|
+
return await self._acall(self.client.get_company, domain)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
class StackResolveGetPricing(StackResolveBaseTool):
|
|
324
|
+
"""Current pricing for a vendor."""
|
|
325
|
+
|
|
326
|
+
name: str = "stackresolve_get_pricing"
|
|
327
|
+
description: str = (
|
|
328
|
+
"Get a company's current pricing as structured data: plans, prices, billing "
|
|
329
|
+
"period, and what each tier includes. Use this for cost questions and "
|
|
330
|
+
"build-vs-buy comparisons. Pricing pages change often, so prefer this over "
|
|
331
|
+
"recalling a price from memory."
|
|
332
|
+
)
|
|
333
|
+
args_schema: Type[BaseModel] = DomainInput
|
|
334
|
+
|
|
335
|
+
def _run(
|
|
336
|
+
self, domain: str, run_manager: Optional[CallbackManagerForToolRun] = None
|
|
337
|
+
) -> str:
|
|
338
|
+
return self._call(self.client.get_pricing, domain)
|
|
339
|
+
|
|
340
|
+
async def _arun(
|
|
341
|
+
self, domain: str, run_manager: Optional[AsyncCallbackManagerForToolRun] = None
|
|
342
|
+
) -> str:
|
|
343
|
+
return await self._acall(self.client.get_pricing, domain)
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
class StackResolveFindCompetitors(StackResolveBaseTool):
|
|
347
|
+
"""Competitors for a company."""
|
|
348
|
+
|
|
349
|
+
name: str = "stackresolve_find_competitors"
|
|
350
|
+
description: str = (
|
|
351
|
+
"Find a company's competitors from its domain, with a short note on how each "
|
|
352
|
+
"one differs. Use this to widen a shortlist or to answer 'what else is there'."
|
|
353
|
+
)
|
|
354
|
+
args_schema: Type[BaseModel] = DomainInput
|
|
355
|
+
|
|
356
|
+
def _run(
|
|
357
|
+
self, domain: str, run_manager: Optional[CallbackManagerForToolRun] = None
|
|
358
|
+
) -> str:
|
|
359
|
+
return self._call(self.client.get_competitors, domain)
|
|
360
|
+
|
|
361
|
+
async def _arun(
|
|
362
|
+
self, domain: str, run_manager: Optional[AsyncCallbackManagerForToolRun] = None
|
|
363
|
+
) -> str:
|
|
364
|
+
return await self._acall(self.client.get_competitors, domain)
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
class ResearchInput(BaseModel):
|
|
368
|
+
domain: str = Field(description="Company domain to research, e.g. 'anthropic.com'.")
|
|
369
|
+
question: Optional[str] = Field(
|
|
370
|
+
default=None,
|
|
371
|
+
description=(
|
|
372
|
+
"Optional specific question, e.g. 'what is their enterprise SLA?'. Omit for "
|
|
373
|
+
"a general research summary."
|
|
374
|
+
),
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
class StackResolveResearchCompany(StackResolveBaseTool):
|
|
379
|
+
"""Deep research on a company, answering a specific question."""
|
|
380
|
+
|
|
381
|
+
name: str = "stackresolve_research_company"
|
|
382
|
+
description: str = (
|
|
383
|
+
"Run deep research on a company and get a synthesized answer with sources. "
|
|
384
|
+
"Takes a domain and an optional question. Use this when the answer needs current "
|
|
385
|
+
"web evidence rather than a stored fact, for example recent funding, a policy "
|
|
386
|
+
"change, or a support commitment. Slower and metered, so try "
|
|
387
|
+
"stackresolve_get_company first for basic facts."
|
|
388
|
+
)
|
|
389
|
+
args_schema: Type[BaseModel] = ResearchInput
|
|
390
|
+
|
|
391
|
+
def _run(
|
|
392
|
+
self,
|
|
393
|
+
domain: str,
|
|
394
|
+
question: Optional[str] = None,
|
|
395
|
+
run_manager: Optional[CallbackManagerForToolRun] = None,
|
|
396
|
+
) -> str:
|
|
397
|
+
return self._call(self.client.research, domain, question)
|
|
398
|
+
|
|
399
|
+
async def _arun(
|
|
400
|
+
self,
|
|
401
|
+
domain: str,
|
|
402
|
+
question: Optional[str] = None,
|
|
403
|
+
run_manager: Optional[AsyncCallbackManagerForToolRun] = None,
|
|
404
|
+
) -> str:
|
|
405
|
+
return await self._acall(self.client.research, domain, question)
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
ALL_TOOL_CLASSES = [
|
|
409
|
+
StackResolveFindTools,
|
|
410
|
+
StackResolveSearchTools,
|
|
411
|
+
StackResolveAudit,
|
|
412
|
+
StackResolveCompareProducts,
|
|
413
|
+
StackResolveGetCompany,
|
|
414
|
+
StackResolveGetPricing,
|
|
415
|
+
StackResolveFindCompetitors,
|
|
416
|
+
StackResolveResearchCompany,
|
|
417
|
+
]
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: langchain-stackresolve
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: LangChain tools for StackResolve: find, compare, and audit software for AI agents, plus structured company research.
|
|
5
|
+
Project-URL: Homepage, https://stackresolve.dev
|
|
6
|
+
Project-URL: Repository, https://github.com/autorevai/stackresolve
|
|
7
|
+
Project-URL: Documentation, https://stackresolve.dev/docs
|
|
8
|
+
Author: StackResolve
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: agent-tools,agentready,ai-agents,companydata,langchain,mcp,stackresolve
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
17
|
+
Requires-Python: >=3.9
|
|
18
|
+
Requires-Dist: langchain-core>=0.3.0
|
|
19
|
+
Requires-Dist: pydantic>=2.0
|
|
20
|
+
Requires-Dist: stackresolve>=0.1.0
|
|
21
|
+
Provides-Extra: test
|
|
22
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'test'
|
|
23
|
+
Requires-Dist: pytest>=7.0; extra == 'test'
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# langchain-stackresolve
|
|
27
|
+
|
|
28
|
+
LangChain tools for [StackResolve](https://stackresolve.dev): web intelligence for AI agents.
|
|
29
|
+
|
|
30
|
+
Pick software for a task, compare vendors, check whether a product is agent-ready, and
|
|
31
|
+
pull structured company facts. One tool call instead of a search-and-scrape loop.
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install langchain-stackresolve
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Quickstart
|
|
38
|
+
|
|
39
|
+
Every tool works without a key, subject to an anonymous rate limit. A free key from
|
|
40
|
+
[stackresolve.dev/developers](https://stackresolve.dev/developers) raises it.
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
import os
|
|
44
|
+
from langchain_stackresolve import StackResolveToolkit
|
|
45
|
+
from langchain.agents import create_agent
|
|
46
|
+
|
|
47
|
+
os.environ["STACKRESOLVE_API_KEY"] = "ar_..." # optional, raises the rate limit
|
|
48
|
+
|
|
49
|
+
agent = create_agent(
|
|
50
|
+
model="claude-sonnet-5",
|
|
51
|
+
tools=StackResolveToolkit().get_tools(),
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
result = agent.invoke({
|
|
55
|
+
"messages": [{
|
|
56
|
+
"role": "user",
|
|
57
|
+
"content": "I need to scrape javascript-heavy sites. What should I use, "
|
|
58
|
+
"and what does it cost?",
|
|
59
|
+
}]
|
|
60
|
+
})
|
|
61
|
+
print(result["messages"][-1].content)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The agent calls `stackresolve_find_tools` to get scored candidates, then
|
|
65
|
+
`stackresolve_get_pricing` on the winner. Two calls, structured answers, no scraping.
|
|
66
|
+
|
|
67
|
+
## Single tools
|
|
68
|
+
|
|
69
|
+
Import only what you need:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from langchain_stackresolve import StackResolveAudit, StackResolveFindTools
|
|
73
|
+
|
|
74
|
+
audit = StackResolveAudit()
|
|
75
|
+
print(audit.invoke({"domain": "stripe.com"}))
|
|
76
|
+
|
|
77
|
+
find = StackResolveFindTools()
|
|
78
|
+
print(find.invoke({"task": "send transactional email from a Node service"}))
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Tools
|
|
82
|
+
|
|
83
|
+
| Tool | What it answers |
|
|
84
|
+
|---|---|
|
|
85
|
+
| `stackresolve_find_tools` | "What should I use for this task?" Ranked, with AgentReady scores. |
|
|
86
|
+
| `stackresolve_search_tools` | Registry search filtered on API, MCP, CLI, OpenAPI, or self-serve. |
|
|
87
|
+
| `stackresolve_compare_products` | Side-by-side on scores, capabilities, and pricing. |
|
|
88
|
+
| `stackresolve_audit` | 0-100 agent-readiness score for a domain, plus failing checks. |
|
|
89
|
+
| `stackresolve_get_company` | Structured company facts from a domain. |
|
|
90
|
+
| `stackresolve_get_pricing` | Current plans and prices as structured data. |
|
|
91
|
+
| `stackresolve_find_competitors` | Competitors, with how each one differs. |
|
|
92
|
+
| `stackresolve_research_company` | Deep research with sources, answering a question. |
|
|
93
|
+
|
|
94
|
+
All eight work without a key, subject to an anonymous rate limit. A free key raises the
|
|
95
|
+
limit and is required for account endpoints (monitors, usage, discovery runs), which this
|
|
96
|
+
package does not expose.
|
|
97
|
+
|
|
98
|
+
## Configuration
|
|
99
|
+
|
|
100
|
+
The toolkit and every tool accept `api_key` and `base_url`, and otherwise read
|
|
101
|
+
`STACKRESOLVE_API_KEY` and `STACKRESOLVE_BASE_URL` from the environment.
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
toolkit = StackResolveToolkit(api_key="ar_...")
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Pass an existing SDK client to share connection state:
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
from stackresolve import StackResolve
|
|
111
|
+
|
|
112
|
+
client = StackResolve(api_key="ar_...")
|
|
113
|
+
tools = StackResolveToolkit(client=client).get_tools()
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Errors
|
|
117
|
+
|
|
118
|
+
Tools return a readable message rather than raising, so a failed call does not end the
|
|
119
|
+
agent's turn. A missing key, an exhausted allowance, and a rate limit each come back as
|
|
120
|
+
text the model can act on.
|
|
121
|
+
|
|
122
|
+
## Output size
|
|
123
|
+
|
|
124
|
+
Tool output is capped at 6,000 characters and marked when truncated, so a large registry
|
|
125
|
+
or research payload cannot flood the context window. Change it with
|
|
126
|
+
`langchain_stackresolve.tools.MAX_CHARS`.
|
|
127
|
+
|
|
128
|
+
## Also available
|
|
129
|
+
|
|
130
|
+
- Hosted MCP server (no install): `https://mcp.stackresolve.dev/mcp`
|
|
131
|
+
- TypeScript SDK and CLI: `npm install stackresolve`
|
|
132
|
+
- Python SDK on its own: `pip install stackresolve`
|
|
133
|
+
- REST API: `https://api.stackresolve.dev`, OpenAPI at `/openapi.json`
|
|
134
|
+
|
|
135
|
+
## License
|
|
136
|
+
|
|
137
|
+
MIT
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
langchain_stackresolve/__init__.py,sha256=_5n5J6YeP0_D_KVwiYiWc02vGpuymqlCH2R_YpAypJA,951
|
|
2
|
+
langchain_stackresolve/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
langchain_stackresolve/toolkit.py,sha256=IzPoJPXv5CG6bArYIwQ4eCHu_OrBBfuURYJdS7TgIt0,1433
|
|
4
|
+
langchain_stackresolve/tools.py,sha256=WCbIImP-R7a7b22SVgDe1crjw-lsY78iSXSPIlacaKw,15610
|
|
5
|
+
langchain_stackresolve-0.1.0.dist-info/METADATA,sha256=xqXEbYLrK0wbZzo_foLVxCzi3cF28V3NSTCn66Rz9Bs,4686
|
|
6
|
+
langchain_stackresolve-0.1.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
7
|
+
langchain_stackresolve-0.1.0.dist-info/licenses/LICENSE,sha256=N2923vLEHEI8BVat-UciamBUfBDvrsQFf50qm4qmQjs,1069
|
|
8
|
+
langchain_stackresolve-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 StackResolve
|
|
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.
|