knowledge2 0.7.0__tar.gz → 0.8.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 (154) hide show
  1. {knowledge2-0.7.0 → knowledge2-0.8.0}/CHANGELOG.md +148 -0
  2. {knowledge2-0.7.0/knowledge2.egg-info → knowledge2-0.8.0}/PKG-INFO +171 -4
  3. {knowledge2-0.7.0 → knowledge2-0.8.0}/README.md +170 -3
  4. {knowledge2-0.7.0 → knowledge2-0.8.0}/__init__.py +3 -0
  5. knowledge2-0.8.0/_embedding_limits.py +13 -0
  6. knowledge2-0.8.0/_sentinels.py +33 -0
  7. {knowledge2-0.7.0 → knowledge2-0.8.0}/_version.py +1 -1
  8. {knowledge2-0.7.0 → knowledge2-0.8.0}/async_client.py +6 -0
  9. {knowledge2-0.7.0 → knowledge2-0.8.0}/async_resources/__init__.py +6 -0
  10. {knowledge2-0.7.0 → knowledge2-0.8.0}/async_resources/agents.py +191 -28
  11. {knowledge2-0.7.0 → knowledge2-0.8.0}/async_resources/corpora.py +61 -1
  12. knowledge2-0.8.0/async_resources/destinations.py +249 -0
  13. {knowledge2-0.7.0 → knowledge2-0.8.0}/async_resources/documents.py +8 -0
  14. knowledge2-0.8.0/async_resources/extraction_templates.py +196 -0
  15. {knowledge2-0.7.0 → knowledge2-0.8.0}/async_resources/feeds.py +101 -2
  16. {knowledge2-0.7.0 → knowledge2-0.8.0}/async_resources/indexes.py +29 -12
  17. {knowledge2-0.7.0 → knowledge2-0.8.0}/async_resources/pipelines.py +181 -4
  18. {knowledge2-0.7.0 → knowledge2-0.8.0}/async_resources/projects.py +29 -0
  19. knowledge2-0.8.0/async_resources/quality.py +216 -0
  20. {knowledge2-0.7.0 → knowledge2-0.8.0}/async_resources/search.py +30 -2
  21. {knowledge2-0.7.0 → knowledge2-0.8.0}/client.py +6 -0
  22. {knowledge2-0.7.0 → knowledge2-0.8.0/knowledge2.egg-info}/PKG-INFO +171 -4
  23. {knowledge2-0.7.0 → knowledge2-0.8.0}/knowledge2.egg-info/SOURCES.txt +28 -0
  24. {knowledge2-0.7.0 → knowledge2-0.8.0}/models/_registry.py +57 -0
  25. knowledge2-0.8.0/models/agents.py +376 -0
  26. {knowledge2-0.7.0 → knowledge2-0.8.0}/models/console.py +3 -0
  27. {knowledge2-0.7.0 → knowledge2-0.8.0}/models/corpora.py +3 -0
  28. knowledge2-0.8.0/models/destinations.py +31 -0
  29. {knowledge2-0.7.0 → knowledge2-0.8.0}/models/embeddings.py +1 -0
  30. knowledge2-0.8.0/models/extraction_templates.py +35 -0
  31. {knowledge2-0.7.0 → knowledge2-0.8.0}/models/feeds.py +30 -0
  32. {knowledge2-0.7.0 → knowledge2-0.8.0}/models/pipelines.py +83 -0
  33. {knowledge2-0.7.0 → knowledge2-0.8.0}/models/projects.py +1 -0
  34. knowledge2-0.8.0/models/quality.py +70 -0
  35. {knowledge2-0.7.0 → knowledge2-0.8.0}/namespaces.py +43 -6
  36. {knowledge2-0.7.0 → knowledge2-0.8.0}/pyproject.toml +1 -1
  37. {knowledge2-0.7.0 → knowledge2-0.8.0}/resources/__init__.py +6 -0
  38. {knowledge2-0.7.0 → knowledge2-0.8.0}/resources/agents.py +243 -28
  39. {knowledge2-0.7.0 → knowledge2-0.8.0}/resources/corpora.py +61 -1
  40. knowledge2-0.8.0/resources/destinations.py +269 -0
  41. {knowledge2-0.7.0 → knowledge2-0.8.0}/resources/documents.py +8 -0
  42. knowledge2-0.8.0/resources/extraction_templates.py +194 -0
  43. {knowledge2-0.7.0 → knowledge2-0.8.0}/resources/feeds.py +112 -2
  44. {knowledge2-0.7.0 → knowledge2-0.8.0}/resources/indexes.py +33 -12
  45. {knowledge2-0.7.0 → knowledge2-0.8.0}/resources/pipeline_builder.py +2 -1
  46. {knowledge2-0.7.0 → knowledge2-0.8.0}/resources/pipelines.py +243 -2
  47. {knowledge2-0.7.0 → knowledge2-0.8.0}/resources/projects.py +45 -0
  48. knowledge2-0.8.0/resources/quality.py +216 -0
  49. {knowledge2-0.7.0 → knowledge2-0.8.0}/resources/search.py +30 -2
  50. {knowledge2-0.7.0 → knowledge2-0.8.0}/types/__init__.py +57 -0
  51. knowledge2-0.8.0/types/agents.py +395 -0
  52. {knowledge2-0.7.0 → knowledge2-0.8.0}/types/console.py +3 -0
  53. {knowledge2-0.7.0 → knowledge2-0.8.0}/types/corpora.py +3 -0
  54. knowledge2-0.8.0/types/destinations.py +31 -0
  55. {knowledge2-0.7.0 → knowledge2-0.8.0}/types/embeddings.py +1 -0
  56. knowledge2-0.8.0/types/extraction_templates.py +39 -0
  57. {knowledge2-0.7.0 → knowledge2-0.8.0}/types/feeds.py +42 -0
  58. {knowledge2-0.7.0 → knowledge2-0.8.0}/types/pipelines.py +100 -0
  59. {knowledge2-0.7.0 → knowledge2-0.8.0}/types/projects.py +1 -0
  60. knowledge2-0.8.0/types/quality.py +80 -0
  61. knowledge2-0.7.0/models/agents.py +0 -147
  62. knowledge2-0.7.0/types/agents.py +0 -189
  63. {knowledge2-0.7.0 → knowledge2-0.8.0}/.github/workflows/pypi-release.yml +0 -0
  64. {knowledge2-0.7.0 → knowledge2-0.8.0}/MANIFEST.in +0 -0
  65. {knowledge2-0.7.0 → knowledge2-0.8.0}/_async_base.py +0 -0
  66. {knowledge2-0.7.0 → knowledge2-0.8.0}/_async_paging.py +0 -0
  67. {knowledge2-0.7.0 → knowledge2-0.8.0}/_base.py +0 -0
  68. {knowledge2-0.7.0 → knowledge2-0.8.0}/_logging.py +0 -0
  69. {knowledge2-0.7.0 → knowledge2-0.8.0}/_paging.py +0 -0
  70. {knowledge2-0.7.0 → knowledge2-0.8.0}/_preview.py +0 -0
  71. {knowledge2-0.7.0 → knowledge2-0.8.0}/_raw_response.py +0 -0
  72. {knowledge2-0.7.0 → knowledge2-0.8.0}/_request_options.py +0 -0
  73. {knowledge2-0.7.0 → knowledge2-0.8.0}/_transport.py +0 -0
  74. {knowledge2-0.7.0 → knowledge2-0.8.0}/_validation.py +0 -0
  75. {knowledge2-0.7.0 → knowledge2-0.8.0}/_validation_response.py +0 -0
  76. {knowledge2-0.7.0 → knowledge2-0.8.0}/async_resources/_mixin_base.py +0 -0
  77. {knowledge2-0.7.0 → knowledge2-0.8.0}/async_resources/a2a.py +0 -0
  78. {knowledge2-0.7.0 → knowledge2-0.8.0}/async_resources/audit.py +0 -0
  79. {knowledge2-0.7.0 → knowledge2-0.8.0}/async_resources/auth.py +0 -0
  80. {knowledge2-0.7.0 → knowledge2-0.8.0}/async_resources/console.py +0 -0
  81. {knowledge2-0.7.0 → knowledge2-0.8.0}/async_resources/generation_models.py +0 -0
  82. {knowledge2-0.7.0 → knowledge2-0.8.0}/async_resources/jobs.py +0 -0
  83. {knowledge2-0.7.0 → knowledge2-0.8.0}/async_resources/metadata.py +0 -0
  84. {knowledge2-0.7.0 → knowledge2-0.8.0}/async_resources/onboarding.py +0 -0
  85. {knowledge2-0.7.0 → knowledge2-0.8.0}/async_resources/orgs.py +0 -0
  86. {knowledge2-0.7.0 → knowledge2-0.8.0}/async_resources/usage.py +0 -0
  87. {knowledge2-0.7.0 → knowledge2-0.8.0}/config.py +0 -0
  88. {knowledge2-0.7.0 → knowledge2-0.8.0}/errors.py +0 -0
  89. {knowledge2-0.7.0 → knowledge2-0.8.0}/examples/auth_factory.py +0 -0
  90. {knowledge2-0.7.0 → knowledge2-0.8.0}/examples/batch_operations.py +0 -0
  91. {knowledge2-0.7.0 → knowledge2-0.8.0}/examples/document_upload.py +0 -0
  92. {knowledge2-0.7.0 → knowledge2-0.8.0}/examples/e2e_lifecycle.py +0 -0
  93. {knowledge2-0.7.0 → knowledge2-0.8.0}/examples/error_handling.py +0 -0
  94. {knowledge2-0.7.0 → knowledge2-0.8.0}/examples/pagination.py +0 -0
  95. {knowledge2-0.7.0 → knowledge2-0.8.0}/examples/quickstart.py +0 -0
  96. {knowledge2-0.7.0 → knowledge2-0.8.0}/examples/request_options.py +0 -0
  97. {knowledge2-0.7.0 → knowledge2-0.8.0}/examples/retrieval_quickstart.py +0 -0
  98. {knowledge2-0.7.0 → knowledge2-0.8.0}/examples/search.py +0 -0
  99. {knowledge2-0.7.0 → knowledge2-0.8.0}/integrations/__init__.py +0 -0
  100. {knowledge2-0.7.0 → knowledge2-0.8.0}/integrations/_client.py +0 -0
  101. {knowledge2-0.7.0 → knowledge2-0.8.0}/integrations/langchain/__init__.py +0 -0
  102. {knowledge2-0.7.0 → knowledge2-0.8.0}/integrations/langchain/retriever.py +0 -0
  103. {knowledge2-0.7.0 → knowledge2-0.8.0}/integrations/langchain/tools.py +0 -0
  104. {knowledge2-0.7.0 → knowledge2-0.8.0}/integrations/llamaindex/__init__.py +0 -0
  105. {knowledge2-0.7.0 → knowledge2-0.8.0}/integrations/llamaindex/filters.py +0 -0
  106. {knowledge2-0.7.0 → knowledge2-0.8.0}/integrations/llamaindex/retriever.py +0 -0
  107. {knowledge2-0.7.0 → knowledge2-0.8.0}/integrations/llamaindex/tools.py +0 -0
  108. {knowledge2-0.7.0 → knowledge2-0.8.0}/integrations/llamaindex/vector_store.py +0 -0
  109. {knowledge2-0.7.0 → knowledge2-0.8.0}/knowledge2.egg-info/dependency_links.txt +0 -0
  110. {knowledge2-0.7.0 → knowledge2-0.8.0}/knowledge2.egg-info/requires.txt +0 -0
  111. {knowledge2-0.7.0 → knowledge2-0.8.0}/knowledge2.egg-info/top_level.txt +0 -0
  112. {knowledge2-0.7.0 → knowledge2-0.8.0}/models/__init__.py +0 -0
  113. {knowledge2-0.7.0 → knowledge2-0.8.0}/models/_base.py +0 -0
  114. {knowledge2-0.7.0 → knowledge2-0.8.0}/models/a2a.py +0 -0
  115. {knowledge2-0.7.0 → knowledge2-0.8.0}/models/audit.py +0 -0
  116. {knowledge2-0.7.0 → knowledge2-0.8.0}/models/auth.py +0 -0
  117. {knowledge2-0.7.0 → knowledge2-0.8.0}/models/chunks.py +0 -0
  118. {knowledge2-0.7.0 → knowledge2-0.8.0}/models/common.py +0 -0
  119. {knowledge2-0.7.0 → knowledge2-0.8.0}/models/documents.py +0 -0
  120. {knowledge2-0.7.0 → knowledge2-0.8.0}/models/feedback.py +0 -0
  121. {knowledge2-0.7.0 → knowledge2-0.8.0}/models/generation_models.py +0 -0
  122. {knowledge2-0.7.0 → knowledge2-0.8.0}/models/indexes.py +0 -0
  123. {knowledge2-0.7.0 → knowledge2-0.8.0}/models/jobs.py +0 -0
  124. {knowledge2-0.7.0 → knowledge2-0.8.0}/models/onboarding.py +0 -0
  125. {knowledge2-0.7.0 → knowledge2-0.8.0}/models/orgs.py +0 -0
  126. {knowledge2-0.7.0 → knowledge2-0.8.0}/models/search.py +0 -0
  127. {knowledge2-0.7.0 → knowledge2-0.8.0}/models/usage.py +0 -0
  128. {knowledge2-0.7.0 → knowledge2-0.8.0}/py.typed +0 -0
  129. {knowledge2-0.7.0 → knowledge2-0.8.0}/resources/_mixin_base.py +0 -0
  130. {knowledge2-0.7.0 → knowledge2-0.8.0}/resources/a2a.py +0 -0
  131. {knowledge2-0.7.0 → knowledge2-0.8.0}/resources/audit.py +0 -0
  132. {knowledge2-0.7.0 → knowledge2-0.8.0}/resources/auth.py +0 -0
  133. {knowledge2-0.7.0 → knowledge2-0.8.0}/resources/console.py +0 -0
  134. {knowledge2-0.7.0 → knowledge2-0.8.0}/resources/generation_models.py +0 -0
  135. {knowledge2-0.7.0 → knowledge2-0.8.0}/resources/jobs.py +0 -0
  136. {knowledge2-0.7.0 → knowledge2-0.8.0}/resources/metadata.py +0 -0
  137. {knowledge2-0.7.0 → knowledge2-0.8.0}/resources/onboarding.py +0 -0
  138. {knowledge2-0.7.0 → knowledge2-0.8.0}/resources/orgs.py +0 -0
  139. {knowledge2-0.7.0 → knowledge2-0.8.0}/resources/usage.py +0 -0
  140. {knowledge2-0.7.0 → knowledge2-0.8.0}/setup.cfg +0 -0
  141. {knowledge2-0.7.0 → knowledge2-0.8.0}/types/a2a.py +0 -0
  142. {knowledge2-0.7.0 → knowledge2-0.8.0}/types/audit.py +0 -0
  143. {knowledge2-0.7.0 → knowledge2-0.8.0}/types/auth.py +0 -0
  144. {knowledge2-0.7.0 → knowledge2-0.8.0}/types/chunks.py +0 -0
  145. {knowledge2-0.7.0 → knowledge2-0.8.0}/types/common.py +0 -0
  146. {knowledge2-0.7.0 → knowledge2-0.8.0}/types/documents.py +0 -0
  147. {knowledge2-0.7.0 → knowledge2-0.8.0}/types/feedback.py +0 -0
  148. {knowledge2-0.7.0 → knowledge2-0.8.0}/types/generation_models.py +0 -0
  149. {knowledge2-0.7.0 → knowledge2-0.8.0}/types/indexes.py +0 -0
  150. {knowledge2-0.7.0 → knowledge2-0.8.0}/types/jobs.py +0 -0
  151. {knowledge2-0.7.0 → knowledge2-0.8.0}/types/onboarding.py +0 -0
  152. {knowledge2-0.7.0 → knowledge2-0.8.0}/types/orgs.py +0 -0
  153. {knowledge2-0.7.0 → knowledge2-0.8.0}/types/search.py +0 -0
  154. {knowledge2-0.7.0 → knowledge2-0.8.0}/types/usage.py +0 -0
