feat: min_similarity override, cloud promo improvements (#570)

Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Paul Hernandez
2026-02-16 16:44:02 -06:00
committed by GitHub
parent 6afe4fd0cc
commit 55d675e278
106 changed files with 7500 additions and 852 deletions
+12 -9
View File
@@ -13,7 +13,7 @@ from pathlib import Path
from loguru import logger
from basic_memory import db
from basic_memory.config import BasicMemoryConfig, ProjectMode
from basic_memory.config import BasicMemoryConfig, DatabaseBackend, ProjectMode
from basic_memory.models import Project
from basic_memory.repository import (
ProjectRepository,
@@ -174,9 +174,13 @@ async def initialize_app(
Args:
app_config: The Basic Memory project configuration
"""
# Skip initialization in cloud mode - cloud manages its own projects
if app_config.cloud_mode_enabled:
logger.debug("Skipping initialization in cloud mode - projects managed by cloud")
# Trigger: database backend is Postgres (cloud deployment)
# Why: cloud deployments manage their own projects and migrations via the cloud platform.
# The local MCP server always uses SQLite and needs initialization even when
# cloud_mode is enabled (for per-project cloud routing).
# Outcome: skip initialization only for actual cloud Postgres deployments.
if app_config.database_backend == DatabaseBackend.POSTGRES:
logger.info("Skipping local initialization - Postgres backend manages its own schema")
return
logger.info("Initializing app...")
@@ -186,7 +190,7 @@ async def initialize_app(
# Reconcile projects from config.json with projects table
await reconcile_projects_with_config(app_config)
logger.info("App initialization completed (migration running in background if needed)")
logger.info("App initialization completed")
def ensure_initialization(app_config: BasicMemoryConfig) -> None:
@@ -195,14 +199,13 @@ def ensure_initialization(app_config: BasicMemoryConfig) -> None:
This is a wrapper for the async initialize_app function that can be
called from synchronous code like CLI entry points.
No-op if app_config.cloud_mode == True. Cloud basic memory manages it's own projects
No-op if database backend is Postgres (cloud deployment manages its own schema).
Args:
app_config: The Basic Memory project configuration
"""
# Skip initialization in cloud mode - cloud manages its own projects
if app_config.cloud_mode_enabled:
logger.debug("Skipping initialization in cloud mode - projects managed by cloud")
if app_config.database_backend == DatabaseBackend.POSTGRES:
logger.info("Skipping local initialization - Postgres backend manages its own schema")
return
async def _init_and_cleanup():
+3 -1
View File
@@ -340,7 +340,9 @@ class LinkResolver:
if not project:
project = await self._project_repository.get_by_name_case_insensitive(identifier)
if not project:
project = await self._project_repository.get_by_permalink(generate_permalink(identifier))
project = await self._project_repository.get_by_permalink(
generate_permalink(identifier)
)
if project:
self._project_cache_by_identifier[cache_key] = project
+28 -20
View File
@@ -20,7 +20,13 @@ from basic_memory.schemas import (
ProjectStatistics,
SystemStatus,
)
from basic_memory.config import WATCH_STATUS_JSON, ConfigManager, get_project_config, ProjectConfig
from basic_memory.config import (
WATCH_STATUS_JSON,
ConfigManager,
ProjectEntry,
get_project_config,
ProjectConfig,
)
from basic_memory.utils import generate_permalink
@@ -62,20 +68,20 @@ class ProjectService:
return self.config_manager.projects
@property
def default_project(self) -> str:
def default_project(self) -> Optional[str]:
"""Get the name of the default project.
Returns:
The name of the default project
The name of the default project, or None if not set
"""
return self.config_manager.default_project
@property
def current_project(self) -> str:
def current_project(self) -> Optional[str]:
"""Get the name of the currently active project.
Returns:
The name of the current project
The name of the current project, or None if not set
"""
return os.environ.get("BASIC_MEMORY_PROJECT", self.config_manager.default_project)
@@ -340,7 +346,9 @@ class ProjectService:
# No default project - set the config default as default
# This is defensive code for edge cases where no default exists
config_default = self.config_manager.default_project # pragma: no cover
config_project = await self.repository.get_by_name(config_default) # pragma: no cover
config_project = (
await self.repository.get_by_name(config_default) if config_default else None
) # pragma: no cover
if config_project: # pragma: no cover
await self.repository.set_as_default(config_project.id) # pragma: no cover
logger.info(
@@ -364,11 +372,12 @@ class ProjectService:
db_projects_by_permalink = {p.permalink: p for p in db_projects}
# Get all projects from configuration and normalize names if needed
config_projects = self.config_manager.projects.copy()
updated_config = {}
# Use .config property (not load_config()) so tests can patch ConfigManager.config
config = self.config_manager.config
updated_config: Dict[str, ProjectEntry] = {}
config_updated = False
for name, path in config_projects.items():
for name, entry in config.projects.items():
# Generate normalized name (what the database expects)
normalized_name = generate_permalink(name)
@@ -376,25 +385,24 @@ class ProjectService:
logger.info(f"Normalizing project name in config: '{name}' -> '{normalized_name}'")
config_updated = True
updated_config[normalized_name] = path
updated_config[normalized_name] = entry
# Update the configuration if any changes were made
if config_updated:
config = self.config_manager.load_config()
config.projects = updated_config
self.config_manager.save_config(config)
logger.info("Config updated with normalized project names")
# Use the normalized config for further processing
config_projects = updated_config
# Use the normalized config for further processing — keys are now project names
config_project_names = updated_config
# Add projects that exist in config but not in DB
for name, path in config_projects.items():
for name, entry in config_project_names.items():
if name not in db_projects_by_permalink:
logger.info(f"Adding project '{name}' to database")
project_data = {
"name": name,
"path": path,
"path": entry.path,
"permalink": generate_permalink(name),
"is_active": True,
# Don't set is_default here - let the enforcement logic handle it
@@ -405,7 +413,7 @@ class ProjectService:
# Config is the source of truth - if a project was deleted from config,
# it should be deleted from DB too (fixes issue #193)
for name, project in db_projects_by_permalink.items():
if name not in config_projects:
if name not in config_project_names:
logger.info(
f"Removing project '{name}' from database (deleted from config, source of truth)"
)
@@ -456,8 +464,8 @@ class ProjectService:
# Update in configuration
config = self.config_manager.load_config()
old_path = config.projects[name]
config.projects[name] = resolved_path
old_path = config.projects[name].path
config.projects[name].path = resolved_path
self.config_manager.save_config(config)
# Update in database using robust lookup
@@ -468,7 +476,7 @@ class ProjectService:
else:
logger.error(f"Project '{name}' exists in config but not in database")
# Restore the old path in config since DB update failed
config.projects[name] = old_path
config.projects[name].path = old_path
self.config_manager.save_config(config)
raise ValueError(f"Project '{name}' not found in database")
@@ -504,7 +512,7 @@ class ProjectService:
# Update in config
config = self.config_manager.load_config()
config.projects[name] = resolved_path
config.projects[name].path = resolved_path
self.config_manager.save_config(config)
# Update in database
@@ -135,6 +135,7 @@ class SearchService:
after_date=after_date,
metadata_filters=metadata_filters,
retrieval_mode=query.retrieval_mode or SearchRetrievalMode.FTS,
min_similarity=query.min_similarity,
limit=limit,
offset=offset,
)