Creating an entity and then creating a relation to it fails with
Target entity '...' does not exist whenever the name is one that entity-name
normalization rewrites. POST /graphs/entity stores the normalized name;
POST /graphs/relation checks existence against the raw, un-normalized string.
Reproduce
import asyncio
from lightrag import LightRAG
# ... LightRAG with graph_storage="NetworkXStorage" ...
await rag.acreate_entity("Tesla(US)", {"description": "d"}) # 200, created
await rag.acreate_relation("GoodA", "Tesla(US)", {"description": "d"})
# ValueError: Target entity 'Tesla(US)' does not exist
Verified against 6702eea9 through the real LightRAG object, the real graph
storage and the real commit path. Four names, four different normalization
rules, all four reproduce:
name sent to acreate_entity |
name actually stored |
acreate_relation with the same name |
with the stored name |
Tesla(US) (full-width parens) |
Tesla(US) |
Target entity 'Tesla(US)' does not exist |
ok |
Tesla Inc (HTML entity) |
Tesla Inc |
does not exist |
ok |
"Tesla" (matched quotes) |
Tesla |
does not exist |
ok |
Tesla Inc (ideographic space) |
Tesla Inc |
does not exist |
ok |
Expected
A name accepted by POST /graphs/entity should be accepted by
POST /graphs/relation for the same entity, since the client has no way to know
the stored spelling from the create call's response.
Root cause
lightrag/utils_graph.py::acreate_entity normalizes and then tolerates both
spellings:
requested_entity_name = entity_name
entity_name = _normalize_manual_entity_name(requested_entity_name)
...
sorted({requested_entity_name, entity_name}), # lock covers both
if requested_entity_name != entity_name and await chunk_entity_relation_graph.has_node(requested_entity_name):
raise ValueError(f"Entity '{requested_entity_name}' already exists")
existing_node = await chunk_entity_relation_graph.has_node(entity_name)
acreate_relation (same module) normalizes nothing and checks the raw values:
source_exists = await chunk_entity_relation_graph.has_node(source_entity)
target_exists = await chunk_entity_relation_graph.has_node(target_entity)
if not target_exists:
raise ValueError(f"Target entity '{target_entity}' does not exist")
LightRAG.acreate_relation only forwards, so the asymmetry is the whole cause.
The self-loop check's "compare them raw" comment is right for the self-loop
condition, but it does not extend to the existence check: the node was stored
under the normalized name, so existence has to be asked under that name.
Suggested fix
Normalize both endpoints once at the top of acreate_relation, the same way
acreate_entity does, and use the normalized values for the existence check, the
self-loop check and the edge write. Normalization is idempotent, and
acreate_entity already handles the two-spellings case for its lock key.
Environment
LightRAG 6702eea912b12df5ba831f58609470ca9da34cfc, Python 3.12,
NetworkXStorage, self-hosted (no external services involved — the failure is
in the API layer, before any embedding or LLM call matters).
Creating an entity and then creating a relation to it fails with
Target entity '...' does not existwhenever the name is one that entity-namenormalization rewrites.
POST /graphs/entitystores the normalized name;POST /graphs/relationchecks existence against the raw, un-normalized string.Reproduce
Verified against
6702eea9through the realLightRAGobject, the real graphstorage and the real commit path. Four names, four different normalization
rules, all four reproduce:
acreate_entityacreate_relationwith the same nameTesla(US)(full-width parens)Tesla(US)Target entity 'Tesla(US)' does not existTesla Inc(HTML entity)Tesla Inc"Tesla"(matched quotes)TeslaTesla Inc(ideographic space)Tesla IncExpected
A name accepted by
POST /graphs/entityshould be accepted byPOST /graphs/relationfor the same entity, since the client has no way to knowthe stored spelling from the create call's response.
Root cause
lightrag/utils_graph.py::acreate_entitynormalizes and then tolerates bothspellings:
acreate_relation(same module) normalizes nothing and checks the raw values:LightRAG.acreate_relationonly forwards, so the asymmetry is the whole cause.The self-loop check's "compare them raw" comment is right for the self-loop
condition, but it does not extend to the existence check: the node was stored
under the normalized name, so existence has to be asked under that name.
Suggested fix
Normalize both endpoints once at the top of
acreate_relation, the same wayacreate_entitydoes, and use the normalized values for the existence check, theself-loop check and the edge write. Normalization is idempotent, and
acreate_entityalready handles the two-spellings case for its lock key.Environment
LightRAG
6702eea912b12df5ba831f58609470ca9da34cfc, Python 3.12,NetworkXStorage, self-hosted (no external services involved — the failure isin the API layer, before any embedding or LLM call matters).