Skip to main content
Version: 0.0.4

Ingest documents

Add files, directories, raw text, or prebuilt Document objects to a Graph with Graph.add. This guide covers the ingestion entrypoint only. See the API Reference for every parameter.

Install​

Install the core package:

uv pip install agentic-graphrag

For PDF, DOCX, PPTX, image, and XML support, install the docling extra:

uv pip install "agentic-graphrag[docling]"

Add a single file​

Open a graph and add one file by path. Graph.open is async and needs no external service.

import asyncio
from agrag.ingestion import Graph

async def main():
graph = await Graph.open()
result = await graph.add(source="path/to/file.pdf")
print(result.ingestion.documents) # number of documents ingested

asyncio.run(main())

Every later snippet on this page continues inside that same async def main(): ... body, using the graph opened above, and still runs through asyncio.run(main()).

Add a directory, a glob, or a list of paths the same way:

result = await graph.add(source="./corpus") # every supported file
result = await graph.add(source="./corpus/**/*.md") # glob pattern
result = await graph.add(source=["a.txt", "b.pdf"]) # explicit list

Graph.add accepts exactly one of source, text, or documents.

Add raw text​

Pass a string to ingest prose as a single document:

result = await graph.add(text="Raw prose to index as one document.")

Add prebuilt documents​

Pass Document objects you already built (for example, documents returned by a loader or reconstructed from storage) to documents. A Document carries its source metadata, so every field is required:

from agrag.ingestion import Graph
from agrag.common.data_models.document import (
Document,
DocumentFamily,
SourceFormat,
)

text = "Prebuilt document text."
doc = Document(
text=text,
title="Prebuilt",
uri="memory://prebuilt",
source_format=SourceFormat.TXT,
family=DocumentFamily.PROSE,
content_hash="replace-with-a-real-sha256",
loader_name="text",
char_count=len(text),
)
result = await graph.add(documents=[doc])

For most callers, source= or text= is simpler; reach for documents= when you already hold Document objects.

Choose how documents are chunked​

Graph.open takes a chunking argument that decides which chunker splits each document. Loaders also normalize the text of each file: they remove the byte-order mark, change line endings to LF, and apply NFKC. Pass read_options=ReadOptions(normalization=...) to Graph.add to change this. PDF and Office documents chunk at up to 1024 tokens by default. See Configure chunking for both.

Add a chat file​

ChatLoader reads a file of chat messages as one document. Use .jsonl or .ndjson with one JSON object per line, or .json with an array of objects. Each message needs a non-empty string role and a string content. It can also have an id, which must be unique in the file.

{"role": "user", "content": "Hi there", "id": 1}
{"role": "assistant", "content": "Hello! How can I help?", "id": 2}

The document text has one [role] content block for each message, with a blank line between blocks. Document.turns records the role, the id and the character span of each block. Pass the loader for one file:

from agrag.loaders.corpus import ChatLoader

result = await graph.add(source="session.jsonl", loader=ChatLoader())

To cut the document between turns, use TurnWindowChunker as Configure chunking shows.

Handle errors per source​

Use error_policy to control what happens when a source fails to load. The policy applies per source, not per run, so one bad file does not stop the rest.

from agrag.ingestion import Graph
from agrag.loaders.corpus.types import ErrorPolicy

result = await graph.add(source="./corpus", error_policy=ErrorPolicy.SKIP)
print(result.ingestion.skipped, result.ingestion.quarantined)
  • ErrorPolicy.RAISE — stop and raise on the first failure (default).
  • ErrorPolicy.SKIP — drop the failing source and continue.
  • ErrorPolicy.QUARANTINE — isolate the failing source and continue.

Read the outcome from the returned AddResult:

print(result.ingestion.documents) # count of documents ingested
print(result.ingestion.sources) # count of sources processed
print(result.ingestion.skipped) # count of skipped sources
print(result.ingestion.quarantined) # count of quarantined sources
print(result.ingestion.quarantined_items) # one StageFailure per quarantined source

An unknown format raises UnsupportedFormatError; a format that needs docling without the extra installed raises MissingExtraError. With SKIP or QUARANTINE, the policy handles those sources instead of stopping the run.

Track progress​

Pass on_progress to receive a LoadStats callback after each batch. Use it to log or stream status without blocking ingestion:

from agrag.ingestion import Graph
from agrag.loaders.corpus.types import LoadStats

def report(stats: LoadStats) -> None:
print(f"loaded {stats.documents} documents from {stats.sources} sources so far")

result = await graph.add(source="./corpus", on_progress=report)

Update or delete a document​

Use Graph.update to ingest new content under the same stable document_key. If the content hash and the chunker settings are unchanged, the operation does not re-chunk or rewrite the document.

result = await graph.update("memory://manual", text="Revised document text")
print(result.no_op)
print(result.chunks_closed)

Use Graph.delete_document to remove a document from current retrieval without deleting its chunks or extracted entities. The graph keeps those records for provenance.

result = await graph.delete_document("memory://manual")
print(result.chunks_closed)

Crash safety​

add, update, and delete_document each run as a Cutover Job. Every node, edge, and vector a call writes is tagged with that job's id, and every retrieval path excludes tagged writes, so an in-flight call is never visible to a search. One transaction then flips the job to committed, closes the superseded version's PART_OF edges, and clears the tags; deletion-triggered pruning runs after that, scoped to the entities the replaced version mentioned.

If the process dies mid-call, the next Graph.open() recovers automatically: a job that died before its commit is rolled back, deleting its tagged writes, and a job that died after committing is rolled forward. Either way the graph converges to as-if-never-happened or as-if-completed. A document's job holds a lease that expires after CUTOVER_JOB_LEASE_TTL_SECONDS seconds (60 by default). A running worker renews its lease, so no other worker takes over a live job, however long it runs. If the worker stops, another worker takes the document over after the lease expires.

Supported formats​

Loaders are chosen by file extension. Core loaders are always available; docling loaders require the docling extra.

FormatExtension(s)Loader
Plain text.txt, .logcore
Markdown.md, .markdowncore
AsciiDoc.adoc, .asciidocdocling, falls back to core (regex)
HTML.html, .htmcore
CSV / TSV.csv, .tsvcore
JSON Lines.jsonl, .ndjsoncore
JSON.jsoncore
PDF.pdfdocling
Word.docxdocling
PowerPoint.pptxdocling
Images.png, .jpg, .jpeg, .tif, .tiff, .bmpdocling
XML.xmldocling

For .md, .html, and .csv, docling registers a loader but the core loader wins by default. For .adoc and .asciidoc, docling takes precedence instead: docling's structural parser wins when the docling extra is installed, and the core regex-based reader is the fallback when it is not. To customize per-source reading (encoding, size limits, column mapping, and more), build Document objects through the lower-level loaders with ReadOptions from agrag.loaders.corpus.types.

Next steps​

See the API Reference for every parameter.