swytchcode-runtime 0.1.0__tar.gz → 1.0.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.
- swytchcode_runtime-1.0.0/LICENSE.md +21 -0
- {swytchcode_runtime-0.1.0 → swytchcode_runtime-1.0.0}/PKG-INFO +71 -3
- {swytchcode_runtime-0.1.0 → swytchcode_runtime-1.0.0}/README.md +67 -1
- {swytchcode_runtime-0.1.0 → swytchcode_runtime-1.0.0}/pyproject.toml +2 -2
- swytchcode_runtime-1.0.0/swytchcode_runtime/__init__.py +24 -0
- swytchcode_runtime-1.0.0/swytchcode_runtime/cli.py +58 -0
- swytchcode_runtime-1.0.0/swytchcode_runtime/client.py +133 -0
- swytchcode_runtime-1.0.0/swytchcode_runtime/discover.py +29 -0
- {swytchcode_runtime-0.1.0 → swytchcode_runtime-1.0.0}/swytchcode_runtime/exec.py +50 -2
- swytchcode_runtime-1.0.0/swytchcode_runtime/manage.py +30 -0
- swytchcode_runtime-1.0.0/swytchcode_runtime/providers/__init__.py +15 -0
- swytchcode_runtime-1.0.0/swytchcode_runtime/providers/anthropic.py +13 -0
- swytchcode_runtime-1.0.0/swytchcode_runtime/providers/base.py +31 -0
- swytchcode_runtime-1.0.0/swytchcode_runtime/providers/crewai.py +22 -0
- swytchcode_runtime-1.0.0/swytchcode_runtime/providers/langgraph.py +17 -0
- swytchcode_runtime-1.0.0/swytchcode_runtime/providers/openai_agents.py +30 -0
- swytchcode_runtime-1.0.0/swytchcode_runtime/providers/vercel.py +30 -0
- swytchcode_runtime-1.0.0/swytchcode_runtime/schema.py +111 -0
- {swytchcode_runtime-0.1.0 → swytchcode_runtime-1.0.0}/swytchcode_runtime.egg-info/PKG-INFO +71 -3
- swytchcode_runtime-1.0.0/swytchcode_runtime.egg-info/SOURCES.txt +22 -0
- swytchcode_runtime-0.1.0/swytchcode_runtime/__init__.py +0 -6
- swytchcode_runtime-0.1.0/swytchcode_runtime.egg-info/SOURCES.txt +0 -9
- {swytchcode_runtime-0.1.0 → swytchcode_runtime-1.0.0}/setup.cfg +0 -0
- {swytchcode_runtime-0.1.0 → swytchcode_runtime-1.0.0}/swytchcode_runtime/errors.py +0 -0
- {swytchcode_runtime-0.1.0 → swytchcode_runtime-1.0.0}/swytchcode_runtime.egg-info/dependency_links.txt +0 -0
- {swytchcode_runtime-0.1.0 → swytchcode_runtime-1.0.0}/swytchcode_runtime.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Swytchcode Technologies Inc.
|
|
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.
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: swytchcode-runtime
|
|
3
|
-
Version:
|
|
3
|
+
Version: 1.0.0
|
|
4
4
|
Summary: Thin runtime wrapper around the Swytchcode CLI
|
|
5
5
|
Author: Swytchcode
|
|
6
6
|
License: MIT
|
|
7
|
-
Project-URL: repository, https://
|
|
7
|
+
Project-URL: repository, https://github.com/swytchcodehq/runtime-py
|
|
8
8
|
Keywords: swytchcode,cli,runtime,exec
|
|
9
9
|
Classifier: License :: OSI Approved :: MIT License
|
|
10
10
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -13,12 +13,18 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.12
|
|
14
14
|
Requires-Python: >=3.10
|
|
15
15
|
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE.md
|
|
17
|
+
Dynamic: license-file
|
|
16
18
|
|
|
17
19
|
# swytchcode-runtime (Python)
|
|
18
20
|
|
|
19
21
|
Thin runtime wrapper around the Swytchcode CLI. Calls `swytchcode exec` for you so you can stay in Python without shell boilerplate.
|
|
20
22
|
|
|
21
|
-
**Requires:** The `swytchcode`
|
|
23
|
+
**Requires:** The `swytchcode` CLI must be installed. The binary is located automatically — no configuration needed in most environments. Resolution order:
|
|
24
|
+
|
|
25
|
+
1. `SWYTCHCODE_BIN` env var — explicit override.
|
|
26
|
+
2. `$PATH` lookup via `shutil.which` — the standard system resolution.
|
|
27
|
+
3. Common install paths — `~/.local/bin`, `/usr/local/bin` (Unix) or `%LOCALAPPDATA%\Programs\swytchcode\bin` (Windows).
|
|
22
28
|
|
|
23
29
|
## Install
|
|
24
30
|
|
|
@@ -83,6 +89,13 @@ output = exec("api.report.export", {"id": "123"}, raw=True)
|
|
|
83
89
|
|
|
84
90
|
This runtime invokes `swytchcode exec [canonical_id]` with the flags above. For full exec behavior, see the Swytchcode kernel documentation.
|
|
85
91
|
|
|
92
|
+
### Environment variables
|
|
93
|
+
|
|
94
|
+
| Variable | Description |
|
|
95
|
+
|----------|-------------|
|
|
96
|
+
| `SWYTCHCODE_BIN` | Override the resolved binary path. Set this only when automatic resolution does not find the correct binary (e.g. non-standard install locations or virtualised environments). |
|
|
97
|
+
| `SWYTCHCODE_TOKEN` | Auth token passed to the CLI via the process environment. |
|
|
98
|
+
|
|
86
99
|
### Error handling
|
|
87
100
|
|
|
88
101
|
Failures raise `SwytchcodeError` (spawn error, non-zero exit, invalid JSON). Use the helper to narrow:
|
|
@@ -131,3 +144,58 @@ def create_account(input: dict) -> any:
|
|
|
131
144
|
```
|
|
132
145
|
|
|
133
146
|
**Avoid:** subprocess boilerplate, HTTP calls, or config parsing. Use `exec(...)` and let the CLI handle execution and policy.
|
|
147
|
+
|
|
148
|
+
## Agentic workflows (framework integrations)
|
|
149
|
+
|
|
150
|
+
On top of `exec`, the runtime exposes a small agentic surface that turns Swytchcode tools
|
|
151
|
+
into the native tool objects each agent framework expects. The Swytchcode part is the same
|
|
152
|
+
two lines regardless of framework:
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
from swytchcode_runtime import Swytchcode
|
|
156
|
+
from swytchcode_runtime.providers.anthropic import AnthropicProvider
|
|
157
|
+
|
|
158
|
+
swx = Swytchcode(provider=AnthropicProvider())
|
|
159
|
+
tools = swx.tools.get(toolkits=["stripe"]) # framework-native tools, full param schemas
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Selecting tools — `swx.tools.get(...)`
|
|
163
|
+
|
|
164
|
+
Pass exactly one selector; IDs resolve against your local Swytchcode state and remote search:
|
|
165
|
+
|
|
166
|
+
- `toolkits=["stripe"]` — every enabled tool whose integration matches a toolkit.
|
|
167
|
+
- `tools=["charges.charge.create"]` — explicit canonical IDs.
|
|
168
|
+
- `search="refund a charge"` — natural-language discovery (via `swytchcode discover`).
|
|
169
|
+
|
|
170
|
+
Each returned tool carries a **full input schema** — every field is surfaced to the model, with
|
|
171
|
+
only the truly-required ones marked `required` — and an `execute` callback that runs `swytchcode
|
|
172
|
+
exec` for you (empty optional values are stripped before the call so APIs like Stripe don't reject them).
|
|
173
|
+
|
|
174
|
+
### Supported providers
|
|
175
|
+
|
|
176
|
+
| Framework | Import | Who runs the tool loop |
|
|
177
|
+
|-----------|--------|------------------------|
|
|
178
|
+
| Anthropic Claude | `providers.anthropic.AnthropicProvider` | you (Messages API + `swx.handle_tool_calls`) |
|
|
179
|
+
| OpenAI Agents SDK | `providers.openai_agents.OpenAIAgentsProvider` | the SDK |
|
|
180
|
+
| Vercel AI SDK | `providers.vercel.VercelProvider` | the SDK |
|
|
181
|
+
| LangGraph | `providers.langgraph.LangGraphProvider` | the prebuilt agent |
|
|
182
|
+
| CrewAI | `providers.crewai.CrewAIProvider` | the crew |
|
|
183
|
+
|
|
184
|
+
### Non-agentic APIs (Anthropic Messages)
|
|
185
|
+
|
|
186
|
+
When you run the tool loop yourself, `handle_tool_calls` executes each `tool_use` block and
|
|
187
|
+
returns the `tool_result` blocks to send back:
|
|
188
|
+
|
|
189
|
+
```python
|
|
190
|
+
import anthropic
|
|
191
|
+
client = anthropic.Anthropic()
|
|
192
|
+
msg = client.messages.create(
|
|
193
|
+
model="claude-sonnet-5", max_tokens=1024, tools=tools,
|
|
194
|
+
messages=[{"role": "user", "content": "Refund charge ch_123 for $20"}],
|
|
195
|
+
)
|
|
196
|
+
results = swx.handle_tool_calls(msg) # runs the tool calls, returns tool_result blocks
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
One runnable file per framework lives in `sdk-examples/`. Install the matching framework SDK
|
|
200
|
+
(`pip install openai-agents` / `anthropic` / `ai` / `langgraph` / `crewai`) alongside the
|
|
201
|
+
`swytchcode` CLI.
|
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
Thin runtime wrapper around the Swytchcode CLI. Calls `swytchcode exec` for you so you can stay in Python without shell boilerplate.
|
|
4
4
|
|
|
5
|
-
**Requires:** The `swytchcode`
|
|
5
|
+
**Requires:** The `swytchcode` CLI must be installed. The binary is located automatically — no configuration needed in most environments. Resolution order:
|
|
6
|
+
|
|
7
|
+
1. `SWYTCHCODE_BIN` env var — explicit override.
|
|
8
|
+
2. `$PATH` lookup via `shutil.which` — the standard system resolution.
|
|
9
|
+
3. Common install paths — `~/.local/bin`, `/usr/local/bin` (Unix) or `%LOCALAPPDATA%\Programs\swytchcode\bin` (Windows).
|
|
6
10
|
|
|
7
11
|
## Install
|
|
8
12
|
|
|
@@ -67,6 +71,13 @@ output = exec("api.report.export", {"id": "123"}, raw=True)
|
|
|
67
71
|
|
|
68
72
|
This runtime invokes `swytchcode exec [canonical_id]` with the flags above. For full exec behavior, see the Swytchcode kernel documentation.
|
|
69
73
|
|
|
74
|
+
### Environment variables
|
|
75
|
+
|
|
76
|
+
| Variable | Description |
|
|
77
|
+
|----------|-------------|
|
|
78
|
+
| `SWYTCHCODE_BIN` | Override the resolved binary path. Set this only when automatic resolution does not find the correct binary (e.g. non-standard install locations or virtualised environments). |
|
|
79
|
+
| `SWYTCHCODE_TOKEN` | Auth token passed to the CLI via the process environment. |
|
|
80
|
+
|
|
70
81
|
### Error handling
|
|
71
82
|
|
|
72
83
|
Failures raise `SwytchcodeError` (spawn error, non-zero exit, invalid JSON). Use the helper to narrow:
|
|
@@ -115,3 +126,58 @@ def create_account(input: dict) -> any:
|
|
|
115
126
|
```
|
|
116
127
|
|
|
117
128
|
**Avoid:** subprocess boilerplate, HTTP calls, or config parsing. Use `exec(...)` and let the CLI handle execution and policy.
|
|
129
|
+
|
|
130
|
+
## Agentic workflows (framework integrations)
|
|
131
|
+
|
|
132
|
+
On top of `exec`, the runtime exposes a small agentic surface that turns Swytchcode tools
|
|
133
|
+
into the native tool objects each agent framework expects. The Swytchcode part is the same
|
|
134
|
+
two lines regardless of framework:
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
from swytchcode_runtime import Swytchcode
|
|
138
|
+
from swytchcode_runtime.providers.anthropic import AnthropicProvider
|
|
139
|
+
|
|
140
|
+
swx = Swytchcode(provider=AnthropicProvider())
|
|
141
|
+
tools = swx.tools.get(toolkits=["stripe"]) # framework-native tools, full param schemas
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Selecting tools — `swx.tools.get(...)`
|
|
145
|
+
|
|
146
|
+
Pass exactly one selector; IDs resolve against your local Swytchcode state and remote search:
|
|
147
|
+
|
|
148
|
+
- `toolkits=["stripe"]` — every enabled tool whose integration matches a toolkit.
|
|
149
|
+
- `tools=["charges.charge.create"]` — explicit canonical IDs.
|
|
150
|
+
- `search="refund a charge"` — natural-language discovery (via `swytchcode discover`).
|
|
151
|
+
|
|
152
|
+
Each returned tool carries a **full input schema** — every field is surfaced to the model, with
|
|
153
|
+
only the truly-required ones marked `required` — and an `execute` callback that runs `swytchcode
|
|
154
|
+
exec` for you (empty optional values are stripped before the call so APIs like Stripe don't reject them).
|
|
155
|
+
|
|
156
|
+
### Supported providers
|
|
157
|
+
|
|
158
|
+
| Framework | Import | Who runs the tool loop |
|
|
159
|
+
|-----------|--------|------------------------|
|
|
160
|
+
| Anthropic Claude | `providers.anthropic.AnthropicProvider` | you (Messages API + `swx.handle_tool_calls`) |
|
|
161
|
+
| OpenAI Agents SDK | `providers.openai_agents.OpenAIAgentsProvider` | the SDK |
|
|
162
|
+
| Vercel AI SDK | `providers.vercel.VercelProvider` | the SDK |
|
|
163
|
+
| LangGraph | `providers.langgraph.LangGraphProvider` | the prebuilt agent |
|
|
164
|
+
| CrewAI | `providers.crewai.CrewAIProvider` | the crew |
|
|
165
|
+
|
|
166
|
+
### Non-agentic APIs (Anthropic Messages)
|
|
167
|
+
|
|
168
|
+
When you run the tool loop yourself, `handle_tool_calls` executes each `tool_use` block and
|
|
169
|
+
returns the `tool_result` blocks to send back:
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
import anthropic
|
|
173
|
+
client = anthropic.Anthropic()
|
|
174
|
+
msg = client.messages.create(
|
|
175
|
+
model="claude-sonnet-5", max_tokens=1024, tools=tools,
|
|
176
|
+
messages=[{"role": "user", "content": "Refund charge ch_123 for $20"}],
|
|
177
|
+
)
|
|
178
|
+
results = swx.handle_tool_calls(msg) # runs the tool calls, returns tool_result blocks
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
One runnable file per framework lives in `sdk-examples/`. Install the matching framework SDK
|
|
182
|
+
(`pip install openai-agents` / `anthropic` / `ai` / `langgraph` / `crewai`) alongside the
|
|
183
|
+
`swytchcode` CLI.
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "swytchcode-runtime"
|
|
7
|
-
version = "
|
|
7
|
+
version = "1.0.0"
|
|
8
8
|
description = "Thin runtime wrapper around the Swytchcode CLI"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -20,7 +20,7 @@ classifiers = [
|
|
|
20
20
|
]
|
|
21
21
|
|
|
22
22
|
[project.urls]
|
|
23
|
-
repository = "https://
|
|
23
|
+
repository = "https://github.com/swytchcodehq/runtime-py"
|
|
24
24
|
|
|
25
25
|
[tool.setuptools.packages.find]
|
|
26
26
|
where = ["."]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Thin runtime wrapper around the Swytchcode CLI."""
|
|
2
|
+
|
|
3
|
+
from .errors import SwytchcodeError, is_swytchcode_error
|
|
4
|
+
from .exec import exec_ as exec
|
|
5
|
+
from .client import Swytchcode
|
|
6
|
+
from .providers import (
|
|
7
|
+
AnthropicProvider,
|
|
8
|
+
OpenAIAgentsProvider,
|
|
9
|
+
VercelProvider,
|
|
10
|
+
LangGraphProvider,
|
|
11
|
+
CrewAIProvider,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"exec",
|
|
16
|
+
"Swytchcode",
|
|
17
|
+
"SwytchcodeError",
|
|
18
|
+
"is_swytchcode_error",
|
|
19
|
+
"AnthropicProvider",
|
|
20
|
+
"OpenAIAgentsProvider",
|
|
21
|
+
"VercelProvider",
|
|
22
|
+
"LangGraphProvider",
|
|
23
|
+
"CrewAIProvider",
|
|
24
|
+
]
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Run a swytchcode CLI subcommand with --json and parse the output."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import subprocess
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from .exec import _resolve_bin
|
|
10
|
+
from .errors import SwytchcodeError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def run_cli(
|
|
14
|
+
args: list[str],
|
|
15
|
+
*,
|
|
16
|
+
cwd: str | None = None,
|
|
17
|
+
env: dict | None = None,
|
|
18
|
+
timeout: float | None = 60,
|
|
19
|
+
) -> Any:
|
|
20
|
+
cmd = [_resolve_bin(), *args]
|
|
21
|
+
if "--json" not in args:
|
|
22
|
+
cmd.append("--json")
|
|
23
|
+
|
|
24
|
+
run_env = os.environ.copy()
|
|
25
|
+
if env:
|
|
26
|
+
run_env.update(env)
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
r = subprocess.run(
|
|
30
|
+
cmd,
|
|
31
|
+
capture_output=True,
|
|
32
|
+
cwd=cwd or os.getcwd(),
|
|
33
|
+
env=run_env,
|
|
34
|
+
timeout=timeout,
|
|
35
|
+
)
|
|
36
|
+
except FileNotFoundError as e:
|
|
37
|
+
raise SwytchcodeError(
|
|
38
|
+
"Failed to spawn swytchcode; is the CLI installed?", e
|
|
39
|
+
) from e
|
|
40
|
+
except subprocess.TimeoutExpired as e:
|
|
41
|
+
raise SwytchcodeError(
|
|
42
|
+
f"swytchcode command timed out after {timeout}s", e
|
|
43
|
+
) from e
|
|
44
|
+
|
|
45
|
+
if r.returncode != 0:
|
|
46
|
+
raise SwytchcodeError(
|
|
47
|
+
r.stderr.decode("utf-8", "replace").strip() or "command failed",
|
|
48
|
+
r.returncode,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
out = r.stdout.decode("utf-8", "replace").strip()
|
|
52
|
+
if not out:
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
return json.loads(out)
|
|
57
|
+
except json.JSONDecodeError as e:
|
|
58
|
+
raise SwytchcodeError("Invalid JSON from swytchcode", out) from e
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""High-level agentic client on top of the existing exec()."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
from typing import Any, Optional
|
|
5
|
+
|
|
6
|
+
from . import discover as _discover, schema as _schema, manage as _manage
|
|
7
|
+
from .exec import exec_ as _exec
|
|
8
|
+
from .providers.base import Provider, Tool
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _strip_empty(obj: Any) -> Any:
|
|
12
|
+
"""Recursively drop keys whose value is None or an empty string ("").
|
|
13
|
+
|
|
14
|
+
With 'expose all fields', agents often fill unused optional fields with "",
|
|
15
|
+
which APIs like Stripe reject ("empty values are an attempt to unset").
|
|
16
|
+
Only None and "" are dropped — 0, False, [], {} are preserved as meaningful.
|
|
17
|
+
"""
|
|
18
|
+
if isinstance(obj, dict):
|
|
19
|
+
out = {}
|
|
20
|
+
for k, v in obj.items():
|
|
21
|
+
if v is None or (isinstance(v, str) and v == ""):
|
|
22
|
+
continue
|
|
23
|
+
out[k] = _strip_empty(v)
|
|
24
|
+
return out
|
|
25
|
+
if isinstance(obj, list):
|
|
26
|
+
return [_strip_empty(v) for v in obj]
|
|
27
|
+
return obj
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class _Tools:
|
|
31
|
+
def __init__(self, client: "Swytchcode"):
|
|
32
|
+
self._c = client
|
|
33
|
+
# Maps a sanitized tool name (dots -> underscores) back to its canonical
|
|
34
|
+
# ID, populated as tools are built. Used to reverse names in
|
|
35
|
+
# handle_tool_calls without a lossy "_"->"." string replace.
|
|
36
|
+
self._name_to_cid: dict[str, str] = {}
|
|
37
|
+
|
|
38
|
+
def get(self, *, toolkits=None, tools=None, search=None):
|
|
39
|
+
neutral = [self._tool(cid) for cid in self._ids(toolkits, tools, search)]
|
|
40
|
+
p = self._c.provider
|
|
41
|
+
return p.format_tools(neutral) if p else neutral
|
|
42
|
+
|
|
43
|
+
def execute(self, canonical_id: str, args: dict, **options) -> Any:
|
|
44
|
+
# If args are flat (no body/params top-level keys), wrap them in body
|
|
45
|
+
# as expected by the Swytchcode CLI kernel (like in run-workflow.js)
|
|
46
|
+
if "body" not in args and "params" not in args:
|
|
47
|
+
args = {"body": args}
|
|
48
|
+
# Drop empty optional fields (None/"") from body & params so values an
|
|
49
|
+
# agent over-filled don't reach the API (e.g. Stripe rejects customer="").
|
|
50
|
+
args = dict(args)
|
|
51
|
+
if isinstance(args.get("body"), (dict, list)):
|
|
52
|
+
args["body"] = _strip_empty(args["body"])
|
|
53
|
+
if isinstance(args.get("params"), (dict, list)):
|
|
54
|
+
args["params"] = _strip_empty(args["params"])
|
|
55
|
+
# Forward exec options (dry_run, raw, allow_raw, cwd, env) to the CLI.
|
|
56
|
+
return _exec(canonical_id, args, **options)
|
|
57
|
+
|
|
58
|
+
def _tool(self, cid: str) -> Tool:
|
|
59
|
+
m = _discover.info(cid)
|
|
60
|
+
name = cid.replace(".", "_")
|
|
61
|
+
self._name_to_cid[name] = cid
|
|
62
|
+
return Tool(
|
|
63
|
+
canonical_id=cid,
|
|
64
|
+
name=name,
|
|
65
|
+
description=m.get("summary") or m.get("description") or cid,
|
|
66
|
+
input_schema=_schema.simplify(m.get("inputs")),
|
|
67
|
+
execute=lambda args, _c=cid: self.execute(_c, args),
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
def _ids(self, toolkits, tools, search) -> list[str]:
|
|
71
|
+
# Implementation for Phase 1: resolve against CLI local state
|
|
72
|
+
if tools:
|
|
73
|
+
return tools
|
|
74
|
+
if search:
|
|
75
|
+
# Mirror the TS runtime: resolve a natural-language search to
|
|
76
|
+
# canonical IDs via the CLI's discover/search.
|
|
77
|
+
return [
|
|
78
|
+
t["canonical_id"]
|
|
79
|
+
for t in _discover.search(search)
|
|
80
|
+
if t.get("canonical_id")
|
|
81
|
+
]
|
|
82
|
+
if toolkits:
|
|
83
|
+
# Resolve toolkits against local tooling.json instead of global search
|
|
84
|
+
res = _manage.list_tools("tooling")
|
|
85
|
+
found = []
|
|
86
|
+
for m in res.get("methods", []):
|
|
87
|
+
integration = m.get("integration", "")
|
|
88
|
+
cid = m.get("canonical_id")
|
|
89
|
+
if not cid:
|
|
90
|
+
continue
|
|
91
|
+
for tk in toolkits:
|
|
92
|
+
# integration is "project.library@version"; match the project
|
|
93
|
+
# segment exactly, not a loose substring (so "hub" != "github").
|
|
94
|
+
if tk == integration or integration.startswith(f"{tk}."):
|
|
95
|
+
found.append(cid)
|
|
96
|
+
break
|
|
97
|
+
return found
|
|
98
|
+
return []
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class Swytchcode:
|
|
102
|
+
def __init__(self, provider: Optional[Provider] = None):
|
|
103
|
+
self.provider = provider
|
|
104
|
+
self.tools = _Tools(self)
|
|
105
|
+
|
|
106
|
+
def handle_tool_calls(self, response: Any) -> list[dict]:
|
|
107
|
+
"""Helper to execute tools for non-agentic APIs like Anthropic."""
|
|
108
|
+
results = []
|
|
109
|
+
for block in getattr(response, "content", []):
|
|
110
|
+
if getattr(block, "type", "") == "tool_use":
|
|
111
|
+
# Reverse the sanitized name via the map built in _tool(). A plain
|
|
112
|
+
# "_"->"." replace would corrupt canonical IDs whose segments
|
|
113
|
+
# contain underscores (e.g. stripe.create_payment).
|
|
114
|
+
cid = self.tools._name_to_cid.get(block.name) or block.name.replace(
|
|
115
|
+
"_", "."
|
|
116
|
+
)
|
|
117
|
+
# Isolate failures per block so one failing tool doesn't drop the
|
|
118
|
+
# results for the other tool_use blocks in the same turn.
|
|
119
|
+
try:
|
|
120
|
+
content = str(self.tools.execute(cid, getattr(block, "input", {})))
|
|
121
|
+
is_error = False
|
|
122
|
+
except Exception as e:
|
|
123
|
+
content = f"Error executing {cid}: {e}"
|
|
124
|
+
is_error = True
|
|
125
|
+
results.append(
|
|
126
|
+
{
|
|
127
|
+
"type": "tool_result",
|
|
128
|
+
"tool_use_id": block.id,
|
|
129
|
+
"content": content,
|
|
130
|
+
"is_error": is_error,
|
|
131
|
+
}
|
|
132
|
+
)
|
|
133
|
+
return results
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Find tools by intent and read their schemas."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
import logging
|
|
5
|
+
from .cli import run_cli
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def search(intent: str, *, top: int = 5) -> list[dict]:
|
|
11
|
+
res = run_cli(["discover", intent, "--top", str(top)]) or {}
|
|
12
|
+
return res.get("capabilities", [])
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def info(canonical_id: str) -> dict:
|
|
16
|
+
try:
|
|
17
|
+
result = run_cli(["info", canonical_id]) or {}
|
|
18
|
+
# CLI returns a JSON array of ToolInfo objects; unwrap the first one
|
|
19
|
+
if isinstance(result, list):
|
|
20
|
+
return result[0] if result else {}
|
|
21
|
+
return result
|
|
22
|
+
except Exception as e:
|
|
23
|
+
# Broad by design: any CLI/parse failure degrades to an empty schema
|
|
24
|
+
# rather than breaking tool discovery. Logged (not printed) so it
|
|
25
|
+
# respects the host app's logging config.
|
|
26
|
+
logger.warning(
|
|
27
|
+
"Failed to fetch info for %s (%s). Using empty schema.", canonical_id, e
|
|
28
|
+
)
|
|
29
|
+
return {"canonical_id": canonical_id, "inputs": {}}
|
|
@@ -2,12 +2,54 @@
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
import os
|
|
5
|
+
import shutil
|
|
5
6
|
import subprocess
|
|
7
|
+
import sys
|
|
6
8
|
from typing import Any
|
|
7
9
|
|
|
8
10
|
from .errors import SwytchcodeError
|
|
9
11
|
|
|
10
12
|
|
|
13
|
+
def _resolve_bin() -> str:
|
|
14
|
+
"""
|
|
15
|
+
Resolve the swytchcode binary path using the following order:
|
|
16
|
+
|
|
17
|
+
1. SWYTCHCODE_BIN env var — explicit override.
|
|
18
|
+
2. PATH lookup via shutil.which — the standard resolution.
|
|
19
|
+
3. Common install-path fallbacks for when PATH is not configured.
|
|
20
|
+
"""
|
|
21
|
+
# 1. Explicit override
|
|
22
|
+
explicit = os.environ.get("SWYTCHCODE_BIN", "").strip()
|
|
23
|
+
if explicit:
|
|
24
|
+
return explicit
|
|
25
|
+
|
|
26
|
+
# 2. PATH lookup
|
|
27
|
+
found = shutil.which("swytchcode")
|
|
28
|
+
if found:
|
|
29
|
+
return found
|
|
30
|
+
|
|
31
|
+
# 3. Common install-path fallbacks
|
|
32
|
+
if sys.platform == "win32":
|
|
33
|
+
local_app_data = os.environ.get("LOCALAPPDATA", "")
|
|
34
|
+
candidates = [
|
|
35
|
+
os.path.join(
|
|
36
|
+
local_app_data, "Programs", "swytchcode", "bin", "swytchcode.exe"
|
|
37
|
+
),
|
|
38
|
+
]
|
|
39
|
+
else:
|
|
40
|
+
home = os.path.expanduser("~")
|
|
41
|
+
candidates = [
|
|
42
|
+
os.path.join(home, ".local", "bin", "swytchcode"),
|
|
43
|
+
"/usr/local/bin/swytchcode",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
for path in candidates:
|
|
47
|
+
if path and os.path.isfile(path) and os.access(path, os.X_OK):
|
|
48
|
+
return path
|
|
49
|
+
|
|
50
|
+
return "swytchcode" # fall through; subprocess.run reports FileNotFoundError if still missing
|
|
51
|
+
|
|
52
|
+
|
|
11
53
|
def exec_( # noqa: A001 - shadowing intentional for API consistency with JS/Go
|
|
12
54
|
canonical_id: str,
|
|
13
55
|
input: Any = None,
|
|
@@ -50,7 +92,8 @@ def exec_( # noqa: A001 - shadowing intentional for API consistency with JS/Go
|
|
|
50
92
|
raise SwytchcodeError("canonical_id must be a non-empty string")
|
|
51
93
|
|
|
52
94
|
flag = "--raw" if raw else "--json"
|
|
53
|
-
|
|
95
|
+
bin_path = _resolve_bin()
|
|
96
|
+
cmd = [bin_path, "exec", canonical_id, flag]
|
|
54
97
|
if dry_run:
|
|
55
98
|
cmd.append("--dry-run")
|
|
56
99
|
if allow_raw:
|
|
@@ -74,7 +117,12 @@ def exec_( # noqa: A001 - shadowing intentional for API consistency with JS/Go
|
|
|
74
117
|
timeout=None,
|
|
75
118
|
)
|
|
76
119
|
except FileNotFoundError as e:
|
|
77
|
-
|
|
120
|
+
hint = (
|
|
121
|
+
f"using binary at {bin_path!r}"
|
|
122
|
+
if "SWYTCHCODE_BIN" in os.environ
|
|
123
|
+
else "install it with: npm install -g swytchcode (or set SWYTCHCODE_BIN=/path/to/binary)"
|
|
124
|
+
)
|
|
125
|
+
raise SwytchcodeError(f"Failed to spawn swytchcode — {hint}", e) from e
|
|
78
126
|
except OSError as e:
|
|
79
127
|
raise SwytchcodeError("Failed to run swytchcode", e) from e
|
|
80
128
|
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Management commands wrapper for the SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
from .cli import run_cli
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def add(library_or_url: str) -> dict:
|
|
8
|
+
return run_cli(["add", library_or_url]) or {}
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def list_tools(filter_arg: str = "") -> dict:
|
|
12
|
+
args = ["list", "--json"]
|
|
13
|
+
if filter_arg:
|
|
14
|
+
args.insert(1, filter_arg)
|
|
15
|
+
return run_cli(args) or {}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def keys() -> list[dict]:
|
|
19
|
+
return run_cli(["key", "list"]) or []
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def policies() -> list[dict]:
|
|
23
|
+
return run_cli(["policy", "list"]) or []
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def search(keyword: str = "") -> dict:
|
|
27
|
+
args = ["search", "--json"]
|
|
28
|
+
if keyword:
|
|
29
|
+
args.insert(1, keyword)
|
|
30
|
+
return run_cli(args) or {}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Providers for integrating Swytchcode tools into various AI frameworks."""
|
|
2
|
+
|
|
3
|
+
from .anthropic import AnthropicProvider
|
|
4
|
+
from .crewai import CrewAIProvider
|
|
5
|
+
from .langgraph import LangGraphProvider
|
|
6
|
+
from .openai_agents import OpenAIAgentsProvider
|
|
7
|
+
from .vercel import VercelProvider
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"AnthropicProvider",
|
|
11
|
+
"CrewAIProvider",
|
|
12
|
+
"LangGraphProvider",
|
|
13
|
+
"OpenAIAgentsProvider",
|
|
14
|
+
"VercelProvider",
|
|
15
|
+
]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Anthropic Claude provider (Non-agentic / raw tool use schemas)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
from .base import Provider, Tool
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class AnthropicProvider(Provider):
|
|
8
|
+
def format_tool(self, tool: Tool):
|
|
9
|
+
return {
|
|
10
|
+
"name": tool.name,
|
|
11
|
+
"description": tool.description,
|
|
12
|
+
"input_schema": tool.input_schema,
|
|
13
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Provider base for adapting neutral Swytchcode tools to each framework.
|
|
2
|
+
|
|
3
|
+
A single Provider base: `format_tool` wraps one neutral Tool into a framework's
|
|
4
|
+
native tool object, and `format_tools` maps over a list. Execution is carried on
|
|
5
|
+
the Tool itself via its `execute` closure (which runs the Swytchcode CLI): agentic
|
|
6
|
+
frameworks (OpenAI Agents, Vercel, LangGraph, CrewAI) invoke that closure directly
|
|
7
|
+
during their own loop, while non-agentic APIs (Anthropic) run it through
|
|
8
|
+
`Swytchcode.handle_tool_calls`. Note this is deliberately NOT Composio's two-class
|
|
9
|
+
agentic/non-agentic split — execution lives on the tool, not an injected function.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from typing import Any, Callable
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class Tool:
|
|
19
|
+
canonical_id: str
|
|
20
|
+
name: str
|
|
21
|
+
description: str
|
|
22
|
+
input_schema: dict
|
|
23
|
+
execute: Callable[[dict], Any]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Provider:
|
|
27
|
+
def format_tool(self, tool: Tool) -> Any:
|
|
28
|
+
raise NotImplementedError
|
|
29
|
+
|
|
30
|
+
def format_tools(self, tools: list[Tool]) -> list[Any]:
|
|
31
|
+
return [self.format_tool(t) for t in tools]
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""CrewAI provider: neutral tools -> BaseTool subclasses."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
from .base import Provider, Tool
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class CrewAIProvider(Provider):
|
|
8
|
+
def format_tool(self, tool: Tool):
|
|
9
|
+
from crewai.tools import BaseTool
|
|
10
|
+
from ..schema import to_pydantic_model
|
|
11
|
+
|
|
12
|
+
args_schema_cls = to_pydantic_model(tool.input_schema, tool.name + "Schema")
|
|
13
|
+
|
|
14
|
+
class DynamicCrewTool(BaseTool):
|
|
15
|
+
name: str = tool.name
|
|
16
|
+
description: str = tool.description
|
|
17
|
+
args_schema: type = args_schema_cls
|
|
18
|
+
|
|
19
|
+
def _run(self, **kwargs):
|
|
20
|
+
return tool.execute(kwargs)
|
|
21
|
+
|
|
22
|
+
return DynamicCrewTool()
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""LangGraph provider: neutral tools -> LangChain StructuredTool objects."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
from .base import Provider, Tool
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class LangGraphProvider(Provider):
|
|
8
|
+
def format_tool(self, tool: Tool):
|
|
9
|
+
from langchain_core.tools import StructuredTool
|
|
10
|
+
from ..schema import to_pydantic_model
|
|
11
|
+
|
|
12
|
+
return StructuredTool.from_function(
|
|
13
|
+
name=tool.name,
|
|
14
|
+
description=tool.description,
|
|
15
|
+
func=lambda **kwargs: tool.execute(kwargs),
|
|
16
|
+
args_schema=to_pydantic_model(tool.input_schema, tool.name + "Schema"),
|
|
17
|
+
)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""OpenAI Agents SDK provider."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
import asyncio
|
|
5
|
+
import json
|
|
6
|
+
from .base import Provider, Tool
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class OpenAIAgentsProvider(Provider):
|
|
10
|
+
def format_tool(self, tool: Tool):
|
|
11
|
+
try:
|
|
12
|
+
from agents import FunctionTool
|
|
13
|
+
except ImportError:
|
|
14
|
+
raise ImportError(
|
|
15
|
+
"Please install the OpenAI Agents SDK: pip install openai-agents"
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
async def _on_invoke(ctx, args: str):
|
|
19
|
+
kwargs = json.loads(args) if args else {}
|
|
20
|
+
# tool.execute drives the CLI through a blocking subprocess; offload
|
|
21
|
+
# it to a worker thread so we never stall the agent's event loop.
|
|
22
|
+
return await asyncio.to_thread(tool.execute, kwargs)
|
|
23
|
+
|
|
24
|
+
return FunctionTool(
|
|
25
|
+
name=str(tool.name).replace(".", "_"),
|
|
26
|
+
description=str(tool.description),
|
|
27
|
+
params_json_schema=tool.input_schema,
|
|
28
|
+
on_invoke_tool=_on_invoke,
|
|
29
|
+
strict_json_schema=False,
|
|
30
|
+
)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Vercel AI SDK provider (Python equivalent)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
from .base import Provider, Tool
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class VercelProvider(Provider):
|
|
8
|
+
def format_tool(self, tool: Tool):
|
|
9
|
+
try:
|
|
10
|
+
from ai_sdk import tool as vercel_tool
|
|
11
|
+
except ImportError:
|
|
12
|
+
raise ImportError(
|
|
13
|
+
"Please install the Vercel AI SDK (requires Python 3.12+): pip install ai-sdk-python"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
# The handler MUST be synchronous. ai_sdk's sync generate_text() calls the
|
|
17
|
+
# tool handler without awaiting it, so an async handler is silently never
|
|
18
|
+
# awaited — the tool never runs and the model hallucinates success. A sync
|
|
19
|
+
# handler returns a plain value that works in generate_text AND is fine for
|
|
20
|
+
# the async Tool.run path (which only awaits awaitables). Called as
|
|
21
|
+
# handler(**kwargs) — arguments by field name, never a positional dict.
|
|
22
|
+
def _execute(**kwargs):
|
|
23
|
+
return tool.execute(kwargs)
|
|
24
|
+
|
|
25
|
+
return vercel_tool(
|
|
26
|
+
name=tool.name,
|
|
27
|
+
description=tool.description,
|
|
28
|
+
parameters=tool.input_schema,
|
|
29
|
+
execute=_execute,
|
|
30
|
+
)
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Simplify a tool's input schema for the LLM: expose all fields, mark required ones."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
from typing import Any, Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def simplify(inputs: Any) -> dict:
|
|
8
|
+
# Handle Wrekenfile shape: a list of single-key dicts (e.g. [{"amount": {"TYPE": "INT"...}}])
|
|
9
|
+
if isinstance(inputs, list):
|
|
10
|
+
props = {}
|
|
11
|
+
required = []
|
|
12
|
+
for item in inputs:
|
|
13
|
+
if not isinstance(item, dict):
|
|
14
|
+
continue
|
|
15
|
+
for name, spec in item.items():
|
|
16
|
+
if not isinstance(spec, dict):
|
|
17
|
+
continue
|
|
18
|
+
|
|
19
|
+
# Default to string if TYPE is missing
|
|
20
|
+
t = spec.get("TYPE", "STRING").lower()
|
|
21
|
+
if t == "int":
|
|
22
|
+
t = "integer"
|
|
23
|
+
elif t in ("float", "number", "double"):
|
|
24
|
+
t = "number"
|
|
25
|
+
elif t == "bool":
|
|
26
|
+
t = "boolean"
|
|
27
|
+
elif t == "object":
|
|
28
|
+
t = "object"
|
|
29
|
+
elif t == "any":
|
|
30
|
+
t = "object"
|
|
31
|
+
elif t.startswith("[]"):
|
|
32
|
+
t = "array"
|
|
33
|
+
else:
|
|
34
|
+
t = "string"
|
|
35
|
+
|
|
36
|
+
props[name] = {"type": t, "description": spec.get("DESC", "")}
|
|
37
|
+
|
|
38
|
+
req = spec.get("REQUIRED", False)
|
|
39
|
+
is_required = req is True or (
|
|
40
|
+
isinstance(req, str) and req.strip().lower() == "true"
|
|
41
|
+
)
|
|
42
|
+
if is_required:
|
|
43
|
+
required.append(name)
|
|
44
|
+
|
|
45
|
+
# Composio-style rule: expose ALL fields to the model and list only the
|
|
46
|
+
# truly-required ones in `required`. A required-only approach hid optional
|
|
47
|
+
# fields — which left all-optional tools (e.g. Stripe) with an empty schema
|
|
48
|
+
# so the model called them with no arguments, and blinded the model to
|
|
49
|
+
# optional fields on tools that do have some required ones.
|
|
50
|
+
return {"type": "object", "properties": props, "required": required}
|
|
51
|
+
|
|
52
|
+
# Fallback to standard JSON Schema handling
|
|
53
|
+
if not isinstance(inputs, dict):
|
|
54
|
+
return {"type": "object", "properties": {}, "required": []}
|
|
55
|
+
|
|
56
|
+
props = inputs.get("properties") or {}
|
|
57
|
+
required = inputs.get("required") or []
|
|
58
|
+
keep = {}
|
|
59
|
+
|
|
60
|
+
# Expose all fields (same rule as the wrekenfile branch above); use the
|
|
61
|
+
# original required list only for the `required` key so optional/nested
|
|
62
|
+
# fields stay optional instead of being dropped or forced required.
|
|
63
|
+
for name, spec in props.items():
|
|
64
|
+
if (
|
|
65
|
+
isinstance(spec, dict)
|
|
66
|
+
and spec.get("type") == "object"
|
|
67
|
+
and "properties" in spec
|
|
68
|
+
):
|
|
69
|
+
spec = simplify(spec) # recurse into nested objects
|
|
70
|
+
keep[name] = spec
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
"type": "object",
|
|
74
|
+
"properties": keep,
|
|
75
|
+
"required": [r for r in required if r in keep],
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def to_pydantic_model(schema: dict, name: str = "ArgsSchema") -> Any:
|
|
80
|
+
"""Dynamically build a Pydantic model from a JSON schema for LangGraph args_schema."""
|
|
81
|
+
from pydantic import create_model
|
|
82
|
+
|
|
83
|
+
if not schema or "properties" not in schema:
|
|
84
|
+
return create_model(name)
|
|
85
|
+
|
|
86
|
+
fields = {}
|
|
87
|
+
properties = schema.get("properties", {})
|
|
88
|
+
required = set(schema.get("required", []))
|
|
89
|
+
|
|
90
|
+
for field_name, field_info in properties.items():
|
|
91
|
+
field_type = str
|
|
92
|
+
t = field_info.get("type")
|
|
93
|
+
if t == "integer":
|
|
94
|
+
field_type = int
|
|
95
|
+
elif t == "number":
|
|
96
|
+
field_type = float
|
|
97
|
+
elif t == "boolean":
|
|
98
|
+
field_type = bool
|
|
99
|
+
elif t == "array":
|
|
100
|
+
field_type = list
|
|
101
|
+
elif t == "object":
|
|
102
|
+
field_type = to_pydantic_model(field_info, f"{name}_{field_name}")
|
|
103
|
+
|
|
104
|
+
if field_name in required:
|
|
105
|
+
fields[field_name] = (field_type, ...)
|
|
106
|
+
else:
|
|
107
|
+
# Optional[...] so an explicit null is accepted (pydantic v2 rejects
|
|
108
|
+
# None for a bare non-optional annotation).
|
|
109
|
+
fields[field_name] = (Optional[field_type], None)
|
|
110
|
+
|
|
111
|
+
return create_model(name, **fields)
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: swytchcode-runtime
|
|
3
|
-
Version:
|
|
3
|
+
Version: 1.0.0
|
|
4
4
|
Summary: Thin runtime wrapper around the Swytchcode CLI
|
|
5
5
|
Author: Swytchcode
|
|
6
6
|
License: MIT
|
|
7
|
-
Project-URL: repository, https://
|
|
7
|
+
Project-URL: repository, https://github.com/swytchcodehq/runtime-py
|
|
8
8
|
Keywords: swytchcode,cli,runtime,exec
|
|
9
9
|
Classifier: License :: OSI Approved :: MIT License
|
|
10
10
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -13,12 +13,18 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.12
|
|
14
14
|
Requires-Python: >=3.10
|
|
15
15
|
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE.md
|
|
17
|
+
Dynamic: license-file
|
|
16
18
|
|
|
17
19
|
# swytchcode-runtime (Python)
|
|
18
20
|
|
|
19
21
|
Thin runtime wrapper around the Swytchcode CLI. Calls `swytchcode exec` for you so you can stay in Python without shell boilerplate.
|
|
20
22
|
|
|
21
|
-
**Requires:** The `swytchcode`
|
|
23
|
+
**Requires:** The `swytchcode` CLI must be installed. The binary is located automatically — no configuration needed in most environments. Resolution order:
|
|
24
|
+
|
|
25
|
+
1. `SWYTCHCODE_BIN` env var — explicit override.
|
|
26
|
+
2. `$PATH` lookup via `shutil.which` — the standard system resolution.
|
|
27
|
+
3. Common install paths — `~/.local/bin`, `/usr/local/bin` (Unix) or `%LOCALAPPDATA%\Programs\swytchcode\bin` (Windows).
|
|
22
28
|
|
|
23
29
|
## Install
|
|
24
30
|
|
|
@@ -83,6 +89,13 @@ output = exec("api.report.export", {"id": "123"}, raw=True)
|
|
|
83
89
|
|
|
84
90
|
This runtime invokes `swytchcode exec [canonical_id]` with the flags above. For full exec behavior, see the Swytchcode kernel documentation.
|
|
85
91
|
|
|
92
|
+
### Environment variables
|
|
93
|
+
|
|
94
|
+
| Variable | Description |
|
|
95
|
+
|----------|-------------|
|
|
96
|
+
| `SWYTCHCODE_BIN` | Override the resolved binary path. Set this only when automatic resolution does not find the correct binary (e.g. non-standard install locations or virtualised environments). |
|
|
97
|
+
| `SWYTCHCODE_TOKEN` | Auth token passed to the CLI via the process environment. |
|
|
98
|
+
|
|
86
99
|
### Error handling
|
|
87
100
|
|
|
88
101
|
Failures raise `SwytchcodeError` (spawn error, non-zero exit, invalid JSON). Use the helper to narrow:
|
|
@@ -131,3 +144,58 @@ def create_account(input: dict) -> any:
|
|
|
131
144
|
```
|
|
132
145
|
|
|
133
146
|
**Avoid:** subprocess boilerplate, HTTP calls, or config parsing. Use `exec(...)` and let the CLI handle execution and policy.
|
|
147
|
+
|
|
148
|
+
## Agentic workflows (framework integrations)
|
|
149
|
+
|
|
150
|
+
On top of `exec`, the runtime exposes a small agentic surface that turns Swytchcode tools
|
|
151
|
+
into the native tool objects each agent framework expects. The Swytchcode part is the same
|
|
152
|
+
two lines regardless of framework:
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
from swytchcode_runtime import Swytchcode
|
|
156
|
+
from swytchcode_runtime.providers.anthropic import AnthropicProvider
|
|
157
|
+
|
|
158
|
+
swx = Swytchcode(provider=AnthropicProvider())
|
|
159
|
+
tools = swx.tools.get(toolkits=["stripe"]) # framework-native tools, full param schemas
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Selecting tools — `swx.tools.get(...)`
|
|
163
|
+
|
|
164
|
+
Pass exactly one selector; IDs resolve against your local Swytchcode state and remote search:
|
|
165
|
+
|
|
166
|
+
- `toolkits=["stripe"]` — every enabled tool whose integration matches a toolkit.
|
|
167
|
+
- `tools=["charges.charge.create"]` — explicit canonical IDs.
|
|
168
|
+
- `search="refund a charge"` — natural-language discovery (via `swytchcode discover`).
|
|
169
|
+
|
|
170
|
+
Each returned tool carries a **full input schema** — every field is surfaced to the model, with
|
|
171
|
+
only the truly-required ones marked `required` — and an `execute` callback that runs `swytchcode
|
|
172
|
+
exec` for you (empty optional values are stripped before the call so APIs like Stripe don't reject them).
|
|
173
|
+
|
|
174
|
+
### Supported providers
|
|
175
|
+
|
|
176
|
+
| Framework | Import | Who runs the tool loop |
|
|
177
|
+
|-----------|--------|------------------------|
|
|
178
|
+
| Anthropic Claude | `providers.anthropic.AnthropicProvider` | you (Messages API + `swx.handle_tool_calls`) |
|
|
179
|
+
| OpenAI Agents SDK | `providers.openai_agents.OpenAIAgentsProvider` | the SDK |
|
|
180
|
+
| Vercel AI SDK | `providers.vercel.VercelProvider` | the SDK |
|
|
181
|
+
| LangGraph | `providers.langgraph.LangGraphProvider` | the prebuilt agent |
|
|
182
|
+
| CrewAI | `providers.crewai.CrewAIProvider` | the crew |
|
|
183
|
+
|
|
184
|
+
### Non-agentic APIs (Anthropic Messages)
|
|
185
|
+
|
|
186
|
+
When you run the tool loop yourself, `handle_tool_calls` executes each `tool_use` block and
|
|
187
|
+
returns the `tool_result` blocks to send back:
|
|
188
|
+
|
|
189
|
+
```python
|
|
190
|
+
import anthropic
|
|
191
|
+
client = anthropic.Anthropic()
|
|
192
|
+
msg = client.messages.create(
|
|
193
|
+
model="claude-sonnet-5", max_tokens=1024, tools=tools,
|
|
194
|
+
messages=[{"role": "user", "content": "Refund charge ch_123 for $20"}],
|
|
195
|
+
)
|
|
196
|
+
results = swx.handle_tool_calls(msg) # runs the tool calls, returns tool_result blocks
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
One runnable file per framework lives in `sdk-examples/`. Install the matching framework SDK
|
|
200
|
+
(`pip install openai-agents` / `anthropic` / `ai` / `langgraph` / `crewai`) alongside the
|
|
201
|
+
`swytchcode` CLI.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
LICENSE.md
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
swytchcode_runtime/__init__.py
|
|
5
|
+
swytchcode_runtime/cli.py
|
|
6
|
+
swytchcode_runtime/client.py
|
|
7
|
+
swytchcode_runtime/discover.py
|
|
8
|
+
swytchcode_runtime/errors.py
|
|
9
|
+
swytchcode_runtime/exec.py
|
|
10
|
+
swytchcode_runtime/manage.py
|
|
11
|
+
swytchcode_runtime/schema.py
|
|
12
|
+
swytchcode_runtime.egg-info/PKG-INFO
|
|
13
|
+
swytchcode_runtime.egg-info/SOURCES.txt
|
|
14
|
+
swytchcode_runtime.egg-info/dependency_links.txt
|
|
15
|
+
swytchcode_runtime.egg-info/top_level.txt
|
|
16
|
+
swytchcode_runtime/providers/__init__.py
|
|
17
|
+
swytchcode_runtime/providers/anthropic.py
|
|
18
|
+
swytchcode_runtime/providers/base.py
|
|
19
|
+
swytchcode_runtime/providers/crewai.py
|
|
20
|
+
swytchcode_runtime/providers/langgraph.py
|
|
21
|
+
swytchcode_runtime/providers/openai_agents.py
|
|
22
|
+
swytchcode_runtime/providers/vercel.py
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
README.md
|
|
2
|
-
pyproject.toml
|
|
3
|
-
swytchcode_runtime/__init__.py
|
|
4
|
-
swytchcode_runtime/errors.py
|
|
5
|
-
swytchcode_runtime/exec.py
|
|
6
|
-
swytchcode_runtime.egg-info/PKG-INFO
|
|
7
|
-
swytchcode_runtime.egg-info/SOURCES.txt
|
|
8
|
-
swytchcode_runtime.egg-info/dependency_links.txt
|
|
9
|
-
swytchcode_runtime.egg-info/top_level.txt
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{swytchcode_runtime-0.1.0 → swytchcode_runtime-1.0.0}/swytchcode_runtime.egg-info/top_level.txt
RENAMED
|
File without changes
|