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.
@@ -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
+ ]
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,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,2 @@
1
+ [console_scripts]
2
+ capsolver-agent = capsolver_agent.__main__:main
@@ -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.