gtwy-sdk 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
gtwy_sdk-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 GTWY
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.
@@ -0,0 +1 @@
1
+ include gtwy/SKILLS.md
@@ -0,0 +1,59 @@
1
+ Metadata-Version: 2.4
2
+ Name: gtwy-sdk
3
+ Version: 0.1.0
4
+ Summary: Official Python SDK for the GTWY API — build and call agentic AI, RAG, and LLM-powered agents
5
+ Author: GTWY
6
+ License: MIT
7
+ Project-URL: Homepage, https://gtwy.ai
8
+ Keywords: gtwy,ai,llm,agents,agentic ai,rag,chatbot,sdk,api client
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3 :: Only
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
15
+ Requires-Python: >=3.8
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Requires-Dist: requests<3,>=2.25
19
+ Dynamic: license-file
20
+
21
+ # gtwy-sdk
22
+
23
+ Official Python client for the GTWY API — build and call agentic AI, RAG,
24
+ and LLM-powered agents.
25
+
26
+ ```bash
27
+ pip install gtwy-sdk
28
+ ```
29
+
30
+ ```python
31
+ from gtwy import Gtwy
32
+
33
+ client = Gtwy(api_key="<pauthkey>")
34
+
35
+ response = client.chat.completions.create(
36
+ agent_id="my-agent-id",
37
+ user="What's the capital of France?",
38
+ )
39
+ print(response.content)
40
+ ```
41
+
42
+ `api_key` can also come from the `GTWY_API_KEY` environment variable.
43
+ `base_url` defaults to `https://api.gtwy.ai`; override it (constructor arg or
44
+ `GTWY_BASE_URL`) to point at a different environment.
45
+
46
+ See [gtwy/SKILLS.md](./gtwy/SKILLS.md) for the full reference: every resource
47
+ (`chat`, `batch`, `rerun`), every request field, response
48
+ shapes, delivery modes (sync/webhook), and error handling. This file
49
+ ships **inside** the installed package (not just this repo) — after
50
+ `pip install gtwy-sdk` it's reachable at `gtwy.SKILLS_PATH` /
51
+ `gtwy.get_skills_doc()`, so an AI agent (e.g. Claude) can read it and use
52
+ this SDK correctly without ever cloning this repo.
53
+
54
+ ## Development
55
+
56
+ ```bash
57
+ cd python
58
+ pip install -e .
59
+ ```
@@ -0,0 +1,39 @@
1
+ # gtwy-sdk
2
+
3
+ Official Python client for the GTWY API — build and call agentic AI, RAG,
4
+ and LLM-powered agents.
5
+
6
+ ```bash
7
+ pip install gtwy-sdk
8
+ ```
9
+
10
+ ```python
11
+ from gtwy import Gtwy
12
+
13
+ client = Gtwy(api_key="<pauthkey>")
14
+
15
+ response = client.chat.completions.create(
16
+ agent_id="my-agent-id",
17
+ user="What's the capital of France?",
18
+ )
19
+ print(response.content)
20
+ ```
21
+
22
+ `api_key` can also come from the `GTWY_API_KEY` environment variable.
23
+ `base_url` defaults to `https://api.gtwy.ai`; override it (constructor arg or
24
+ `GTWY_BASE_URL`) to point at a different environment.
25
+
26
+ See [gtwy/SKILLS.md](./gtwy/SKILLS.md) for the full reference: every resource
27
+ (`chat`, `batch`, `rerun`), every request field, response
28
+ shapes, delivery modes (sync/webhook), and error handling. This file
29
+ ships **inside** the installed package (not just this repo) — after
30
+ `pip install gtwy-sdk` it's reachable at `gtwy.SKILLS_PATH` /
31
+ `gtwy.get_skills_doc()`, so an AI agent (e.g. Claude) can read it and use
32
+ this SDK correctly without ever cloning this repo.
33
+
34
+ ## Development
35
+
36
+ ```bash
37
+ cd python
38
+ pip install -e .
39
+ ```
@@ -0,0 +1,332 @@
1
+ # GTWY Python SDK — reference
2
+
3
+ This file ships **inside** the installed `gtwy` package (`gtwy/SKILLS.md`,
4
+ also reachable at runtime via `gtwy.SKILLS_PATH` / `gtwy.get_skills_doc()`)
5
+ specifically so an AI coding agent (e.g. Claude) can read it after a plain
6
+ `pip install gtwy-sdk` — no access to this source repo required — and correctly
7
+ write code against every function and every field below without guessing.
8
+
9
+ If you are an agent: read this whole file before writing any `gtwy` call.
10
+ Every parameter of every public method is listed with its type, whether
11
+ it's required, and what it does. Don't invent parameter names that aren't
12
+ in these tables — pass anything genuinely new through `**extra` (documented
13
+ per-method) and it goes straight into the request body.
14
+
15
+ ## What this SDK is
16
+
17
+ `gtwy` is a thin HTTP client around GTWY's own `/api/v2/model/*` routes
18
+ (the same routes GTWY's own playground and integrations use). It does not
19
+ reimplement any model-calling logic — every request is a plain authenticated
20
+ POST, and every response is parsed into a small attribute-access wrapper.
21
+
22
+ ## Install
23
+
24
+ ```bash
25
+ pip install gtwy-sdk
26
+ ```
27
+
28
+ ## Authenticating
29
+
30
+ ```python
31
+ from gtwy import Gtwy
32
+
33
+ client = Gtwy(api_key="<pauthkey>") # org-level API key from the GTWY dashboard
34
+ ```
35
+
36
+ | Constructor argument | Type | Default | Notes |
37
+ |---|---|---|---|
38
+ | `api_key` | str | `os.environ["GTWY_API_KEY"]` | **Required** (directly or via env var). Sent as the `pauthkey` HTTP header on every request. This is an **org-level** key from the GTWY dashboard — it is NOT the same as the `service`/`apikey` request field used to select a model provider. |
39
+ | `base_url` | str | `https://api.gtwy.ai`, or `os.environ["GTWY_BASE_URL"]` if set | GTWY API host. Override to target a different environment (e.g. staging). |
40
+ | `timeout` | float | `None` (no timeout — waits indefinitely) | Per-request timeout in seconds. Set explicitly if you want requests to give up after a fixed duration. |
41
+ | `max_retries` | int | `0` | Automatic retries on `429`/`5xx` responses only (see "Error handling"). Off by default — raise `max_retries` if you want automatic retries. |
42
+ | `session` | `requests.Session` | new session | Advanced: reuse connection pooling across multiple `Gtwy` instances. |
43
+
44
+ Raises `gtwy.GtwyError` at construction time if `api_key` is missing.
45
+ Missing/invalid `api_key` at request time raises `gtwy.AuthenticationError`
46
+ (HTTP 401). A disabled org raises `gtwy.PermissionDeniedError` (HTTP 403).
47
+
48
+ There is no separate "streaming login" step — delivery mode (sync JSON vs.
49
+ webhook) is controlled per-request via `settings.response_format`, not at
50
+ the client level. See "Delivery modes" below.
51
+
52
+ ## Resource map
53
+
54
+ | You want to... | Call |
55
+ |---|---|
56
+ | Send one message to an agent/bridge and get a reply | `client.chat.completions.create(...)` |
57
+ | Run many prompts through one bridge, delivered to a webhook | `client.batch.completions.create(...)` |
58
+ | Replay a past message with the bridge's current config | `client.rerun.create(...)` |
59
+
60
+ ---
61
+
62
+ ## `client.chat.completions.create(...)`
63
+
64
+ ```python
65
+ response = client.chat.completions.create(
66
+ agent_id="my-agent-id",
67
+ user="What's the capital of France?",
68
+ )
69
+ print(response.content) # "Paris."
70
+ print(response.usage.total_tokens)
71
+ ```
72
+
73
+ All parameters are keyword-only.
74
+
75
+ | Parameter | Type | Required? | Description |
76
+ |---|---|---|---|
77
+ | `agent_id` | str | **yes** | Identifies the agent/bridge configuration to run. |
78
+ | `user` | str | one of `user`/`messages` | The user's message text for this turn. |
79
+ | `messages` | list[dict] | one of `user`/`messages` | Explicit message list (e.g. `[{"role": "user", "content": "..."}]`), overrides/extends history. |
80
+ | `thread_id` | str | no | Conversation/session id — carries memory across calls. Omit for a stateless one-off call. |
81
+ | `sub_thread_id` | str | no | Sub-conversation id within a thread (e.g. per browser tab). |
82
+ | `version_id` | str | no | Pin a specific bridge version instead of the default/live one. |
83
+ | `variables` | dict | no | Template variable substitutions for the bridge's prompt, e.g. `{"customer_name": "Acme"}`. |
84
+ | `variables_path` | str | no | Dot-path filter selecting a subset of `variables` to apply, for bridges with nested variable structures. |
85
+ | `configuration` | dict | no | Per-call overrides — see the dedicated table below for every recognized key. |
86
+ | `service` | str | no | Model provider override, e.g. `"openai"`, `"anthropic"`, `"groq"`. Must be a provider the bridge/org has configured. |
87
+ | `apikey` | str | no | Provider API key override (falls back to the bridge's stored key). Unrelated to SDK auth (`Gtwy(api_key=...)`). |
88
+ | `user_urls` | list[dict] | no | Attachments by URL: `[{"type": "image"\|"audio", "url": "https://..."}]`. |
89
+ | `auto_model_select` | bool | no | Let GTWY pick the model automatically instead of using `configuration.model`. |
90
+ | `cache_on` | bool | no | Enable response caching for this call. |
91
+ | `stream` | bool | no | Shorthand for `configuration["stream"]`. |
92
+ | `settings` | dict | no | General passthrough for the request's `settings` object — see the dedicated breakdown below. Omit entirely for a normal synchronous call. |
93
+ | `extra_tools` | list[dict] | no | Additional tool definitions available to the model for this call, on top of the bridge's configured tools. |
94
+ | `built_in_tools` | list[dict] | no | Enable/configure GTWY's built-in tools (e.g. web search, code execution) for this call. |
95
+ | `guardrails` | dict | no | Feeds bridge configuration resolution at request setup time. This is **not** the field the completion route actually checks the user's message against at runtime — for that, use `settings.guardrails` below. |
96
+ | `web_search_filters` | dict | no | Filters applied when a web-search tool is invoked (e.g. allowed domains, date range). |
97
+ | `orchestrator_flag` | bool | no | Enables multi-agent orchestrator behavior for this call. |
98
+ | `environment` | str | no | Environment override (e.g. run against a `"staging"` bridge configuration). |
99
+ | `**extra` | any | no | Anything not listed above is passed through verbatim in the request body, for forwards-compatibility with new API fields. |
100
+
101
+ #### Keys inside `settings`
102
+
103
+ | Key | Type | Meaning |
104
+ |---|---|---|
105
+ | `response_format` | dict | Delivery mode override — see "Delivery modes" below. |
106
+ | `fall_back` | dict | Automatic fallback to a different model/service if the primary call fails: `{"is_enable": bool, "service": str, "model": str}`. `service`+`model` must be a valid, existing combination. |
107
+ | `maximum_iterations` | int | Caps how many tool-call/agent iteration loops a single request can run. Defaults to `3` if omitted. |
108
+ | `guardrails` | dict | The guardrail policy actually enforced against the user's message at runtime: `{"is_enabled": bool, "guardrails_configuration": {"toxicity": bool, "bias": bool, "hallucination": bool, ...}, "guardrails_custom_prompt": str}`. This is the field the completion route checks — distinct from the top-level `guardrails` parameter above. |
109
+
110
+ #### Keys inside `configuration`
111
+
112
+ The server validates `configuration`'s keys and rejects unrecognized ones
113
+ with a `BadRequestError` (400) — including a "did you mean '...'?" hint for
114
+ near-typos. There are two categories of valid keys:
115
+
116
+ **Structural keys** — same across every model/service:
117
+
118
+ | Key | Type | Meaning |
119
+ |---|---|---|
120
+ | `model` | str | Which model to use, e.g. `"gpt-4o"`, `"claude-3-5-sonnet"`. Must be a model the selected `service` actually offers. |
121
+ | `type` | str | Request type: `"chat"` (default), `"embedding"`, or `"image"`. |
122
+ | `prompt` | str | Override the bridge's stored system prompt for this call. |
123
+ | `stream` | bool | Enable/disable streaming for this call (also settable via the top-level `stream` parameter, which is a shorthand for this). |
124
+ | `tools` | list[dict] | Function-calling tool definitions available to the model for this call. |
125
+ | `tool_choice` | str \| dict | Controls function-calling behavior: `"auto"`, `"none"`, `"required"`, `"default"`, or a specific tool identifier. |
126
+ | `response_type` | str \| dict | Output format selector: `"text"` (default), `"json_object"`, or `"json_schema"` (the latter takes a nested `{"type": "json_schema", "json_schema": {...}}` shape). |
127
+ | `response_format` | dict | Delivery mode override — see "Delivery modes" below. If sent here, the server relocates it into `settings.response_format` automatically. Prefer setting it directly via the `settings` parameter on `create(...)`. |
128
+ | `fine_tune_model` | dict | Fine-tuned model override: `{"current_model": "..."}`. |
129
+ | `is_rich_text` | bool | Toggles rich-text response handling. |
130
+ | `mcp_config` | dict | MCP server config: `{"servers": [{"name": ..., "url": ..., "headers": ..., "tools": [...]}]}` — auto-expanded into `tools`. |
131
+
132
+ **Model-specific sampling params** — e.g. `temperature`, `top_p`,
133
+ `max_tokens`, `reasoning_effort`, etc. Which of these are valid, and their
134
+ allowed min/max/default, is defined **per model** in GTWY's own model
135
+ registry, not by this SDK — different models expose different sampling
136
+ parameters. The SDK does not (and cannot) validate these client-side; send
137
+ whatever the target model's own documentation/dashboard entry says it
138
+ supports, and the server will validate them against that model's actual
139
+ schema. An unrecognized key for the resolved model raises a `BadRequestError`
140
+ naming exactly which key was invalid.
141
+
142
+ **Returns** a `ChatCompletion` object:
143
+
144
+ | Property | Type | Meaning |
145
+ |---|---|---|
146
+ | `.content` | str \| None | Assistant reply text. `None` if the call was queued for async delivery — check `.queued` first. |
147
+ | `.model` | str \| None | Model that produced the reply. |
148
+ | `.usage` | `Usage` | Token/cost accounting, see below. |
149
+ | `.images` | list \| None | Generated/returned images, if any (image-type calls). |
150
+ | `.tools_data` | dict | Raw tool-call data from the model, if any. |
151
+ | `.queued` | bool | `True` if this call was queued for async delivery instead of answered inline. |
152
+ | `.message_id` | str \| None | Correlation id — present on both sync and queued responses. |
153
+ | `.data` | dict | The raw `response.data` dict, for any field not wrapped above. |
154
+ | `.raw` | dict | The full untouched response dict. |
155
+ | `["any_key"]` / `.get("any_key")` | any | Index/get into `.raw` directly. |
156
+
157
+ `Usage` (found at `response.usage`):
158
+
159
+ | Property | Meaning |
160
+ |---|---|
161
+ | `.input_tokens` | Tokens in the prompt. |
162
+ | `.output_tokens` | Tokens in the completion. |
163
+ | `.total_tokens` | Sum of input + output. |
164
+ | `.cost` | Estimated cost of the call, if GTWY computed one. |
165
+
166
+ If `configuration = {"type": "embedding"}` or `{"type": "image"}`, the
167
+ response's `data` shape is different (embedding vectors / image URLs
168
+ respectively) — read it via `response.raw` or `response.data` in that case;
169
+ the typed `.content`/`.images` accessors are tuned for the default chat
170
+ case.
171
+
172
+ ---
173
+
174
+ ## `client.batch.completions.create(...)`
175
+
176
+ ```python
177
+ response = client.batch.completions.create(
178
+ agent_id="my-agent-id",
179
+ batch=["Summarize doc 1", "Summarize doc 2"],
180
+ batch_variables=[{"doc_id": "1"}, {"doc_id": "2"}],
181
+ batch_webhook="https://your-service.example.com/gtwy-batch-hook",
182
+ )
183
+ print(response.batch_id)
184
+ ```
185
+
186
+ | Parameter | Type | Required? | Description |
187
+ |---|---|---|---|
188
+ | `agent_id` | str | **yes** | Bridge to run every prompt in the batch against. |
189
+ | `batch` | list[str] | **yes** | One prompt string per item to run. |
190
+ | `batch_webhook` | str | **yes** | URL that receives the results — batch runs are always async, this call never returns results inline. |
191
+ | `batch_variables` | list[dict] | no | Per-item template variables. If given, **must be the same length as `batch`** — the SDK raises `ValueError` locally (before any network call) if the lengths mismatch. |
192
+ | `thread_id` | str | no | Conversation/session id to attach the batch run to. |
193
+ | `configuration` | dict | no | Same shape as in `chat.completions.create` — applies to every item in the batch. |
194
+ | `**extra` | any | no | Passed through verbatim. |
195
+
196
+ **Returns** a `BatchCompletion`:
197
+
198
+ | Property | Meaning |
199
+ |---|---|
200
+ | `.batch_id` | Id to correlate with results later delivered to `batch_webhook`. |
201
+ | `.messages` | Any messages the API returned inline (rare — batch results normally arrive via webhook, not here). |
202
+ | `.raw` | Full response dict. |
203
+
204
+ ---
205
+
206
+ ## `client.rerun.create(...)`
207
+
208
+ ```python
209
+ client.rerun.create(agent_id="my-agent-id", message_ids=["msg-1", "msg-2"])
210
+ # or, rerun the last message in a thread:
211
+ client.rerun.create(agent_id="my-agent-id", thread_id="t1", sub_thread_id="s1")
212
+ ```
213
+
214
+ | Parameter | Type | Required? | Description |
215
+ |---|---|---|---|
216
+ | `agent_id` | str | **yes** | Bridge whose current configuration will be used for the rerun. |
217
+ | `message_ids` | list[str] | one addressing mode | Specific past message ids to rerun. |
218
+ | `thread_id` + `sub_thread_id` | str + str | the other addressing mode | Reruns the last message in that thread instead of specific ids. |
219
+ | `**extra` | any | no | Passed through verbatim. |
220
+
221
+ Exactly one addressing mode is required — the SDK raises `ValueError`
222
+ locally if neither `message_ids` nor the `thread_id`+`sub_thread_id` pair is
223
+ given.
224
+
225
+ **Returns** a `RerunResult`:
226
+
227
+ | Property | Meaning |
228
+ |---|---|
229
+ | `.queued` | list of ids accepted and queued for rerun. |
230
+ | `.not_found` | list of ids that couldn't be matched to a conversation. |
231
+ | `.raw` | Full response dict. |
232
+
233
+ Reran results are delivered per each message's own bridge `response_format`
234
+ (see "Delivery modes"), not returned inline here.
235
+
236
+ ---
237
+
238
+ ## Delivery modes (sync vs. webhook)
239
+
240
+ Every method above is a synchronous HTTP call from the SDK's point of view,
241
+ but the underlying GTWY route can be configured — via the bridge itself, or
242
+ via `settings.response_format` on `chat.completions.create` — to deliver
243
+ the actual model output somewhere else instead of in the HTTP response:
244
+
245
+ ```python
246
+ response = client.chat.completions.create(
247
+ agent_id="my-agent-id",
248
+ user="hi",
249
+ settings={"response_format": {"type": "webhook", "cred": {"url": "https://your-service.example.com/hook"}}},
250
+ )
251
+ ```
252
+
253
+ - **Default** (`settings` unset, or `response_format` unset/`{"type": "default"}`
254
+ within it): the HTTP response body *is* the result — `response.content`
255
+ is populated immediately. This is what you want for typical SDK usage.
256
+ - **`{"type": "webhook", "cred": {"url": "https://..."}}`**: GTWY queues the
257
+ request and POSTs the result to your webhook later. The call returns
258
+ instantly with `response.queued == True` and a `response.message_id` to
259
+ correlate.
260
+
261
+ If you just want a normal request/response call, don't pass `settings` at
262
+ all — the API defaults to synchronous delivery.
263
+
264
+ ## Error handling
265
+
266
+ Every raised exception is a subclass of `gtwy.GtwyError`.
267
+
268
+ ```python
269
+ from gtwy import (
270
+ Gtwy, GtwyError, GtwyConnectionError, GtwyTimeoutError,
271
+ APIStatusError, AuthenticationError, BadRequestError,
272
+ PermissionDeniedError, NotFoundError, RateLimitError, InternalServerError,
273
+ )
274
+
275
+ try:
276
+ client.chat.completions.create(agent_id="bad-id", user="hi")
277
+ except AuthenticationError:
278
+ ... # bad/missing pauthkey (401)
279
+ except BadRequestError as e:
280
+ ... # invalid body / bridge config problem (400) — e.message, e.body
281
+ except APIStatusError as e:
282
+ ... # any other non-2xx response — e.status_code, e.message, e.body
283
+ except GtwyConnectionError:
284
+ ... # network/DNS/timeout failure before any response was received
285
+ ```
286
+
287
+ | Exception | HTTP status | Meaning |
288
+ |---|---|---|
289
+ | `AuthenticationError` | 401 | Missing or invalid `pauthkey`. |
290
+ | `PermissionDeniedError` | 403 | Organization disabled/blocked. |
291
+ | `BadRequestError` | 400 | Malformed body or invalid bridge configuration. |
292
+ | `NotFoundError` | 404 | Bridge, thread, or message not found. |
293
+ | `RateLimitError` | 429 | Too many requests for this bridge/thread — retried automatically first. |
294
+ | `InternalServerError` | 5xx | Unexpected failure on the GTWY side — retried automatically first. |
295
+ | `APIStatusError` | any other non-2xx | Base class for all of the above; catch this to handle any API error generically. |
296
+ | `GtwyConnectionError` | n/a | Network/DNS failure before any response was received. |
297
+ | `GtwyTimeoutError` | n/a | Subclass of `GtwyConnectionError` — request exceeded `timeout`. |
298
+
299
+ `APIStatusError` and its subclasses carry `.status_code`, `.message`, and
300
+ `.body` (the raw parsed error JSON, when available).
301
+
302
+ Retries: `429` and `5xx` responses are retried automatically only if
303
+ `max_retries` is set above its default of `0` (e.g. `Gtwy(api_key=..., max_retries=2)`);
304
+ all other error statuses raise immediately without retrying, and no
305
+ retries happen at all with the default settings.
306
+
307
+ ## Reading the packaged doc programmatically
308
+
309
+ ```python
310
+ import gtwy
311
+ print(gtwy.SKILLS_PATH) # pathlib.Path to this file, wherever it's installed
312
+ print(gtwy.get_skills_doc()) # this file's full contents as a string
313
+ ```
314
+
315
+ Useful for an agent harness that wants to inject this reference into its
316
+ own context/prompt at runtime rather than reading the file off disk
317
+ directly.
318
+
319
+ ## What this SDK deliberately does not do
320
+
321
+ - No streaming token-by-token iterator — GTWY's delivery for a single call
322
+ is either "one JSON response" or "queued to webhook" (see above), not an
323
+ SSE token stream. Don't invent a `.stream()` method that doesn't exist
324
+ server-side.
325
+ - No automatic webhook subscriber — you own receiving async deliveries;
326
+ this SDK only sends the request that triggers them.
327
+ - No async/await client in this version — `Gtwy` is synchronous
328
+ (`requests`-based). If you need concurrency, run calls in a thread pool.
329
+ - Field names in `configuration`/`variables`/etc. are passed through
330
+ untouched — this SDK does not validate their contents against the
331
+ bridge's actual configuration; a bad `configuration.model` value fails
332
+ server-side as a `BadRequestError`, not client-side.
@@ -0,0 +1,37 @@
1
+ from .client import Gtwy
2
+ from ._skills import SKILLS_PATH, get_skills_doc
3
+ from ._exceptions import (
4
+ APIStatusError,
5
+ AuthenticationError,
6
+ BadRequestError,
7
+ GtwyConnectionError,
8
+ GtwyError,
9
+ GtwyTimeoutError,
10
+ InternalServerError,
11
+ NotFoundError,
12
+ PermissionDeniedError,
13
+ RateLimitError,
14
+ )
15
+ from ._models import BatchCompletion, ChatCompletion, RerunResult, Usage
16
+
17
+ __version__ = "0.1.0"
18
+
19
+ __all__ = [
20
+ "Gtwy",
21
+ "GtwyError",
22
+ "GtwyConnectionError",
23
+ "GtwyTimeoutError",
24
+ "APIStatusError",
25
+ "AuthenticationError",
26
+ "BadRequestError",
27
+ "PermissionDeniedError",
28
+ "NotFoundError",
29
+ "RateLimitError",
30
+ "InternalServerError",
31
+ "ChatCompletion",
32
+ "BatchCompletion",
33
+ "RerunResult",
34
+ "Usage",
35
+ "SKILLS_PATH",
36
+ "get_skills_doc",
37
+ ]
@@ -0,0 +1,97 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from typing import Any, Dict, Optional
5
+
6
+ import requests
7
+
8
+ from ._exceptions import GtwyConnectionError, GtwyError, GtwyTimeoutError, error_for_status
9
+
10
+ DEFAULT_MAX_RETRIES = 0
11
+ DEFAULT_BASE_URL = "https://api.gtwy.ai"
12
+ # Retried because they're transient on the GTWY side, per src/handler/executionHandler.py
13
+ _RETRYABLE_STATUS_CODES = {429, 500, 502, 503, 504}
14
+
15
+
16
+ class BaseClient:
17
+ """Shared request plumbing for :class:`gtwy.Gtwy`.
18
+
19
+ Not part of the public API — use ``Gtwy`` / its resources instead.
20
+ """
21
+
22
+ def __init__(
23
+ self,
24
+ api_key: Optional[str] = None,
25
+ base_url: Optional[str] = None,
26
+ timeout: Optional[float] = None,
27
+ max_retries: int = DEFAULT_MAX_RETRIES,
28
+ session: Optional[requests.Session] = None,
29
+ ):
30
+ api_key = api_key or os.environ.get("GTWY_API_KEY")
31
+ if not api_key:
32
+ raise GtwyError(
33
+ "No API key provided. Pass `api_key` to Gtwy(...) or set the "
34
+ "GTWY_API_KEY environment variable. This is the org-level "
35
+ "`pauthkey` shown in your GTWY dashboard."
36
+ )
37
+ base_url = base_url or os.environ.get("GTWY_BASE_URL") or DEFAULT_BASE_URL
38
+
39
+ self.api_key = api_key
40
+ self.base_url = base_url.rstrip("/")
41
+ self.timeout = timeout
42
+ self.max_retries = max_retries
43
+ self._session = session or requests.Session()
44
+
45
+ def _headers(self) -> Dict[str, str]:
46
+ return {
47
+ "pauthkey": self.api_key,
48
+ "Content-Type": "application/json",
49
+ "User-Agent": "gtwy-python/0.1.0",
50
+ }
51
+
52
+ def post(self, path: str, body: Dict[str, Any]) -> Dict[str, Any]:
53
+ url = f"{self.base_url}{path}"
54
+ # Drop keys the caller left as None so the server sees its own defaults,
55
+ # instead of an explicit null overriding bridge configuration.
56
+ payload = {k: v for k, v in body.items() if v is not None}
57
+
58
+ last_error: Optional[Exception] = None
59
+ for attempt in range(self.max_retries + 1):
60
+ try:
61
+ response = self._session.post(
62
+ url, json=payload, headers=self._headers(), timeout=self.timeout
63
+ )
64
+ except requests.Timeout as exc:
65
+ last_error = GtwyTimeoutError(f"Request to {url} timed out after {self.timeout}s")
66
+ last_error.__cause__ = exc
67
+ except requests.RequestException as exc:
68
+ last_error = GtwyConnectionError(f"Failed to reach {url}: {exc}")
69
+ last_error.__cause__ = exc
70
+ else:
71
+ if response.status_code < 400:
72
+ return response.json()
73
+
74
+ try:
75
+ data = response.json()
76
+ except ValueError:
77
+ data = {"error": response.text}
78
+
79
+ message = (
80
+ data.get("error")
81
+ or data.get("error_message")
82
+ or data.get("detail")
83
+ or f"Request failed with status {response.status_code}"
84
+ )
85
+ if isinstance(message, dict):
86
+ message = message.get("error") or str(message)
87
+
88
+ last_error = error_for_status(response.status_code, str(message), data)
89
+ if response.status_code not in _RETRYABLE_STATUS_CODES:
90
+ raise last_error
91
+
92
+ if attempt < self.max_retries:
93
+ continue
94
+ raise last_error
95
+
96
+ # Unreachable, but keeps type-checkers happy.
97
+ raise last_error or GtwyConnectionError(f"Failed to reach {url}")
@@ -0,0 +1,75 @@
1
+ """Exception hierarchy for the GTWY SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Optional
6
+
7
+
8
+ class GtwyError(Exception):
9
+ """Base class for all errors raised by this SDK."""
10
+
11
+
12
+ class GtwyConnectionError(GtwyError):
13
+ """Raised when the request could not reach the GTWY API at all."""
14
+
15
+
16
+ class GtwyTimeoutError(GtwyConnectionError):
17
+ """Raised when a request exceeds the configured timeout."""
18
+
19
+
20
+ class APIStatusError(GtwyError):
21
+ """Raised when the GTWY API responds with an error.
22
+
23
+ Mirrors the ``{"success": False, "error": "..."}`` envelope returned by
24
+ every GTWY route, plus non-2xx HTTP statuses raised before that envelope
25
+ can even be parsed (auth/middleware failures).
26
+ """
27
+
28
+ def __init__(self, message: str, *, status_code: int, body: Optional[Any] = None):
29
+ super().__init__(message)
30
+ self.message = message
31
+ self.status_code = status_code
32
+ self.body = body
33
+
34
+ def __str__(self) -> str:
35
+ return f"[{self.status_code}] {self.message}"
36
+
37
+
38
+ class BadRequestError(APIStatusError):
39
+ """HTTP 400 — malformed request body or invalid bridge configuration."""
40
+
41
+
42
+ class AuthenticationError(APIStatusError):
43
+ """HTTP 401 — missing or invalid ``pauthkey``."""
44
+
45
+
46
+ class PermissionDeniedError(APIStatusError):
47
+ """HTTP 403 — organization disabled or blocked."""
48
+
49
+
50
+ class NotFoundError(APIStatusError):
51
+ """HTTP 404 — bridge, thread, or message not found."""
52
+
53
+
54
+ class RateLimitError(APIStatusError):
55
+ """HTTP 429 — too many requests for this bridge/thread."""
56
+
57
+
58
+ class InternalServerError(APIStatusError):
59
+ """HTTP 5xx — unexpected failure on the GTWY side."""
60
+
61
+
62
+ def error_for_status(status_code: int, message: str, body: Optional[Any]) -> APIStatusError:
63
+ if status_code == 400:
64
+ return BadRequestError(message, status_code=status_code, body=body)
65
+ if status_code == 401:
66
+ return AuthenticationError(message, status_code=status_code, body=body)
67
+ if status_code == 403:
68
+ return PermissionDeniedError(message, status_code=status_code, body=body)
69
+ if status_code == 404:
70
+ return NotFoundError(message, status_code=status_code, body=body)
71
+ if status_code == 429:
72
+ return RateLimitError(message, status_code=status_code, body=body)
73
+ if status_code >= 500:
74
+ return InternalServerError(message, status_code=status_code, body=body)
75
+ return APIStatusError(message, status_code=status_code, body=body)
@@ -0,0 +1,123 @@
1
+ """Lightweight response wrappers.
2
+
3
+ These intentionally avoid a pydantic dependency: GTWY route responses are
4
+ plain JSON dicts, so each wrapper just gives attribute access on top of the
5
+ raw dict while still exposing ``.to_dict()`` / ``.raw`` for anything the
6
+ typed surface doesn't cover yet.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Any, Dict, Optional
12
+
13
+
14
+ class GtwyObject:
15
+ """Attribute-access view over a raw API dict.
16
+
17
+ Unknown/untyped fields are still reachable via ``obj["field"]`` or
18
+ ``obj.raw``, so a newer API field never requires an SDK upgrade to read.
19
+ """
20
+
21
+ def __init__(self, raw: Dict[str, Any]):
22
+ self.raw = raw or {}
23
+
24
+ def __getitem__(self, key: str) -> Any:
25
+ return self.raw[key]
26
+
27
+ def get(self, key: str, default: Any = None) -> Any:
28
+ return self.raw.get(key, default)
29
+
30
+ def to_dict(self) -> Dict[str, Any]:
31
+ return self.raw
32
+
33
+ def __repr__(self) -> str:
34
+ return f"{self.__class__.__name__}({self.raw!r})"
35
+
36
+
37
+ class Usage(GtwyObject):
38
+ @property
39
+ def input_tokens(self) -> Optional[int]:
40
+ return self.raw.get("input_tokens")
41
+
42
+ @property
43
+ def output_tokens(self) -> Optional[int]:
44
+ return self.raw.get("output_tokens")
45
+
46
+ @property
47
+ def total_tokens(self) -> Optional[int]:
48
+ return self.raw.get("total_tokens")
49
+
50
+ @property
51
+ def cost(self) -> Optional[float]:
52
+ return self.raw.get("cost")
53
+
54
+
55
+ class ChatCompletion(GtwyObject):
56
+ """Result of :meth:`gtwy.resources.chat.Completions.create`.
57
+
58
+ Wraps the synchronous ``{"success": true, "response": {"data": ..., "usage": ...}}``
59
+ envelope. When the bridge is configured for async (webhook) delivery, the
60
+ API instead returns a queuing acknowledgement — in that case ``content``
61
+ is ``None`` and ``message_id`` is the id to correlate with the
62
+ out-of-band delivery.
63
+ """
64
+
65
+ @property
66
+ def queued(self) -> bool:
67
+ return "response" not in self.raw
68
+
69
+ @property
70
+ def message_id(self) -> Optional[str]:
71
+ if self.queued:
72
+ return self.raw.get("message_id")
73
+ return self.data.get("message_id")
74
+
75
+ @property
76
+ def data(self) -> Dict[str, Any]:
77
+ return self.raw.get("response", {}).get("data", {}) or {}
78
+
79
+ @property
80
+ def content(self) -> Optional[str]:
81
+ content = self.data.get("content")
82
+ if isinstance(content, list):
83
+ return "\n".join(
84
+ chunk.get("text", "") if isinstance(chunk, dict) else str(chunk)
85
+ for chunk in content
86
+ ).strip()
87
+ return content
88
+
89
+ @property
90
+ def model(self) -> Optional[str]:
91
+ return self.data.get("model")
92
+
93
+ @property
94
+ def images(self) -> Optional[list]:
95
+ return self.data.get("images")
96
+
97
+ @property
98
+ def tools_data(self) -> Dict[str, Any]:
99
+ return self.data.get("tools_data", {}) or {}
100
+
101
+ @property
102
+ def usage(self) -> Usage:
103
+ return Usage(self.raw.get("response", {}).get("usage", {}) or {})
104
+
105
+
106
+ class BatchCompletion(GtwyObject):
107
+ @property
108
+ def batch_id(self) -> Optional[str]:
109
+ return self.raw.get("batch_id")
110
+
111
+ @property
112
+ def messages(self) -> Optional[list]:
113
+ return self.raw.get("messages")
114
+
115
+
116
+ class RerunResult(GtwyObject):
117
+ @property
118
+ def queued(self) -> list:
119
+ return self.raw.get("queued", [])
120
+
121
+ @property
122
+ def not_found(self) -> list:
123
+ return self.raw.get("not_found", [])
@@ -0,0 +1,19 @@
1
+ """Access to the packaged SKILLS.md reference doc.
2
+
3
+ SKILLS.md ships inside the installed package (see pyproject.toml
4
+ package-data) specifically so it survives a plain `pip install gtwy` and not
5
+ just an editable checkout of this repo — an agent or IDE can locate it at
6
+ runtime via `gtwy.SKILLS_PATH` / `gtwy.get_skills_doc()` without needing the
7
+ source repo on disk.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from pathlib import Path
13
+
14
+ SKILLS_PATH: Path = Path(__file__).parent / "SKILLS.md"
15
+
16
+
17
+ def get_skills_doc() -> str:
18
+ """Return the full contents of SKILLS.md as a string."""
19
+ return SKILLS_PATH.read_text(encoding="utf-8")
@@ -0,0 +1,47 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Optional
4
+
5
+ import requests
6
+
7
+ from ._base_client import DEFAULT_MAX_RETRIES, BaseClient
8
+ from .resources import Batch, Chat, Rerun
9
+
10
+
11
+ class Gtwy(BaseClient):
12
+ """Client for the GTWY API.
13
+
14
+ Example:
15
+ >>> from gtwy import Gtwy
16
+ >>> client = Gtwy(api_key="<pauthkey>")
17
+ >>> response = client.chat.completions.create(
18
+ ... agent_id="...",
19
+ ... user="Hello!",
20
+ ... )
21
+ >>> print(response.content)
22
+
23
+ ``api_key`` is the org-level ``pauthkey`` from your GTWY dashboard (falls
24
+ back to the ``GTWY_API_KEY`` env var). ``base_url`` defaults to
25
+ ``https://api.gtwy.ai``; override it (or set ``GTWY_BASE_URL``) to point
26
+ at a different environment. See SKILLS.md for the full API reference and
27
+ every resource this client exposes.
28
+ """
29
+
30
+ def __init__(
31
+ self,
32
+ api_key: Optional[str] = None,
33
+ base_url: Optional[str] = None,
34
+ timeout: Optional[float] = None,
35
+ max_retries: int = DEFAULT_MAX_RETRIES,
36
+ session: Optional[requests.Session] = None,
37
+ ):
38
+ super().__init__(
39
+ api_key=api_key,
40
+ base_url=base_url,
41
+ timeout=timeout,
42
+ max_retries=max_retries,
43
+ session=session,
44
+ )
45
+ self.chat = Chat(self)
46
+ self.batch = Batch(self)
47
+ self.rerun = Rerun(self)
@@ -0,0 +1,5 @@
1
+ from .chat import Chat
2
+ from .batch import Batch
3
+ from .rerun import Rerun
4
+
5
+ __all__ = ["Chat", "Batch", "Rerun"]
@@ -0,0 +1,50 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict, List, Optional
4
+
5
+ from .._models import BatchCompletion
6
+
7
+ _PATH = "/api/v2/model/batch/chat/completion"
8
+
9
+
10
+ class BatchCompletions:
11
+ def __init__(self, client):
12
+ self._client = client
13
+
14
+ def create(
15
+ self,
16
+ *,
17
+ agent_id: str,
18
+ batch: List[str],
19
+ batch_webhook: str,
20
+ batch_variables: Optional[List[Dict[str, Any]]] = None,
21
+ thread_id: Optional[str] = None,
22
+ configuration: Optional[Dict[str, Any]] = None,
23
+ **extra: Any,
24
+ ) -> BatchCompletion:
25
+ """Run many prompts against one bridge in a single call.
26
+
27
+ ``batch`` is the list of per-message prompts; if given,
28
+ ``batch_variables`` must be the same length. Results are delivered to
29
+ ``batch_webhook`` — the immediate return value is just an
30
+ acknowledgement (``response.batch_id``).
31
+ """
32
+ if batch_variables is not None and len(batch_variables) != len(batch):
33
+ raise ValueError("batch_variables must have the same length as batch")
34
+
35
+ body: Dict[str, Any] = {
36
+ "agent_id": agent_id,
37
+ "batch": batch,
38
+ "batch_webhook": batch_webhook,
39
+ "batch_variables": batch_variables,
40
+ "thread_id": thread_id,
41
+ "configuration": configuration,
42
+ **extra,
43
+ }
44
+ raw = self._client.post(_PATH, body)
45
+ return BatchCompletion(raw)
46
+
47
+
48
+ class Batch:
49
+ def __init__(self, client):
50
+ self.completions = BatchCompletions(client)
@@ -0,0 +1,93 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict, List, Optional
4
+
5
+ from .._models import ChatCompletion
6
+
7
+ _PATH = "/api/v2/model/chat/completion"
8
+
9
+
10
+ class Completions:
11
+ def __init__(self, client):
12
+ self._client = client
13
+
14
+ def create(
15
+ self,
16
+ *,
17
+ agent_id: str,
18
+ user: Optional[str] = None,
19
+ messages: Optional[List[Dict[str, Any]]] = None,
20
+ thread_id: Optional[str] = None,
21
+ sub_thread_id: Optional[str] = None,
22
+ variables: Optional[Dict[str, Any]] = None,
23
+ variables_path: Optional[str] = None,
24
+ configuration: Optional[Dict[str, Any]] = None,
25
+ version_id: Optional[str] = None,
26
+ service: Optional[str] = None,
27
+ apikey: Optional[str] = None,
28
+ user_urls: Optional[List[Dict[str, str]]] = None,
29
+ auto_model_select: Optional[bool] = None,
30
+ cache_on: Optional[bool] = None,
31
+ stream: Optional[bool] = None,
32
+ settings: Optional[Dict[str, Any]] = None,
33
+ extra_tools: Optional[List[Dict[str, Any]]] = None,
34
+ built_in_tools: Optional[List[Dict[str, Any]]] = None,
35
+ guardrails: Optional[Dict[str, Any]] = None,
36
+ web_search_filters: Optional[Dict[str, Any]] = None,
37
+ orchestrator_flag: Optional[bool] = None,
38
+ environment: Optional[str] = None,
39
+ **extra: Any,
40
+ ) -> ChatCompletion:
41
+ """Send one message to a bridge/agent and get its reply.
42
+
43
+ Exactly one of ``user`` or ``messages`` is normally supplied. By
44
+ default the call is synchronous and ``response.content`` has the
45
+ reply. If the bridge's delivery mode (``settings.response_format``)
46
+ is ``webhook``, the API instead queues the request and returns
47
+ immediately — check ``response.queued`` and correlate the async
48
+ result with ``response.message_id``. See SKILLS.md for the full
49
+ field reference and delivery modes.
50
+
51
+ ``settings`` is a general passthrough for the request's ``settings``
52
+ object, e.g. ``{"response_format": {"type": "webhook", "cred": {...}}}``
53
+ or ``{"fall_back": {"is_enable": True, "service": "openai", "model": "gpt-4o-mini"}}``.
54
+
55
+ Any keyword not listed here is passed through verbatim via
56
+ ``**extra`` for forwards-compatibility with new API fields.
57
+ """
58
+ body: Dict[str, Any] = {
59
+ "agent_id": agent_id,
60
+ "user": user,
61
+ "messages": messages,
62
+ "thread_id": thread_id,
63
+ "sub_thread_id": sub_thread_id,
64
+ "variables": variables,
65
+ "variables_path": variables_path,
66
+ "configuration": configuration,
67
+ "version_id": version_id,
68
+ "service": service,
69
+ "apikey": apikey,
70
+ "user_urls": user_urls,
71
+ "auto_model_select": auto_model_select,
72
+ "cache_on": cache_on,
73
+ "stream": stream,
74
+ "extra_tools": extra_tools,
75
+ "built_in_tools": built_in_tools,
76
+ "guardrails": guardrails,
77
+ "web_search_filters": web_search_filters,
78
+ "orchestrator_flag": orchestrator_flag,
79
+ "environment": environment,
80
+ **extra,
81
+ }
82
+ if settings is not None:
83
+ body["settings"] = settings
84
+
85
+ raw = self._client.post(_PATH, body)
86
+ return ChatCompletion(raw)
87
+
88
+
89
+ class Chat:
90
+ """Namespace mirroring ``client.chat.completions.create(...)``."""
91
+
92
+ def __init__(self, client):
93
+ self.completions = Completions(client)
@@ -0,0 +1,41 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict, List, Optional
4
+
5
+ from .._models import RerunResult
6
+
7
+ _PATH = "/api/v2/model/rerun"
8
+
9
+
10
+ class Rerun:
11
+ def __init__(self, client):
12
+ self._client = client
13
+
14
+ def create(
15
+ self,
16
+ *,
17
+ agent_id: str,
18
+ message_ids: Optional[List[str]] = None,
19
+ thread_id: Optional[str] = None,
20
+ sub_thread_id: Optional[str] = None,
21
+ **extra: Any,
22
+ ) -> RerunResult:
23
+ """Rerun previous message(s) with the bridge's current configuration.
24
+
25
+ Provide either ``message_ids``, or both ``thread_id`` and
26
+ ``sub_thread_id`` (reruns the last message in that thread). Results
27
+ are delivered per the bridge's configured ``response_format``, not
28
+ returned inline here.
29
+ """
30
+ if not message_ids and not (thread_id and sub_thread_id):
31
+ raise ValueError("Provide either message_ids, or both thread_id and sub_thread_id")
32
+
33
+ body: Dict[str, Any] = {
34
+ "agent_id": agent_id,
35
+ "message_ids": message_ids,
36
+ "thread_id": thread_id,
37
+ "sub_thread_id": sub_thread_id,
38
+ **extra,
39
+ }
40
+ raw = self._client.post(_PATH, body)
41
+ return RerunResult(raw)
@@ -0,0 +1,59 @@
1
+ Metadata-Version: 2.4
2
+ Name: gtwy-sdk
3
+ Version: 0.1.0
4
+ Summary: Official Python SDK for the GTWY API — build and call agentic AI, RAG, and LLM-powered agents
5
+ Author: GTWY
6
+ License: MIT
7
+ Project-URL: Homepage, https://gtwy.ai
8
+ Keywords: gtwy,ai,llm,agents,agentic ai,rag,chatbot,sdk,api client
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3 :: Only
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
15
+ Requires-Python: >=3.8
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Requires-Dist: requests<3,>=2.25
19
+ Dynamic: license-file
20
+
21
+ # gtwy-sdk
22
+
23
+ Official Python client for the GTWY API — build and call agentic AI, RAG,
24
+ and LLM-powered agents.
25
+
26
+ ```bash
27
+ pip install gtwy-sdk
28
+ ```
29
+
30
+ ```python
31
+ from gtwy import Gtwy
32
+
33
+ client = Gtwy(api_key="<pauthkey>")
34
+
35
+ response = client.chat.completions.create(
36
+ agent_id="my-agent-id",
37
+ user="What's the capital of France?",
38
+ )
39
+ print(response.content)
40
+ ```
41
+
42
+ `api_key` can also come from the `GTWY_API_KEY` environment variable.
43
+ `base_url` defaults to `https://api.gtwy.ai`; override it (constructor arg or
44
+ `GTWY_BASE_URL`) to point at a different environment.
45
+
46
+ See [gtwy/SKILLS.md](./gtwy/SKILLS.md) for the full reference: every resource
47
+ (`chat`, `batch`, `rerun`), every request field, response
48
+ shapes, delivery modes (sync/webhook), and error handling. This file
49
+ ships **inside** the installed package (not just this repo) — after
50
+ `pip install gtwy-sdk` it's reachable at `gtwy.SKILLS_PATH` /
51
+ `gtwy.get_skills_doc()`, so an AI agent (e.g. Claude) can read it and use
52
+ this SDK correctly without ever cloning this repo.
53
+
54
+ ## Development
55
+
56
+ ```bash
57
+ cd python
58
+ pip install -e .
59
+ ```
@@ -0,0 +1,20 @@
1
+ LICENSE
2
+ MANIFEST.in
3
+ README.md
4
+ pyproject.toml
5
+ gtwy/SKILLS.md
6
+ gtwy/__init__.py
7
+ gtwy/_base_client.py
8
+ gtwy/_exceptions.py
9
+ gtwy/_models.py
10
+ gtwy/_skills.py
11
+ gtwy/client.py
12
+ gtwy/resources/__init__.py
13
+ gtwy/resources/batch.py
14
+ gtwy/resources/chat.py
15
+ gtwy/resources/rerun.py
16
+ gtwy_sdk.egg-info/PKG-INFO
17
+ gtwy_sdk.egg-info/SOURCES.txt
18
+ gtwy_sdk.egg-info/dependency_links.txt
19
+ gtwy_sdk.egg-info/requires.txt
20
+ gtwy_sdk.egg-info/top_level.txt
@@ -0,0 +1 @@
1
+ requests<3,>=2.25
@@ -0,0 +1 @@
1
+ gtwy
@@ -0,0 +1,43 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "gtwy-sdk"
7
+ version = "0.1.0"
8
+ description = "Official Python SDK for the GTWY API — build and call agentic AI, RAG, and LLM-powered agents"
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "GTWY" }]
13
+ keywords = [
14
+ "gtwy",
15
+ "ai",
16
+ "llm",
17
+ "agents",
18
+ "agentic ai",
19
+ "rag",
20
+ "chatbot",
21
+ "sdk",
22
+ "api client",
23
+ ]
24
+ classifiers = [
25
+ "License :: OSI Approved :: MIT License",
26
+ "Programming Language :: Python :: 3",
27
+ "Programming Language :: Python :: 3 :: Only",
28
+ "Operating System :: OS Independent",
29
+ "Intended Audience :: Developers",
30
+ "Topic :: Software Development :: Libraries :: Python Modules",
31
+ ]
32
+ dependencies = [
33
+ "requests>=2.25,<3",
34
+ ]
35
+
36
+ [project.urls]
37
+ Homepage = "https://gtwy.ai"
38
+
39
+ [tool.setuptools.packages.find]
40
+ include = ["gtwy*"]
41
+
42
+ [tool.setuptools.package-data]
43
+ gtwy = ["SKILLS.md"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+