capsolver-agent 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.
- capsolver_agent/__init__.py +28 -0
- capsolver_agent/__main__.py +112 -0
- capsolver_agent/langchain_tools.py +213 -0
- capsolver_agent/py.typed +0 -0
- capsolver_agent/schema.py +468 -0
- capsolver_agent-0.1.0.dist-info/METADATA +197 -0
- capsolver_agent-0.1.0.dist-info/RECORD +10 -0
- capsolver_agent-0.1.0.dist-info/WHEEL +4 -0
- capsolver_agent-0.1.0.dist-info/entry_points.txt +2 -0
- capsolver_agent-0.1.0.dist-info/licenses/LICENSE +15 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""CapSolver Agent — tool definitions for AI agent frameworks.
|
|
2
|
+
|
|
3
|
+
Provides two integration layers:
|
|
4
|
+
- ``schema`` : Framework-agnostic tool definitions (JSON schemas + executor)
|
|
5
|
+
- ``langchain_tools`` : LangChain BaseTool implementations
|
|
6
|
+
|
|
7
|
+
Usage (framework-agnostic):
|
|
8
|
+
from capsolver_agent.schema import get_all_tools, execute_tool
|
|
9
|
+
|
|
10
|
+
tools = get_all_tools() # list of ToolDef with JSON schemas
|
|
11
|
+
result = await execute_tool("solve_captcha", {...})
|
|
12
|
+
|
|
13
|
+
Usage (LangChain):
|
|
14
|
+
from capsolver_agent.langchain_tools import get_langchain_tools
|
|
15
|
+
|
|
16
|
+
tools = get_langchain_tools(api_key="your-key")
|
|
17
|
+
agent = create_react_agent(llm, tools)
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from capsolver_agent.schema import ToolDef, get_all_tools, execute_tool, create_executor
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"ToolDef",
|
|
24
|
+
"get_all_tools",
|
|
25
|
+
"execute_tool",
|
|
26
|
+
"create_executor",
|
|
27
|
+
]
|
|
28
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""CLI entry point for ``python -m capsolver_agent`` and the ``capsolver-agent`` console script.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
capsolver-agent list # list all available tools
|
|
5
|
+
capsolver-agent schema solve_captcha # show JSON Schema for a tool
|
|
6
|
+
capsolver-agent schema --format openai # export all tools in OpenAI format
|
|
7
|
+
capsolver-agent schema --format openai solve_captcha # export one tool in OpenAI format
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import json
|
|
14
|
+
import sys
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _cmd_list(_args: argparse.Namespace) -> None:
|
|
18
|
+
"""List all tools with name and description."""
|
|
19
|
+
from capsolver_agent.schema import get_all_tools
|
|
20
|
+
|
|
21
|
+
tools = get_all_tools()
|
|
22
|
+
for i, t in enumerate(tools, 1):
|
|
23
|
+
print(f"{i}. {t.name}")
|
|
24
|
+
# Wrap description at 72 chars
|
|
25
|
+
desc_lines = _wrap(t.description, width=72)
|
|
26
|
+
for line in desc_lines:
|
|
27
|
+
print(f" {line}")
|
|
28
|
+
if i < len(tools):
|
|
29
|
+
print()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _cmd_schema(args: argparse.Namespace) -> None:
|
|
33
|
+
"""Show JSON Schema for one or all tools."""
|
|
34
|
+
from capsolver_agent.schema import get_all_tools
|
|
35
|
+
|
|
36
|
+
tools = get_all_tools()
|
|
37
|
+
fmt = args.format
|
|
38
|
+
|
|
39
|
+
if args.tool:
|
|
40
|
+
# Single tool
|
|
41
|
+
found = next((t for t in tools if t.name == args.tool), None)
|
|
42
|
+
if found is None:
|
|
43
|
+
names = ", ".join(t.name for t in tools)
|
|
44
|
+
print(f"Error: unknown tool '{args.tool}'. Available: {names}", file=sys.stderr)
|
|
45
|
+
sys.exit(1)
|
|
46
|
+
if fmt == "openai":
|
|
47
|
+
print(json.dumps(found.to_openai_function(), indent=2, ensure_ascii=False))
|
|
48
|
+
else:
|
|
49
|
+
print(json.dumps(found.to_json_schema(), indent=2, ensure_ascii=False))
|
|
50
|
+
else:
|
|
51
|
+
# All tools
|
|
52
|
+
if fmt == "openai":
|
|
53
|
+
data = [t.to_openai_function() for t in tools]
|
|
54
|
+
else:
|
|
55
|
+
data = [t.to_json_schema() for t in tools]
|
|
56
|
+
print(json.dumps(data, indent=2, ensure_ascii=False))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _wrap(text: str, width: int = 72) -> list[str]:
|
|
60
|
+
"""Simple word-wrap helper."""
|
|
61
|
+
words = text.split()
|
|
62
|
+
lines: list[str] = []
|
|
63
|
+
current: list[str] = []
|
|
64
|
+
length = 0
|
|
65
|
+
for w in words:
|
|
66
|
+
if length + len(w) + 1 > width and current:
|
|
67
|
+
lines.append(" ".join(current))
|
|
68
|
+
current = [w]
|
|
69
|
+
length = len(w)
|
|
70
|
+
else:
|
|
71
|
+
current.append(w)
|
|
72
|
+
length += len(w) + 1
|
|
73
|
+
if current:
|
|
74
|
+
lines.append(" ".join(current))
|
|
75
|
+
return lines
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def main() -> None:
|
|
79
|
+
parser = argparse.ArgumentParser(
|
|
80
|
+
prog="capsolver-agent",
|
|
81
|
+
description="CapSolver Agent — inspect available tools and their schemas.",
|
|
82
|
+
)
|
|
83
|
+
sub = parser.add_subparsers(dest="command")
|
|
84
|
+
|
|
85
|
+
# list
|
|
86
|
+
sub.add_parser("list", help="List all available tools.")
|
|
87
|
+
|
|
88
|
+
# schema
|
|
89
|
+
schema_p = sub.add_parser("schema", help="Show JSON Schema for tools.")
|
|
90
|
+
schema_p.add_argument("tool", nargs="?", default=None, help="Tool name (omit for all).")
|
|
91
|
+
schema_p.add_argument(
|
|
92
|
+
"--format",
|
|
93
|
+
choices=["json", "openai"],
|
|
94
|
+
default="json",
|
|
95
|
+
help="Output format: json (default) or openai (OpenAI function-calling).",
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
args = parser.parse_args()
|
|
99
|
+
|
|
100
|
+
if args.command is None:
|
|
101
|
+
parser.print_help()
|
|
102
|
+
sys.exit(0)
|
|
103
|
+
|
|
104
|
+
dispatch = {
|
|
105
|
+
"list": _cmd_list,
|
|
106
|
+
"schema": _cmd_schema,
|
|
107
|
+
}
|
|
108
|
+
dispatch[args.command](args)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
if __name__ == "__main__":
|
|
112
|
+
main()
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"""LangChain BaseTool implementations for CapSolver.
|
|
2
|
+
|
|
3
|
+
Provides ready-to-use LangChain tools that can be plugged directly
|
|
4
|
+
into any LangChain Agent (ReAct, OpenAI Functions, etc.).
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
from capsolver_agent.langchain_tools import get_langchain_tools
|
|
8
|
+
|
|
9
|
+
tools = get_langchain_tools(api_key="your-api-key")
|
|
10
|
+
# Pass `tools` to your LangChain agent
|
|
11
|
+
agent = create_react_agent(llm, tools, ...)
|
|
12
|
+
|
|
13
|
+
Or import individual tools:
|
|
14
|
+
from capsolver_agent.langchain_tools import SolveCaptchaTool
|
|
15
|
+
tool = SolveCaptchaTool(api_key="your-api-key")
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import os
|
|
21
|
+
from typing import Any, Optional, Type
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
from langchain_core.tools import BaseTool
|
|
25
|
+
from pydantic import BaseModel, Field
|
|
26
|
+
|
|
27
|
+
_HAS_LANGCHAIN = True
|
|
28
|
+
except ImportError:
|
|
29
|
+
_HAS_LANGCHAIN = False
|
|
30
|
+
|
|
31
|
+
# Provide stubs so the module can be imported without langchain
|
|
32
|
+
class BaseModel: # type: ignore[no-redef]
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
class BaseTool: # type: ignore[no-redef]
|
|
36
|
+
def __init_subclass__(cls, **kwargs: Any) -> None:
|
|
37
|
+
raise ImportError(
|
|
38
|
+
"langchain-core is required for LangChain tools. Install with: pip install capsolver-agent[langchain]"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
def Field(*args: Any, **kwargs: Any) -> Any: # type: ignore[no-redef]
|
|
42
|
+
return None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _ensure_langchain() -> None:
|
|
46
|
+
if not _HAS_LANGCHAIN:
|
|
47
|
+
raise ImportError(
|
|
48
|
+
"langchain-core is required for LangChain tools. Install with: pip install capsolver-agent[langchain]"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# ── Input Schemas ─────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class SolveCaptchaInput(BaseModel):
|
|
56
|
+
"""Input schema for SolveCaptchaTool."""
|
|
57
|
+
|
|
58
|
+
captcha_type: str = Field(
|
|
59
|
+
description="Captcha family: reCaptchaV2, reCaptchaV3, or cloudflare."
|
|
60
|
+
)
|
|
61
|
+
website_url: str = Field(description="URL of the page where the captcha appears.")
|
|
62
|
+
website_key: str = Field(description="Site key used by the captcha widget.")
|
|
63
|
+
version: Optional[str] = Field(default=None, description="reCAPTCHA version: v2 or v3.")
|
|
64
|
+
page_action: Optional[str] = Field(default=None, description="Action name for reCAPTCHA v3.")
|
|
65
|
+
min_score: Optional[float] = Field(default=None, description="Minimum score for reCAPTCHA v3 (0.0-1.0).")
|
|
66
|
+
invisible: Optional[bool] = Field(default=None, description="Whether reCAPTCHA is invisible.")
|
|
67
|
+
enterprise: Optional[bool] = Field(default=None, description="Whether this is an Enterprise captcha.")
|
|
68
|
+
s_token: Optional[str] = Field(default=None, description="Enterprise 's' token.")
|
|
69
|
+
cdata: Optional[str] = Field(default=None, description="Cloudflare Turnstile cdata.")
|
|
70
|
+
proxy: Optional[str] = Field(default=None, description="Proxy (e.g. http://user:pass@ip:port).")
|
|
71
|
+
user_agent: Optional[str] = Field(default=None, description="User-Agent string.")
|
|
72
|
+
timeout: Optional[float] = Field(default=None, description="Max wait time in seconds.")
|
|
73
|
+
polling_interval: Optional[float] = Field(default=None, description="Polling interval in seconds.")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class DetectCaptchasInput(BaseModel):
|
|
77
|
+
"""Input schema for DetectCaptchasTool."""
|
|
78
|
+
|
|
79
|
+
page_url: str = Field(description="URL of the page to scan for captchas.")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class SolveOnPageInput(BaseModel):
|
|
83
|
+
"""Input schema for SolveOnPageTool."""
|
|
84
|
+
|
|
85
|
+
page_url: str = Field(description="URL of the page to solve captchas on.")
|
|
86
|
+
autofill: Optional[bool] = Field(default=True, description="Autofill solved tokens into the page.")
|
|
87
|
+
timeout: Optional[float] = Field(default=None, description="Max wait time per captcha.")
|
|
88
|
+
polling_interval: Optional[float] = Field(default=None, description="Polling interval in seconds.")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class EmptyInput(BaseModel):
|
|
92
|
+
"""Empty input for tools that take no parameters."""
|
|
93
|
+
|
|
94
|
+
pass
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# ── Tool Implementations ──────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
if _HAS_LANGCHAIN:
|
|
101
|
+
from capsolver_agent.schema import ToolExecutor
|
|
102
|
+
|
|
103
|
+
class _CapsolverToolBase(BaseTool):
|
|
104
|
+
"""Shared base for all CapSolver LangChain tools.
|
|
105
|
+
|
|
106
|
+
Holds a cached ``ToolExecutor`` so repeated calls reuse the same
|
|
107
|
+
Capsolver instance instead of constructing a new one each time.
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
api_key: str = ""
|
|
111
|
+
_executor: ToolExecutor | None = None
|
|
112
|
+
|
|
113
|
+
def _get_executor(self) -> ToolExecutor:
|
|
114
|
+
if self._executor is None:
|
|
115
|
+
from capsolver_agent.schema import create_executor
|
|
116
|
+
|
|
117
|
+
self._executor = create_executor(api_key=self.api_key)
|
|
118
|
+
return self._executor
|
|
119
|
+
|
|
120
|
+
def _run(self, **kwargs: Any) -> dict[str, Any]:
|
|
121
|
+
raise NotImplementedError("Use arun() — CapSolver tools are async.")
|
|
122
|
+
|
|
123
|
+
class SolveCaptchaTool(_CapsolverToolBase):
|
|
124
|
+
"""Solve a captcha via CapSolver API (token mode, no browser required)."""
|
|
125
|
+
|
|
126
|
+
name: str = "solve_captcha"
|
|
127
|
+
description: str = (
|
|
128
|
+
"Solve a captcha using the CapSolver API. Supports reCaptchaV2, "
|
|
129
|
+
"reCaptchaV3, and Cloudflare Turnstile. Returns the solved token."
|
|
130
|
+
)
|
|
131
|
+
args_schema: Type[BaseModel] = SolveCaptchaInput
|
|
132
|
+
|
|
133
|
+
async def _arun(self, **kwargs: Any) -> dict[str, Any]:
|
|
134
|
+
return await self._get_executor().execute("solve_captcha", kwargs)
|
|
135
|
+
|
|
136
|
+
class DetectCaptchasTool(_CapsolverToolBase):
|
|
137
|
+
"""Detect captcha types on a web page (requires playwright)."""
|
|
138
|
+
|
|
139
|
+
name: str = "detect_captchas"
|
|
140
|
+
description: str = (
|
|
141
|
+
"Scan a web page and detect which captcha types are present. "
|
|
142
|
+
"Returns a list of detected captcha families. Requires playwright."
|
|
143
|
+
)
|
|
144
|
+
args_schema: Type[BaseModel] = DetectCaptchasInput
|
|
145
|
+
|
|
146
|
+
async def _arun(self, **kwargs: Any) -> dict[str, Any]:
|
|
147
|
+
return await self._get_executor().execute("detect_captchas", kwargs)
|
|
148
|
+
|
|
149
|
+
class SolveOnPageTool(_CapsolverToolBase):
|
|
150
|
+
"""Detect, solve, and autofill captchas on a page (requires playwright)."""
|
|
151
|
+
|
|
152
|
+
name: str = "solve_on_page"
|
|
153
|
+
description: str = (
|
|
154
|
+
"One-shot: open a page, detect all captchas, solve them, and autofill tokens. Requires playwright."
|
|
155
|
+
)
|
|
156
|
+
args_schema: Type[BaseModel] = SolveOnPageInput
|
|
157
|
+
|
|
158
|
+
async def _arun(self, **kwargs: Any) -> dict[str, Any]:
|
|
159
|
+
return await self._get_executor().execute("solve_on_page", kwargs)
|
|
160
|
+
|
|
161
|
+
class GetBalanceTool(_CapsolverToolBase):
|
|
162
|
+
"""Get CapSolver account balance."""
|
|
163
|
+
|
|
164
|
+
name: str = "get_balance"
|
|
165
|
+
description: str = "Get the current CapSolver account balance and package information."
|
|
166
|
+
args_schema: Type[BaseModel] = EmptyInput
|
|
167
|
+
|
|
168
|
+
async def _arun(self, **kwargs: Any) -> dict[str, Any]:
|
|
169
|
+
return await self._get_executor().execute("get_balance", kwargs)
|
|
170
|
+
|
|
171
|
+
class GetSupportedCaptchasTool(_CapsolverToolBase):
|
|
172
|
+
"""List supported captcha types."""
|
|
173
|
+
|
|
174
|
+
name: str = "get_supported_captchas"
|
|
175
|
+
description: str = "List all captcha types supported by this CapSolver instance."
|
|
176
|
+
args_schema: Type[BaseModel] = EmptyInput
|
|
177
|
+
|
|
178
|
+
async def _arun(self, **kwargs: Any) -> dict[str, Any]:
|
|
179
|
+
return await self._get_executor().execute("get_supported_captchas", kwargs)
|
|
180
|
+
|
|
181
|
+
else:
|
|
182
|
+
# Stubs when langchain is not installed
|
|
183
|
+
SolveCaptchaTool = None # type: ignore[assignment,misc]
|
|
184
|
+
DetectCaptchasTool = None # type: ignore[assignment,misc]
|
|
185
|
+
SolveOnPageTool = None # type: ignore[assignment,misc]
|
|
186
|
+
GetBalanceTool = None # type: ignore[assignment,misc]
|
|
187
|
+
GetSupportedCaptchasTool = None # type: ignore[assignment,misc]
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
# ── Factory ───────────────────────────────────────────────────────
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def get_langchain_tools(api_key: str | None = None) -> list[Any]:
|
|
194
|
+
"""Create all LangChain tools, ready to pass to an Agent.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
api_key: CapSolver API key. Falls back to CAPSOLVER_API_KEY env var.
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
A list of BaseTool instances.
|
|
201
|
+
|
|
202
|
+
Raises:
|
|
203
|
+
ImportError: If langchain-core is not installed.
|
|
204
|
+
"""
|
|
205
|
+
_ensure_langchain()
|
|
206
|
+
key = api_key or os.environ.get("CAPSOLVER_API_KEY", "")
|
|
207
|
+
return [
|
|
208
|
+
SolveCaptchaTool(api_key=key),
|
|
209
|
+
DetectCaptchasTool(api_key=key),
|
|
210
|
+
SolveOnPageTool(api_key=key),
|
|
211
|
+
GetBalanceTool(api_key=key),
|
|
212
|
+
GetSupportedCaptchasTool(api_key=key),
|
|
213
|
+
]
|
capsolver_agent/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
"""Framework-agnostic tool schema definitions and executor.
|
|
2
|
+
|
|
3
|
+
Each tool is described as a ``ToolDef`` — a simple dataclass carrying the
|
|
4
|
+
tool's name, description, and JSON Schema for parameters. Any agent
|
|
5
|
+
framework (OpenAI function calling, custom orchestrators, etc.) can
|
|
6
|
+
consume these definitions directly.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
from capsolver_agent.schema import get_all_tools, create_executor
|
|
10
|
+
|
|
11
|
+
executor = create_executor(api_key="your-key")
|
|
12
|
+
tools = get_all_tools()
|
|
13
|
+
|
|
14
|
+
# Feed tool schemas to your LLM
|
|
15
|
+
schemas = [t.to_openai_function() for t in tools]
|
|
16
|
+
|
|
17
|
+
# Execute a tool call from the LLM response
|
|
18
|
+
result = await executor.execute("solve_captcha", {
|
|
19
|
+
"captcha_type": "reCaptchaV2",
|
|
20
|
+
"website_url": "https://example.com",
|
|
21
|
+
"website_key": "6Le-wvkSAAAAAPBMRT...",
|
|
22
|
+
})
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import os
|
|
28
|
+
from dataclasses import dataclass
|
|
29
|
+
from typing import Any
|
|
30
|
+
|
|
31
|
+
from capsolver_core import Capsolver, CaptchaInfo, CaptchaType
|
|
32
|
+
from capsolver_core.core.errors import CapsolverError, CapsolverTimeoutError, NetworkError, RateLimitError
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _error_response(exc: Exception) -> dict[str, Any]:
|
|
36
|
+
"""Build a consistent error dict from an exception.
|
|
37
|
+
|
|
38
|
+
Extracts structured fields from CapsolverError when available.
|
|
39
|
+
"""
|
|
40
|
+
base: dict[str, Any] = {"success": False, "error": str(exc)}
|
|
41
|
+
if isinstance(exc, CapsolverTimeoutError) and exc.task_id is not None:
|
|
42
|
+
base["task_id"] = exc.task_id
|
|
43
|
+
if isinstance(exc, NetworkError) and exc.cause is not None:
|
|
44
|
+
base["cause"] = str(exc.cause)
|
|
45
|
+
if isinstance(exc, RateLimitError):
|
|
46
|
+
base["error_type"] = "rate_limit"
|
|
47
|
+
if isinstance(exc, CapsolverError):
|
|
48
|
+
if exc.error_id is not None:
|
|
49
|
+
base["error_id"] = exc.error_id
|
|
50
|
+
if exc.error_code:
|
|
51
|
+
base["error_code"] = exc.error_code
|
|
52
|
+
if exc.error_description:
|
|
53
|
+
base["error_description"] = exc.error_description
|
|
54
|
+
if exc.http_status is not None:
|
|
55
|
+
base["http_status"] = exc.http_status
|
|
56
|
+
return base
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# ── Tool Schema ───────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass
|
|
63
|
+
class ToolDef:
|
|
64
|
+
"""Framework-agnostic definition of a single tool."""
|
|
65
|
+
|
|
66
|
+
name: str
|
|
67
|
+
description: str
|
|
68
|
+
parameters: dict[str, Any] # JSON Schema object
|
|
69
|
+
|
|
70
|
+
def to_openai_function(self) -> dict[str, Any]:
|
|
71
|
+
"""Convert to OpenAI function-calling format."""
|
|
72
|
+
return {
|
|
73
|
+
"type": "function",
|
|
74
|
+
"function": {
|
|
75
|
+
"name": self.name,
|
|
76
|
+
"description": self.description,
|
|
77
|
+
"parameters": self.parameters,
|
|
78
|
+
},
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
def to_json_schema(self) -> dict[str, Any]:
|
|
82
|
+
"""Return the full tool spec as a JSON-schema-compatible dict."""
|
|
83
|
+
return {
|
|
84
|
+
"name": self.name,
|
|
85
|
+
"description": self.description,
|
|
86
|
+
"inputSchema": self.parameters,
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# ── Tool Definitions ─────────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
_SOLVE_CAPTCHA_SCHEMA: dict[str, Any] = {
|
|
94
|
+
"type": "object",
|
|
95
|
+
"properties": {
|
|
96
|
+
"captcha_type": {
|
|
97
|
+
"type": "string",
|
|
98
|
+
"enum": ["reCaptchaV2", "reCaptchaV3", "cloudflare"],
|
|
99
|
+
"description": "The captcha family to solve.",
|
|
100
|
+
},
|
|
101
|
+
"website_url": {
|
|
102
|
+
"type": "string",
|
|
103
|
+
"description": "The URL of the page where the captcha appears.",
|
|
104
|
+
},
|
|
105
|
+
"website_key": {
|
|
106
|
+
"type": "string",
|
|
107
|
+
"description": "The site key used by the captcha widget.",
|
|
108
|
+
},
|
|
109
|
+
"version": {
|
|
110
|
+
"type": ["string", "null"],
|
|
111
|
+
"enum": ["v2", "v3", None],
|
|
112
|
+
"description": "reCAPTCHA version (v2 or v3). Only for reCAPTCHA.",
|
|
113
|
+
},
|
|
114
|
+
"page_action": {
|
|
115
|
+
"type": ["string", "null"],
|
|
116
|
+
"description": "The action name for reCAPTCHA v3.",
|
|
117
|
+
},
|
|
118
|
+
"min_score": {
|
|
119
|
+
"type": ["number", "null"],
|
|
120
|
+
"description": "Minimum score for reCAPTCHA v3 (0.0 - 1.0).",
|
|
121
|
+
},
|
|
122
|
+
"invisible": {
|
|
123
|
+
"type": ["boolean", "null"],
|
|
124
|
+
"description": "Whether the reCAPTCHA widget uses invisible mode.",
|
|
125
|
+
},
|
|
126
|
+
"enterprise": {
|
|
127
|
+
"type": ["boolean", "null"],
|
|
128
|
+
"description": "Whether this is an Enterprise captcha.",
|
|
129
|
+
},
|
|
130
|
+
"s_token": {
|
|
131
|
+
"type": ["string", "null"],
|
|
132
|
+
"description": "Enterprise 's' token.",
|
|
133
|
+
},
|
|
134
|
+
"cdata": {
|
|
135
|
+
"type": ["string", "null"],
|
|
136
|
+
"description": "Cloudflare Turnstile cdata parameter.",
|
|
137
|
+
},
|
|
138
|
+
"proxy": {
|
|
139
|
+
"type": ["string", "null"],
|
|
140
|
+
"description": "Proxy string (e.g. http://user:pass@ip:port).",
|
|
141
|
+
},
|
|
142
|
+
"user_agent": {
|
|
143
|
+
"type": ["string", "null"],
|
|
144
|
+
"description": "User-Agent string to use for solving.",
|
|
145
|
+
},
|
|
146
|
+
"timeout": {
|
|
147
|
+
"type": ["number", "null"],
|
|
148
|
+
"description": "Max seconds to wait for a solution (default: 120).",
|
|
149
|
+
},
|
|
150
|
+
"polling_interval": {
|
|
151
|
+
"type": ["number", "null"],
|
|
152
|
+
"description": "Seconds between polling attempts (default: 5).",
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
"required": ["captcha_type", "website_url", "website_key"],
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
_DETECT_CAPTCHAS_SCHEMA: dict[str, Any] = {
|
|
159
|
+
"type": "object",
|
|
160
|
+
"properties": {
|
|
161
|
+
"page_url": {
|
|
162
|
+
"type": "string",
|
|
163
|
+
"description": "URL of the page to scan for captchas.",
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
"required": ["page_url"],
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
_SOLVE_ON_PAGE_SCHEMA: dict[str, Any] = {
|
|
170
|
+
"type": "object",
|
|
171
|
+
"properties": {
|
|
172
|
+
"page_url": {
|
|
173
|
+
"type": "string",
|
|
174
|
+
"description": "URL of the page to solve captchas on.",
|
|
175
|
+
},
|
|
176
|
+
"autofill": {
|
|
177
|
+
"type": ["boolean", "null"],
|
|
178
|
+
"description": "Whether to autofill solved tokens into the page (default: true).",
|
|
179
|
+
},
|
|
180
|
+
"timeout": {
|
|
181
|
+
"type": ["number", "null"],
|
|
182
|
+
"description": "Max seconds to wait per captcha.",
|
|
183
|
+
},
|
|
184
|
+
"polling_interval": {
|
|
185
|
+
"type": ["number", "null"],
|
|
186
|
+
"description": "Seconds between polling attempts.",
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
"required": ["page_url"],
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
_GET_BALANCE_SCHEMA: dict[str, Any] = {
|
|
193
|
+
"type": "object",
|
|
194
|
+
"properties": {},
|
|
195
|
+
"required": [],
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
_GET_SUPPORTED_CAPTCHAS_SCHEMA: dict[str, Any] = {
|
|
199
|
+
"type": "object",
|
|
200
|
+
"properties": {},
|
|
201
|
+
"required": [],
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def get_all_tools() -> list[ToolDef]:
|
|
206
|
+
"""Return framework-agnostic definitions for all CapSolver tools."""
|
|
207
|
+
return [
|
|
208
|
+
ToolDef(
|
|
209
|
+
name="solve_captcha",
|
|
210
|
+
description=(
|
|
211
|
+
"Solve a captcha via the CapSolver API (token mode, no browser required). "
|
|
212
|
+
"Supports reCaptchaV2, reCaptchaV3, and Cloudflare Turnstile. "
|
|
213
|
+
"Returns the solved token that can be submitted with a form or injected into a page."
|
|
214
|
+
),
|
|
215
|
+
parameters=_SOLVE_CAPTCHA_SCHEMA,
|
|
216
|
+
),
|
|
217
|
+
ToolDef(
|
|
218
|
+
name="detect_captchas",
|
|
219
|
+
description=(
|
|
220
|
+
"Scan a web page and detect which captcha types are present. "
|
|
221
|
+
"Returns a list of captcha families found on the page. "
|
|
222
|
+
"Requires browser automation support (playwright)."
|
|
223
|
+
),
|
|
224
|
+
parameters=_DETECT_CAPTCHAS_SCHEMA,
|
|
225
|
+
),
|
|
226
|
+
ToolDef(
|
|
227
|
+
name="solve_on_page",
|
|
228
|
+
description=(
|
|
229
|
+
"One-shot operation: open a page in a browser, detect all captchas, "
|
|
230
|
+
"solve them, and optionally autofill the solved tokens back into the page. "
|
|
231
|
+
"Requires browser automation support (playwright)."
|
|
232
|
+
),
|
|
233
|
+
parameters=_SOLVE_ON_PAGE_SCHEMA,
|
|
234
|
+
),
|
|
235
|
+
ToolDef(
|
|
236
|
+
name="get_balance",
|
|
237
|
+
description="Get the current CapSolver account balance and package information.",
|
|
238
|
+
parameters=_GET_BALANCE_SCHEMA,
|
|
239
|
+
),
|
|
240
|
+
ToolDef(
|
|
241
|
+
name="get_supported_captchas",
|
|
242
|
+
description="List all captcha types and handler names supported by this CapSolver instance.",
|
|
243
|
+
parameters=_GET_SUPPORTED_CAPTCHAS_SCHEMA,
|
|
244
|
+
),
|
|
245
|
+
]
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
# ── Executor ──────────────────────────────────────────────────────
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class ToolExecutor:
|
|
252
|
+
"""Executes tool calls against a Capsolver instance.
|
|
253
|
+
|
|
254
|
+
This is the bridge between LLM-generated tool calls and the SDK.
|
|
255
|
+
"""
|
|
256
|
+
|
|
257
|
+
def __init__(self, capsolver: Capsolver) -> None:
|
|
258
|
+
self._capsolver = capsolver
|
|
259
|
+
|
|
260
|
+
async def execute(self, tool_name: str, arguments: dict[str, Any]) -> dict[str, Any]:
|
|
261
|
+
"""Dispatch a tool call by name and return the result.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
tool_name: One of the registered tool names.
|
|
265
|
+
arguments: Keyword arguments matching the tool's JSON schema.
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
A JSON-serializable dict with the result or error.
|
|
269
|
+
"""
|
|
270
|
+
dispatch = {
|
|
271
|
+
"solve_captcha": self._solve_captcha,
|
|
272
|
+
"detect_captchas": self._detect_captchas,
|
|
273
|
+
"solve_on_page": self._solve_on_page,
|
|
274
|
+
"get_balance": self._get_balance,
|
|
275
|
+
"get_supported_captchas": self._get_supported_captchas,
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
handler = dispatch.get(tool_name)
|
|
279
|
+
if handler is None:
|
|
280
|
+
return {"success": False, "error": f"Unknown tool: {tool_name}. Available: {list(dispatch.keys())}"}
|
|
281
|
+
|
|
282
|
+
try:
|
|
283
|
+
return await handler(arguments)
|
|
284
|
+
except Exception as e:
|
|
285
|
+
return _error_response(e)
|
|
286
|
+
|
|
287
|
+
# ── private handlers ──────────────────────────────────────────
|
|
288
|
+
|
|
289
|
+
async def _solve_captcha(self, args: dict[str, Any]) -> dict[str, Any]:
|
|
290
|
+
try:
|
|
291
|
+
ct = CaptchaType(args["captcha_type"])
|
|
292
|
+
except (ValueError, KeyError) as e:
|
|
293
|
+
return {"success": False, "error": f"Invalid captcha_type: {e}"}
|
|
294
|
+
|
|
295
|
+
info = CaptchaInfo(
|
|
296
|
+
type=ct,
|
|
297
|
+
website_url=args.get("website_url", ""),
|
|
298
|
+
website_key=args.get("website_key", ""),
|
|
299
|
+
version=args.get("version"),
|
|
300
|
+
page_action=args.get("page_action"),
|
|
301
|
+
min_score=args.get("min_score"),
|
|
302
|
+
invisible=args.get("invisible"),
|
|
303
|
+
enterprise=args.get("enterprise"),
|
|
304
|
+
s=args.get("s_token"),
|
|
305
|
+
cdata=args.get("cdata"),
|
|
306
|
+
proxy=args.get("proxy"),
|
|
307
|
+
user_agent=args.get("user_agent"),
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
from capsolver_core.core.client import WaitOptions
|
|
311
|
+
|
|
312
|
+
wait_opts = None
|
|
313
|
+
timeout = args.get("timeout")
|
|
314
|
+
interval = args.get("polling_interval")
|
|
315
|
+
if timeout is not None or interval is not None:
|
|
316
|
+
wait_opts = WaitOptions(timeout=timeout, polling_interval=interval)
|
|
317
|
+
|
|
318
|
+
solution = await self._capsolver.solve(info, wait_options=wait_opts)
|
|
319
|
+
return {
|
|
320
|
+
"success": True,
|
|
321
|
+
"solution": {
|
|
322
|
+
"captcha_type": solution.captcha_type.value,
|
|
323
|
+
"token": solution.token,
|
|
324
|
+
"expire_time": solution.expire_time,
|
|
325
|
+
"user_agent": solution.user_agent,
|
|
326
|
+
},
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async def _detect_captchas(self, args: dict[str, Any]) -> dict[str, Any]:
|
|
330
|
+
page_url = args.get("page_url", "")
|
|
331
|
+
if not page_url:
|
|
332
|
+
return {"success": False, "error": "page_url is required"}
|
|
333
|
+
|
|
334
|
+
try:
|
|
335
|
+
driver = await _launch_browser(page_url)
|
|
336
|
+
except ImportError:
|
|
337
|
+
return {"success": False, "error": "playwright is required. Install: pip install capsolver-agent[browser]"}
|
|
338
|
+
|
|
339
|
+
try:
|
|
340
|
+
detected = await self._capsolver.detect(driver)
|
|
341
|
+
return {
|
|
342
|
+
"success": True,
|
|
343
|
+
"url": page_url,
|
|
344
|
+
"detected_captchas": [t.value for t in detected],
|
|
345
|
+
}
|
|
346
|
+
finally:
|
|
347
|
+
await _close_browser(driver)
|
|
348
|
+
|
|
349
|
+
async def _solve_on_page(self, args: dict[str, Any]) -> dict[str, Any]:
|
|
350
|
+
page_url = args.get("page_url", "")
|
|
351
|
+
if not page_url:
|
|
352
|
+
return {"success": False, "error": "page_url is required"}
|
|
353
|
+
|
|
354
|
+
try:
|
|
355
|
+
driver = await _launch_browser(page_url)
|
|
356
|
+
except ImportError:
|
|
357
|
+
return {"success": False, "error": "playwright is required. Install: pip install capsolver-agent[browser]"}
|
|
358
|
+
|
|
359
|
+
try:
|
|
360
|
+
from capsolver_core.capsolver import SolveOnPageOptions
|
|
361
|
+
|
|
362
|
+
opts = SolveOnPageOptions(
|
|
363
|
+
autofill=args.get("autofill", True),
|
|
364
|
+
throw_on_error=False,
|
|
365
|
+
timeout=args.get("timeout"),
|
|
366
|
+
polling_interval=args.get("polling_interval"),
|
|
367
|
+
)
|
|
368
|
+
results = await self._capsolver.solve_on_page(driver, options=opts)
|
|
369
|
+
return {
|
|
370
|
+
"success": True,
|
|
371
|
+
"url": page_url,
|
|
372
|
+
"results": [
|
|
373
|
+
{
|
|
374
|
+
"captcha_type": r.info.type.value,
|
|
375
|
+
"solved": r.solution is not None,
|
|
376
|
+
"token": r.solution.token if r.solution else None,
|
|
377
|
+
"filled": r.filled,
|
|
378
|
+
"error": r.error,
|
|
379
|
+
}
|
|
380
|
+
for r in results
|
|
381
|
+
],
|
|
382
|
+
}
|
|
383
|
+
finally:
|
|
384
|
+
await _close_browser(driver)
|
|
385
|
+
|
|
386
|
+
async def _get_balance(self, _args: dict[str, Any]) -> dict[str, Any]:
|
|
387
|
+
balance = await self._capsolver.get_balance()
|
|
388
|
+
return {"success": True, "balance": balance.balance, "packages": balance.packages}
|
|
389
|
+
|
|
390
|
+
async def _get_supported_captchas(self, _args: dict[str, Any]) -> dict[str, Any]:
|
|
391
|
+
handlers = self._capsolver.get_supported_captchas()
|
|
392
|
+
return {
|
|
393
|
+
"success": True,
|
|
394
|
+
"registered_handlers": handlers,
|
|
395
|
+
"captcha_types": [t.value for t in CaptchaType],
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def create_executor(
|
|
400
|
+
api_key: str | None = None,
|
|
401
|
+
**capsolver_kwargs: Any,
|
|
402
|
+
) -> ToolExecutor:
|
|
403
|
+
"""Create a ToolExecutor backed by a Capsolver instance.
|
|
404
|
+
|
|
405
|
+
Args:
|
|
406
|
+
api_key: CapSolver API key. Falls back to CAPSOLVER_API_KEY env var.
|
|
407
|
+
**capsolver_kwargs: Extra arguments forwarded to Capsolver(...).
|
|
408
|
+
"""
|
|
409
|
+
key = api_key or os.environ.get("CAPSOLVER_API_KEY", "")
|
|
410
|
+
capsolver = Capsolver(api_key=key, **capsolver_kwargs)
|
|
411
|
+
return ToolExecutor(capsolver)
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
# Convenience: module-level executor for quick usage
|
|
415
|
+
async def execute_tool(
|
|
416
|
+
tool_name: str,
|
|
417
|
+
arguments: dict[str, Any],
|
|
418
|
+
api_key: str | None = None,
|
|
419
|
+
) -> dict[str, Any]:
|
|
420
|
+
"""One-shot convenience: create an executor and run a single tool call.
|
|
421
|
+
|
|
422
|
+
For repeated calls, prefer ``create_executor()`` to reuse the same instance.
|
|
423
|
+
"""
|
|
424
|
+
executor = create_executor(api_key=api_key)
|
|
425
|
+
return await executor.execute(tool_name, arguments)
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
# ── Browser helpers (shared with MCP server) ──────────────────────
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
async def _launch_browser(page_url: str) -> Any:
|
|
432
|
+
"""Launch headless Chromium and navigate to page_url. Returns a PageDriver."""
|
|
433
|
+
try:
|
|
434
|
+
from playwright.async_api import async_playwright
|
|
435
|
+
except ImportError:
|
|
436
|
+
raise
|
|
437
|
+
|
|
438
|
+
from capsolver_core.browser.adapter import from_playwright_page
|
|
439
|
+
|
|
440
|
+
pw = await async_playwright().start()
|
|
441
|
+
browser = await pw.chromium.launch(headless=True)
|
|
442
|
+
page = await browser.new_page()
|
|
443
|
+
await page.goto(page_url, wait_until="domcontentloaded", timeout=30_000)
|
|
444
|
+
|
|
445
|
+
# Captcha widgets (reCAPTCHA api.js, Turnstile) load asynchronously after
|
|
446
|
+
# DOMContentLoaded. Wait for the network to settle so their scripts can
|
|
447
|
+
# register before we detect — bounded so pages with long-lived
|
|
448
|
+
# connections don't hang. Best-effort: ignore timeout.
|
|
449
|
+
try:
|
|
450
|
+
await page.wait_for_load_state("networkidle", timeout=5_000)
|
|
451
|
+
except Exception:
|
|
452
|
+
pass
|
|
453
|
+
|
|
454
|
+
driver = from_playwright_page(page)
|
|
455
|
+
setattr(driver, "_pw", pw)
|
|
456
|
+
setattr(driver, "_browser", browser)
|
|
457
|
+
return driver
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
async def _close_browser(driver: Any) -> None:
|
|
461
|
+
"""Clean up browser resources."""
|
|
462
|
+
try:
|
|
463
|
+
if hasattr(driver, "_browser") and driver._browser:
|
|
464
|
+
await driver._browser.close()
|
|
465
|
+
if hasattr(driver, "_pw") and driver._pw:
|
|
466
|
+
await driver._pw.stop()
|
|
467
|
+
except Exception:
|
|
468
|
+
pass
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: capsolver-agent
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Agent integrations for CapSolver — framework-agnostic tool definitions + LangChain tools.
|
|
5
|
+
Project-URL: Homepage, https://capsolver.com
|
|
6
|
+
Project-URL: Repository, https://github.com/capsolver-ai/agent-capsolver
|
|
7
|
+
Project-URL: Issues, https://github.com/capsolver-ai/agent-capsolver/issues
|
|
8
|
+
Project-URL: Documentation, https://github.com/capsolver-ai/agent-capsolver/blob/main/docs/agent-integration.md
|
|
9
|
+
Project-URL: Changelog, https://github.com/capsolver-ai/agent-capsolver/blob/main/CHANGELOG.md
|
|
10
|
+
Project-URL: Security, https://github.com/capsolver-ai/agent-capsolver/blob/main/SECURITY.md
|
|
11
|
+
Author-email: capsolver-ai <dev@capsolver.ai>
|
|
12
|
+
License-Expression: ISC
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Keywords: agent,ai-agent,capsolver,captcha,langchain,tools
|
|
15
|
+
Classifier: Development Status :: 3 - Alpha
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
24
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
25
|
+
Classifier: Typing :: Typed
|
|
26
|
+
Requires-Python: >=3.10
|
|
27
|
+
Requires-Dist: capsolver-core>=0.1.0
|
|
28
|
+
Provides-Extra: browser
|
|
29
|
+
Requires-Dist: playwright>=1.40; extra == 'browser'
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
32
|
+
Requires-Dist: pytest-asyncio>=1.0; extra == 'dev'
|
|
33
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
34
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
35
|
+
Provides-Extra: langchain
|
|
36
|
+
Requires-Dist: langchain-core>=0.3.0; extra == 'langchain'
|
|
37
|
+
Description-Content-Type: text/markdown
|
|
38
|
+
|
|
39
|
+
# capsolver-agent
|
|
40
|
+
|
|
41
|
+
Agent integrations for [CapSolver](https://capsolver.com) — framework-agnostic tool definitions and LangChain BaseTool implementations.
|
|
42
|
+
|
|
43
|
+
See the [capsolver-ai](https://github.com/capsolver-ai/capsolver-ai) hub repo for integration examples and the full documentation.
|
|
44
|
+
|
|
45
|
+
For framework integration guides (OpenAI, LangChain, LlamaIndex, CrewAI, Google ADK, and more), see [docs/agent-integration.md](docs/agent-integration.md).
|
|
46
|
+
|
|
47
|
+
## Install
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pip install capsolver-agent
|
|
51
|
+
pip install capsolver-agent[langchain] # with LangChain support
|
|
52
|
+
pip install capsolver-agent[browser] # with Playwright support (for detect/solve_on_page)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
All packages read the API key from the environment:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
# bash / zsh
|
|
59
|
+
export CAPSOLVER_API_KEY="your-capsolver-api-key"
|
|
60
|
+
|
|
61
|
+
# PowerShell
|
|
62
|
+
$env:CAPSOLVER_API_KEY = "your-capsolver-api-key"
|
|
63
|
+
|
|
64
|
+
# cmd
|
|
65
|
+
set CAPSOLVER_API_KEY=your-capsolver-api-key
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Framework-agnostic tools (any LLM / agent framework)
|
|
69
|
+
|
|
70
|
+
Use `schema.py` to get tool schemas as JSON and an async executor to run tool calls. Works with OpenAI function calling, OpenAI Agents SDK, Browser Use, or any custom agent loop.
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
import asyncio
|
|
74
|
+
from capsolver_agent.schema import get_all_tools, create_executor
|
|
75
|
+
|
|
76
|
+
async def main():
|
|
77
|
+
# 1. Get tool schemas — feed to your LLM's function-calling API
|
|
78
|
+
tools = get_all_tools()
|
|
79
|
+
openai_functions = [t.to_openai_function() for t in tools]
|
|
80
|
+
|
|
81
|
+
# 2. Execute a tool call returned by the LLM
|
|
82
|
+
executor = create_executor(api_key="YOUR_API_KEY")
|
|
83
|
+
result = await executor.execute("solve_captcha", {
|
|
84
|
+
"captcha_type": "reCaptchaV2",
|
|
85
|
+
"website_url": "https://example.com",
|
|
86
|
+
"website_key": "6Le-wvkSAAAAAPBMRT...",
|
|
87
|
+
})
|
|
88
|
+
print(result)
|
|
89
|
+
# {"success": True, "solution": {"token": "03AF...", ...}}
|
|
90
|
+
|
|
91
|
+
asyncio.run(main())
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Each `ToolDef` provides two export formats:
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
tool = get_all_tools()[0]
|
|
98
|
+
tool.to_openai_function() # → OpenAI function-calling schema
|
|
99
|
+
tool.to_json_schema() # → MCP-style tool descriptor (name + inputSchema)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
For a quick one-shot call without creating an executor:
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
from capsolver_agent.schema import execute_tool
|
|
106
|
+
|
|
107
|
+
result = await execute_tool("solve_captcha", {
|
|
108
|
+
"captcha_type": "reCaptchaV2",
|
|
109
|
+
"website_url": "https://example.com",
|
|
110
|
+
"website_key": "6Le-wvkSAAAAAPBMRT...",
|
|
111
|
+
}, api_key="YOUR_API_KEY")
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## LangChain integration
|
|
115
|
+
|
|
116
|
+
Pre-built `BaseTool` subclasses with Pydantic input schemas — plug directly into any LangChain agent.
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
import asyncio
|
|
120
|
+
from capsolver_agent.langchain_tools import get_langchain_tools
|
|
121
|
+
from langchain_openai import ChatOpenAI
|
|
122
|
+
from langgraph.prebuilt import create_react_agent
|
|
123
|
+
|
|
124
|
+
tools = get_langchain_tools(api_key="YOUR_API_KEY")
|
|
125
|
+
|
|
126
|
+
llm = ChatOpenAI(model="gpt-4o")
|
|
127
|
+
agent = create_react_agent(llm, tools)
|
|
128
|
+
|
|
129
|
+
async def main():
|
|
130
|
+
result = await agent.ainvoke({"messages": [...]})
|
|
131
|
+
|
|
132
|
+
asyncio.run(main())
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Or import individual tools:
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
from capsolver_agent.langchain_tools import SolveCaptchaTool, GetBalanceTool
|
|
139
|
+
|
|
140
|
+
solver = SolveCaptchaTool(api_key="YOUR_API_KEY")
|
|
141
|
+
balance = GetBalanceTool(api_key="YOUR_API_KEY")
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## CLI
|
|
145
|
+
|
|
146
|
+
The `capsolver-agent` command lets you inspect available tools and their schemas from the terminal.
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
# List all tools with descriptions
|
|
150
|
+
capsolver-agent list
|
|
151
|
+
|
|
152
|
+
# Show JSON Schema for a specific tool
|
|
153
|
+
capsolver-agent schema solve_captcha
|
|
154
|
+
|
|
155
|
+
# Export all tools in OpenAI function-calling format
|
|
156
|
+
capsolver-agent schema --format openai
|
|
157
|
+
|
|
158
|
+
# Export one tool in OpenAI format
|
|
159
|
+
capsolver-agent schema --format openai detect_captchas
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Also works via `python -m capsolver_agent list`.
|
|
163
|
+
|
|
164
|
+
## Available tools
|
|
165
|
+
|
|
166
|
+
| Tool | Browser? | Description |
|
|
167
|
+
|---|---|---|
|
|
168
|
+
| `solve_captcha` | No | Token-mode solving — provide type + URL + site key, get a token back |
|
|
169
|
+
| `detect_captchas` | Yes | Scan a page URL and return which captcha types are present |
|
|
170
|
+
| `solve_on_page` | Yes | One-shot: detect + solve + autofill all captchas on a page |
|
|
171
|
+
| `get_balance` | No | Check account balance and packages |
|
|
172
|
+
| `get_supported_captchas` | No | List all supported captcha types and handler names |
|
|
173
|
+
|
|
174
|
+
Browser-based tools require `pip install capsolver-agent[browser]` and `playwright install chromium`.
|
|
175
|
+
|
|
176
|
+
## Integration examples
|
|
177
|
+
|
|
178
|
+
See the [capsolver-ai examples](https://github.com/capsolver-ai/capsolver-ai/tree/main/examples) for runnable demos:
|
|
179
|
+
|
|
180
|
+
- `openai_function_calling.py` — agentic loop with OpenAI function calling
|
|
181
|
+
- `openai_agents.py` — OpenAI Agents SDK with `@function_tool`
|
|
182
|
+
- `langchain_agent.py` — LangChain ReAct agent
|
|
183
|
+
- `browser_use_agent.py` — Browser Use with `@tools.action()`
|
|
184
|
+
|
|
185
|
+
## Development
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
git clone https://github.com/capsolver-ai/agent-capsolver.git
|
|
189
|
+
cd agent-capsolver
|
|
190
|
+
uv sync --all-extras # or: pip install -r requirements-dev.txt
|
|
191
|
+
uv run pytest # run tests
|
|
192
|
+
uv run ruff check src tests # lint
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## License
|
|
196
|
+
|
|
197
|
+
ISC
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
capsolver_agent/__init__.py,sha256=zsm76h8RrmBl_zp6oaFqxbI07vcns_6HzomurF0Tb8k,889
|
|
2
|
+
capsolver_agent/__main__.py,sha256=doKy3xdhM5PVUo7Yjluj14mCjzfjjETWT9eSKv_NrMg,3516
|
|
3
|
+
capsolver_agent/langchain_tools.py,sha256=OxZAhuxKmkxaUQqYPbkBBiRlkCiPVQKun8AZJvfovRA,8732
|
|
4
|
+
capsolver_agent/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
capsolver_agent/schema.py,sha256=5K7AQehx41vZ18Zzf5UQ2dPyTzxhdhmzBlW5ewakkuY,17032
|
|
6
|
+
capsolver_agent-0.1.0.dist-info/METADATA,sha256=HNewsp4sqs0-yteLcT5BfrXJeGjoxClW5no2rUXO09k,6675
|
|
7
|
+
capsolver_agent-0.1.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
8
|
+
capsolver_agent-0.1.0.dist-info/entry_points.txt,sha256=C-Y78xkd0ECZQfs6qXJUXJDHE7x6Vs24Jy3HK3dvi5M,66
|
|
9
|
+
capsolver_agent-0.1.0.dist-info/licenses/LICENSE,sha256=p65mlslnCknoxoEoP3lESj40mUaZRt2xzgMlrGE72hM,763
|
|
10
|
+
capsolver_agent-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
ISC License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025-2026 capsolver-ai
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
7
|
+
copyright notice and this permission notice appear in all copies.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
10
|
+
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
11
|
+
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
12
|
+
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
13
|
+
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
14
|
+
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
15
|
+
PERFORMANCE OF THIS SOFTWARE.
|