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.
Files changed (132) hide show
  1. querri-2.2.0/CHANGELOG.md +238 -0
  2. {querri-2.0.0 → querri-2.2.0}/CLAUDE.md +4 -4
  3. {querri-2.0.0 → querri-2.2.0}/PKG-INFO +59 -2
  4. {querri-2.0.0 → querri-2.2.0}/README.md +58 -1
  5. querri-2.2.0/docs/data-library.md +185 -0
  6. querri-2.2.0/docs/roles.md +151 -0
  7. {querri-2.0.0 → querri-2.2.0}/docs/server-sdk.md +212 -5
  8. {querri-2.0.0 → querri-2.2.0}/examples/05_get_session.py +29 -1
  9. {querri-2.0.0 → querri-2.2.0}/examples/10_error_handling.py +14 -0
  10. {querri-2.0.0 → querri-2.2.0}/examples/web-embed/server.py +46 -3
  11. querri-2.2.0/querri/__init__.py +110 -0
  12. {querri-2.0.0 → querri-2.2.0}/querri/_auth.py +12 -1
  13. {querri-2.0.0 → querri-2.2.0}/querri/_base_client.py +42 -4
  14. {querri-2.0.0 → querri-2.2.0}/querri/_client.py +48 -0
  15. {querri-2.0.0 → querri-2.2.0}/querri/_config.py +11 -0
  16. {querri-2.0.0 → querri-2.2.0}/querri/_convenience.py +25 -1
  17. querri-2.2.0/querri/_exceptions.py +440 -0
  18. {querri-2.0.0 → querri-2.2.0}/querri/_user_client.py +6 -2
  19. querri-2.2.0/querri/_version.py +1 -0
  20. {querri-2.0.0 → querri-2.2.0}/querri/cli/_app.py +12 -0
  21. {querri-2.0.0 → querri-2.2.0}/querri/cli/_context.py +20 -6
  22. {querri-2.0.0 → querri-2.2.0}/querri/cli/_output.py +63 -4
  23. {querri-2.0.0 → querri-2.2.0}/querri/cli/auth.py +6 -1
  24. {querri-2.0.0 → querri-2.2.0}/querri/cli/embed.py +21 -3
  25. {querri-2.0.0 → querri-2.2.0}/querri/cli/keys.py +2 -1
  26. querri-2.2.0/querri/cli/library.py +1763 -0
  27. querri-2.2.0/querri/cli/roles.py +411 -0
  28. {querri-2.0.0 → querri-2.2.0}/querri/cli/sources.py +1 -0
  29. {querri-2.0.0 → querri-2.2.0}/querri/cli/views.py +2 -1
  30. {querri-2.0.0 → querri-2.2.0}/querri/resources/embed.py +66 -6
  31. querri-2.2.0/querri/resources/library.py +1180 -0
  32. querri-2.2.0/querri/resources/roles.py +324 -0
  33. {querri-2.0.0 → querri-2.2.0}/querri/resources/users.py +16 -0
  34. {querri-2.0.0 → querri-2.2.0}/querri/types/__init__.py +19 -0
  35. {querri-2.0.0 → querri-2.2.0}/querri/types/embed.py +2 -0
  36. querri-2.2.0/querri/types/library.py +355 -0
  37. querri-2.2.0/querri/types/role.py +136 -0
  38. {querri-2.0.0 → querri-2.2.0}/skills/querri-cli/SKILL.md +55 -5
  39. {querri-2.0.0 → querri-2.2.0}/skills/querri-cli/references/json-shapes.md +71 -8
  40. {querri-2.0.0 → querri-2.2.0}/tests/test_cli.py +60 -3
  41. {querri-2.0.0 → querri-2.2.0}/tests/test_cli_commands.py +222 -0
  42. querri-2.2.0/tests/test_cli_json_errors.py +402 -0
  43. querri-2.2.0/tests/test_client_header.py +62 -0
  44. {querri-2.0.0 → querri-2.2.0}/tests/test_convenience.py +149 -0
  45. querri-2.2.0/tests/test_exceptions.py +977 -0
  46. querri-2.2.0/tests/test_library.py +842 -0
  47. querri-2.2.0/tests/test_library_streaming.py +325 -0
  48. {querri-2.0.0 → querri-2.2.0}/tests/test_resources.py +199 -1
  49. querri-2.2.0/tests/test_roles.py +529 -0
  50. {querri-2.0.0 → querri-2.2.0}/tests/test_security.py +49 -0
  51. querri-2.0.0/CHANGELOG.md +0 -89
  52. querri-2.0.0/querri/__init__.py +0 -53
  53. querri-2.0.0/querri/_exceptions.py +0 -201
  54. querri-2.0.0/querri/_version.py +0 -1
  55. querri-2.0.0/tests/test_exceptions.py +0 -162
  56. {querri-2.0.0 → querri-2.2.0}/.github/CONTRIBUTING.md +0 -0
  57. {querri-2.0.0 → querri-2.2.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  58. {querri-2.0.0 → querri-2.2.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  59. {querri-2.0.0 → querri-2.2.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  60. {querri-2.0.0 → querri-2.2.0}/.github/SECURITY.md +0 -0
  61. {querri-2.0.0 → querri-2.2.0}/.github/workflows/ci.yml +0 -0
  62. {querri-2.0.0 → querri-2.2.0}/.github/workflows/release.yml +0 -0
  63. {querri-2.0.0 → querri-2.2.0}/.gitignore +0 -0
  64. {querri-2.0.0 → querri-2.2.0}/LICENSE +0 -0
  65. {querri-2.0.0 → querri-2.2.0}/TODO.md +0 -0
  66. {querri-2.0.0 → querri-2.2.0}/docs/MIGRATION.md +0 -0
  67. {querri-2.0.0 → querri-2.2.0}/examples/01_quick_start.py +0 -0
  68. {querri-2.0.0 → querri-2.2.0}/examples/02_user_management.py +0 -0
  69. {querri-2.0.0 → querri-2.2.0}/examples/03_access_policies.py +0 -0
  70. {querri-2.0.0 → querri-2.2.0}/examples/04_embed_session.py +0 -0
  71. {querri-2.0.0 → querri-2.2.0}/examples/06_projects.py +0 -0
  72. {querri-2.0.0 → querri-2.2.0}/examples/07_dashboards.py +0 -0
  73. {querri-2.0.0 → querri-2.2.0}/examples/08_chat_streaming.py +0 -0
  74. {querri-2.0.0 → querri-2.2.0}/examples/09_async_client.py +0 -0
  75. {querri-2.0.0 → querri-2.2.0}/examples/11_user_scoped_client.py +0 -0
  76. {querri-2.0.0 → querri-2.2.0}/examples/12_data_operations.py +0 -0
  77. {querri-2.0.0 → querri-2.2.0}/examples/web-embed/index.html +0 -0
  78. {querri-2.0.0 → querri-2.2.0}/pyproject.toml +0 -0
  79. {querri-2.0.0 → querri-2.2.0}/querri/_pagination.py +0 -0
  80. {querri-2.0.0 → querri-2.2.0}/querri/_streaming.py +0 -0
  81. {querri-2.0.0 → querri-2.2.0}/querri/cli/__init__.py +0 -0
  82. {querri-2.0.0 → querri-2.2.0}/querri/cli/_image.py +0 -0
  83. {querri-2.0.0 → querri-2.2.0}/querri/cli/audit.py +0 -0
  84. {querri-2.0.0 → querri-2.2.0}/querri/cli/chat.py +0 -0
  85. {querri-2.0.0 → querri-2.2.0}/querri/cli/chats.py +0 -0
  86. {querri-2.0.0 → querri-2.2.0}/querri/cli/dashboards.py +0 -0
  87. {querri-2.0.0 → querri-2.2.0}/querri/cli/files.py +0 -0
  88. {querri-2.0.0 → querri-2.2.0}/querri/cli/policies.py +0 -0
  89. {querri-2.0.0 → querri-2.2.0}/querri/cli/projects.py +0 -0
  90. {querri-2.0.0 → querri-2.2.0}/querri/cli/sharing.py +0 -0
  91. {querri-2.0.0 → querri-2.2.0}/querri/cli/steps.py +0 -0
  92. {querri-2.0.0 → querri-2.2.0}/querri/cli/usage.py +0 -0
  93. {querri-2.0.0 → querri-2.2.0}/querri/cli/users.py +0 -0
  94. {querri-2.0.0 → querri-2.2.0}/querri/cli/whoami.py +0 -0
  95. {querri-2.0.0 → querri-2.2.0}/querri/py.typed +0 -0
  96. {querri-2.0.0 → querri-2.2.0}/querri/resources/__init__.py +0 -0
  97. {querri-2.0.0 → querri-2.2.0}/querri/resources/audit.py +0 -0
  98. {querri-2.0.0 → querri-2.2.0}/querri/resources/dashboards.py +0 -0
  99. {querri-2.0.0 → querri-2.2.0}/querri/resources/files.py +0 -0
  100. {querri-2.0.0 → querri-2.2.0}/querri/resources/keys.py +0 -0
  101. {querri-2.0.0 → querri-2.2.0}/querri/resources/policies.py +0 -0
  102. {querri-2.0.0 → querri-2.2.0}/querri/resources/projects.py +0 -0
  103. {querri-2.0.0 → querri-2.2.0}/querri/resources/sharing.py +0 -0
  104. {querri-2.0.0 → querri-2.2.0}/querri/resources/sources.py +0 -0
  105. {querri-2.0.0 → querri-2.2.0}/querri/resources/usage.py +0 -0
  106. {querri-2.0.0 → querri-2.2.0}/querri/resources/views.py +0 -0
  107. {querri-2.0.0 → querri-2.2.0}/querri/types/audit.py +0 -0
  108. {querri-2.0.0 → querri-2.2.0}/querri/types/chat.py +0 -0
  109. {querri-2.0.0 → querri-2.2.0}/querri/types/dashboard.py +0 -0
  110. {querri-2.0.0 → querri-2.2.0}/querri/types/data.py +0 -0
  111. {querri-2.0.0 → querri-2.2.0}/querri/types/file.py +0 -0
  112. {querri-2.0.0 → querri-2.2.0}/querri/types/key.py +0 -0
  113. {querri-2.0.0 → querri-2.2.0}/querri/types/policy.py +0 -0
  114. {querri-2.0.0 → querri-2.2.0}/querri/types/project.py +0 -0
  115. {querri-2.0.0 → querri-2.2.0}/querri/types/sharing.py +0 -0
  116. {querri-2.0.0 → querri-2.2.0}/querri/types/usage.py +0 -0
  117. {querri-2.0.0 → querri-2.2.0}/querri/types/user.py +0 -0
  118. {querri-2.0.0 → querri-2.2.0}/scripts/release.sh +0 -0
  119. {querri-2.0.0 → querri-2.2.0}/skills/README.md +0 -0
  120. {querri-2.0.0 → querri-2.2.0}/skills/querri-cli/references/troubleshooting.md +0 -0
  121. {querri-2.0.0 → querri-2.2.0}/tests/__init__.py +0 -0
  122. {querri-2.0.0 → querri-2.2.0}/tests/conftest.py +0 -0
  123. {querri-2.0.0 → querri-2.2.0}/tests/rls-integration-results.md +0 -0
  124. {querri-2.0.0 → querri-2.2.0}/tests/test_client.py +0 -0
  125. {querri-2.0.0 → querri-2.2.0}/tests/test_config.py +0 -0
  126. {querri-2.0.0 → querri-2.2.0}/tests/test_integration.py +0 -0
  127. {querri-2.0.0 → querri-2.2.0}/tests/test_new_features.py +0 -0
  128. {querri-2.0.0 → querri-2.2.0}/tests/test_pagination.py +0 -0
  129. {querri-2.0.0 → querri-2.2.0}/tests/test_rls_integration.py +0 -0
  130. {querri-2.0.0 → querri-2.2.0}/tests/test_streaming.py +0 -0
  131. {querri-2.0.0 → querri-2.2.0}/tests/test_streaming_v2.py +0 -0
  132. {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.9.
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 internal API (`/api` instead of `/api/v1`). Only expose FGA-filtered resources: `projects`, `dashboards`, `sources`, `data`, `chats`.
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.9
92
- - **ruff**: Rules E, F, I, N, UP, B, SIM (target Python 3.9)
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.0.0
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).