Source Content Ingestion Pipeline in Open Notebook: How Files and URLs Are Extracted and Vectorized
Open Notebook processes uploaded files and URLs through an asynchronous LangGraph workflow that extracts content using content_core, persists records to SurrealDB, and optionally generates searchable embeddings via a background job system with automatic chunking and provider-agnostic AI integration.
Open Notebook is an open-source knowledge management system that transforms documents and web content into vectorized knowledge. The source content ingestion pipeline handles MIME type detection, OCR, content extraction, and embedding generation through a modular architecture built on FastAPI, LangGraph, and SurrealDB.
How the Pipeline Works: A 7-Step Overview
The ingestion flow moves through distinct architectural layers, from HTTP upload to vector storage:
- API Reception:
api/routers/sources.pyreceives multipart/form-data or JSON requests - Graph Execution:
open_notebook/graphs/source.pyorchestrates the workflow - Content Extraction:
content_coreperforms MIME type detection and text extraction - Error Handling: Failed extractions raise
ValueErrorfor retry logic - Persistence:
Sourcerecords are saved to SurrealDB via thesave_sourcenode - Job Queueing:
Source.vectorize()submitsembed_sourcecommands asynchronously - Embedding Generation:
commands/embedding_commands.pychunks text and generates vectors
Step 1: API Reception and File Handling
Incoming requests hit the FastAPI endpoint in api/routers/sources.py, which delegates to api/sources_service.py. The service handles file uploads by storing binaries in UPLOADS_FOLDER before constructing a SourceCreate model.
# POST /api/sources (api/routers/sources.py)
source_data, upload_file = parse_source_form_data(...)
if upload_file:
file_path = await save_uploaded_file(upload_file)
source_data.file_path = file_path
result = SourcesService().create_source(**source_data.dict())
The endpoint automatically converts form strings to booleans (e.g., embed, async_processing) and validates JSON fields like notebooks and transformations.
Step 2: The LangGraph Source Workflow
The core orchestration happens in open_notebook/graphs/source.py using LangGraph's StateGraph. The workflow compiles three primary nodes with conditional edges:
# open_notebook/graphs/source.py
workflow = StateGraph(SourceState)
workflow.add_node("content_process", content_process) # extraction
workflow.add_node("save_source", save_source) # persistence
workflow.add_node("transform_content", transform_content) # optional
workflow.add_edge(START, "content_process")
workflow.add_edge("content_process", "save_source")
workflow.add_conditional_edges("save_source", trigger_transformations,
["transform_content"])
workflow.add_edge("transform_content", END)
source_graph = workflow.compile()
The graph executes asynchronously via await source_graph.ainvoke(state), ensuring non-blocking processing of large files.
Step 3: Content Extraction with content_core
The content_process node delegates to content_core.extract_content, which detects MIME types and applies appropriate extractors:
- PDFs: OCR and text extraction
- YouTube URLs: Subtitle fetching
- Web pages: HTML body parsing
# content_process node implementation
content_state = state["content_state"]
content_state["url_engine"] = content_settings.default_content_processing_engine_url or "auto"
content_state["document_engine"] = content_settings.default_content_processing_engine_doc or "auto"
content_state["output_format"] = "markdown"
processed_state = await extract_content(content_state) # Returns ProcessSourceState
The result populates a ProcessSourceState object containing title, content, url, and file_path.
Step 4: Error Handling and Validation
If extraction fails, content_core returns a title of "Error" with content prefixed by "Failed to extract content:". The pipeline converts this soft failure into a hard exception to enable retry logic:
if processed_state.title == "Error" and processed_state.content.startswith("Failed to extract content:"):
raise ValueError("Could not extract content from this source …")
This ensures the job is marked failed in the UI rather than silently storing error strings as content.
Step 5: Persisting Sources to SurrealDB
The save_source node populates the Source domain model and persists it via source.save():
source.asset = Asset(url=processed_state.url,
file_path=processed_state.file_path)
source.full_text = processed_state.content
if processed_state.title and (not source.title or source.title == "Processing..."):
source.title = processed_state.title
await source.save()
The Source class in open_notebook/domain/notebook.py handles SurrealDB serialization and timestamp hooks.
Step 6: Asynchronous Vectorization
When embed=True is passed during creation, the Source.vectorize() method queues a background job:
# open_notebook/domain/notebook.py
command_id = submit_command(
"open_notebook", "embed_source", {"source_id": str(self.id)}
)
return str(command_id)
This fire-and-forget pattern prevents HTTP worker thread blocking by delegating to the surreal_commands queue.
Step 7: Embedding Generation and Chunking
The embed_source command in commands/embedding_commands.py implements the vectorization logic with exponential backoff retries:
@command("embed_source", app="open_notebook", retry={…})
async def embed_source_command(input: EmbedSourceInput) -> EmbedSourceOutput:
source = await Source.get(input.source_id)
# 1. Detect content type → chunk_text()
# 2. Generate embeddings → generate_embeddings()
# 3. Bulk UPSERT into source_embedding table
The pipeline uses open_notebook/utils/chunking.py to detect content type and split text appropriately, while open_notebook/utils/embedding.py provides provider-agnostic embedding generation via the Esperanto library.
Practical Usage Examples
Uploading a File via cURL
Submit a PDF for ingestion with automatic embedding:
curl -X POST http://localhost:5055/sources \
-F type=file \
-F notebook_id=nb123 \
-F file=@/path/to/document.pdf \
-F embed=true
The response includes the source ID and a command_id for polling embedding status.
Ingesting a URL via JSON
Create a source from a web article:
curl -X POST http://localhost:5055/sources \
-H "Content-Type: application/json" \
-d '{
"type": "link",
"url": "https://example.com/article.html",
"embed": true,
"title": "Interesting article"
}'
Checking Embedding Status
Poll the command endpoint to track asynchronous processing:
curl http://localhost:5055/commands/abcd-1234
# Response: {"status":"completed","result":{…}}
Programmatic Vectorization with Python
Manually trigger embedding for an existing source:
from open_notebook.domain.notebook import Source
source = await Source.get("source-id-xyz")
job_id = await source.vectorize() # Returns command ID
print(f"Embedding job submitted, ID={job_id}")
Summary
- Entry Point:
api/routers/sources.pyhandles multipart uploads and JSON payloads, storing files inUPLOADS_FOLDER - Orchestration:
open_notebook/graphs/source.pyruns a LangGraph workflow with extraction, persistence, and optional transformation nodes - Extraction:
content_coreperforms MIME type detection, OCR, and content extraction, returning structuredProcessSourceState - Error Handling: Failures raise
ValueErrorwhen titles equal"Error"with specific prefixes, enabling retry logic - Persistence:
Sourcemodels inopen_notebook/domain/notebook.pysave to SurrealDB with asset metadata and full text - Vectorization: Asynchronous
embed_sourcecommands handle chunking viaopen_notebook/utils/chunking.pyand embedding viaopen_notebook/utils/embedding.py
Frequently Asked Questions
How does Open Notebook handle PDF extraction and OCR?
The pipeline delegates to content_core.extract_content, which automatically detects MIME types and applies OCR when processing PDF documents. This occurs in the content_process node of open_notebook/graphs/source.py, where the document_engine parameter defaults to "auto" to select the appropriate extraction strategy.
What happens if content extraction fails?
When content_core cannot extract text, it returns a ProcessSourceState with title="Error" and content prefixed by "Failed to extract content:". The content_process node detects this pattern and raises a ValueError, causing the LangGraph workflow to fail visibly. This allows the UI to surface the error and enable retry mechanisms rather than storing corrupted data.
Can I ingest content without automatically generating embeddings?
Yes. The embed parameter is optional in both the REST API and Python SDK. When embed=false (or omitted), the pipeline stops after persisting the Source record. You can later trigger vectorization manually by calling source.vectorize() on the Source instance, which submits an embed_source command to the background queue.
How does the embedding pipeline handle large documents?
The embed_source command in commands/embedding_commands.py uses detect_content_type and chunk_text from open_notebook/utils/chunking.py to split documents into appropriate segments before embedding. It then calls generate_embeddings from open_notebook/utils/embedding.py to process chunks through the configured AI provider, performing bulk upserts into the source_embedding table with exponential jitter retries for transient failures.
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 →