@@ -5,6 +5,154 @@ All notable changes to the Knowledge² Python SDK will be documented in this fil
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## Unreleased
9
+
10
+ ### Changed
11
+
12
+ - **`validate_responses=True` now returns typed run envelopes.** Under response
13
+ validation, `run_agent(..., wait=True)`'s `result` and each `list_agent_runs()`
14
+ item's `result` are now `AgentRunEnvelope` models (attribute access:
15
+ `.content`, `.fields.inferred`, `.metadata.inferred`, `.metadata.provenance`)
16
+ rather than raw dicts. A malformed envelope raises `ValidationError` (as with
17
+ every other validated model), consistent across both read paths. The default
18
+ (`validate_responses=False`) is unchanged — raw dicts. Also: `Provenance` now
19
+ types `source_refs`, and `TaskTypeInfo.unavailable_reason` is narrowed to
20
+ `Literal["env", "org"] | None`. (#2594)
21
+
22
+ ### Added
23
+
24
+ - `client.backfill_feed(feed_id, *, start_from)` (sync) and
25
+ `await client.backfill_feed(...)` (async) — re-enqueue a backfill
26
+ on an existing feed without delete-and-recreate. Resets the feed
27
+ cursor and updates `Feed.start_from`; the worker re-sweeps the
28
+ corpus `CorpusChangeEvent` history from the supplied cutoff.
29
+ Returns `{job_id, start_from, previous_last_checked_seq}` (`202
30
+ Accepted`). Raises `ConflictError` (`code="feed_backfill_in_flight"`)
31
+ when a `run_feed` job for this feed is already queued or running;
32
+ raises `ValidationError` for future `start_from`. Per-feed-run
33
+ feedback lineage (#969) survives — every backfill produces a new
34
+ `feed_run_id`. (#2328)
35
+ - `schedule_cron` kwarg on `create_feed(...)` / `update_feed(...)`
36
+ (sync + async) — UTC five-field cron expression for calendar-aware
37
+ feed schedules (e.g. `"0 9 * * MON"`). Mutually exclusive with
38
+ `schedule_interval`; requires `persistent=True`; rejected when
39
+ combined with `reactive=True`. Invalid expressions surface as
40
+ `422 ValidationError` with the underlying croniter message. New
41
+ field also surfaces on `FeedResponse`. (#2305)
42
+ - `FeedRunResponse.generated_answer` (`dict | None`) — when
43
+ `execution_mode="answer"` and the retrieval step returns results,
44
+ the API now runs the full agent LLM via `execute_agent_task` over
45
+ the chunks and returns the envelope on the run response. Generation
46
+ failures are logged and downgrade gracefully (the response is still
47
+ returned without `generated_answer`). Previously the API raised
48
+ `501` for `answer` mode. (#2303)
49
+ - `client.trigger_pipeline_spec(pipeline_spec_id)` (sync) and
50
+ `await client.trigger_pipeline_spec(...)` (async) — manually fire a
51
+ pipeline run against the spec's bound feeds. Returns
52
+ `{pipeline_run_id, child_run_ids}`; the freshly minted UUID is
53
+ stamped on every follow-on job spawned during the trigger's unwind so
54
+ the entire causal tree shares one correlation id (queryable via the
55
+ forthcoming FR-16e history endpoint). New `PipelineTriggerResponse`
56
+ TypedDict + Pydantic model registered for `validate_responses=True`.
57
+ (#2298)
58
+ - `client.list_feed_runs(feed_id, *, limit, offset)` (sync) and
59
+ `await client.list_feed_runs(...)` (async) — paginated feed-run
60
+ history mirroring `list_agent_runs`. Returns the `run_feed` job
61
+ rows for the feed; the worker-internal `ingest_feed_results`
62
+ follow-on jobs are excluded server-side. (#2296)
63
+ - `client.list_project_subscriptions(project_id, *, agent_id, feed_id,
64
+ limit, offset)` (sync) and `await client.list_project_subscriptions(...)`
65
+ (async) — paginated project-scoped subscriptions admin list. Returns
66
+ per-row admin summary fields (derived `mode`, `destination_count`,
67
+ `last_delivery_status`, `last_delivery_at`) so callers can build a
68
+ cross-agent overview without N+1 fetches. Powers the
69
+ `/subscriptions` page in the console. (#2297)
70
+ - `client.run_agent(agent_id, *, input_chunks, wait, poll_s, timeout_s)`
71
+ (sync) and `await client.run_agent(...)` (async) gain
72
+ `wait=True` synchronous semantics. The default remains `wait=False`
73
+ (preserves today's 202-and-return behavior); opting in returns the
74
+ full job dict (`job_id` / `status` / `result` / `error_message`) once
75
+ the run reaches a terminal state. Raises `TimeoutError` past
76
+ `timeout_s` and `RuntimeError` on `failed` / `canceled` —
77
+ matching the exception contract of `upload_documents_batch(...,
78
+ wait=True)` and `optimize_indexes(..., wait=True)`. (#2343)
79
+
80
+ ### Changed
81
+
82
+ - **`task_type` and `intent_fallback` are now narrowed to `Literal` aliases.**
83
+ Pre-#2291 the Python SDK declared both as bare `str`, so typos like
84
+ `task_type="exract"` only failed at the server's 422. Post-#2291 the new
85
+ `TaskType` and `IntentFallback` `Literal` aliases at `sdk.types.agents`
86
+ (re-exported from `sdk.types`) catch typos at static-analysis time. The
87
+ Pydantic response models (`sdk.models.agents`) and the resource method
88
+ signatures (`create_agent`, `update_agent` on both sync and async clients)
89
+ are narrowed in lockstep. The TS SDK gets symmetric `TaskType` and
90
+ `IntentFallback` literal unions exported from
91
+ `sdk-ts/src/resources/agents.ts` and re-exported from the package root
92
+ (`@knowledge2/sdk`) so npm consumers can import them directly. Note: the
93
+ TS SDK additionally corrected a pre-existing phantom-field bug on its
94
+ `AgentRun` interface — the Python `AgentRunListItem` TypedDict was
95
+ already correct and is unchanged by this PR. (#2291)
96
+ - **`tool_config` is now a kind-discriminated tagged union.** Pre-#2307,
97
+ `tool_config` was a flat dict treated as webhook-only. Post-#2307 the
98
+ canonical shape is `{"kind": "webhook" | "mcp", "webhook": {...} | "mcp": {...}}`.
99
+ Two TypedDict envelopes are added (`ToolConfigWebhook`, `ToolConfigMcp`) plus
100
+ the helper alias `ToolConfigRequest` in `sdk.types.agents`. The legacy flat
101
+ shape (`{"webhook_url": "..."}`) is still accepted on write — the server
102
+ coerces it to the kinded shape via `_coerce_legacy_tool_config`. Response
103
+ shape (`ToolConfigResponseModel`) now carries `kind`, `webhook`, and `mcp`
104
+ fields; pre-#2307 top-level fields (`webhook_url`/`webhook_secret`/
105
+ `webhook_retries`) are tolerated for one minor version for SDK consumers
106
+ reading older API responses. The MCP kind is a stub — the server rejects
107
+ it at entitlement check with HTTP 403 + `feature_not_enabled` until the
108
+ enterprise rollout ships. Follow-up #2313 covers dropping the back-compat
109
+ paths once the engine is confirmed tightened. (#2307)
110
+ - **`HarvestPolicy` enum narrowed from 5 values to 3.** Legacy aliases
111
+ `"always"` (equivalent to `"declared-only"`) and `"declared_and_inferred"`
112
+ (equivalent to `"inferred"`) are no longer accepted by the API or SDK.
113
+ The canonical values are now `"off"` / `"inferred"` / `"declared-only"`.
114
+ Existing stored values are normalised by Alembic
115
+ `0100_harvest_policy_dedupe`. The Python `Literal` type at
116
+ `sdk.types.agents.HarvestPolicy` and the TS union at
117
+ `sdk-ts/src/resources/agents.ts::HarvestPolicy` are narrowed in lockstep.
118
+ Callers using the legacy aliases will see a 422 from `create_agent` /
119
+ `update_agent`; switch to the canonical equivalent. (#2286)
120
+ - `create_agent` / `async_create_agent` docstrings now recommend the
121
+ structured-destination flow for `task_type="notify"`: create the agent
122
+ without a `tool_config`, then attach destinations via
123
+ `create_destination(...)` against the agent's output subscription
124
+ (look up the subscription id via `list_subscriptions(agent_id)` first).
125
+ `tool_config.webhook_url` remains accepted as a generic-HTTPS shortcut
126
+ for backwards compat; the dispatcher fans out to both paths when both
127
+ are configured, so prefer one to avoid duplicate deliveries (#2241,
128
+ #2244).
129
+
130
+ ## [0.8.0] - 2026-05-04
131
+
132
+ ### Added
133
+
134
+ - `create_corpus` and `async_create_corpus` now accept `extraction_template_id`
135
+ and `re_extraction_policy` keyword arguments so callers can bind an extraction
136
+ template at create time without a follow-up PATCH.
137
+ - `update_corpus` and `async_update_corpus` now accept `extraction_template_id`
138
+ (pass `None` to unbind) and `re_extraction_policy` (`None` resets to server
139
+ default `"lazy"`). Both use the `NOT_GIVEN` sentinel to distinguish omit from
140
+ explicit `null`, preventing unintended field resets.
141
+ - `create_agent` and `async_create_agent` accept `declared_schema`
142
+ (`AgentDeclaredSchema | None`) and `harvest_policy` (`HarvestPolicy | None`).
143
+ - `update_agent` and `async_update_agent` accept `declared_schema` and
144
+ `harvest_policy` with `NOT_GIVEN` sentinel semantics.
145
+ - New types exported from `sdk.types`: `AgentDeclaredSchema`, `HarvestPolicy`,
146
+ `NotGivenOr` — use `NOT_GIVEN` from `sdk` for update-only sentinel kwargs.
147
+ - `NOT_GIVEN` sentinel and `_NotGiven` class are now exported from the top-level
148
+ `sdk` package for callers that build update payloads programmatically.
149
+
150
+ ### Changed
151
+
152
+ - `CorpusResponse` (and the underlying Pydantic model) now includes
153
+ `extraction_template_id` and `re_extraction_policy` fields, matching the
154
+ server-side schema added in API v0.8 (#1988).
155
+
8
156
  ## [0.7.0] - 2026-04-23
9
157
 
10
158
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: knowledge2
3
- Version: 0.7.0
3
+ Version: 0.8.0
4
4
  Summary: Python SDK for the Knowledge² retrieval platform
5
5
  Author-email: Knowledge2 <contact@knowledge2.ai>
6
6
  License: MIT
@@ -72,7 +72,8 @@ pip install -e ".[yaml]"
72
72
  | Category | Surface |
73
73
  |---|---|
74
74
  | Core retrieval workflow | orgs, auth, projects, corpora, documents, indexes, search, jobs, metadata, onboarding, audit, usage, console, generation models |
75
- | Enterprise capabilities | agents, feeds, pipelines, A2A |
75
+ | Enterprise capabilities | agents, feeds, destinations, pipelines, A2A, extraction templates |
76
+ | Quality & schema evolution (preview) | quality (engagement, metrics, retrieval outcomes, proposals) — proposals require the `schema_evolution_enabled` org flag; metrics and retrieval-outcomes reads are ungated |
76
77
 
77
78
  The main docs and examples below focus on the core retrieval workflow.
78
79
 
@@ -142,6 +143,41 @@ counters that track admitted documents as processing advances. For broader
142
143
  operational context during a large import, you can still pair them with
143
144
  `get_corpus_status(...)`, `get_job(...)`, or document-level status checks.
144
145
 
146
+ ### Indexing semantics
147
+
148
+ Uploads run in two stages and `get_corpus_status(...)` exposes both:
149
+
150
+ 1. **Ingestion** — chunking + embedding writes. Tracked by `ingesting`. Once
151
+ `ingesting=false`, all chunks for the upload are persisted.
152
+ 2. **Indexing** — the dense + sparse retrieval indexes are rebuilt. Tracked by
153
+ `indexing` and `search_status`. Search and chat reflect the new chunks once
154
+ `search_status="ready"` (or `retrieval_ready=true`).
155
+
156
+ When `auto_index=True` (the default), uploads automatically queue an
157
+ incremental index build after ingestion completes. Search keeps serving the
158
+ previous ready index until the new one finishes — so a doc you just uploaded
159
+ will not appear in search/chat between `ingesting=false` and
160
+ `search_status="ready"`. Poll `get_corpus_status` for `search_status="ready"`
161
+ (or `retrieval_ready=true`) before relying on the new chunks:
162
+
163
+ ```python
164
+ import time
165
+
166
+ deadline = time.monotonic() + 300 # cap the wait — adjust to your SLA
167
+ while time.monotonic() < deadline:
168
+ status = client.get_corpus_status(corpus["id"])
169
+ if status.get("retrieval_ready") or status.get("search_status") == "ready":
170
+ break
171
+ if status.get("search_status") in ("failed", "error"):
172
+ raise RuntimeError(f"Index build failed: {status}")
173
+ time.sleep(3)
174
+ else:
175
+ raise TimeoutError("Corpus did not reach search-ready before deadline")
176
+ ```
177
+
178
+ If you set `auto_index=False`, call `client.sync_indexes(corpus["id"], wait=True)`
179
+ yourself when you are ready to publish the new chunks to search.
180
+
145
181
  ## Improve Retrieval Quality
146
182
 
147
183
  ```python
@@ -240,19 +276,118 @@ from sdk.integrations.llamaindex import K2LlamaIndexRetriever
240
276
 
241
277
  Agents, feeds, pipelines, and A2A are available for enterprise deployments. Keep the primary examples focused on the core retrieval flow.
242
278
 
279
+ ### Agent Declared Schema and Harvest Policy (Preview)
280
+
281
+ `create_agent` and `update_agent` accept a typed `declared_schema` envelope and a `harvest_policy` literal. The server requires `declared_schema` to wrap the JSON Schema under a `fields_schema` key — bare schemas are rejected with HTTP 422.
282
+
283
+ ```python
284
+ agent = client.create_agent(
285
+ name="Case Brief Extractor",
286
+ corpus_id=corpus["id"],
287
+ task_type="extract",
288
+ declared_schema={
289
+ "fields_schema": {
290
+ "type": "object",
291
+ "properties": {
292
+ "court": {"type": "string"},
293
+ "decision_date": {"type": "string", "format": "date"},
294
+ },
295
+ "required": ["court"],
296
+ }
297
+ },
298
+ harvest_policy="declared-only",
299
+ )
300
+ ```
301
+
302
+ `harvest_policy` is one of `"off"`, `"inferred"`, or `"declared-only"`:
303
+
304
+ - `"off"` (default) — agent runs do not harvest any output back into the corpus.
305
+ - `"inferred"` — harvest the LLM-inferred fields (and, if `declared_schema` is set, the declared fields too) into the corpus on each run.
306
+ - `"declared-only"` — harvest only the declared fields from `declared_schema` (no inferred fields). **Requires `declared_schema` to be set**; if it's not, no structured output is requested and the agent behaves as if `harvest_policy="off"`.
307
+
308
+ The legacy aliases `"always"` (→ `"declared-only"`) and `"declared_and_inferred"` (→ `"inferred"`) were normalised by Alembic 0100_harvest_policy_dedupe and are no longer accepted as of #2286.
309
+
310
+ ### Running an Agent (Preview)
311
+
312
+ `run_agent(...)` enqueues a background job and by default returns the 202 handle
313
+ immediately. Pass `wait=True` to block until the run reaches a terminal state
314
+ and receive the full job dict (including `result`):
315
+
316
+ ```python
317
+ # Fire-and-forget (default) — returns {"job_id": ..., "status": "queued"}.
318
+ run = client.run_agent(agent_id, input_chunks=[{"text": "..."}])
319
+
320
+ # Synchronous — blocks until the run reaches a terminal state.
321
+ job = client.run_agent(
322
+ agent_id,
323
+ input_chunks=[{"text": "..."}],
324
+ wait=True,
325
+ poll_s=2,
326
+ timeout_s=120,
327
+ )
328
+ envelope = job.get("result") # AgentRunEnvelope | None: content / fields / metadata
329
+ ```
330
+
331
+ `timeout_s` bounds the **full** wall-clock budget from enqueue through polling
332
+ (a slow POST eats into the polling budget; once `timeout_s` elapses the call
333
+ raises `TimeoutError` regardless of which phase is in flight). `wait=True`
334
+ raises `RuntimeError` (carrying the job's `error_message` when present,
335
+ otherwise a synthesized `"Job <id> ended with status=<status>"` string) if the
336
+ run ends `failed` or `canceled` — same exception contract as
337
+ `upload_documents_batch(..., wait=True)` and `optimize_indexes(...,
338
+ wait=True)`. If the server returns a 202 without a `job_id` (contract
339
+ violation), `wait=True` raises `RuntimeError` rather than silently returning
340
+ the enqueue handle.
341
+
342
+ Under `client.with_raw_response.run_agent(..., wait=True)` the return value is
343
+ a `RawResponse` wrapping the terminal job (`status_code`, `headers`, and
344
+ `parsed` dict) — the trailing `GET /v1/jobs/{id}` honors raw mode so callers
345
+ who opt into raw responses keep getting envelopes on the wait path.
346
+
347
+ ### Corpus Extraction Template (Preview)
348
+
349
+ > **API path convention:** All v1 endpoints use hyphens in the path. Use `/v1/extraction-templates`, not `/v1/extraction_templates`. The underscore variant returns 404.
350
+
351
+ `create_corpus` and `update_corpus` accept `extraction_template_id` and `re_extraction_policy` (`"lazy"` or `"eager"`). The template must be a seed template or owned by the **target corpus/project's organization** — for ordinary org-scoped keys this is the caller's org, but global-scope/admin keys can bind templates owned by the cross-org project they're targeting.
352
+
353
+ > **Typed-contract note for `declared_schema`:** `AgentDeclaredSchema` is a Python `TypedDict` with `total=False`. The server requires at least one of `fields_schema` / `metadata_schema` (an empty envelope `{}` is rejected with `422`), but `TypedDict` cannot express that constraint statically — so static type checkers will accept `declared_schema={}` even though it will fail at runtime. The TS SDK models the same constraint as a discriminated union, which **does** catch `{}` at compile time. If you need stricter Python validation, build the request through the Pydantic model layer (`sdk.models.agents.AgentDeclaredSchemaModel`) before passing it through.
354
+
355
+ ```python
356
+ corpus = client.create_corpus(
357
+ project["id"],
358
+ "Legal Decisions",
359
+ extraction_template_id="tpl_legal_v1",
360
+ re_extraction_policy="eager",
361
+ )
362
+ ```
363
+
364
+ `re_extraction_policy` has no effect at create time (a fresh corpus has no documents) but is persisted so that it governs re-extraction behavior when the template binding is later changed on a non-empty corpus.
365
+
366
+ To unbind a previously-bound template, pass `extraction_template_id=None` to `update_corpus(...)` — the SDK distinguishes "omit" (leave unchanged) from "explicitly null" (clear the binding) via a sentinel default. The same pattern applies to `update_agent(declared_schema=None, ...)` and `update_agent(harvest_policy=None, ...)`.
367
+
368
+ ### Per-corpus intent-routing kill-switch
369
+
370
+ `create_corpus` and `update_corpus` accept `intent_routing_enabled` (`bool`), echoed back on `get_corpus`. It defaults to `True` (routing on — opt-out). Set it to `False` to disable the whole intent pre-pass + tiered-filter pipeline for the corpus, so chat/agent-run against it skips query classification and tier assignment entirely — useful for incident response and eval baselines. Omit the kwarg (leave `None`) on `update_corpus` to leave the current value unchanged. This is broader than a per-field hard-threshold disable, which only turns off the hard tier.
371
+
372
+ ```python
373
+ client.update_corpus(corpus["id"], intent_routing_enabled=False) # routing off
374
+ ```
375
+
243
376
  ### Subscription Modes (Preview)
244
377
 
245
- Agent-feed subscriptions support three authoring modes on `create_subscription`, gated behind the `knowledge_agents_enabled` feature flag:
378
+ Agent-feed subscriptions support four authoring modes on `create_subscription`, gated behind the `knowledge_agents_enabled` feature flag:
246
379
 
247
380
  | Mode | Use | Required fields |
248
381
  |------|-----|-----------------|
249
382
  | `always` | Route every envelope from the feed | `feed_id`, `role` |
250
383
  | `explicit` | Evaluate a predicate DSL against the envelope | `feed_id`, `role`, `match_spec` |
251
384
  | `nl_semantic` | Describe the match in plain English; compiled server-side into a `semantic_like` predicate against `content` | `feed_id`, `role`, `match_spec_description` (10-500 chars); optional `threshold` (default 0.75) |
385
+ | `structured` | Compose filter clauses without writing raw DSL; compiled server-side into an explicit MatchSpec | `feed_id`, `role`, `structured_filters` (list of filter dicts); optional `structured_logic` (`"and"` default or `"or"`) |
252
386
 
253
387
  The create response echoes the compiled `match_spec` and the raw `match_spec_description`, so no separate `/preview` endpoint is required:
254
388
 
255
389
  ```python
390
+ # Natural-language mode
256
391
  sub = client.create_subscription(
257
392
  agent_id,
258
393
  feed_id=feed_id,
@@ -262,6 +397,19 @@ sub = client.create_subscription(
262
397
  )
263
398
  print(sub["match_spec"]) # compiled semantic_like predicate
264
399
  print(sub["match_spec_description"]) # raw NL description (echoed)
400
+
401
+ # Structured filter mode
402
+ sub = client.create_subscription(
403
+ agent_id,
404
+ feed_id=feed_id,
405
+ role="output",
406
+ mode="structured",
407
+ structured_filters=[
408
+ {"path": "metadata.declared.claim_type", "op": "==", "value": "theft"},
409
+ ],
410
+ structured_logic="and",
411
+ )
412
+ print(sub["match_spec"]) # compiled explicit predicate
265
413
  ```
266
414
 
267
415
  ### Feed Drafts, Subscriptions, and Feedback (Preview)
@@ -279,13 +427,32 @@ editing and feedback surface of the Feeds API as flat methods on `client`
279
427
  | `list_feed_subscriptions(feed_id)` | Read-only view | Returns subscriptions embedded on the feed record; use `create_subscription` on the Agents mixin to attach new ones |
280
428
  | `submit_feed_feedback(feed_id, *, rating, chunk_id, feed_run_id)` | `POST /v1/feeds/{id}/feedback` | `rating` is `1` (thumbs up) or `0` (thumbs down) |
281
429
  | `get_feed_feedback_stats(feed_id, *, feed_run_id=None)` | `GET /v1/feeds/{id}/feedback` | Optional `feed_run_id` scopes stats to a single run |
430
+ | `backfill_feed(feed_id, *, start_from)` | `POST /v1/feeds/{id}/backfill` | Re-enqueue a backfill on an existing feed without delete-and-recreate. Resets the feed cursor and updates `start_from`. Returns `202` with `{job_id, start_from, previous_last_checked_seq}`; raises `409 feed_backfill_in_flight` if a run_feed job is already queued or running (#2328) |
282
431
 
283
432
  ```python
284
433
  draft = client.create_feed_draft(feed_id)
285
434
  client.update_feed(draft["id"], name="new name")
286
435
  client.activate_feed_draft(feed_id) # applies the draft; returns the parent
287
436
 
437
+ # Cron-scheduled feed (UTC; mutually exclusive with schedule_interval) — #2305
438
+ client.create_feed(
439
+ project_id=project_id,
440
+ source_agent_id=agent_id,
441
+ name="weekly digest",
442
+ persistent=True,
443
+ target_corpus={"existing": corpus_id},
444
+ schedule_cron="0 9 * * MON", # Mondays at 09:00 UTC
445
+ )
446
+
447
+ # execution_mode='answer' returns an LLM-generated answer over the chunks (#2303)
288
448
  run = client.run_feed(feed_id, return_results=True)
449
+ if run.get("generated_answer"):
450
+ answer_text = run["generated_answer"]["content"] # envelope shape
451
+
452
+ # Re-backfill in place (#2328); the response carries the previous cursor for
453
+ # forensic correlation in audit logs.
454
+ client.backfill_feed(feed_id, start_from="2026-01-01T00:00:00Z")
455
+
289
456
  # `results` is only populated for non-persistent feeds run with
290
457
  # `return_results=True`; guard the example for safe use.
291
458
  if run.get("results"):
@@ -298,7 +465,7 @@ if run.get("results"):
298
465
  stats = client.get_feed_feedback_stats(feed_id) # org-wide for this feed
299
466
  ```
300
467
 
301
- All three areas are fully mirrored on `AsyncKnowledge2` under the same names.
468
+ All methods above are fully mirrored on `AsyncKnowledge2` under the same names.
302
469
 
303
470
  ## Error Handling
304
471
 
@@ -43,7 +43,8 @@ pip install -e ".[yaml]"
43
43
  | Category | Surface |
44
44
  |---|---|
45
45
  | Core retrieval workflow | orgs, auth, projects, corpora, documents, indexes, search, jobs, metadata, onboarding, audit, usage, console, generation models |
46
- | Enterprise capabilities | agents, feeds, pipelines, A2A |
46
+ | Enterprise capabilities | agents, feeds, destinations, pipelines, A2A, extraction templates |
47
+ | Quality & schema evolution (preview) | quality (engagement, metrics, retrieval outcomes, proposals) — proposals require the `schema_evolution_enabled` org flag; metrics and retrieval-outcomes reads are ungated |
47
48
 
48
49
  The main docs and examples below focus on the core retrieval workflow.
49
50
 
@@ -113,6 +114,41 @@ counters that track admitted documents as processing advances. For broader
113
114
  operational context during a large import, you can still pair them with
114
115
  `get_corpus_status(...)`, `get_job(...)`, or document-level status checks.
115
116
 
117
+ ### Indexing semantics
118
+
119
+ Uploads run in two stages and `get_corpus_status(...)` exposes both:
120
+
121
+ 1. **Ingestion** — chunking + embedding writes. Tracked by `ingesting`. Once
122
+ `ingesting=false`, all chunks for the upload are persisted.
123
+ 2. **Indexing** — the dense + sparse retrieval indexes are rebuilt. Tracked by
124
+ `indexing` and `search_status`. Search and chat reflect the new chunks once
125
+ `search_status="ready"` (or `retrieval_ready=true`).
126
+
127
+ When `auto_index=True` (the default), uploads automatically queue an
128
+ incremental index build after ingestion completes. Search keeps serving the
129
+ previous ready index until the new one finishes — so a doc you just uploaded
130
+ will not appear in search/chat between `ingesting=false` and
131
+ `search_status="ready"`. Poll `get_corpus_status` for `search_status="ready"`
132
+ (or `retrieval_ready=true`) before relying on the new chunks:
133
+
134
+ ```python
135
+ import time
136
+
137
+ deadline = time.monotonic() + 300 # cap the wait — adjust to your SLA
138
+ while time.monotonic() < deadline:
139
+ status = client.get_corpus_status(corpus["id"])
140
+ if status.get("retrieval_ready") or status.get("search_status") == "ready":
141
+ break
142
+ if status.get("search_status") in ("failed", "error"):
143
+ raise RuntimeError(f"Index build failed: {status}")
144
+ time.sleep(3)
145
+ else:
146
+ raise TimeoutError("Corpus did not reach search-ready before deadline")
147
+ ```
148
+
149
+ If you set `auto_index=False`, call `client.sync_indexes(corpus["id"], wait=True)`
150
+ yourself when you are ready to publish the new chunks to search.
151
+
116
152
  ## Improve Retrieval Quality
117
153
 
118
154
  ```python
@@ -211,19 +247,118 @@ from sdk.integrations.llamaindex import K2LlamaIndexRetriever
211
247
 
212
248
  Agents, feeds, pipelines, and A2A are available for enterprise deployments. Keep the primary examples focused on the core retrieval flow.
213
249
 
250
+ ### Agent Declared Schema and Harvest Policy (Preview)
251
+
252
+ `create_agent` and `update_agent` accept a typed `declared_schema` envelope and a `harvest_policy` literal. The server requires `declared_schema` to wrap the JSON Schema under a `fields_schema` key — bare schemas are rejected with HTTP 422.
253
+
254
+ ```python
255
+ agent = client.create_agent(
256
+ name="Case Brief Extractor",
257
+ corpus_id=corpus["id"],
258
+ task_type="extract",
259
+ declared_schema={
260
+ "fields_schema": {
261
+ "type": "object",
262
+ "properties": {
263
+ "court": {"type": "string"},
264
+ "decision_date": {"type": "string", "format": "date"},
265
+ },
266
+ "required": ["court"],
267
+ }
268
+ },
269
+ harvest_policy="declared-only",
270
+ )
271
+ ```
272
+
273
+ `harvest_policy` is one of `"off"`, `"inferred"`, or `"declared-only"`:
274
+
275
+ - `"off"` (default) — agent runs do not harvest any output back into the corpus.
276
+ - `"inferred"` — harvest the LLM-inferred fields (and, if `declared_schema` is set, the declared fields too) into the corpus on each run.
277
+ - `"declared-only"` — harvest only the declared fields from `declared_schema` (no inferred fields). **Requires `declared_schema` to be set**; if it's not, no structured output is requested and the agent behaves as if `harvest_policy="off"`.
278
+
279
+ The legacy aliases `"always"` (→ `"declared-only"`) and `"declared_and_inferred"` (→ `"inferred"`) were normalised by Alembic 0100_harvest_policy_dedupe and are no longer accepted as of #2286.
280
+
281
+ ### Running an Agent (Preview)
282
+
283
+ `run_agent(...)` enqueues a background job and by default returns the 202 handle
284
+ immediately. Pass `wait=True` to block until the run reaches a terminal state
285
+ and receive the full job dict (including `result`):
286
+
287
+ ```python
288
+ # Fire-and-forget (default) — returns {"job_id": ..., "status": "queued"}.
289
+ run = client.run_agent(agent_id, input_chunks=[{"text": "..."}])
290
+
291
+ # Synchronous — blocks until the run reaches a terminal state.
292
+ job = client.run_agent(
293
+ agent_id,
294
+ input_chunks=[{"text": "..."}],
295
+ wait=True,
296
+ poll_s=2,
297
+ timeout_s=120,
298
+ )
299
+ envelope = job.get("result") # AgentRunEnvelope | None: content / fields / metadata
300
+ ```
301
+
302
+ `timeout_s` bounds the **full** wall-clock budget from enqueue through polling
303
+ (a slow POST eats into the polling budget; once `timeout_s` elapses the call
304
+ raises `TimeoutError` regardless of which phase is in flight). `wait=True`
305
+ raises `RuntimeError` (carrying the job's `error_message` when present,
306
+ otherwise a synthesized `"Job <id> ended with status=<status>"` string) if the
307
+ run ends `failed` or `canceled` — same exception contract as
308
+ `upload_documents_batch(..., wait=True)` and `optimize_indexes(...,
309
+ wait=True)`. If the server returns a 202 without a `job_id` (contract
310
+ violation), `wait=True` raises `RuntimeError` rather than silently returning
311
+ the enqueue handle.
312
+
313
+ Under `client.with_raw_response.run_agent(..., wait=True)` the return value is
314
+ a `RawResponse` wrapping the terminal job (`status_code`, `headers`, and
315
+ `parsed` dict) — the trailing `GET /v1/jobs/{id}` honors raw mode so callers
316
+ who opt into raw responses keep getting envelopes on the wait path.
317
+
318
+ ### Corpus Extraction Template (Preview)
319
+
320
+ > **API path convention:** All v1 endpoints use hyphens in the path. Use `/v1/extraction-templates`, not `/v1/extraction_templates`. The underscore variant returns 404.
321
+
322
+ `create_corpus` and `update_corpus` accept `extraction_template_id` and `re_extraction_policy` (`"lazy"` or `"eager"`). The template must be a seed template or owned by the **target corpus/project's organization** — for ordinary org-scoped keys this is the caller's org, but global-scope/admin keys can bind templates owned by the cross-org project they're targeting.
323
+
324
+ > **Typed-contract note for `declared_schema`:** `AgentDeclaredSchema` is a Python `TypedDict` with `total=False`. The server requires at least one of `fields_schema` / `metadata_schema` (an empty envelope `{}` is rejected with `422`), but `TypedDict` cannot express that constraint statically — so static type checkers will accept `declared_schema={}` even though it will fail at runtime. The TS SDK models the same constraint as a discriminated union, which **does** catch `{}` at compile time. If you need stricter Python validation, build the request through the Pydantic model layer (`sdk.models.agents.AgentDeclaredSchemaModel`) before passing it through.
325
+
326
+ ```python
327
+ corpus = client.create_corpus(
328
+ project["id"],
329
+ "Legal Decisions",
330
+ extraction_template_id="tpl_legal_v1",
331
+ re_extraction_policy="eager",
332
+ )
333
+ ```
334
+
335
+ `re_extraction_policy` has no effect at create time (a fresh corpus has no documents) but is persisted so that it governs re-extraction behavior when the template binding is later changed on a non-empty corpus.
336
+
337
+ To unbind a previously-bound template, pass `extraction_template_id=None` to `update_corpus(...)` — the SDK distinguishes "omit" (leave unchanged) from "explicitly null" (clear the binding) via a sentinel default. The same pattern applies to `update_agent(declared_schema=None, ...)` and `update_agent(harvest_policy=None, ...)`.
338
+
339
+ ### Per-corpus intent-routing kill-switch
340
+
341
+ `create_corpus` and `update_corpus` accept `intent_routing_enabled` (`bool`), echoed back on `get_corpus`. It defaults to `True` (routing on — opt-out). Set it to `False` to disable the whole intent pre-pass + tiered-filter pipeline for the corpus, so chat/agent-run against it skips query classification and tier assignment entirely — useful for incident response and eval baselines. Omit the kwarg (leave `None`) on `update_corpus` to leave the current value unchanged. This is broader than a per-field hard-threshold disable, which only turns off the hard tier.
342
+
343
+ ```python
344
+ client.update_corpus(corpus["id"], intent_routing_enabled=False) # routing off
345
+ ```
346
+
214
347
  ### Subscription Modes (Preview)
215
348
 
216
- Agent-feed subscriptions support three authoring modes on `create_subscription`, gated behind the `knowledge_agents_enabled` feature flag:
349
+ Agent-feed subscriptions support four authoring modes on `create_subscription`, gated behind the `knowledge_agents_enabled` feature flag:
217
350
 
218
351
  | Mode | Use | Required fields |
219
352
  |------|-----|-----------------|
220
353
  | `always` | Route every envelope from the feed | `feed_id`, `role` |
221
354
  | `explicit` | Evaluate a predicate DSL against the envelope | `feed_id`, `role`, `match_spec` |
222
355
  | `nl_semantic` | Describe the match in plain English; compiled server-side into a `semantic_like` predicate against `content` | `feed_id`, `role`, `match_spec_description` (10-500 chars); optional `threshold` (default 0.75) |
356
+ | `structured` | Compose filter clauses without writing raw DSL; compiled server-side into an explicit MatchSpec | `feed_id`, `role`, `structured_filters` (list of filter dicts); optional `structured_logic` (`"and"` default or `"or"`) |
223
357
 
224
358
  The create response echoes the compiled `match_spec` and the raw `match_spec_description`, so no separate `/preview` endpoint is required:
225
359
 
226
360
  ```python
361
+ # Natural-language mode
227
362
  sub = client.create_subscription(
228
363
  agent_id,
229
364
  feed_id=feed_id,
@@ -233,6 +368,19 @@ sub = client.create_subscription(
233
368
  )
234
369
  print(sub["match_spec"]) # compiled semantic_like predicate
235
370
  print(sub["match_spec_description"]) # raw NL description (echoed)
371
+
372
+ # Structured filter mode
373
+ sub = client.create_subscription(
374
+ agent_id,
375
+ feed_id=feed_id,
376
+ role="output",
377
+ mode="structured",
378
+ structured_filters=[
379
+ {"path": "metadata.declared.claim_type", "op": "==", "value": "theft"},
380
+ ],
381
+ structured_logic="and",
382
+ )
383
+ print(sub["match_spec"]) # compiled explicit predicate
236
384
  ```
237
385
 
238
386
  ### Feed Drafts, Subscriptions, and Feedback (Preview)
@@ -250,13 +398,32 @@ editing and feedback surface of the Feeds API as flat methods on `client`
250
398
  | `list_feed_subscriptions(feed_id)` | Read-only view | Returns subscriptions embedded on the feed record; use `create_subscription` on the Agents mixin to attach new ones |
251
399
  | `submit_feed_feedback(feed_id, *, rating, chunk_id, feed_run_id)` | `POST /v1/feeds/{id}/feedback` | `rating` is `1` (thumbs up) or `0` (thumbs down) |
252
400
  | `get_feed_feedback_stats(feed_id, *, feed_run_id=None)` | `GET /v1/feeds/{id}/feedback` | Optional `feed_run_id` scopes stats to a single run |
401
+ | `backfill_feed(feed_id, *, start_from)` | `POST /v1/feeds/{id}/backfill` | Re-enqueue a backfill on an existing feed without delete-and-recreate. Resets the feed cursor and updates `start_from`. Returns `202` with `{job_id, start_from, previous_last_checked_seq}`; raises `409 feed_backfill_in_flight` if a run_feed job is already queued or running (#2328) |
253
402
 
254
403
  ```python
255
404
  draft = client.create_feed_draft(feed_id)
256
405
  client.update_feed(draft["id"], name="new name")
257
406
  client.activate_feed_draft(feed_id) # applies the draft; returns the parent
258
407
 
408
+ # Cron-scheduled feed (UTC; mutually exclusive with schedule_interval) — #2305
409
+ client.create_feed(
410
+ project_id=project_id,
411
+ source_agent_id=agent_id,
412
+ name="weekly digest",
413
+ persistent=True,
414
+ target_corpus={"existing": corpus_id},
415
+ schedule_cron="0 9 * * MON", # Mondays at 09:00 UTC
416
+ )
417
+
418
+ # execution_mode='answer' returns an LLM-generated answer over the chunks (#2303)
259
419
  run = client.run_feed(feed_id, return_results=True)
420
+ if run.get("generated_answer"):
421
+ answer_text = run["generated_answer"]["content"] # envelope shape
422
+
423
+ # Re-backfill in place (#2328); the response carries the previous cursor for
424
+ # forensic correlation in audit logs.
425
+ client.backfill_feed(feed_id, start_from="2026-01-01T00:00:00Z")
426
+
260
427
  # `results` is only populated for non-persistent feeds run with
261
428
  # `return_results=True`; guard the example for safe use.
262
429
  if run.get("results"):
@@ -269,7 +436,7 @@ if run.get("results"):
269
436
  stats = client.get_feed_feedback_stats(feed_id) # org-wide for this feed
270
437
  ```
271
438
 
272
- All three areas are fully mirrored on `AsyncKnowledge2` under the same names.
439
+ All methods above are fully mirrored on `AsyncKnowledge2` under the same names.
273
440
 
274
441
  ## Error Handling
275
442
 
@@ -8,6 +8,7 @@ from ._logging import set_debug
8
8
  from ._paging import Page, SyncPager
9
9
  from ._raw_response import RawResponse
10
10
  from ._request_options import RequestOptions
11
+ from ._sentinels import NOT_GIVEN, NotGivenOr
11
12
  from .async_client import AsyncKnowledge2
12
13
  from .client import Knowledge2, Knowledge2Validated
13
14
  from .errors import (
@@ -42,6 +43,7 @@ def __getattr__(name: str):
42
43
 
43
44
 
44
45
  __all__ = [
46
+ "NOT_GIVEN",
45
47
  "APIConnectionError",
46
48
  "APIError",
47
49
  "APITimeoutError",
@@ -59,6 +61,7 @@ __all__ = [
59
61
  "Knowledge2Error",
60
62
  "Knowledge2Validated",
61
63
  "NotFoundError",
64
+ "NotGivenOr",
62
65
  "Page",
63
66
  "PermissionDeniedError",
64
67
  "PipelineBuilder",
@@ -0,0 +1,13 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Mapping
4
+ from typing import Any
5
+
6
+
7
+ def validate_chunking_config(config: Mapping[str, Any] | None) -> None:
8
+ if not config:
9
+ return
10
+ chunk_size = config.get("chunk_size")
11
+ overlap = config.get("overlap")
12
+ if chunk_size is not None and overlap is not None and int(overlap) >= int(chunk_size):
13
+ raise ValueError("chunking overlap must be smaller than chunk_size")