Thanks to visit codestin.com
Credit goes to github.com

Skip to content

fix: ABOUT entity linking for facts/preferences + logger fix#93

Open
AhmedHamadto wants to merge 1 commit into
neo4j-labs:mainfrom
AhmedHamadto:fix/entity-linking-and-tests
Open

fix: ABOUT entity linking for facts/preferences + logger fix#93
AhmedHamadto wants to merge 1 commit into
neo4j-labs:mainfrom
AhmedHamadto:fix/entity-linking-and-tests

Conversation

@AhmedHamadto
Copy link
Copy Markdown
Contributor

Summary

  • Adds _link_to_entity_by_name() to LongTermMemory that automatically creates ABOUT relationships between Facts/Preferences and matching Entities when add_fact() or add_preference() is called
  • Adds LINK_FACT_TO_ENTITY query constant and enhances GET_ENTITY_BY_NAME with case-insensitive matching (toLower(), canonical_name, aliases)
  • Adds fallback name-based entity lookup in MCP memory_get_entity tool when vector search returns nothing
  • Adds _get_entity_facts_and_preferences() helper to include linked facts/preferences in entity results
  • Fixes missing import logging in long_term.py that caused a NameError in the except handler, silently breaking all entity linking when called through the MCP server

How It Works

When a fact is added with add_fact(subject="Rust", predicate="USED_FOR", obj="edge binary"):

  1. The fact is stored in Neo4j (existing behavior)
  2. _link_to_entity_by_name("Rust", fact_id, "fact", role="subject") is called
  3. It looks up an entity named "Rust" (case-insensitive)
  4. If found, creates (fact)-[:ABOUT {role: "subject"}]->(entity)
  5. Same for the object: looks up "edge binary" entity

Preferences link by category name: add_preference(category="Python", ...) links to an entity named "Python".

Bug Context

Without this fix, add_fact() and add_preference() create isolated nodes with zero relationships to entities — making the knowledge graph effectively a flat key-value store. The memory_get_context tool cannot traverse from entities to related facts, and memory_get_entity returns no linked knowledge.

The missing import logging (#87) caused a cascading failure: any exception during linking triggered NameError instead of the actual error, making the root cause invisible through the MCP server.

Test Plan

5 new integration tests in tests/integration/test_about_relationships.py:

  • test_fact_linked_to_entity_via_about_subject — entity + fact with matching subject → ABOUT with role=subject
  • test_fact_linked_to_entity_via_about_object — entity + fact with matching object → ABOUT with role=object
  • test_preference_linked_to_entity_via_about — entity + preference with matching category → ABOUT link
  • test_fact_no_matching_entity_no_error — fact with no matching entity → no crash, zero ABOUT links
  • test_fact_case_insensitive_linking — entity "Rust" + fact subject "rust" → still linked

All tests use clean_memory_client (real Neo4j via testcontainer), no external API calls.

Additionally verified end-to-end through the MCP server against a live Neo4j instance — both fact and preference ABOUT linking confirmed working.

Fixes #77
Fixes #87
Supersedes #78 (based on pre-v0.1.0 code)

When add_fact() or add_preference() is called, the new
_link_to_entity_by_name() method looks up matching entities by name
(case-insensitive) and creates ABOUT relationships in the graph.

Changes:
- Add _link_to_entity_by_name() to LongTermMemory with calls from
  add_fact() (subject + object) and add_preference() (category)
- Add LINK_FACT_TO_ENTITY query constant and enhance GET_ENTITY_BY_NAME
  with case-insensitive matching
- Add fallback name-based entity lookup and linked facts/preferences
  retrieval in MCP memory_get_entity tool
- Fix missing 'import logging' in long_term.py that caused NameError
  in the except handler, silently breaking all entity linking via MCP
- Add 5 integration tests verifying ABOUT relationship creation

Fixes neo4j-labs#77
Fixes neo4j-labs#87
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR improves long-term memory graph connectivity by automatically linking newly stored Facts/Preferences to existing Entities via ABOUT relationships, and enhances entity retrieval (including MCP tool fallback) so linked knowledge is discoverable.

Changes:

  • Add automatic ABOUT-linking in LongTermMemory.add_fact() / add_preference() via _link_to_entity_by_name().
  • Extend entity lookup behavior: case-insensitive name matching in GET_ENTITY_BY_NAME, MCP memory_get_entity name-based fallback, and inclusion of linked facts/preferences in tool output.
  • Add integration tests validating ABOUT relationship creation and case-insensitive linking.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.

File Description
tests/integration/test_about_relationships.py New integration tests validating ABOUT relationship creation for facts/preferences.
src/neo4j_agent_memory/memory/long_term.py Adds linking helper + calls from add_fact/add_preference; fixes missing module logger setup.
src/neo4j_agent_memory/mcp/_tools.py Adds name-based fallback entity lookup and returns linked facts/preferences in memory_get_entity.
src/neo4j_agent_memory/graph/queries.py Updates GET_ENTITY_BY_NAME to be more case-insensitive and adds LINK_FACT_TO_ENTITY.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +455 to +460
# Include linked facts and preferences
linked = await _get_entity_facts_and_preferences(client, str(entity.id))
if linked.get("facts"):
result["facts"] = linked["facts"]
if linked.get("preferences"):
result["preferences"] = linked["preferences"]
WHERE e.name = $name OR e.canonical_name = $name OR $name IN COALESCE(e.aliases, [])
WHERE e.name = $name
OR toLower(e.name) = toLower($name)
OR e.canonical_name = toLower($name)
Comment on lines +431 to +432
MERGE (f)-[r:ABOUT]->(e)
ON CREATE SET r.role = $role
Comment on lines +816 to +822
OPTIONAL MATCH (f:Fact)-[:ABOUT]->(e)
OPTIONAL MATCH (p:Preference)-[:ABOUT]->(e)
WITH e,
collect(DISTINCT {type: 'fact', subject: f.subject,
predicate: f.predicate, object: f.object}) AS facts,
collect(DISTINCT {type: 'preference', category: p.category,
preference: p.preference}) AS preferences
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants