Skip to content

Commit 54f63ee

Browse files
feat(ingestion): submit all sources then poll the last episode (v0.3.0) (#607)
* feat(ingestion): submit all sources then poll the last episode Stop treating sequential ingest as wait-for-each-file. Queue every file into a graph first; wait() is opt-in, polls the tail of the per-graph queue, and scales its timeout with how many episodes were submitted. Bump zep-ingest to 0.3.0 for the release. Co-authored-by: Cursor <cursoragent@cursor.com> * chore(ingestion): increase auto wait timeout to 60s per item Give episodes and manual graph updates more headroom before wait() raises IngestTimeoutError. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(ingestion): align wait() polling with ingestion status docs Poll thread backfills via the last message UUID per request, document batch vs episode vs task handles, and warn against mixing batch and sequential graph.add on one graph. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(ingestion): correct thread polling and multi-thread wait semantics Regular thread.add_messages returns message_uuids only (verified against production). Remove dead task_id fallback, poll one tail per thread, and stop falsely marking all threads done when one saga finishes. Co-authored-by: Cursor <cursoragent@cursor.com> * chore(ingestion): add production release smoke script for v0.3.0 Document pre-release prod checks and finalize 0.3.0 changelog notes for thread polling and combine(). Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 7de18df commit 54f63ee

31 files changed

Lines changed: 1155 additions & 148 deletions

ingestion/CHANGELOG.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,36 @@ All notable changes to `zep-ingest` are documented here. The project follows
44
[Semantic Versioning](https://semver.org); while at `0.x` the public API may
55
still change between minor versions.
66

7+
## 0.3.0
8+
9+
- **Submit everything, then wait once.** Multiple files or loaders destined for
10+
the same graph are submitted together. Sequential vs batch only chooses the
11+
submit API (`graph.add` vs Batch API); neither waits for one file to finish
12+
processing before the next is sent. If you do not need to block, submit and
13+
return — `wait()` stays opt-in.
14+
- `wait()` aligns with [Check data ingestion status](https://help.getzep.com/check-data-ingestion-status):
15+
Batch API paths poll the last batch via `batch.get`; sequential `graph.add`
16+
polls the last-submitted episode; sequential `thread.add_messages` polls the
17+
last message UUID per thread (regular ``add_messages`` returns message UUIDs,
18+
not ``task_id``); nodes/triples poll every task id.
19+
Default timeout is `wait_timeout_seconds(items_submitted)` — 60s per item,
20+
minimum 120s. Pass `timeout=None` to wait without a deadline.
21+
`IngestResult.from_batch_ids(...).wait()` has no item count, so auto timeout
22+
does not invent a 120s cap. Do not mix Batch API and sequential `graph.add`
23+
on the same graph and expect one `wait()` to cover both.
24+
- File one-liners and loaders accept a sequence of paths/globs in caller order
25+
(`ingest_json_records(client, [issues, prs, jira], graph_id=...)`).
26+
- `ConcatLoader` concatenates heterogeneous loaders into one submit stream.
27+
- `IngestResult.combine(...)` merges poll handles for separate submits to the same
28+
graph (`batch_ids`, `episode_uuids`, `task_ids`); it does not merge
29+
`node_uuids` / `edge_uuids` (zip alignment). Prefer separate `wait()` calls
30+
for seeding vs episode ingest.
31+
- Multi-thread sequential backfills poll the last message UUID **per thread**
32+
(threads are independent sagas on the server). Regular `thread.add_messages`
33+
returns `message_uuids` only — not `task_id`.
34+
- Production smoke script: `ingestion/scripts/release_smoke_prod.py` (requires
35+
`ZEP_API_KEY`; run via KeyBank `zep-prod`).
36+
737
## 0.2.1
838

939
- Batch submission now rolls over at 10,000 items by default

ingestion/README.md

Lines changed: 78 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,13 @@ endpoint to call (HTTP 404 — an older server, a self-hosted or Community
139139
deployment, or a base URL that doesn't route `/batches`). Authorization and
140140
quota errors are raised as errors instead of quietly downgrading the run.
141141
Every source here ingests fine without the Batch API — pass
142-
`method="sequential"` to take that path deliberately.
142+
`method="sequential"` to take that path deliberately. Sequential does **not**
143+
mean "wait until this file has finished extracting before submitting the next."
144+
It only means each item is sent with `graph.add` (or `thread.add_messages`)
145+
instead of the Batch API. Submit every file into the graph first; `wait()` is
146+
opt-in. On the default Batch path it monitors `batch.get` on the last batch;
147+
on sequential fallback it polls the last-submitted episode's `processed` flag
148+
(see [Check data ingestion status](https://help.getzep.com/check-data-ingestion-status)).
143149

144150
## The pipeline
145151

@@ -166,7 +172,7 @@ pipeline = Pipeline(
166172
)
167173
report = pipeline.preview() # NO Zep API calls: inspect episodes + warnings first
168174
result = pipeline.run(client, graph_id="company_kb")
169-
result.wait(timeout=3600) # submission returns immediately; blocking is opt-in
175+
result.wait() # opt-in; batch.get tail or last-submitted episode; timeout scales with item count
170176
```
171177

172178
`preview()` shows the transformed episodes and validation warnings (including
@@ -383,7 +389,49 @@ graph):
383389
`source_node_uuid`/`target_node_uuid`. Extraction dedups against the existing
384390
graph, so known entities anchor resolution.
385391
5. Ingest the corpus with real `created_at` timestamps and alias
386-
canonicalization, then block on the bound result with `result.wait(...)`.
392+
canonicalization. Submit every source for a graph before waiting. Then, if
393+
you need extraction to finish, block on the bound result with
394+
`result.wait()` (or `combined.wait()` after `IngestResult.combine`).
395+
396+
## Many files, one graph
397+
398+
Do not wait for one file (or one graph) to finish processing before submitting
399+
the next. Create destinations and set ontology, then enqueue everything.
400+
401+
```python
402+
from zep_ingest import ConcatLoader, ingest, ingest_json_records, ingest_nodes
403+
404+
# Same loader, several files — submitted in this order, one wait:
405+
result = ingest_json_records(
406+
client,
407+
["data/issues.jsonl", "data/prs.jsonl", "data/jira.jsonl"],
408+
graph_id="engineering",
409+
)
410+
result.wait() # batch.get on the last batch (default path); timeout scales with items_submitted
411+
412+
# Mixed sources, still one submit stream:
413+
from zep_ingest import JsonRecordsLoader, TextFileLoader
414+
415+
result = ingest(
416+
client,
417+
ConcatLoader(
418+
[
419+
JsonRecordsLoader(["data/issues.jsonl", "data/prs.jsonl"]),
420+
TextFileLoader("data/runbooks/**/*.md"),
421+
]
422+
),
423+
graph_id="engineering",
424+
)
425+
result.wait()
426+
427+
# Already-separate calls: prefer separate wait() for seeding vs episodes.
428+
nodes = ingest_nodes(client, node_items, graph_id="engineering")
429+
nodes.wait()
430+
docs = ingest_json_records(client, ["data/issues.jsonl", "data/prs.jsonl"], graph_id="engineering")
431+
docs.wait()
432+
```
433+
434+
If you are not polling, stop after submit — there is nothing else to do.
387435

388436
## Fact triples
389437

@@ -407,7 +455,7 @@ result = ingest_fact_triples(
407455
],
408456
graph_id="org",
409457
)
410-
result.wait(timeout=600)
458+
result.wait()
411459
# Zep assigns the fact UUID; it lands in task params as edge_uuid after completion.
412460
# Parallel to the submitted triples — failed tasks leave None in that slot.
413461
result.edge_uuids
@@ -463,7 +511,7 @@ Sequential only (the Batch API doesn't take direct nodes).
463511
```python
464512
result = ingest_slack_export(client, "export.zip", graph_id="g1")
465513
result.status # queued | processing | untracked | succeeded | partial | failed | canceled
466-
result.wait(timeout=3600)
514+
result.wait()
467515
result.failed_items() # Batch API item records and/or submission AddErrors
468516
result.warnings # everything the pipeline noticed
469517
result.raise_for_status() # opt-in strictness
@@ -476,6 +524,20 @@ but when it raises — a timeout, or a submission the API left untracked —
476524
nothing was ever bound, and `batch_ids` / `task_ids` are the only handles for
477525
resuming or diagnosing that run.
478526

527+
**What `wait()` polls** (aligned with
528+
[Check data ingestion status](https://help.getzep.com/check-data-ingestion-status)):
529+
530+
| Submission path | Monitor via `wait()` |
531+
| --- | --- |
532+
| Batch API (default for episodes and thread backfill) | Last `batch_id` via `batch.get` |
533+
| Sequential `graph.add` (batch fallback) | Last-submitted episode (`episode_uuids[-1]`) |
534+
| Sequential `thread.add_messages` | Last message UUID per thread (`message_uuids`; no `task_id`) |
535+
| `ingest_nodes` / `ingest_fact_triples` | Every `task_id` (separate queue from episodes) |
536+
537+
Do not mix Batch API and sequential `graph.add` into the same graph and expect
538+
one `wait()` to cover both. Seed nodes/triples first with their own `wait()`,
539+
then ingest episodes.
540+
479541
Ingestion is asynchronous — a just-added fact is not instantly retrievable,
480542
even after `wait()`: search indexing lands a few seconds after processing.
481543
`search_when_ready` owns that gap so scripts don't hand-roll poll loops:
@@ -491,9 +553,9 @@ recorded as `AddError`s (indices and API messages only — never episode content
491553
and the run continues. `batch_ids` / `episode_uuids` / `task_ids` are the
492554
resume handles; `node_uuids` / `edge_uuids` record identities Zep assigned on
493555
`ingest_nodes` and completed `ingest_fact_triples` tasks (`None` slots mark
494-
failures so later successes stay zip-aligned). Task IDs are used by
495-
asynchronous operations such as fact triples, direct node creation, and
496-
sequential thread submissions, and `wait()` polls them through `client.task`.
556+
failures so later successes stay zip-aligned). Task IDs come from
557+
``ingest_nodes``, ``ingest_fact_triples``, and ``add_messages_batch`` — not from
558+
regular ``thread.add_messages``, which returns message UUIDs instead.
497559

498560
If the API accepts a task-backed submission without returning a completion
499561
handle, the result reports `status == "untracked"` instead of claiming success.
@@ -565,5 +627,13 @@ make install # uv sync --extra dev
565627
make all # format + lint + type-check + test
566628
```
567629

630+
Before a release, run the production smoke script against a throwaway graph
631+
(requires `ZEP_API_KEY` and `ZEP_API_URL`):
632+
633+
```bash
634+
keybank run zep-prod -- env ZEP_API_URL=https://api.getzep.com \
635+
uv run python scripts/release_smoke_prod.py
636+
```
637+
568638
Live integration tests run only when `ZEP_API_KEY` is set. See
569639
[`SETUP.md`](https://github.com/getzep/zep/blob/main/ingestion/SETUP.md) for account setup.

ingestion/examples/documents_example.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ def main() -> None:
6565
)
6666
# Submission returns immediately; blocking is opt-in. Bind the result first
6767
# so a wait() timeout still leaves you the ids below to resume from.
68-
result.wait(timeout=600)
68+
result.wait()
6969
print(f"Submitted {result.items_submitted} chunks via {result.method}: {result.status}")
7070
for warning in result.warnings:
7171
print(f"WARNING: {warning}")

ingestion/examples/email_example.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ def main() -> None:
4949
)
5050
# Submission returns immediately; blocking is opt-in. Bind the result first
5151
# so a wait() timeout still leaves you the ids below to resume from.
52-
result.wait(timeout=600)
52+
result.wait()
5353
print(f"Submitted {result.items_submitted} emails via {result.method}: {result.status}")
5454
for warning in result.warnings:
5555
print(f"WARNING: {warning}")

ingestion/examples/fact_triples_example.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ def main() -> None:
157157
# already happened at FactTriple construction — before any API call.
158158
triples = load_triples()
159159
result = ingest_fact_triples(client, triples, graph_id=graph_id)
160-
result.wait(timeout=600)
160+
result.wait()
161161
result.raise_for_status()
162162
print(f"Molded org_chart.json into {len(triples)} fact triples: {result.status}")
163163

ingestion/examples/json_records_example.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ def main() -> None:
5252
)
5353
# Submission returns immediately; blocking is opt-in. Bind the result first
5454
# so a wait() timeout still leaves you the ids below to resume from.
55-
result.wait(timeout=600)
55+
result.wait()
5656
print(f"Submitted {result.items_submitted} records via {result.method}: {result.status}")
5757
for warning in result.warnings:
5858
print(f"WARNING: {warning}")

ingestion/examples/slack_export_example.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ def main() -> None:
7272
)
7373
# Submission returns immediately; blocking is opt-in. Bind the result first
7474
# so a wait() timeout still leaves you the ids below to resume from.
75-
result.wait(timeout=600)
75+
result.wait()
7676
print(f"Submitted {result.items_submitted} episodes via {result.method}: {result.status}")
7777
for warning in result.warnings:
7878
print(f"WARNING: {warning}")

ingestion/examples/user_graph_example.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ def main() -> None:
8585
),
8686
]
8787
profile_result = ingest_fact_triples(client, profile, user_id=user_id)
88-
profile_result.wait(timeout=600)
88+
profile_result.wait()
8989
profile_result.raise_for_status()
9090
print(f"Seeded {len(profile)} profile facts")
9191

@@ -108,7 +108,7 @@ def main() -> None:
108108
user_id=user_id,
109109
created_at="2025-06-20T00:00:00Z", # generated source date
110110
)
111-
docs.wait(timeout=600)
111+
docs.wait()
112112
print(f"Ingested {docs.items_submitted} document chunks: {docs.status}")
113113

114114
# Extraction is asynchronous; wait until facts are searchable, then pull

ingestion/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "zep-ingest"
3-
version = "0.2.1"
3+
version = "0.3.0"
44
description = "Bulk data ingestion pipeline for Zep: chunk, contextualize, canonicalize, and submit unstructured and structured data"
55
readme = "README.md"
66
requires-python = ">=3.11"

0 commit comments

Comments
 (0)