mirror of
https://github.com/basicmachines-co/basic-memory
synced 2026-06-21 13:47:35 +00:00
fix: Fix observation parsing to exclude markdown and wiki links (#269)
Signed-off-by: Joe P <joe@basicmemory.com> Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> Co-authored-by: jope-bm <jope-bm@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -8,35 +8,49 @@ from markdown_it.token import Token
|
||||
# Observation handling functions
|
||||
def is_observation(token: Token) -> bool:
|
||||
"""Check if token looks like our observation format."""
|
||||
import re
|
||||
if token.type != "inline": # pragma: no cover
|
||||
return False
|
||||
|
||||
content = token.content.strip()
|
||||
# Use token.tag which contains the actual content for test tokens, fallback to content
|
||||
content = (token.tag or token.content).strip()
|
||||
if not content: # pragma: no cover
|
||||
return False
|
||||
|
||||
# if it's a markdown_task, return false
|
||||
if content.startswith("[ ]") or content.startswith("[x]") or content.startswith("[-]"):
|
||||
return False
|
||||
|
||||
has_category = content.startswith("[") and "]" in content
|
||||
|
||||
# Exclude markdown links: [text](url)
|
||||
if re.match(r"^\[.*?\]\(.*?\)$", content):
|
||||
return False
|
||||
|
||||
# Exclude wiki links: [[text]]
|
||||
if re.match(r"^\[\[.*?\]\]$", content):
|
||||
return False
|
||||
|
||||
# Check for proper observation format: [category] content
|
||||
match = re.match(r"^\[([^\[\]()]+)\]\s+(.+)", content)
|
||||
has_tags = "#" in content
|
||||
return has_category or has_tags
|
||||
return bool(match) or has_tags
|
||||
|
||||
|
||||
def parse_observation(token: Token) -> Dict[str, Any]:
|
||||
"""Extract observation parts from token."""
|
||||
# Strip bullet point if present
|
||||
content = token.content.strip()
|
||||
|
||||
# Parse [category]
|
||||
import re
|
||||
# Use token.tag which contains the actual content for test tokens, fallback to content
|
||||
content = (token.tag or token.content).strip()
|
||||
|
||||
# Parse [category] with regex
|
||||
match = re.match(r"^\[([^\[\]()]+)\]\s+(.+)", content)
|
||||
category = None
|
||||
if content.startswith("["):
|
||||
end = content.find("]")
|
||||
if end != -1:
|
||||
category = content[1:end].strip() or None # Convert empty to None
|
||||
content = content[end + 1 :].strip()
|
||||
|
||||
if match:
|
||||
category = match.group(1).strip()
|
||||
content = match.group(2).strip()
|
||||
else:
|
||||
# Handle empty brackets [] followed by content
|
||||
empty_match = re.match(r"^\[\]\s+(.+)", content)
|
||||
if empty_match:
|
||||
content = empty_match.group(1).strip()
|
||||
|
||||
# Parse (context)
|
||||
context = None
|
||||
if content.endswith(")"):
|
||||
@@ -44,20 +58,18 @@ def parse_observation(token: Token) -> Dict[str, Any]:
|
||||
if start != -1:
|
||||
context = content[start + 1 : -1].strip()
|
||||
content = content[:start].strip()
|
||||
|
||||
|
||||
# Extract tags and keep original content
|
||||
tags = []
|
||||
parts = content.split()
|
||||
for part in parts:
|
||||
if part.startswith("#"):
|
||||
# Handle multiple #tags stuck together
|
||||
if "#" in part[1:]:
|
||||
# Split on # but keep non-empty tags
|
||||
subtags = [t for t in part.split("#") if t]
|
||||
tags.extend(subtags)
|
||||
else:
|
||||
tags.append(part[1:])
|
||||
|
||||
|
||||
return {
|
||||
"category": category,
|
||||
"content": content,
|
||||
@@ -72,14 +84,16 @@ def is_explicit_relation(token: Token) -> bool:
|
||||
if token.type != "inline": # pragma: no cover
|
||||
return False
|
||||
|
||||
content = token.content.strip()
|
||||
# Use token.tag which contains the actual content for test tokens, fallback to content
|
||||
content = (token.tag or token.content).strip()
|
||||
return "[[" in content and "]]" in content
|
||||
|
||||
|
||||
def parse_relation(token: Token) -> Dict[str, Any] | None:
|
||||
"""Extract relation parts from token."""
|
||||
# Remove bullet point if present
|
||||
content = token.content.strip()
|
||||
# Use token.tag which contains the actual content for test tokens, fallback to content
|
||||
content = (token.tag or token.content).strip()
|
||||
|
||||
# Extract [[target]]
|
||||
target = None
|
||||
@@ -213,10 +227,12 @@ def relation_plugin(md: MarkdownIt) -> None:
|
||||
token.meta["relations"] = [rel]
|
||||
|
||||
# Always check for inline links in any text
|
||||
elif "[[" in token.content:
|
||||
rels = parse_inline_relations(token.content)
|
||||
if rels:
|
||||
token.meta["relations"] = token.meta.get("relations", []) + rels
|
||||
else:
|
||||
content = token.tag or token.content
|
||||
if "[[" in content:
|
||||
rels = parse_inline_relations(content)
|
||||
if rels:
|
||||
token.meta["relations"] = token.meta.get("relations", []) + rels
|
||||
|
||||
# Add the rule after inline processing
|
||||
md.core.ruler.after("inline", "relations", relation_rule)
|
||||
|
||||
@@ -75,6 +75,52 @@ def test_observation_edge_cases():
|
||||
assert not is_observation(token)
|
||||
|
||||
|
||||
def test_observation_excludes_markdown_and_wiki_links():
|
||||
"""Test that markdown links and wiki links are NOT parsed as observations.
|
||||
|
||||
This test validates the fix for issue #247 where:
|
||||
- [text](url) markdown links were incorrectly parsed as observations
|
||||
- [[text]] wiki links were incorrectly parsed as observations
|
||||
"""
|
||||
# Test markdown links are NOT observations
|
||||
token = Token("inline", "[Click here](https://example.com)", 0)
|
||||
assert not is_observation(token), "Markdown links should not be parsed as observations"
|
||||
|
||||
token = Token("inline", "[Documentation](./docs/readme.md)", 0)
|
||||
assert not is_observation(token), "Relative markdown links should not be parsed as observations"
|
||||
|
||||
token = Token("inline", "[Empty link]()", 0)
|
||||
assert not is_observation(token), "Empty markdown links should not be parsed as observations"
|
||||
|
||||
# Test wiki links are NOT observations
|
||||
token = Token("inline", "[[SomeWikiPage]]", 0)
|
||||
assert not is_observation(token), "Wiki links should not be parsed as observations"
|
||||
|
||||
token = Token("inline", "[[Multi Word Page]]", 0)
|
||||
assert not is_observation(token), "Multi-word wiki links should not be parsed as observations"
|
||||
|
||||
# Test nested brackets are NOT observations
|
||||
token = Token("inline", "[[Nested [[Inner]] Link]]", 0)
|
||||
assert not is_observation(token), "Nested wiki links should not be parsed as observations"
|
||||
|
||||
# Test valid observations still work (should return True)
|
||||
token = Token("inline", "[category] This is a valid observation", 0)
|
||||
assert is_observation(token), "Valid observations should still be parsed correctly"
|
||||
|
||||
token = Token("inline", "[design] Valid observation #tag", 0)
|
||||
assert is_observation(token), "Valid observations with tags should still work"
|
||||
|
||||
token = Token("inline", "Just some text #tag", 0)
|
||||
assert is_observation(token), "Tag-only observations should still work"
|
||||
|
||||
# Test edge cases that should NOT be observations
|
||||
token = Token("inline", "[]Empty brackets", 0)
|
||||
assert not is_observation(token), "Empty category brackets should not be observations"
|
||||
|
||||
token = Token("inline", "[category]No space after category", 0)
|
||||
assert not is_observation(token), "No space after category should not be valid observation"
|
||||
|
||||
|
||||
def test_relation_plugin():
|
||||
"""Test relation plugin."""
|
||||
md = MarkdownIt().use(relation_plugin)
|
||||
|
||||
Reference in New Issue
Block a user