uniqos 0.4.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.
- uniqos-0.4.0/.gitignore +21 -0
- uniqos-0.4.0/CHANGELOG.md +113 -0
- uniqos-0.4.0/Makefile +59 -0
- uniqos-0.4.0/PKG-INFO +160 -0
- uniqos-0.4.0/README.md +138 -0
- uniqos-0.4.0/docs/dev.md +92 -0
- uniqos-0.4.0/examples/README.md +26 -0
- uniqos-0.4.0/examples/dogfood.py +39 -0
- uniqos-0.4.0/examples/error_handling.py +48 -0
- uniqos-0.4.0/examples/hello_world.py +35 -0
- uniqos-0.4.0/examples/stateful.py +44 -0
- uniqos-0.4.0/examples/streaming.py +37 -0
- uniqos-0.4.0/pyproject.toml +89 -0
- uniqos-0.4.0/src/uniqos/__init__.py +81 -0
- uniqos-0.4.0/src/uniqos/_core/__init__.py +7 -0
- uniqos-0.4.0/src/uniqos/_core/config.py +130 -0
- uniqos-0.4.0/src/uniqos/_core/logging.py +109 -0
- uniqos-0.4.0/src/uniqos/_core/retry.py +50 -0
- uniqos-0.4.0/src/uniqos/_core/streaming.py +157 -0
- uniqos-0.4.0/src/uniqos/_core/transport.py +315 -0
- uniqos-0.4.0/src/uniqos/async_client.py +126 -0
- uniqos-0.4.0/src/uniqos/client.py +133 -0
- uniqos-0.4.0/src/uniqos/errors.py +282 -0
- uniqos-0.4.0/src/uniqos/generated/__init__.py +7 -0
- uniqos-0.4.0/src/uniqos/generated/stream_events.py +115 -0
- uniqos-0.4.0/src/uniqos/py.typed +0 -0
- uniqos-0.4.0/src/uniqos/resources/__init__.py +5 -0
- uniqos-0.4.0/src/uniqos/resources/_base.py +52 -0
- uniqos-0.4.0/src/uniqos/resources/_mixin.py +81 -0
- uniqos-0.4.0/src/uniqos/resources/api_keys.py +25 -0
- uniqos-0.4.0/src/uniqos/resources/billing.py +107 -0
- uniqos-0.4.0/src/uniqos/resources/catalog.py +16 -0
- uniqos-0.4.0/src/uniqos/resources/end_users.py +68 -0
- uniqos-0.4.0/src/uniqos/resources/engine.py +80 -0
- uniqos-0.4.0/src/uniqos/resources/interactions.py +35 -0
- uniqos-0.4.0/src/uniqos/resources/me.py +16 -0
- uniqos-0.4.0/src/uniqos/resources/organizations.py +38 -0
- uniqos-0.4.0/src/uniqos/resources/personalities.py +56 -0
- uniqos-0.4.0/src/uniqos/resources/relationships.py +64 -0
- uniqos-0.4.0/src/uniqos/resources/vocabularies.py +36 -0
- uniqos-0.4.0/tests/static/test_typing.py +30 -0
- uniqos-0.4.0/tests/test_client.py +31 -0
- uniqos-0.4.0/tests/test_config.py +77 -0
- uniqos-0.4.0/tests/test_errors.py +129 -0
- uniqos-0.4.0/tests/test_examples.py +22 -0
- uniqos-0.4.0/tests/test_parity.py +38 -0
- uniqos-0.4.0/tests/test_resources.py +160 -0
- uniqos-0.4.0/tests/test_respond.py +61 -0
- uniqos-0.4.0/tests/test_retry.py +144 -0
- uniqos-0.4.0/tests/test_smoke.py +10 -0
- uniqos-0.4.0/tests/test_streaming.py +156 -0
- uniqos-0.4.0/tests/test_transport.py +144 -0
- uniqos-0.4.0/uv.lock +1005 -0
uniqos-0.4.0/.gitignore
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
node_modules/
|
|
2
|
+
dist/
|
|
3
|
+
.env
|
|
4
|
+
.env.local
|
|
5
|
+
*.log
|
|
6
|
+
.DS_Store
|
|
7
|
+
coverage/
|
|
8
|
+
*.tsbuildinfo
|
|
9
|
+
.turbo/
|
|
10
|
+
*.tgz
|
|
11
|
+
|
|
12
|
+
# Python
|
|
13
|
+
__pycache__/
|
|
14
|
+
*.py[cod]
|
|
15
|
+
.venv/
|
|
16
|
+
.mypy_cache/
|
|
17
|
+
.ruff_cache/
|
|
18
|
+
.pytest_cache/
|
|
19
|
+
*.egg-info/
|
|
20
|
+
build/
|
|
21
|
+
*.whl
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to the `uniqos` Python SDK are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to
|
|
5
|
+
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.4.0] - 2026-06-13
|
|
10
|
+
|
|
11
|
+
Coordinated refresh release. Both SDKs re-vendor the post-S1+S2 canonical OpenAPI contract
|
|
12
|
+
(root `openapi/openapi.json`) and move onto a single `0.4.0` version line. Python jumps
|
|
13
|
+
`0.2.0 → 0.4.0` (skipping `0.3.0`) to align with `@uniqos/sdk`; the two packages now share a
|
|
14
|
+
version. This cuts the previously-Unreleased `504 turn_timeout` handling (below).
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- **`TurnTimeoutError`** — typed error for `504 turn_timeout` (a `/v1/respond` turn that
|
|
19
|
+
exceeded the server's engine budget and was aborted; ADR-0009). The aborted turn is
|
|
20
|
+
**not billed**. Mapped both code-first (`turn_timeout`) and by status (504).
|
|
21
|
+
|
|
22
|
+
### Notes
|
|
23
|
+
|
|
24
|
+
- **OpenAPI snapshot refreshed; generated models unchanged.** The vendored snapshot was
|
|
25
|
+
re-vendored to the current contract, but `make generate` is a no-op for Python: code
|
|
26
|
+
generation is scoped to the 6 named streaming schemas (ADR PY-0004), which are byte-identical
|
|
27
|
+
in the new contract. The contract's additive changes — `big_five_facets` on personality
|
|
28
|
+
responses, the formal `504` on `/v1/respond`, the new `/v1/auth/refresh` endpoint — live in
|
|
29
|
+
the path surface, which Python passes through as snake_case dicts rather than generating. The
|
|
30
|
+
personality dict simply carries the new `big_five_facets` key; `/v1/auth/refresh` stays outside
|
|
31
|
+
the integration surface (platform/auth, `docs/api-sdk-delta.md` §4). Behavioral parity with
|
|
32
|
+
`@uniqos/sdk@0.4.0` is maintained.
|
|
33
|
+
|
|
34
|
+
### Changed
|
|
35
|
+
|
|
36
|
+
- **504 is no longer auto-retried.** A budget timeout means the turn was already too
|
|
37
|
+
slow; a blind retry risks another long turn and — combined with client-vs-server
|
|
38
|
+
timing — a silent double bill. Retrying a 504 is now the integrator's explicit choice.
|
|
39
|
+
429 `rate_limit_exceeded` and 500/502/503 retry as before.
|
|
40
|
+
- **Default request timeout raised 30s → 90s** (`DEFAULT_TIMEOUT`), comfortably above the
|
|
41
|
+
server's ~75s turn ceiling (`ENGINE_TURN_BUDGET_MS` 60s + 15s socket margin, exported
|
|
42
|
+
as `SERVER_TURN_CEILING_SECONDS`). This guarantees the SDK receives the server's clean
|
|
43
|
+
`504` instead of abandoning a turn the server may still finish **and bill**.
|
|
44
|
+
Integrators who set `timeout` below the server budget take on that risk explicitly.
|
|
45
|
+
|
|
46
|
+
## [0.2.0] - 2026-06-04
|
|
47
|
+
|
|
48
|
+
### Added
|
|
49
|
+
|
|
50
|
+
- **`relationships.delete` now accepts `cascade` + `reason` (SPEC-14 §11.4, ADR-0006).** Mirrors
|
|
51
|
+
`end_users.delete`: keyword-only `cascade` (`Literal["all", "memory_only"]`) and `reason`
|
|
52
|
+
(`str`). `cascade` selects the deletion granularity (default `all` server-side); `reason`
|
|
53
|
+
records an optional audit note. Both travel in the DELETE body; omitting them keeps the prior
|
|
54
|
+
behavior (a bodyless full deletion). Unlike `end_users.delete`, there is **no** `model_only` —
|
|
55
|
+
the psychological model lives at the user level, not the relationship level (use
|
|
56
|
+
`end_users.delete(cascade="model_only")` for that). Closes the last TS↔Python `delete` surface
|
|
57
|
+
asymmetry (see `docs/api-sdk-delta.md`).
|
|
58
|
+
|
|
59
|
+
### Notes
|
|
60
|
+
|
|
61
|
+
- Purely additive: the new arguments are keyword-only with `None` defaults, so existing
|
|
62
|
+
`relationships.delete(relationship_id)` callers are unaffected (no breaking change — unlike the
|
|
63
|
+
TypeScript `@uniqos/sdk@0.3.0` bump, where `config` shifted from the 2nd to the 3rd positional
|
|
64
|
+
argument).
|
|
65
|
+
- A type-only regression guard (`tests/static/test_typing.py`, checked by `mypy --strict`, not run
|
|
66
|
+
by `pytest`) pins `cascade` to the two contract literals and asserts `model_only` is rejected.
|
|
67
|
+
|
|
68
|
+
## [0.1.0] - 2026-06-02
|
|
69
|
+
|
|
70
|
+
First published version. Functional parity with the TypeScript SDK (`@uniqos/sdk@0.1.0`),
|
|
71
|
+
implemented idiomatically for Python (snake_case, type hints, `py.typed`).
|
|
72
|
+
|
|
73
|
+
### Added
|
|
74
|
+
|
|
75
|
+
- **Sync and async clients** (`UniqOS` / `AsyncUniqOS`) over httpx, with an identical public
|
|
76
|
+
surface — only the I/O is awaited (ADR PY-0001).
|
|
77
|
+
- **Configuration with fail-fast validation** (SPEC-17 §6): API-key format + environment
|
|
78
|
+
detection (`uniq_live_` / `uniq_test_`), `base_url` normalization, `timeout`, `max_retries`,
|
|
79
|
+
and log levels. An invalid key raises `ConfigurationError` before any HTTP call.
|
|
80
|
+
- **Idiomatic namespaces** (SPEC-17 §7): `engine` (+ the `respond` shortcut), `personalities`,
|
|
81
|
+
`catalog`, `vocabularies`, `end_users`, `relationships` (+ `memory`), `interactions`, `me`,
|
|
82
|
+
`organizations`, `api_keys`, `billing`.
|
|
83
|
+
- **Typed error hierarchy** with a code-first mapping (SPEC-17 §8): `rate_limit_exceeded` vs
|
|
84
|
+
`quota_exhausted` are distinguished by `code` on the same HTTP 429;
|
|
85
|
+
`RateLimitError.retry_after_seconds` is read from the `Retry-After` header.
|
|
86
|
+
- **Automatic retries** with exponential backoff + jitter, honoring `Retry-After`
|
|
87
|
+
(SPEC-17 §10.2): retries network errors / `429 rate_limit_exceeded` / 500–504; never
|
|
88
|
+
`quota_exhausted` / 401 / 403 / other 4xx.
|
|
89
|
+
- **Automatic idempotency keys** (UUID v4) on creation POSTs when none is provided, reused
|
|
90
|
+
across retries; an explicit key is honored (SPEC-17 §10.1, §10.4).
|
|
91
|
+
- **SSE streaming**: `respond(stream=True)` yields the five generated `RespondStreamEvent`
|
|
92
|
+
variants as typed dataclasses — a sync iterator or an async iterator (SPEC-17 §9). A
|
|
93
|
+
mid-stream drop raises `NetworkError` with the interruption point.
|
|
94
|
+
- **Configurable logging** (SPEC-17 §11) that never logs the full API key (only the prefix) nor
|
|
95
|
+
message content beyond 50 characters.
|
|
96
|
+
- **Five runnable examples** (`examples/`) and a quickstart README.
|
|
97
|
+
|
|
98
|
+
### Implementation notes
|
|
99
|
+
|
|
100
|
+
- Data model: **dataclasses + `TypedDict`**, with **httpx** as the only runtime dependency
|
|
101
|
+
(ADR PY-0002). Responses are passed through as snake_case dicts; the streaming events are
|
|
102
|
+
frozen dataclasses generated from the OpenAPI contract.
|
|
103
|
+
- Generation: `make generate` (datamodel-code-generator) reads the shared root
|
|
104
|
+
`openapi/openapi.json` snapshot, scoped to the contract's 6 named schemas (ADR PY-0004).
|
|
105
|
+
|
|
106
|
+
### Compatibility notes
|
|
107
|
+
|
|
108
|
+
- Streaming consumers that branch exhaustively on `event.type` must handle all five variants
|
|
109
|
+
(`metadata`, `text`, `guardrail_modulation`, `completion`, `error`).
|
|
110
|
+
- `end_users.delete` accepts `cascade` (`'all' | 'model_only' | 'memory_only'`) and `reason`,
|
|
111
|
+
ahead of the TypeScript SDK (tracked in `../../docs/api-sdk-delta.md`).
|
|
112
|
+
|
|
113
|
+
[0.1.0]: https://github.com/filuco2001/uniqos-sdks/releases/tag/python-v0.1.0
|
uniqos-0.4.0/Makefile
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Developer commands for the uniqos Python SDK.
|
|
2
|
+
# Mirrors the pnpm scripts of the TS SDK; all commands run through uv (ADR PY-0003).
|
|
3
|
+
#
|
|
4
|
+
# make install # create the venv and install the package + dev tools
|
|
5
|
+
# make generate # regenerate models from the shared OpenAPI snapshot (ADR PY-0004)
|
|
6
|
+
# make check # format-check + lint + typecheck + test (what CI runs)
|
|
7
|
+
|
|
8
|
+
.PHONY: install generate lint format format-check typecheck test build check clean
|
|
9
|
+
|
|
10
|
+
# Shared OpenAPI snapshot at the monorepo root — the SAME file the TS SDK reads.
|
|
11
|
+
OPENAPI := ../../openapi/openapi.json
|
|
12
|
+
GENERATED := src/uniqos/generated
|
|
13
|
+
|
|
14
|
+
install:
|
|
15
|
+
uv sync
|
|
16
|
+
|
|
17
|
+
# Regenerate the streaming-event dataclasses from the shared OpenAPI snapshot.
|
|
18
|
+
# Scoped to `schemas` (the contract's only 6 named schemas, all streaming — ADR PY-0004).
|
|
19
|
+
# --disable-timestamp keeps the output byte-stable so regeneration is a no-op when the
|
|
20
|
+
# snapshot is unchanged (CI verifies with `git diff --exit-code`).
|
|
21
|
+
generate:
|
|
22
|
+
uv run datamodel-codegen \
|
|
23
|
+
--input $(OPENAPI) \
|
|
24
|
+
--input-file-type openapi \
|
|
25
|
+
--output $(GENERATED)/stream_events.py \
|
|
26
|
+
--output-model-type dataclasses.dataclass \
|
|
27
|
+
--openapi-scopes schemas \
|
|
28
|
+
--enum-field-as-literal all \
|
|
29
|
+
--frozen-dataclasses \
|
|
30
|
+
--keyword-only \
|
|
31
|
+
--target-python-version 3.10 \
|
|
32
|
+
--use-standard-collections \
|
|
33
|
+
--use-union-operator \
|
|
34
|
+
--use-schema-description \
|
|
35
|
+
--disable-timestamp
|
|
36
|
+
|
|
37
|
+
lint:
|
|
38
|
+
uv run ruff check src tests examples
|
|
39
|
+
|
|
40
|
+
format:
|
|
41
|
+
uv run ruff format src tests examples
|
|
42
|
+
|
|
43
|
+
format-check:
|
|
44
|
+
uv run ruff format --check src tests examples
|
|
45
|
+
|
|
46
|
+
typecheck:
|
|
47
|
+
uv run mypy
|
|
48
|
+
|
|
49
|
+
test:
|
|
50
|
+
uv run pytest
|
|
51
|
+
|
|
52
|
+
build:
|
|
53
|
+
uv build
|
|
54
|
+
|
|
55
|
+
check: format-check lint typecheck test
|
|
56
|
+
|
|
57
|
+
clean:
|
|
58
|
+
rm -rf dist build .mypy_cache .ruff_cache .pytest_cache
|
|
59
|
+
find . -type d -name __pycache__ -prune -exec rm -rf {} +
|
uniqos-0.4.0/PKG-INFO
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: uniqos
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: Official uniqOS SDK — Personality & Emotional Intelligence Engine + Relational Memory for AI agents.
|
|
5
|
+
Project-URL: Homepage, https://docs.uniqos.ai
|
|
6
|
+
Project-URL: Documentation, https://docs.uniqos.ai
|
|
7
|
+
Project-URL: Repository, https://github.com/filuco2001/uniqos-sdks
|
|
8
|
+
Author: uniqOS
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
Keywords: agents,ai,emotional-intelligence,personality,relational-memory,sdk,uniqos
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Requires-Dist: httpx<1,>=0.27
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# uniqos
|
|
24
|
+
|
|
25
|
+
Official Python SDK for [uniqOS](https://uniqos.ai) — a Personality & Emotional Intelligence
|
|
26
|
+
Engine + Relational Memory layer for AI agents.
|
|
27
|
+
|
|
28
|
+
- Sync **and** async clients (`UniqOS` / `AsyncUniqOS`)
|
|
29
|
+
- Typed errors, automatic retries with backoff, automatic idempotency keys
|
|
30
|
+
- SSE streaming as a plain iterator
|
|
31
|
+
- Fully type-hinted (`py.typed`); one runtime dependency (`httpx`)
|
|
32
|
+
- Python 3.10+
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install uniqos
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Quickstart
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from uniqos import UniqOS
|
|
44
|
+
|
|
45
|
+
client = UniqOS(api_key="uniq_test_...")
|
|
46
|
+
response = client.respond(personality_id="pers_...", message="hola")
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
`response` is the parsed JSON (snake_case, mirroring the API): `response["response"]` is the
|
|
50
|
+
agent's reply.
|
|
51
|
+
|
|
52
|
+
Get an API key from the [uniqOS dashboard](https://uniqos.ai). Keys start with `uniq_live_`
|
|
53
|
+
(production) or `uniq_test_` (sandbox); the SDK detects the environment from the prefix and
|
|
54
|
+
rejects a malformed key immediately.
|
|
55
|
+
|
|
56
|
+
## Async
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from uniqos import AsyncUniqOS
|
|
60
|
+
|
|
61
|
+
async with AsyncUniqOS(api_key="uniq_test_...") as client:
|
|
62
|
+
response = await client.respond(personality_id="pers_...", message="hola")
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Both clients expose the identical surface (`client.personalities`, `client.end_users`,
|
|
66
|
+
`client.billing`, …); the async one just awaits.
|
|
67
|
+
|
|
68
|
+
## Streaming
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
stream = client.respond(personality_id="pers_...", message="tell me a story", stream=True)
|
|
72
|
+
for event in stream:
|
|
73
|
+
if event.type == "text":
|
|
74
|
+
print(event.delta, end="", flush=True)
|
|
75
|
+
elif event.type == "completion":
|
|
76
|
+
print(f"\n[{event.latency_ms}ms]")
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The five event types (`metadata`, `text`, `guardrail_modulation`, `completion`, `error`) are
|
|
80
|
+
typed dataclasses with attribute access. Async uses `async for`.
|
|
81
|
+
|
|
82
|
+
## Errors
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
from uniqos import RateLimitError, QuotaExhaustedError
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
response = client.respond(personality_id="pers_...", message="hi")
|
|
89
|
+
except RateLimitError as err:
|
|
90
|
+
print(f"Rate limited. Retry after {err.retry_after_seconds}s")
|
|
91
|
+
except QuotaExhaustedError as err:
|
|
92
|
+
print(f"Quota exhausted: {err.details}")
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Every error carries `code`, `message`, `request_id`, `http_status`, and `details`. Catch
|
|
96
|
+
`UniqOSError` for anything the SDK can raise. The SDK retries transient failures
|
|
97
|
+
(`429 rate_limit_exceeded`, `500/502/503`, network) automatically with exponential backoff; it
|
|
98
|
+
never retries `quota_exhausted`, `401`, `403`, or `504 turn_timeout` (see Configuration below).
|
|
99
|
+
|
|
100
|
+
## Configuration
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
client = UniqOS(
|
|
104
|
+
api_key="uniq_test_...",
|
|
105
|
+
base_url="https://api.uniqos.ai", # host only; do NOT include /v1
|
|
106
|
+
timeout=90, # seconds; default 90s; 0 disables
|
|
107
|
+
max_retries=3, # 0 disables retries (504 is never auto-retried)
|
|
108
|
+
log_level="warning", # debug | info | warning | error | silent
|
|
109
|
+
logger=my_logger, # optional: any object with debug/info/warning/error
|
|
110
|
+
)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
> **Default timeout is 90s**, deliberately above the server's ~75s turn ceiling, so a slow
|
|
114
|
+
> turn surfaces as the server's clean `504 turn_timeout` (a `TurnTimeoutError`) instead of a
|
|
115
|
+
> client-side abort. A `504 turn_timeout` is **not retried automatically**: the turn already
|
|
116
|
+
> ran to the budget, so a blind retry risks a second long turn and — combined with
|
|
117
|
+
> client-vs-server timing — a double bill (ADR-0009). Retrying it is your explicit choice.
|
|
118
|
+
|
|
119
|
+
The SDK never logs the full API key (only the prefix) nor message content beyond 50 chars.
|
|
120
|
+
|
|
121
|
+
## Examples
|
|
122
|
+
|
|
123
|
+
Runnable scripts live in [`examples/`](examples): `hello_world.py`, `stateful.py`,
|
|
124
|
+
`streaming.py`, `error_handling.py`, and `dogfood.py`.
|
|
125
|
+
|
|
126
|
+
**With a real (or local) API** — set the environment and run:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
export UNIQOS_API_KEY="uniq_test_..."
|
|
130
|
+
export UNIQOS_PERSONALITY_ID="pers_..."
|
|
131
|
+
# optional, for a local backend:
|
|
132
|
+
export UNIQOS_BASE_URL="http://localhost:3000"
|
|
133
|
+
|
|
134
|
+
python examples/hello_world.py
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
**Without a network** — the test suite drives every code path against a mocked HTTP layer
|
|
138
|
+
([`respx`](https://lundberg.github.io/respx/)), so no live API is needed to exercise the SDK:
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
import httpx, respx
|
|
142
|
+
from uniqos import UniqOS
|
|
143
|
+
|
|
144
|
+
@respx.mock
|
|
145
|
+
def test_it():
|
|
146
|
+
respx.post("https://api.uniqos.ai/v1/respond").mock(
|
|
147
|
+
return_value=httpx.Response(200, json={"response": "hi"})
|
|
148
|
+
)
|
|
149
|
+
with UniqOS(api_key="uniq_test_abcd1234") as client:
|
|
150
|
+
assert client.respond(personality_id="p_1", message="hi")["response"] == "hi"
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Documentation
|
|
154
|
+
|
|
155
|
+
Full reference and guides live at [docs.uniqos.ai](https://docs.uniqos.ai). This README and the
|
|
156
|
+
in-editor docstrings cover the essentials; the deep documentation is not duplicated here.
|
|
157
|
+
|
|
158
|
+
## License
|
|
159
|
+
|
|
160
|
+
MIT
|
uniqos-0.4.0/README.md
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# uniqos
|
|
2
|
+
|
|
3
|
+
Official Python SDK for [uniqOS](https://uniqos.ai) — a Personality & Emotional Intelligence
|
|
4
|
+
Engine + Relational Memory layer for AI agents.
|
|
5
|
+
|
|
6
|
+
- Sync **and** async clients (`UniqOS` / `AsyncUniqOS`)
|
|
7
|
+
- Typed errors, automatic retries with backoff, automatic idempotency keys
|
|
8
|
+
- SSE streaming as a plain iterator
|
|
9
|
+
- Fully type-hinted (`py.typed`); one runtime dependency (`httpx`)
|
|
10
|
+
- Python 3.10+
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pip install uniqos
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Quickstart
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
from uniqos import UniqOS
|
|
22
|
+
|
|
23
|
+
client = UniqOS(api_key="uniq_test_...")
|
|
24
|
+
response = client.respond(personality_id="pers_...", message="hola")
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
`response` is the parsed JSON (snake_case, mirroring the API): `response["response"]` is the
|
|
28
|
+
agent's reply.
|
|
29
|
+
|
|
30
|
+
Get an API key from the [uniqOS dashboard](https://uniqos.ai). Keys start with `uniq_live_`
|
|
31
|
+
(production) or `uniq_test_` (sandbox); the SDK detects the environment from the prefix and
|
|
32
|
+
rejects a malformed key immediately.
|
|
33
|
+
|
|
34
|
+
## Async
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from uniqos import AsyncUniqOS
|
|
38
|
+
|
|
39
|
+
async with AsyncUniqOS(api_key="uniq_test_...") as client:
|
|
40
|
+
response = await client.respond(personality_id="pers_...", message="hola")
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Both clients expose the identical surface (`client.personalities`, `client.end_users`,
|
|
44
|
+
`client.billing`, …); the async one just awaits.
|
|
45
|
+
|
|
46
|
+
## Streaming
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
stream = client.respond(personality_id="pers_...", message="tell me a story", stream=True)
|
|
50
|
+
for event in stream:
|
|
51
|
+
if event.type == "text":
|
|
52
|
+
print(event.delta, end="", flush=True)
|
|
53
|
+
elif event.type == "completion":
|
|
54
|
+
print(f"\n[{event.latency_ms}ms]")
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The five event types (`metadata`, `text`, `guardrail_modulation`, `completion`, `error`) are
|
|
58
|
+
typed dataclasses with attribute access. Async uses `async for`.
|
|
59
|
+
|
|
60
|
+
## Errors
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from uniqos import RateLimitError, QuotaExhaustedError
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
response = client.respond(personality_id="pers_...", message="hi")
|
|
67
|
+
except RateLimitError as err:
|
|
68
|
+
print(f"Rate limited. Retry after {err.retry_after_seconds}s")
|
|
69
|
+
except QuotaExhaustedError as err:
|
|
70
|
+
print(f"Quota exhausted: {err.details}")
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Every error carries `code`, `message`, `request_id`, `http_status`, and `details`. Catch
|
|
74
|
+
`UniqOSError` for anything the SDK can raise. The SDK retries transient failures
|
|
75
|
+
(`429 rate_limit_exceeded`, `500/502/503`, network) automatically with exponential backoff; it
|
|
76
|
+
never retries `quota_exhausted`, `401`, `403`, or `504 turn_timeout` (see Configuration below).
|
|
77
|
+
|
|
78
|
+
## Configuration
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
client = UniqOS(
|
|
82
|
+
api_key="uniq_test_...",
|
|
83
|
+
base_url="https://api.uniqos.ai", # host only; do NOT include /v1
|
|
84
|
+
timeout=90, # seconds; default 90s; 0 disables
|
|
85
|
+
max_retries=3, # 0 disables retries (504 is never auto-retried)
|
|
86
|
+
log_level="warning", # debug | info | warning | error | silent
|
|
87
|
+
logger=my_logger, # optional: any object with debug/info/warning/error
|
|
88
|
+
)
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
> **Default timeout is 90s**, deliberately above the server's ~75s turn ceiling, so a slow
|
|
92
|
+
> turn surfaces as the server's clean `504 turn_timeout` (a `TurnTimeoutError`) instead of a
|
|
93
|
+
> client-side abort. A `504 turn_timeout` is **not retried automatically**: the turn already
|
|
94
|
+
> ran to the budget, so a blind retry risks a second long turn and — combined with
|
|
95
|
+
> client-vs-server timing — a double bill (ADR-0009). Retrying it is your explicit choice.
|
|
96
|
+
|
|
97
|
+
The SDK never logs the full API key (only the prefix) nor message content beyond 50 chars.
|
|
98
|
+
|
|
99
|
+
## Examples
|
|
100
|
+
|
|
101
|
+
Runnable scripts live in [`examples/`](examples): `hello_world.py`, `stateful.py`,
|
|
102
|
+
`streaming.py`, `error_handling.py`, and `dogfood.py`.
|
|
103
|
+
|
|
104
|
+
**With a real (or local) API** — set the environment and run:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
export UNIQOS_API_KEY="uniq_test_..."
|
|
108
|
+
export UNIQOS_PERSONALITY_ID="pers_..."
|
|
109
|
+
# optional, for a local backend:
|
|
110
|
+
export UNIQOS_BASE_URL="http://localhost:3000"
|
|
111
|
+
|
|
112
|
+
python examples/hello_world.py
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
**Without a network** — the test suite drives every code path against a mocked HTTP layer
|
|
116
|
+
([`respx`](https://lundberg.github.io/respx/)), so no live API is needed to exercise the SDK:
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
import httpx, respx
|
|
120
|
+
from uniqos import UniqOS
|
|
121
|
+
|
|
122
|
+
@respx.mock
|
|
123
|
+
def test_it():
|
|
124
|
+
respx.post("https://api.uniqos.ai/v1/respond").mock(
|
|
125
|
+
return_value=httpx.Response(200, json={"response": "hi"})
|
|
126
|
+
)
|
|
127
|
+
with UniqOS(api_key="uniq_test_abcd1234") as client:
|
|
128
|
+
assert client.respond(personality_id="p_1", message="hi")["response"] == "hi"
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Documentation
|
|
132
|
+
|
|
133
|
+
Full reference and guides live at [docs.uniqos.ai](https://docs.uniqos.ai). This README and the
|
|
134
|
+
in-editor docstrings cover the essentials; the deep documentation is not duplicated here.
|
|
135
|
+
|
|
136
|
+
## License
|
|
137
|
+
|
|
138
|
+
MIT
|
uniqos-0.4.0/docs/dev.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Developing the `uniqos` Python SDK
|
|
2
|
+
|
|
3
|
+
Conventions for this package. The decisions behind them live in the ADRs at
|
|
4
|
+
`../../docs/adr/py-0001..0004`. The TypeScript SDK (`../typescript`) is the reference
|
|
5
|
+
pattern; this package mirrors its structure and behavior with idiomatic Python.
|
|
6
|
+
|
|
7
|
+
## Setup
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
cd packages/python
|
|
11
|
+
make install # uv sync — creates .venv and installs the package + dev tools
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Requires [uv](https://docs.astral.sh/uv/). Everything else (ruff, mypy, pytest,
|
|
15
|
+
datamodel-code-generator) is a dev dependency installed by `uv sync`.
|
|
16
|
+
|
|
17
|
+
## Commands
|
|
18
|
+
|
|
19
|
+
| Command | What it does |
|
|
20
|
+
| ---------------- | ----------------------------------------------------- |
|
|
21
|
+
| `make generate` | Regenerate models from the shared OpenAPI snapshot |
|
|
22
|
+
| `make format` | `ruff format` the code |
|
|
23
|
+
| `make lint` | `ruff check` |
|
|
24
|
+
| `make typecheck` | `mypy --strict` over `src/uniqos` |
|
|
25
|
+
| `make test` | `pytest` |
|
|
26
|
+
| `make build` | `uv build` → wheel + sdist in `dist/` |
|
|
27
|
+
| `make check` | format-check + lint + typecheck + test (what CI runs) |
|
|
28
|
+
|
|
29
|
+
## Layout
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
src/uniqos/
|
|
33
|
+
__init__.py # public surface + __version__ (single source of the version)
|
|
34
|
+
py.typed # PEP 561 marker — the package ships types
|
|
35
|
+
client.py # UniqOS (sync) — Bloque 2
|
|
36
|
+
async_client.py # AsyncUniqOS (async) — Bloque 2
|
|
37
|
+
errors.py # typed error hierarchy — Bloque 3
|
|
38
|
+
resources/ # idiomatic namespaces — Bloque 4
|
|
39
|
+
generated/ # AUTO-GENERATED, never edit — Bloque 1
|
|
40
|
+
_core/ # pure shared core (transport, config, retry, logging, streaming)
|
|
41
|
+
tests/ # pytest; mock httpx with respx (no live network)
|
|
42
|
+
examples/ # runnable examples — Bloque 6
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Core conventions
|
|
46
|
+
|
|
47
|
+
- **snake_case passthrough.** Request/response fields mirror the API verbatim. No
|
|
48
|
+
casing translation — in Python this is also the idiomatic form.
|
|
49
|
+
- **dataclasses + TypedDict, no runtime modeling (ADR PY-0002).** The only runtime
|
|
50
|
+
dependency is `httpx`. Streaming events are generated frozen dataclasses (the
|
|
51
|
+
contract's only named schemas); request payloads and key returned entities are
|
|
52
|
+
hand-written `TypedDict`s; the long tail is `dict[str, Any]` passthrough. We do
|
|
53
|
+
**not** validate responses at runtime — like the TS SDK, we trust the contract and
|
|
54
|
+
type structurally. The only fail-fast validation is client configuration
|
|
55
|
+
(api_key format, invalid option combinations).
|
|
56
|
+
- **Shared pure core, two thin clients (ADR PY-0001).** `_core/` holds I/O-agnostic
|
|
57
|
+
logic (config, errors, retry policy, SSE parsing) used by both `UniqOS` (over
|
|
58
|
+
`httpx.Client`) and `AsyncUniqOS` (over `httpx.AsyncClient`). Add a method to one
|
|
59
|
+
client → add it to the other; a surface-parity test guards against drift.
|
|
60
|
+
- **Never edit `generated/`.** It is overwritten by `make generate` from the vendored
|
|
61
|
+
snapshot and excluded from hand-maintenance. CI regenerates and fails on any diff.
|
|
62
|
+
- **Secrets & message content never logged.** Only the api_key prefix
|
|
63
|
+
(`uniq_test_kx7H...`); message content truncated to 50 chars at debug (SPEC-17 §11.3).
|
|
64
|
+
|
|
65
|
+
## Code generation
|
|
66
|
+
|
|
67
|
+
`make generate` runs `datamodel-code-generator` over the shared snapshot and writes
|
|
68
|
+
`src/uniqos/generated/stream_events.py` — the **frozen dataclasses** for the five SSE
|
|
69
|
+
events plus their nested objects, and the `RespondStreamEvent` union (ADR PY-0004).
|
|
70
|
+
|
|
71
|
+
It is scoped to `--openapi-scopes schemas`: the contract's _only_ 6 named schemas are
|
|
72
|
+
the streaming ones, so nothing else is generated. Everything else in the SDK is
|
|
73
|
+
hand-written. `--disable-timestamp` keeps the output byte-stable, so **regenerating an
|
|
74
|
+
unchanged snapshot is a no-op** — CI runs `make generate` then `git diff --exit-code`
|
|
75
|
+
and fails if the committed file drifts. The formatter (black/isort, pinned via
|
|
76
|
+
`uv.lock`) makes the output reproducible across machines and CI.
|
|
77
|
+
|
|
78
|
+
ruff skips `generated/` (it is machine-emitted); mypy `--strict` still type-checks it.
|
|
79
|
+
Never edit the file by hand — change the contract and regenerate.
|
|
80
|
+
|
|
81
|
+
## OpenAPI snapshot
|
|
82
|
+
|
|
83
|
+
Both SDKs read the **same** vendored snapshot at the monorepo root,
|
|
84
|
+
[`../../openapi/openapi.json`](../../openapi/openapi.json). To refresh the contract,
|
|
85
|
+
update the API, export its OpenAPI, refresh the snapshot, review the diff, regenerate,
|
|
86
|
+
and commit — see `../../openapi/README.md`.
|
|
87
|
+
|
|
88
|
+
## Style
|
|
89
|
+
|
|
90
|
+
ruff (`line-length = 100`, target `py310`) for both format and lint; mypy `--strict`.
|
|
91
|
+
Match the surrounding code: function over lambda for exported helpers, `async`/`await`,
|
|
92
|
+
named exports, Google-style docstrings on public methods (SPEC-17 §14.4).
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Examples
|
|
2
|
+
|
|
3
|
+
Runnable scripts for the `uniqos` SDK:
|
|
4
|
+
|
|
5
|
+
| File | What it shows |
|
|
6
|
+
| ------------------- | ---------------------------------------------------------- |
|
|
7
|
+
| `hello_world.py` | A single stateless `respond` call |
|
|
8
|
+
| `stateful.py` | Persistent `user_id`, multi-turn relational memory |
|
|
9
|
+
| `streaming.py` | Token-by-token SSE (`for event in stream`) |
|
|
10
|
+
| `error_handling.py` | Catching `RateLimitError` / `QuotaExhaustedError` / others |
|
|
11
|
+
| `dogfood.py` | Smoke test against a local (or remote) API |
|
|
12
|
+
|
|
13
|
+
## Running
|
|
14
|
+
|
|
15
|
+
Set the environment and run any script directly:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
export UNIQOS_API_KEY="uniq_test_..."
|
|
19
|
+
export UNIQOS_PERSONALITY_ID="pers_..."
|
|
20
|
+
export UNIQOS_BASE_URL="http://localhost:3000" # optional, for a local backend
|
|
21
|
+
|
|
22
|
+
python examples/hello_world.py
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
To exercise the SDK **without a network**, see the mocked-`httpx` (`respx`) pattern in the
|
|
26
|
+
[package README](../README.md#examples) and the test suite under [`../tests/`](../tests).
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Dogfood smoke test — exercise the SDK against a local (or remote) API.
|
|
2
|
+
|
|
3
|
+
Mirror of the TypeScript SDK's dogfood smoke. Reads a couple of endpoints and runs one
|
|
4
|
+
stateless turn, printing what came back.
|
|
5
|
+
|
|
6
|
+
Run against a local API:
|
|
7
|
+
export UNIQOS_API_KEY="uniq_test_..."
|
|
8
|
+
export UNIQOS_BASE_URL="http://localhost:3000"
|
|
9
|
+
export UNIQOS_PERSONALITY_ID="pers_..."
|
|
10
|
+
python examples/dogfood.py
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
|
|
15
|
+
from uniqos import UniqOS
|
|
16
|
+
|
|
17
|
+
DOGFOOD_USER = "dogfood_user_chess_001"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def main() -> None:
|
|
21
|
+
client = UniqOS(
|
|
22
|
+
api_key=os.environ["UNIQOS_API_KEY"],
|
|
23
|
+
base_url=os.environ.get("UNIQOS_BASE_URL", "http://localhost:3000"),
|
|
24
|
+
)
|
|
25
|
+
with client:
|
|
26
|
+
print(f"environment: {client.environment} base_url: {client.base_url}")
|
|
27
|
+
|
|
28
|
+
page = client.interactions.list(user_id=DOGFOOD_USER)
|
|
29
|
+
print(f"interactions for {DOGFOOD_USER}: {len(page.get('data', []))}")
|
|
30
|
+
|
|
31
|
+
result = client.respond(
|
|
32
|
+
personality_id=os.environ["UNIQOS_PERSONALITY_ID"],
|
|
33
|
+
message="Hello from the Python dogfood smoke test.",
|
|
34
|
+
)
|
|
35
|
+
print("respond:", result.get("response", result))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
if __name__ == "__main__":
|
|
39
|
+
main()
|