Pre release fixups (#5)

* configure logging

* set mcp output logging also

* fix type check errors

* fix type check

* rename Permalink schema type

* fix type errors

* add typechecks to ci workflow

* pytest coverage setup

* add tests for status cli

* sync tests coverage

* watch_service test coverage

* tests for tool_utils.py

* clean up imports

* file_utils coverage

* markdown plugins coverage

* 99% test coverage

* more test coverage, remove ObservationCategory

* more tool coverage

* fix type-check

* format, upgrade deps

---------

Co-authored-by: phernandez <phernandez@basicmachines.co>
This commit is contained in:
Paul Hernandez
2025-02-12 08:23:10 -06:00
committed by GitHub
parent 7f7ec67cbb
commit 5b4f0eafcc
104 changed files with 2587 additions and 2863 deletions
+2 -1
View File
@@ -1,4 +1,5 @@
"""Basic Memory API module."""
from .app import app
__all__ = ["app"]
__all__ = ["app"]
+4 -5
View File
@@ -13,10 +13,10 @@ from basic_memory.services import DatabaseService
@asynccontextmanager
async def lifespan(app: FastAPI):
async def lifespan(app: FastAPI): # pragma: no cover
"""Lifecycle manager for the FastAPI app."""
logger.info("Starting Basic Memory API")
# check the db state
await check_db(app)
yield
@@ -24,7 +24,7 @@ async def lifespan(app: FastAPI):
await db.shutdown_db()
async def check_db(app: FastAPI):
async def check_db(app: FastAPI): # pragma: no cover
logger.info("Checking database state")
# Initialize DB management service
@@ -40,7 +40,6 @@ async def check_db(app: FastAPI):
await db_service.cleanup_backups()
# Initialize FastAPI app
app = FastAPI(
title="Basic Memory API",
@@ -57,7 +56,7 @@ app.include_router(resource.router)
@app.exception_handler(Exception)
async def exception_handler(request, exc):
async def exception_handler(request, exc): # pragma: no cover
logger.exception(
f"An unhandled exception occurred for request '{request.url}', exception: {exc}"
)
@@ -17,7 +17,7 @@ from basic_memory.schemas import (
DeleteEntitiesResponse,
DeleteEntitiesRequest,
)
from basic_memory.schemas.base import PathId, Entity
from basic_memory.schemas.base import Permalink, Entity
from basic_memory.services.exceptions import EntityNotFoundError
router = APIRouter(prefix="/knowledge", tags=["knowledge"])
@@ -27,10 +27,10 @@ router = APIRouter(prefix="/knowledge", tags=["knowledge"])
@router.post("/entities", response_model=EntityResponse)
async def create_entity(
data: Entity,
background_tasks: BackgroundTasks,
entity_service: EntityServiceDep,
search_service: SearchServiceDep,
data: Entity,
background_tasks: BackgroundTasks,
entity_service: EntityServiceDep,
search_service: SearchServiceDep,
) -> EntityResponse:
"""Create an entity."""
logger.info(f"request: create_entity with data={data}")
@@ -47,12 +47,12 @@ async def create_entity(
@router.put("/entities/{permalink:path}", response_model=EntityResponse)
async def create_or_update_entity(
permalink: PathId,
data: Entity,
response: Response,
background_tasks: BackgroundTasks,
entity_service: EntityServiceDep,
search_service: SearchServiceDep,
permalink: Permalink,
data: Entity,
response: Response,
background_tasks: BackgroundTasks,
entity_service: EntityServiceDep,
search_service: SearchServiceDep,
) -> EntityResponse:
"""Create or update an entity. If entity exists, it will be updated, otherwise created."""
logger.info(f"request: create_or_update_entity with permalink={permalink}, data={data}")
@@ -69,7 +69,9 @@ async def create_or_update_entity(
await search_service.index_entity(entity, background_tasks=background_tasks)
result = EntityResponse.model_validate(entity)
logger.info(f"response: create_or_update_entity with result={result}, status_code={response.status_code}")
logger.info(
f"response: create_or_update_entity with result={result}, status_code={response.status_code}"
)
return result
@@ -78,8 +80,8 @@ async def create_or_update_entity(
@router.get("/entities/{permalink:path}", response_model=EntityResponse)
async def get_entity(
entity_service: EntityServiceDep,
permalink: str,
entity_service: EntityServiceDep,
permalink: str,
) -> EntityResponse:
"""Get a specific entity by ID.
@@ -102,13 +104,13 @@ async def get_entity(
@router.get("/entities", response_model=EntityListResponse)
async def get_entities(
entity_service: EntityServiceDep,
permalink: Annotated[list[str] | None, Query()] = None,
entity_service: EntityServiceDep,
permalink: Annotated[list[str] | None, Query()] = None,
) -> EntityListResponse:
"""Open specific entities"""
logger.info(f"request: get_entities with permalinks={permalink}")
entities = await entity_service.get_entities_by_permalinks(permalink)
entities = await entity_service.get_entities_by_permalinks(permalink) if permalink else []
result = EntityListResponse(
entities=[EntityResponse.model_validate(entity) for entity in entities]
)
@@ -122,11 +124,11 @@ async def get_entities(
@router.delete("/entities/{identifier:path}", response_model=DeleteEntitiesResponse)
async def delete_entity(
identifier: str,
background_tasks: BackgroundTasks,
entity_service: EntityServiceDep,
link_resolver: LinkResolverDep,
search_service=Depends(get_search_service),
identifier: str,
background_tasks: BackgroundTasks,
entity_service: EntityServiceDep,
link_resolver: LinkResolverDep,
search_service=Depends(get_search_service),
) -> DeleteEntitiesResponse:
"""Delete a single entity and remove from search index."""
logger.info(f"request: delete_entity with identifier={identifier}")
@@ -149,10 +151,10 @@ async def delete_entity(
@router.post("/entities/delete", response_model=DeleteEntitiesResponse)
async def delete_entities(
data: DeleteEntitiesRequest,
background_tasks: BackgroundTasks,
entity_service: EntityServiceDep,
search_service=Depends(get_search_service),
data: DeleteEntitiesRequest,
background_tasks: BackgroundTasks,
entity_service: EntityServiceDep,
search_service=Depends(get_search_service),
) -> DeleteEntitiesResponse:
"""Delete entities and remove from search index."""
logger.info(f"request: delete_entities with data={data}")
+17 -11
View File
@@ -1,13 +1,11 @@
"""Routes for memory:// URI operations."""
from datetime import datetime, timedelta
from typing import Optional, List, Annotated
from typing import Annotated
from dateparser import parse
from fastapi import APIRouter, Query
from loguru import logger
from basic_memory.config import config
from basic_memory.deps import ContextServiceDep, EntityRepositoryDep
from basic_memory.repository import EntityRepository
from basic_memory.repository.search_repository import SearchIndexRow
@@ -17,7 +15,8 @@ from basic_memory.schemas.memory import (
RelationSummary,
EntitySummary,
ObservationSummary,
MemoryMetadata, normalize_memory_url,
MemoryMetadata,
normalize_memory_url,
)
from basic_memory.schemas.search import SearchItemType
from basic_memory.services.context_service import ContextResultRow
@@ -25,12 +24,14 @@ from basic_memory.services.context_service import ContextResultRow
router = APIRouter(prefix="/memory", tags=["memory"])
async def to_graph_context(context, entity_repository: EntityRepository):
# return results
async def to_summary(item: SearchIndexRow | ContextResultRow):
match item.type:
case SearchItemType.ENTITY:
assert item.title is not None
assert item.created_at is not None
return EntitySummary(
title=item.title,
permalink=item.permalink,
@@ -38,12 +39,18 @@ async def to_graph_context(context, entity_repository: EntityRepository):
created_at=item.created_at,
)
case SearchItemType.OBSERVATION:
assert item.category is not None
assert item.content is not None
return ObservationSummary(
category=item.category, content=item.content, permalink=item.permalink
)
case SearchItemType.RELATION:
assert item.from_id is not None
from_entity = await entity_repository.find_by_id(item.from_id)
to_entity = await entity_repository.find_by_id(item.to_id)
assert from_entity is not None
to_entity = await entity_repository.find_by_id(item.to_id) if item.to_id else None
return RelationSummary(
permalink=item.permalink,
@@ -51,6 +58,8 @@ async def to_graph_context(context, entity_repository: EntityRepository):
from_id=from_entity.permalink,
to_id=to_entity.permalink if to_entity else None,
)
case _: # pragma: no cover
raise ValueError(f"Unexpected type: {item.type}")
primary_results = [await to_summary(r) for r in context["primary_results"]]
related_results = [await to_summary(r) for r in context["related_results"]]
@@ -61,7 +70,6 @@ async def to_graph_context(context, entity_repository: EntityRepository):
)
@router.get("/recent", response_model=GraphContext)
async def recent(
context_service: ContextServiceDep,
@@ -91,7 +99,8 @@ async def recent(
return await to_graph_context(context, entity_repository=entity_repository)
# get_memory_context needs to be declared last so other paths can match
# get_memory_context needs to be declared last so other paths can match
@router.get("/{uri:path}", response_model=GraphContext)
async def get_memory_context(
@@ -118,6 +127,3 @@ async def get_memory_context(
memory_url, depth=depth, since=since, max_results=max_results
)
return await to_graph_context(context, entity_repository=entity_repository)
+6 -12
View File
@@ -1,34 +1,28 @@
"""Router for search operations."""
from dataclasses import asdict
from fastapi import APIRouter, Depends, BackgroundTasks
from typing import List
from loguru import logger
from basic_memory.services.search_service import SearchService
from basic_memory.schemas.search import SearchQuery, SearchResult, SearchResponse
from basic_memory.deps import get_search_service
router = APIRouter(prefix="/search", tags=["search"])
@router.post("/", response_model=SearchResponse)
async def search(
query: SearchQuery,
search_service: SearchService = Depends(get_search_service)
):
async def search(query: SearchQuery, search_service: SearchService = Depends(get_search_service)):
"""Search across all knowledge and documents."""
results = await search_service.search(query)
search_results = [SearchResult.model_validate(asdict(r)) for r in results]
return SearchResponse(results=search_results)
@router.post("/reindex")
async def reindex(
background_tasks: BackgroundTasks,
search_service: SearchService = Depends(get_search_service)
background_tasks: BackgroundTasks, search_service: SearchService = Depends(get_search_service)
):
"""Recreate and populate the search index."""
await search_service.reindex_all(background_tasks=background_tasks)
return {
"status": "ok",
"message": "Reindex initiated"
}
return {"status": "ok", "message": "Reindex initiated"}