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 +21 -0
- gtwy_sdk-0.1.0/MANIFEST.in +1 -0
- gtwy_sdk-0.1.0/PKG-INFO +59 -0
- gtwy_sdk-0.1.0/README.md +39 -0
- gtwy_sdk-0.1.0/gtwy/SKILLS.md +332 -0
- gtwy_sdk-0.1.0/gtwy/__init__.py +37 -0
- gtwy_sdk-0.1.0/gtwy/_base_client.py +97 -0
- gtwy_sdk-0.1.0/gtwy/_exceptions.py +75 -0
- gtwy_sdk-0.1.0/gtwy/_models.py +123 -0
- gtwy_sdk-0.1.0/gtwy/_skills.py +19 -0
- gtwy_sdk-0.1.0/gtwy/client.py +47 -0
- gtwy_sdk-0.1.0/gtwy/resources/__init__.py +5 -0
- gtwy_sdk-0.1.0/gtwy/resources/batch.py +50 -0
- gtwy_sdk-0.1.0/gtwy/resources/chat.py +93 -0
- gtwy_sdk-0.1.0/gtwy/resources/rerun.py +41 -0
- gtwy_sdk-0.1.0/gtwy_sdk.egg-info/PKG-INFO +59 -0
- gtwy_sdk-0.1.0/gtwy_sdk.egg-info/SOURCES.txt +20 -0
- gtwy_sdk-0.1.0/gtwy_sdk.egg-info/dependency_links.txt +1 -0
- gtwy_sdk-0.1.0/gtwy_sdk.egg-info/requires.txt +1 -0
- gtwy_sdk-0.1.0/gtwy_sdk.egg-info/top_level.txt +1 -0
- gtwy_sdk-0.1.0/pyproject.toml +43 -0
- gtwy_sdk-0.1.0/setup.cfg +4 -0
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
|
gtwy_sdk-0.1.0/PKG-INFO
ADDED
|
@@ -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
|
+
```
|
gtwy_sdk-0.1.0/README.md
ADDED
|
@@ -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,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
|
+
|
|
@@ -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"]
|
gtwy_sdk-0.1.0/setup.cfg
ADDED