mirror of
https://github.com/basicmachines-co/basic-memory
synced 2026-06-21 13:47:35 +00:00
feat: implement SPEC-11 API performance optimizations (#315)
Signed-off-by: phernandez <paul@basicmachines.co> Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -22,7 +22,7 @@ from basic_memory.api.routers import (
|
||||
webdav,
|
||||
)
|
||||
from basic_memory.config import ConfigManager
|
||||
from basic_memory.services.initialization import initialize_app, initialize_file_sync
|
||||
from basic_memory.services.initialization import initialize_file_sync
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
@@ -30,10 +30,15 @@ async def lifespan(app: FastAPI): # pragma: no cover
|
||||
"""Lifecycle manager for the FastAPI app."""
|
||||
|
||||
app_config = ConfigManager().config
|
||||
# Initialize app and database
|
||||
logger.info("Starting Basic Memory API")
|
||||
print(f"fastapi {app_config.projects}")
|
||||
await initialize_app(app_config)
|
||||
|
||||
# Cache database connections in app state for performance (no project reconciliation)
|
||||
logger.info("Initializing database and caching connections...")
|
||||
engine, session_maker = await db.get_or_create_db(app_config.database_path)
|
||||
app.state.engine = engine
|
||||
app.state.session_maker = session_maker
|
||||
logger.info("Database connections cached in app state")
|
||||
|
||||
logger.info(f"Sync changes enabled: {app_config.sync_changes}")
|
||||
if app_config.sync_changes:
|
||||
|
||||
@@ -93,6 +93,11 @@ class BasicMemoryConfig(BaseSettings):
|
||||
description="Format for generated filenames. False preserves spaces and special chars, True converts them to hyphens for consistency with permalinks",
|
||||
)
|
||||
|
||||
skip_initialization_sync: bool = Field(
|
||||
default=False,
|
||||
description="Skip expensive initialization synchronization. Useful for cloud/stateless deployments where project reconciliation is not needed.",
|
||||
)
|
||||
|
||||
# API connection configuration
|
||||
api_url: Optional[str] = Field(
|
||||
default=None,
|
||||
@@ -341,8 +346,6 @@ def save_basic_memory_config(file_path: Path, config: BasicMemoryConfig) -> None
|
||||
logger.error(f"Failed to save config: {e}")
|
||||
|
||||
|
||||
|
||||
|
||||
# setup logging to a single log file in user home directory
|
||||
user_home = Path.home()
|
||||
log_dir = user_home / DATA_DIR_NAME
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from typing import Annotated
|
||||
from loguru import logger
|
||||
|
||||
from fastapi import Depends, HTTPException, Path, status
|
||||
from fastapi import Depends, HTTPException, Path, status, Request
|
||||
from sqlalchemy.ext.asyncio import (
|
||||
AsyncSession,
|
||||
AsyncEngine,
|
||||
@@ -78,9 +78,24 @@ ProjectConfigDep = Annotated[ProjectConfig, Depends(get_project_config)] # prag
|
||||
|
||||
|
||||
async def get_engine_factory(
|
||||
app_config: AppConfigDep,
|
||||
request: Request,
|
||||
) -> tuple[AsyncEngine, async_sessionmaker[AsyncSession]]: # pragma: no cover
|
||||
"""Get engine and session maker."""
|
||||
"""Get cached engine and session maker from app state.
|
||||
|
||||
For API requests, returns cached connections from app.state for optimal performance.
|
||||
For non-API contexts (CLI), falls back to direct database connection.
|
||||
"""
|
||||
# Try to get cached connections from app state (API context)
|
||||
if (
|
||||
hasattr(request, "app")
|
||||
and hasattr(request.app.state, "engine")
|
||||
and hasattr(request.app.state, "session_maker")
|
||||
):
|
||||
return request.app.state.engine, request.app.state.session_maker
|
||||
|
||||
# Fallback for non-API contexts (CLI)
|
||||
logger.debug("Using fallback database connection for non-API context")
|
||||
app_config = get_app_config()
|
||||
engine, session_maker = await db.get_or_create_db(app_config.database_path)
|
||||
return engine, session_maker
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ from basic_memory.mcp.tools.project_management import (
|
||||
create_memory_project,
|
||||
delete_project,
|
||||
)
|
||||
|
||||
# ChatGPT-compatible tools
|
||||
from basic_memory.mcp.tools.chatgpt_tools import search, fetch
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ def _format_search_results_for_chatgpt(results: SearchResponse) -> List[Dict[str
|
||||
formatted_result = {
|
||||
"id": result.permalink or f"doc-{len(formatted_results)}",
|
||||
"title": result.title if result.title and result.title.strip() else "Untitled",
|
||||
"url": result.permalink or ""
|
||||
"url": result.permalink or "",
|
||||
}
|
||||
formatted_results.append(formatted_result)
|
||||
|
||||
@@ -43,11 +43,11 @@ def _format_document_for_chatgpt(
|
||||
"""
|
||||
# Extract title from markdown content if not provided
|
||||
if not title and isinstance(content, str):
|
||||
lines = content.split('\n')
|
||||
if lines and lines[0].startswith('# '):
|
||||
lines = content.split("\n")
|
||||
if lines and lines[0].startswith("# "):
|
||||
title = lines[0][2:].strip()
|
||||
else:
|
||||
title = identifier.split('/')[-1].replace('-', ' ').title()
|
||||
title = identifier.split("/")[-1].replace("-", " ").title()
|
||||
|
||||
# Ensure title is never None
|
||||
if not title:
|
||||
@@ -60,7 +60,7 @@ def _format_document_for_chatgpt(
|
||||
"title": title or "Document Not Found",
|
||||
"text": content,
|
||||
"url": identifier,
|
||||
"metadata": {"error": "Document not found"}
|
||||
"metadata": {"error": "Document not found"},
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -68,13 +68,11 @@ def _format_document_for_chatgpt(
|
||||
"title": title or "Untitled Document",
|
||||
"text": content,
|
||||
"url": identifier,
|
||||
"metadata": {"format": "markdown"}
|
||||
"metadata": {"format": "markdown"},
|
||||
}
|
||||
|
||||
|
||||
@mcp.tool(
|
||||
description="Search for content across the knowledge base"
|
||||
)
|
||||
@mcp.tool(description="Search for content across the knowledge base")
|
||||
async def search(
|
||||
query: str,
|
||||
context: Context | None = None,
|
||||
@@ -99,7 +97,7 @@ async def search(
|
||||
page=1,
|
||||
page_size=10, # Reasonable default for ChatGPT consumption
|
||||
search_type="text", # Default to full-text search
|
||||
context=context
|
||||
context=context,
|
||||
)
|
||||
|
||||
# Handle string error responses from search_notes
|
||||
@@ -108,7 +106,7 @@ async def search(
|
||||
search_results = {
|
||||
"results": [],
|
||||
"error": "Search failed",
|
||||
"error_details": results[:500] # Truncate long error messages
|
||||
"error_details": results[:500], # Truncate long error messages
|
||||
}
|
||||
else:
|
||||
# Format successful results for ChatGPT
|
||||
@@ -116,36 +114,24 @@ async def search(
|
||||
search_results = {
|
||||
"results": formatted_results,
|
||||
"total_count": len(results.results), # Use actual count from results
|
||||
"query": query
|
||||
"query": query,
|
||||
}
|
||||
logger.info(f"Search completed: {len(formatted_results)} results returned")
|
||||
|
||||
# Return in MCP content array format as required by OpenAI
|
||||
return [
|
||||
{
|
||||
"type": "text",
|
||||
"text": json.dumps(search_results, ensure_ascii=False)
|
||||
}
|
||||
]
|
||||
return [{"type": "text", "text": json.dumps(search_results, ensure_ascii=False)}]
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"ChatGPT search failed for query '{query}': {e}")
|
||||
error_results = {
|
||||
"results": [],
|
||||
"error": "Internal search error",
|
||||
"error_message": str(e)[:200]
|
||||
"error_message": str(e)[:200],
|
||||
}
|
||||
return [
|
||||
{
|
||||
"type": "text",
|
||||
"text": json.dumps(error_results, ensure_ascii=False)
|
||||
}
|
||||
]
|
||||
return [{"type": "text", "text": json.dumps(error_results, ensure_ascii=False)}]
|
||||
|
||||
|
||||
@mcp.tool(
|
||||
description="Fetch the full contents of a search result document"
|
||||
)
|
||||
@mcp.tool(description="Fetch the full contents of a search result document")
|
||||
async def fetch(
|
||||
id: str,
|
||||
context: Context | None = None,
|
||||
@@ -169,7 +155,7 @@ async def fetch(
|
||||
project=None, # Let project resolution happen automatically
|
||||
page=1,
|
||||
page_size=10, # Default pagination
|
||||
context=context
|
||||
context=context,
|
||||
)
|
||||
|
||||
# Format the document for ChatGPT
|
||||
@@ -178,12 +164,7 @@ async def fetch(
|
||||
logger.info(f"Fetch completed: id='{id}', content_length={len(document.get('text', ''))}")
|
||||
|
||||
# Return in MCP content array format as required by OpenAI
|
||||
return [
|
||||
{
|
||||
"type": "text",
|
||||
"text": json.dumps(document, ensure_ascii=False)
|
||||
}
|
||||
]
|
||||
return [{"type": "text", "text": json.dumps(document, ensure_ascii=False)}]
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"ChatGPT fetch failed for id '{id}': {e}")
|
||||
@@ -192,11 +173,6 @@ async def fetch(
|
||||
"title": "Fetch Error",
|
||||
"text": f"Failed to fetch document: {str(e)[:200]}",
|
||||
"url": id,
|
||||
"metadata": {"error": "Fetch failed"}
|
||||
"metadata": {"error": "Fetch failed"},
|
||||
}
|
||||
return [
|
||||
{
|
||||
"type": "text",
|
||||
"text": json.dumps(error_document, ensure_ascii=False)
|
||||
}
|
||||
]
|
||||
return [{"type": "text", "text": json.dumps(error_document, ensure_ascii=False)}]
|
||||
|
||||
Reference in New Issue
Block a user