How the Source Ingestion Pipeline Extracts Content and Generates Vector Embeddings in Open Notebook
Open Notebook processes raw inputs through a three-stage LangGraph workflow that extracts content using content-core, persists data to the database, and asynchronously generates vector embeddings via a background job queue with automatic retry logic.
The source ingestion pipeline in the lfnovo/open-notebook repository transforms URLs, uploaded files, and plain text into searchable vector embeddings. This architecture decouples content extraction from embedding generation to maintain API responsiveness while ensuring reliable processing through asynchronous job queues.
The Three-Stage Pipeline Architecture
The pipeline is implemented as a LangGraph workflow defined in [open_notebook/graphs/source.py](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/graphs/source.py). The source_graph wires together three distinct nodes that handle extraction, persistence, and optional embedding generation.
Stage 1: Content Extraction with content-core
The content_process node handles the initial ingestion of raw input. It accepts a ProcessSourceState object containing a URL, file path, or plain text, then delegates the heavy lifting to content-core.
processed_state = await extract_content(content_state)
This call converts documents to markdown format and returns structured metadata. The node supports multiple input types and normalizes them into a consistent internal representation before passing control to the next stage.
Stage 2: Persisting Source Metadata
The save_source node receives the processed state and writes the extracted data back to the Source record. According to the source code in open_notebook/graphs/source.py, this node performs three critical operations:
- Stores the asset reference (
urlorfile_path) in theSource.assetfield - Saves the full extracted text to
source.full_text - Updates the source title if present in the metadata
source.asset = Asset(url=content_state.url, file_path=content_state.file_path)
source.full_text = content_state.content
await source.save()
Stage 3: Asynchronous Vector Embedding Generation
When the ingestion request includes embed=True, the save_source node triggers the embedding phase. Rather than processing inline, it calls source.vectorize() which submits an asynchronous job to the command queue.
The Source.vectorize method in [open_notebook/domain/notebook.py](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/domain/notebook.py) submits an embed_source command:
command_id = submit_command(
"open_notebook",
"embed_source",
{"source_id": str(self.id)},
)
This approach returns immediately to the API caller while the heavy computational work happens in the background.
Inside the Embedding Command
The actual embedding work is performed by embed_source_command in [commands/embedding_commands.py](https://github.com/lfnovo/open-notebook/blob/main/commands/embedding_commands.py). This command handler implements a robust six-step workflow with automatic retry logic (up to five attempts for transient failures).
The process follows these steps:
- Load the source using
Source.getto retrieve the full text and metadata - Delete existing embeddings for idempotency, ensuring no duplicate vectors
- Detect content type via
detect_content_typeto inform chunking strategy - Chunk the text using
chunk_textwith content-type-aware splitting - Generate embeddings in batches via
generate_embeddingsto optimize API calls - Bulk-insert the resulting vectors as
source_embeddingrows
The command is decorated with a retry policy that handles network timeouts and transient failures automatically.
Triggering the Pipeline via API
The API entry point in [api/routers/sources.py](https://github.com/lfnovo/open-notebook/blob/main/api/routers/sources.py) exposes the pipeline through the POST /sources endpoint. The handler constructs a content_state dictionary and supports two execution modes:
Synchronous mode executes process_source immediately using execute_command_sync, blocking until extraction completes.
Asynchronous mode creates the Source record first, then enqueues the process_source command via CommandService.submit_command_job, returning a command_id for tracking.
Both paths execute the same LangGraph graph, guaranteeing identical extraction and embedding behavior regardless of invocation method.
Creating a Source with Embedding
import requests
payload = {
"type": "link",
"url": "https://example.com/article",
"title": "Example article",
"embed": True,
"notebooks": ["notebook-123"]
}
resp = requests.post(
"http://localhost:5055/sources",
json=payload,
headers={"Content-Type": "application/json"}
)
print(resp.json())
# Returns source ID and command ID for tracking
Tracking Embedding Progress
command_id = resp.json()["command_id"]
status_resp = requests.get(f"http://localhost:5055/commands/{command_id}")
print(status_resp.json())
# Status transitions: "running" → "completed"
Direct Command Invocation
from commands.embedding_commands import embed_source_command, EmbedSourceInput
result = await embed_source_command(
EmbedSourceInput(source_id="source-abc")
)
print(result.success, result.chunks_created)
Summary
- Content extraction happens in the
content_processnode usingcontent-coreto convert inputs to markdown - Persistence occurs in
save_source, which stores full text and metadata in theSourcemodel - Vector embedding is handled asynchronously via
Source.vectorize()submitting to theembed_source_commandqueue - The embedding command performs content-type-aware chunking, batch embedding generation, and bulk insertion with automatic retry logic
- The API supports both sync and async invocation modes through
api/routers/sources.py
Frequently Asked Questions
How does the pipeline handle different content types like PDFs versus web pages?
The content_process node delegates to content-core for extraction, which handles multiple formats. During embedding, the detect_content_type function in commands/embedding_commands.py determines the optimal chunking strategy, ensuring code files split differently than prose documents.
What happens if the embedding service fails temporarily?
The embed_source_command includes a retry decorator that automatically retries transient failures up to five times. This handles network timeouts and temporary service outages without manual intervention, ensuring reliable processing of large document batches.
Can I ingest content without generating embeddings immediately?
Yes. When creating a source via the API, omit the embed parameter or set it to False. The pipeline will extract and save the content but skip the source.vectorize() call. You can trigger embedding later by calling the vectorize method directly or re-submitting the embed command.
How does the pipeline maintain data consistency between the source and its embeddings?
The embedding command first deletes all existing source_embedding rows for the given source ID before generating new chunks. This idempotent approach ensures that re-processing a source always results in a clean state, preventing orphaned vectors or duplicate embeddings when content updates.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →