general-augment-sdk 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- genaug/__init__.py +20 -0
- genaug/agent.py +48 -0
- genaug/client.py +726 -0
- genaug/identity.py +37 -0
- genaug/tools.py +26 -0
- general_augment_sdk-0.1.0.dist-info/METADATA +165 -0
- general_augment_sdk-0.1.0.dist-info/RECORD +8 -0
- general_augment_sdk-0.1.0.dist-info/WHEEL +4 -0
genaug/__init__.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""General Augment Python SDK public import path."""
|
|
2
|
+
|
|
3
|
+
from genaug.agent import AgentClient
|
|
4
|
+
from genaug.client import (
|
|
5
|
+
GeneralAugmentAPIError,
|
|
6
|
+
GeneralAugmentClient,
|
|
7
|
+
response_output_text,
|
|
8
|
+
response_structured_output,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
__version__ = "0.1.0"
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"AgentClient",
|
|
15
|
+
"GeneralAugmentAPIError",
|
|
16
|
+
"GeneralAugmentClient",
|
|
17
|
+
"__version__",
|
|
18
|
+
"response_output_text",
|
|
19
|
+
"response_structured_output",
|
|
20
|
+
]
|
genaug/agent.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Agent test helpers for General Augment SDK users."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from genaug.client import GeneralAugmentClient
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AgentClient:
|
|
11
|
+
"""Convenience wrapper scoped to one General Augment project."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, client: GeneralAugmentClient, project_id: str) -> None:
|
|
14
|
+
"""Initialize a project-scoped agent client."""
|
|
15
|
+
self.client = client
|
|
16
|
+
self.project_id = project_id
|
|
17
|
+
|
|
18
|
+
def test(
|
|
19
|
+
self,
|
|
20
|
+
message: str,
|
|
21
|
+
*,
|
|
22
|
+
phone_e164: str = "+15550000000",
|
|
23
|
+
channel: str = "whatsapp",
|
|
24
|
+
) -> dict[str, Any]:
|
|
25
|
+
"""Send a test message to the configured agent."""
|
|
26
|
+
return self.client.test_agent(
|
|
27
|
+
self.project_id,
|
|
28
|
+
message,
|
|
29
|
+
phone_e164=phone_e164,
|
|
30
|
+
channel=channel,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test(
|
|
35
|
+
client: GeneralAugmentClient,
|
|
36
|
+
project_id: str,
|
|
37
|
+
message: str,
|
|
38
|
+
*,
|
|
39
|
+
phone_e164: str = "+15550000000",
|
|
40
|
+
channel: str = "whatsapp",
|
|
41
|
+
) -> dict[str, Any]:
|
|
42
|
+
"""Send a one-off test message to a General Augment project."""
|
|
43
|
+
return client.test_agent(
|
|
44
|
+
project_id,
|
|
45
|
+
message,
|
|
46
|
+
phone_e164=phone_e164,
|
|
47
|
+
channel=channel,
|
|
48
|
+
)
|
genaug/client.py
ADDED
|
@@ -0,0 +1,726 @@
|
|
|
1
|
+
"""Typed API client for General Augment.
|
|
2
|
+
|
|
3
|
+
The SDK targets the public admin and integration APIs exposed by the General Augment platform:
|
|
4
|
+
|
|
5
|
+
- `/api/v1/admin/*` for project, usage, logs, config, and test-message operations
|
|
6
|
+
- `/api/v1/integrations/*` for app-user identity linking
|
|
7
|
+
|
|
8
|
+
It also wraps `/v1/responses` and `/api/v1/agent/memory/*` for app backend
|
|
9
|
+
integrations. See `docs/public/SDK-REFERENCE.md` in the monorepo for end-to-end
|
|
10
|
+
examples.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json as json_module
|
|
16
|
+
from collections.abc import Iterator, Mapping
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any
|
|
19
|
+
from urllib.parse import quote
|
|
20
|
+
|
|
21
|
+
import httpx
|
|
22
|
+
|
|
23
|
+
ADMIN_API_KEY_HEADER = "X-Admin-Key"
|
|
24
|
+
BEARER_AUTH_HEADER = "Authorization"
|
|
25
|
+
ADMIN_PREFIX = "/api/v1/admin"
|
|
26
|
+
INTEGRATIONS_PREFIX = "/api/v1/integrations"
|
|
27
|
+
DEFAULT_BASE_URL = "https://api.generalaugment.com"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class GeneralAugmentAPIError(RuntimeError):
|
|
31
|
+
"""Raised when the General Augment API returns an error response."""
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
status_code: int,
|
|
36
|
+
detail: str,
|
|
37
|
+
*,
|
|
38
|
+
code: str | None = None,
|
|
39
|
+
reason: str | None = None,
|
|
40
|
+
request_id: str | None = None,
|
|
41
|
+
trace_id: str | None = None,
|
|
42
|
+
retry_after: str | None = None,
|
|
43
|
+
rate_limit: Mapping[str, str] | None = None,
|
|
44
|
+
body: Any | None = None,
|
|
45
|
+
) -> None:
|
|
46
|
+
"""Create an API error with the HTTP status and response detail."""
|
|
47
|
+
super().__init__(f"General Augment API returned {status_code}: {detail}")
|
|
48
|
+
self.status_code = status_code
|
|
49
|
+
self.detail = detail
|
|
50
|
+
self.code = code
|
|
51
|
+
self.reason = reason
|
|
52
|
+
self.request_id = request_id
|
|
53
|
+
self.trace_id = trace_id
|
|
54
|
+
self.retry_after = retry_after
|
|
55
|
+
self.rate_limit = dict(rate_limit or {})
|
|
56
|
+
self.body = body
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class GeneralAugmentClient:
|
|
60
|
+
"""Synchronous client for the General Augment API.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
api_key: Admin API key. Project-scoped keys are supported.
|
|
64
|
+
base_url: General Augment API base URL.
|
|
65
|
+
timeout: Request timeout in seconds.
|
|
66
|
+
client: Optional injected `httpx.Client`, useful for tests.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
def __init__(
|
|
70
|
+
self,
|
|
71
|
+
api_key: str,
|
|
72
|
+
*,
|
|
73
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
74
|
+
timeout: float = 30.0,
|
|
75
|
+
client: httpx.Client | None = None,
|
|
76
|
+
) -> None:
|
|
77
|
+
"""Initialize the General Augment API client."""
|
|
78
|
+
self.api_key = api_key
|
|
79
|
+
self.base_url = base_url.rstrip("/")
|
|
80
|
+
self.timeout = timeout
|
|
81
|
+
self._client = client or httpx.Client(timeout=timeout)
|
|
82
|
+
self._owns_client = client is None
|
|
83
|
+
|
|
84
|
+
def close(self) -> None:
|
|
85
|
+
"""Close the underlying HTTP client if the SDK created it."""
|
|
86
|
+
if self._owns_client:
|
|
87
|
+
self._client.close()
|
|
88
|
+
|
|
89
|
+
def __enter__(self) -> GeneralAugmentClient:
|
|
90
|
+
"""Return the context-managed client."""
|
|
91
|
+
return self
|
|
92
|
+
|
|
93
|
+
def __exit__(self, *_: object) -> None:
|
|
94
|
+
"""Close the client on context-manager exit."""
|
|
95
|
+
self.close()
|
|
96
|
+
|
|
97
|
+
def admin_request(
|
|
98
|
+
self,
|
|
99
|
+
method: str,
|
|
100
|
+
path: str,
|
|
101
|
+
*,
|
|
102
|
+
json: Mapping[str, Any] | None = None,
|
|
103
|
+
params: Mapping[str, Any] | None = None,
|
|
104
|
+
) -> Any:
|
|
105
|
+
"""Call an admin API endpoint and return decoded JSON."""
|
|
106
|
+
return self._request(method, f"{ADMIN_PREFIX}{path}", json=json, params=params)
|
|
107
|
+
|
|
108
|
+
def integration_request(
|
|
109
|
+
self,
|
|
110
|
+
method: str,
|
|
111
|
+
path: str,
|
|
112
|
+
*,
|
|
113
|
+
json: Mapping[str, Any] | None = None,
|
|
114
|
+
params: Mapping[str, Any] | None = None,
|
|
115
|
+
) -> Any:
|
|
116
|
+
"""Call a developer integration API endpoint and return decoded JSON."""
|
|
117
|
+
return self._request(method, f"{INTEGRATIONS_PREFIX}{path}", json=json, params=params)
|
|
118
|
+
|
|
119
|
+
def list_projects(
|
|
120
|
+
self,
|
|
121
|
+
*,
|
|
122
|
+
limit: int | None = None,
|
|
123
|
+
offset: int | None = None,
|
|
124
|
+
) -> list[dict[str, Any]]:
|
|
125
|
+
"""Return projects visible to this API key."""
|
|
126
|
+
payload = self.admin_request(
|
|
127
|
+
"GET",
|
|
128
|
+
"/projects",
|
|
129
|
+
params=_defined_params(
|
|
130
|
+
{
|
|
131
|
+
"limit": limit,
|
|
132
|
+
"offset": offset,
|
|
133
|
+
}
|
|
134
|
+
),
|
|
135
|
+
)
|
|
136
|
+
if isinstance(payload, dict):
|
|
137
|
+
items = payload.get("items", [])
|
|
138
|
+
return [item for item in items if isinstance(item, dict)]
|
|
139
|
+
return []
|
|
140
|
+
|
|
141
|
+
def create_response(
|
|
142
|
+
self,
|
|
143
|
+
payload: Mapping[str, Any],
|
|
144
|
+
*,
|
|
145
|
+
idempotency_key: str | None = None,
|
|
146
|
+
request_id: str | None = None,
|
|
147
|
+
traceparent: str | None = None,
|
|
148
|
+
tracestate: str | None = None,
|
|
149
|
+
) -> dict[str, Any]:
|
|
150
|
+
"""Create one Responses-compatible General Augment turn."""
|
|
151
|
+
return _as_dict(
|
|
152
|
+
self._request(
|
|
153
|
+
"POST",
|
|
154
|
+
"/v1/responses",
|
|
155
|
+
json=payload,
|
|
156
|
+
headers=_response_headers(
|
|
157
|
+
idempotency_key=idempotency_key,
|
|
158
|
+
request_id=request_id,
|
|
159
|
+
traceparent=traceparent,
|
|
160
|
+
tracestate=tracestate,
|
|
161
|
+
),
|
|
162
|
+
auth="bearer",
|
|
163
|
+
)
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
def stream_response(
|
|
167
|
+
self,
|
|
168
|
+
payload: Mapping[str, Any],
|
|
169
|
+
*,
|
|
170
|
+
idempotency_key: str | None = None,
|
|
171
|
+
request_id: str | None = None,
|
|
172
|
+
traceparent: str | None = None,
|
|
173
|
+
tracestate: str | None = None,
|
|
174
|
+
) -> Iterator[dict[str, Any]]:
|
|
175
|
+
"""Stream semantic Responses SSE events."""
|
|
176
|
+
body = dict(payload)
|
|
177
|
+
body["stream"] = True
|
|
178
|
+
try:
|
|
179
|
+
with self._client.stream(
|
|
180
|
+
"POST",
|
|
181
|
+
f"{self.base_url}/v1/responses",
|
|
182
|
+
headers=self._headers(
|
|
183
|
+
_response_headers(
|
|
184
|
+
idempotency_key=idempotency_key,
|
|
185
|
+
request_id=request_id,
|
|
186
|
+
traceparent=traceparent,
|
|
187
|
+
tracestate=tracestate,
|
|
188
|
+
),
|
|
189
|
+
auth="bearer",
|
|
190
|
+
),
|
|
191
|
+
json=body,
|
|
192
|
+
) as response:
|
|
193
|
+
if response.is_error:
|
|
194
|
+
response.read()
|
|
195
|
+
raise _api_error_from_response(response)
|
|
196
|
+
yield from _iter_sse_events(response.iter_lines())
|
|
197
|
+
except (httpx.ConnectError, httpx.TimeoutException, httpx.RequestError) as exc:
|
|
198
|
+
raise _api_error_from_transport(exc) from exc
|
|
199
|
+
|
|
200
|
+
def store_memory(self, payload: Mapping[str, Any]) -> dict[str, Any]:
|
|
201
|
+
"""Store one durable memory fact for an app user."""
|
|
202
|
+
return _as_dict(
|
|
203
|
+
self._request(
|
|
204
|
+
"POST",
|
|
205
|
+
"/api/v1/agent/memory/store",
|
|
206
|
+
json=payload,
|
|
207
|
+
auth="bearer",
|
|
208
|
+
)
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
def search_memory(self, payload: Mapping[str, Any]) -> dict[str, Any]:
|
|
212
|
+
"""Search memory facts for an app user."""
|
|
213
|
+
return _as_dict(
|
|
214
|
+
self._request(
|
|
215
|
+
"POST",
|
|
216
|
+
"/api/v1/agent/memory/search",
|
|
217
|
+
json=payload,
|
|
218
|
+
auth="bearer",
|
|
219
|
+
)
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
def memory_profile(self, user_id: str) -> dict[str, Any]:
|
|
223
|
+
"""Return profile and recent facts for one app user."""
|
|
224
|
+
return _as_dict(
|
|
225
|
+
self._request(
|
|
226
|
+
"GET",
|
|
227
|
+
f"/api/v1/agent/memory/profile/{_path_segment(user_id)}",
|
|
228
|
+
auth="bearer",
|
|
229
|
+
)
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
def delete_memory(self, memory_id: str, *, user_id: str) -> dict[str, Any]:
|
|
233
|
+
"""Delete one memory fact for the scoped app user."""
|
|
234
|
+
return _as_dict(
|
|
235
|
+
self._request(
|
|
236
|
+
"DELETE",
|
|
237
|
+
f"/api/v1/agent/memory/{_path_segment(memory_id)}",
|
|
238
|
+
params={"user_id": user_id},
|
|
239
|
+
auth="bearer",
|
|
240
|
+
)
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
def purge_user_memory(self, user_id: str) -> dict[str, Any]:
|
|
244
|
+
"""Delete all memory facts for one app user."""
|
|
245
|
+
return _as_dict(
|
|
246
|
+
self._request(
|
|
247
|
+
"DELETE",
|
|
248
|
+
f"/api/v1/agent/memory/user/{_path_segment(user_id)}",
|
|
249
|
+
auth="bearer",
|
|
250
|
+
)
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
def get_project(self, project_id: str) -> dict[str, Any]:
|
|
254
|
+
"""Return one project by ID."""
|
|
255
|
+
return _as_dict(self.admin_request("GET", f"/projects/{_path_segment(project_id)}"))
|
|
256
|
+
|
|
257
|
+
def create_project_from_config(
|
|
258
|
+
self,
|
|
259
|
+
yaml_content: str,
|
|
260
|
+
*,
|
|
261
|
+
soul_content: str | None = None,
|
|
262
|
+
skills: list[str] | None = None,
|
|
263
|
+
) -> dict[str, Any]:
|
|
264
|
+
"""Create a project from a General Augment project YAML document."""
|
|
265
|
+
return _as_dict(
|
|
266
|
+
self.admin_request(
|
|
267
|
+
"POST",
|
|
268
|
+
"/projects/from-config",
|
|
269
|
+
json={
|
|
270
|
+
"yaml_content": yaml_content,
|
|
271
|
+
"soul_content": soul_content,
|
|
272
|
+
"skills": skills or [],
|
|
273
|
+
},
|
|
274
|
+
)
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
def deploy_config_file(self, config_path: str | Path) -> dict[str, Any]:
|
|
278
|
+
"""Create a project from a local General Augment project YAML file."""
|
|
279
|
+
content = Path(config_path).read_text(encoding="utf-8")
|
|
280
|
+
return self.create_project_from_config(content)
|
|
281
|
+
|
|
282
|
+
def update_project(self, project_id: str, **fields: Any) -> dict[str, Any]:
|
|
283
|
+
"""Patch mutable project fields."""
|
|
284
|
+
payload = {key: value for key, value in fields.items() if value is not None}
|
|
285
|
+
return _as_dict(
|
|
286
|
+
self.admin_request("PATCH", f"/projects/{_path_segment(project_id)}", json=payload)
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
def integration_prompt(self, project_id: str) -> str:
|
|
290
|
+
"""Return the copy-paste AI coding agent integration prompt."""
|
|
291
|
+
payload = _as_dict(
|
|
292
|
+
self.admin_request("GET", f"/projects/{_path_segment(project_id)}/integration-prompt")
|
|
293
|
+
)
|
|
294
|
+
return str(payload.get("prompt", ""))
|
|
295
|
+
|
|
296
|
+
def usage(
|
|
297
|
+
self,
|
|
298
|
+
project_id: str,
|
|
299
|
+
*,
|
|
300
|
+
start_date: str | None = None,
|
|
301
|
+
end_date: str | None = None,
|
|
302
|
+
) -> dict[str, Any]:
|
|
303
|
+
"""Return daily usage and billing aggregates for a project."""
|
|
304
|
+
params = {
|
|
305
|
+
key: value
|
|
306
|
+
for key, value in {"start_date": start_date, "end_date": end_date}.items()
|
|
307
|
+
if value is not None
|
|
308
|
+
}
|
|
309
|
+
return _as_dict(
|
|
310
|
+
self.admin_request("GET", f"/projects/{_path_segment(project_id)}/usage", params=params)
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
def test_agent(
|
|
314
|
+
self,
|
|
315
|
+
project_id: str,
|
|
316
|
+
message: str,
|
|
317
|
+
*,
|
|
318
|
+
phone_e164: str = "+15550000000",
|
|
319
|
+
channel: str = "whatsapp",
|
|
320
|
+
) -> dict[str, Any]:
|
|
321
|
+
"""Send a test message to an agent without using a live channel webhook."""
|
|
322
|
+
return _as_dict(
|
|
323
|
+
self.admin_request(
|
|
324
|
+
"POST",
|
|
325
|
+
f"/projects/{_path_segment(project_id)}/test",
|
|
326
|
+
json={"message": message, "phone_e164": phone_e164, "channel": channel},
|
|
327
|
+
)
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
def link_user(
|
|
331
|
+
self,
|
|
332
|
+
project_id: str,
|
|
333
|
+
*,
|
|
334
|
+
phone: str,
|
|
335
|
+
app_user_id: str,
|
|
336
|
+
provider_name: str = "app",
|
|
337
|
+
metadata: Mapping[str, Any] | None = None,
|
|
338
|
+
) -> dict[str, Any]:
|
|
339
|
+
"""Link an app user account to a WhatsApp/SMS phone number."""
|
|
340
|
+
return _as_dict(
|
|
341
|
+
self.integration_request(
|
|
342
|
+
"POST",
|
|
343
|
+
f"/{_path_segment(project_id)}/link-user",
|
|
344
|
+
json={
|
|
345
|
+
"phone_e164": phone,
|
|
346
|
+
"provider_user_id": app_user_id,
|
|
347
|
+
"provider_name": provider_name,
|
|
348
|
+
"metadata": dict(metadata or {}),
|
|
349
|
+
},
|
|
350
|
+
)
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
def resolve_user(self, project_id: str, phone: str) -> dict[str, Any]:
|
|
354
|
+
"""Resolve a linked phone number to the external app user ID."""
|
|
355
|
+
return _as_dict(
|
|
356
|
+
self.integration_request(
|
|
357
|
+
"GET",
|
|
358
|
+
f"/{_path_segment(project_id)}/resolve/{_path_segment(phone)}",
|
|
359
|
+
)
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
def unlink_user(self, project_id: str, phone: str) -> dict[str, Any]:
|
|
363
|
+
"""Remove a phone-to-app identity link."""
|
|
364
|
+
return _as_dict(
|
|
365
|
+
self.integration_request(
|
|
366
|
+
"DELETE",
|
|
367
|
+
f"/{_path_segment(project_id)}/unlink/{_path_segment(phone)}",
|
|
368
|
+
)
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
def register_openapi_tools(
|
|
372
|
+
self,
|
|
373
|
+
project_id: str,
|
|
374
|
+
spec_url: str,
|
|
375
|
+
*,
|
|
376
|
+
include_paths: list[str] | None = None,
|
|
377
|
+
exclude_paths: list[str] | None = None,
|
|
378
|
+
target_count: int = 15,
|
|
379
|
+
auto_deploy: bool = True,
|
|
380
|
+
) -> dict[str, Any]:
|
|
381
|
+
"""Ask General Augment to parse an OpenAPI spec and register curated generated tools."""
|
|
382
|
+
return _as_dict(
|
|
383
|
+
self.admin_request(
|
|
384
|
+
"POST",
|
|
385
|
+
f"/projects/{_path_segment(project_id)}/tools/from-openapi",
|
|
386
|
+
json={
|
|
387
|
+
"spec_url": spec_url,
|
|
388
|
+
"include_paths": include_paths or [],
|
|
389
|
+
"exclude_paths": exclude_paths or [],
|
|
390
|
+
"target_count": target_count,
|
|
391
|
+
"auto_deploy": auto_deploy,
|
|
392
|
+
},
|
|
393
|
+
)
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
def _request(
|
|
397
|
+
self,
|
|
398
|
+
method: str,
|
|
399
|
+
path: str,
|
|
400
|
+
*,
|
|
401
|
+
json: Mapping[str, Any] | None = None,
|
|
402
|
+
params: Mapping[str, Any] | None = None,
|
|
403
|
+
headers: Mapping[str, str] | None = None,
|
|
404
|
+
auth: str = "admin",
|
|
405
|
+
) -> Any:
|
|
406
|
+
"""Execute a raw request against the General Augment API."""
|
|
407
|
+
try:
|
|
408
|
+
response = self._client.request(
|
|
409
|
+
method,
|
|
410
|
+
f"{self.base_url}{path}",
|
|
411
|
+
headers=self._headers(headers, auth=auth),
|
|
412
|
+
json=json,
|
|
413
|
+
params=params,
|
|
414
|
+
)
|
|
415
|
+
except (httpx.ConnectError, httpx.TimeoutException, httpx.RequestError) as exc:
|
|
416
|
+
raise _api_error_from_transport(exc) from exc
|
|
417
|
+
if response.is_error:
|
|
418
|
+
raise _api_error_from_response(response)
|
|
419
|
+
if response.status_code == 204:
|
|
420
|
+
return None
|
|
421
|
+
return _success_body(response)
|
|
422
|
+
|
|
423
|
+
def _headers(self, extra: Mapping[str, str] | None = None, *, auth: str) -> dict[str, str]:
|
|
424
|
+
"""Build request headers for admin or project-key app calls."""
|
|
425
|
+
headers = {"Content-Type": "application/json"}
|
|
426
|
+
if auth == "bearer":
|
|
427
|
+
headers[BEARER_AUTH_HEADER] = f"Bearer {self.api_key}"
|
|
428
|
+
else:
|
|
429
|
+
headers[ADMIN_API_KEY_HEADER] = self.api_key
|
|
430
|
+
headers.update(dict(extra or {}))
|
|
431
|
+
return headers
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
__all__ = [
|
|
435
|
+
"GeneralAugmentAPIError",
|
|
436
|
+
"GeneralAugmentClient",
|
|
437
|
+
"response_output_text",
|
|
438
|
+
"response_structured_output",
|
|
439
|
+
]
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def response_output_text(response: Mapping[str, Any]) -> str:
|
|
443
|
+
"""Return concatenated assistant output text from a Responses object."""
|
|
444
|
+
output_text = response.get("output_text")
|
|
445
|
+
if isinstance(output_text, str):
|
|
446
|
+
return output_text
|
|
447
|
+
text_parts: list[str] = []
|
|
448
|
+
for part in _response_content_parts(response):
|
|
449
|
+
if not isinstance(part, Mapping):
|
|
450
|
+
continue
|
|
451
|
+
part_type = part.get("type")
|
|
452
|
+
if part_type in {"output_text", "text"} and isinstance(part.get("text"), str):
|
|
453
|
+
text_parts.append(part["text"])
|
|
454
|
+
return "".join(text_parts)
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def response_structured_output(response: Mapping[str, Any]) -> Any:
|
|
458
|
+
"""Return parsed structured output from a Responses object.
|
|
459
|
+
|
|
460
|
+
The API may expose parsed structured output directly on a content part or as
|
|
461
|
+
JSON text. This helper keeps app code from hand-walking the Responses shape.
|
|
462
|
+
"""
|
|
463
|
+
if "output_parsed" in response:
|
|
464
|
+
return response["output_parsed"]
|
|
465
|
+
for part in _response_content_parts(response):
|
|
466
|
+
if isinstance(part, Mapping) and "parsed" in part:
|
|
467
|
+
return part["parsed"]
|
|
468
|
+
text = response_output_text(response).strip()
|
|
469
|
+
if not text:
|
|
470
|
+
raise ValueError("Response output text is empty; no structured JSON to parse.")
|
|
471
|
+
try:
|
|
472
|
+
return json_module.loads(text)
|
|
473
|
+
except json_module.JSONDecodeError as exc:
|
|
474
|
+
raise ValueError("Response output text is not valid JSON.") from exc
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def _as_dict(payload: Any) -> dict[str, Any]:
|
|
478
|
+
"""Return a JSON object payload or fail with a useful SDK error."""
|
|
479
|
+
if isinstance(payload, dict):
|
|
480
|
+
return payload
|
|
481
|
+
raise TypeError(f"Expected General Augment API object response, got {type(payload).__name__}")
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def _response_content_parts(response: Mapping[str, Any]) -> Iterator[Any]:
|
|
485
|
+
"""Yield content parts from a Responses object."""
|
|
486
|
+
output = response.get("output")
|
|
487
|
+
if not isinstance(output, list):
|
|
488
|
+
return
|
|
489
|
+
for item in output:
|
|
490
|
+
if not isinstance(item, Mapping):
|
|
491
|
+
continue
|
|
492
|
+
content = item.get("content")
|
|
493
|
+
if not isinstance(content, list):
|
|
494
|
+
continue
|
|
495
|
+
yield from content
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def _path_segment(value: str) -> str:
|
|
499
|
+
"""Encode one URL path segment safely."""
|
|
500
|
+
return quote(value, safe="")
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
def _defined_params(params: Mapping[str, Any]) -> dict[str, Any]:
|
|
504
|
+
"""Return query params with omitted optional values removed."""
|
|
505
|
+
return {key: value for key, value in params.items() if value is not None}
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def _api_error_from_response(response: httpx.Response) -> GeneralAugmentAPIError:
|
|
509
|
+
"""Build a rich SDK exception from a General Augment error response."""
|
|
510
|
+
body = _response_body(response)
|
|
511
|
+
return GeneralAugmentAPIError(
|
|
512
|
+
response.status_code,
|
|
513
|
+
_error_detail(body, response.text),
|
|
514
|
+
code=_error_code(body),
|
|
515
|
+
reason=_error_reason(body),
|
|
516
|
+
request_id=response.headers.get("X-Request-ID") or _error_string(body, "request_id"),
|
|
517
|
+
trace_id=(
|
|
518
|
+
response.headers.get("X-Trace-ID")
|
|
519
|
+
or response.headers.get("X-Trace-Id")
|
|
520
|
+
or _error_string(body, "trace_id")
|
|
521
|
+
),
|
|
522
|
+
retry_after=response.headers.get("Retry-After") or _error_retry_after(body),
|
|
523
|
+
rate_limit=_rate_limit_headers(response.headers),
|
|
524
|
+
body=body,
|
|
525
|
+
)
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
def _api_error_from_transport(exc: httpx.HTTPError) -> GeneralAugmentAPIError:
|
|
529
|
+
"""Build a typed SDK exception for transport-level API failures."""
|
|
530
|
+
if isinstance(exc, httpx.TimeoutException):
|
|
531
|
+
detail = "General Augment API request timed out."
|
|
532
|
+
reason = "request_timeout"
|
|
533
|
+
elif isinstance(exc, httpx.ConnectError):
|
|
534
|
+
detail = "General Augment API could not be reached."
|
|
535
|
+
reason = "connection_failed"
|
|
536
|
+
else:
|
|
537
|
+
detail = "General Augment API request failed."
|
|
538
|
+
reason = "request_failed"
|
|
539
|
+
return GeneralAugmentAPIError(
|
|
540
|
+
0,
|
|
541
|
+
detail,
|
|
542
|
+
reason=reason,
|
|
543
|
+
body=None,
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
def _success_body(response: httpx.Response) -> Any:
|
|
548
|
+
"""Decode a successful API response or raise a typed SDK parse error."""
|
|
549
|
+
try:
|
|
550
|
+
return response.json()
|
|
551
|
+
except ValueError as exc:
|
|
552
|
+
raise GeneralAugmentAPIError(
|
|
553
|
+
response.status_code,
|
|
554
|
+
"General Augment API returned malformed JSON.",
|
|
555
|
+
reason="malformed_json",
|
|
556
|
+
request_id=response.headers.get("X-Request-ID"),
|
|
557
|
+
trace_id=response.headers.get("X-Trace-ID") or response.headers.get("X-Trace-Id"),
|
|
558
|
+
body=response.text,
|
|
559
|
+
) from exc
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
def _response_body(response: httpx.Response) -> Any:
|
|
563
|
+
"""Decode an error response body when it is JSON."""
|
|
564
|
+
try:
|
|
565
|
+
return response.json()
|
|
566
|
+
except ValueError:
|
|
567
|
+
return None
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
def _error_detail(body: Any, fallback: str) -> str:
|
|
571
|
+
"""Extract a compact error detail from a decoded response body."""
|
|
572
|
+
if isinstance(body, dict):
|
|
573
|
+
detail = body.get("detail")
|
|
574
|
+
if isinstance(detail, str):
|
|
575
|
+
return detail
|
|
576
|
+
if detail is not None:
|
|
577
|
+
return json_module.dumps(detail, sort_keys=True)
|
|
578
|
+
message = body.get("message")
|
|
579
|
+
if isinstance(message, str):
|
|
580
|
+
return message
|
|
581
|
+
return json_module.dumps(body, sort_keys=True)
|
|
582
|
+
if body is not None:
|
|
583
|
+
return str(body)
|
|
584
|
+
return fallback
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
def _error_reason(body: Any) -> str | None:
|
|
588
|
+
"""Return the stable reason/code field from an API error body when present."""
|
|
589
|
+
error = _structured_error(body)
|
|
590
|
+
if error is not None:
|
|
591
|
+
reason = error.get("reason")
|
|
592
|
+
if isinstance(reason, str):
|
|
593
|
+
return reason
|
|
594
|
+
code = error.get("code")
|
|
595
|
+
if isinstance(code, str):
|
|
596
|
+
return code
|
|
597
|
+
if not isinstance(body, dict):
|
|
598
|
+
return None
|
|
599
|
+
for key in ("reason", "code", "error"):
|
|
600
|
+
value = body.get(key)
|
|
601
|
+
if isinstance(value, str):
|
|
602
|
+
return value
|
|
603
|
+
return None
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
def _error_code(body: Any) -> str | None:
|
|
607
|
+
"""Return a machine-readable API error code when present."""
|
|
608
|
+
error = _structured_error(body)
|
|
609
|
+
if error is not None:
|
|
610
|
+
code = error.get("code")
|
|
611
|
+
if isinstance(code, str):
|
|
612
|
+
return code
|
|
613
|
+
if isinstance(body, dict):
|
|
614
|
+
code = body.get("code")
|
|
615
|
+
if isinstance(code, str):
|
|
616
|
+
return code
|
|
617
|
+
return None
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
def _error_string(body: Any, key: str) -> str | None:
|
|
621
|
+
"""Return one string field from structured or flat API error bodies."""
|
|
622
|
+
error = _structured_error(body)
|
|
623
|
+
if error is not None:
|
|
624
|
+
value = error.get(key)
|
|
625
|
+
if isinstance(value, str):
|
|
626
|
+
return value
|
|
627
|
+
if isinstance(body, dict):
|
|
628
|
+
value = body.get(key)
|
|
629
|
+
if isinstance(value, str):
|
|
630
|
+
return value
|
|
631
|
+
return None
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
def _error_retry_after(body: Any) -> str | None:
|
|
635
|
+
"""Return retry-after seconds from structured error JSON when present."""
|
|
636
|
+
for key in ("retry_after", "retry_after_seconds"):
|
|
637
|
+
value = _error_string(body, key)
|
|
638
|
+
if value is not None:
|
|
639
|
+
return value
|
|
640
|
+
error = _structured_error(body)
|
|
641
|
+
if error is not None and isinstance(error.get(key), (int, float)):
|
|
642
|
+
return str(error[key])
|
|
643
|
+
if isinstance(body, dict) and isinstance(body.get(key), (int, float)):
|
|
644
|
+
return str(body[key])
|
|
645
|
+
return None
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
def _structured_error(body: Any) -> dict[str, Any] | None:
|
|
649
|
+
"""Return the nested or flat structured API error object when present."""
|
|
650
|
+
if not isinstance(body, dict):
|
|
651
|
+
return None
|
|
652
|
+
detail = body.get("detail")
|
|
653
|
+
if isinstance(detail, dict):
|
|
654
|
+
return detail
|
|
655
|
+
error = body.get("error")
|
|
656
|
+
if isinstance(error, dict):
|
|
657
|
+
return error
|
|
658
|
+
if any(isinstance(body.get(key), str) for key in ("code", "reason", "message")):
|
|
659
|
+
return body
|
|
660
|
+
return None
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
def _rate_limit_headers(headers: httpx.Headers) -> dict[str, str]:
|
|
664
|
+
"""Return rate-limit headers using stable snake_case keys."""
|
|
665
|
+
mapping = {
|
|
666
|
+
"limit": "X-RateLimit-Limit",
|
|
667
|
+
"remaining": "X-RateLimit-Remaining",
|
|
668
|
+
"reset": "X-RateLimit-Reset",
|
|
669
|
+
"policy": "X-RateLimit-Policy",
|
|
670
|
+
}
|
|
671
|
+
return {key: value for key, header in mapping.items() if (value := headers.get(header))}
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
def _response_headers(
|
|
675
|
+
*,
|
|
676
|
+
idempotency_key: str | None,
|
|
677
|
+
request_id: str | None,
|
|
678
|
+
traceparent: str | None,
|
|
679
|
+
tracestate: str | None,
|
|
680
|
+
) -> dict[str, str]:
|
|
681
|
+
"""Build optional Responses correlation headers."""
|
|
682
|
+
headers: dict[str, str] = {}
|
|
683
|
+
if idempotency_key:
|
|
684
|
+
headers["X-Idempotency-Key"] = idempotency_key
|
|
685
|
+
if request_id:
|
|
686
|
+
headers["X-Request-ID"] = request_id
|
|
687
|
+
if traceparent:
|
|
688
|
+
headers["traceparent"] = traceparent
|
|
689
|
+
if tracestate:
|
|
690
|
+
headers["tracestate"] = tracestate
|
|
691
|
+
return headers
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
def _iter_sse_events(lines: Iterator[str]) -> Iterator[dict[str, Any]]:
|
|
695
|
+
"""Parse semantic SSE events from an iterator of text lines."""
|
|
696
|
+
event = "message"
|
|
697
|
+
data_lines: list[str] = []
|
|
698
|
+
for line in lines:
|
|
699
|
+
if line == "":
|
|
700
|
+
parsed = _sse_event(event, data_lines)
|
|
701
|
+
if parsed is not None:
|
|
702
|
+
yield parsed
|
|
703
|
+
event = "message"
|
|
704
|
+
data_lines = []
|
|
705
|
+
continue
|
|
706
|
+
if line.startswith(":"):
|
|
707
|
+
continue
|
|
708
|
+
if line.startswith("event:"):
|
|
709
|
+
event = line.removeprefix("event:").strip()
|
|
710
|
+
elif line.startswith("data:"):
|
|
711
|
+
data_lines.append(line.removeprefix("data:").lstrip())
|
|
712
|
+
parsed = _sse_event(event, data_lines)
|
|
713
|
+
if parsed is not None:
|
|
714
|
+
yield parsed
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
def _sse_event(event: str, data_lines: list[str]) -> dict[str, Any] | None:
|
|
718
|
+
"""Return one parsed SSE event or None for empty blocks."""
|
|
719
|
+
if not data_lines:
|
|
720
|
+
return None
|
|
721
|
+
data = "\n".join(data_lines)
|
|
722
|
+
try:
|
|
723
|
+
parsed_data: Any = json_module.loads(data)
|
|
724
|
+
except json_module.JSONDecodeError:
|
|
725
|
+
parsed_data = data
|
|
726
|
+
return {"event": event, "data": parsed_data}
|
genaug/identity.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Identity-linking helpers for app backends."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from genaug.client import GeneralAugmentClient
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def link_user(
|
|
12
|
+
client: GeneralAugmentClient,
|
|
13
|
+
project_id: str,
|
|
14
|
+
*,
|
|
15
|
+
phone: str,
|
|
16
|
+
app_user_id: str,
|
|
17
|
+
provider_name: str = "app",
|
|
18
|
+
metadata: Mapping[str, Any] | None = None,
|
|
19
|
+
) -> dict[str, Any]:
|
|
20
|
+
"""Link a WhatsApp/SMS phone number to the app's user ID."""
|
|
21
|
+
return client.link_user(
|
|
22
|
+
project_id,
|
|
23
|
+
phone=phone,
|
|
24
|
+
app_user_id=app_user_id,
|
|
25
|
+
provider_name=provider_name,
|
|
26
|
+
metadata=metadata,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def resolve_user(client: GeneralAugmentClient, project_id: str, *, phone: str) -> dict[str, Any]:
|
|
31
|
+
"""Resolve a linked phone number to app identity metadata."""
|
|
32
|
+
return client.resolve_user(project_id, phone)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def unlink_user(client: GeneralAugmentClient, project_id: str, *, phone: str) -> dict[str, Any]:
|
|
36
|
+
"""Remove a phone-to-app identity link."""
|
|
37
|
+
return client.unlink_user(project_id, phone)
|
genaug/tools.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Tool registration helpers for General Augment integrations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from genaug.client import GeneralAugmentClient
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def register_from_openapi(
|
|
9
|
+
spec_url: str,
|
|
10
|
+
*,
|
|
11
|
+
client: GeneralAugmentClient,
|
|
12
|
+
project_id: str,
|
|
13
|
+
include_paths: list[str] | None = None,
|
|
14
|
+
exclude_paths: list[str] | None = None,
|
|
15
|
+
target_count: int = 15,
|
|
16
|
+
auto_deploy: bool = True,
|
|
17
|
+
) -> dict[str, object]:
|
|
18
|
+
"""Generate and register curated MCP tools from an OpenAPI specification."""
|
|
19
|
+
return client.register_openapi_tools(
|
|
20
|
+
project_id,
|
|
21
|
+
spec_url,
|
|
22
|
+
include_paths=include_paths,
|
|
23
|
+
exclude_paths=exclude_paths,
|
|
24
|
+
target_count=target_count,
|
|
25
|
+
auto_deploy=auto_deploy,
|
|
26
|
+
)
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: general-augment-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for the General Augment admin and integration APIs.
|
|
5
|
+
Project-URL: Documentation, https://docs.generalaugment.com
|
|
6
|
+
Project-URL: Source, https://github.com/Bikz/general-augment-platform
|
|
7
|
+
Author: General Augment
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Requires-Dist: httpx>=0.28.0
|
|
11
|
+
Requires-Dist: pyyaml>=6.0.0
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# General Augment Python SDK
|
|
15
|
+
|
|
16
|
+
Backend SDK for General Augment app integrations. Use it from trusted server code.
|
|
17
|
+
Project-scoped keys are for app traffic such as Responses and memory calls. Admin and
|
|
18
|
+
setup helpers require a management/admin-capable key and send it as `X-Admin-Key`.
|
|
19
|
+
|
|
20
|
+
During private beta, package publishing may not be available in every package index. If
|
|
21
|
+
installing the package fails, use the repository package path for local tests or raw HTTP
|
|
22
|
+
examples in the public docs until `scripts/package-registry-readiness.py` reports the
|
|
23
|
+
expected package version as published.
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install general-augment-sdk
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
import os
|
|
31
|
+
|
|
32
|
+
from genaug import (
|
|
33
|
+
GeneralAugmentClient,
|
|
34
|
+
__version__,
|
|
35
|
+
response_output_text,
|
|
36
|
+
response_structured_output,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
client = GeneralAugmentClient(
|
|
40
|
+
api_key=os.environ["GENAUG_API_KEY"],
|
|
41
|
+
base_url=os.getenv("GENAUG_API_BASE_URL", "https://api.generalaugment.com"),
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
print(f"General Augment SDK {__version__}")
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Responses
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
response = client.create_response(
|
|
51
|
+
{
|
|
52
|
+
"model": "balanced",
|
|
53
|
+
"user": "app-user-123",
|
|
54
|
+
"input": "Reply with a concise onboarding summary.",
|
|
55
|
+
"metadata": {"feature": "onboarding"},
|
|
56
|
+
},
|
|
57
|
+
idempotency_key="onboarding-turn-1",
|
|
58
|
+
request_id="req_app_123",
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
print(response_output_text(response))
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Structured output:
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
structured_response = client.create_response(
|
|
68
|
+
{
|
|
69
|
+
"model": "balanced",
|
|
70
|
+
"user": "app-user-123",
|
|
71
|
+
"input": "Extract the user's preference: window seat.",
|
|
72
|
+
"text": {
|
|
73
|
+
"format": {
|
|
74
|
+
"type": "json_schema",
|
|
75
|
+
"name": "preference",
|
|
76
|
+
"strict": True,
|
|
77
|
+
"schema": {
|
|
78
|
+
"type": "object",
|
|
79
|
+
"required": ["seat"],
|
|
80
|
+
"properties": {"seat": {"type": "string"}},
|
|
81
|
+
"additionalProperties": False,
|
|
82
|
+
},
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
}
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
preference = response_structured_output(structured_response)
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Streaming:
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
for event in client.stream_response(
|
|
95
|
+
{
|
|
96
|
+
"model": "balanced",
|
|
97
|
+
"user": "app-user-123",
|
|
98
|
+
"input": "Draft a two sentence welcome message.",
|
|
99
|
+
}
|
|
100
|
+
):
|
|
101
|
+
if event["event"] == "response.output_text.delta":
|
|
102
|
+
print(event["data"].get("delta", ""), end="")
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Memory
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
stored = client.store_memory(
|
|
109
|
+
{
|
|
110
|
+
"user_id": "app-user-123",
|
|
111
|
+
"fact": "User prefers window seats",
|
|
112
|
+
"fact_type": "preference",
|
|
113
|
+
"importance_score": 0.9,
|
|
114
|
+
"idempotency_key": "memory-window-seat-1",
|
|
115
|
+
}
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
client.search_memory({"user_id": "app-user-123", "query": "seat preference"})
|
|
119
|
+
client.memory_profile("app-user-123")
|
|
120
|
+
client.delete_memory(str(stored["memory_id"]), user_id="app-user-123")
|
|
121
|
+
client.purge_user_memory("app-user-123")
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Usage
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
usage = client.usage("project_123", start_date="2026-04-01", end_date="2026-04-24")
|
|
128
|
+
print(usage["totals"])
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Error Handling
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
from genaug import GeneralAugmentAPIError
|
|
135
|
+
|
|
136
|
+
try:
|
|
137
|
+
client.create_response({"model": "balanced", "input": "Hello"})
|
|
138
|
+
except GeneralAugmentAPIError as exc:
|
|
139
|
+
if exc.reason == "rate_limit_exceeded":
|
|
140
|
+
print(f"Retry after {exc.retry_after} seconds")
|
|
141
|
+
print(exc.request_id, exc.trace_id, exc.detail)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
`GeneralAugmentAPIError` preserves the HTTP status, stable `reason`/`code` when the
|
|
145
|
+
API returns one, `Retry-After`, `X-RateLimit-*`, request/trace IDs, and the decoded JSON
|
|
146
|
+
body. Existing code that only reads `status_code` or `detail` keeps working.
|
|
147
|
+
|
|
148
|
+
## Local Tests
|
|
149
|
+
|
|
150
|
+
Run the local mock server and point the SDK at it:
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
uv run --project packages/cli genaug mock --host 127.0.0.1 --port 8787 --quiet
|
|
154
|
+
export GENAUG_API_BASE_URL="http://127.0.0.1:8787"
|
|
155
|
+
export GENAUG_API_KEY="local-test"
|
|
156
|
+
PYTHONPATH=src python examples/contract_test.py
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
The contract example covers a Responses turn plus memory store/search against the same
|
|
160
|
+
deterministic routes used by app backend CI.
|
|
161
|
+
|
|
162
|
+
## Other Helpers
|
|
163
|
+
|
|
164
|
+
The SDK also includes `create_project_from_config`, `register_openapi_tools`,
|
|
165
|
+
`link_user`, `usage`, and `test_agent` for admin and integration workflows.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
genaug/__init__.py,sha256=drXrvkEckJh5MSPAZwiKcrrG6CAjffj8YKRM1wC-q3s,429
|
|
2
|
+
genaug/agent.py,sha256=A7UDEpMePNRiMUJk-McIiZutGswT8vu82yHYhQ7okb8,1221
|
|
3
|
+
genaug/client.py,sha256=DUIX9g18UCM9XIEU27pxFV6o04R-CAp7GghRAovGFlU,24612
|
|
4
|
+
genaug/identity.py,sha256=cNoRVLJbIAJ6JjtJj-90YPbg9OT0EQShg4MdjXmQ5SE,1059
|
|
5
|
+
genaug/tools.py,sha256=-qMKrw8wJFaPbKhiLKVPxrYx170Yvludj8LtebazZto,739
|
|
6
|
+
general_augment_sdk-0.1.0.dist-info/METADATA,sha256=k633KncJVPdeiFoCphslS_ijo16l-esU0tRZK0V714w,4605
|
|
7
|
+
general_augment_sdk-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
8
|
+
general_augment_sdk-0.1.0.dist-info/RECORD,,
|