swytchcode-runtime 1.0.0__tar.gz → 1.0.2__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/swytchcode_runtime.egg-info → swytchcode_runtime-1.0.2}/PKG-INFO +52 -17
- {swytchcode_runtime-1.0.0 → swytchcode_runtime-1.0.2}/README.md +51 -16
- {swytchcode_runtime-1.0.0 → swytchcode_runtime-1.0.2}/pyproject.toml +1 -1
- swytchcode_runtime-1.0.2/swytchcode_runtime/__init__.py +30 -0
- swytchcode_runtime-1.0.2/swytchcode_runtime/client.py +243 -0
- {swytchcode_runtime-1.0.0 → swytchcode_runtime-1.0.2}/swytchcode_runtime/exec.py +7 -1
- swytchcode_runtime-1.0.2/swytchcode_runtime/providers/__init__.py +27 -0
- {swytchcode_runtime-1.0.0 → swytchcode_runtime-1.0.2}/swytchcode_runtime/providers/base.py +1 -1
- {swytchcode_runtime-1.0.0 → swytchcode_runtime-1.0.2}/swytchcode_runtime/providers/openai_agents.py +1 -1
- {swytchcode_runtime-1.0.0 → swytchcode_runtime-1.0.2}/swytchcode_runtime/providers/vercel.py +2 -2
- {swytchcode_runtime-1.0.0 → swytchcode_runtime-1.0.2}/swytchcode_runtime/schema.py +13 -9
- {swytchcode_runtime-1.0.0 → swytchcode_runtime-1.0.2/swytchcode_runtime.egg-info}/PKG-INFO +52 -17
- {swytchcode_runtime-1.0.0 → swytchcode_runtime-1.0.2}/swytchcode_runtime.egg-info/SOURCES.txt +3 -1
- swytchcode_runtime-1.0.2/tests/test_alias.py +42 -0
- swytchcode_runtime-1.0.2/tests/test_schema_routing.py +62 -0
- swytchcode_runtime-1.0.0/swytchcode_runtime/__init__.py +0 -24
- swytchcode_runtime-1.0.0/swytchcode_runtime/client.py +0 -133
- swytchcode_runtime-1.0.0/swytchcode_runtime/providers/__init__.py +0 -15
- {swytchcode_runtime-1.0.0 → swytchcode_runtime-1.0.2}/LICENSE.md +0 -0
- {swytchcode_runtime-1.0.0 → swytchcode_runtime-1.0.2}/setup.cfg +0 -0
- {swytchcode_runtime-1.0.0 → swytchcode_runtime-1.0.2}/swytchcode_runtime/cli.py +0 -0
- {swytchcode_runtime-1.0.0 → swytchcode_runtime-1.0.2}/swytchcode_runtime/discover.py +0 -0
- {swytchcode_runtime-1.0.0 → swytchcode_runtime-1.0.2}/swytchcode_runtime/errors.py +0 -0
- {swytchcode_runtime-1.0.0 → swytchcode_runtime-1.0.2}/swytchcode_runtime/manage.py +0 -0
- {swytchcode_runtime-1.0.0 → swytchcode_runtime-1.0.2}/swytchcode_runtime/providers/anthropic.py +0 -0
- {swytchcode_runtime-1.0.0 → swytchcode_runtime-1.0.2}/swytchcode_runtime/providers/crewai.py +0 -0
- {swytchcode_runtime-1.0.0 → swytchcode_runtime-1.0.2}/swytchcode_runtime/providers/langgraph.py +0 -0
- {swytchcode_runtime-1.0.0 → swytchcode_runtime-1.0.2}/swytchcode_runtime.egg-info/dependency_links.txt +0 -0
- {swytchcode_runtime-1.0.0 → swytchcode_runtime-1.0.2}/swytchcode_runtime.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: swytchcode-runtime
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.2
|
|
4
4
|
Summary: Thin runtime wrapper around the Swytchcode CLI
|
|
5
5
|
Author: Swytchcode
|
|
6
6
|
License: MIT
|
|
@@ -147,39 +147,74 @@ def create_account(input: dict) -> any:
|
|
|
147
147
|
|
|
148
148
|
## Agentic workflows (framework integrations)
|
|
149
149
|
|
|
150
|
-
|
|
151
|
-
into the native tool objects each agent framework expects. The Swytchcode part is the same
|
|
152
|
-
two lines regardless of framework:
|
|
150
|
+
For full, production-ready examples across all major frameworks, check out the [Swytchcode Examples Repository](https://github.com/swytchcodehq/swytchcode-examples).
|
|
153
151
|
|
|
152
|
+
On top of `exec`, the runtime exposes a small agentic surface that turns Swytchcode tools into the native tool objects each agent framework expects.
|
|
153
|
+
|
|
154
|
+
### Quickstart: Anthropic SDK
|
|
155
|
+
|
|
156
|
+
Here is a clean example of building a simple agent using the Anthropic SDK. We use `python-dotenv` to load environment variables (like `ANTHROPIC_API_KEY`).
|
|
157
|
+
|
|
158
|
+
**Installation:**
|
|
159
|
+
```bash
|
|
160
|
+
pip install swytchcode-runtime anthropic python-dotenv
|
|
161
|
+
```
|
|
162
|
+
*(Note: You only need to install the SDK for the framework you are actually using. You **do not** need to install `openai-agents` or `langchain` if you are only using Anthropic. The `swytchcode-runtime` isolates these dependencies via lazy loading.)*
|
|
163
|
+
|
|
164
|
+
**Example:**
|
|
154
165
|
```python
|
|
166
|
+
import os
|
|
167
|
+
from dotenv import load_dotenv
|
|
168
|
+
import anthropic
|
|
155
169
|
from swytchcode_runtime import Swytchcode
|
|
156
170
|
from swytchcode_runtime.providers.anthropic import AnthropicProvider
|
|
157
171
|
|
|
158
|
-
|
|
159
|
-
|
|
172
|
+
load_dotenv() # Loads .env automatically
|
|
173
|
+
|
|
174
|
+
def run_agent():
|
|
175
|
+
client = anthropic.Anthropic()
|
|
176
|
+
|
|
177
|
+
# 1. Initialize Swytchcode with the Anthropic provider
|
|
178
|
+
swx = Swytchcode(provider=AnthropicProvider())
|
|
179
|
+
|
|
180
|
+
# 2. Fetch the tools you want your agent to use (e.g., Stripe tools)
|
|
181
|
+
tools = swx.tools.get(toolkits=["stripe"])
|
|
182
|
+
|
|
183
|
+
# 3. Pass them to Claude
|
|
184
|
+
response = client.messages.create(
|
|
185
|
+
model="claude-3-5-sonnet-latest",
|
|
186
|
+
max_tokens=1024,
|
|
187
|
+
tools=tools,
|
|
188
|
+
messages=[{"role": "user", "content": "Refund charge ch_123 for $20."}],
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
print(response)
|
|
192
|
+
|
|
193
|
+
if __name__ == "__main__":
|
|
194
|
+
run_agent()
|
|
160
195
|
```
|
|
161
196
|
|
|
162
|
-
### Selecting tools
|
|
197
|
+
### Selecting tools - `swx.tools.get(...)`
|
|
163
198
|
|
|
164
199
|
Pass exactly one selector; IDs resolve against your local Swytchcode state and remote search:
|
|
165
200
|
|
|
166
|
-
- `toolkits=["stripe"]`
|
|
167
|
-
- `tools=["charges.charge.create"]`
|
|
168
|
-
- `search="refund a charge"`
|
|
201
|
+
- `toolkits=["stripe"]` - every enabled tool whose integration matches a toolkit.
|
|
202
|
+
- `tools=["charges.charge.create"]` - explicit canonical IDs.
|
|
203
|
+
- `search="refund a charge"` - natural-language discovery (via `swytchcode discover`).
|
|
169
204
|
|
|
170
|
-
Each returned tool carries a **full input schema**
|
|
171
|
-
only the truly-required ones marked `required`
|
|
205
|
+
Each returned tool carries a **full input schema** - every field is surfaced to the model, with
|
|
206
|
+
only the truly-required ones marked `required` - and an `execute` callback that runs `swytchcode
|
|
172
207
|
exec` for you (empty optional values are stripped before the call so APIs like Stripe don't reject them).
|
|
173
208
|
|
|
174
209
|
### Supported providers
|
|
175
210
|
|
|
176
211
|
| Framework | Import | Who runs the tool loop |
|
|
177
212
|
|-----------|--------|------------------------|
|
|
178
|
-
| Anthropic Claude | `providers.anthropic
|
|
179
|
-
| OpenAI Agents SDK | `providers.openai_agents
|
|
180
|
-
| Vercel AI SDK | `providers.vercel
|
|
181
|
-
| LangGraph | `providers.langgraph
|
|
182
|
-
| CrewAI | `providers.crewai
|
|
213
|
+
| Anthropic Claude | `from swytchcode_runtime.providers.anthropic import AnthropicProvider` | you (Messages API + `swx.handle_tool_calls`) |
|
|
214
|
+
| OpenAI Agents SDK | `from swytchcode_runtime.providers.openai_agents import OpenAIAgentsProvider` | the SDK |
|
|
215
|
+
| Vercel AI SDK | `from swytchcode_runtime.providers.vercel import VercelProvider` | the SDK |
|
|
216
|
+
| LangGraph | `from swytchcode_runtime.providers.langgraph import LangGraphProvider` | the prebuilt agent |
|
|
217
|
+
| CrewAI | `from swytchcode_runtime.providers.crewai import CrewAIProvider` | the crew |
|
|
183
218
|
|
|
184
219
|
### Non-agentic APIs (Anthropic Messages)
|
|
185
220
|
|
|
@@ -129,39 +129,74 @@ def create_account(input: dict) -> any:
|
|
|
129
129
|
|
|
130
130
|
## Agentic workflows (framework integrations)
|
|
131
131
|
|
|
132
|
-
|
|
133
|
-
into the native tool objects each agent framework expects. The Swytchcode part is the same
|
|
134
|
-
two lines regardless of framework:
|
|
132
|
+
For full, production-ready examples across all major frameworks, check out the [Swytchcode Examples Repository](https://github.com/swytchcodehq/swytchcode-examples).
|
|
135
133
|
|
|
134
|
+
On top of `exec`, the runtime exposes a small agentic surface that turns Swytchcode tools into the native tool objects each agent framework expects.
|
|
135
|
+
|
|
136
|
+
### Quickstart: Anthropic SDK
|
|
137
|
+
|
|
138
|
+
Here is a clean example of building a simple agent using the Anthropic SDK. We use `python-dotenv` to load environment variables (like `ANTHROPIC_API_KEY`).
|
|
139
|
+
|
|
140
|
+
**Installation:**
|
|
141
|
+
```bash
|
|
142
|
+
pip install swytchcode-runtime anthropic python-dotenv
|
|
143
|
+
```
|
|
144
|
+
*(Note: You only need to install the SDK for the framework you are actually using. You **do not** need to install `openai-agents` or `langchain` if you are only using Anthropic. The `swytchcode-runtime` isolates these dependencies via lazy loading.)*
|
|
145
|
+
|
|
146
|
+
**Example:**
|
|
136
147
|
```python
|
|
148
|
+
import os
|
|
149
|
+
from dotenv import load_dotenv
|
|
150
|
+
import anthropic
|
|
137
151
|
from swytchcode_runtime import Swytchcode
|
|
138
152
|
from swytchcode_runtime.providers.anthropic import AnthropicProvider
|
|
139
153
|
|
|
140
|
-
|
|
141
|
-
|
|
154
|
+
load_dotenv() # Loads .env automatically
|
|
155
|
+
|
|
156
|
+
def run_agent():
|
|
157
|
+
client = anthropic.Anthropic()
|
|
158
|
+
|
|
159
|
+
# 1. Initialize Swytchcode with the Anthropic provider
|
|
160
|
+
swx = Swytchcode(provider=AnthropicProvider())
|
|
161
|
+
|
|
162
|
+
# 2. Fetch the tools you want your agent to use (e.g., Stripe tools)
|
|
163
|
+
tools = swx.tools.get(toolkits=["stripe"])
|
|
164
|
+
|
|
165
|
+
# 3. Pass them to Claude
|
|
166
|
+
response = client.messages.create(
|
|
167
|
+
model="claude-3-5-sonnet-latest",
|
|
168
|
+
max_tokens=1024,
|
|
169
|
+
tools=tools,
|
|
170
|
+
messages=[{"role": "user", "content": "Refund charge ch_123 for $20."}],
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
print(response)
|
|
174
|
+
|
|
175
|
+
if __name__ == "__main__":
|
|
176
|
+
run_agent()
|
|
142
177
|
```
|
|
143
178
|
|
|
144
|
-
### Selecting tools
|
|
179
|
+
### Selecting tools - `swx.tools.get(...)`
|
|
145
180
|
|
|
146
181
|
Pass exactly one selector; IDs resolve against your local Swytchcode state and remote search:
|
|
147
182
|
|
|
148
|
-
- `toolkits=["stripe"]`
|
|
149
|
-
- `tools=["charges.charge.create"]`
|
|
150
|
-
- `search="refund a charge"`
|
|
183
|
+
- `toolkits=["stripe"]` - every enabled tool whose integration matches a toolkit.
|
|
184
|
+
- `tools=["charges.charge.create"]` - explicit canonical IDs.
|
|
185
|
+
- `search="refund a charge"` - natural-language discovery (via `swytchcode discover`).
|
|
151
186
|
|
|
152
|
-
Each returned tool carries a **full input schema**
|
|
153
|
-
only the truly-required ones marked `required`
|
|
187
|
+
Each returned tool carries a **full input schema** - every field is surfaced to the model, with
|
|
188
|
+
only the truly-required ones marked `required` - and an `execute` callback that runs `swytchcode
|
|
154
189
|
exec` for you (empty optional values are stripped before the call so APIs like Stripe don't reject them).
|
|
155
190
|
|
|
156
191
|
### Supported providers
|
|
157
192
|
|
|
158
193
|
| Framework | Import | Who runs the tool loop |
|
|
159
194
|
|-----------|--------|------------------------|
|
|
160
|
-
| Anthropic Claude | `providers.anthropic
|
|
161
|
-
| OpenAI Agents SDK | `providers.openai_agents
|
|
162
|
-
| Vercel AI SDK | `providers.vercel
|
|
163
|
-
| LangGraph | `providers.langgraph
|
|
164
|
-
| CrewAI | `providers.crewai
|
|
195
|
+
| Anthropic Claude | `from swytchcode_runtime.providers.anthropic import AnthropicProvider` | you (Messages API + `swx.handle_tool_calls`) |
|
|
196
|
+
| OpenAI Agents SDK | `from swytchcode_runtime.providers.openai_agents import OpenAIAgentsProvider` | the SDK |
|
|
197
|
+
| Vercel AI SDK | `from swytchcode_runtime.providers.vercel import VercelProvider` | the SDK |
|
|
198
|
+
| LangGraph | `from swytchcode_runtime.providers.langgraph import LangGraphProvider` | the prebuilt agent |
|
|
199
|
+
| CrewAI | `from swytchcode_runtime.providers.crewai import CrewAIProvider` | the crew |
|
|
165
200
|
|
|
166
201
|
### Non-agentic APIs (Anthropic Messages)
|
|
167
202
|
|
|
@@ -0,0 +1,30 @@
|
|
|
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
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"exec",
|
|
9
|
+
"Swytchcode",
|
|
10
|
+
"SwytchcodeError",
|
|
11
|
+
"is_swytchcode_error",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def __getattr__(name):
|
|
16
|
+
"""Lazy provider imports for backward compatibility."""
|
|
17
|
+
_provider_map = {
|
|
18
|
+
"AnthropicProvider": ("providers.anthropic", "AnthropicProvider"),
|
|
19
|
+
"OpenAIAgentsProvider": ("providers.openai_agents", "OpenAIAgentsProvider"),
|
|
20
|
+
"VercelProvider": ("providers.vercel", "VercelProvider"),
|
|
21
|
+
"LangGraphProvider": ("providers.langgraph", "LangGraphProvider"),
|
|
22
|
+
"CrewAIProvider": ("providers.crewai", "CrewAIProvider"),
|
|
23
|
+
}
|
|
24
|
+
if name in _provider_map:
|
|
25
|
+
module_path, attr = _provider_map[name]
|
|
26
|
+
import importlib
|
|
27
|
+
|
|
28
|
+
mod = importlib.import_module(f".{module_path}", __package__)
|
|
29
|
+
return getattr(mod, attr)
|
|
30
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
@@ -0,0 +1,243 @@
|
|
|
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
|
+
import re
|
|
7
|
+
import hashlib
|
|
8
|
+
|
|
9
|
+
from . import discover as _discover, schema as _schema, manage as _manage
|
|
10
|
+
from .exec import exec_ as _exec
|
|
11
|
+
from .providers.base import Provider, Tool
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
MAX_TOOL_NAME_LEN = 64 # OpenAI and Anthropic strict limit
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _make_alias(cid: str, taken: dict[str, str]) -> str:
|
|
18
|
+
base = re.sub(r"[^a-zA-Z0-9_-]", "_", cid)
|
|
19
|
+
existing = taken.get(base)
|
|
20
|
+
needs_hash = len(base) > MAX_TOOL_NAME_LEN or (existing and existing != cid)
|
|
21
|
+
|
|
22
|
+
if not needs_hash:
|
|
23
|
+
return base
|
|
24
|
+
|
|
25
|
+
h = hashlib.sha1(cid.encode("utf-8")).hexdigest()[:6]
|
|
26
|
+
keep = MAX_TOOL_NAME_LEN - 1 - len(h)
|
|
27
|
+
return base[:keep] + "_" + h
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _toolkit_matches(toolkit: str, integration: str) -> bool:
|
|
31
|
+
"""Case-insensitive match of a toolkit name against an integration string.
|
|
32
|
+
|
|
33
|
+
integration has the shape "ProjectDisplayName.library_slug@version"
|
|
34
|
+
(e.g. "GitHub.github@1.1.4"). We compare against both the project name
|
|
35
|
+
and the library slug so that ``toolkits=["github"]`` matches regardless
|
|
36
|
+
of the capitalised display name.
|
|
37
|
+
"""
|
|
38
|
+
tk = toolkit.lower()
|
|
39
|
+
# Strip version: "GitHub.github@1.1.4" → "GitHub.github"
|
|
40
|
+
at_idx = integration.find("@")
|
|
41
|
+
prefix = integration[:at_idx] if at_idx != -1 else integration
|
|
42
|
+
# Split project.library: "GitHub.github" → ("GitHub", "github")
|
|
43
|
+
dot_idx = prefix.find(".")
|
|
44
|
+
if dot_idx != -1:
|
|
45
|
+
project = prefix[:dot_idx].lower()
|
|
46
|
+
lib_slug = prefix[dot_idx + 1 :].lower()
|
|
47
|
+
else:
|
|
48
|
+
project = prefix.lower()
|
|
49
|
+
lib_slug = ""
|
|
50
|
+
return tk == project or tk == lib_slug or tk == prefix.lower()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _strip_empty(obj: Any) -> Any:
|
|
54
|
+
"""Recursively drop keys whose value is None or an empty string ("").
|
|
55
|
+
|
|
56
|
+
With 'expose all fields', agents often fill unused optional fields with "",
|
|
57
|
+
which APIs like Stripe reject ("empty values are an attempt to unset").
|
|
58
|
+
Only None and "" are dropped - 0, False, [], {} are preserved as meaningful.
|
|
59
|
+
"""
|
|
60
|
+
if isinstance(obj, dict):
|
|
61
|
+
out = {}
|
|
62
|
+
for k, v in obj.items():
|
|
63
|
+
if v is None or (isinstance(v, str) and v == ""):
|
|
64
|
+
continue
|
|
65
|
+
out[k] = _strip_empty(v)
|
|
66
|
+
return out
|
|
67
|
+
if isinstance(obj, list):
|
|
68
|
+
return [_strip_empty(v) for v in obj]
|
|
69
|
+
return obj
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _split_by_location(inputs: Any, flat_args: dict) -> dict:
|
|
73
|
+
"""Route flat args into body/params based on LOCATION metadata from wrekenfile."""
|
|
74
|
+
body = {}
|
|
75
|
+
params = {}
|
|
76
|
+
# inputs is the raw wrekenfile list-of-single-key-dicts with LOCATION, or a JSON schema dict
|
|
77
|
+
locations = {}
|
|
78
|
+
|
|
79
|
+
if isinstance(inputs, list):
|
|
80
|
+
for item in inputs:
|
|
81
|
+
if isinstance(item, dict):
|
|
82
|
+
for name, spec in item.items():
|
|
83
|
+
if isinstance(spec, dict):
|
|
84
|
+
loc = str(
|
|
85
|
+
spec.get("LOCATION", spec.get("location", "body"))
|
|
86
|
+
).lower()
|
|
87
|
+
locations[name] = loc
|
|
88
|
+
elif isinstance(inputs, dict) and isinstance(inputs.get("properties"), dict):
|
|
89
|
+
for name, spec in inputs["properties"].items():
|
|
90
|
+
if isinstance(spec, dict):
|
|
91
|
+
loc = str(spec.get("LOCATION", spec.get("location", "body"))).lower()
|
|
92
|
+
locations[name] = loc
|
|
93
|
+
|
|
94
|
+
for key, val in flat_args.items():
|
|
95
|
+
loc = locations.get(key, "body")
|
|
96
|
+
if loc in ("path", "query"):
|
|
97
|
+
params[key] = val
|
|
98
|
+
else:
|
|
99
|
+
body[key] = val
|
|
100
|
+
|
|
101
|
+
result = {}
|
|
102
|
+
if body:
|
|
103
|
+
result["body"] = body
|
|
104
|
+
if params:
|
|
105
|
+
result["params"] = params
|
|
106
|
+
return result if result else {"body": flat_args}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class _Tools:
|
|
110
|
+
def __init__(self, client: "Swytchcode"):
|
|
111
|
+
self._c = client
|
|
112
|
+
# Maps a sanitized tool name (dots -> underscores) back to its canonical
|
|
113
|
+
# ID, populated as tools are built. Used to reverse names in
|
|
114
|
+
# handle_tool_calls without a lossy "_"->"." string replace.
|
|
115
|
+
self._name_to_cid: dict[str, str] = {}
|
|
116
|
+
self._cid_to_inputs: dict[str, Any] = {}
|
|
117
|
+
|
|
118
|
+
def get(self, *, toolkits=None, tools=None, search=None):
|
|
119
|
+
# Sort canonical IDs lexicographically before alias assignment.
|
|
120
|
+
# This ensures deterministic assignment order across runs, guaranteeing the
|
|
121
|
+
# same canonical ID always receives the exact same alias (and same hash if colliding).
|
|
122
|
+
ids = sorted(self._ids(toolkits, tools, search))
|
|
123
|
+
neutral = [self._tool(cid) for cid in ids]
|
|
124
|
+
p = self._c.provider
|
|
125
|
+
return p.format_tools(neutral) if p else neutral
|
|
126
|
+
|
|
127
|
+
def execute(self, canonical_id: str, args: dict, **options) -> Any:
|
|
128
|
+
final_args = dict(args)
|
|
129
|
+
|
|
130
|
+
# If args are flat (no body/params top-level keys), wrap them in body
|
|
131
|
+
# as expected by the Swytchcode CLI kernel (like in run-workflow.js)
|
|
132
|
+
if "body" not in final_args and "params" not in final_args:
|
|
133
|
+
raw_inputs = options.pop("_raw_inputs", None)
|
|
134
|
+
if raw_inputs is not None:
|
|
135
|
+
final_args = _split_by_location(raw_inputs, final_args)
|
|
136
|
+
else:
|
|
137
|
+
final_args = {"body": final_args}
|
|
138
|
+
else:
|
|
139
|
+
options.pop("_raw_inputs", None)
|
|
140
|
+
|
|
141
|
+
# Drop empty optional fields (None/"") from body & params so values an
|
|
142
|
+
# agent over-filled don't reach the API (e.g. Stripe rejects customer="").
|
|
143
|
+
if isinstance(final_args.get("body"), (dict, list)):
|
|
144
|
+
final_args["body"] = _strip_empty(final_args["body"])
|
|
145
|
+
if isinstance(final_args.get("params"), (dict, list)):
|
|
146
|
+
final_args["params"] = _strip_empty(final_args["params"])
|
|
147
|
+
# Forward exec options (dry_run, raw, allow_raw, cwd, env) to the CLI.
|
|
148
|
+
return _exec(canonical_id, final_args, **options)
|
|
149
|
+
|
|
150
|
+
def _tool(self, cid: str) -> Tool:
|
|
151
|
+
m = _discover.info(cid)
|
|
152
|
+
if not m or not m.get("inputs"):
|
|
153
|
+
raise ValueError(
|
|
154
|
+
f"Tool discovery failed for {cid}: Invalid or missing Wrekenfile schema"
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
name = _make_alias(cid, self._name_to_cid)
|
|
158
|
+
|
|
159
|
+
self._name_to_cid[name] = cid
|
|
160
|
+
raw_inputs = m.get("inputs")
|
|
161
|
+
self._cid_to_inputs[cid] = raw_inputs
|
|
162
|
+
return Tool(
|
|
163
|
+
canonical_id=cid,
|
|
164
|
+
name=name,
|
|
165
|
+
description=m.get("summary") or m.get("description") or cid,
|
|
166
|
+
input_schema=_schema.simplify(raw_inputs),
|
|
167
|
+
execute=lambda args, _c=cid, _inputs=raw_inputs: self.execute(
|
|
168
|
+
_c, args, _raw_inputs=_inputs
|
|
169
|
+
),
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
def _ids(self, toolkits, tools, search) -> list[str]:
|
|
173
|
+
# Implementation for Phase 1: resolve against CLI local state
|
|
174
|
+
if tools:
|
|
175
|
+
return tools
|
|
176
|
+
if search:
|
|
177
|
+
# Mirror the TS runtime: resolve a natural-language search to
|
|
178
|
+
# canonical IDs via the CLI's discover/search.
|
|
179
|
+
return [
|
|
180
|
+
t["canonical_id"]
|
|
181
|
+
for t in _discover.search(search)
|
|
182
|
+
if t.get("canonical_id")
|
|
183
|
+
]
|
|
184
|
+
if toolkits:
|
|
185
|
+
# Resolve toolkits against local tooling.json instead of global search
|
|
186
|
+
res = _manage.list_tools("tooling")
|
|
187
|
+
found = []
|
|
188
|
+
for m in res.get("methods", []):
|
|
189
|
+
integration = m.get("integration", "")
|
|
190
|
+
cid = m.get("canonical_id")
|
|
191
|
+
if not cid:
|
|
192
|
+
continue
|
|
193
|
+
for tk in toolkits:
|
|
194
|
+
if _toolkit_matches(tk, integration):
|
|
195
|
+
found.append(cid)
|
|
196
|
+
break
|
|
197
|
+
return found
|
|
198
|
+
return []
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
class Swytchcode:
|
|
202
|
+
def __init__(self, provider: Optional[Provider] = None):
|
|
203
|
+
self.provider = provider
|
|
204
|
+
self.tools = _Tools(self)
|
|
205
|
+
|
|
206
|
+
def handle_tool_calls(
|
|
207
|
+
self, response: Any, timeout: float | None = None
|
|
208
|
+
) -> list[dict]:
|
|
209
|
+
"""Helper to execute tools for non-agentic APIs like Anthropic."""
|
|
210
|
+
results = []
|
|
211
|
+
for block in getattr(response, "content", []):
|
|
212
|
+
if getattr(block, "type", "") == "tool_use":
|
|
213
|
+
# Reverse the sanitized name via the map built in _tool(). A plain
|
|
214
|
+
# "_"->"." replace would corrupt canonical IDs whose segments
|
|
215
|
+
# contain underscores (e.g. stripe.create_payment).
|
|
216
|
+
cid = self.tools._name_to_cid.get(block.name) or block.name.replace(
|
|
217
|
+
"_", "."
|
|
218
|
+
)
|
|
219
|
+
raw_inputs = self.tools._cid_to_inputs.get(cid, {})
|
|
220
|
+
# Isolate failures per block so one failing tool doesn't drop the
|
|
221
|
+
# results for the other tool_use blocks in the same turn.
|
|
222
|
+
try:
|
|
223
|
+
content = str(
|
|
224
|
+
self.tools.execute(
|
|
225
|
+
cid,
|
|
226
|
+
getattr(block, "input", {}),
|
|
227
|
+
_raw_inputs=raw_inputs,
|
|
228
|
+
timeout=timeout,
|
|
229
|
+
)
|
|
230
|
+
)
|
|
231
|
+
is_error = False
|
|
232
|
+
except Exception as e:
|
|
233
|
+
content = f"Error executing {cid}: {e}"
|
|
234
|
+
is_error = True
|
|
235
|
+
results.append(
|
|
236
|
+
{
|
|
237
|
+
"type": "tool_result",
|
|
238
|
+
"tool_use_id": block.id,
|
|
239
|
+
"content": content,
|
|
240
|
+
"is_error": is_error,
|
|
241
|
+
}
|
|
242
|
+
)
|
|
243
|
+
return results
|
|
@@ -59,6 +59,7 @@ def exec_( # noqa: A001 - shadowing intentional for API consistency with JS/Go
|
|
|
59
59
|
raw: bool = False,
|
|
60
60
|
dry_run: bool = False,
|
|
61
61
|
allow_raw: bool = False,
|
|
62
|
+
timeout: float | None = None,
|
|
62
63
|
) -> Any:
|
|
63
64
|
"""
|
|
64
65
|
Run swytchcode exec <canonical_id> with optional JSON args on stdin.
|
|
@@ -80,6 +81,7 @@ def exec_( # noqa: A001 - shadowing intentional for API consistency with JS/Go
|
|
|
80
81
|
raw: If True, use --raw and return stdout as a string.
|
|
81
82
|
dry_run: If True, pass --dry-run; request details are output instead of calling the server.
|
|
82
83
|
allow_raw: If True, pass --allow-raw; required for executing raw methods (kernel default is disabled).
|
|
84
|
+
timeout: Optional timeout in seconds for the subprocess.
|
|
83
85
|
|
|
84
86
|
Returns:
|
|
85
87
|
Parsed result (any) or raw string when raw is True.
|
|
@@ -114,8 +116,12 @@ def exec_( # noqa: A001 - shadowing intentional for API consistency with JS/Go
|
|
|
114
116
|
capture_output=True,
|
|
115
117
|
cwd=cwd or os.getcwd(),
|
|
116
118
|
env=run_env,
|
|
117
|
-
timeout=
|
|
119
|
+
timeout=timeout,
|
|
118
120
|
)
|
|
121
|
+
except subprocess.TimeoutExpired as e:
|
|
122
|
+
raise SwytchcodeError(
|
|
123
|
+
f"swytchcode exec timed out after {timeout} seconds", e
|
|
124
|
+
) from e
|
|
119
125
|
except FileNotFoundError as e:
|
|
120
126
|
hint = (
|
|
121
127
|
f"using binary at {bin_path!r}"
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Providers for integrating Swytchcode tools into various AI frameworks."""
|
|
2
|
+
|
|
3
|
+
__all__ = [
|
|
4
|
+
"AnthropicProvider",
|
|
5
|
+
"CrewAIProvider",
|
|
6
|
+
"LangGraphProvider",
|
|
7
|
+
"OpenAIAgentsProvider",
|
|
8
|
+
"VercelProvider",
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def __getattr__(name):
|
|
13
|
+
"""Lazy provider imports for backward compatibility."""
|
|
14
|
+
_provider_map = {
|
|
15
|
+
"AnthropicProvider": "anthropic",
|
|
16
|
+
"OpenAIAgentsProvider": "openai_agents",
|
|
17
|
+
"VercelProvider": "vercel",
|
|
18
|
+
"LangGraphProvider": "langgraph",
|
|
19
|
+
"CrewAIProvider": "crewai",
|
|
20
|
+
}
|
|
21
|
+
if name in _provider_map:
|
|
22
|
+
module_name = _provider_map[name]
|
|
23
|
+
import importlib
|
|
24
|
+
|
|
25
|
+
mod = importlib.import_module(f".{module_name}", __package__)
|
|
26
|
+
return getattr(mod, name)
|
|
27
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
@@ -6,7 +6,7 @@ the Tool itself via its `execute` closure (which runs the Swytchcode CLI): agent
|
|
|
6
6
|
frameworks (OpenAI Agents, Vercel, LangGraph, CrewAI) invoke that closure directly
|
|
7
7
|
during their own loop, while non-agentic APIs (Anthropic) run it through
|
|
8
8
|
`Swytchcode.handle_tool_calls`. Note this is deliberately NOT Composio's two-class
|
|
9
|
-
agentic/non-agentic split
|
|
9
|
+
agentic/non-agentic split - execution lives on the tool, not an injected function.
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
12
|
from __future__ import annotations
|
{swytchcode_runtime-1.0.0 → swytchcode_runtime-1.0.2}/swytchcode_runtime/providers/openai_agents.py
RENAMED
|
@@ -22,7 +22,7 @@ class OpenAIAgentsProvider(Provider):
|
|
|
22
22
|
return await asyncio.to_thread(tool.execute, kwargs)
|
|
23
23
|
|
|
24
24
|
return FunctionTool(
|
|
25
|
-
name=str(tool.name)
|
|
25
|
+
name=str(tool.name),
|
|
26
26
|
description=str(tool.description),
|
|
27
27
|
params_json_schema=tool.input_schema,
|
|
28
28
|
on_invoke_tool=_on_invoke,
|
{swytchcode_runtime-1.0.0 → swytchcode_runtime-1.0.2}/swytchcode_runtime/providers/vercel.py
RENAMED
|
@@ -15,10 +15,10 @@ class VercelProvider(Provider):
|
|
|
15
15
|
|
|
16
16
|
# The handler MUST be synchronous. ai_sdk's sync generate_text() calls the
|
|
17
17
|
# tool handler without awaiting it, so an async handler is silently never
|
|
18
|
-
# awaited
|
|
18
|
+
# awaited - the tool never runs and the model hallucinates success. A sync
|
|
19
19
|
# handler returns a plain value that works in generate_text AND is fine for
|
|
20
20
|
# the async Tool.run path (which only awaits awaitables). Called as
|
|
21
|
-
# handler(**kwargs)
|
|
21
|
+
# handler(**kwargs) - arguments by field name, never a positional dict.
|
|
22
22
|
def _execute(**kwargs):
|
|
23
23
|
return tool.execute(kwargs)
|
|
24
24
|
|
|
@@ -36,15 +36,18 @@ def simplify(inputs: Any) -> dict:
|
|
|
36
36
|
props[name] = {"type": t, "description": spec.get("DESC", "")}
|
|
37
37
|
|
|
38
38
|
req = spec.get("REQUIRED", False)
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
loc = str(spec.get("LOCATION", spec.get("location", ""))).lower()
|
|
40
|
+
is_required = (
|
|
41
|
+
loc == "path"
|
|
42
|
+
or req is True
|
|
43
|
+
or (isinstance(req, str) and req.strip().lower() == "true")
|
|
41
44
|
)
|
|
42
45
|
if is_required:
|
|
43
46
|
required.append(name)
|
|
44
47
|
|
|
45
48
|
# Composio-style rule: expose ALL fields to the model and list only the
|
|
46
49
|
# truly-required ones in `required`. A required-only approach hid optional
|
|
47
|
-
# fields
|
|
50
|
+
# fields - which left all-optional tools (e.g. Stripe) with an empty schema
|
|
48
51
|
# so the model called them with no arguments, and blinded the model to
|
|
49
52
|
# optional fields on tools that do have some required ones.
|
|
50
53
|
return {"type": "object", "properties": props, "required": required}
|
|
@@ -61,12 +64,13 @@ def simplify(inputs: Any) -> dict:
|
|
|
61
64
|
# original required list only for the `required` key so optional/nested
|
|
62
65
|
# fields stay optional instead of being dropped or forced required.
|
|
63
66
|
for name, spec in props.items():
|
|
64
|
-
if (
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
spec
|
|
67
|
+
if isinstance(spec, dict):
|
|
68
|
+
loc = str(spec.get("LOCATION", spec.get("location", ""))).lower()
|
|
69
|
+
if loc == "path" and name not in required:
|
|
70
|
+
required.append(name)
|
|
71
|
+
|
|
72
|
+
if spec.get("type") == "object" and "properties" in spec:
|
|
73
|
+
spec = simplify(spec) # recurse into nested objects
|
|
70
74
|
keep[name] = spec
|
|
71
75
|
|
|
72
76
|
return {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: swytchcode-runtime
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.2
|
|
4
4
|
Summary: Thin runtime wrapper around the Swytchcode CLI
|
|
5
5
|
Author: Swytchcode
|
|
6
6
|
License: MIT
|
|
@@ -147,39 +147,74 @@ def create_account(input: dict) -> any:
|
|
|
147
147
|
|
|
148
148
|
## Agentic workflows (framework integrations)
|
|
149
149
|
|
|
150
|
-
|
|
151
|
-
into the native tool objects each agent framework expects. The Swytchcode part is the same
|
|
152
|
-
two lines regardless of framework:
|
|
150
|
+
For full, production-ready examples across all major frameworks, check out the [Swytchcode Examples Repository](https://github.com/swytchcodehq/swytchcode-examples).
|
|
153
151
|
|
|
152
|
+
On top of `exec`, the runtime exposes a small agentic surface that turns Swytchcode tools into the native tool objects each agent framework expects.
|
|
153
|
+
|
|
154
|
+
### Quickstart: Anthropic SDK
|
|
155
|
+
|
|
156
|
+
Here is a clean example of building a simple agent using the Anthropic SDK. We use `python-dotenv` to load environment variables (like `ANTHROPIC_API_KEY`).
|
|
157
|
+
|
|
158
|
+
**Installation:**
|
|
159
|
+
```bash
|
|
160
|
+
pip install swytchcode-runtime anthropic python-dotenv
|
|
161
|
+
```
|
|
162
|
+
*(Note: You only need to install the SDK for the framework you are actually using. You **do not** need to install `openai-agents` or `langchain` if you are only using Anthropic. The `swytchcode-runtime` isolates these dependencies via lazy loading.)*
|
|
163
|
+
|
|
164
|
+
**Example:**
|
|
154
165
|
```python
|
|
166
|
+
import os
|
|
167
|
+
from dotenv import load_dotenv
|
|
168
|
+
import anthropic
|
|
155
169
|
from swytchcode_runtime import Swytchcode
|
|
156
170
|
from swytchcode_runtime.providers.anthropic import AnthropicProvider
|
|
157
171
|
|
|
158
|
-
|
|
159
|
-
|
|
172
|
+
load_dotenv() # Loads .env automatically
|
|
173
|
+
|
|
174
|
+
def run_agent():
|
|
175
|
+
client = anthropic.Anthropic()
|
|
176
|
+
|
|
177
|
+
# 1. Initialize Swytchcode with the Anthropic provider
|
|
178
|
+
swx = Swytchcode(provider=AnthropicProvider())
|
|
179
|
+
|
|
180
|
+
# 2. Fetch the tools you want your agent to use (e.g., Stripe tools)
|
|
181
|
+
tools = swx.tools.get(toolkits=["stripe"])
|
|
182
|
+
|
|
183
|
+
# 3. Pass them to Claude
|
|
184
|
+
response = client.messages.create(
|
|
185
|
+
model="claude-3-5-sonnet-latest",
|
|
186
|
+
max_tokens=1024,
|
|
187
|
+
tools=tools,
|
|
188
|
+
messages=[{"role": "user", "content": "Refund charge ch_123 for $20."}],
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
print(response)
|
|
192
|
+
|
|
193
|
+
if __name__ == "__main__":
|
|
194
|
+
run_agent()
|
|
160
195
|
```
|
|
161
196
|
|
|
162
|
-
### Selecting tools
|
|
197
|
+
### Selecting tools - `swx.tools.get(...)`
|
|
163
198
|
|
|
164
199
|
Pass exactly one selector; IDs resolve against your local Swytchcode state and remote search:
|
|
165
200
|
|
|
166
|
-
- `toolkits=["stripe"]`
|
|
167
|
-
- `tools=["charges.charge.create"]`
|
|
168
|
-
- `search="refund a charge"`
|
|
201
|
+
- `toolkits=["stripe"]` - every enabled tool whose integration matches a toolkit.
|
|
202
|
+
- `tools=["charges.charge.create"]` - explicit canonical IDs.
|
|
203
|
+
- `search="refund a charge"` - natural-language discovery (via `swytchcode discover`).
|
|
169
204
|
|
|
170
|
-
Each returned tool carries a **full input schema**
|
|
171
|
-
only the truly-required ones marked `required`
|
|
205
|
+
Each returned tool carries a **full input schema** - every field is surfaced to the model, with
|
|
206
|
+
only the truly-required ones marked `required` - and an `execute` callback that runs `swytchcode
|
|
172
207
|
exec` for you (empty optional values are stripped before the call so APIs like Stripe don't reject them).
|
|
173
208
|
|
|
174
209
|
### Supported providers
|
|
175
210
|
|
|
176
211
|
| Framework | Import | Who runs the tool loop |
|
|
177
212
|
|-----------|--------|------------------------|
|
|
178
|
-
| Anthropic Claude | `providers.anthropic
|
|
179
|
-
| OpenAI Agents SDK | `providers.openai_agents
|
|
180
|
-
| Vercel AI SDK | `providers.vercel
|
|
181
|
-
| LangGraph | `providers.langgraph
|
|
182
|
-
| CrewAI | `providers.crewai
|
|
213
|
+
| Anthropic Claude | `from swytchcode_runtime.providers.anthropic import AnthropicProvider` | you (Messages API + `swx.handle_tool_calls`) |
|
|
214
|
+
| OpenAI Agents SDK | `from swytchcode_runtime.providers.openai_agents import OpenAIAgentsProvider` | the SDK |
|
|
215
|
+
| Vercel AI SDK | `from swytchcode_runtime.providers.vercel import VercelProvider` | the SDK |
|
|
216
|
+
| LangGraph | `from swytchcode_runtime.providers.langgraph import LangGraphProvider` | the prebuilt agent |
|
|
217
|
+
| CrewAI | `from swytchcode_runtime.providers.crewai import CrewAIProvider` | the crew |
|
|
183
218
|
|
|
184
219
|
### Non-agentic APIs (Anthropic Messages)
|
|
185
220
|
|
{swytchcode_runtime-1.0.0 → swytchcode_runtime-1.0.2}/swytchcode_runtime.egg-info/SOURCES.txt
RENAMED
|
@@ -19,4 +19,6 @@ swytchcode_runtime/providers/base.py
|
|
|
19
19
|
swytchcode_runtime/providers/crewai.py
|
|
20
20
|
swytchcode_runtime/providers/langgraph.py
|
|
21
21
|
swytchcode_runtime/providers/openai_agents.py
|
|
22
|
-
swytchcode_runtime/providers/vercel.py
|
|
22
|
+
swytchcode_runtime/providers/vercel.py
|
|
23
|
+
tests/test_alias.py
|
|
24
|
+
tests/test_schema_routing.py
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from swytchcode_runtime.client import Swytchcode, _make_alias
|
|
3
|
+
from unittest.mock import patch
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TestAlias(unittest.TestCase):
|
|
7
|
+
@patch("swytchcode_runtime.discover.info")
|
|
8
|
+
@patch("swytchcode_runtime.discover.search")
|
|
9
|
+
def test_deterministic_alias(self, mock_search, mock_info):
|
|
10
|
+
long_id = "google_workspace_admin_directory_users_aliases_insert_extra_padding_to_exceed_limit"
|
|
11
|
+
mock_search.return_value = [{"canonical_id": long_id}]
|
|
12
|
+
mock_info.return_value = {
|
|
13
|
+
"inputs": {"email": {"TYPE": "STRING"}},
|
|
14
|
+
"summary": "Test tool",
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
client = Swytchcode()
|
|
18
|
+
tools = client.tools.get(search="test")
|
|
19
|
+
self.assertEqual(len(tools), 1)
|
|
20
|
+
alias = tools[0].name
|
|
21
|
+
|
|
22
|
+
self.assertLessEqual(len(alias), 64)
|
|
23
|
+
|
|
24
|
+
# Test determinism
|
|
25
|
+
client2 = Swytchcode()
|
|
26
|
+
tools2 = client2.tools.get(search="test")
|
|
27
|
+
self.assertEqual(tools2[0].name, alias)
|
|
28
|
+
|
|
29
|
+
# Test collision
|
|
30
|
+
taken = {}
|
|
31
|
+
alias1 = _make_alias("a.b", taken)
|
|
32
|
+
taken[alias1] = "a.b"
|
|
33
|
+
alias2 = _make_alias("a_b", taken) # Collides on sanitize
|
|
34
|
+
|
|
35
|
+
self.assertNotEqual(alias1, alias2)
|
|
36
|
+
|
|
37
|
+
import re
|
|
38
|
+
self.assertTrue(re.search(r"_[0-9a-f]{6}$", alias2) or re.search(r"_[0-9a-f]{6}$", alias1))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
if __name__ == "__main__":
|
|
42
|
+
unittest.main()
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from swytchcode_runtime.schema import simplify
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class TestSchemaRouting(unittest.TestCase):
|
|
6
|
+
def test_path_params_required(self):
|
|
7
|
+
raw_schema = [
|
|
8
|
+
{"userId": {"TYPE": "STRING", "LOCATION": "path", "DESC": "The user ID"}},
|
|
9
|
+
{"amount": {"TYPE": "INT", "LOCATION": "query"}},
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
simplified = simplify(raw_schema)
|
|
13
|
+
|
|
14
|
+
# Path parameters should be required
|
|
15
|
+
self.assertIn("userId", simplified["required"])
|
|
16
|
+
self.assertEqual(simplified["properties"]["userId"]["type"], "string")
|
|
17
|
+
self.assertEqual(simplified["properties"]["amount"]["type"], "integer")
|
|
18
|
+
|
|
19
|
+
def test_json_schema_path_params_required(self):
|
|
20
|
+
raw_schema = {
|
|
21
|
+
"type": "object",
|
|
22
|
+
"properties": {
|
|
23
|
+
"owner": {"type": "string", "location": "path"},
|
|
24
|
+
"repo": {"type": "string", "location": "path"},
|
|
25
|
+
"title": {"type": "string", "location": "body"},
|
|
26
|
+
},
|
|
27
|
+
}
|
|
28
|
+
simplified = simplify(raw_schema)
|
|
29
|
+
self.assertIn("owner", simplified["required"])
|
|
30
|
+
self.assertIn("repo", simplified["required"])
|
|
31
|
+
self.assertNotIn("title", simplified["required"])
|
|
32
|
+
|
|
33
|
+
def test_split_by_location_json_schema(self):
|
|
34
|
+
from swytchcode_runtime.client import _split_by_location
|
|
35
|
+
|
|
36
|
+
raw_schema = {
|
|
37
|
+
"type": "object",
|
|
38
|
+
"properties": {
|
|
39
|
+
"owner": {"type": "string", "location": "path"},
|
|
40
|
+
"title": {"type": "string", "location": "body"},
|
|
41
|
+
},
|
|
42
|
+
}
|
|
43
|
+
args = {"owner": "swytchcode", "title": "hello"}
|
|
44
|
+
result = _split_by_location(raw_schema, args)
|
|
45
|
+
self.assertEqual(result.get("params", {}).get("owner"), "swytchcode")
|
|
46
|
+
self.assertEqual(result.get("body", {}).get("title"), "hello")
|
|
47
|
+
|
|
48
|
+
def test_split_by_location_array_wreken_shape(self):
|
|
49
|
+
from swytchcode_runtime.client import _split_by_location
|
|
50
|
+
|
|
51
|
+
raw_schema = [
|
|
52
|
+
{"owner": {"TYPE": "STRING", "LOCATION": "path"}},
|
|
53
|
+
{"title": {"TYPE": "STRING", "LOCATION": "body"}},
|
|
54
|
+
]
|
|
55
|
+
args = {"owner": "swytchcode", "title": "hello"}
|
|
56
|
+
result = _split_by_location(raw_schema, args)
|
|
57
|
+
self.assertEqual(result.get("params", {}).get("owner"), "swytchcode")
|
|
58
|
+
self.assertEqual(result.get("body", {}).get("title"), "hello")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
if __name__ == "__main__":
|
|
62
|
+
unittest.main()
|
|
@@ -1,24 +0,0 @@
|
|
|
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
|
-
]
|
|
@@ -1,133 +0,0 @@
|
|
|
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
|
|
@@ -1,15 +0,0 @@
|
|
|
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
|
-
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{swytchcode_runtime-1.0.0 → swytchcode_runtime-1.0.2}/swytchcode_runtime/providers/anthropic.py
RENAMED
|
File without changes
|
{swytchcode_runtime-1.0.0 → swytchcode_runtime-1.0.2}/swytchcode_runtime/providers/crewai.py
RENAMED
|
File without changes
|
{swytchcode_runtime-1.0.0 → swytchcode_runtime-1.0.2}/swytchcode_runtime/providers/langgraph.py
RENAMED
|
File without changes
|
|
File without changes
|
{swytchcode_runtime-1.0.0 → swytchcode_runtime-1.0.2}/swytchcode_runtime.egg-info/top_level.txt
RENAMED
|
File without changes
|