tex-sdk 0.3.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.
tex_sdk-0.3.0/PKG-INFO ADDED
@@ -0,0 +1,627 @@
1
+ Metadata-Version: 2.4
2
+ Name: tex-sdk
3
+ Version: 0.3.0
4
+ Summary: Tex Python SDK for IntegrationBackend
5
+ Author: Tex
6
+ Requires-Python: >=3.9
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: httpx>=0.24.0
9
+ Provides-Extra: http2
10
+ Requires-Dist: h2>=4.1.0; extra == "http2"
11
+
12
+ # Tex SDK (Python)
13
+
14
+ Tex is a lightweight Python client for Tex’s IntegrationBackend HTTP API.
15
+
16
+ This README is intentionally implementation-grounded: it documents exactly what the SDK does today based on the code in:
17
+
18
+ - `tex/__init__.py` (public exports)
19
+ - `tex/config.py` (endpoint paths + config)
20
+ - `tex/client.py` (client behavior, auth, retries, request/response handling)
21
+ - `tex/errors.py` (error types)
22
+
23
+ If you are reading this inside the repo, those files are the source of truth.
24
+
25
+ ## What you get
26
+
27
+ - One client class: `Tex`
28
+ - Three auth modes (API key, org+user login, or direct access token)
29
+ - Minimal, predictable return values: most methods return JSON as `dict`
30
+ - Friendly NLQ output via `AskResult`
31
+ - Typed package (`tex/py.typed`) for mypy/pyright
32
+
33
+ ## Install
34
+
35
+ ```bash
36
+ pip install tex-sdk
37
+ ```
38
+
39
+ Optional: enable HTTP/2 (requires `h2`):
40
+
41
+ ```bash
42
+ pip install "tex-sdk[http2]"
43
+ ```
44
+
45
+ Python requirement (from `pyproject.toml`): Python 3.9+
46
+
47
+ ## Quick start
48
+
49
+ Important: the SDK’s `base_url` must point at IntegrationBackend (the FastAPI gateway), not HelixDB directly.
50
+
51
+ ### 1) API key auth (recommended)
52
+
53
+ This is the simplest and most “production-like” flow.
54
+
55
+ ```python
56
+ from tex import Tex
57
+
58
+ tx = Tex("http://localhost:8000", api_key="sk_live_...")
59
+
60
+ tx.store_memory(
61
+ "Hello from Tex SDK.",
62
+ type="document",
63
+ metadata={"source": "quickstart"},
64
+ )
65
+
66
+ answer = tx.ask("What did I just store?")
67
+ print(answer.text)
68
+ ```
69
+
70
+ ### 2) Org + user login (dev / testing)
71
+
72
+ If your IntegrationBackend allows `/auth/login` for a given org+user, you can use:
73
+
74
+ ```python
75
+ from tex import Tex
76
+
77
+ tx = Tex(
78
+ "http://localhost:8000",
79
+ org_id="org_123",
80
+ user_id="user_456",
81
+ session_id="s1", # optional
82
+ )
83
+
84
+ tx.store_memory("I like espresso.", type="document")
85
+ print(tx.ask("What do I like?").text)
86
+ ```
87
+
88
+ ### 3) Direct access token (bring-your-own JWT)
89
+
90
+ If you already obtained an access token (e.g., out-of-band), pass it directly:
91
+
92
+ ```python
93
+ from tex import Tex
94
+
95
+ tx = Tex("http://localhost:8000", access_token="eyJ...")
96
+ print(tx.whoami())
97
+ ```
98
+
99
+ ## The public API surface
100
+
101
+ The package exports (see `tex/__init__.py`):
102
+
103
+ - `Tex`
104
+ - `AskResult`
105
+ - `TexConfig`, `TexEndpoints`
106
+ - `TexError`, `TexAuthError`, `TexHTTPError`
107
+
108
+ ## Concepts
109
+
110
+ ### Multi-tenant scope
111
+
112
+ IntegrationBackend is multi-tenant. Requests typically belong to a tenant:
113
+
114
+ - `org_id`
115
+ - `user_id`
116
+ - `session_id` (optional)
117
+
118
+ In the SDK, “scope” is included in ingestion payloads via `Tex._scope_payload()`. Internally:
119
+
120
+ - The backend ultimately trusts the JWT for tenant claims.
121
+ - The SDK still sends `scope` because the request models expect it.
122
+ - When using API keys, the SDK attempts to discover tenant claims by calling `GET /auth/verify`.
123
+
124
+ If tenant discovery fails, the SDK may send placeholders (`"_"`) for `org_id`/`user_id`; the backend is expected to ignore these and use the JWT tenant instead.
125
+
126
+ ### Correlation IDs
127
+
128
+ Each request includes a unique `X-Correlation-ID` header generated per request (see `tex/client.py`).
129
+
130
+ - This is useful for tracing requests through your logs.
131
+ - When available, the SDK also exposes this as `request_id` on raised exceptions.
132
+
133
+ ## Authentication: exact behavior
134
+
135
+ This section mirrors `Tex._ensure_auth()` and `Tex._refresh()` in `tex/client.py`.
136
+
137
+ ### Auth modes (in priority order)
138
+
139
+ When the SDK needs auth, it chooses the first available:
140
+
141
+ 1) If `access_token` is already present in memory → use it.
142
+ 2) If `TexConfig.access_token` is provided → use it.
143
+ 3) Else if `api_key` is provided:
144
+ - `POST /auth/token-exchange` with `{ "api_key": "..." }` → get `access_token` (+ optional `refresh_token`)
145
+ - `GET /auth/verify` to discover tenant claims
146
+ - If you also explicitly provided `user_id`, the SDK then calls `POST /auth/login` to mint a user-scoped token.
147
+ 4) Else if `org_id` + `user_id` is provided:
148
+ - `POST /auth/login` with `{ org_id, user_id, session_id }`
149
+ 5) Otherwise → raise `TexAuthError` (“No auth configured...”).
150
+
151
+ ### Refresh + retry
152
+
153
+ All authenticated requests go through `_request()`.
154
+
155
+ - If a request returns HTTP 401:
156
+ - The SDK attempts `_refresh()` and retries the request once.
157
+ - Refresh rules:
158
+ - If `refresh_token` exists: `POST /auth/refresh`.
159
+ - Else if `api_key` exists: re-exchange the API key (and, if `user_id` was set, re-login).
160
+ - Otherwise: raise `TexAuthError`.
161
+
162
+ ## Configuration
163
+
164
+ ### `TexConfig`
165
+
166
+ You can construct the client with either:
167
+
168
+ - A `base_url` string: `Tex("http://localhost:8000", api_key=...)`
169
+ - A `TexConfig` object: `Tex(TexConfig(base_url=..., api_key=...))`
170
+
171
+ Fields (see `tex/config.py`):
172
+
173
+ - `base_url`: IntegrationBackend URL, e.g. `http://localhost:8000`
174
+ - Auth fields: `api_key`, `org_id`, `user_id`, `session_id`, `access_token`, `refresh_token`
175
+ - Transport: `timeout_s` (default 15s), `http2` (default True)
176
+ - `endpoints`: a `TexEndpoints` instance
177
+
178
+ ### `TexEndpoints`
179
+
180
+ `TexEndpoints` contains the path strings the SDK calls (all relative to `base_url`).
181
+
182
+ Defaults (see `tex/config.py`):
183
+
184
+ - Auth:
185
+ - `auth_login`: `/auth/login`
186
+ - `auth_refresh`: `/auth/refresh`
187
+ - `auth_token_exchange`: `/auth/token-exchange`
188
+ - `auth_verify`: `/auth/verify`
189
+ - Ingestion:
190
+ - `ingestion_document`: `/ingestion/document`
191
+ - `ingestion_episode`: `/ingestion/episode`
192
+ - `ingestion_preference`: `/ingestion/preference`
193
+ - `ingestion_status`: `/ingestion/status/{job_id}`
194
+ - `ingestion_batch`: `/ingestion/batch`
195
+ - DB:
196
+ - `db_query`: `/helixdb/query`
197
+ - `db_schema`: `/helixdb/schema`
198
+ - NLQ: `nlq_execute`: `/nlq/execute`
199
+ - Search: `search`: `/search`
200
+ - Memories CRUD: `memories_list`, `memories_get`, `memories_delete`, `memories_update`
201
+ - Episodes: `episodes_list`: `/memories/episodes`
202
+ - Users: `user_profile`: `/users/profile`
203
+
204
+ If your deployment uses different routes/prefixes, override:
205
+
206
+ ```python
207
+ from tex import Tex, TexConfig, TexEndpoints
208
+
209
+ endpoints = TexEndpoints(
210
+ # example override
211
+ db_query="/db/query",
212
+ db_schema="/db/schema",
213
+ )
214
+
215
+ tx = Tex(TexConfig(base_url="https://api.example.com", api_key="sk_live_...", endpoints=endpoints))
216
+ ```
217
+
218
+ ## API reference (method-by-method)
219
+
220
+ All examples assume:
221
+
222
+ ```python
223
+ from tex import Tex
224
+
225
+ tx = Tex("http://localhost:8000", api_key="sk_live_...")
226
+ ```
227
+
228
+ ### `Tex.store_memory(...)`
229
+
230
+ Single ingestion entrypoint. Behavior depends on `type`.
231
+
232
+ Signature (from `tex/client.py`):
233
+
234
+ - `content`: `str` or `list[dict]` (depending on `type`)
235
+ - `type`: one of `"document"`, `"episode"`, `"preference"`
236
+ - `format`: for documents, default `"text"`
237
+ - `metadata`: optional dict
238
+ - `options`: optional dict (passed through)
239
+ - `episode_id`: only for episode ingestion
240
+
241
+ #### Document ingestion (`type="document"`)
242
+
243
+ ```python
244
+ resp = tx.store_memory(
245
+ "A short document to ingest.",
246
+ type="document",
247
+ format="text",
248
+ metadata={"source": "docs"},
249
+ )
250
+ print(resp)
251
+ ```
252
+
253
+ Notes:
254
+
255
+ - For `type="document"`, `content` must be a string or the SDK raises `ValueError`.
256
+ - The request payload includes `scope` derived from your auth.
257
+
258
+ #### Episode ingestion (`type="episode"`)
259
+
260
+ Episodes represent chat-like messages.
261
+
262
+ You can pass a single string (it becomes one `{"role":"user","content":...}` message):
263
+
264
+ ```python
265
+ resp = tx.store_memory(
266
+ "Today I called Alice and discussed the plan.",
267
+ type="episode",
268
+ )
269
+ ```
270
+
271
+ Or pass a message list:
272
+
273
+ ```python
274
+ messages = [
275
+ {"role": "user", "content": "Book a table for two."},
276
+ {"role": "assistant", "content": "Which restaurant?"},
277
+ {"role": "user", "content": "Somewhere near downtown."},
278
+ ]
279
+
280
+ resp = tx.store_memory(messages, type="episode", episode_id="ep_001")
281
+ ```
282
+
283
+ If you pass an invalid structure, the SDK raises `ValueError`.
284
+
285
+ #### Preference ingestion (`type="preference"`)
286
+
287
+ Preference ingestion expects preferences in `metadata["preferences"]`.
288
+
289
+ ```python
290
+ resp = tx.store_memory(
291
+ "ignored",
292
+ type="preference",
293
+ metadata={
294
+ "preferences": [
295
+ {"key": "drink", "value": "espresso", "confidence": 0.9},
296
+ ]
297
+ },
298
+ )
299
+ ```
300
+
301
+ Important:
302
+
303
+ - For `type="preference"`, the SDK ignores `content` and requires a non-empty list at `metadata["preferences"]`.
304
+ - The SDK sends `{ org_id, user_id, session_id, preferences }` using discovered scope.
305
+
306
+ ### `Tex.job(job_id)`
307
+
308
+ Fetch background ingestion status.
309
+
310
+ ```python
311
+ status = tx.job("job_...")
312
+ print(status)
313
+ ```
314
+
315
+ Internally calls `GET /ingestion/status/{job_id}`.
316
+
317
+ ### `Tex.batch_store(documents)`
318
+
319
+ Ingest multiple documents in one call.
320
+
321
+ Each document dict should contain:
322
+
323
+ - `data` (str)
324
+ - optional `format`, `metadata`, `options`
325
+
326
+ ```python
327
+ resp = tx.batch_store(
328
+ [
329
+ {"data": "doc 1", "metadata": {"source": "batch"}},
330
+ {"data": "doc 2", "format": "text"},
331
+ ]
332
+ )
333
+ print(resp)
334
+ ```
335
+
336
+ Return shape depends on IntegrationBackend; the SDK returns the JSON response.
337
+
338
+ ### `Tex.search(query, ...)`
339
+
340
+ Fast semantic search.
341
+
342
+ ```python
343
+ resp = tx.search("espresso", top_k=5)
344
+ print(resp)
345
+ ```
346
+
347
+ Optional parameters:
348
+
349
+ - `min_score`: float
350
+ - `label`: string label filter
351
+ - `metadata_filter`: dict
352
+
353
+ ### `Tex.ask(question, ...)` → `AskResult`
354
+
355
+ Natural language query execution.
356
+
357
+ ```python
358
+ ans = tx.ask("What did I store recently?")
359
+ print(ans.text)
360
+ ```
361
+
362
+ Return type is `AskResult`:
363
+
364
+ - `text`: human-readable summary assembled from evidence/bindings/documents
365
+ - `evidence`: list of evidence strings
366
+ - `entities`: list of extracted/bound entity names
367
+ - `documents`: list of document identifiers
368
+ - `raw`: the full JSON response dict
369
+
370
+ Parameters (passed through to `/nlq/execute`):
371
+
372
+ - `execute` (default True)
373
+ - `enable_pruning` (default True)
374
+ - `use_local_intelligence` (default True)
375
+ - `intent_match_options` (optional dict)
376
+ - `compact` (default True)
377
+
378
+ ### `Tex.query(query, params=None)`
379
+
380
+ Execute a DB query via IntegrationBackend.
381
+
382
+ ```python
383
+ resp = tx.query(
384
+ "MATCH (n) RETURN n LIMIT $limit",
385
+ params={"limit": 5},
386
+ )
387
+ print(resp)
388
+ ```
389
+
390
+ This calls `POST /helixdb/query` by default (see `TexEndpoints.db_query`).
391
+
392
+ ### `Tex.schema()`
393
+
394
+ Fetch the database schema.
395
+
396
+ ```python
397
+ schema = tx.schema()
398
+ print(schema)
399
+ ```
400
+
401
+ This calls `GET /helixdb/schema` by default.
402
+
403
+ ### `Tex.whoami()`
404
+
405
+ Returns the tenant context as seen by IntegrationBackend (great for debugging auth/scope).
406
+
407
+ ```python
408
+ print(tx.whoami())
409
+ ```
410
+
411
+ Internally, it ensures auth, then calls `GET /auth/verify` (unless already cached).
412
+
413
+ ### Memories CRUD
414
+
415
+ These map to `/memories` endpoints.
416
+
417
+ #### `Tex.get_memory(memory_id)`
418
+
419
+ ```python
420
+ mem = tx.get_memory("mem_...")
421
+ print(mem)
422
+ ```
423
+
424
+ #### `Tex.list_memories(type=None, limit=50, offset=0)`
425
+
426
+ ```python
427
+ resp = tx.list_memories(limit=20, offset=0)
428
+ print(resp)
429
+
430
+ docs = tx.list_memories(type="document", limit=20)
431
+ print(docs)
432
+ ```
433
+
434
+ #### `Tex.update_memory(memory_id, content=None, metadata=None, options=None)`
435
+
436
+ ```python
437
+ resp = tx.update_memory(
438
+ "mem_...",
439
+ content="updated content",
440
+ metadata={"tag": "updated"},
441
+ )
442
+ print(resp)
443
+ ```
444
+
445
+ #### `Tex.delete_memory(memory_id)`
446
+
447
+ ```python
448
+ resp = tx.delete_memory("mem_...")
449
+ print(resp)
450
+ ```
451
+
452
+ #### `Tex.delete_memories(user_id=None)`
453
+
454
+ Bulk delete.
455
+
456
+ - If `user_id` is omitted, deletes the caller’s memories.
457
+ - If `user_id` is provided and differs from caller, IntegrationBackend may require elevated roles.
458
+
459
+ ```python
460
+ resp = tx.delete_memories()
461
+ print(resp)
462
+ ```
463
+
464
+ ### Episodes
465
+
466
+ #### `Tex.list_episodes(limit=50, offset=0, since=None)`
467
+
468
+ ```python
469
+ resp = tx.list_episodes(limit=10)
470
+ print(resp)
471
+ ```
472
+
473
+ ### User profile
474
+
475
+ #### `Tex.get_profile(format="text")`
476
+
477
+ Returns a synthesized profile derived from preferences and episodic memories.
478
+
479
+ ```python
480
+ profile = tx.get_profile(format="text")
481
+ print(profile)
482
+ ```
483
+
484
+ ## Errors and exception handling
485
+
486
+ Errors are defined in `tex/errors.py`.
487
+
488
+ ### `TexHTTPError`
489
+
490
+ Raised for non-auth HTTP failures (status >= 400 excluding 401/403) and network/timeout errors.
491
+
492
+ Fields:
493
+
494
+ - `message` (str)
495
+ - `status_code` (optional int)
496
+ - `request_id` (optional str; pulled from `X-Correlation-ID` response header if present)
497
+ - `response_text` (optional str; truncated to 2000 chars)
498
+ - `details` (any; parsed JSON when possible)
499
+
500
+ ### `TexAuthError`
501
+
502
+ Subclass of `TexHTTPError`, raised for auth issues:
503
+
504
+ - “No auth configured”
505
+ - token exchange/login failures
506
+ - HTTP 401/403 responses
507
+
508
+ ### Typical pattern
509
+
510
+ ```python
511
+ from tex import Tex, TexAuthError, TexHTTPError
512
+
513
+ tx = Tex("http://localhost:8000", api_key="sk_live_...")
514
+
515
+ try:
516
+ print(tx.whoami())
517
+ except TexAuthError as e:
518
+ # Wrong key, missing roles, expired/invalid refresh, etc.
519
+ print("auth failed", str(e), e.status_code)
520
+ except TexHTTPError as e:
521
+ # Non-auth HTTP errors (422, 500, network issues)
522
+ print("request failed", str(e), e.status_code)
523
+ ```
524
+
525
+ ## Request/response handling details
526
+
527
+ This mirrors `Tex._request()` and `_post_noauth()`.
528
+
529
+ - JSON responses are returned as Python dicts.
530
+ - If the response body is valid JSON but not a dict (e.g., a list), the SDK wraps it as `{ "data": <payload> }`.
531
+ - If the response is not JSON, the SDK returns `{ "data": "<text>" }`.
532
+ - For error responses, the SDK tries to extract `detail` or `message` from JSON. Otherwise: “Request failed”.
533
+
534
+ Special hint for HTTP 422:
535
+
536
+ - If the error mentions “IntentGraph validation” / “root variable must have a type”, the SDK appends a hint suggesting schema/planner issues.
537
+
538
+ ## Running the smoketest script (repo)
539
+
540
+ The repo includes a minimal end-to-end tester:
541
+
542
+ - `tools/tex_sdk_smoketest.py`
543
+
544
+ It is designed to work:
545
+
546
+ - When installed from PyPI (`pip install tex-sdk`)
547
+ - When run directly from this repo (it falls back to adding the repo root to `sys.path`)
548
+
549
+ ### Environment variables
550
+
551
+ - `TEX_BASE_URL` (default: `http://localhost:8000`)
552
+ - `TEX_TIMEOUT_S` (default: `30`)
553
+
554
+ Auth (set exactly one of the following groups):
555
+
556
+ 1) API key:
557
+
558
+ - `TEX_API_KEY=sk_live_...`
559
+
560
+ 2) Org+user login:
561
+
562
+ - `TEX_ORG_ID=...`
563
+ - `TEX_USER_ID=...`
564
+ - `TEX_SESSION_ID=...` (optional)
565
+
566
+ 3) Direct token:
567
+
568
+ - `TEX_ACCESS_TOKEN=eyJ...`
569
+
570
+ ### Run
571
+
572
+ ```powershell
573
+ $env:TEX_BASE_URL = "http://localhost:8000"
574
+ $env:TEX_API_KEY = "sk_live_..."
575
+
576
+ python tools/tex_sdk_smoketest.py
577
+ ```
578
+
579
+ The script performs:
580
+
581
+ - `GET /health` (unauth quick check)
582
+ - `whoami()`
583
+ - `store_memory(type="document")`
584
+ - polls `job(job_id)` (if job_id returned)
585
+ - `search(...)`
586
+ - `ask(...)`
587
+
588
+ ## Migration notes (in-repo users)
589
+
590
+ Inside this repo, there is also an `sdk/` package that re-exports everything from `tex/` as a compatibility shim.
591
+
592
+ - New code should import from `tex`.
593
+ - Old code importing `sdk` will continue to work (see `sdk/__init__.py`).
594
+
595
+ ## Troubleshooting
596
+
597
+ ### “Connection refused” / health check fails
598
+
599
+ - Ensure IntegrationBackend is running (default: `http://localhost:8000/health`).
600
+ - The SDK does not start services; it only calls HTTP endpoints.
601
+
602
+ ### HTTP 401 / 403
603
+
604
+ - Verify you’re using the correct auth mode.
605
+ - For API key auth: ensure the key is valid and belongs to the tenant you expect.
606
+ - For org+user login: ensure `/auth/login` is enabled for that org/user.
607
+
608
+ ### HTTP 422 (validation errors)
609
+
610
+ - Usually means your request payload doesn’t match backend expectations.
611
+ - For NLQ/planner-related validation errors, confirm the DB schema is available and the planner can fetch it.
612
+
613
+ ### HTTP/2 import error (`h2`)
614
+
615
+ If you see an ImportError mentioning `h2`, install:
616
+
617
+ ```bash
618
+ pip install "tex-sdk[http2]"
619
+ ```
620
+
621
+ Or disable HTTP/2:
622
+
623
+ ```python
624
+ from tex import Tex
625
+
626
+ tx = Tex("http://localhost:8000", api_key="sk_live_...", http2=False)
627
+ ```