qoni 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
qoni-0.1.0/.gitignore ADDED
@@ -0,0 +1,8 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .venv/
7
+ .pytest_cache/
8
+ .coverage
@@ -0,0 +1,84 @@
1
+ stages:
2
+ - test
3
+ - package
4
+ - sync
5
+ - publish
6
+
7
+ # ---------- 测试:多 Python 版本矩阵 ----------
8
+ test:
9
+ stage: test
10
+ image: python:${PYTHON_VERSION}-slim
11
+ parallel:
12
+ matrix:
13
+ - PYTHON_VERSION: ["3.9", "3.10", "3.11", "3.12", "3.13"]
14
+ script:
15
+ - pip install --upgrade pip
16
+ - pip install -e ".[dev]"
17
+ - pytest -v
18
+ rules:
19
+ - if: $CI_PIPELINE_SOURCE == "merge_request_event"
20
+ - if: $CI_COMMIT_BRANCH == "main"
21
+
22
+ # ---------- 包内容检查:构建 + 元数据 + wheel 内容 + 安装验证 ----------
23
+ package-check:
24
+ stage: package
25
+ image: python:3.12-slim
26
+ script:
27
+ - pip install --upgrade pip
28
+ - pip install build twine check-wheel-contents
29
+ # 版本一致性检查:pyproject.toml 与 qoni/__init__.py
30
+ - |
31
+ PKG_VERSION=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
32
+ INIT_VERSION=$(python -c "import re; print(re.search(r'__version__ = \"(.+?)\"', open('qoni/__init__.py').read()).group(1))")
33
+ echo "pyproject.toml: $PKG_VERSION / qoni.__version__: $INIT_VERSION"
34
+ if [ "$PKG_VERSION" != "$INIT_VERSION" ]; then
35
+ echo "ERROR: 版本号不一致" >&2
36
+ exit 1
37
+ fi
38
+ - python -m build
39
+ - twine check --strict dist/*
40
+ - check-wheel-contents dist/*.whl
41
+ - pip install dist/*.whl
42
+ - python -c "import qoni; print('qoni', qoni.__version__, 'imported OK')"
43
+ artifacts:
44
+ paths:
45
+ - dist/
46
+ expire_in: 1 week
47
+ rules:
48
+ - if: $CI_PIPELINE_SOURCE == "merge_request_event"
49
+ - if: $CI_COMMIT_BRANCH == "main"
50
+
51
+ # ---------- 同步到 GitHub(镜像推送) ----------
52
+ sync-github:
53
+ stage: sync
54
+ image: alpine:3.20
55
+ variables:
56
+ GIT_DEPTH: 0
57
+ before_script:
58
+ - apk add --no-cache git
59
+ script:
60
+ - git push --force
61
+ "https://${GITHUB_SYNC_USER}:${GITHUB_SYNC_TOKEN}@github.com/QoniAI/qoni-sdk-python.git"
62
+ "HEAD:main" --tags
63
+ rules:
64
+ - if: $CI_COMMIT_BRANCH == "main" && $GITHUB_SYNC_TOKEN
65
+ needs:
66
+ - test
67
+ - package-check
68
+
69
+ # ---------- 发布到 PyPI(仅手动触发) ----------
70
+ publish-pypi:
71
+ stage: publish
72
+ image: python:3.12-slim
73
+ script:
74
+ - pip install --upgrade pip twine
75
+ - twine upload --non-interactive dist/*
76
+ variables:
77
+ TWINE_USERNAME: __token__
78
+ TWINE_PASSWORD: $PYPI_API_TOKEN
79
+ rules:
80
+ - if: $CI_COMMIT_BRANCH == "main"
81
+ when: manual
82
+ needs:
83
+ - test
84
+ - package-check
qoni-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Eazo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
qoni-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,342 @@
1
+ Metadata-Version: 2.5
2
+ Name: qoni
3
+ Version: 0.1.0
4
+ Summary: Unified Python SDK for Qoni Agent delegation, GUMem memory, and WebAgent automation.
5
+ Project-URL: Homepage, https://github.com/QONIAI/qoni-sdk-python
6
+ Project-URL: Issues, https://github.com/QONIAI/qoni-sdk-python/issues
7
+ License: MIT License
8
+
9
+ Copyright (c) 2026 Eazo
10
+
11
+ Permission is hereby granted, free of charge, to any person obtaining a copy
12
+ of this software and associated documentation files (the "Software"), to deal
13
+ in the Software without restriction, including without limitation the rights
14
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15
+ copies of the Software, and to permit persons to whom the Software is
16
+ furnished to do so, subject to the following conditions:
17
+
18
+ The above copyright notice and this permission notice shall be included in all
19
+ copies or substantial portions of the Software.
20
+
21
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
27
+ SOFTWARE.
28
+ License-File: LICENSE
29
+ Keywords: agent,delegation,gumem,qoni,sdk,webagent
30
+ Classifier: Development Status :: 4 - Beta
31
+ Classifier: Intended Audience :: Developers
32
+ Classifier: License :: OSI Approved :: MIT License
33
+ Classifier: Programming Language :: Python :: 3
34
+ Classifier: Programming Language :: Python :: 3.9
35
+ Classifier: Programming Language :: Python :: 3.10
36
+ Classifier: Programming Language :: Python :: 3.11
37
+ Classifier: Programming Language :: Python :: 3.12
38
+ Classifier: Programming Language :: Python :: 3.13
39
+ Requires-Python: >=3.9
40
+ Requires-Dist: httpx>=0.24
41
+ Provides-Extra: dev
42
+ Requires-Dist: pytest>=7; extra == 'dev'
43
+ Description-Content-Type: text/markdown
44
+
45
+ # qoni
46
+
47
+ Unified Python SDK for Qoni Agent delegation, GenAuth user management, GUMem memory, WebAgent automation, web search, and monitoring.
48
+
49
+ `qoni` gives a trusted server one compact way to use Qoni AK/SK. For runtime product calls, your service verifies the GenAuth user, requests short-lived delegation with `delegate_token`, and then uses the returned token in GUMem, Do Anything, Web Search, Deep Research, and Track calls. For GenAuth user management, the SDK exchanges AK/SK for a standard GenAuth management token internally and calls GenAuth v3 users APIs with that token.
50
+
51
+ AK/SK credentials must stay on a trusted server. Do not ship them to browsers, mobile apps, public CLI config, or untrusted Agent runtimes.
52
+
53
+ > This is the Python counterpart of [`@qoniai/qoni`](https://github.com/QONIAI/qoni-sdk-node) (the Node.js SDK). The API surface mirrors it, adapted to Python conventions: snake_case naming, keyword arguments, exceptions, generators, and dataclasses.
54
+
55
+ ## Why Qoni
56
+
57
+ Agents that perform real work need more than a backend API key. They need a user boundary, explicit scopes, expiry, observable execution, and audit metadata that explains what happened later.
58
+
59
+ Qoni keeps that model small:
60
+
61
+ - One SDK entry: `Qoni(access_key=..., secret_key=...)`.
62
+ - One discovery path: `host` can override the Qoni Console/SDK gateway for private or local deployments, and the SDK reads downstream runtime URLs from `/api/v3/eak/runtime-config`.
63
+ - One delegation entry: call `delegate_token`; silent mode returns `data["token"]`, while interactive mode returns an authorization URL and later completes on your server.
64
+ - One management path: call `genauth.users.*`, and the SDK exchanges AK/SK for a GenAuth management token before calling GenAuth v3 users APIs.
65
+ - Capability-first namespaces: `genauth`, `gumem`, `do_anything`, `web_search`, `deep_research`, and `track`.
66
+ - Readable scope strings for least-privilege authorization.
67
+ - Typed exceptions with `request_id`, `trace_id`, `audit_id`, and `retryable`.
68
+
69
+ ## Installation
70
+
71
+ ```bash
72
+ pip install qoni
73
+ ```
74
+
75
+ Requirements:
76
+
77
+ - Python 3.9 or later.
78
+ - Qoni `access_key` and `secret_key` created in Qoni Console.
79
+ - For silent runtime product calls, a real GenAuth user id from the userpool bound to the Qoni credential. For smoke tests, call `qoni.resolve_any_bound_user()` to grab the first bound user; in application code, resolve it with `current_user` or your existing server-side user session.
80
+ - For `genauth.users.*` management calls, no user id is required. The SDK uses AK/SK to request a GenAuth management token from Qoni.
81
+ - Optional `host` for private or local Qoni deployments. Leave it unset for hosted Qoni.
82
+
83
+ ## Quick Start
84
+
85
+ ```python
86
+ import os
87
+
88
+ from qoni import Qoni
89
+
90
+ qoni = Qoni(
91
+ access_key=os.environ["QONI_ACCESS_KEY"],
92
+ secret_key=os.environ["QONI_SECRET_KEY"],
93
+ )
94
+ ```
95
+
96
+ ### GenAuth User Management
97
+
98
+ `genauth.users.*` is a management-plane capability. It does not need a user id or `delegate_token`; the SDK exchanges AK/SK for a GenAuth management token internally.
99
+
100
+ ```python
101
+ users = qoni.genauth.users.list(page=1, limit=20)
102
+ print("GenAuth users:", users.data)
103
+
104
+ created = qoni.genauth.users.create(
105
+ username="sdk-demo",
106
+ password=os.environ["GENAUTH_DEMO_USER_PASSWORD"],
107
+ )
108
+
109
+ profile = qoni.genauth.users.get(user_id=created.data["userId"])
110
+
111
+ qoni.genauth.users.update(
112
+ user_id=profile.data["userId"],
113
+ nickname="SDK demo user",
114
+ )
115
+
116
+ # Optional smoke-test cleanup:
117
+ # qoni.genauth.users.delete_batch(user_ids=[profile.data["userId"]])
118
+ ```
119
+
120
+ ### Runtime Product Delegation
121
+
122
+ GUMem, Do Anything, Web Search, Deep Research, and Track act for an end user. Silent delegation calls need a real GenAuth user id, then a `delegate_token` result:
123
+
124
+ ```python
125
+ user_id = os.environ["QONI_USER_ID"]
126
+
127
+ # `products` is per-product authorization sugar; `agent` defaults to "sdk".
128
+ delegation = qoni.delegate_token(user_id=user_id, products=["do_anything"])
129
+ token = delegation.data["token"]
130
+ ```
131
+
132
+ Interactive delegation returns an authorization URL instead; complete it on your server after the user authorizes:
133
+
134
+ ```python
135
+ started = qoni.delegate_token(
136
+ mode="interactive",
137
+ redirect_uri="https://example.com/callback",
138
+ state="opaque-state",
139
+ products=["do_anything"],
140
+ )
141
+ print("send the user to:", started.data["authorizationUrl"])
142
+
143
+ # later, in the redirect handler:
144
+ completed = qoni.complete_delegate_token(
145
+ grant_id=started.data["grantId"],
146
+ code=code_from_callback,
147
+ state="opaque-state",
148
+ )
149
+ token = completed.data["token"]
150
+ ```
151
+
152
+ ### Do Anything
153
+
154
+ ```python
155
+ from qoni import CaptureOptions, QoniEventTypes
156
+
157
+ run = qoni.do_anything.run(
158
+ token=token, # passed once — the handle holds it; handle methods never take a token
159
+ prompt="Open https://en.wikipedia.org/wiki/Singapore and summarize the country's key facts.",
160
+ capture=CaptureOptions(screenshots=True),
161
+ )
162
+
163
+ # Stream semantic events (a generator; ends at the terminal event):
164
+ for event in run.events():
165
+ if event.type == QoniEventTypes.PROGRESS:
166
+ print("progress:", event.data)
167
+ elif event.type == QoniEventTypes.MESSAGE:
168
+ print(f"[{event.data['role']}] {event.data['text']}")
169
+ elif event.type == QoniEventTypes.SCREENSHOT:
170
+ open(f"step-{event.data.get('step', 0)}.png", "wb").write(event.image.data)
171
+ elif event.type == QoniEventTypes.DONE:
172
+ print("done:", event.data["terminal_reason"])
173
+
174
+ # Or drive the run to a settled result in one call:
175
+ result = run.wait(timeout=600)
176
+ print(result.status, result.output)
177
+
178
+ # Reuse the same browser session for a follow-up run:
179
+ follow_up = qoni.do_anything.run(
180
+ token=token,
181
+ prompt="Now open the History section and summarize it.",
182
+ session=run.session_ref,
183
+ )
184
+ ```
185
+
186
+ Reconnect to a run later from anywhere:
187
+
188
+ ```python
189
+ run = qoni.do_anything.attach(run_id, token=token)
190
+ print(run.status().status)
191
+ run.cancel("no longer needed") # idempotent — terminal runs return their state
192
+ ```
193
+
194
+ ### Human-in-the-loop interactions
195
+
196
+ When a run needs the user (site login, clarification, confirmation, take-control, wait), it emits an `interaction` event carrying a typed `Interaction`. Act on it via the handle's declared-action methods:
197
+
198
+ ```python
199
+ def on_interaction(handle, event):
200
+ if handle.type == "clarification":
201
+ print("agent asks:", handle.interaction.payload["question"])
202
+ handle.answer("Use the first option.")
203
+ elif handle.type == "site_login":
204
+ for site in handle.interaction.payload["sites"]:
205
+ print("sign in at:", site["login_url"])
206
+ handle.confirm_signed_in()
207
+
208
+ result = run.wait(on_interaction=on_interaction)
209
+ ```
210
+
211
+ Calling a method the backend did not declare raises `QoniValidationError` — check `handle.can(kind)` first if unsure.
212
+
213
+ ### Web Search
214
+
215
+ ```python
216
+ search = qoni.web_search.run(
217
+ token=qoni.delegate_token(user_id=user_id, products=["web_search"]).data["token"],
218
+ prompt=["latest Qoni SDK release", "Qoni delegation model"],
219
+ max_results_per_query=5,
220
+ )
221
+ result = search.wait(timeout=300)
222
+ print(result.output)
223
+ ```
224
+
225
+ ### Deep Research
226
+
227
+ ```python
228
+ research = qoni.deep_research.run(
229
+ token=qoni.delegate_token(user_id=user_id, products=["deep_research"]).data["token"],
230
+ prompt="The state of server-side agent authorization in 2026",
231
+ depth="standard",
232
+ )
233
+ result = research.wait(timeout=3600)
234
+ for artifact in result.artifacts:
235
+ open(artifact.name or f"{artifact.id}.md", "wb").write(artifact.content())
236
+ ```
237
+
238
+ ### Track (monitors)
239
+
240
+ ```python
241
+ monitor = qoni.track.create(
242
+ token=qoni.delegate_token(user_id=user_id, products=["track"]).data["token"],
243
+ prompt="Watch the pricing page of example.com and alert me on changes.",
244
+ )
245
+
246
+ monitor.run_now()
247
+ for event in monitor.events(): # resident stream — break when done observing
248
+ if event.type == QoniEventTypes.TRIGGERED:
249
+ print("change detected:", event.data)
250
+ break
251
+
252
+ monitor.pause()
253
+ monitor.refine(schedule={"kind": "interval", "interval_seconds": 3600})
254
+ monitor.resume()
255
+ print(monitor.runs(limit=10))
256
+ monitor.delete()
257
+ ```
258
+
259
+ ### GUMem memory
260
+
261
+ ```python
262
+ gumem_token = qoni.delegate_token(
263
+ user_id=user_id,
264
+ scopes=["gumem.memory:read", "gumem.memory:write"],
265
+ ).data["token"]
266
+
267
+ qoni.gumem.create_session(token=gumem_token, session_id="demo", title="Demo")
268
+ qoni.gumem.add_messages(
269
+ token=gumem_token,
270
+ session_id="demo",
271
+ messages=[{"role": "user", "content": "I prefer aisle seats."}],
272
+ )
273
+ context = qoni.gumem.recall(token=gumem_token, session_id="demo", query="seating preference")
274
+ print(context.data)
275
+ ```
276
+
277
+ ## Error handling
278
+
279
+ Every failure raises a typed exception from one hierarchy:
280
+
281
+ ```python
282
+ from qoni import (
283
+ QoniError, # base — code / status / request_id / trace_id / audit_id / retryable / body
284
+ QoniAuthError, # 401
285
+ QoniPermissionDeniedError, # 403 / missing scopes (message lists known scopes)
286
+ QoniValidationError, # 400 / 422 / local pre-validation
287
+ QoniRateLimitError, # 429 (retryable)
288
+ QoniUpstreamError, # 5xx / network (retryable)
289
+ QoniTimeoutError, # request or wait() timeout (retryable)
290
+ QoniTokenExpiredError,
291
+ QoniDelegationRequiredError,
292
+ )
293
+
294
+ try:
295
+ qoni.do_anything.run(token=token, prompt="...")
296
+ except QoniPermissionDeniedError as err:
297
+ print(err.code, err.status, err.request_id)
298
+ except QoniError as err:
299
+ if err.retryable:
300
+ ... # safe to retry with backoff
301
+ ```
302
+
303
+ Local pre-validation fails fast with actionable messages: unknown products, malformed scopes, missing user ids, and unsupported run options are raised before any request is made.
304
+
305
+ ## Scopes
306
+
307
+ ```python
308
+ from qoni import QoniScopes, QONI_PRODUCT_SCOPES, QONI_SCOPE_BUNDLES
309
+
310
+ QoniScopes.DO_ANYTHING_MANAGE # "webagent.do_anything:manage"
311
+ QONI_PRODUCT_SCOPES["do_anything"] # ("webagent.do_anything:read", "webagent.do_anything:manage")
312
+ QONI_SCOPE_BUNDLES["GUMEM_READONLY"]
313
+ ```
314
+
315
+ `delegate_token(products=[...])` accepts `"do_anything"`, `"web_search"`, `"deep_research"`, `"track"` and expands each to its read + manage pair.
316
+
317
+ ## Event model
318
+
319
+ `run.events()` and `run.wait(on_event=...)` deliver semantic `RunEvent` objects. Match on `event.type` against `QoniEventTypes` constants:
320
+
321
+ | type | `event.data` |
322
+ | --- | --- |
323
+ | `PROGRESS` | human-readable line (str) |
324
+ | `MESSAGE` | `{"text": str, "role": str}` |
325
+ | `INTERACTION` | typed `Interaction` (act via `run.interaction_handle(...)`) |
326
+ | `SCREENSHOT` | `{"page_url", "step"}` — decoded image on `event.image` |
327
+ | `DONE` | `{"output", "succeeded", "terminal_reason"}` (terminal) |
328
+ | `RESULTS_READY` | result count (int, web search) |
329
+ | `PHASE` / `SECTION_READY` | phase name / section title (deep research) |
330
+ | `MONITOR_CREATED` / `TRIGGERED` / `CHECK_COMPLETED` | monitor id / change summary / bool (track) |
331
+
332
+ All internal wire churn folds into `PROGRESS`; the original wire frame stays on `event.raw`. Dropped SSE connections reconnect automatically with `Last-Event-ID` catch-up (`sse_max_retries`, default 5).
333
+
334
+ ## Notes
335
+
336
+ - The public package and API use the Qoni brand. The deployed server protocol has not migrated yet, so internal signed routes remain under `/api/v3/eak/*`, the delegation token claim remains `eak_delegation_token`, and existing `eak.*` backend error codes are surfaced unchanged. Applications should use the public Qoni API and must not construct these internal wire values themselves.
337
+ - The client is synchronous and thread-safe for independent calls; construct it once per process. It is also a context manager (`with Qoni(...) as qoni:`).
338
+ - The client honors proxy environment variables (`HTTP_PROXY` / `HTTPS_PROXY` / `ALL_PROXY` / `NO_PROXY`) by default. Behind a local SOCKS proxy, either add your gateway to `NO_PROXY`, install `httpx[socks]`, or pass `Qoni(..., trust_env=False)` to bypass system proxies entirely.
339
+
340
+ ## License
341
+
342
+ MIT