querri 2.0.0__tar.gz → 2.2.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.
- querri-2.2.0/CHANGELOG.md +238 -0
- {querri-2.0.0 → querri-2.2.0}/CLAUDE.md +4 -4
- {querri-2.0.0 → querri-2.2.0}/PKG-INFO +59 -2
- {querri-2.0.0 → querri-2.2.0}/README.md +58 -1
- querri-2.2.0/docs/data-library.md +185 -0
- querri-2.2.0/docs/roles.md +151 -0
- {querri-2.0.0 → querri-2.2.0}/docs/server-sdk.md +212 -5
- {querri-2.0.0 → querri-2.2.0}/examples/05_get_session.py +29 -1
- {querri-2.0.0 → querri-2.2.0}/examples/10_error_handling.py +14 -0
- {querri-2.0.0 → querri-2.2.0}/examples/web-embed/server.py +46 -3
- querri-2.2.0/querri/__init__.py +110 -0
- {querri-2.0.0 → querri-2.2.0}/querri/_auth.py +12 -1
- {querri-2.0.0 → querri-2.2.0}/querri/_base_client.py +42 -4
- {querri-2.0.0 → querri-2.2.0}/querri/_client.py +48 -0
- {querri-2.0.0 → querri-2.2.0}/querri/_config.py +11 -0
- {querri-2.0.0 → querri-2.2.0}/querri/_convenience.py +25 -1
- querri-2.2.0/querri/_exceptions.py +440 -0
- {querri-2.0.0 → querri-2.2.0}/querri/_user_client.py +6 -2
- querri-2.2.0/querri/_version.py +1 -0
- {querri-2.0.0 → querri-2.2.0}/querri/cli/_app.py +12 -0
- {querri-2.0.0 → querri-2.2.0}/querri/cli/_context.py +20 -6
- {querri-2.0.0 → querri-2.2.0}/querri/cli/_output.py +63 -4
- {querri-2.0.0 → querri-2.2.0}/querri/cli/auth.py +6 -1
- {querri-2.0.0 → querri-2.2.0}/querri/cli/embed.py +21 -3
- {querri-2.0.0 → querri-2.2.0}/querri/cli/keys.py +2 -1
- querri-2.2.0/querri/cli/library.py +1763 -0
- querri-2.2.0/querri/cli/roles.py +411 -0
- {querri-2.0.0 → querri-2.2.0}/querri/cli/sources.py +1 -0
- {querri-2.0.0 → querri-2.2.0}/querri/cli/views.py +2 -1
- {querri-2.0.0 → querri-2.2.0}/querri/resources/embed.py +66 -6
- querri-2.2.0/querri/resources/library.py +1180 -0
- querri-2.2.0/querri/resources/roles.py +324 -0
- {querri-2.0.0 → querri-2.2.0}/querri/resources/users.py +16 -0
- {querri-2.0.0 → querri-2.2.0}/querri/types/__init__.py +19 -0
- {querri-2.0.0 → querri-2.2.0}/querri/types/embed.py +2 -0
- querri-2.2.0/querri/types/library.py +355 -0
- querri-2.2.0/querri/types/role.py +136 -0
- {querri-2.0.0 → querri-2.2.0}/skills/querri-cli/SKILL.md +55 -5
- {querri-2.0.0 → querri-2.2.0}/skills/querri-cli/references/json-shapes.md +71 -8
- {querri-2.0.0 → querri-2.2.0}/tests/test_cli.py +60 -3
- {querri-2.0.0 → querri-2.2.0}/tests/test_cli_commands.py +222 -0
- querri-2.2.0/tests/test_cli_json_errors.py +402 -0
- querri-2.2.0/tests/test_client_header.py +62 -0
- {querri-2.0.0 → querri-2.2.0}/tests/test_convenience.py +149 -0
- querri-2.2.0/tests/test_exceptions.py +977 -0
- querri-2.2.0/tests/test_library.py +842 -0
- querri-2.2.0/tests/test_library_streaming.py +325 -0
- {querri-2.0.0 → querri-2.2.0}/tests/test_resources.py +199 -1
- querri-2.2.0/tests/test_roles.py +529 -0
- {querri-2.0.0 → querri-2.2.0}/tests/test_security.py +49 -0
- querri-2.0.0/CHANGELOG.md +0 -89
- querri-2.0.0/querri/__init__.py +0 -53
- querri-2.0.0/querri/_exceptions.py +0 -201
- querri-2.0.0/querri/_version.py +0 -1
- querri-2.0.0/tests/test_exceptions.py +0 -162
- {querri-2.0.0 → querri-2.2.0}/.github/CONTRIBUTING.md +0 -0
- {querri-2.0.0 → querri-2.2.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {querri-2.0.0 → querri-2.2.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {querri-2.0.0 → querri-2.2.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {querri-2.0.0 → querri-2.2.0}/.github/SECURITY.md +0 -0
- {querri-2.0.0 → querri-2.2.0}/.github/workflows/ci.yml +0 -0
- {querri-2.0.0 → querri-2.2.0}/.github/workflows/release.yml +0 -0
- {querri-2.0.0 → querri-2.2.0}/.gitignore +0 -0
- {querri-2.0.0 → querri-2.2.0}/LICENSE +0 -0
- {querri-2.0.0 → querri-2.2.0}/TODO.md +0 -0
- {querri-2.0.0 → querri-2.2.0}/docs/MIGRATION.md +0 -0
- {querri-2.0.0 → querri-2.2.0}/examples/01_quick_start.py +0 -0
- {querri-2.0.0 → querri-2.2.0}/examples/02_user_management.py +0 -0
- {querri-2.0.0 → querri-2.2.0}/examples/03_access_policies.py +0 -0
- {querri-2.0.0 → querri-2.2.0}/examples/04_embed_session.py +0 -0
- {querri-2.0.0 → querri-2.2.0}/examples/06_projects.py +0 -0
- {querri-2.0.0 → querri-2.2.0}/examples/07_dashboards.py +0 -0
- {querri-2.0.0 → querri-2.2.0}/examples/08_chat_streaming.py +0 -0
- {querri-2.0.0 → querri-2.2.0}/examples/09_async_client.py +0 -0
- {querri-2.0.0 → querri-2.2.0}/examples/11_user_scoped_client.py +0 -0
- {querri-2.0.0 → querri-2.2.0}/examples/12_data_operations.py +0 -0
- {querri-2.0.0 → querri-2.2.0}/examples/web-embed/index.html +0 -0
- {querri-2.0.0 → querri-2.2.0}/pyproject.toml +0 -0
- {querri-2.0.0 → querri-2.2.0}/querri/_pagination.py +0 -0
- {querri-2.0.0 → querri-2.2.0}/querri/_streaming.py +0 -0
- {querri-2.0.0 → querri-2.2.0}/querri/cli/__init__.py +0 -0
- {querri-2.0.0 → querri-2.2.0}/querri/cli/_image.py +0 -0
- {querri-2.0.0 → querri-2.2.0}/querri/cli/audit.py +0 -0
- {querri-2.0.0 → querri-2.2.0}/querri/cli/chat.py +0 -0
- {querri-2.0.0 → querri-2.2.0}/querri/cli/chats.py +0 -0
- {querri-2.0.0 → querri-2.2.0}/querri/cli/dashboards.py +0 -0
- {querri-2.0.0 → querri-2.2.0}/querri/cli/files.py +0 -0
- {querri-2.0.0 → querri-2.2.0}/querri/cli/policies.py +0 -0
- {querri-2.0.0 → querri-2.2.0}/querri/cli/projects.py +0 -0
- {querri-2.0.0 → querri-2.2.0}/querri/cli/sharing.py +0 -0
- {querri-2.0.0 → querri-2.2.0}/querri/cli/steps.py +0 -0
- {querri-2.0.0 → querri-2.2.0}/querri/cli/usage.py +0 -0
- {querri-2.0.0 → querri-2.2.0}/querri/cli/users.py +0 -0
- {querri-2.0.0 → querri-2.2.0}/querri/cli/whoami.py +0 -0
- {querri-2.0.0 → querri-2.2.0}/querri/py.typed +0 -0
- {querri-2.0.0 → querri-2.2.0}/querri/resources/__init__.py +0 -0
- {querri-2.0.0 → querri-2.2.0}/querri/resources/audit.py +0 -0
- {querri-2.0.0 → querri-2.2.0}/querri/resources/dashboards.py +0 -0
- {querri-2.0.0 → querri-2.2.0}/querri/resources/files.py +0 -0
- {querri-2.0.0 → querri-2.2.0}/querri/resources/keys.py +0 -0
- {querri-2.0.0 → querri-2.2.0}/querri/resources/policies.py +0 -0
- {querri-2.0.0 → querri-2.2.0}/querri/resources/projects.py +0 -0
- {querri-2.0.0 → querri-2.2.0}/querri/resources/sharing.py +0 -0
- {querri-2.0.0 → querri-2.2.0}/querri/resources/sources.py +0 -0
- {querri-2.0.0 → querri-2.2.0}/querri/resources/usage.py +0 -0
- {querri-2.0.0 → querri-2.2.0}/querri/resources/views.py +0 -0
- {querri-2.0.0 → querri-2.2.0}/querri/types/audit.py +0 -0
- {querri-2.0.0 → querri-2.2.0}/querri/types/chat.py +0 -0
- {querri-2.0.0 → querri-2.2.0}/querri/types/dashboard.py +0 -0
- {querri-2.0.0 → querri-2.2.0}/querri/types/data.py +0 -0
- {querri-2.0.0 → querri-2.2.0}/querri/types/file.py +0 -0
- {querri-2.0.0 → querri-2.2.0}/querri/types/key.py +0 -0
- {querri-2.0.0 → querri-2.2.0}/querri/types/policy.py +0 -0
- {querri-2.0.0 → querri-2.2.0}/querri/types/project.py +0 -0
- {querri-2.0.0 → querri-2.2.0}/querri/types/sharing.py +0 -0
- {querri-2.0.0 → querri-2.2.0}/querri/types/usage.py +0 -0
- {querri-2.0.0 → querri-2.2.0}/querri/types/user.py +0 -0
- {querri-2.0.0 → querri-2.2.0}/scripts/release.sh +0 -0
- {querri-2.0.0 → querri-2.2.0}/skills/README.md +0 -0
- {querri-2.0.0 → querri-2.2.0}/skills/querri-cli/references/troubleshooting.md +0 -0
- {querri-2.0.0 → querri-2.2.0}/tests/__init__.py +0 -0
- {querri-2.0.0 → querri-2.2.0}/tests/conftest.py +0 -0
- {querri-2.0.0 → querri-2.2.0}/tests/rls-integration-results.md +0 -0
- {querri-2.0.0 → querri-2.2.0}/tests/test_client.py +0 -0
- {querri-2.0.0 → querri-2.2.0}/tests/test_config.py +0 -0
- {querri-2.0.0 → querri-2.2.0}/tests/test_integration.py +0 -0
- {querri-2.0.0 → querri-2.2.0}/tests/test_new_features.py +0 -0
- {querri-2.0.0 → querri-2.2.0}/tests/test_pagination.py +0 -0
- {querri-2.0.0 → querri-2.2.0}/tests/test_rls_integration.py +0 -0
- {querri-2.0.0 → querri-2.2.0}/tests/test_streaming.py +0 -0
- {querri-2.0.0 → querri-2.2.0}/tests/test_streaming_v2.py +0 -0
- {querri-2.0.0 → querri-2.2.0}/uv.lock +0 -0
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 2.2.0 (2026-10-31)
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **Custom roles as code.** `client.roles` (sync and async): `list()`,
|
|
8
|
+
`get(ref)`, `catalog()`, `apply(definition, dry_run=)` (idempotent upsert,
|
|
9
|
+
`PUT /roles/{key}`), `import_roles(definitions, dry_run=)` (one call, each
|
|
10
|
+
role independent), `delete(ref, reassign_to=)` and `export()` (definitions
|
|
11
|
+
with the output-only fields stripped, ready to save and re-apply). New
|
|
12
|
+
types in `querri.types`: `RoleDefinition`, `RoleApplyResult`,
|
|
13
|
+
`RoleChange`, `RoleApplyError`, `RoleImportResult`, `RoleDeleteResponse`,
|
|
14
|
+
`PermissionCatalog`, `CatalogPermission`. Needs the `admin:roles:read` /
|
|
15
|
+
`admin:roles:write` scopes. See docs/roles.md.
|
|
16
|
+
- **`querri role`** (alias `querri roles`): `list`, `get`, `export`,
|
|
17
|
+
`apply -f FILE [--dry-run] [--watch]`, `delete [--reassign-to] [--yes]` and
|
|
18
|
+
`catalog [--area]`. `apply` prints a per-field change summary
|
|
19
|
+
(`denies: +chat.code.view -tools.dashboards.use`) and exits non-zero when
|
|
20
|
+
any role failed; `--watch` re-applies whenever the file is saved. JSON
|
|
21
|
+
files only.
|
|
22
|
+
- The **Roles** scope group in the `querri key new` scope picker.
|
|
23
|
+
- **Custom Roles on embed sessions.** `client.embed.create_session()` and
|
|
24
|
+
`client.embed.get_session()` (sync and async) take `role=` — the id or key
|
|
25
|
+
of the Custom Role the session runs as. It is sent only when set, and it
|
|
26
|
+
is not `user["role"]` (the org role used when creating a user). Decide it
|
|
27
|
+
on your server, never from the browser's request. Granting a role the user
|
|
28
|
+
does not hold is **permanent**: it is added to the user's role assignment
|
|
29
|
+
(later sessions and the app see it), revoking the session does not remove
|
|
30
|
+
it, and an admin removes it in Settings → People. `EmbedSession.role_id`
|
|
31
|
+
and `EmbedSessionListItem.role_id` carry the applied role (`None` without
|
|
32
|
+
one — or when an older server ignored `role`, in which case the session
|
|
33
|
+
runs as the user's own role; servers released after 2026-10-21 also
|
|
34
|
+
return it from refresh and list, earlier ones only from create);
|
|
35
|
+
`refresh_session()` returns it too, and the `get_session()` dict carries
|
|
36
|
+
`role_id` when a role was applied. CLI: `querri session new --role` /
|
|
37
|
+
`querri session get --role`; `session new` and `session refresh` print
|
|
38
|
+
`Role ID`, and the `session list` table has a `Role ID` column.
|
|
39
|
+
- **`APIError.permission` and `APIError.reason`.** A `403
|
|
40
|
+
permission_denied` names the permission the call needed (from the body,
|
|
41
|
+
else the `X-Permission-Denied` header) and why it was refused
|
|
42
|
+
(`role_denied`, `not_in_plan`, `flag_off`, `embed_disabled`,
|
|
43
|
+
`dependency_unmet`, `forbidden`, `scope_exceeds_role`; the server may add
|
|
44
|
+
more). The header is used only when the error's code is
|
|
45
|
+
`permission_denied` or there is no code, and only when it holds a
|
|
46
|
+
permission key (contains a `.`), not an error code — so an
|
|
47
|
+
`escalation_denied` refusal never borrows it. The in-app flat body
|
|
48
|
+
`{"detail": {"error": "<code>", ...}}` is parsed too. `RateLimitError`
|
|
49
|
+
keeps them too.
|
|
50
|
+
- **`APIError.body`**: the parsed JSON response body (any JSON value; `None`
|
|
51
|
+
when the response was not JSON), on every `APIError`, streamed requests
|
|
52
|
+
included. It carries the fields the exception does not lift out, e.g.
|
|
53
|
+
`err.body["detail"]["error"]["scopes"]` on `scope_exceeds_role` and
|
|
54
|
+
`err.body["detail"]["error"]["missing"]` on `escalation_denied`.
|
|
55
|
+
- **Error-code constants**: `querri.ROLE_DISABLED`, `querri.PERMISSION_DENIED`,
|
|
56
|
+
`querri.ESCALATION_DENIED` (403), `querri.LAST_ADMIN` (409),
|
|
57
|
+
`querri.INVALID_PARAMETERS` (400), … (26 in all) and the
|
|
58
|
+
`querri.ErrorCode` / `querri.PermissionDeniedReason` `Literal`s — the
|
|
59
|
+
codes for authentication, access, Custom Roles, embed sessions and user
|
|
60
|
+
role changes; the API sends other codes too, so treat `code` as a string.
|
|
61
|
+
docs/server-sdk.md has an Error codes table.
|
|
62
|
+
- `embed:role:assign` in the `querri key new` scope picker.
|
|
63
|
+
- **`X-Querri-Client` header.** Every client sends `X-Querri-Client:
|
|
64
|
+
sdk/<version>`; clients built by the `querri` command, and its
|
|
65
|
+
login/refresh token requests, send `cli/<version>`, so the server can tell
|
|
66
|
+
CLI use from library use.
|
|
67
|
+
|
|
68
|
+
### Changed
|
|
69
|
+
|
|
70
|
+
- **`422` request-validation errors raise `ValidationError`** (was the base
|
|
71
|
+
`APIError`) with a readable message — `"Validation failed: body.ttl: Input
|
|
72
|
+
should be greater than or equal to 900"` — instead of the raw `detail`
|
|
73
|
+
list. A bare `{"detail": {"code", "message"}}` body is parsed like the
|
|
74
|
+
`error` envelope.
|
|
75
|
+
- **Behaviour change — CLI: a 403 exits `6`** instead of `1`. Scripts that
|
|
76
|
+
test for exit code `1` on a refused call must accept `6`. In text mode the
|
|
77
|
+
message gains the permission and reason (`(permission:
|
|
78
|
+
dashboards.list.view, reason: role_denied)`); the API code is printed once,
|
|
79
|
+
on its `Code:` line, as before.
|
|
80
|
+
- **CLI `--json` errors: unchanged from 2.1.0, plus `exit_code`.** Every
|
|
81
|
+
JSON error prints the same keys and values 2.1.0 printed for the same
|
|
82
|
+
situation — a 403 is still `"error": "api_error"`, `code` is still the API
|
|
83
|
+
error code when the server sent one and otherwise the exit code, and
|
|
84
|
+
`permission` / `reason` are not added as keys — plus `"exit_code"`, the
|
|
85
|
+
process exit code. The one value that moves is a consequence of the exit
|
|
86
|
+
code above: a 403 with no API code has `"code": 6` (it was `1`). The `querri
|
|
87
|
+
view run` timeout (`{"error": "<message>"}`) and `querri auth switch-org`
|
|
88
|
+
single-org outputs keep their 2.1.0 shapes, plus `exit_code`.
|
|
89
|
+
- Docs: API-key scopes follow a person's role — a key keeps only the scopes
|
|
90
|
+
both its creator and its bound user currently hold, and the permission
|
|
91
|
+
checks made on its behalf (embed role grants, the users endpoints' role
|
|
92
|
+
guards) use the permissions both hold (servers released after 2026-10-21;
|
|
93
|
+
earlier servers cap only keys created in Settings and check role grants
|
|
94
|
+
against the bound user, else the creator). The keys section covers
|
|
95
|
+
binding (Admins only), pseudo ids, and what a key minted by another key
|
|
96
|
+
inherits. On those servers `client.users` `create` / `update` / `delete` /
|
|
97
|
+
`get_or_create` (and `get_session()` for a new user) can be refused with
|
|
98
|
+
`escalation_denied` or `last_admin`. The real scope list replaces the
|
|
99
|
+
invented names in the CLI skill. `as_user` calls can raise
|
|
100
|
+
`permission_denied` under the session's role.
|
|
101
|
+
|
|
102
|
+
### Fixed
|
|
103
|
+
|
|
104
|
+
- **Streaming calls keep their error details.** A streamed request
|
|
105
|
+
(`projects.chats.stream()`, view streams) that failed raised an
|
|
106
|
+
`APIError` with no `code`, `message`, `permission` or `reason`, because
|
|
107
|
+
the unread body could not be parsed. The body is now read (and the
|
|
108
|
+
response closed) before raising; a body that fails mid-read still raises
|
|
109
|
+
the status's exception class.
|
|
110
|
+
- **Retried streamed responses are closed.** A streamed request that got a
|
|
111
|
+
429 or a retryable 5xx left each discarded response open, holding its
|
|
112
|
+
connection; each is now closed before the retry.
|
|
113
|
+
- **A JSON array or scalar error body no longer crashes error handling.**
|
|
114
|
+
`raise_for_status` assumed a JSON object and raised `AttributeError` on a
|
|
115
|
+
body like `["bad"]` or `"oops"`; it now raises the status's exception.
|
|
116
|
+
- `create_session(role="")` / `get_session(role="")` no longer send an
|
|
117
|
+
empty `role`.
|
|
118
|
+
- **`querri view list` no longer truncates IDs.** The Rich table clipped
|
|
119
|
+
36-character view UUIDs with `…`, and pasting the clipped ID into `querri
|
|
120
|
+
view chat` failed with a misleading 403. ID/UUID columns now never wrap
|
|
121
|
+
(QUE-135).
|
|
122
|
+
- `skills/querri-cli/references/json-shapes.md` documented a non-existent
|
|
123
|
+
`querri embed create-session` and a stale `querri key create`; it now
|
|
124
|
+
documents `querri session new` / `refresh` / `list` / `get`, `querri key
|
|
125
|
+
new` and the `--json` error shape.
|
|
126
|
+
- **`examples/web-embed/server.py` no longer exposes the Werkzeug
|
|
127
|
+
debugger.** It bound `0.0.0.0` with `debug=True`, letting anyone on the
|
|
128
|
+
network run code on the machine. It now binds `127.0.0.1` with debug off;
|
|
129
|
+
`HOST` and `FLASK_DEBUG=1` opt in, and debug is refused on a non-local
|
|
130
|
+
host.
|
|
131
|
+
|
|
132
|
+
## 2.1.0 (2026-09-24)
|
|
133
|
+
|
|
134
|
+
### Added
|
|
135
|
+
|
|
136
|
+
- **`client.library`** (sync and async) and the `querri library` CLI: the Data
|
|
137
|
+
Library surface — libraries, collections, anchor and refining questions,
|
|
138
|
+
graph search and zoom, `record_fact`, the Librarian `chat` / `chat_stream`,
|
|
139
|
+
`ask`, `intake`, `consolidate`, `eval`, backfill and health. Reference:
|
|
140
|
+
[docs/data-library.md](docs/data-library.md). This is the `dev` line that
|
|
141
|
+
had been building since 1.1.0; it ships here unchanged.
|
|
142
|
+
|
|
143
|
+
### Fixed
|
|
144
|
+
|
|
145
|
+
- **Error bodies without a Stripe-style envelope keep their message and
|
|
146
|
+
code.** FastAPI raises `{"detail": {"error": {...}}}` (or a plain string
|
|
147
|
+
`detail`); `APIError.message`, `.code` and `.type` were left as
|
|
148
|
+
`"HTTP <status>"` / `None` for those, so a `NotFoundError` for a step with
|
|
149
|
+
no data read the same as one for a missing project. Both shapes are parsed
|
|
150
|
+
now.
|
|
151
|
+
|
|
152
|
+
## 2.0.0 (2026-08-31)
|
|
153
|
+
|
|
154
|
+
Migration guide with before/after snippets: [docs/MIGRATION.md](docs/MIGRATION.md).
|
|
155
|
+
|
|
156
|
+
### Breaking
|
|
157
|
+
|
|
158
|
+
- **`source_scope` removed from `client.embed.create_session()`** (sync and
|
|
159
|
+
async). It was never enforced by the server — a silently-ignored parameter
|
|
160
|
+
that *looked* like access control. Passing it now raises `TypeError`. Scope
|
|
161
|
+
sessions with access policies instead: `client.embed.get_session(access=...)`.
|
|
162
|
+
- **`as_user()` / `UserQuerri` rebased from `/api` to `/api/v1`.** Embed
|
|
163
|
+
sessions (`X-Embed-Session`) are the public API's highest-priority
|
|
164
|
+
credential, so user-scoped clients now call the same public API paths as
|
|
165
|
+
the admin client.
|
|
166
|
+
- **`user_client.dashboards` is read-only.** Embed sessions exclude the
|
|
167
|
+
`admin:dashboards:write` scope server-side; the user-scoped surface now
|
|
168
|
+
exposes only `list()`, `get()`, and `refresh_status()` — `create`,
|
|
169
|
+
`update`, `delete`, and `refresh` no longer exist there (they always
|
|
170
|
+
belonged to the admin client).
|
|
171
|
+
- There is no `user_client.embed`: embed sessions exclude
|
|
172
|
+
`embed:session:create` server-side (a session cannot mint sessions).
|
|
173
|
+
The accessor never existed in the SDK; the contract is now documented
|
|
174
|
+
and tested.
|
|
175
|
+
- **`ttl` is validated client-side**: `create_session()` and
|
|
176
|
+
`get_session()` raise `ValueError` for TTLs outside [900, 86400] seconds
|
|
177
|
+
instead of sending a value the server would silently clamp.
|
|
178
|
+
|
|
179
|
+
### Added
|
|
180
|
+
|
|
181
|
+
- `client.embed.get_ui_config(org)` — fetches the public, unauthenticated
|
|
182
|
+
`GET {host}/api/embed/ui-config?org=...` (main app path, not `/api/v1`)
|
|
183
|
+
and returns the raw `{chrome, theme, privacy}` dict.
|
|
184
|
+
- `querri session ui-config --org <id>` CLI command for the same.
|
|
185
|
+
- `OriginRequiredError` (subclass of `ValidationError`), raised for
|
|
186
|
+
`400 origin_required` with a message explaining the org's embed-domain
|
|
187
|
+
allowlist and how to pass `origin`.
|
|
188
|
+
- `querri share source org` gained `--disable` to turn org-wide sharing
|
|
189
|
+
off (the server contract always supported `enabled: false`).
|
|
190
|
+
- `querri keys create` interactive scope picker now includes the
|
|
191
|
+
`admin:skills:*`, `admin:library:*` scopes and `*` (superadmin).
|
|
192
|
+
|
|
193
|
+
### Fixed
|
|
194
|
+
|
|
195
|
+
- `client.embed.revoke_user_sessions()` no longer stops at the first
|
|
196
|
+
listing page: the server caps listings at 200 with no cursor, so it now
|
|
197
|
+
loops revoke-and-relist until a listing shows no sessions for the user
|
|
198
|
+
(bounded at 50 passes), and tolerates a session expiring between list
|
|
199
|
+
and delete. Still best-effort — with more than 200 concurrent sessions
|
|
200
|
+
org-wide the listing is a sample and stragglers can survive; do not
|
|
201
|
+
treat a return as proof every session is gone.
|
|
202
|
+
|
|
203
|
+
## 1.1.1 (2026-08-31)
|
|
204
|
+
|
|
205
|
+
### Fixed
|
|
206
|
+
|
|
207
|
+
- **Web-embed example** (`examples/web-embed/server.py`): three dispatched
|
|
208
|
+
actions called keyword-only SDK methods positionally, raising `TypeError`
|
|
209
|
+
(returned as HTTP 500): `embed.refreshSession`, `users.getOrCreate`, and
|
|
210
|
+
`policies.resolve`. All now pass keyword arguments.
|
|
211
|
+
- **Web-embed example** (`examples/web-embed/index.html`): the SDK Explorer's
|
|
212
|
+
"Data" group sent `data.*` action names the server never dispatched — every
|
|
213
|
+
action in the group returned "Unknown action". Renamed to the real
|
|
214
|
+
`sources.*` actions.
|
|
215
|
+
- **Docs**: `docs/server-sdk.md` and `README.md` showed `create_session`,
|
|
216
|
+
`refresh_session`, and `get_session` with positional parameters; all three
|
|
217
|
+
are keyword-only (`create_session(*, user_id=..., origin=..., ttl=...)`,
|
|
218
|
+
`refresh_session(session_token=...)`). Snippets updated to keyword form.
|
|
219
|
+
- **Browser embed loading**: examples and docs now load the embed script from
|
|
220
|
+
the product — `<script src="{serverUrl}/sdk/querri-embed.js">` — instead of
|
|
221
|
+
an unverifiable CDN/npm IIFE path. The web-embed example injects the script
|
|
222
|
+
after fetching `serverUrl` from its `/api/config` endpoint.
|
|
223
|
+
- **Embed chrome options**: examples use the v2 vocabulary
|
|
224
|
+
(`chrome: { rail: { show: true }, header: { show: true } }` — `rail`
|
|
225
|
+
replaces `sidebar`).
|
|
226
|
+
- **Origin allowlist guidance**: create-session docs and examples now pass
|
|
227
|
+
`origin` and document the enforcement behavior — when the organization
|
|
228
|
+
configures allowed embed domains, session creation without an origin fails
|
|
229
|
+
with `400 origin_required`, and an origin not on the allowlist fails with
|
|
230
|
+
`403 origin_not_allowed`.
|
|
231
|
+
|
|
232
|
+
### Deprecated
|
|
233
|
+
|
|
234
|
+
- `source_scope` on `client.embed.create_session()` (sync and async): the
|
|
235
|
+
server does not enforce it, and it will be removed in v2.0.0. Passing it now
|
|
236
|
+
emits a `DeprecationWarning`; the field is still sent, so behavior is
|
|
237
|
+
unchanged. Scope sessions with access policies instead — see
|
|
238
|
+
`client.embed.get_session(access=...)`.
|
|
@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|
|
4
4
|
|
|
5
5
|
## Project Overview
|
|
6
6
|
|
|
7
|
-
Querri Python SDK — a typed Python client for the Querri API, providing both sync (`Querri`) and async (`AsyncQuerri`) clients. Built on httpx + Pydantic v2. Requires Python >=3.
|
|
7
|
+
Querri Python SDK — a typed Python client for the Querri API, providing both sync (`Querri`) and async (`AsyncQuerri`) clients. Built on httpx + Pydantic v2. Requires Python >=3.10.
|
|
8
8
|
|
|
9
9
|
## Commands
|
|
10
10
|
|
|
@@ -37,7 +37,7 @@ ruff check querri/ tests/
|
|
|
37
37
|
`Querri` / `AsyncQuerri` (`_client.py`) are the public entry points. They hold lazily-initialized resource namespaces (e.g., `client.users`, `client.projects`) and support context-manager usage. They delegate HTTP to `SyncHTTPClient` / `AsyncHTTPClient` (`_base_client.py`), which handle auth headers (Bearer token + X-Tenant-ID), retries with exponential backoff, and error-to-exception mapping.
|
|
38
38
|
|
|
39
39
|
### User-Scoped Client
|
|
40
|
-
`UserQuerri` / `AsyncUserQuerri` (`_user_client.py`) are created via `client.as_user(session)`. They use `X-Embed-Session` auth against the
|
|
40
|
+
`UserQuerri` / `AsyncUserQuerri` (`_user_client.py`) are created via `client.as_user(session)`. They use `X-Embed-Session` auth against the same public API (`/api/v1`) as the admin client. Only expose FGA-filtered resources: `projects`, `dashboards` (read-only), `sources`, `chats`. Calls are subject to the session's role and can raise `PermissionError` (`permission_denied`).
|
|
41
41
|
|
|
42
42
|
### Resource Pattern
|
|
43
43
|
Each API resource lives in `querri/resources/` with paired sync/async classes (e.g., `Users` / `AsyncUsers`). Resources receive the HTTP client at init and expose CRUD methods that return Pydantic models from `querri/types/`. Projects has a nested `Chats` sub-resource for chat streaming.
|
|
@@ -88,8 +88,8 @@ Every resource, pagination class, and stream has mirrored sync/async implementat
|
|
|
88
88
|
|
|
89
89
|
## Code Quality
|
|
90
90
|
|
|
91
|
-
- **mypy**: Strict mode, target Python 3.
|
|
92
|
-
- **ruff**: Rules E, F, I, N, UP, B, SIM (target Python 3.
|
|
91
|
+
- **mypy**: Strict mode, target Python 3.10
|
|
92
|
+
- **ruff**: Rules E, F, I, N, UP, B, SIM (target Python 3.10)
|
|
93
93
|
- **Tests**: pytest + pytest-asyncio (mode: auto) + respx for HTTP mocking
|
|
94
94
|
|
|
95
95
|
## Releasing
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: querri
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.0
|
|
4
4
|
Summary: Python SDK and CLI for the Querri data analysis platform
|
|
5
5
|
Project-URL: Homepage, https://querri.com
|
|
6
6
|
Project-URL: Documentation, https://docs.querri.com/sdk/python
|
|
@@ -257,6 +257,32 @@ session["expires_in"] # int — seconds until expiry
|
|
|
257
257
|
session["user_id"] # str — Querri user ID
|
|
258
258
|
```
|
|
259
259
|
|
|
260
|
+
To run the session as one of your org's **Custom Roles**, pass `role` (a role
|
|
261
|
+
id or key); the result carries `role_id` when it was applied:
|
|
262
|
+
|
|
263
|
+
```python
|
|
264
|
+
session = client.embed.get_session(user="customer-42", role="analyst")
|
|
265
|
+
session.get("role_id") # str — present only when a role was applied
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
`role_id` is a custom role's id (`role_…`) or a built-in role's
|
|
269
|
+
(`system:creator`). No `role_id` means the role was not applied — an older
|
|
270
|
+
server ignores `role`, and the session runs as the user's own role.
|
|
271
|
+
|
|
272
|
+
`role` is not `user["role"]` (the org role, `admin`/`member`, used when the
|
|
273
|
+
user is created). **Decide `role` on your server from your own auth — never
|
|
274
|
+
read it from the browser's request.** Granting a role the user does not yet
|
|
275
|
+
hold needs an API key with the `embed:role:assign` scope, and **the grant is
|
|
276
|
+
permanent**: the role is added to the user's assignment, so their later
|
|
277
|
+
sessions and the Querri app see it, and revoking the session does not remove
|
|
278
|
+
it (an admin removes it in Settings → People). Failures raise
|
|
279
|
+
`NotFoundError` (`role_not_found`) or `PermissionError` (`role_not_allowed`,
|
|
280
|
+
`role_disabled`, `insufficient_scope`, `seat_change_required`). The user is
|
|
281
|
+
created/updated and access policies applied before the session is requested,
|
|
282
|
+
so if the role is refused those steps have still happened. On servers
|
|
283
|
+
released after 2026-10-21, creating that user can itself be refused
|
|
284
|
+
(`escalation_denied`, `last_admin` — see below).
|
|
285
|
+
|
|
260
286
|
You can also pass pre-created policy IDs directly:
|
|
261
287
|
|
|
262
288
|
```python
|
|
@@ -290,6 +316,7 @@ See **[docs/server-sdk.md](docs/server-sdk.md#user-scoped-client-as_user)** for
|
|
|
290
316
|
| `client.projects.chats` | Chats within projects | `create`, `list`, `get`, `stream`, `cancel`, `delete` |
|
|
291
317
|
| `client.sources` | Sources, connectors & data | `list`, `create`, `create_data_source`, `query`, `source_data`, `append_rows`, `replace_data`, `ask`, `sync`, `list_connectors` |
|
|
292
318
|
| `client.views` | SQL-defined views | `list`, `create`, `get`, `update`, `delete`, `run`, `get_run`, `wait_for_run`, `preview`, `chat` |
|
|
319
|
+
| `client.library` | Data Library — graph + librarian agent | `create_library`, `create_collection`, `create_anchor_question`, `create_refining_question`, `link`, `status`, `list_nodes`, `get_node`, `search`, `zoom`, `record_fact`, `chat`, `chat_stream`, `backfill`, `seed_fixture`, `health` |
|
|
293
320
|
| `client.files` | File management | `upload`, `list`, `get`, `delete` |
|
|
294
321
|
| `client.keys` | API key management | `create`, `list`, `get`, `delete` |
|
|
295
322
|
| `client.sharing` | Sharing & permissions | `share_project`, `share_dashboard`, `share_source`, `list_project_shares`, `list_dashboard_shares`, `revoke_project_share`, `revoke_dashboard_share`, `org_share_source` |
|
|
@@ -317,7 +344,7 @@ All errors extend `QuerriError`:
|
|
|
317
344
|
```
|
|
318
345
|
QuerriError
|
|
319
346
|
├── APIError — HTTP error responses
|
|
320
|
-
│ ├── ValidationError — 400
|
|
347
|
+
│ ├── ValidationError — 400 / 422
|
|
321
348
|
│ │ └── OriginRequiredError — 400 origin_required (org has an embed-domain allowlist)
|
|
322
349
|
│ ├── AuthenticationError — 401
|
|
323
350
|
│ ├── PermissionError — 403
|
|
@@ -329,10 +356,40 @@ QuerriError
|
|
|
329
356
|
└── ConfigError — missing/invalid configuration
|
|
330
357
|
```
|
|
331
358
|
|
|
359
|
+
Every `APIError` carries `status`, `message`, `type`, `code`, `doc_url`,
|
|
360
|
+
`request_id` and `body` (the parsed JSON response, `None` when it was not
|
|
361
|
+
JSON — it holds the fields the exception does not lift out, e.g.
|
|
362
|
+
`e.body["detail"]["error"]["scopes"]` on `scope_exceeds_role`). A `403
|
|
363
|
+
permission_denied` (the caller's role does not allow the action) also carries
|
|
364
|
+
`permission` and `reason` (`role_denied`, `not_in_plan`, `flag_off`,
|
|
365
|
+
`embed_disabled`, `dependency_unmet`, `forbidden`, `scope_exceeds_role` — the
|
|
366
|
+
server may add more). On servers
|
|
367
|
+
released after 2026-10-21, `client.users` `create` / `update` / `delete` /
|
|
368
|
+
`get_or_create` — and `get_session()` for a new user — can raise
|
|
369
|
+
`PermissionError` (`escalation_denied`, the permissions you lack in
|
|
370
|
+
`e.body["detail"]["error"]["missing"]`) or `ConflictError` (`last_admin`).
|
|
371
|
+
Compare `code` against the
|
|
372
|
+
exported constants (they cover authentication, access, Custom Roles and embed
|
|
373
|
+
sessions; the API sends other codes too, so treat `code` as a string):
|
|
374
|
+
|
|
375
|
+
```python
|
|
376
|
+
from querri import PermissionError, PERMISSION_DENIED # shadows the builtin
|
|
377
|
+
|
|
378
|
+
try:
|
|
379
|
+
client.dashboards.list()
|
|
380
|
+
except PermissionError as e:
|
|
381
|
+
if e.code == PERMISSION_DENIED:
|
|
382
|
+
print(f"needs {e.permission} ({e.reason})")
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
See [docs/server-sdk.md](docs/server-sdk.md#error-codes) for the code table.
|
|
386
|
+
|
|
332
387
|
The SDK automatically retries **429** (always) and **5xx** (idempotent methods only) with exponential backoff + jitter.
|
|
333
388
|
|
|
334
389
|
### Full Reference
|
|
335
390
|
|
|
391
|
+
Custom roles can be managed as code (export, edit, `querri role apply -f role.json --watch`): see **[docs/roles.md](docs/roles.md)**.
|
|
392
|
+
|
|
336
393
|
See **[docs/server-sdk.md](docs/server-sdk.md)** for complete method signatures, all framework examples, and the `get_session()` deep dive.
|
|
337
394
|
|
|
338
395
|
---
|
|
@@ -211,6 +211,32 @@ session["expires_in"] # int — seconds until expiry
|
|
|
211
211
|
session["user_id"] # str — Querri user ID
|
|
212
212
|
```
|
|
213
213
|
|
|
214
|
+
To run the session as one of your org's **Custom Roles**, pass `role` (a role
|
|
215
|
+
id or key); the result carries `role_id` when it was applied:
|
|
216
|
+
|
|
217
|
+
```python
|
|
218
|
+
session = client.embed.get_session(user="customer-42", role="analyst")
|
|
219
|
+
session.get("role_id") # str — present only when a role was applied
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
`role_id` is a custom role's id (`role_…`) or a built-in role's
|
|
223
|
+
(`system:creator`). No `role_id` means the role was not applied — an older
|
|
224
|
+
server ignores `role`, and the session runs as the user's own role.
|
|
225
|
+
|
|
226
|
+
`role` is not `user["role"]` (the org role, `admin`/`member`, used when the
|
|
227
|
+
user is created). **Decide `role` on your server from your own auth — never
|
|
228
|
+
read it from the browser's request.** Granting a role the user does not yet
|
|
229
|
+
hold needs an API key with the `embed:role:assign` scope, and **the grant is
|
|
230
|
+
permanent**: the role is added to the user's assignment, so their later
|
|
231
|
+
sessions and the Querri app see it, and revoking the session does not remove
|
|
232
|
+
it (an admin removes it in Settings → People). Failures raise
|
|
233
|
+
`NotFoundError` (`role_not_found`) or `PermissionError` (`role_not_allowed`,
|
|
234
|
+
`role_disabled`, `insufficient_scope`, `seat_change_required`). The user is
|
|
235
|
+
created/updated and access policies applied before the session is requested,
|
|
236
|
+
so if the role is refused those steps have still happened. On servers
|
|
237
|
+
released after 2026-10-21, creating that user can itself be refused
|
|
238
|
+
(`escalation_denied`, `last_admin` — see below).
|
|
239
|
+
|
|
214
240
|
You can also pass pre-created policy IDs directly:
|
|
215
241
|
|
|
216
242
|
```python
|
|
@@ -244,6 +270,7 @@ See **[docs/server-sdk.md](docs/server-sdk.md#user-scoped-client-as_user)** for
|
|
|
244
270
|
| `client.projects.chats` | Chats within projects | `create`, `list`, `get`, `stream`, `cancel`, `delete` |
|
|
245
271
|
| `client.sources` | Sources, connectors & data | `list`, `create`, `create_data_source`, `query`, `source_data`, `append_rows`, `replace_data`, `ask`, `sync`, `list_connectors` |
|
|
246
272
|
| `client.views` | SQL-defined views | `list`, `create`, `get`, `update`, `delete`, `run`, `get_run`, `wait_for_run`, `preview`, `chat` |
|
|
273
|
+
| `client.library` | Data Library — graph + librarian agent | `create_library`, `create_collection`, `create_anchor_question`, `create_refining_question`, `link`, `status`, `list_nodes`, `get_node`, `search`, `zoom`, `record_fact`, `chat`, `chat_stream`, `backfill`, `seed_fixture`, `health` |
|
|
247
274
|
| `client.files` | File management | `upload`, `list`, `get`, `delete` |
|
|
248
275
|
| `client.keys` | API key management | `create`, `list`, `get`, `delete` |
|
|
249
276
|
| `client.sharing` | Sharing & permissions | `share_project`, `share_dashboard`, `share_source`, `list_project_shares`, `list_dashboard_shares`, `revoke_project_share`, `revoke_dashboard_share`, `org_share_source` |
|
|
@@ -271,7 +298,7 @@ All errors extend `QuerriError`:
|
|
|
271
298
|
```
|
|
272
299
|
QuerriError
|
|
273
300
|
├── APIError — HTTP error responses
|
|
274
|
-
│ ├── ValidationError — 400
|
|
301
|
+
│ ├── ValidationError — 400 / 422
|
|
275
302
|
│ │ └── OriginRequiredError — 400 origin_required (org has an embed-domain allowlist)
|
|
276
303
|
│ ├── AuthenticationError — 401
|
|
277
304
|
│ ├── PermissionError — 403
|
|
@@ -283,10 +310,40 @@ QuerriError
|
|
|
283
310
|
└── ConfigError — missing/invalid configuration
|
|
284
311
|
```
|
|
285
312
|
|
|
313
|
+
Every `APIError` carries `status`, `message`, `type`, `code`, `doc_url`,
|
|
314
|
+
`request_id` and `body` (the parsed JSON response, `None` when it was not
|
|
315
|
+
JSON — it holds the fields the exception does not lift out, e.g.
|
|
316
|
+
`e.body["detail"]["error"]["scopes"]` on `scope_exceeds_role`). A `403
|
|
317
|
+
permission_denied` (the caller's role does not allow the action) also carries
|
|
318
|
+
`permission` and `reason` (`role_denied`, `not_in_plan`, `flag_off`,
|
|
319
|
+
`embed_disabled`, `dependency_unmet`, `forbidden`, `scope_exceeds_role` — the
|
|
320
|
+
server may add more). On servers
|
|
321
|
+
released after 2026-10-21, `client.users` `create` / `update` / `delete` /
|
|
322
|
+
`get_or_create` — and `get_session()` for a new user — can raise
|
|
323
|
+
`PermissionError` (`escalation_denied`, the permissions you lack in
|
|
324
|
+
`e.body["detail"]["error"]["missing"]`) or `ConflictError` (`last_admin`).
|
|
325
|
+
Compare `code` against the
|
|
326
|
+
exported constants (they cover authentication, access, Custom Roles and embed
|
|
327
|
+
sessions; the API sends other codes too, so treat `code` as a string):
|
|
328
|
+
|
|
329
|
+
```python
|
|
330
|
+
from querri import PermissionError, PERMISSION_DENIED # shadows the builtin
|
|
331
|
+
|
|
332
|
+
try:
|
|
333
|
+
client.dashboards.list()
|
|
334
|
+
except PermissionError as e:
|
|
335
|
+
if e.code == PERMISSION_DENIED:
|
|
336
|
+
print(f"needs {e.permission} ({e.reason})")
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
See [docs/server-sdk.md](docs/server-sdk.md#error-codes) for the code table.
|
|
340
|
+
|
|
286
341
|
The SDK automatically retries **429** (always) and **5xx** (idempotent methods only) with exponential backoff + jitter.
|
|
287
342
|
|
|
288
343
|
### Full Reference
|
|
289
344
|
|
|
345
|
+
Custom roles can be managed as code (export, edit, `querri role apply -f role.json --watch`): see **[docs/roles.md](docs/roles.md)**.
|
|
346
|
+
|
|
290
347
|
See **[docs/server-sdk.md](docs/server-sdk.md)** for complete method signatures, all framework examples, and the `get_session()` deep dive.
|
|
291
348
|
|
|
292
349
|
---
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# Data Library — CLI + SDK reference
|
|
2
|
+
|
|
3
|
+
The Data Library is Querri's knowledge-graph + semantic-search layer over Collections, Questions, Sources, Views, KPIs, and Facts. The `querri library` CLI subcommand + `client.library` SDK resource expose the full surface.
|
|
4
|
+
|
|
5
|
+
This doc is the user-facing reference. For module-level architecture see [`~/Q/Querri/server-api/querri_core/library/MODULE.md`](https://github.com/Querri-inc/server-api/blob/feat/data-overview/server-api/querri_core/library/MODULE.md) and the program-level [`documentation/data-library/`](https://github.com/Querri-inc/server-api/tree/feat/data-overview/documentation/data-library).
|
|
6
|
+
|
|
7
|
+
## Auth + setup
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
export QUERRI_API_KEY=qk_...
|
|
11
|
+
export QUERRI_ORG_ID=<tenant-id> # required for API-key auth
|
|
12
|
+
export QUERRI_HOST=http://localhost # dev; production defaults to https://app.querri.com
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Most commands take `--library-id` explicitly. To save typing, set an active library once and subsequent commands pick it up:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
querri library use lib_<tenant-id>_<uuid>
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The active library is stored in `~/.querri/library_context.json`. Override per-call with `--library-id` or `QUERRI_LIBRARY_ID`.
|
|
22
|
+
|
|
23
|
+
## Quick start — the demo path
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
# 1. Create a library and select it.
|
|
27
|
+
querri library create-library "Primary Library"
|
|
28
|
+
querri library use <library-id-printed-above>
|
|
29
|
+
|
|
30
|
+
# 2. Seed it with realistic demo data (5 collections, 16 sources, ~150 series).
|
|
31
|
+
querri library seed-demo --fixture demo-acme
|
|
32
|
+
|
|
33
|
+
# 3. Inspect.
|
|
34
|
+
querri library status
|
|
35
|
+
querri library list Collection
|
|
36
|
+
querri library list ViewStub
|
|
37
|
+
|
|
38
|
+
# 4. Search semantically.
|
|
39
|
+
querri library search "customer churn signals"
|
|
40
|
+
querri library zoom "marketing attribution by channel" --zoom 50 --explain
|
|
41
|
+
|
|
42
|
+
# 5. Chat with the Librarian agent.
|
|
43
|
+
querri library chat "What collections relate to revenue?" --new --show-tools
|
|
44
|
+
|
|
45
|
+
# 6. Record a Fact (CLI-direct OR via the agent).
|
|
46
|
+
querri library record-fact \
|
|
47
|
+
"orders.status enum gained 'awaiting_carrier' in Mar 2026" \
|
|
48
|
+
--attach <source-node-id> --kind data_quality
|
|
49
|
+
|
|
50
|
+
# 7. Have the agent commission a new view.
|
|
51
|
+
querri library chat \
|
|
52
|
+
"Commission a view called orders_by_country that sums total_amount grouped by shipping_country, using the orders source. Build it." \
|
|
53
|
+
--show-tools
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## CLI reference
|
|
57
|
+
|
|
58
|
+
All commands accept `--json` (a top-level flag, e.g. `querri --json library status`) for scripting.
|
|
59
|
+
|
|
60
|
+
### Workspace state
|
|
61
|
+
|
|
62
|
+
| Command | What it does |
|
|
63
|
+
|---|---|
|
|
64
|
+
| `querri library use <library-id>` | Set active library (persists across invocations). |
|
|
65
|
+
| `querri library status [--library-id <id>]` | Counts per node_kind + tenant + total. |
|
|
66
|
+
| `querri library health` | Tenant-scoped health probe (Mongo doc count, embedding model). |
|
|
67
|
+
|
|
68
|
+
### Create
|
|
69
|
+
|
|
70
|
+
| Command | What it does |
|
|
71
|
+
|---|---|
|
|
72
|
+
| `querri library create-library <name> [--summary <s>]` | New `Library` node. |
|
|
73
|
+
| `querri library create-collection <name> [--anchor "<text>"] [--summary <s>]` | New `Collection`. With `--anchor` also mints an `AnchorQuestion` + the `anchor_of` edge in one call. |
|
|
74
|
+
| `querri library create-question <text> [--refining] [--name <n>]` | Anchor question by default; pass `--refining` for a `RefiningQuestion`. |
|
|
75
|
+
| `querri library add-refining <anchor-id> "<text>"` | New `RefiningQuestion` linked to the anchor via `refines`. |
|
|
76
|
+
| `querri library link <a> <b> <relation> [--weight X] [--confidence X]` | Bidirectional edge between two existing nodes. `<relation>` is one of `contains`, `anchor_of`, `refines`, `parent_of`, `in_category`, `uses_source`, `uses_view`, `connector_provides`, `view_built_from`, `series_in`, `about`, `semantic_neighbor`, `mentions`. |
|
|
77
|
+
|
|
78
|
+
### Read
|
|
79
|
+
|
|
80
|
+
| Command | What it does |
|
|
81
|
+
|---|---|
|
|
82
|
+
| `querri library get <node-id>` | Fetch a single node by id. |
|
|
83
|
+
| `querri library list <node-kind> [--limit N]` | All nodes of a given kind in the active library. |
|
|
84
|
+
| `querri library search <query> [--kind K] [--limit N] [--surface S]` | Vector ANN search — flat top-K, no graph traversal. Fast surface. |
|
|
85
|
+
| `querri library zoom <query> [--zoom N] [--budget N] [--top-k N] [--threshold X] [--kind K] [--explain]` | Vector-seeded multi-focal graph zoom: ANN-resolves focals, BFS up to 2 hops, scores by `edge.confidence × edge.weight × (zoom/100)^hops`. `--explain` prints per-stage latency + algorithm decisions. |
|
|
86
|
+
| `querri library zoom --focal <id> [--focal <id>] [--zoom N]` | Skip ANN; zoom directly from given focal IDs (for iterative drill-in). |
|
|
87
|
+
|
|
88
|
+
### Write — facts + view commissioning
|
|
89
|
+
|
|
90
|
+
| Command | What it does |
|
|
91
|
+
|---|---|
|
|
92
|
+
| `querri library record-fact "<statement>" --attach <node-id> [--attach <id>] [--kind <K>] [--evidence <url>] [--confidence X]` | Create a `Fact` node + `about` edges to each `--attach` target. `--kind` is one of `contextual_note` (default), `data_quality`, `timing`, `scope_constraint`. |
|
|
93
|
+
| `querri library chat "<message>" [--new] [--chat-id <id>] [--show-tools]` | Streaming chat with the LibrarianAgent. Three tools available to the agent: `search_graph`, `record_fact`, `commission_view`. `--show-tools` prints tool calls + Views-agent progress dots in real time. Active chat id persists per-library so subsequent calls continue the same conversation; `--new` starts fresh. |
|
|
94
|
+
|
|
95
|
+
### Bulk / admin
|
|
96
|
+
|
|
97
|
+
| Command | What it does |
|
|
98
|
+
|---|---|
|
|
99
|
+
| `querri library seed-demo [--fixture <name>]` | Apply a named demo fixture (currently: `demo-acme`). Writes both library_graph stubs AND real legacy `sources` + `connectors` docs so the Views agent can resolve fixture sources. Idempotent. |
|
|
100
|
+
| `querri library backfill [--library-id <id>] [--no-series]` | Walk the tenant's existing `connectors` + `sources` collections and create matching stubs in `library_graph`. Idempotent. |
|
|
101
|
+
|
|
102
|
+
## SDK reference
|
|
103
|
+
|
|
104
|
+
Sync (`client.library`) and async (`client.library` on `AsyncQuerri`) have identical surfaces.
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
from querri import Querri
|
|
108
|
+
|
|
109
|
+
client = Querri(api_key="qk_...", org_id="<tenant>", host="http://localhost")
|
|
110
|
+
|
|
111
|
+
# Workspace
|
|
112
|
+
status = client.library.status(library_id="lib_...")
|
|
113
|
+
print(status.counts_by_kind) # {"Collection": 8, "Fact": 4, ...}
|
|
114
|
+
|
|
115
|
+
# Search
|
|
116
|
+
hits = client.library.search(query="churn risk", library_id="lib_...", limit=5)
|
|
117
|
+
for h in hits.results:
|
|
118
|
+
print(h.score, h.node_kind, h.name)
|
|
119
|
+
|
|
120
|
+
# Vector-seeded graph zoom
|
|
121
|
+
zoom = client.library.zoom(
|
|
122
|
+
library_id="lib_...",
|
|
123
|
+
query="channel attribution and ROI",
|
|
124
|
+
zoom=50, budget_tokens=2000, top_k_focal=4,
|
|
125
|
+
)
|
|
126
|
+
for f in zoom.focal_nodes:
|
|
127
|
+
print(f.score, f.confidence_tier, f.node_kind, f.name)
|
|
128
|
+
for n in zoom.subgraph_nodes:
|
|
129
|
+
print(n.edge_strength, n.hops, "/".join(n.edge_path), n.name)
|
|
130
|
+
|
|
131
|
+
# Record a fact via the API directly
|
|
132
|
+
fact = client.library.record_fact(
|
|
133
|
+
library_id="lib_...",
|
|
134
|
+
statement="orders.status enum gained 'awaiting_carrier' in Mar 2026",
|
|
135
|
+
fact_kind="data_quality",
|
|
136
|
+
source_node_ids=["src_..."],
|
|
137
|
+
evidence_refs=["https://runbooks/orders-status-evolution"],
|
|
138
|
+
confidence=0.95,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
# Chat — drained ChatResponse
|
|
142
|
+
result = client.library.chat(
|
|
143
|
+
library_id="lib_...",
|
|
144
|
+
message="What collections relate to revenue?",
|
|
145
|
+
chat_id=None, # omit to start fresh
|
|
146
|
+
)
|
|
147
|
+
print(result.assistant_message)
|
|
148
|
+
print(result.tool_calls) # list of {name, input, result, duration_ms}
|
|
149
|
+
|
|
150
|
+
# Chat — streaming events (sync iterator)
|
|
151
|
+
for event in client.library.chat_stream(
|
|
152
|
+
library_id="lib_...",
|
|
153
|
+
message="Commission a payment_failures view from the payments source",
|
|
154
|
+
):
|
|
155
|
+
print(event["type"], event.get("name") or event.get("text", "")[:80])
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Streaming event types
|
|
159
|
+
|
|
160
|
+
The chat stream yields these `type`s (in order, typically):
|
|
161
|
+
|
|
162
|
+
| Type | Payload |
|
|
163
|
+
|---|---|
|
|
164
|
+
| `tool_use` | `{tool_use_id, name, input}` — emitted right before each tool call |
|
|
165
|
+
| `tool_progress` | `{tool_use_id, raw}` — pass-through inner-agent SSE chunks during `commission_view` |
|
|
166
|
+
| `tool_result` | `{tool_use_id, name, input, result, duration_ms}` |
|
|
167
|
+
| `assistant_text` | `{text}` — terminal LLM text in the final turn |
|
|
168
|
+
| `done` | `{chat_id, library_id, assistant_message, turns_used, stop_reason, input_tokens, output_tokens, total_ms}` |
|
|
169
|
+
| `error` | `{message}` — stream-level failure |
|
|
170
|
+
|
|
171
|
+
## Scopes
|
|
172
|
+
|
|
173
|
+
Library routes require API-key scopes:
|
|
174
|
+
|
|
175
|
+
| Scope | Routes |
|
|
176
|
+
|---|---|
|
|
177
|
+
| `admin:library:read` | GET `/library/nodes`, `/library/nodes/{id}`, `/library/status`, `/library/health`, POST `/library/search`, `/library/zoom` |
|
|
178
|
+
| `admin:library:write` | POST `/library/libraries`, `/library/collections`, `/library/questions/*`, `/library/edges`, `/library/facts`, `/library/backfill`, `/library/seed-fixture`, `/library/chat` |
|
|
179
|
+
|
|
180
|
+
The wildcard `*` superadmin scope satisfies both. With JWT auth (the CLI after
|
|
181
|
+
`querri auth login`) the scopes follow your active role (Custom Roles), so
|
|
182
|
+
whether you hold the write scope depends on that role; a Guest role is
|
|
183
|
+
read-only. An API key keeps only the scopes both its creator and its bound
|
|
184
|
+
user currently hold (servers released after 2026-10-21; earlier servers cap
|
|
185
|
+
only keys created in Settings).
|