Skip to content

Commit 0130573

Browse files
phernandezclaude
andcommitted
fix: prompts call MCP tools directly, sync handles semantic errors, status uses local routing
- Prompts (search, continue_conversation) now call MCP tools directly instead of going through API endpoints, matching the recent_activity pattern and fixing #526 where prompts returned empty results - sync_file catches SemanticDependenciesMissingError separately so entities are still returned successfully when vector embedding fails, with a clear warning instead of silent failure (#578) - `bm status` and `bm doctor` default to local routing since they scan the local filesystem — cloud routing returned Docker-internal paths Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: phernandez <paul@basicmachines.co>
1 parent c372dfb commit 0130573

8 files changed

Lines changed: 426 additions & 85 deletions

File tree

src/basic_memory/cli/commands/doctor.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,9 @@ def doctor(
139139
"""Run local consistency checks to verify file/database sync."""
140140
try:
141141
validate_routing_flags(local, cloud)
142+
# Doctor runs local filesystem checks — always default to local routing
143+
if not local and not cloud:
144+
local = True
142145
with force_routing(local=local, cloud=cloud):
143146
run_with_cleanup(run_doctor())
144147
except (ToolError, ValueError) as e:

src/basic_memory/cli/commands/status.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,13 @@ def status(
179179

180180
try:
181181
validate_routing_flags(local, cloud)
182+
# Trigger: no explicit routing flag provided
183+
# Why: status scans the local filesystem — cloud routing would use the
184+
# Docker-internal path stored in the cloud database, which doesn't
185+
# exist locally.
186+
# Outcome: default to local routing unless --cloud was explicitly requested.
187+
if not local and not cloud:
188+
local = True
182189
with force_routing(local=local, cloud=cloud):
183190
run_with_cleanup(run_status(project, verbose)) # pragma: no cover
184191
except ValueError as e:

src/basic_memory/mcp/prompts/continue_conversation.py

Lines changed: 95 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,16 @@
44
providing context from previous interactions to maintain continuity.
55
"""
66

7+
from textwrap import dedent
78
from typing import Annotated, Optional
89

910
from loguru import logger
1011
from pydantic import Field
1112

12-
from basic_memory.config import ConfigManager
13-
from basic_memory.mcp.async_client import get_client
14-
from basic_memory.mcp.project_context import get_active_project
1513
from basic_memory.mcp.server import mcp
16-
from basic_memory.mcp.tools.utils import call_post
17-
from basic_memory.schemas.prompt import ContinueConversationRequest
14+
from basic_memory.mcp.tools.recent_activity import recent_activity
15+
from basic_memory.mcp.tools.search import search_notes
16+
from basic_memory.schemas.search import SearchResponse
1817

1918

2019
@mcp.prompt(
@@ -42,22 +41,94 @@ async def continue_conversation(
4241
"""
4342
logger.info(f"Continuing session, topic: {topic}, timeframe: {timeframe}")
4443

45-
async with get_client() as client:
46-
config = ConfigManager().config
47-
active_project = await get_active_project(client, project=config.default_project)
48-
49-
# Create request model
50-
request = ContinueConversationRequest( # pyright: ignore [reportCallIssue]
51-
topic=topic, timeframe=timeframe
52-
)
53-
54-
# Call the prompt API endpoint
55-
response = await call_post(
56-
client,
57-
f"/v2/projects/{active_project.external_id}/prompt/continue-conversation",
58-
json=request.model_dump(exclude_none=True),
59-
)
60-
61-
# Extract the rendered prompt from the response
62-
result = response.json()
63-
return result["prompt"]
44+
if topic:
45+
# Search for the topic using the search tool directly
46+
result = await search_notes(query=topic, after_date=timeframe)
47+
48+
if isinstance(result, SearchResponse):
49+
context_text = _format_continuation_results(result, topic)
50+
result_count = len(result.results)
51+
elif isinstance(result, dict):
52+
results = result.get("results", [])
53+
context_text = str(result)
54+
result_count = len(results)
55+
else:
56+
# Error string
57+
context_text = str(result)
58+
result_count = 0
59+
else:
60+
# No topic — show recent activity
61+
effective_timeframe = timeframe or "7d"
62+
activity_text = await recent_activity(timeframe=effective_timeframe)
63+
context_text = str(activity_text)
64+
result_count = -1 # Signals we used recent_activity
65+
66+
target = f"'{topic}'" if topic else "recent activity"
67+
68+
prompt = dedent(f"""
69+
# Continuing conversation on: {target}
70+
71+
This is a memory retrieval session.
72+
73+
Please use the available basic-memory tools to gather relevant context before responding.
74+
Start by executing one of the suggested commands below to retrieve content.
75+
76+
{context_text}
77+
78+
---
79+
80+
## Next Steps
81+
""")
82+
83+
if topic and result_count > 0:
84+
prompt += dedent(f"""
85+
Found {result_count} results related to '{topic}'.
86+
87+
1. **Read full content** - Use `read_note("permalink")` to dive into specific notes
88+
2. **Build context** - Use `build_context("memory://path")` to see relationships
89+
3. **Search deeper** - Use `search_notes("{topic}")` with different filters
90+
91+
> **Knowledge Capture:** As you continue this conversation, actively look for
92+
> opportunities to record new information, decisions, or insights using `write_note()`.
93+
""")
94+
elif topic:
95+
prompt += dedent(f"""
96+
No previous context found for '{topic}'.
97+
98+
This is an opportunity to start documenting this topic:
99+
100+
1. **Create a new note** - Use `write_note(title="{topic}", content="...")` to start
101+
2. **Search with variations** - Try `search_notes("{topic}")` with different terms
102+
3. **Check recent activity** - Use `recent_activity(timeframe="7d")` to see what's new
103+
""")
104+
else:
105+
prompt += dedent("""
106+
1. **Explore specific items** - Use `read_note("permalink")` to dive deeper
107+
2. **Search for topics** - Use `search_notes("topic")` to find specific content
108+
3. **Build context** - Use `build_context("memory://path")` to see relationships
109+
""")
110+
111+
return prompt
112+
113+
114+
def _format_continuation_results(result: SearchResponse, topic: str) -> str:
115+
"""Format search results for conversation continuation context."""
116+
if not result.results:
117+
return f"No previous context found for '{topic}'."
118+
119+
lines = [f"## Previous Context for '{topic}'\n"]
120+
121+
for item in result.results:
122+
title = item.title or "Untitled"
123+
permalink = item.permalink or ""
124+
125+
lines.append(f"### {title}")
126+
if permalink:
127+
lines.append(f"permalink: {permalink}")
128+
lines.append(f"Read with: `read_note(\"{permalink}\")`")
129+
if item.content:
130+
content = item.content[:300] + "..." if len(item.content) > 300 else item.content
131+
lines.append(f"\n{content}")
132+
lines.append("")
133+
134+
return "\n".join(lines)

src/basic_memory/mcp/prompts/search.py

Lines changed: 62 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,15 @@
33
These prompts help users search and explore their knowledge base.
44
"""
55

6+
from textwrap import dedent
67
from typing import Annotated, Optional
78

89
from loguru import logger
910
from pydantic import Field
1011

11-
from basic_memory.config import ConfigManager
12-
from basic_memory.mcp.async_client import get_client
13-
from basic_memory.mcp.project_context import get_active_project
1412
from basic_memory.mcp.server import mcp
15-
from basic_memory.mcp.tools.utils import call_post
16-
from basic_memory.schemas.prompt import SearchPromptRequest
13+
from basic_memory.mcp.tools.search import search_notes
14+
from basic_memory.schemas.search import SearchResponse
1715

1816

1917
@mcp.prompt(
@@ -41,20 +39,65 @@ async def search_prompt(
4139
"""
4240
logger.info(f"Searching knowledge base, query: {query}, timeframe: {timeframe}")
4341

44-
async with get_client() as client:
45-
config = ConfigManager().config
46-
active_project = await get_active_project(client, project=config.default_project)
42+
# Call the search tool directly — it returns SearchResponse, dict, or error string
43+
result = await search_notes(query=query, after_date=timeframe)
4744

48-
# Create request model
49-
request = SearchPromptRequest(query=query, timeframe=timeframe)
45+
# Format the tool output into a prompt with guidance
46+
if isinstance(result, SearchResponse):
47+
result_count = len(result.results)
48+
result_text = _format_search_results(result, query)
49+
elif isinstance(result, dict):
50+
# json output format
51+
results = result.get("results", [])
52+
result_count = len(results)
53+
result_text = str(result)
54+
else:
55+
# Error string from search tool
56+
result_count = 0
57+
result_text = str(result)
5058

51-
# Call the prompt API endpoint
52-
response = await call_post(
53-
client,
54-
f"/v2/projects/{active_project.external_id}/prompt/search",
55-
json=request.model_dump(exclude_none=True),
56-
)
59+
return dedent(f"""
60+
# Search Results: "{query}"
5761
58-
# Extract the rendered prompt from the response
59-
result = response.json()
60-
return result["prompt"]
62+
This is a memory retrieval session showing search results.
63+
64+
{result_text}
65+
66+
---
67+
68+
## Next Steps
69+
70+
Based on these {result_count} results, you can:
71+
72+
1. **Read a specific note** - Use `read_note("permalink")` to see full content
73+
2. **Build context** - Use `build_context("memory://path")` to see relationships
74+
3. **Refine search** - Use `search_notes("refined query")` to narrow results
75+
4. **Check recent activity** - Use `recent_activity(timeframe="7d")` for recent changes
76+
""")
77+
78+
79+
def _format_search_results(result: SearchResponse, query: str) -> str:
80+
"""Format SearchResponse into readable markdown."""
81+
if not result.results:
82+
return f"No results found for '{query}'."
83+
84+
lines = [f"Found {len(result.results)} results:\n"]
85+
86+
for item in result.results:
87+
title = item.title or "Untitled"
88+
permalink = item.permalink or ""
89+
score = f" (score: {item.score:.2f})" if item.score else ""
90+
91+
lines.append(f"- **{title}**{score}")
92+
if permalink:
93+
lines.append(f" permalink: {permalink}")
94+
if item.content:
95+
# Truncate content snippet
96+
content = item.content[:200] + "..." if len(item.content) > 200 else item.content
97+
lines.append(f" {content}")
98+
lines.append("")
99+
100+
if result.has_more:
101+
lines.append("*More results available. Use page=2 to see next page.*")
102+
103+
return "\n".join(lines)

src/basic_memory/sync/sync_service.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
)
3030
from basic_memory.repository.search_repository import create_search_repository
3131
from basic_memory.services import EntityService, FileService
32+
from basic_memory.repository.semantic_errors import SemanticDependenciesMissingError
3233
from basic_memory.services.exceptions import SyncFatalError
3334
from basic_memory.services.link_resolver import LinkResolver
3435
from basic_memory.services.search_service import SearchService
@@ -600,7 +601,18 @@ async def sync_file(
600601
entity, checksum = await self.sync_regular_file(path, new)
601602

602603
if entity is not None:
603-
await self.search_service.index_entity(entity)
604+
try:
605+
await self.search_service.index_entity(entity)
606+
except SemanticDependenciesMissingError:
607+
# Trigger: sqlite-vec or embedding provider unavailable
608+
# Why: FTS indexing succeeded but vector embeddings cannot be generated.
609+
# Don't fail the entire sync — the entity is usable for text search.
610+
# Outcome: entity returned successfully, warning logged for visibility.
611+
logger.warning(
612+
f"Semantic search dependencies missing — vector embeddings skipped "
613+
f"for path={path}. Run 'bm reindex --embeddings' after resolving "
614+
f"the dependency issue."
615+
)
604616

605617
# Clear failure tracking on successful sync
606618
self._clear_failure(path)

0 commit comments

Comments
 (0)