Skip to content

Commit 0689e7a

Browse files
phernandezphernandez
andauthored
feat: return semantic info in markdown after write_note (#11)
Co-authored-by: phernandez <phernandez@basicmachines.co>
1 parent 16466e9 commit 0689e7a

8 files changed

Lines changed: 186 additions & 149 deletions

File tree

src/basic_memory/api/routers/resource_router.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ def get_entity_ids(item: SearchIndexRow) -> list[int]:
3131
from_entity = item.from_id
3232
to_entity = item.to_id # pyright: ignore [reportReturnType]
3333
return [from_entity, to_entity] if to_entity else [from_entity] # pyright: ignore [reportReturnType]
34-
case _:
34+
case _: # pragma: no cover
3535
raise ValueError(f"Unexpected type: {item.type}")
3636

3737

src/basic_memory/mcp/tools/notes.py

Lines changed: 41 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,14 @@
1717

1818

1919
@mcp.tool(
20-
description="Create or update a markdown note. Returns the permalink for referencing.",
20+
description="Create or update a markdown note. Returns a markdown formatted summary of the semantic content.",
2121
)
2222
async def write_note(
2323
title: str,
2424
content: str,
2525
folder: str,
2626
tags: Optional[List[str]] = None,
27-
verbose: bool = False,
28-
) -> EntityResponse | str:
27+
) -> str:
2928
"""Write a markdown note to the knowledge base.
3029
3130
The content can include semantic observations and relations using markdown syntax.
@@ -53,14 +52,16 @@ async def write_note(
5352
content: Markdown content for the note, can include observations and relations
5453
folder: the folder where the file should be saved
5554
tags: Optional list of tags to categorize the note
56-
verbose: If True, returns full EntityResponse with semantic info
5755
5856
Returns:
59-
If verbose=False: Permalink that can be used to reference the note
60-
If verbose=True: EntityResponse with full semantic details
57+
A markdown formatted summary of the semantic content, including:
58+
- Creation/update status
59+
- File path and checksum
60+
- Observation counts by category
61+
- Relation counts (resolved/unresolved)
62+
- Tags if present
6163
6264
Examples:
63-
# Note with both explicit and inline relations
6465
write_note(
6566
title="Search Implementation",
6667
content="# Search Component\\n\\n"
@@ -73,20 +74,6 @@ async def write_note(
7374
"- depends_on [[Database Schema]]",
7475
folder="docs/components"
7576
)
76-
77-
# Note with tags
78-
write_note(
79-
title="Error Handling Design",
80-
content="# Error Handling\\n\\n"
81-
"This design builds on [[Reliability Design]].\\n\\n"
82-
"## Approach\\n"
83-
"- [design] Use error codes #architecture\\n"
84-
"- [tech] Implement retry logic #implementation\\n\\n"
85-
"## Relations\\n"
86-
"- extends [[Base Error Handling]]",
87-
folder="docs/design",
88-
tags=["architecture", "reliability"]
89-
)
9077
"""
9178
logger.info(f"Writing note folder:'{folder}' title: '{title}'")
9279

@@ -101,12 +88,43 @@ async def write_note(
10188
entity_metadata=metadata,
10289
)
10390

104-
# Use existing knowledge tool
91+
# Create or update via knowledge API
10592
logger.info(f"Creating {entity.permalink}")
10693
url = f"/knowledge/entities/{entity.permalink}"
10794
response = await call_put(client, url, json=entity.model_dump())
10895
result = EntityResponse.model_validate(response.json())
109-
return result if verbose else result.permalink
96+
97+
# Format semantic summary based on status code
98+
action = "Created" if response.status_code == 201 else "Updated"
99+
assert result.checksum is not None
100+
summary = [
101+
f"# {action} {result.file_path} ({result.checksum[:8]})",
102+
f"permalink: {result.permalink}",
103+
]
104+
105+
if result.observations:
106+
categories = {}
107+
for obs in result.observations:
108+
categories[obs.category] = categories.get(obs.category, 0) + 1
109+
110+
summary.append("\n## Observations")
111+
for category, count in sorted(categories.items()):
112+
summary.append(f"- {category}: {count}")
113+
114+
if result.relations:
115+
unresolved = sum(1 for r in result.relations if not r.to_id)
116+
resolved = len(result.relations) - unresolved
117+
118+
summary.append("\n## Relations")
119+
summary.append(f"- Resolved: {resolved}")
120+
if unresolved:
121+
summary.append(f"- Unresolved: {unresolved}")
122+
summary.append("\nUnresolved relations will be retried on next sync.")
123+
124+
if tags:
125+
summary.append(f"\n## Tags\n- {', '.join(tags)}")
126+
127+
return "\n".join(summary)
110128

111129

112130
@mcp.tool(description="Read note content by title, permalink, relation, or pattern")

tests/api/test_resource_router.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,6 @@ async def test_get_resource_entities(client, test_config, entity_repository):
158158
entity2 = EntityResponse(**entity_response)
159159

160160
assert len(entity2.relations) == 1
161-
relation = entity2.relations[0]
162161

163162
# Test getting the content via the relation
164163
response = await client.get("/resource/test/*")

tests/mcp/test_tool_get_entity.py

Lines changed: 0 additions & 45 deletions
This file was deleted.

tests/mcp/test_tool_knowledge.py

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
async def test_get_single_entity(client):
1414
"""Test retrieving a single entity."""
1515
# First create an entity
16-
permalink = await notes.write_note(
16+
result = await notes.write_note(
1717
title="Test Note",
1818
folder="test",
1919
content="""
@@ -22,9 +22,10 @@ async def test_get_single_entity(client):
2222
""",
2323
tags=["test", "documentation"],
2424
)
25+
assert result
2526

2627
# Get the entity
27-
entity = await get_entity(permalink)
28+
entity = await get_entity("test/test-note")
2829

2930
# Verify entity details
3031
assert entity.title == "Test Note"
@@ -36,48 +37,48 @@ async def test_get_single_entity(client):
3637
async def test_get_multiple_entities(client):
3738
"""Test retrieving multiple entities."""
3839
# Create two test entities
39-
permalink1 = await notes.write_note(
40+
await notes.write_note(
4041
title="Test Note 1",
4142
folder="test",
4243
content="# Test 1",
4344
)
44-
permalink2 = await notes.write_note(
45+
await notes.write_note(
4546
title="Test Note 2",
4647
folder="test",
4748
content="# Test 2",
4849
)
4950

5051
# Get both entities
51-
request = GetEntitiesRequest(permalinks=[permalink1, permalink2])
52+
request = GetEntitiesRequest(permalinks=["test/test-note-1", "test/test-note-2"])
5253
response = await get_entities(request)
5354

5455
# Verify we got both entities
5556
assert len(response.entities) == 2
5657
permalinks = {e.permalink for e in response.entities}
57-
assert permalink1 in permalinks
58-
assert permalink2 in permalinks
58+
assert "test/test-note-1" in permalinks
59+
assert "test/test-note-2" in permalinks
5960

6061

6162
@pytest.mark.asyncio
6263
async def test_delete_entities(client):
6364
"""Test deleting entities."""
6465
# Create a test entity
65-
permalink = await notes.write_note(
66+
await notes.write_note(
6667
title="Test Note",
6768
folder="test",
6869
content="# Test Note to Delete",
6970
)
7071

7172
# Delete the entity
72-
request = DeleteEntitiesRequest(permalinks=[permalink])
73+
request = DeleteEntitiesRequest(permalinks=["test/test-note"])
7374
response = await delete_entities(request)
7475

7576
# Verify deletion
7677
assert response.deleted is True
7778

7879
# Verify entity no longer exists
7980
with pytest.raises(ToolError):
80-
await get_entity(permalink)
81+
await get_entity("test/test-note")
8182

8283

8384
@pytest.mark.asyncio

0 commit comments

Comments
 (0)