llama-index-tools-stackresolve 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,6 @@
1
+ .venv/
2
+ dist/
3
+ build/
4
+ *.egg-info/
5
+ __pycache__/
6
+ .pytest_cache/
@@ -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.
@@ -0,0 +1,75 @@
1
+ Metadata-Version: 2.5
2
+ Name: llama-index-tools-stackresolve
3
+ Version: 0.1.0
4
+ Summary: LlamaIndex 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
+ Author: StackResolve
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Keywords: agent-tools,agentready,ai-agents,llama-index,llamaindex,mcp,stackresolve
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
+ Requires-Python: >=3.9
16
+ Requires-Dist: llama-index-core>=0.11.0
17
+ Requires-Dist: stackresolve>=0.1.0
18
+ Provides-Extra: test
19
+ Requires-Dist: pytest>=7.0; extra == 'test'
20
+ Description-Content-Type: text/markdown
21
+
22
+ # llama-index-tools-stackresolve
23
+
24
+ LlamaIndex tools for [StackResolve](https://stackresolve.dev): web intelligence for AI agents.
25
+
26
+ Choose software for a task, compare vendors, audit agent-readiness, and pull structured
27
+ company facts. One tool call instead of a search-and-scrape loop.
28
+
29
+ ```bash
30
+ pip install llama-index-tools-stackresolve
31
+ ```
32
+
33
+ ## Quickstart
34
+
35
+ ```python
36
+ from llama_index.tools.stackresolve import StackResolveToolSpec
37
+ from llama_index.core.agent.workflow import FunctionAgent
38
+ from llama_index.llms.anthropic import Anthropic
39
+
40
+ agent = FunctionAgent(
41
+ tools=StackResolveToolSpec().to_tool_list(),
42
+ llm=Anthropic(model="claude-sonnet-5"),
43
+ )
44
+
45
+ print(await agent.run("I need to scrape javascript-heavy sites. What should I use, and what does it cost?"))
46
+ ```
47
+
48
+ ## Tools
49
+
50
+ `find_tools`, `search_tools`, `compare_products`, `audit`, `get_company`, `get_pricing`,
51
+ `find_competitors`, `research_company`.
52
+
53
+ All of them work without a key, subject to an anonymous rate limit. A free key from
54
+ [stackresolve.dev/developers](https://stackresolve.dev/developers) raises it, passed to
55
+ the spec or set as `STACKRESOLVE_API_KEY`.
56
+
57
+ ```python
58
+ StackResolveToolSpec(api_key="ar_...")
59
+ ```
60
+
61
+ ## Behavior worth knowing
62
+
63
+ Tools return readable text on failure rather than raising, so a timeout or an exhausted
64
+ allowance does not end the agent's run. Output is capped at 6,000 characters and stays
65
+ valid JSON when truncated.
66
+
67
+ ## Also available
68
+
69
+ Hosted MCP server: `https://mcp.stackresolve.dev/mcp`. LangChain: `pip install
70
+ langchain-stackresolve`. CrewAI: `pip install crewai-stackresolve`. TypeScript: `npm
71
+ install stackresolve`.
72
+
73
+ ## License
74
+
75
+ MIT
@@ -0,0 +1,54 @@
1
+ # llama-index-tools-stackresolve
2
+
3
+ LlamaIndex tools for [StackResolve](https://stackresolve.dev): web intelligence for AI agents.
4
+
5
+ Choose software for a task, compare vendors, audit agent-readiness, and pull structured
6
+ company facts. One tool call instead of a search-and-scrape loop.
7
+
8
+ ```bash
9
+ pip install llama-index-tools-stackresolve
10
+ ```
11
+
12
+ ## Quickstart
13
+
14
+ ```python
15
+ from llama_index.tools.stackresolve import StackResolveToolSpec
16
+ from llama_index.core.agent.workflow import FunctionAgent
17
+ from llama_index.llms.anthropic import Anthropic
18
+
19
+ agent = FunctionAgent(
20
+ tools=StackResolveToolSpec().to_tool_list(),
21
+ llm=Anthropic(model="claude-sonnet-5"),
22
+ )
23
+
24
+ print(await agent.run("I need to scrape javascript-heavy sites. What should I use, and what does it cost?"))
25
+ ```
26
+
27
+ ## Tools
28
+
29
+ `find_tools`, `search_tools`, `compare_products`, `audit`, `get_company`, `get_pricing`,
30
+ `find_competitors`, `research_company`.
31
+
32
+ All of them work without a key, subject to an anonymous rate limit. A free key from
33
+ [stackresolve.dev/developers](https://stackresolve.dev/developers) raises it, passed to
34
+ the spec or set as `STACKRESOLVE_API_KEY`.
35
+
36
+ ```python
37
+ StackResolveToolSpec(api_key="ar_...")
38
+ ```
39
+
40
+ ## Behavior worth knowing
41
+
42
+ Tools return readable text on failure rather than raising, so a timeout or an exhausted
43
+ allowance does not end the agent's run. Output is capped at 6,000 characters and stays
44
+ valid JSON when truncated.
45
+
46
+ ## Also available
47
+
48
+ Hosted MCP server: `https://mcp.stackresolve.dev/mcp`. LangChain: `pip install
49
+ langchain-stackresolve`. CrewAI: `pip install crewai-stackresolve`. TypeScript: `npm
50
+ install stackresolve`.
51
+
52
+ ## License
53
+
54
+ MIT
@@ -0,0 +1,6 @@
1
+ """StackResolve tools for LlamaIndex."""
2
+
3
+ from .base import StackResolveToolSpec
4
+
5
+ __version__ = "0.1.0"
6
+ __all__ = ["StackResolveToolSpec", "__version__"]
@@ -0,0 +1,98 @@
1
+ """Shared plumbing for the StackResolve framework adapters.
2
+
3
+ Vendored into each adapter package rather than published as a fourth PyPI package:
4
+ one more dependency on every install is a worse trade than ~80 duplicated lines,
5
+ and the adapters must be able to move independently of each other.
6
+
7
+ Keep this file the single source and re-copy it, so a fix like the uncaught-timeout
8
+ one lands everywhere at once.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import os
15
+ from typing import Any, Dict, Optional
16
+
17
+ from stackresolve import StackResolve, StackResolveError
18
+
19
+ # Tool output goes straight into a prompt, so cap it. Registry and research payloads
20
+ # can run to tens of kilobytes and the tail is rarely what the model needs.
21
+ MAX_CHARS = 6000
22
+
23
+
24
+ def build_client(
25
+ api_key: Optional[str] = None, base_url: Optional[str] = None
26
+ ) -> StackResolve:
27
+ """Construct an SDK client, falling back to the standard environment variables."""
28
+ opts: Dict[str, Any] = {}
29
+ key = api_key or os.environ.get("STACKRESOLVE_API_KEY")
30
+ if key:
31
+ opts["api_key"] = key
32
+ url = base_url or os.environ.get("STACKRESOLVE_BASE_URL")
33
+ if url:
34
+ opts["base_url"] = url
35
+ return StackResolve(**opts)
36
+
37
+
38
+ def dump(payload: Any, max_chars: int = MAX_CHARS) -> str:
39
+ """Serialize to JSON, truncating on a character budget and staying valid JSON.
40
+
41
+ Slicing the serialized text mid-token would leave output a caller re-parsing tool
42
+ results cannot load, and that failure would only appear on large payloads.
43
+ """
44
+ text = json.dumps(payload, indent=2, default=str, ensure_ascii=False)
45
+ if len(text) <= max_chars:
46
+ return text
47
+ return json.dumps(
48
+ {
49
+ "truncated": True,
50
+ "omitted_characters": len(text) - max_chars,
51
+ "note": (
52
+ f"Result exceeded {max_chars} characters. 'partial' holds the start of "
53
+ "the JSON payload. Narrow the query for a complete result."
54
+ ),
55
+ "partial": text[:max_chars],
56
+ },
57
+ indent=2,
58
+ ensure_ascii=False,
59
+ )
60
+
61
+
62
+ def call(fn: Any, *args: Any, **kwargs: Any) -> str:
63
+ """Run an SDK call and return JSON, converting any failure into readable text.
64
+
65
+ A tool that raises ends the agent's turn. A tool that explains what went wrong lets
66
+ the model recover, so a 402 becomes an instruction rather than a traceback.
67
+
68
+ The catch-all matters as much as the status branches: a read timeout, a refused
69
+ connection, or a DNS failure is an ordinary condition for a network tool.
70
+ """
71
+ try:
72
+ return dump(fn(*args, **kwargs))
73
+ except StackResolveError as err:
74
+ if err.status in (401, 403):
75
+ return (
76
+ "StackResolve rejected the credentials. Set STACKRESOLVE_API_KEY to a "
77
+ "key from https://stackresolve.dev/developers and retry."
78
+ )
79
+ if err.status == 402:
80
+ return (
81
+ "This StackResolve tool is metered and the free allowance is used up. "
82
+ "Registry reads (search, profiles, compare) still work."
83
+ )
84
+ if err.status == 429:
85
+ return "StackResolve rate limit reached. Wait a moment and retry."
86
+ return f"StackResolve API error {err.status}: {err.body}"
87
+ except Exception as err: # noqa: BLE001 - a tool must never end the agent's turn
88
+ return (
89
+ f"StackResolve request failed: {type(err).__name__}: {err}. This is usually "
90
+ "a timeout or a network problem. Retry, or check STACKRESOLVE_BASE_URL if "
91
+ "it is set."
92
+ )
93
+
94
+
95
+ def requirements(**kwargs: Any) -> Optional[Dict[str, Any]]:
96
+ """Drop unset capability filters so the API sees only what the caller asked for."""
97
+ reqs = {k: v for k, v in kwargs.items() if v is not None}
98
+ return reqs or None
@@ -0,0 +1,148 @@
1
+ """LlamaIndex tool spec for StackResolve.
2
+
3
+ from llama_index.tools.stackresolve import StackResolveToolSpec
4
+ from llama_index.core.agent.workflow import FunctionAgent
5
+
6
+ agent = FunctionAgent(tools=StackResolveToolSpec().to_tool_list(), llm=llm)
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import List, Optional
12
+
13
+ from llama_index.core.tools.tool_spec.base import BaseToolSpec
14
+ from stackresolve import StackResolve
15
+
16
+ from ._common import build_client, call, requirements
17
+
18
+
19
+ class StackResolveToolSpec(BaseToolSpec):
20
+ """Web intelligence for agents: choose software, compare vendors, research companies.
21
+
22
+ Every tool works without a key, subject to an anonymous rate limit. A free key from
23
+ https://stackresolve.dev/developers raises it, passed here or as
24
+ STACKRESOLVE_API_KEY.
25
+ """
26
+
27
+ spec_functions: List[str] = [
28
+ "find_tools",
29
+ "search_tools",
30
+ "compare_products",
31
+ "audit",
32
+ "get_company",
33
+ "get_pricing",
34
+ "find_competitors",
35
+ "research_company",
36
+ ]
37
+
38
+ def __init__(
39
+ self,
40
+ api_key: Optional[str] = None,
41
+ base_url: Optional[str] = None,
42
+ client: Optional[StackResolve] = None,
43
+ ) -> None:
44
+ self.client = client if client is not None else build_client(api_key, base_url)
45
+
46
+ def find_tools(self, task: str) -> str:
47
+ """Find the best software for an engineering task, ranked for AI agents.
48
+
49
+ Returns candidates with an AgentReady score (0-100), what each does, and why it
50
+ fits. Use this to choose a library, API, or service with current scored options
51
+ rather than recalling one from memory.
52
+
53
+ Args:
54
+ task: The task in plain English, e.g. "scrape javascript-heavy sites".
55
+ """
56
+ return call(self.client.find_tools, task)
57
+
58
+ def search_tools(
59
+ self,
60
+ query: str,
61
+ api: Optional[bool] = None,
62
+ mcp: Optional[bool] = None,
63
+ cli: Optional[bool] = None,
64
+ openapi: Optional[bool] = None,
65
+ self_serve: Optional[bool] = None,
66
+ ) -> str:
67
+ """Search the registry, filtering on capabilities an agent needs.
68
+
69
+ Use this when the requirement is concrete, e.g. "a payments provider with an MCP
70
+ server and self-serve signup".
71
+
72
+ Args:
73
+ query: Search query.
74
+ api: Require a public API.
75
+ mcp: Require an MCP server.
76
+ cli: Require a CLI.
77
+ openapi: Require a published OpenAPI spec.
78
+ self_serve: Require self-serve signup with no sales call.
79
+ """
80
+ return call(
81
+ self.client.search,
82
+ query,
83
+ requirements(
84
+ api=api, mcp=mcp, cli=cli, openapi=openapi, self_serve=self_serve
85
+ ),
86
+ )
87
+
88
+ def compare_products(self, slugs: List[str]) -> str:
89
+ """Compare products side by side on scores, capabilities, and pricing.
90
+
91
+ Args:
92
+ slugs: Registry slugs, e.g. ["firecrawl", "apify"], from find_tools or
93
+ search_tools.
94
+ """
95
+ return call(self.client.compare, slugs)
96
+
97
+ def audit(self, domain: str) -> str:
98
+ """Audit a domain for agent readiness.
99
+
100
+ Returns a 0-100 score, sub-scores for discovery, understanding, adoption, and
101
+ operability, and the checks that failed. Use it to judge whether an agent can
102
+ actually work with a vendor.
103
+
104
+ Args:
105
+ domain: Domain to audit, e.g. "stripe.com".
106
+ """
107
+ return call(self.client.audit, domain)
108
+
109
+ def get_company(self, domain: str) -> str:
110
+ """Get structured facts about a company: what it does, category, size, funding.
111
+
112
+ Prefer this over searching the web and reading pages, which costs many more tool
113
+ calls and returns unstructured text.
114
+
115
+ Args:
116
+ domain: Company domain, e.g. "vercel.com".
117
+ """
118
+ return call(self.client.get_company, domain)
119
+
120
+ def get_pricing(self, domain: str) -> str:
121
+ """Get a company's current pricing: plans, prices, period, and what each includes.
122
+
123
+ Pricing pages change often, so prefer this over recalling a price.
124
+
125
+ Args:
126
+ domain: Company domain, e.g. "vercel.com".
127
+ """
128
+ return call(self.client.get_pricing, domain)
129
+
130
+ def find_competitors(self, domain: str) -> str:
131
+ """Find a company's competitors, with a note on how each differs.
132
+
133
+ Args:
134
+ domain: Company domain, e.g. "vercel.com".
135
+ """
136
+ return call(self.client.get_competitors, domain)
137
+
138
+ def research_company(self, domain: str, question: Optional[str] = None) -> str:
139
+ """Run deep research on a company and get a synthesized answer with sources.
140
+
141
+ Use this when the answer needs current web evidence rather than a stored fact.
142
+ Slower and metered, so try get_company first for basic facts.
143
+
144
+ Args:
145
+ domain: Company domain, e.g. "anthropic.com".
146
+ question: Optional specific question. Omit for a general summary.
147
+ """
148
+ return call(self.client.research, domain, question)
@@ -0,0 +1,31 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "llama-index-tools-stackresolve"
7
+ version = "0.1.0"
8
+ description = "LlamaIndex tools for StackResolve: find, compare, and audit software for AI agents, plus structured company research."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ license-files = ["LICENSE"]
13
+ authors = [{ name = "StackResolve" }]
14
+ keywords = ["llamaindex", "llama-index", "stackresolve", "agentready", "ai-agents", "agent-tools", "mcp"]
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Intended Audience :: Developers",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Programming Language :: Python :: 3",
20
+ ]
21
+ dependencies = ["llama-index-core>=0.11.0", "stackresolve>=0.1.0"]
22
+
23
+ [project.optional-dependencies]
24
+ test = ["pytest>=7.0"]
25
+
26
+ [project.urls]
27
+ Homepage = "https://stackresolve.dev"
28
+ Repository = "https://github.com/autorevai/stackresolve"
29
+
30
+ [tool.hatch.build.targets.wheel]
31
+ packages = ["llama_index"]
@@ -0,0 +1,129 @@
1
+ """Unit tests for the StackResolve LlamaIndex tool spec. No network."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+
7
+ import pytest
8
+ from stackresolve import StackResolveError
9
+
10
+ from llama_index.tools.stackresolve import StackResolveToolSpec
11
+ from llama_index.tools.stackresolve._common import MAX_CHARS
12
+
13
+
14
+ class FakeClient:
15
+ def __init__(self, payload=None, error=None):
16
+ self.payload = payload if payload is not None else {"ok": True}
17
+ self.error = error
18
+ self.calls = []
19
+
20
+ def _handle(self, name, *args, **kwargs):
21
+ self.calls.append((name, args, kwargs))
22
+ if self.error is not None:
23
+ raise self.error
24
+ return self.payload
25
+
26
+ def find_tools(self, task):
27
+ return self._handle("find_tools", task)
28
+
29
+ def search(self, query, requirements=None):
30
+ return self._handle("search", query, requirements)
31
+
32
+ def compare(self, slugs):
33
+ return self._handle("compare", slugs)
34
+
35
+ def audit(self, domain):
36
+ return self._handle("audit", domain)
37
+
38
+ def get_company(self, domain):
39
+ return self._handle("get_company", domain)
40
+
41
+ def get_pricing(self, domain):
42
+ return self._handle("get_pricing", domain)
43
+
44
+ def get_competitors(self, domain):
45
+ return self._handle("get_competitors", domain)
46
+
47
+ def research(self, domain, question=None):
48
+ return self._handle("research", domain, question)
49
+
50
+
51
+ def spec(**kw):
52
+ return StackResolveToolSpec(client=FakeClient(**kw))
53
+
54
+
55
+ def test_tool_list_exposes_every_spec_function():
56
+ s = spec()
57
+ tools = s.to_tool_list()
58
+ assert len(tools) == len(s.spec_functions) == 8
59
+ names = [t.metadata.name for t in tools]
60
+ assert len(set(names)) == len(names)
61
+
62
+
63
+ def test_every_spec_function_exists_and_is_documented():
64
+ s = spec()
65
+ for fn_name in s.spec_functions:
66
+ fn = getattr(s, fn_name, None)
67
+ assert callable(fn), f"{fn_name} missing"
68
+ assert fn.__doc__ and len(fn.__doc__) > 60, f"{fn_name} needs a real docstring"
69
+
70
+
71
+ def test_find_tools_passes_the_task_through():
72
+ client = FakeClient(payload={"results": [{"slug": "firecrawl"}]})
73
+ out = StackResolveToolSpec(client=client).find_tools("scrape a site")
74
+ assert client.calls == [("find_tools", ("scrape a site",), {})]
75
+ assert json.loads(out)["results"][0]["slug"] == "firecrawl"
76
+
77
+
78
+ def test_search_drops_unset_requirement_flags():
79
+ client = FakeClient(payload=[])
80
+ StackResolveToolSpec(client=client).search_tools("payments", mcp=True, cli=False)
81
+ assert client.calls[0][1][1] == {"mcp": True, "cli": False}
82
+
83
+
84
+ def test_search_sends_none_when_no_requirements_given():
85
+ client = FakeClient(payload=[])
86
+ StackResolveToolSpec(client=client).search_tools("email")
87
+ assert client.calls[0][1][1] is None
88
+
89
+
90
+ def test_truncated_output_is_still_valid_json():
91
+ client = FakeClient(payload={"rows": [{"v": "x" * 200} for _ in range(200)]})
92
+ parsed = json.loads(StackResolveToolSpec(client=client).audit("stripe.com"))
93
+ assert parsed["truncated"] is True
94
+ assert parsed["omitted_characters"] > 0
95
+ assert len(parsed["partial"]) == MAX_CHARS
96
+
97
+
98
+ @pytest.mark.parametrize(
99
+ "status,expected",
100
+ [
101
+ (401, "STACKRESOLVE_API_KEY"),
102
+ (402, "free allowance"),
103
+ (429, "rate limit"),
104
+ (500, "API error 500"),
105
+ ],
106
+ )
107
+ def test_api_errors_become_readable_text(status, expected):
108
+ client = FakeClient(error=StackResolveError(status, {"detail": "nope"}))
109
+ assert expected in StackResolveToolSpec(client=client).get_pricing("vercel.com")
110
+
111
+
112
+ @pytest.mark.parametrize(
113
+ "exc",
114
+ [
115
+ ConnectionError("[Errno 61] Connection refused"),
116
+ TimeoutError("the read operation timed out"),
117
+ ],
118
+ )
119
+ def test_transport_failures_do_not_escape(exc):
120
+ client = FakeClient(error=exc)
121
+ out = StackResolveToolSpec(client=client).audit("stripe.com")
122
+ assert "StackResolve request failed" in out
123
+ assert type(exc).__name__ in out
124
+
125
+
126
+ def test_research_forwards_the_optional_question():
127
+ client = FakeClient()
128
+ StackResolveToolSpec(client=client).research_company("anthropic.com", "SLA?")
129
+ assert client.calls[0] == ("research", ("anthropic.com", "SLA?"), {})