graphrag_sdk.
Table of Contents
- GraphRAG (Facade)
- Connection
- Providers
- Data Models
- Schema
- Ingestion Strategies
- Ingestion Pipeline
- Structured Ingestion
- Retrieval Strategies
- Reranking Strategies
- Storage
- Context
- Exceptions
GraphRAG (Facade)
The main entry point. Three primary operations:ingest(), retrieve(), and completion(). Tables go through the same ingest(); see Structured Ingestion.
Constructor
Public attributes:
llm, embedder, schema, graph_store, vector_store
ingest()
.csv, .tsv, .psv or .tab is read as records, not prose: its mapping is looked up in the ontology by filename (see Structured Ingestion), no model is called, and a StructuredIngestionResult is returned. A table may not appear in a list — each is written on its own — and chunker/extractor/resolver are rejected for one; identity across sources is judged by finalize(). Pass loader=TextLoader() to read a CSV as prose anyway.
Returns:
IngestionResult for a single document, StructuredIngestionResult for a table, list[IngestionResult | Exception] (aligned by index) for multiple sources.
retrieve()
Returns:
RetrieverResult
completion()
history is provided, messages are passed natively to the LLM provider’s multi-turn chat API.
Returns:
RagResult
Conversation history:
History accepts a list of ChatMessage objects or plain dicts with role and content keys. Supported roles: "system", "user", "assistant". Invalid roles raise ValueError.
completion() builds a native messages list: [system_prompt, *history, user_question] and calls LLMInterface.ainvoke_messages(). Without history, it uses the single-turn ainvoke() path.
query()
params ($name) for values, never string formatting.
drop_table()
entity_key, is_stub — unless another table still keys the node), and its stored mapping, so the next ingest() of that filename is proposed a mapping afresh. The label stays in the ontology; drop_entity() removes a label nothing uses. source is matched on its basename, like ingest().
Raises: ValueError when no table of that name is in the ontology.
deduplicate_entities()
judge is any DedupJudge, and None is LLMJudge with the instance’s llm. Anything else — judge=False among them — raises TypeError before any work.
It is incremental: only the pending entities are checked — those without a name vector (ingest writes entities without one, and marks nothing on them) and those a pending system record lists — against the whole graph, through the __Entity__ vector index, never by reading every entity. A run lists an entity before it embeds it and clears it once its check completes, so a second call with nothing pending does no work. An entity whose check did not complete — a failed lookup or judge call, an answer that was not a clean partition, a group held for one, a merge an update() changed — stays pending for the next run; after 3 judged runs without a usable verdict it is unresolved: reported, left out of every set, never merged automatically. dedup_all=True checks every entity instead, and retries the unresolved ones. It is crash-safe: every merge is decided before any is written, written as a sealed plan, and applied one atomic query per merge, each ticking itself off, so an interrupted call is finished by the next one. One call per graph at a time (a second raises DedupRunActiveError); update() and deletes may run meanwhile. Like every SDK connection, it refuses a FalkorDB server older than 4.20 at its first query (FalkorDBVersionError).
- Phase 1 (always): Canonical name match between the pending entities and their nearest names from the vector index — case, accents, punctuation, inner dots (
A.I.=AI), a leading English article,Surname, Giveninversion, common abbreviations and one trailing legal form (Ltd,Inc) are normalised; word order is not. An acronym joins its unique same-label long form. A group holding a table row merges here, by rule; a group of extracted mentions only goes to the judge (Phase 2): the same spelling is evidence, not proof. Keeps the survivor ranked by declared key, real-over-placeholder, long form over acronym, degree, description length, then name length; merges the duplicate’s description (" | "-joined, members kept indescriptions),aliases,source_chunk_idsand any property the survivor lacks into it, and moves every relationship of every type to it, in the same query that deletes the duplicate. - Phase 2: judged cross-document dedup — each pending entity’s 5 nearest names from the vector index (cosine ≥ 0.5) and Phase 1’s same-spelling mention pairs and ambiguous acronym/long-form pairs nominate candidates, dense sets of ≤ 8 (more than half of a set’s pairs must be nominated, so a chain A–B–C–D cannot form one set; agreements in separate sets still chain, see below), and the judge partitions each set once, reading names, labels, existing aliases, descriptions and the values of the attributes the ontology declares, and names each merged group. Agreed pairs merge by the same rules as Phase 1 (the survivor additionally gains every member’s label, recorded in
merged_labels, and takes the judge’s name when it is one of the members’ names or aliases, except a table row, which keeps its declared name); agreements chain across sets, but never into a group holding a pair the judge kept apart. A “different” answer writes nothing. Every judge’s answer is checked; a set without a usable answer is asked again (up to 2 more times;LLMJudgeasks it on its own); a group such a set could still change is held, not merged. Two keyed rows of a table, and a mention two such rows could own, are never asked. Descriptions are not embedded. ForLLMJudgea gpt-4.1-class model is recommended. A run with nothing pending asks the judge nothing. Stats in_deduplicator.last_judge_stats.
finalize()
- Remove NULL-name stub entities left by legacy writes (only nodes no mapping wrote)
deduplicate_entities(judge=..., dedup_all=...)— incremental and crash-safe: the pending entities (added since the last dedup run, or left pending by it) get canonical-name and acronym dedup, then one judge pass over their nearest names. Dedup writes every entity’s name embedding, listing the entity first;entities_embeddedcounts themembed_relationships()— fact text embeddings on RELATES edgesensure_indices()— all indexes- A consistency report over the whole graph
Returns:
FinalizeResult — counts entities_deduplicated, judge_calls, judge_stats, dedup_pending (entities still owed a check), dedup_unresolved (entities no longer retried, with the reason), dedup_merges_changed (planned merges skipped because an update() changed their entities), dedup_collisions (merges whose entities held different values for one property: which value stayed, by the judge’s choice or the survivor’s, and which went to <property>__dropped), descriptions_summarised (entities whose descriptions were replaced by one LLM summary), description_summary_failures (entities whose summary failed, with the error, or the failed pass; tried again by the next run), entities_embedded, relationships_embedded, null_stubs_removed, indexes, and the report: probable_duplicates (spellings the canonical rule declined to merge and the judge did not, plus mentions two keyed rows could own, with the reason; pairs involving the entities this run checked), unmerged_name_collisions (a name under more than one label), property_conflicts (a property two tables both supply, with how many entities hold differing values; every value is kept, none resolved for you), unresolved_references (placeholders whose owning table has not arrived), entities_without_a_name, proposed_mappings (tables loaded on a proposed, not declared, mapping), mapping_changed, stale_signed_properties. Everything non-empty is also logged at WARNING.
Sync Wrappers
asyncio.run().
Connection
ConnectionConfig
FalkorDBConnection
Providers
LLMInterface (ABC)
ainvoke_messages() is used by completion() when conversation history is provided. The default implementation concatenates messages into a single prompt string and calls ainvoke(), so custom providers work without changes. LiteLLM and OpenRouterLLM override this with native multi-turn implementations.
Embedder (ABC)
LLMBatchItem
LiteLLM
LiteLLMEmbedder
OpenRouterLLM
OpenRouterEmbedder
Data Models
All models extendDataModel (Pydantic BaseModel with extra="allow").
GraphNode
GraphRelationship
GraphData
TextChunk
TextChunks
DocumentInfo
DocumentOutput
IngestionResult
RagResult
RetrieverResult
RetrieverResultItem
ResolutionResult
ChatMessage
completion(history=...) and LLMInterface.ainvoke_messages(). Invalid roles raise a validation error on construction.
LLMMessage is a backward-compatible alias for ChatMessage.
LLMResponse
SearchType
Extraction Models
compute_entity_id()
entity_type is provided, appends a __type suffix to prevent cross-type collisions (e.g. paris__person vs paris__location). Without entity_type, returns just the normalized name for backwards compatibility.
Schema
EntityType
RelationType
PropertyType
GraphSchema
Ingestion Strategies
LoaderStrategy (ABC)
TextLoader(encoding="utf-8"), PdfLoader()
ChunkingStrategy (ABC)
FixedSizeChunking(chunk_size=1000, chunk_overlap=100)
ExtractionStrategy (ABC)
GraphExtraction(llm, *, entity_extractor=None, coref_resolver=None, entity_types=None, relation_types=None, max_concurrency=None)—entity_types=NoneselectsDEFAULT_ENTITY_TYPES;relation_types=NoneselectsDEFAULT_RELATION_TYPES, a 31-label UPPER_SNAKE_CASE vocabulary (LOCATED_IN,PART_OF,EMPLOYED_AT,AUTHORED, …) the step-2 prompt asks the LLM to prefer; off-list labels are still kept. Passrelation_types=[]for open-vocabulary labels. Both are overridden by a declaredOntology, whose relations are enforced.
GraphExtraction):
GLiNERExtractor(threshold=None, model_name=None, window_tokens=None, window_overlap=48, candidate_threshold=<25 % below threshold>)— default, local NER.model_name=Noneselectsurchade/gliner_medium-v2.1;threshold=Noneselects the measured per-model value (0.75 for that model). Spans scoring betweencandidate_thresholdandthresholdare returned as"Unknown"; passcandidate_threshold=Noneto disable. Chunks longer thanwindow_tokens(derived from the model’smax_lenwhenNone) are processed as overlapping windows.LLMExtractor(llm, threshold=0.75)— LLM-based NER- Subclass
EntityExtractorfor custom backends
ResolutionStrategy (ABC)
ExactMatchResolution(llm=None, resolve_property="name", cross_label_merge=True)—ingest()’s default isExactMatchResolution(llm=None, cross_label_merge=False)LLMVerifiedResolution(llm=None, embedder=None, *, hard_threshold=0.95, soft_threshold=0.65, max_llm_pairs=500, max_llm_concurrency=None, force_summary_threshold=3, max_summary_tokens=500, ann_top_k=50, batch_verification=True, verification_token_budget=4000, verification_max_pairs_per_call=5, cross_label_merge=True, cross_label_vector_door=0.60, cross_label_rank_floor=0.55, cross_label_max_pairs=200, cross_label_min_descriptions=3, unified_stage=True, unified_threshold=0.65, label_family_gate=True, cross_label_vote=True)— opt-in atingest(), per document; cross-document dedup isfinalize()’s judge
Ingestion Pipeline
Structured Ingestion
ingest(). No model is involved in writing it: identity comes from a declared key, every property type is declared, and the same file always produces the same graph. A row and a prose mention of the same thing share one node from the first write. The full guide is Structured Ingestion; the runnable version is examples/11_structured_ingestion.py.
TableMapping
MappingError is raised for a declaration that does not fit — an unknown column, a cell that does not parse as its type, a label the sanitiser would rewrite — and the graph is left untouched.
Column
COLUMN_TYPES: STRING, INTEGER, FLOAT, BOOLEAN, DATE, LIST. Types are declared, never sniffed. INTEGER and FLOAT accept 1,234.56 and 1.234,56; FLOAT refuses nan/inf; LIST is parsed as a CSV row. A blank cell writes no property.
Link
RELATES edge with rel_type=type, like every data edge. The target is keyed, never named: a missing target is created as a placeholder (is_stub: true) that is renamed in place — edges intact — when the owning table arrives; an existing target, another table’s row or an entity a document mentioned, only gains the key. A link can therefore never overwrite what the owning source wrote.
Record loaders
CsvRecordLoader is the default; delimiter=None sniffs comma, semicolon, tab or pipe from the first kilobyte, and the default encoding reads UTF-8 with or without the byte-order mark Excel writes. The file is read once before anything is written: a row with more fields than the header — an unquoted delimiter inside a cell — is refused with its row number rather than loaded shifted. Implement RecordLoaderStrategy for another format and pass it as ingest(..., record_loader=...).
StructuredIngestionResult
Returned byingest() for a table (graphrag_sdk.ingestion.structured_pipeline.StructuredIngestionResult).
How a table is written
- Re-ingesting is a re-sync. A table is a snapshot: a deleted row is removed, a blanked cell loses its column, a moved foreign key leaves its old target, and a node another source still mentions survives with the table’s signed columns and identity retracted. The content hash covers the mapping, so re-declaring a type rewrites the source.
- A Document records how it was written, so
update()cannot re-read a table as prose or a document as records.update(),delete_document()anddrop_table()all take the table’s name. - No mapping declared? With a model, one is proposed from the measured columns, the first rows and the current ontology, held to the data (unique key, existing columns, known link targets), stored as
derived=Trueand reported byfinalize().proposed_mappings. Without a model the file is read as-is: the label from the filename, the leftmost unique-and-complete column as key, no name column. Declare aTableMappingto replace it, ordrop_table()to refuse it. - Ontology registration is additive. Declared types reach the ontology so text-to-Cypher sees
ageas anINTEGER; an existing label is extended, not redeclared, and a type contradiction raises.
Retrieval Strategies
RetrievalStrategy (ABC)
Uses the Template Method pattern.set_ontology() is called by the facade whenever the working ontology changes — an evolution call, a table declaring typed columns — so a strategy that generates Cypher always sees the current schema. A no-op by default; MultiPathRetrieval overrides it.