stacklink-sdk 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
stacklink/tools.py ADDED
@@ -0,0 +1,144 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from typing import Any
5
+
6
+ from .types import Json, ToolSchemasResponse, ToolSearchResponse
7
+
8
+
9
+ class ToolsResource:
10
+ def __init__(self, client):
11
+ self.client = client
12
+
13
+ def search(self, body: dict[str, Any]) -> ToolSearchResponse:
14
+ return self.client.request("POST", "/v1/tools/search", json_body=body)
15
+
16
+ def search_for_session(self, session_id: str, body: dict[str, Any], *, cache: str = "default") -> ToolSearchResponse:
17
+ search_body = {
18
+ "responseMode": "compact",
19
+ "includeSchemas": False,
20
+ "includeRelatedTools": False,
21
+ "includeExecutionPlan": False,
22
+ **body,
23
+ }
24
+ payload = _server_cache_body(search_body, cache)
25
+ return self.client.cached_request(
26
+ f"session:{session_id}:search:{_stable_json(_without_cache_control(search_body))}",
27
+ "POST",
28
+ f"/v1/sessions/{session_id}/tools/search",
29
+ json_body=payload,
30
+ bypass_cache=cache == "bypass",
31
+ )
32
+
33
+ def schemas(self, body: dict[str, Any]) -> ToolSchemasResponse:
34
+ return self.client.request("POST", "/v1/tools/schemas", json_body=body)
35
+
36
+ def schemas_for_session(self, session_id: str, body: dict[str, Any], *, cache: str = "default") -> ToolSchemasResponse:
37
+ payload = _server_cache_body(body, cache)
38
+ return self.client.cached_request(
39
+ f"session:{session_id}:schemas:{_stable_json(_without_cache_control(body))}",
40
+ "POST",
41
+ f"/v1/sessions/{session_id}/tools/schemas",
42
+ json_body=payload,
43
+ bypass_cache=cache == "bypass",
44
+ )
45
+
46
+ def runtime_contracts(self, body: dict[str, Any]) -> dict[str, Any]:
47
+ return self.client.request("POST", "/v1/tools/runtime-contracts", json_body=body)
48
+
49
+ def runtime_contracts_for_session(self, session_id: str, body: dict[str, Any] | None = None) -> dict[str, Any]:
50
+ return self.client.request(
51
+ "POST",
52
+ f"/v1/sessions/{session_id}/tools/runtime-contracts",
53
+ json_body=body or {},
54
+ )
55
+
56
+ def prepare_runtime_for_session(
57
+ self,
58
+ session_id: str,
59
+ *,
60
+ toolkit_keys: list[str] | None = None,
61
+ tool_keys: list[str] | None = None,
62
+ include_planned: bool = False,
63
+ ) -> dict[str, Any]:
64
+ return self.runtime_contracts_for_session(session_id, {
65
+ **({"toolkitKeys": toolkit_keys} if toolkit_keys else {}),
66
+ **({"toolKeys": tool_keys} if tool_keys else {}),
67
+ "includePlanned": include_planned,
68
+ })
69
+
70
+ def prepare_for_session(
71
+ self,
72
+ session_id: str,
73
+ *,
74
+ intents: list[str],
75
+ include_schemas: bool = True,
76
+ limit_per_intent: int = 5,
77
+ include_planned: bool = False,
78
+ toolkit_key: str | None = None,
79
+ capability: str | None = None,
80
+ model: str | None = None,
81
+ cache: str = "default",
82
+ ) -> dict[str, Any]:
83
+ search = self.search_for_session(session_id, {
84
+ "responseMode": "compact",
85
+ "includeSchemas": False,
86
+ "includeRelatedTools": False,
87
+ "includeExecutionPlan": False,
88
+ "queries": [
89
+ {
90
+ "query": intent,
91
+ "limit": limit_per_intent,
92
+ **({"toolkitKey": toolkit_key} if toolkit_key else {}),
93
+ **({"capability": capability} if capability else {}),
94
+ }
95
+ for intent in intents
96
+ ],
97
+ "includePlanned": include_planned,
98
+ "limit": max(limit_per_intent, len(intents) * limit_per_intent),
99
+ **({"model": model} if model else {}),
100
+ }, cache=cache)
101
+ tools = search.get("tools", []) if isinstance(search, dict) else []
102
+ tool_keys = []
103
+ for tool in tools:
104
+ key = tool.get("key") if isinstance(tool, dict) else None
105
+ if isinstance(key, str) and key not in tool_keys:
106
+ tool_keys.append(key)
107
+ schemas = self.schemas_for_session(
108
+ session_id,
109
+ {"toolKeys": tool_keys, "includePlanned": include_planned},
110
+ cache=cache,
111
+ ) if include_schemas and tool_keys else {"tools": []}
112
+ return {
113
+ "tools": tools,
114
+ "schemas": schemas.get("tools", []) if isinstance(schemas, dict) else [],
115
+ "toolKeys": tool_keys,
116
+ "search": search,
117
+ }
118
+
119
+ def execute(self, body: dict[str, Any]) -> Json:
120
+ return self.client.request("POST", "/v1/tools/execute", json_body=body)
121
+
122
+ def execute_for_session(self, session_id: str, body: dict[str, Any]) -> Json:
123
+ return self.client.request("POST", f"/v1/sessions/{session_id}/tools/execute", json_body=body)
124
+
125
+ def execute_batch(self, body: dict[str, Any]) -> Json:
126
+ return self.client.request("POST", "/v1/tools/execute-batch", json_body=body)
127
+
128
+ def execute_batch_for_session(self, session_id: str, body: dict[str, Any]) -> Json:
129
+ return self.client.request("POST", f"/v1/sessions/{session_id}/tools/execute-batch", json_body=body)
130
+
131
+
132
+ def _without_cache_control(body: dict[str, Any]) -> dict[str, Any]:
133
+ return {key: value for key, value in body.items() if key != "cache"}
134
+
135
+
136
+ def _server_cache_body(body: dict[str, Any], cache: str) -> dict[str, Any]:
137
+ payload = _without_cache_control(body)
138
+ if cache == "bypass":
139
+ payload["cache"] = "bypass"
140
+ return payload
141
+
142
+
143
+ def _stable_json(value: Any) -> str:
144
+ return json.dumps(value, sort_keys=True, separators=(",", ":"), default=str)
stacklink/triggers.py ADDED
@@ -0,0 +1,27 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+
6
+ class TriggersResource:
7
+ def __init__(self, client):
8
+ self.client = client
9
+
10
+ def types(self, **query):
11
+ return self.client.request("GET", "/v1/triggers/types", query=query)
12
+
13
+ def list(self, **query):
14
+ return self.client.request("GET", "/v1/triggers/subscriptions", query=query)
15
+
16
+ def create(self, body: dict[str, Any]):
17
+ return self.client.request("POST", "/v1/triggers/subscriptions", json_body=body)
18
+
19
+ def get(self, subscription_id: str):
20
+ return self.client.request("GET", f"/v1/triggers/subscriptions/{subscription_id}")
21
+
22
+ def update(self, subscription_id: str, body: dict[str, Any]):
23
+ return self.client.request("PATCH", f"/v1/triggers/subscriptions/{subscription_id}", json_body=body)
24
+
25
+ def delete(self, subscription_id: str):
26
+ return self.client.request("DELETE", f"/v1/triggers/subscriptions/{subscription_id}")
27
+
stacklink/types.py ADDED
@@ -0,0 +1,290 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Callable, Literal, Mapping, MutableMapping, Optional, Tuple, TypedDict
4
+
5
+ Json = Any
6
+ JsonObject = dict[str, Any]
7
+ Headers = Mapping[str, str]
8
+ Transport = Callable[[str, str, Headers, Optional[Json], Mapping[str, Any], float], Tuple[int, MutableMapping[str, str], Json]]
9
+
10
+ BillingClass = Literal["standard", "premium", "free"]
11
+ BillingUnit = Literal["call", "runtime_10s", "token_block", "byte_block"]
12
+ BillingQuantitySource = Literal["fixed", "duration_ms", "tokens", "bytes"]
13
+ BillingVersion = Literal["stacklink-billing-v1"]
14
+ PricingProductArea = Literal["provider", "knowledge", "model", "workbench", "platform"]
15
+ PricingUnit = Literal[
16
+ "operation",
17
+ "call",
18
+ "1000_calls",
19
+ "1m_input_tokens",
20
+ "1m_output_tokens",
21
+ "1m_reasoning_tokens",
22
+ "runtime_10s",
23
+ "byte_block",
24
+ ]
25
+
26
+ StacklinkBillingDescriptor = TypedDict(
27
+ "StacklinkBillingDescriptor",
28
+ {
29
+ "class": BillingClass,
30
+ "unit": BillingUnit,
31
+ "multiplier": Literal[1, 2, 3],
32
+ "quantitySource": BillingQuantitySource,
33
+ "version": BillingVersion,
34
+ },
35
+ total=False,
36
+ )
37
+
38
+
39
+ class StacklinkExecutionCapability(TypedDict, total=False):
40
+ cancellable: Literal["hard", "cooperative", "request_only", "none"]
41
+ streamingSupport: Literal["none", "progress", "output_delta", "both"]
42
+ backgroundSupport: Literal["none", "optional", "required"]
43
+ defaultTimeoutMs: int
44
+ maxTimeoutMs: int
45
+
46
+
47
+ ProviderConnectorType = Literal["app", "platform", "sandbox", "api"]
48
+ ProviderRuntimeKind = Literal["native_api", "native_sdk", "remote_mcp", "managed_mcp_gateway"]
49
+ ProviderRuntimeOwner = Literal["stacklink", "provider", "third_party"]
50
+ ProviderMcpTransport = Literal["streamable_http"]
51
+ ProviderMcpAuthType = Literal["oauth_dcr", "oauth_static_client", "bearer", "none"]
52
+ ProviderMcpImportPolicy = Literal["certified_allowlist", "discover_then_review"]
53
+ ProviderMcpIdentityKind = Literal["user", "workspace", "organization", "project"]
54
+ ProviderMcpResourceKind = Literal[
55
+ "user",
56
+ "workspace",
57
+ "organization",
58
+ "project",
59
+ "site",
60
+ "repository",
61
+ "channel",
62
+ "folder",
63
+ "database",
64
+ ]
65
+ ConnectionRequestPhase = Literal[
66
+ "request_created",
67
+ "connect_page_opened",
68
+ "oauth_started",
69
+ "provider_authorizing",
70
+ "oauth_callback_received",
71
+ "token_exchange_started",
72
+ "account_storing",
73
+ "remote_mcp_tools_importing",
74
+ "remote_mcp_tools_imported",
75
+ "completed",
76
+ "failed",
77
+ "expired",
78
+ ]
79
+ ConnectionRequestNextAction = Literal[
80
+ "open_connect_url",
81
+ "continue_to_provider",
82
+ "wait_for_user",
83
+ "wait_for_provider",
84
+ "wait_for_stacklink",
85
+ "refresh_toolkits",
86
+ "retry",
87
+ "none",
88
+ ]
89
+
90
+
91
+ class ProviderProfileCapabilityItem(TypedDict):
92
+ key: str
93
+ label: str
94
+ description: str
95
+
96
+
97
+ class ProviderProfileCapabilityGroup(TypedDict):
98
+ key: str
99
+ title: str
100
+ items: list[ProviderProfileCapabilityItem]
101
+
102
+
103
+ class ProviderProfileDetails(TypedDict, total=False):
104
+ connectorType: ProviderConnectorType
105
+ author: Literal["Stacklink"]
106
+ providerKey: str
107
+ websiteUrl: str
108
+ docsUrl: str
109
+ privacyPolicyUrl: str
110
+ termsUrl: str
111
+
112
+
113
+ class ProviderMarketplaceMetadata(TypedDict):
114
+ visible: bool
115
+ connectable: bool
116
+ recommended: bool
117
+ internal: bool
118
+
119
+
120
+ class ProviderProfile(TypedDict):
121
+ headline: str
122
+ summary: str
123
+ examplePrompts: list[str]
124
+ capabilityGroups: list[ProviderProfileCapabilityGroup]
125
+ accessText: str
126
+ authText: str
127
+ safetyText: str
128
+ details: ProviderProfileDetails
129
+ marketplace: ProviderMarketplaceMetadata
130
+
131
+
132
+ class ProviderMcpAuth(TypedDict):
133
+ type: ProviderMcpAuthType
134
+
135
+
136
+ class ProviderMcpIdentity(TypedDict, total=False):
137
+ strategy: Literal["tool_call"]
138
+ identityKind: ProviderMcpIdentityKind
139
+ toolName: str
140
+ arguments: JsonObject
141
+ payloadRoot: JsonObject
142
+ externalAccountIdPath: list[str]
143
+ displayNamePath: list[str]
144
+ displayNameTemplate: str
145
+ metadataPaths: dict[str, list[str]]
146
+ fallbackDisplayName: str
147
+ timeoutMs: int
148
+ contractVersion: int
149
+
150
+
151
+ class ProviderMcpResourceParent(TypedDict, total=False):
152
+ kind: ProviderMcpResourceKind
153
+ externalResourceIdPath: list[str]
154
+ labelPath: list[str]
155
+ labelTemplate: str
156
+
157
+
158
+ class ProviderMcpResourceEnrichment(TypedDict, total=False):
159
+ toolName: str
160
+ argumentsTemplate: JsonObject
161
+ payloadRoot: JsonObject
162
+ metadataPaths: dict[str, list[str]]
163
+ urlPath: list[str]
164
+ timeoutMs: int
165
+
166
+
167
+ class ProviderMcpResource(TypedDict, total=False):
168
+ key: str
169
+ strategy: Literal["tool_call"]
170
+ resourceKind: ProviderMcpResourceKind
171
+ toolName: str
172
+ arguments: JsonObject
173
+ payloadRoot: JsonObject
174
+ externalResourceIdPath: list[str]
175
+ labelPath: list[str]
176
+ labelTemplate: str
177
+ parent: ProviderMcpResourceParent
178
+ metadataPaths: dict[str, list[str]]
179
+ enrichments: list[ProviderMcpResourceEnrichment]
180
+ timeoutMs: int
181
+ maxItems: int
182
+ contractVersion: int
183
+
184
+
185
+ class ProviderMcpRuntime(TypedDict, total=False):
186
+ endpointUrl: str
187
+ transport: ProviderMcpTransport
188
+ auth: ProviderMcpAuth
189
+ sourceUrl: str
190
+ importPolicy: ProviderMcpImportPolicy
191
+ allowlisted: bool
192
+ identity: ProviderMcpIdentity
193
+ resources: list[ProviderMcpResource]
194
+
195
+
196
+ class ProviderRuntime(TypedDict, total=False):
197
+ kind: ProviderRuntimeKind
198
+ owner: ProviderRuntimeOwner
199
+ label: str
200
+ mcp: ProviderMcpRuntime
201
+
202
+
203
+ class ConnectionRequest(TypedDict, total=False):
204
+ id: str
205
+ providerKey: str
206
+ providerName: str
207
+ authConfigId: str | None
208
+ authMode: str | None
209
+ status: str
210
+ phase: ConnectionRequestPhase
211
+ nextAction: ConnectionRequestNextAction
212
+ redirectUrl: str
213
+ callbackUrl: str | None
214
+ failureReason: str | None
215
+ failureCode: str | None
216
+ expiresAt: str
217
+ authModes: list[str]
218
+ authRequirements: list[JsonObject]
219
+ connectedAccount: JsonObject | None
220
+ metadata: Json
221
+
222
+
223
+ class PublicPricingSummary(TypedDict, total=False):
224
+ billingModel: Literal["included", "free", "metered", "token_metered", "custom"]
225
+ productArea: PricingProductArea
226
+ unit: PricingUnit
227
+ unitLabel: str
228
+ priceMicros: str
229
+ priceText: str
230
+ currency: str
231
+ effectiveFrom: str
232
+ notes: list[str]
233
+
234
+
235
+ class ProviderToolkit(TypedDict, total=False):
236
+ id: str
237
+ key: str
238
+ displayName: str
239
+ category: str
240
+ status: str
241
+ authModes: list[str]
242
+ capabilities: list[str]
243
+ metadata: Json
244
+ brand: Json
245
+ profile: ProviderProfile
246
+ runtime: ProviderRuntime
247
+ pricingSummary: PublicPricingSummary
248
+ createdAt: str
249
+ updatedAt: str
250
+
251
+
252
+ class ProjectProvider(ProviderToolkit, total=False):
253
+ providerKey: str
254
+ name: str
255
+ projectStatus: str
256
+ toolCount: int
257
+ triggerCount: int
258
+ enabledAt: str | None
259
+ disabledAt: str | None
260
+ connection: Json
261
+ toolkit: ProviderToolkit
262
+
263
+
264
+ class ProviderTool(TypedDict, total=False):
265
+ id: str
266
+ key: str
267
+ toolkitKey: str
268
+ displayName: str
269
+ description: str
270
+ capability: str
271
+ mutationLevel: str
272
+ status: str
273
+ inputSchema: Json
274
+ outputSchema: Json
275
+ searchDescriptor: Json
276
+ metadata: Json
277
+ aliases: list[Json]
278
+ providerProfile: ProviderProfile
279
+ execution: StacklinkExecutionCapability
280
+ billing: StacklinkBillingDescriptor
281
+
282
+
283
+ class ToolSearchResponse(TypedDict, total=False):
284
+ tools: list[ProviderTool]
285
+ queries: list[Json]
286
+ meta: Json
287
+
288
+
289
+ class ToolSchemasResponse(TypedDict):
290
+ tools: list[ProviderTool]
stacklink/webhooks.py ADDED
@@ -0,0 +1,29 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+
6
+ class WebhooksResource:
7
+ def __init__(self, client):
8
+ self.client = client
9
+
10
+ def get(self):
11
+ return self.client.request("GET", "/v1/webhook")
12
+
13
+ def update(self, body: dict[str, Any]):
14
+ return self.client.request("PUT", "/v1/webhook", json_body=body)
15
+
16
+ def rotate_secret(self):
17
+ return self.client.request("POST", "/v1/webhook/rotate-secret")
18
+
19
+ def send_test(self, body: dict[str, Any] | None = None):
20
+ return self.client.request("POST", "/v1/webhook/test", json_body=body or {})
21
+
22
+ def retry_delivery(self, delivery_id: str, body: dict[str, Any] | None = None):
23
+ return self.client.request("POST", f"/v1/webhook-deliveries/{delivery_id}/retry", json_body=body or {})
24
+
25
+ def redrive_dead_letters(self, body: dict[str, Any] | None = None):
26
+ return self.client.request("POST", "/v1/webhook-deliveries/dead-letter/redrive", json_body=body or {})
27
+
28
+ def replay_event(self, event_id: str):
29
+ return self.client.request("POST", f"/v1/events/{event_id}/replay")