Skip to content

[Bug]: POST /graphs/relation rejects entity names that POST /graphs/entity accepted #4113

Description

@passionworkeer

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).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions