crewai-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.
@@ -0,0 +1,29 @@
1
+ """StackResolve tools for CrewAI."""
2
+
3
+ from .tools import (
4
+ ALL_TOOL_CLASSES,
5
+ StackResolveAudit,
6
+ StackResolveCompareProducts,
7
+ StackResolveFindCompetitors,
8
+ StackResolveFindTools,
9
+ StackResolveGetCompany,
10
+ StackResolveGetPricing,
11
+ StackResolveResearchCompany,
12
+ StackResolveSearchTools,
13
+ stackresolve_tools,
14
+ )
15
+
16
+ __version__ = "0.1.0"
17
+ __all__ = [
18
+ "stackresolve_tools",
19
+ "StackResolveFindTools",
20
+ "StackResolveSearchTools",
21
+ "StackResolveCompareProducts",
22
+ "StackResolveAudit",
23
+ "StackResolveGetCompany",
24
+ "StackResolveGetPricing",
25
+ "StackResolveFindCompetitors",
26
+ "StackResolveResearchCompany",
27
+ "ALL_TOOL_CLASSES",
28
+ "__version__",
29
+ ]
@@ -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,216 @@
1
+ """CrewAI tools for StackResolve.
2
+
3
+ from crewai import Agent
4
+ from crewai_stackresolve import stackresolve_tools
5
+
6
+ analyst = Agent(role="Tooling analyst", tools=stackresolve_tools())
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Any, List, Optional, Type
12
+
13
+ from crewai.tools import BaseTool
14
+ from pydantic import BaseModel, ConfigDict, Field, PrivateAttr
15
+ from stackresolve import StackResolve
16
+
17
+ from ._common import build_client, call, requirements
18
+
19
+
20
+ class _StackResolveTool(BaseTool):
21
+ """Shared plumbing: one client, uniform error handling."""
22
+
23
+ model_config = ConfigDict(arbitrary_types_allowed=True)
24
+
25
+ _client: Optional[StackResolve] = PrivateAttr(default=None)
26
+
27
+ def __init__(
28
+ self,
29
+ client: Optional[StackResolve] = None,
30
+ api_key: Optional[str] = None,
31
+ base_url: Optional[str] = None,
32
+ **kwargs: Any,
33
+ ) -> None:
34
+ super().__init__(**kwargs)
35
+ self._client = client if client is not None else build_client(api_key, base_url)
36
+
37
+ @property
38
+ def client(self) -> StackResolve:
39
+ if self._client is None: # pragma: no cover - defensive
40
+ self._client = build_client()
41
+ return self._client
42
+
43
+
44
+ class FindToolsInput(BaseModel):
45
+ task: str = Field(
46
+ description="The engineering task in plain English, e.g. 'scrape javascript-heavy sites'."
47
+ )
48
+
49
+
50
+ class StackResolveFindTools(_StackResolveTool):
51
+ name: str = "Find software for a task"
52
+ description: str = (
53
+ "Find the best software for an engineering task, ranked by how well it works "
54
+ "for AI agents. Returns candidates with an AgentReady score (0-100), what each "
55
+ "does, and why it fits. Use this to choose a library, API, or service with "
56
+ "current scored options rather than recalling one from memory."
57
+ )
58
+ args_schema: Type[BaseModel] = FindToolsInput
59
+
60
+ def _run(self, task: str) -> str:
61
+ return call(self.client.find_tools, task)
62
+
63
+
64
+ class SearchToolsInput(BaseModel):
65
+ query: str = Field(description="Search query for the StackResolve registry.")
66
+ api: Optional[bool] = Field(default=None, description="Require a public API.")
67
+ mcp: Optional[bool] = Field(default=None, description="Require an MCP server.")
68
+ cli: Optional[bool] = Field(default=None, description="Require a CLI.")
69
+ openapi: Optional[bool] = Field(
70
+ default=None, description="Require a published OpenAPI spec."
71
+ )
72
+ self_serve: Optional[bool] = Field(
73
+ default=None, description="Require self-serve signup with no sales call."
74
+ )
75
+
76
+
77
+ class StackResolveSearchTools(_StackResolveTool):
78
+ name: str = "Search the software registry"
79
+ description: str = (
80
+ "Search the StackResolve registry, filtering on capabilities an agent needs: a "
81
+ "public API, an MCP server, a CLI, an OpenAPI spec, or self-serve signup. Use "
82
+ "this when the requirement is concrete."
83
+ )
84
+ args_schema: Type[BaseModel] = SearchToolsInput
85
+
86
+ def _run(
87
+ self,
88
+ query: str,
89
+ api: Optional[bool] = None,
90
+ mcp: Optional[bool] = None,
91
+ cli: Optional[bool] = None,
92
+ openapi: Optional[bool] = None,
93
+ self_serve: Optional[bool] = None,
94
+ ) -> str:
95
+ return call(
96
+ self.client.search,
97
+ query,
98
+ requirements(
99
+ api=api, mcp=mcp, cli=cli, openapi=openapi, self_serve=self_serve
100
+ ),
101
+ )
102
+
103
+
104
+ class CompareInput(BaseModel):
105
+ slugs: List[str] = Field(
106
+ description="Registry slugs to compare, e.g. ['firecrawl', 'apify']."
107
+ )
108
+
109
+
110
+ class StackResolveCompareProducts(_StackResolveTool):
111
+ name: str = "Compare products"
112
+ description: str = (
113
+ "Compare two or more registry products side by side on AgentReady scores, "
114
+ "capabilities, and pricing. Use after narrowing to a shortlist."
115
+ )
116
+ args_schema: Type[BaseModel] = CompareInput
117
+
118
+ def _run(self, slugs: List[str]) -> str:
119
+ return call(self.client.compare, slugs)
120
+
121
+
122
+ class DomainInput(BaseModel):
123
+ domain: str = Field(description="Domain, e.g. 'stripe.com'.")
124
+
125
+
126
+ class StackResolveAudit(_StackResolveTool):
127
+ name: str = "Audit agent readiness"
128
+ description: str = (
129
+ "Audit a domain for agent readiness. Returns a 0-100 score, sub-scores for "
130
+ "discovery, understanding, adoption, and operability, and the checks that "
131
+ "failed. Use it to judge whether an agent can actually work with a vendor."
132
+ )
133
+ args_schema: Type[BaseModel] = DomainInput
134
+
135
+ def _run(self, domain: str) -> str:
136
+ return call(self.client.audit, domain)
137
+
138
+
139
+ class StackResolveGetCompany(_StackResolveTool):
140
+ name: str = "Get company facts"
141
+ description: str = (
142
+ "Get structured facts about a company from its domain: what it does, category, "
143
+ "size, funding, and location. Prefer this over searching the web and reading "
144
+ "pages, which costs many more tool calls and returns unstructured text."
145
+ )
146
+ args_schema: Type[BaseModel] = DomainInput
147
+
148
+ def _run(self, domain: str) -> str:
149
+ return call(self.client.get_company, domain)
150
+
151
+
152
+ class StackResolveGetPricing(_StackResolveTool):
153
+ name: str = "Get company pricing"
154
+ description: str = (
155
+ "Get a company's current pricing as structured data: plans, prices, billing "
156
+ "period, and what each tier includes. Pricing pages change often, so prefer "
157
+ "this over recalling a price from memory."
158
+ )
159
+ args_schema: Type[BaseModel] = DomainInput
160
+
161
+ def _run(self, domain: str) -> str:
162
+ return call(self.client.get_pricing, domain)
163
+
164
+
165
+ class StackResolveFindCompetitors(_StackResolveTool):
166
+ name: str = "Find competitors"
167
+ description: str = (
168
+ "Find a company's competitors from its domain, with a short note on how each "
169
+ "one differs. Use this to widen a shortlist."
170
+ )
171
+ args_schema: Type[BaseModel] = DomainInput
172
+
173
+ def _run(self, domain: str) -> str:
174
+ return call(self.client.get_competitors, domain)
175
+
176
+
177
+ class ResearchInput(BaseModel):
178
+ domain: str = Field(description="Company domain, e.g. 'anthropic.com'.")
179
+ question: Optional[str] = Field(
180
+ default=None, description="Optional specific question. Omit for a summary."
181
+ )
182
+
183
+
184
+ class StackResolveResearchCompany(_StackResolveTool):
185
+ name: str = "Research a company"
186
+ description: str = (
187
+ "Run deep research on a company and get a synthesized answer with sources. Use "
188
+ "this when the answer needs current web evidence rather than a stored fact. "
189
+ "Slower and metered, so try the company facts tool first."
190
+ )
191
+ args_schema: Type[BaseModel] = ResearchInput
192
+
193
+ def _run(self, domain: str, question: Optional[str] = None) -> str:
194
+ return call(self.client.research, domain, question)
195
+
196
+
197
+ ALL_TOOL_CLASSES = [
198
+ StackResolveFindTools,
199
+ StackResolveSearchTools,
200
+ StackResolveCompareProducts,
201
+ StackResolveAudit,
202
+ StackResolveGetCompany,
203
+ StackResolveGetPricing,
204
+ StackResolveFindCompetitors,
205
+ StackResolveResearchCompany,
206
+ ]
207
+
208
+
209
+ def stackresolve_tools(
210
+ api_key: Optional[str] = None,
211
+ base_url: Optional[str] = None,
212
+ client: Optional[StackResolve] = None,
213
+ ) -> List[BaseTool]:
214
+ """Every StackResolve tool, sharing one HTTP client."""
215
+ shared = client if client is not None else build_client(api_key, base_url)
216
+ return [cls(client=shared) for cls in ALL_TOOL_CLASSES]
@@ -0,0 +1,89 @@
1
+ Metadata-Version: 2.5
2
+ Name: crewai-stackresolve
3
+ Version: 0.1.0
4
+ Summary: CrewAI 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,crewai,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.14,>=3.10
16
+ Requires-Dist: crewai>=0.100.0
17
+ Requires-Dist: pydantic>=2.0
18
+ Requires-Dist: stackresolve>=0.1.0
19
+ Provides-Extra: test
20
+ Requires-Dist: pytest>=7.0; extra == 'test'
21
+ Description-Content-Type: text/markdown
22
+
23
+ # crewai-stackresolve
24
+
25
+ CrewAI tools for [StackResolve](https://stackresolve.dev): web intelligence for AI agents.
26
+
27
+ Choose software for a task, compare vendors, audit agent-readiness, and pull structured
28
+ company facts. One tool call instead of a search-and-scrape loop.
29
+
30
+ ```bash
31
+ pip install crewai-stackresolve
32
+ ```
33
+
34
+ ## Quickstart
35
+
36
+ ```python
37
+ from crewai import Agent, Task, Crew
38
+ from crewai_stackresolve import stackresolve_tools
39
+
40
+ analyst = Agent(
41
+ role="Tooling analyst",
42
+ goal="Pick the right software for a task and justify the cost",
43
+ backstory="You evaluate vendors on agent-readiness, not marketing copy.",
44
+ tools=stackresolve_tools(),
45
+ )
46
+
47
+ task = Task(
48
+ description="We need to scrape javascript-heavy sites. Recommend a tool and its cost.",
49
+ expected_output="A recommendation with the AgentReady score and current pricing.",
50
+ agent=analyst,
51
+ )
52
+
53
+ print(Crew(agents=[analyst], tasks=[task]).kickoff())
54
+ ```
55
+
56
+ ## Tools
57
+
58
+ Find software for a task, Search the software registry, Compare products, Audit agent
59
+ readiness, Get company facts, Get company pricing, Find competitors, Research a company.
60
+
61
+ All of them work without a key, subject to an anonymous rate limit. A free key from
62
+ [stackresolve.dev/developers](https://stackresolve.dev/developers) raises it:
63
+
64
+ ```python
65
+ stackresolve_tools(api_key="ar_...") # or set STACKRESOLVE_API_KEY
66
+ ```
67
+
68
+ Import a single tool if you do not want the whole set:
69
+
70
+ ```python
71
+ from crewai_stackresolve import StackResolveAudit
72
+ agent = Agent(role="Auditor", tools=[StackResolveAudit()])
73
+ ```
74
+
75
+ ## Behavior worth knowing
76
+
77
+ Tools return readable text on failure rather than raising, so a timeout or an exhausted
78
+ allowance does not end the crew's run. Output is capped at 6,000 characters and stays
79
+ valid JSON when truncated.
80
+
81
+ ## Also available
82
+
83
+ Hosted MCP server: `https://mcp.stackresolve.dev/mcp`. LangChain: `pip install
84
+ langchain-stackresolve`. LlamaIndex: `pip install llama-index-tools-stackresolve`.
85
+ TypeScript: `npm install stackresolve`.
86
+
87
+ ## License
88
+
89
+ MIT
@@ -0,0 +1,7 @@
1
+ crewai_stackresolve/__init__.py,sha256=pgBy-UWZ9kKQCruFgUNJlfD3ml9lmfWF39o5JzAj4-Y,697
2
+ crewai_stackresolve/_common.py,sha256=Tv_iE5JC6K4EM_0v6QIKEAojDum4ia4dCHwFGlbeUNg,3830
3
+ crewai_stackresolve/tools.py,sha256=kfub1z1XSEtn-OvCJ6OooSw4mnwcGDJWKUY16nEw4vc,7417
4
+ crewai_stackresolve-0.1.0.dist-info/METADATA,sha256=aIgdzF3jXJqc2LHC9ocGryRG0rU8FAPnw86icRtktjQ,2849
5
+ crewai_stackresolve-0.1.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
6
+ crewai_stackresolve-0.1.0.dist-info/licenses/LICENSE,sha256=N2923vLEHEI8BVat-UciamBUfBDvrsQFf50qm4qmQjs,1069
7
+ crewai_stackresolve-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -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.