Skip to content

Commit 6afe4fd

Browse files
groksrcclaude
andauthored
feat: expose external_id in EntityResponse and link resolver (#569)
Signed-off-by: Drew Cain <groksrc@gmail.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 113d1b6 commit 6afe4fd

3 files changed

Lines changed: 57 additions & 0 deletions

File tree

src/basic_memory/schemas/response.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ class EntityResponse(SQLAlchemyModel):
195195
entity_metadata: Optional[Dict] = None
196196
checksum: Optional[str] = None
197197
content_type: ContentType
198+
external_id: Optional[str] = None
198199
observations: List[ObservationResponse] = []
199200
relations: List[RelationResponse] = []
200201
created_at: datetime

src/basic_memory/services/link_resolver.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Service for resolving markdown links to permalinks."""
22

3+
import uuid as uuid_mod
34
from typing import Optional, Tuple, Dict
45

56
from loguru import logger
@@ -63,6 +64,19 @@ async def resolve_link(
6364
explicit_project_reference = "::" in clean_text
6465
clean_text = normalize_project_reference(clean_text)
6566

67+
# --- External ID Resolution ---
68+
# Try external_id first if identifier looks like a UUID.
69+
# Canonicalize to lowercase-hyphen form so uppercase or unhyphenated
70+
# UUIDs also match the stored external_id values.
71+
try:
72+
canonical_id = str(uuid_mod.UUID(clean_text))
73+
entity = await self.entity_repository.get_by_external_id(canonical_id)
74+
if entity:
75+
logger.debug(f"Found entity by external_id: {entity.permalink}")
76+
return entity
77+
except ValueError:
78+
pass
79+
6680
# Trigger: link uses project namespace syntax (project::note)
6781
# Why: treat it as an explicit cross-project reference
6882
# Outcome: resolve only within the referenced project scope

tests/services/test_link_resolver.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Tests for link resolution service."""
22

3+
import uuid
34
from datetime import datetime, timezone
45

56
import pytest
@@ -857,3 +858,44 @@ async def test_simple_link_no_slash_skips_relative_resolution(relative_path_reso
857858
# testing/nested/ is not the same folder as testing/, but it's closer than nested/
858859
# The context-aware resolution will pick the closest match
859860
assert result.file_path == "testing/nested/deep-note.md"
861+
862+
863+
# ============================================================================
864+
# External ID (UUID) resolution tests
865+
# ============================================================================
866+
867+
868+
@pytest.mark.asyncio
869+
async def test_resolve_link_by_external_id(link_resolver, test_entities):
870+
"""Test resolving a link using a valid external_id (UUID)."""
871+
entity = test_entities[0]
872+
result = await link_resolver.resolve_link(entity.external_id)
873+
assert result is not None
874+
assert result.id == entity.id
875+
assert result.external_id == entity.external_id
876+
877+
878+
@pytest.mark.asyncio
879+
async def test_resolve_link_by_external_id_uppercase(link_resolver, test_entities):
880+
"""Test that uppercase UUID is canonicalized and resolves correctly."""
881+
entity = test_entities[0]
882+
upper_id = entity.external_id.upper()
883+
result = await link_resolver.resolve_link(upper_id)
884+
assert result is not None
885+
assert result.id == entity.id
886+
887+
888+
@pytest.mark.asyncio
889+
async def test_resolve_link_by_external_id_nonexistent(link_resolver):
890+
"""Test that a valid UUID format that doesn't match any entity returns None."""
891+
fake_id = str(uuid.uuid4())
892+
result = await link_resolver.resolve_link(fake_id)
893+
assert result is None
894+
895+
896+
@pytest.mark.asyncio
897+
async def test_resolve_link_non_uuid_falls_through(link_resolver, test_entities, project_prefix):
898+
"""Test that non-UUID strings skip UUID resolution and use normal lookup."""
899+
result = await link_resolver.resolve_link("Core Service")
900+
assert result is not None
901+
assert result.permalink == f"{project_prefix}/components/core-service"

0 commit comments

Comments
 (0)