kapsl 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
kapsl-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kapsl
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.
kapsl-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,55 @@
1
+ Metadata-Version: 2.4
2
+ Name: kapsl
3
+ Version: 0.1.0
4
+ Summary: Python SDK for the Kapsl agent runtime
5
+ Author-email: kapsl <kapsl.xyz@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://kapsl.xyz
8
+ Project-URL: Repository, https://github.com/kapsl-xyz/python-sdk
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Programming Language :: Python :: 3
12
+ Requires-Python: >=3.10
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Requires-Dist: requests>=2.28
16
+ Dynamic: license-file
17
+
18
+ # kapsl
19
+
20
+ Python SDK for the [Kapsl](https://kapsl.xyz) agent runtime.
21
+
22
+ ## Install
23
+
24
+ ```bash
25
+ pip install kapsl
26
+ ```
27
+
28
+ ## Quick start
29
+
30
+ ```python
31
+ from kapsl import Agent, tool, Client
32
+
33
+ @tool
34
+ def search(query: str) -> str:
35
+ """Search the web."""
36
+ return f"results for {query}"
37
+
38
+ agent = Agent(
39
+ name="researcher",
40
+ model="claude-sonnet-4-20250514",
41
+ system="You are a research assistant.",
42
+ tools=[search],
43
+ )
44
+
45
+ client = Client()
46
+ client.deploy(agent)
47
+
48
+ run = client.run("researcher", "What is Kapsl?")
49
+ print(run.wait())
50
+ ```
51
+
52
+ ## Requirements
53
+
54
+ - Python 3.10+
55
+ - A running Kapsl daemon (`kapsl start`)
kapsl-0.1.0/README.md ADDED
@@ -0,0 +1,38 @@
1
+ # kapsl
2
+
3
+ Python SDK for the [Kapsl](https://kapsl.xyz) agent runtime.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install kapsl
9
+ ```
10
+
11
+ ## Quick start
12
+
13
+ ```python
14
+ from kapsl import Agent, tool, Client
15
+
16
+ @tool
17
+ def search(query: str) -> str:
18
+ """Search the web."""
19
+ return f"results for {query}"
20
+
21
+ agent = Agent(
22
+ name="researcher",
23
+ model="claude-sonnet-4-20250514",
24
+ system="You are a research assistant.",
25
+ tools=[search],
26
+ )
27
+
28
+ client = Client()
29
+ client.deploy(agent)
30
+
31
+ run = client.run("researcher", "What is Kapsl?")
32
+ print(run.wait())
33
+ ```
34
+
35
+ ## Requirements
36
+
37
+ - Python 3.10+
38
+ - A running Kapsl daemon (`kapsl start`)
@@ -0,0 +1,50 @@
1
+ """Kapsl Python SDK — define tools, deploy agents, run them."""
2
+
3
+ from ._tool import tool
4
+ from ._client import Client, Run, KapslError
5
+
6
+ __all__ = ["tool", "Agent", "Client", "Run", "KapslError"]
7
+
8
+
9
+ class Agent:
10
+ """An agent definition with tools, ready to deploy.
11
+
12
+ Example::
13
+
14
+ from kapsl import tool, Agent, Client
15
+
16
+ @tool
17
+ def search(query: str) -> str:
18
+ \"\"\"Search the web.\"\"\"
19
+ return f"results for {query}"
20
+
21
+ agent = Agent(name="researcher", system="You are a researcher.", tools=[search])
22
+
23
+ client = Client()
24
+ client.deploy(agent)
25
+ run = client.run("researcher", "Find info about Kapsl")
26
+ print(run.wait())
27
+ """
28
+
29
+ def __init__(
30
+ self,
31
+ name: str,
32
+ *,
33
+ model: str = "claude-sonnet-4-20250514",
34
+ system: str | None = None,
35
+ tools: list | None = None,
36
+ requirements: list[str] | None = None,
37
+ ):
38
+ self.name = name
39
+ self.model = model
40
+ self.system = system
41
+ self.tools = tools or []
42
+ self.requirements = requirements
43
+
44
+ # Validate all tools have the @tool decorator
45
+ for fn in self.tools:
46
+ if not getattr(fn, "_kapsl_tool", False):
47
+ raise ValueError(
48
+ f"Function '{fn.__name__}' is not decorated with @tool. "
49
+ f"Add @tool before the function definition."
50
+ )
@@ -0,0 +1,214 @@
1
+ """Kapsl HTTP client — deploy agents, create runs, stream events."""
2
+
3
+ import json
4
+ import logging
5
+ from typing import Any, Callable
6
+
7
+ import requests
8
+
9
+ from ._tool import _build_tool_defs, generate_tool_code
10
+
11
+ logger = logging.getLogger("kapsl")
12
+
13
+
14
+ class KapslError(Exception):
15
+ """Error from the Kapsl API."""
16
+
17
+ def __init__(self, status_code: int, message: str):
18
+ self.status_code = status_code
19
+ self.message = message
20
+ super().__init__(f"[{status_code}] {message}")
21
+
22
+
23
+ def _default_on_event(event: dict) -> None:
24
+ """Print step progress to stderr.
25
+
26
+ Event shape: {"event": "<kind>", "data": {"id": ..., "run_id": ..., "step": N, "kind": "...", "data": {...}}}
27
+ """
28
+ kind = event.get("event", "")
29
+ payload = event.get("data", {})
30
+ step = payload.get("step", "")
31
+ inner = payload.get("data", {})
32
+
33
+ if kind == "step_started":
34
+ logger.info("step %s", step)
35
+ elif kind == "tool_call":
36
+ logger.info(" tool: %s", inner.get("name", "?"))
37
+ elif kind == "tool_result":
38
+ name = inner.get("name", "?")
39
+ ok = inner.get("ok", False)
40
+ if ok:
41
+ logger.info(" result: %s (ok)", name)
42
+ else:
43
+ logger.warning(" result: %s (error)", name)
44
+ elif kind == "run_completed":
45
+ logger.info("done")
46
+ elif kind == "run_failed":
47
+ logger.error("failed: %s", inner.get("error", "unknown error"))
48
+
49
+
50
+ class Run:
51
+ """Handle to a running or completed run."""
52
+
53
+ def __init__(self, data: dict, base_url: str):
54
+ self.id: str = data["id"]
55
+ self.status: str = data["status"]
56
+ self.output: Any = data.get("output")
57
+ self._data = data
58
+ self._base_url = base_url
59
+
60
+ def events(self):
61
+ """Stream SSE events from the run. Yields dicts with 'event' and 'data' keys."""
62
+ url = f"{self._base_url}/v1/runs/{self.id}/events"
63
+ # read timeout only — no connect timeout so initial connection can take time,
64
+ # but if no data arrives for 60s the stream is considered dead.
65
+ resp = requests.get(url, stream=True, headers={"Accept": "text/event-stream"}, timeout=(10, 60))
66
+ resp.raise_for_status()
67
+
68
+ event_type = None
69
+ data_buf = []
70
+
71
+ for raw_line in resp.iter_lines(decode_unicode=True):
72
+ if raw_line is None:
73
+ continue
74
+
75
+ line = raw_line
76
+
77
+ if line.startswith("event:"):
78
+ event_type = line[len("event:"):].strip()
79
+ continue
80
+
81
+ if line.startswith("data:"):
82
+ data_buf.append(line[len("data:"):].strip())
83
+ continue
84
+
85
+ if line.startswith(":"):
86
+ # SSE comment / keepalive
87
+ continue
88
+
89
+ if line == "":
90
+ # Empty line = event boundary
91
+ if event_type and data_buf:
92
+ raw_data = "\n".join(data_buf)
93
+ try:
94
+ parsed = json.loads(raw_data)
95
+ except json.JSONDecodeError:
96
+ parsed = raw_data
97
+
98
+ event = {"event": event_type, "data": parsed}
99
+ yield event
100
+
101
+ if event_type in ("run_completed", "run_failed", "run_suspended"):
102
+ return
103
+
104
+ event_type = None
105
+ data_buf = []
106
+
107
+ def refresh(self) -> "Run":
108
+ """Re-fetch run state from the server."""
109
+ resp = requests.get(f"{self._base_url}/v1/runs/{self.id}")
110
+ if resp.status_code != 200:
111
+ raise KapslError(resp.status_code, resp.text)
112
+ data = resp.json()
113
+ self.status = data["status"]
114
+ self.output = data.get("output")
115
+ self._data = data
116
+ return self
117
+
118
+ def wait(self, on_event: Callable[[dict], None] | None = _default_on_event) -> Any:
119
+ """Block until the run reaches a terminal state. Returns the output.
120
+
121
+ Args:
122
+ on_event: Callback for each event. Pass None to suppress output.
123
+ Defaults to printing step progress to stderr.
124
+ """
125
+ for event in self.events():
126
+ if on_event:
127
+ on_event(event)
128
+ self.refresh()
129
+ return self.output
130
+
131
+
132
+ class Client:
133
+ """Kapsl API client."""
134
+
135
+ def __init__(self, base_url: str = "http://127.0.0.1:50051"):
136
+ self.base_url = base_url.rstrip("/")
137
+
138
+ def _request(self, method: str, path: str, **kwargs) -> requests.Response:
139
+ url = f"{self.base_url}{path}"
140
+ resp = requests.request(method, url, **kwargs)
141
+ if resp.status_code >= 400:
142
+ try:
143
+ body = resp.json()
144
+ msg = body.get("error", resp.text)
145
+ except Exception:
146
+ msg = resp.text
147
+ raise KapslError(resp.status_code, msg)
148
+ return resp
149
+
150
+ def deploy(self, agent) -> dict:
151
+ """Deploy an agent to the runtime.
152
+
153
+ Args:
154
+ agent: An Agent instance with name, model, system, and tools.
155
+
156
+ Returns:
157
+ The created/updated agent dict from the API.
158
+ """
159
+ payload: dict[str, Any] = {
160
+ "name": agent.name,
161
+ "model": agent.model,
162
+ }
163
+ if agent.system:
164
+ payload["system"] = agent.system
165
+ if agent.tools:
166
+ payload["tools"] = _build_tool_defs(agent.tools)
167
+ payload["tool_code"] = generate_tool_code(agent.tools)
168
+ if agent.requirements:
169
+ payload["requirements"] = agent.requirements
170
+
171
+ resp = self._request("POST", "/v1/agents", json=payload)
172
+ return resp.json()
173
+
174
+ def run(self, agent_name: str, input: Any, **overrides) -> Run:
175
+ """Create a new run for a deployed agent.
176
+
177
+ Args:
178
+ agent_name: Name of the deployed agent.
179
+ input: The input to send (string or JSON-serializable value).
180
+ **overrides: Optional per-run overrides (model, system, tools, tool_code).
181
+
182
+ Returns:
183
+ A Run handle for streaming events and getting output.
184
+ """
185
+ payload: dict[str, Any] = {"agent": agent_name, "input": input}
186
+ for key in ("model", "system", "tools", "tool_code"):
187
+ if key in overrides:
188
+ payload[key] = overrides[key]
189
+
190
+ resp = self._request("POST", "/v1/runs", json=payload)
191
+ return Run(resp.json(), self.base_url)
192
+
193
+ def get_run(self, run_id: str) -> dict:
194
+ """Fetch a run by ID."""
195
+ return self._request("GET", f"/v1/runs/{run_id}").json()
196
+
197
+ def list_runs(self) -> list[dict]:
198
+ """List all runs."""
199
+ return self._request("GET", "/v1/runs").json()
200
+
201
+ def list_agents(self) -> list[dict]:
202
+ """List all registered agents."""
203
+ return self._request("GET", "/v1/agents").json()
204
+
205
+ def dry_run(self, tool_code: str, name: str, input: dict) -> dict:
206
+ """Execute a single tool call without creating a run.
207
+
208
+ Args:
209
+ tool_code: The complete Python tool script.
210
+ name: Tool function name to call.
211
+ input: Input dict for the tool.
212
+ """
213
+ payload = {"tool_code": tool_code, "name": name, "input": input}
214
+ return self._request("POST", "/v1/tools/dry-run", json=payload).json()
@@ -0,0 +1,277 @@
1
+ """Tool decorator, JSON Schema generation, and tool code generation."""
2
+
3
+ import ast
4
+ import inspect
5
+ import re
6
+ import textwrap
7
+ import typing
8
+ from pathlib import Path
9
+
10
+
11
+ def tool(fn):
12
+ """Mark a function as a Kapsl tool.
13
+
14
+ Attaches metadata used by Agent and Client to build tool definitions
15
+ and generate the bundled tool_code script.
16
+ """
17
+ hints = typing.get_type_hints(fn)
18
+ sig = inspect.signature(fn)
19
+ doc = inspect.getdoc(fn) or ""
20
+
21
+ fn._kapsl_tool = True
22
+ fn._kapsl_name = fn.__name__
23
+ fn._kapsl_description = doc.split("\n\n")[0].strip() # first paragraph
24
+ fn._kapsl_input_schema = _schema_from_hints(sig, hints, doc)
25
+ return fn
26
+
27
+
28
+ # --- Schema generation ---
29
+
30
+ _TYPE_MAP = {
31
+ str: {"type": "string"},
32
+ int: {"type": "integer"},
33
+ float: {"type": "number"},
34
+ bool: {"type": "boolean"},
35
+ }
36
+
37
+
38
+ def _resolve_type(tp):
39
+ """Convert a Python type annotation to a JSON Schema fragment."""
40
+ if tp in _TYPE_MAP:
41
+ return dict(_TYPE_MAP[tp])
42
+
43
+ # Bare list/dict without type args
44
+ if tp is list:
45
+ return {"type": "array"}
46
+ if tp is dict:
47
+ return {"type": "object"}
48
+
49
+ origin = typing.get_origin(tp)
50
+ args = typing.get_args(tp)
51
+
52
+ # list[X]
53
+ if origin is list:
54
+ item_type = args[0] if args else str
55
+ return {"type": "array", "items": _resolve_type(item_type)}
56
+
57
+ # dict[str, X]
58
+ if origin is dict:
59
+ val_type = args[1] if len(args) > 1 else str
60
+ return {"type": "object", "additionalProperties": _resolve_type(val_type)}
61
+
62
+ import warnings
63
+ warnings.warn(f"Unsupported type annotation {tp!r}, defaulting to string schema", stacklevel=3)
64
+ return {"type": "string"}
65
+
66
+
67
+ def _is_union(tp):
68
+ """Check if a type is a Union (typing.Union or PEP 604 X | Y)."""
69
+ import types as _types
70
+ return typing.get_origin(tp) is typing.Union or isinstance(tp, _types.UnionType)
71
+
72
+
73
+ def _is_optional(tp):
74
+ """Check if a type is Optional[X] or X | None."""
75
+ if not _is_union(tp):
76
+ return False
77
+ return type(None) in typing.get_args(tp)
78
+
79
+
80
+ def _unwrap_optional(tp):
81
+ """Extract T from Optional[T]."""
82
+ args = typing.get_args(tp)
83
+ non_none = [a for a in args if a is not type(None)]
84
+ return non_none[0] if non_none else str
85
+
86
+
87
+ def _parse_arg_descriptions(doc: str) -> dict[str, str]:
88
+ """Extract parameter descriptions from Google-style docstring Args section."""
89
+ descriptions = {}
90
+ in_args = False
91
+ current_param = None
92
+ current_desc = []
93
+
94
+ for line in doc.split("\n"):
95
+ stripped = line.strip()
96
+ if stripped.lower().startswith("args:"):
97
+ in_args = True
98
+ continue
99
+ if in_args:
100
+ # New section header (e.g., Returns:, Raises:)
101
+ if stripped and not stripped.startswith(" ") and stripped.endswith(":") and not stripped.startswith("-"):
102
+ # Check if it's indented relative to Args — if not, it's a new section
103
+ if not line.startswith(" " * 4) and not line.startswith("\t\t"):
104
+ break
105
+ # Parameter line: " param_name: description" or " param_name (type): description"
106
+ m = re.match(r"\s{2,}(\w+)(?:\s*\([^)]*\))?\s*:\s*(.*)", line)
107
+ if m:
108
+ if current_param:
109
+ descriptions[current_param] = " ".join(current_desc).strip()
110
+ current_param = m.group(1)
111
+ current_desc = [m.group(2)] if m.group(2) else []
112
+ elif current_param and stripped:
113
+ # Continuation line
114
+ current_desc.append(stripped)
115
+ elif not stripped:
116
+ if current_param:
117
+ descriptions[current_param] = " ".join(current_desc).strip()
118
+ current_param = None
119
+ current_desc = []
120
+
121
+ if current_param:
122
+ descriptions[current_param] = " ".join(current_desc).strip()
123
+
124
+ return descriptions
125
+
126
+
127
+ def _schema_from_hints(sig: inspect.Signature, hints: dict, doc: str) -> dict:
128
+ """Build a JSON Schema object from function signature and type hints."""
129
+ properties = {}
130
+ required = []
131
+ arg_descs = _parse_arg_descriptions(doc)
132
+
133
+ for name, param in sig.parameters.items():
134
+ tp = hints.get(name)
135
+ if tp is None:
136
+ tp = str # no annotation → string
137
+
138
+ # Skip return type
139
+ if name == "return":
140
+ continue
141
+
142
+ optional = _is_optional(tp)
143
+ if optional:
144
+ tp = _unwrap_optional(tp)
145
+
146
+ prop = _resolve_type(tp)
147
+ if name in arg_descs:
148
+ prop["description"] = arg_descs[name]
149
+
150
+ properties[name] = prop
151
+
152
+ # Required if: no default AND not Optional
153
+ if param.default is inspect.Parameter.empty and not optional:
154
+ required.append(name)
155
+
156
+ schema = {"type": "object", "properties": properties}
157
+ if required:
158
+ schema["required"] = required
159
+ return schema
160
+
161
+
162
+ # --- Tool definitions (for API) ---
163
+
164
+ def _build_tool_defs(tools: list) -> list[dict]:
165
+ """Build the tool definitions list for the Anthropic API."""
166
+ defs = []
167
+ for fn in tools:
168
+ defs.append({
169
+ "name": fn._kapsl_name,
170
+ "description": fn._kapsl_description,
171
+ "input_schema": fn._kapsl_input_schema,
172
+ })
173
+ return defs
174
+
175
+
176
+ # --- Code generation ---
177
+
178
+ def _extract_imports(fn) -> list[str]:
179
+ """Extract import statements from the module where fn is defined."""
180
+ try:
181
+ source_file = inspect.getfile(fn)
182
+ except (TypeError, OSError):
183
+ return []
184
+
185
+ try:
186
+ source = Path(source_file).read_text()
187
+ except (OSError, UnicodeDecodeError):
188
+ return []
189
+
190
+ try:
191
+ tree = ast.parse(source)
192
+ except SyntaxError:
193
+ return []
194
+
195
+ imports = []
196
+ for node in ast.iter_child_nodes(tree):
197
+ if isinstance(node, ast.Import):
198
+ line = ast.get_source_segment(source, node)
199
+ if line:
200
+ imports.append(line)
201
+ elif isinstance(node, ast.ImportFrom):
202
+ # Skip kapsl imports
203
+ if node.module and node.module.startswith("kapsl"):
204
+ continue
205
+ line = ast.get_source_segment(source, node)
206
+ if line:
207
+ imports.append(line)
208
+
209
+ return imports
210
+
211
+
212
+ def _extract_function_source(fn) -> str:
213
+ """Get the source of fn, stripping the @tool decorator line."""
214
+ source = inspect.getsource(fn)
215
+ source = textwrap.dedent(source)
216
+ lines = source.split("\n")
217
+ # Strip @tool decorator line(s)
218
+ filtered = []
219
+ skip_next = False
220
+ for line in lines:
221
+ stripped = line.strip()
222
+ if stripped.startswith("@tool"):
223
+ continue
224
+ filtered.append(line)
225
+ return "\n".join(filtered)
226
+
227
+
228
+ _DISPATCH_LOOP = '''
229
+ _TOOLS = {%s}
230
+
231
+ import sys as _sys, json as _json
232
+ for _line in _sys.stdin:
233
+ _line = _line.strip()
234
+ if not _line:
235
+ continue
236
+ _req = _json.loads(_line)
237
+ _fn = _TOOLS.get(_req["name"])
238
+ if _fn is None:
239
+ print(_json.dumps({"error": f"tool \\'{_req['name']}\\' not found"}))
240
+ else:
241
+ try:
242
+ _result = _fn(**_req["input"])
243
+ _out = _result if isinstance(_result, (dict, list, str, int, float, bool, type(None))) else str(_result)
244
+ print(_json.dumps({"output": _out}))
245
+ except Exception as _e:
246
+ print(_json.dumps({"error": str(_e)}))
247
+ _sys.stdout.flush()
248
+ '''
249
+
250
+
251
+ def generate_tool_code(tools: list) -> str:
252
+ """Generate a complete, self-contained Python script for tool execution."""
253
+ # Collect imports from all tool modules (deduplicated)
254
+ seen_imports = set()
255
+ import_lines = []
256
+ for fn in tools:
257
+ for imp in _extract_imports(fn):
258
+ if imp not in seen_imports:
259
+ seen_imports.add(imp)
260
+ import_lines.append(imp)
261
+
262
+ # Collect function sources
263
+ func_sources = []
264
+ for fn in tools:
265
+ func_sources.append(_extract_function_source(fn))
266
+
267
+ # Build tool map string
268
+ tool_map = ", ".join(f'"{fn._kapsl_name}": {fn._kapsl_name}' for fn in tools)
269
+
270
+ parts = []
271
+ if import_lines:
272
+ parts.append("\n".join(import_lines))
273
+ parts.append("")
274
+ parts.append("\n\n".join(func_sources))
275
+ parts.append(_DISPATCH_LOOP % tool_map)
276
+
277
+ return "\n".join(parts)
@@ -0,0 +1,55 @@
1
+ Metadata-Version: 2.4
2
+ Name: kapsl
3
+ Version: 0.1.0
4
+ Summary: Python SDK for the Kapsl agent runtime
5
+ Author-email: kapsl <kapsl.xyz@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://kapsl.xyz
8
+ Project-URL: Repository, https://github.com/kapsl-xyz/python-sdk
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Programming Language :: Python :: 3
12
+ Requires-Python: >=3.10
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Requires-Dist: requests>=2.28
16
+ Dynamic: license-file
17
+
18
+ # kapsl
19
+
20
+ Python SDK for the [Kapsl](https://kapsl.xyz) agent runtime.
21
+
22
+ ## Install
23
+
24
+ ```bash
25
+ pip install kapsl
26
+ ```
27
+
28
+ ## Quick start
29
+
30
+ ```python
31
+ from kapsl import Agent, tool, Client
32
+
33
+ @tool
34
+ def search(query: str) -> str:
35
+ """Search the web."""
36
+ return f"results for {query}"
37
+
38
+ agent = Agent(
39
+ name="researcher",
40
+ model="claude-sonnet-4-20250514",
41
+ system="You are a research assistant.",
42
+ tools=[search],
43
+ )
44
+
45
+ client = Client()
46
+ client.deploy(agent)
47
+
48
+ run = client.run("researcher", "What is Kapsl?")
49
+ print(run.wait())
50
+ ```
51
+
52
+ ## Requirements
53
+
54
+ - Python 3.10+
55
+ - A running Kapsl daemon (`kapsl start`)
@@ -0,0 +1,11 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ kapsl/__init__.py
5
+ kapsl/_client.py
6
+ kapsl/_tool.py
7
+ kapsl.egg-info/PKG-INFO
8
+ kapsl.egg-info/SOURCES.txt
9
+ kapsl.egg-info/dependency_links.txt
10
+ kapsl.egg-info/requires.txt
11
+ kapsl.egg-info/top_level.txt
@@ -0,0 +1 @@
1
+ requests>=2.28
@@ -0,0 +1 @@
1
+ kapsl
@@ -0,0 +1,22 @@
1
+ [project]
2
+ name = "kapsl"
3
+ version = "0.1.0"
4
+ description = "Python SDK for the Kapsl agent runtime"
5
+ requires-python = ">=3.10"
6
+ dependencies = ["requests>=2.28"]
7
+ license = "MIT"
8
+ authors = [{ name = "kapsl", email = "kapsl.xyz@gmail.com" }]
9
+ readme = "README.md"
10
+ classifiers = [
11
+ "Development Status :: 3 - Alpha",
12
+ "Intended Audience :: Developers",
13
+ "Programming Language :: Python :: 3",
14
+ ]
15
+
16
+ [project.urls]
17
+ Homepage = "https://kapsl.xyz"
18
+ Repository = "https://github.com/kapsl-xyz/python-sdk"
19
+
20
+ [build-system]
21
+ requires = ["setuptools>=68"]
22
+ build-backend = "setuptools.build_meta"
kapsl-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+