Skip to content

feat(transfer): export/import documents between banks without re-running the LLM - #1909

Merged
nicoloboschi merged 4 commits into
mainfrom
feat/document-transfer
Jun 3, 2026
Merged

nicoloboschi merged 4 commits into
mainfrom
feat/document-transfer

Conversation

@nicoloboschi

Copy link
Copy Markdown
Collaborator

What

Adds a document transfer feature: export a bank's already-extracted facts to a ZIP archive and import them into another bank (or instance) without re-running the LLM.

Since fact extraction is the expensive, non-deterministic part of ingestion, transferring already-extracted facts is fast, cheap, and reproducible.

Two target use cases:

  1. Test a different embedding model — export, then import into a bank configured with another embedding model (facts/entities/links preserved, vectors regenerated).
  2. Move data between banks/instances for free — no LLM cost (only local re-embedding).

How it works

  • Export dumps documents, chunks, and world/experience facts (text, timestamps, context, tags, metadata, observation scopes), each fact's entities by canonical name, and causal links (as ordinals). No embeddings, no DB ids.
  • Import replays the deterministic half of the retain pipeline: rebuild facts → re-embed locally with the target bank's model → entity resolution → fact/link insert. No LLM extraction runs. Links/entities are computed against the target bank's existing memories, so imported docs are integrated, not cloned byte-for-byte.
  • Consolidated observations are excluded (derived data — regenerated by consolidation in the target bank).
  • on_conflict: skip (default) / replace / new-id for existing document ids.

Surfaces

  • Engine: hindsight_api/engine/transfer/ (schema, export, importer) + MemoryEngine.export_documents_async / import_documents_async.
  • Admin CLI: hindsight-admin export-documents / import-documents.
  • HTTP API: GET/POST /v1/default/banks/{bank_id}/document-transfer (export streams a zip; import is a multipart upload).
  • Feature flags (default on, gated independently, enforced server-side):
    • HINDSIGHT_API_ENABLE_DOCUMENT_EXPORT_API
    • HINDSIGHT_API_ENABLE_DOCUMENT_IMPORT_API
    • Surfaced via /version features.document_export_api / document_import_api.
  • Control plane Documents page: Export All / Import (zip upload) toolbar actions + per-document Export, all hidden when the backend disables the feature.

Tests

tests/test_document_transfer.py:

  • Export→import round trip that asserts fact extraction is never called (LLM-free).
  • HTTP endpoint round trip + 404 for unknown bank.
  • on_conflict modes (skip / replace / new-id).
  • Endpoints return 404 + /version reports disabled when flags are off.
  • Rejects unsupported archive schema version / invalid on_conflict.

Docs / generated

  • New developer/document-transfer.md (+ sidebar), admin-cli and configuration references updated.
  • Regenerated OpenAPI spec and all client SDKs.

🤖 Generated with Claude Code

…ing the LLM

Export a bank's already-extracted facts (text, entity canonical names, causal
links, chunks) to a ZIP archive, and import them into another bank by replaying
the deterministic half of the retain pipeline — re-embedding locally with the
target bank's model and re-resolving entities. No LLM fact extraction runs on
import. Consolidated observations are excluded (regenerated by consolidation in
the target bank).

Two use cases: testing a different embedding model, and moving data between
banks/instances without LLM cost.

- engine/transfer/: schema, export, importer (LLM-free replay)
- MemoryEngine.export_documents_async / import_documents_async
- Admin CLI: export-documents / import-documents
- HTTP API: GET/POST /v1/default/banks/{bank_id}/document-transfer
- Gated by HINDSIGHT_API_ENABLE_DOCUMENT_EXPORT_API / _IMPORT_API
  (default on), surfaced via /version features for the control plane
- Control plane Documents page: Export All / Import (zip upload) +
  per-document Export, hidden when the backend disables the feature
- Tests, docs, regenerated OpenAPI spec and client SDKs
… import

Imported documents were second-class citizens: unlike a normal retain, an
import fired no retain.completed webhooks and never enqueued consolidation
or graph maintenance, so imported facts never produced observations.

Thread an outbox callback factory through import_documents -> _import_one_document
so each imported document fires its retain.completed webhook transactionally
inside its own insert. After the import completes, submit async consolidation
(when observations + auto-consolidation are enabled) and graph maintenance,
mirroring the post-retain side effects.
…in and import

The consolidation + graph-maintenance triggers added for import duplicated the
retain post-processing block verbatim. Extract it into
_submit_post_insert_maintenance and call it from both the retain pipeline and
the import pipeline, so the two paths stay in lockstep.
Import now fires the post-retain extension hook (usage tracking / metrics /
notifications) once per imported document, mirroring retain — so imported
facts are first-class for extensions. Token counts are zero and
processed_content_tokens is 0 (import runs no LLM extraction), so cost-metering
extensions correctly bill an import as free.

The importer returns per-document outcomes (ImportedDocument) so the engine can
build the RetainResult; these are not serialized into the operation's
result_metadata (the worker still writes counts only).

Tests: assert the hook fires once per document with zero tokens, and that
import queues a retain.completed webhook delivery per document.
@nicoloboschi
nicoloboschi force-pushed the feat/document-transfer branch from 51452be to bbbac16 Compare June 3, 2026 14:04
@nicoloboschi
nicoloboschi merged commit 1d6d73b into main Jun 3, 2026
74 of 79 checks passed
@nicoloboschi
nicoloboschi deleted the feat/document-transfer branch June 3, 2026 14:24
cdbartholomew added a commit that referenced this pull request Oct 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant