mirror of
https://github.com/basicmachines-co/basic-memory
synced 2026-06-21 13:47:35 +00:00
add logfire instrumentation to services and repository code
Signed-off-by: phernandez <paul@basicmachines.co>
This commit is contained in:
+1
-1
@@ -34,7 +34,7 @@ dependencies = [
|
||||
"python-dotenv>=1.1.0",
|
||||
"pytest-aio>=1.9.0",
|
||||
"aiofiles>=24.1.0", # Async file I/O
|
||||
"logfire>=0.73.0", # Optional observability (disabled by default via config)
|
||||
"logfire[fastapi]>=0.73.0", # Optional observability (disabled by default via config)
|
||||
"asyncpg>=0.30.0",
|
||||
"nest-asyncio>=1.6.0", # For Alembic migrations with Postgres
|
||||
]
|
||||
|
||||
@@ -6,6 +6,7 @@ from contextlib import asynccontextmanager
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from fastapi.exception_handlers import http_exception_handler
|
||||
from loguru import logger
|
||||
import logfire
|
||||
|
||||
from basic_memory import __version__ as version
|
||||
from basic_memory import db
|
||||
@@ -100,7 +101,8 @@ app.include_router(v2_project, prefix="/v2")
|
||||
app.include_router(project.project_resource_router)
|
||||
app.include_router(management.router)
|
||||
|
||||
# Auth routes are handled by FastMCP automatically when auth is enabled
|
||||
# instrument app with logfire
|
||||
logfire.instrument_fastapi(app)
|
||||
|
||||
|
||||
@app.exception_handler(Exception)
|
||||
|
||||
@@ -368,11 +368,10 @@ MarkdownProcessorV2Dep = Annotated[MarkdownProcessor, Depends(get_markdown_proce
|
||||
async def get_file_service(
|
||||
project_config: ProjectConfigDep, markdown_processor: MarkdownProcessorDep
|
||||
) -> FileService:
|
||||
logger.debug(
|
||||
f"Creating FileService for project: {project_config.name}, base_path: {project_config.home}"
|
||||
)
|
||||
file_service = FileService(project_config.home, markdown_processor)
|
||||
logger.debug(f"Created FileService for project: {file_service} ")
|
||||
logger.debug(
|
||||
f"Created FileService for project: {project_config.name}, base_path: {project_config.home} "
|
||||
)
|
||||
return file_service
|
||||
|
||||
|
||||
@@ -382,11 +381,10 @@ FileServiceDep = Annotated[FileService, Depends(get_file_service)]
|
||||
async def get_file_service_v2(
|
||||
project_config: ProjectConfigV2Dep, markdown_processor: MarkdownProcessorV2Dep
|
||||
) -> FileService:
|
||||
logger.debug(
|
||||
f"Creating FileService for project: {project_config.name}, base_path: {project_config.home}"
|
||||
)
|
||||
file_service = FileService(project_config.home, markdown_processor)
|
||||
logger.debug(f"Created FileService for project: {file_service} ")
|
||||
logger.debug(
|
||||
f"Created FileService for project: {project_config.name}, base_path: {project_config.home}"
|
||||
)
|
||||
return file_service
|
||||
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from pathlib import Path
|
||||
from typing import List, Optional, Sequence, Union, Any
|
||||
|
||||
import logfire
|
||||
from loguru import logger
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
@@ -32,6 +33,7 @@ class EntityRepository(Repository[Entity]):
|
||||
"""
|
||||
super().__init__(session_maker, Entity, project_id=project_id)
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def get_by_id(self, entity_id: int) -> Optional[Entity]:
|
||||
"""Get entity by numeric ID.
|
||||
|
||||
@@ -44,6 +46,7 @@ class EntityRepository(Repository[Entity]):
|
||||
async with db.scoped_session(self.session_maker) as session:
|
||||
return await self.select_by_id(session, entity_id)
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def get_by_permalink(self, permalink: str) -> Optional[Entity]:
|
||||
"""Get entity by permalink.
|
||||
|
||||
@@ -53,6 +56,7 @@ class EntityRepository(Repository[Entity]):
|
||||
query = self.select().where(Entity.permalink == permalink).options(*self.get_load_options())
|
||||
return await self.find_one(query)
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def get_by_title(self, title: str) -> Sequence[Entity]:
|
||||
"""Get entity by title.
|
||||
|
||||
@@ -63,6 +67,7 @@ class EntityRepository(Repository[Entity]):
|
||||
result = await self.execute_query(query)
|
||||
return list(result.scalars().all())
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def get_by_file_path(self, file_path: Union[Path, str]) -> Optional[Entity]:
|
||||
"""Get entity by file_path.
|
||||
|
||||
@@ -76,6 +81,7 @@ class EntityRepository(Repository[Entity]):
|
||||
)
|
||||
return await self.find_one(query)
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def get_by_file_paths(
|
||||
self, session: AsyncSession, file_paths: Sequence[Union[Path, str]]
|
||||
) -> List[Row[Any]]:
|
||||
@@ -104,6 +110,7 @@ class EntityRepository(Repository[Entity]):
|
||||
result = await session.execute(query)
|
||||
return list(result.all())
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def find_by_checksum(self, checksum: str) -> Sequence[Entity]:
|
||||
"""Find entities with the given checksum.
|
||||
|
||||
@@ -121,6 +128,7 @@ class EntityRepository(Repository[Entity]):
|
||||
result = await self.execute_query(query, use_query_options=False)
|
||||
return list(result.scalars().all())
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def find_by_checksums(self, checksums: Sequence[str]) -> Sequence[Entity]:
|
||||
"""Find entities with any of the given checksums (batch query for move detection).
|
||||
|
||||
@@ -149,6 +157,7 @@ class EntityRepository(Repository[Entity]):
|
||||
result = await self.execute_query(query, use_query_options=False)
|
||||
return list(result.scalars().all())
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def delete_by_file_path(self, file_path: Union[Path, str]) -> bool:
|
||||
"""Delete entity with the provided file_path.
|
||||
|
||||
@@ -169,6 +178,7 @@ class EntityRepository(Repository[Entity]):
|
||||
selectinload(Entity.incoming_relations).selectinload(Relation.to_entity),
|
||||
]
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def find_by_permalinks(self, permalinks: List[str]) -> Sequence[Entity]:
|
||||
"""Find multiple entities by their permalink.
|
||||
|
||||
@@ -187,6 +197,7 @@ class EntityRepository(Repository[Entity]):
|
||||
result = await self.execute_query(query)
|
||||
return list(result.scalars().all())
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def upsert_entity(self, entity: Entity) -> Entity:
|
||||
"""Insert or update entity using simple try/catch with database-level conflict resolution.
|
||||
|
||||
@@ -288,6 +299,7 @@ class EntityRepository(Repository[Entity]):
|
||||
entity = await self._handle_permalink_conflict(entity, session)
|
||||
return entity
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def get_all_file_paths(self) -> List[str]:
|
||||
"""Get all file paths for this project - optimized for deletion detection.
|
||||
|
||||
@@ -303,6 +315,7 @@ class EntityRepository(Repository[Entity]):
|
||||
result = await self.execute_query(query, use_query_options=False)
|
||||
return list(result.scalars().all())
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def get_distinct_directories(self) -> List[str]:
|
||||
"""Extract unique directory paths from file_path column.
|
||||
|
||||
@@ -331,6 +344,7 @@ class EntityRepository(Repository[Entity]):
|
||||
|
||||
return sorted(directories)
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def find_by_directory_prefix(self, directory_prefix: str) -> Sequence[Entity]:
|
||||
"""Find entities whose file_path starts with the given directory prefix.
|
||||
|
||||
@@ -363,6 +377,7 @@ class EntityRepository(Repository[Entity]):
|
||||
result = await self.execute_query(query, use_query_options=False)
|
||||
return list(result.scalars().all())
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def _handle_permalink_conflict(self, entity: Entity, session: AsyncSession) -> Entity:
|
||||
"""Handle permalink conflicts by generating a unique permalink."""
|
||||
base_permalink = entity.permalink
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from typing import Dict, List, Sequence
|
||||
|
||||
import logfire
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import async_sessionmaker
|
||||
|
||||
@@ -21,30 +22,35 @@ class ObservationRepository(Repository[Observation]):
|
||||
"""
|
||||
super().__init__(session_maker, Observation, project_id=project_id)
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def find_by_entity(self, entity_id: int) -> Sequence[Observation]:
|
||||
"""Find all observations for a specific entity."""
|
||||
query = select(Observation).filter(Observation.entity_id == entity_id)
|
||||
result = await self.execute_query(query)
|
||||
return result.scalars().all()
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def find_by_context(self, context: str) -> Sequence[Observation]:
|
||||
"""Find observations with a specific context."""
|
||||
query = select(Observation).filter(Observation.context == context)
|
||||
result = await self.execute_query(query)
|
||||
return result.scalars().all()
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def find_by_category(self, category: str) -> Sequence[Observation]:
|
||||
"""Find observations with a specific context."""
|
||||
query = select(Observation).filter(Observation.category == category)
|
||||
result = await self.execute_query(query)
|
||||
return result.scalars().all()
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def observation_categories(self) -> Sequence[str]:
|
||||
"""Return a list of all observation categories."""
|
||||
query = select(Observation.category).distinct()
|
||||
result = await self.execute_query(query, use_query_options=False)
|
||||
return result.scalars().all()
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def find_by_entities(self, entity_ids: List[int]) -> Dict[int, List[Observation]]:
|
||||
"""Find all observations for multiple entities in a single query.
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import re
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
|
||||
import logfire
|
||||
from loguru import logger
|
||||
from sqlalchemy import text
|
||||
|
||||
@@ -25,6 +26,7 @@ class PostgresSearchRepository(SearchRepositoryBase):
|
||||
- JSONB containment operators for metadata search
|
||||
"""
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def init_search_index(self):
|
||||
"""Create Postgres table with tsvector column and GIN indexes.
|
||||
|
||||
@@ -145,6 +147,7 @@ class PostgresSearchRepository(SearchRepositoryBase):
|
||||
else:
|
||||
return cleaned_term
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def search(
|
||||
self,
|
||||
search_text: Optional[str] = None,
|
||||
@@ -312,6 +315,7 @@ class PostgresSearchRepository(SearchRepositoryBase):
|
||||
|
||||
return results
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def bulk_index_items(self, search_index_rows: List[SearchIndexRow]) -> None:
|
||||
"""Index multiple items in a single batch operation using UPSERT.
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from pathlib import Path
|
||||
from typing import Optional, Sequence, Union
|
||||
|
||||
import logfire
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
|
||||
|
||||
@@ -22,6 +23,7 @@ class ProjectRepository(Repository[Project]):
|
||||
"""Initialize with session maker."""
|
||||
super().__init__(session_maker, Project)
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def get_by_name(self, name: str) -> Optional[Project]:
|
||||
"""Get project by name.
|
||||
|
||||
@@ -31,6 +33,7 @@ class ProjectRepository(Repository[Project]):
|
||||
query = self.select().where(Project.name == name)
|
||||
return await self.find_one(query)
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def get_by_permalink(self, permalink: str) -> Optional[Project]:
|
||||
"""Get project by permalink.
|
||||
|
||||
@@ -40,6 +43,7 @@ class ProjectRepository(Repository[Project]):
|
||||
query = self.select().where(Project.permalink == permalink)
|
||||
return await self.find_one(query)
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def get_by_path(self, path: Union[Path, str]) -> Optional[Project]:
|
||||
"""Get project by filesystem path.
|
||||
|
||||
@@ -49,6 +53,7 @@ class ProjectRepository(Repository[Project]):
|
||||
query = self.select().where(Project.path == Path(path).as_posix())
|
||||
return await self.find_one(query)
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def get_by_id(self, project_id: int) -> Optional[Project]:
|
||||
"""Get project by numeric ID.
|
||||
|
||||
@@ -61,17 +66,20 @@ class ProjectRepository(Repository[Project]):
|
||||
async with db.scoped_session(self.session_maker) as session:
|
||||
return await self.select_by_id(session, project_id)
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def get_default_project(self) -> Optional[Project]:
|
||||
"""Get the default project (the one marked as is_default=True)."""
|
||||
query = self.select().where(Project.is_default.is_not(None))
|
||||
return await self.find_one(query)
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def get_active_projects(self) -> Sequence[Project]:
|
||||
"""Get all active projects."""
|
||||
query = self.select().where(Project.is_active == True) # noqa: E712
|
||||
result = await self.execute_query(query)
|
||||
return list(result.scalars().all())
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def set_as_default(self, project_id: int) -> Optional[Project]:
|
||||
"""Set a project as the default and unset previous default.
|
||||
|
||||
@@ -96,6 +104,7 @@ class ProjectRepository(Repository[Project]):
|
||||
return target_project
|
||||
return None # pragma: no cover
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def update_path(self, project_id: int, new_path: str) -> Optional[Project]:
|
||||
"""Update project path.
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"""Repository for managing Relation objects."""
|
||||
|
||||
from sqlalchemy import and_, delete
|
||||
from typing import Sequence, List, Optional
|
||||
|
||||
from sqlalchemy import select
|
||||
import logfire
|
||||
from sqlalchemy import and_, delete, select
|
||||
from sqlalchemy.ext.asyncio import async_sessionmaker
|
||||
from sqlalchemy.orm import selectinload, aliased
|
||||
from sqlalchemy.orm.interfaces import LoaderOption
|
||||
@@ -25,6 +25,7 @@ class RelationRepository(Repository[Relation]):
|
||||
"""
|
||||
super().__init__(session_maker, Relation, project_id=project_id)
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def find_relation(
|
||||
self, from_permalink: str, to_permalink: str, relation_type: str
|
||||
) -> Optional[Relation]:
|
||||
@@ -46,18 +47,21 @@ class RelationRepository(Repository[Relation]):
|
||||
)
|
||||
return await self.find_one(query)
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def find_by_entities(self, from_id: int, to_id: int) -> Sequence[Relation]:
|
||||
"""Find all relations between two entities."""
|
||||
query = select(Relation).where((Relation.from_id == from_id) & (Relation.to_id == to_id))
|
||||
result = await self.execute_query(query)
|
||||
return result.scalars().all()
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def find_by_type(self, relation_type: str) -> Sequence[Relation]:
|
||||
"""Find all relations of a specific type."""
|
||||
query = select(Relation).filter(Relation.relation_type == relation_type)
|
||||
result = await self.execute_query(query)
|
||||
return result.scalars().all()
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def delete_outgoing_relations_from_entity(self, entity_id: int) -> None:
|
||||
"""Delete outgoing relations for an entity.
|
||||
|
||||
@@ -67,12 +71,14 @@ class RelationRepository(Repository[Relation]):
|
||||
async with db.scoped_session(self.session_maker) as session:
|
||||
await session.execute(delete(Relation).where(Relation.from_id == entity_id))
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def find_unresolved_relations(self) -> Sequence[Relation]:
|
||||
"""Find all unresolved relations, where to_id is null."""
|
||||
query = select(Relation).filter(Relation.to_id.is_(None))
|
||||
result = await self.execute_query(query)
|
||||
return result.scalars().all()
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def find_unresolved_relations_for_entity(self, entity_id: int) -> Sequence[Relation]:
|
||||
"""Find unresolved relations for a specific entity.
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from typing import Type, Optional, Any, Sequence, TypeVar, List, Dict
|
||||
|
||||
import logfire
|
||||
from loguru import logger
|
||||
from sqlalchemy import (
|
||||
select,
|
||||
@@ -84,6 +85,7 @@ class Repository[T: Base]:
|
||||
result = await session.execute(query)
|
||||
return result.scalars().one_or_none()
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def select_by_ids(self, session: AsyncSession, ids: List[int]) -> Sequence[T]:
|
||||
"""Select multiple entities by IDs using an existing session."""
|
||||
query = (
|
||||
@@ -95,6 +97,7 @@ class Repository[T: Base]:
|
||||
result = await session.execute(query)
|
||||
return result.scalars().all()
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def add(self, model: T) -> T:
|
||||
"""
|
||||
Add a model to the repository. This will also add related objects
|
||||
@@ -121,6 +124,7 @@ class Repository[T: Base]:
|
||||
)
|
||||
return found
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def add_all(self, models: List[T]) -> Sequence[T]:
|
||||
"""
|
||||
Add a list of models to the repository. This will also add related objects
|
||||
@@ -152,6 +156,7 @@ class Repository[T: Base]:
|
||||
# Add project filter if applicable
|
||||
return self._add_project_filter(query)
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def find_all(
|
||||
self, skip: int = 0, limit: Optional[int] = None, use_load_options: bool = True
|
||||
) -> Sequence[T]:
|
||||
@@ -183,6 +188,7 @@ class Repository[T: Base]:
|
||||
logger.debug(f"Found {len(items)} {self.Model.__name__} records")
|
||||
return items
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def find_by_id(self, entity_id: int) -> Optional[T]:
|
||||
"""Fetch an entity by its unique identifier."""
|
||||
logger.debug(f"Finding {self.Model.__name__} by ID: {entity_id}")
|
||||
@@ -190,6 +196,7 @@ class Repository[T: Base]:
|
||||
async with db.scoped_session(self.session_maker) as session:
|
||||
return await self.select_by_id(session, entity_id)
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def find_by_ids(self, ids: List[int]) -> Sequence[T]:
|
||||
"""Fetch multiple entities by their identifiers in a single query."""
|
||||
logger.debug(f"Finding {self.Model.__name__} by IDs: {ids}")
|
||||
@@ -197,6 +204,7 @@ class Repository[T: Base]:
|
||||
async with db.scoped_session(self.session_maker) as session:
|
||||
return await self.select_by_ids(session, ids)
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def find_one(self, query: Select[tuple[T]]) -> Optional[T]:
|
||||
"""Execute a query and retrieve a single record."""
|
||||
# add in load options
|
||||
@@ -210,6 +218,7 @@ class Repository[T: Base]:
|
||||
logger.trace(f"No {self.Model.__name__} found")
|
||||
return entity
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def create(self, data: dict) -> T:
|
||||
"""Create a new record from a model instance."""
|
||||
logger.debug(f"Creating {self.Model.__name__} from entity_data: {data}")
|
||||
@@ -241,6 +250,7 @@ class Repository[T: Base]:
|
||||
)
|
||||
return return_instance
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def create_all(self, data_list: List[dict]) -> Sequence[T]:
|
||||
"""Create multiple records in a single transaction."""
|
||||
logger.debug(f"Bulk creating {len(data_list)} {self.Model.__name__} instances")
|
||||
@@ -266,6 +276,7 @@ class Repository[T: Base]:
|
||||
|
||||
return await self.select_by_ids(session, [model.id for model in model_list]) # pyright: ignore [reportAttributeAccessIssue]
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def update(self, entity_id: int, entity_data: dict | T) -> Optional[T]:
|
||||
"""Update an entity with the given data."""
|
||||
logger.debug(f"Updating {self.Model.__name__} {entity_id} with data: {entity_data}")
|
||||
@@ -295,6 +306,7 @@ class Repository[T: Base]:
|
||||
logger.debug(f"No {self.Model.__name__} found to update: {entity_id}")
|
||||
return None
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def delete(self, entity_id: int) -> bool:
|
||||
"""Delete an entity from the database."""
|
||||
logger.debug(f"Deleting {self.Model.__name__}: {entity_id}")
|
||||
@@ -312,6 +324,7 @@ class Repository[T: Base]:
|
||||
logger.debug(f"No {self.Model.__name__} found to delete: {entity_id}")
|
||||
return False
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def delete_by_ids(self, ids: List[int]) -> int:
|
||||
"""Delete records matching given IDs."""
|
||||
logger.debug(f"Deleting {self.Model.__name__} by ids: {ids}")
|
||||
@@ -327,6 +340,7 @@ class Repository[T: Base]:
|
||||
logger.debug(f"Deleted {result.rowcount} records")
|
||||
return result.rowcount
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def delete_by_fields(self, **filters: Any) -> bool:
|
||||
"""Delete records matching given field values."""
|
||||
logger.debug(f"Deleting {self.Model.__name__} by fields: {filters}")
|
||||
@@ -343,6 +357,7 @@ class Repository[T: Base]:
|
||||
logger.debug(f"Deleted {result.rowcount} records")
|
||||
return deleted
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def count(self, query: Executable | None = None) -> int:
|
||||
"""Count entities in the database table."""
|
||||
async with db.scoped_session(self.session_maker) as session:
|
||||
@@ -364,6 +379,7 @@ class Repository[T: Base]:
|
||||
logger.debug(f"Counted {count} {self.Model.__name__} records")
|
||||
return count
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def execute_query(
|
||||
self,
|
||||
query: Executable,
|
||||
|
||||
@@ -4,6 +4,7 @@ from abc import ABC, abstractmethod
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
import logfire
|
||||
from loguru import logger
|
||||
from sqlalchemy import Executable, Result, text
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
|
||||
@@ -102,6 +103,7 @@ class SearchRepositoryBase(ABC):
|
||||
"""
|
||||
pass
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def index_item(self, search_index_row: SearchIndexRow) -> None:
|
||||
"""Index or update a single item.
|
||||
|
||||
@@ -145,6 +147,7 @@ class SearchRepositoryBase(ABC):
|
||||
logger.debug(f"indexed row {search_index_row}")
|
||||
await session.commit()
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def bulk_index_items(self, search_index_rows: List[SearchIndexRow]) -> None:
|
||||
"""Index multiple items in a single batch operation.
|
||||
|
||||
@@ -192,6 +195,7 @@ class SearchRepositoryBase(ABC):
|
||||
logger.debug(f"Bulk indexed {len(search_index_rows)} rows")
|
||||
await session.commit()
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def delete_by_entity_id(self, entity_id: int) -> None:
|
||||
"""Delete all search index entries for an entity.
|
||||
|
||||
@@ -206,6 +210,7 @@ class SearchRepositoryBase(ABC):
|
||||
)
|
||||
await session.commit()
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def delete_by_permalink(self, permalink: str) -> None:
|
||||
"""Delete a search index entry by permalink.
|
||||
|
||||
@@ -220,6 +225,7 @@ class SearchRepositoryBase(ABC):
|
||||
)
|
||||
await session.commit()
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def execute_query(
|
||||
self,
|
||||
query: Executable,
|
||||
|
||||
@@ -5,6 +5,7 @@ import re
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
|
||||
import logfire
|
||||
from loguru import logger
|
||||
from sqlalchemy import text
|
||||
|
||||
@@ -25,6 +26,7 @@ class SQLiteSearchRepository(SearchRepositoryBase):
|
||||
- Prefix wildcard matching with *
|
||||
"""
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def init_search_index(self):
|
||||
"""Create FTS5 virtual table for search.
|
||||
|
||||
@@ -279,6 +281,7 @@ class SQLiteSearchRepository(SearchRepositoryBase):
|
||||
# For non-Boolean queries, use the single term preparation logic
|
||||
return self._prepare_single_term(term, is_prefix)
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def search(
|
||||
self,
|
||||
search_text: Optional[str] = None,
|
||||
|
||||
@@ -4,6 +4,7 @@ from dataclasses import dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
import logfire
|
||||
from loguru import logger
|
||||
from sqlalchemy import text
|
||||
|
||||
@@ -85,6 +86,7 @@ class ContextService:
|
||||
self.entity_repository = entity_repository
|
||||
self.observation_repository = observation_repository
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def build_context(
|
||||
self,
|
||||
memory_url: Optional[MemoryUrl] = None,
|
||||
@@ -215,6 +217,7 @@ class ContextService:
|
||||
# Return the structured ContextResult
|
||||
return ContextResult(results=context_results, metadata=metadata)
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def find_related(
|
||||
self,
|
||||
type_id_pairs: List[Tuple[str, int]],
|
||||
|
||||
@@ -4,6 +4,7 @@ import fnmatch
|
||||
import logging
|
||||
import os
|
||||
from typing import Dict, List, Optional, Sequence
|
||||
import logfire
|
||||
|
||||
from basic_memory.models import Entity
|
||||
from basic_memory.repository import EntityRepository
|
||||
@@ -23,6 +24,7 @@ class DirectoryService:
|
||||
"""
|
||||
self.entity_repository = entity_repository
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def get_directory_tree(self) -> DirectoryNode:
|
||||
"""Build a hierarchical directory tree from indexed files."""
|
||||
|
||||
@@ -90,6 +92,7 @@ class DirectoryService:
|
||||
# Return the root node with its children
|
||||
return root_node
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def get_directory_structure(self) -> DirectoryNode:
|
||||
"""Build a hierarchical directory structure without file details.
|
||||
|
||||
@@ -133,6 +136,7 @@ class DirectoryService:
|
||||
|
||||
return root_node
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def list_directory(
|
||||
self,
|
||||
dir_name: str = "/",
|
||||
|
||||
@@ -7,6 +7,7 @@ import frontmatter
|
||||
import yaml
|
||||
from loguru import logger
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
import logfire
|
||||
|
||||
from basic_memory.config import ProjectConfig, BasicMemoryConfig
|
||||
from basic_memory.file_utils import (
|
||||
@@ -52,6 +53,7 @@ class EntityService(BaseService[EntityModel]):
|
||||
self.link_resolver = link_resolver
|
||||
self.app_config = app_config
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def detect_file_path_conflicts(
|
||||
self, file_path: str, skip_check: bool = False
|
||||
) -> List[Entity]:
|
||||
@@ -91,6 +93,7 @@ class EntityService(BaseService[EntityModel]):
|
||||
|
||||
return conflicts
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def resolve_permalink(
|
||||
self,
|
||||
file_path: Permalink | Path,
|
||||
@@ -149,6 +152,7 @@ class EntityService(BaseService[EntityModel]):
|
||||
|
||||
return permalink
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def create_or_update_entity(self, schema: EntitySchema) -> Tuple[EntityModel, bool]:
|
||||
"""Create new entity or update existing one.
|
||||
Returns: (entity, is_new) where is_new is True if a new entity was created
|
||||
@@ -170,6 +174,7 @@ class EntityService(BaseService[EntityModel]):
|
||||
# Create new entity
|
||||
return await self.create_entity(schema), True
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def create_entity(self, schema: EntitySchema) -> EntityModel:
|
||||
"""Create a new entity and write to filesystem."""
|
||||
logger.debug(f"Creating entity: {schema.title}")
|
||||
@@ -236,6 +241,7 @@ class EntityService(BaseService[EntityModel]):
|
||||
# Set final checksum to mark complete
|
||||
return await self.repository.update(entity.id, {"checksum": checksum})
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def update_entity(self, entity: EntityModel, schema: EntitySchema) -> EntityModel:
|
||||
"""Update an entity's content and metadata."""
|
||||
logger.debug(
|
||||
@@ -316,6 +322,7 @@ class EntityService(BaseService[EntityModel]):
|
||||
|
||||
return entity
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def delete_entity(self, permalink_or_id: str | int) -> bool:
|
||||
"""Delete entity and its file."""
|
||||
logger.debug(f"Deleting entity: {permalink_or_id}")
|
||||
@@ -345,6 +352,7 @@ class EntityService(BaseService[EntityModel]):
|
||||
logger.info(f"Entity not found: {permalink_or_id}")
|
||||
return True # Already deleted
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def get_by_permalink(self, permalink: str) -> EntityModel:
|
||||
"""Get entity by type and name combination."""
|
||||
logger.debug(f"Getting entity by permalink: {permalink}")
|
||||
@@ -353,20 +361,24 @@ class EntityService(BaseService[EntityModel]):
|
||||
raise EntityNotFoundError(f"Entity not found: {permalink}")
|
||||
return db_entity
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def get_entities_by_id(self, ids: List[int]) -> Sequence[EntityModel]:
|
||||
"""Get specific entities and their relationships."""
|
||||
logger.debug(f"Getting entities: {ids}")
|
||||
return await self.repository.find_by_ids(ids)
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def get_entities_by_permalinks(self, permalinks: List[str]) -> Sequence[EntityModel]:
|
||||
"""Get specific nodes and their relationships."""
|
||||
logger.debug(f"Getting entities permalinks: {permalinks}")
|
||||
return await self.repository.find_by_permalinks(permalinks)
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def delete_entity_by_file_path(self, file_path: Union[str, Path]) -> None:
|
||||
"""Delete entity by file path."""
|
||||
await self.repository.delete_by_file_path(str(file_path))
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def create_entity_from_markdown(
|
||||
self, file_path: Path, markdown: EntityMarkdown
|
||||
) -> EntityModel:
|
||||
@@ -390,6 +402,7 @@ class EntityService(BaseService[EntityModel]):
|
||||
logger.error(f"Failed to upsert entity for {file_path}: {e}")
|
||||
raise EntityCreationError(f"Failed to create entity: {str(e)}") from e
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def update_entity_and_observations(
|
||||
self, file_path: Path, markdown: EntityMarkdown
|
||||
) -> EntityModel:
|
||||
@@ -430,6 +443,7 @@ class EntityService(BaseService[EntityModel]):
|
||||
db_entity,
|
||||
)
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def update_entity_relations(
|
||||
self,
|
||||
path: str,
|
||||
@@ -501,6 +515,7 @@ class EntityService(BaseService[EntityModel]):
|
||||
|
||||
return await self.repository.get_by_file_path(path)
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def edit_entity(
|
||||
self,
|
||||
identifier: str,
|
||||
@@ -558,6 +573,7 @@ class EntityService(BaseService[EntityModel]):
|
||||
|
||||
return entity
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
def apply_edit_operation(
|
||||
self,
|
||||
current_content: str,
|
||||
@@ -610,6 +626,7 @@ class EntityService(BaseService[EntityModel]):
|
||||
else:
|
||||
raise ValueError(f"Unsupported operation: {operation}")
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
def replace_section_content(
|
||||
self, current_content: str, section_header: str, new_content: str
|
||||
) -> str:
|
||||
@@ -729,6 +746,7 @@ class EntityService(BaseService[EntityModel]):
|
||||
return content + "\n" + current_content # pragma: no cover
|
||||
return content + current_content # pragma: no cover
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def move_entity(
|
||||
self,
|
||||
identifier: str,
|
||||
|
||||
@@ -8,6 +8,7 @@ from pathlib import Path
|
||||
from typing import Any, Dict, Tuple, Union
|
||||
|
||||
import aiofiles
|
||||
import logfire
|
||||
import yaml
|
||||
|
||||
from basic_memory import file_utils
|
||||
@@ -59,6 +60,7 @@ class FileService:
|
||||
"""
|
||||
return self.base_path / entity.file_path
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def read_entity_content(self, entity: EntityModel) -> str:
|
||||
"""Get entity's content without frontmatter or structured sections.
|
||||
|
||||
@@ -77,6 +79,7 @@ class FileService:
|
||||
markdown = await self.markdown_processor.read_file(file_path)
|
||||
return markdown.content or ""
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def delete_entity_file(self, entity: EntityModel) -> None:
|
||||
"""Delete entity file from filesystem.
|
||||
|
||||
@@ -89,6 +92,7 @@ class FileService:
|
||||
path = self.get_entity_path(entity)
|
||||
await self.delete_file(path)
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def exists(self, path: FilePath) -> bool:
|
||||
"""Check if file exists at the provided path.
|
||||
|
||||
@@ -115,6 +119,7 @@ class FileService:
|
||||
logger.error("Failed to check file existence", path=str(path), error=str(e))
|
||||
raise FileOperationError(f"Failed to check file existence: {e}")
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def ensure_directory(self, path: FilePath) -> None:
|
||||
"""Ensure directory exists, creating if necessary.
|
||||
|
||||
@@ -142,6 +147,7 @@ class FileService:
|
||||
logger.error("Failed to create directory", path=str(path), error=str(e))
|
||||
raise FileOperationError(f"Failed to create directory {path}: {e}")
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def write_file(self, path: FilePath, content: str) -> str:
|
||||
"""Write content to file and return checksum.
|
||||
|
||||
@@ -185,6 +191,7 @@ class FileService:
|
||||
logger.exception("File write error", path=str(full_path), error=str(e))
|
||||
raise FileOperationError(f"Failed to write file: {e}")
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def read_file_content(self, path: FilePath) -> str:
|
||||
"""Read file content using true async I/O with aiofiles.
|
||||
|
||||
@@ -220,6 +227,7 @@ class FileService:
|
||||
logger.exception("File read error", path=str(full_path), error=str(e))
|
||||
raise FileOperationError(f"Failed to read file: {e}")
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def read_file(self, path: FilePath) -> Tuple[str, str]:
|
||||
"""Read file and compute checksum using true async I/O.
|
||||
|
||||
@@ -262,6 +270,7 @@ class FileService:
|
||||
logger.exception("File read error", path=str(full_path), error=str(e))
|
||||
raise FileOperationError(f"Failed to read file: {e}")
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def delete_file(self, path: FilePath) -> None:
|
||||
"""Delete file if it exists.
|
||||
|
||||
@@ -276,6 +285,7 @@ class FileService:
|
||||
full_path = path_obj if path_obj.is_absolute() else self.base_path / path_obj
|
||||
full_path.unlink(missing_ok=True)
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def update_frontmatter(self, path: FilePath, updates: Dict[str, Any]) -> str:
|
||||
"""Update frontmatter fields in a file while preserving all content.
|
||||
|
||||
@@ -344,6 +354,7 @@ class FileService:
|
||||
)
|
||||
raise FileOperationError(f"Failed to update frontmatter: {e}")
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def compute_checksum(self, path: FilePath) -> str:
|
||||
"""Compute checksum for a file using true async I/O.
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ to ensure consistent application startup across all entry points.
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
|
||||
import logfire
|
||||
from loguru import logger
|
||||
|
||||
from basic_memory import db
|
||||
@@ -17,6 +18,7 @@ from basic_memory.repository import (
|
||||
)
|
||||
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def initialize_database(app_config: BasicMemoryConfig) -> None:
|
||||
"""Initialize database with migrations handled automatically by get_or_create_db.
|
||||
|
||||
@@ -38,6 +40,7 @@ async def initialize_database(app_config: BasicMemoryConfig) -> None:
|
||||
# more specific error if the database is actually unusable
|
||||
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def reconcile_projects_with_config(app_config: BasicMemoryConfig):
|
||||
"""Ensure all projects in config.json exist in the projects table and vice versa.
|
||||
|
||||
@@ -71,6 +74,7 @@ async def reconcile_projects_with_config(app_config: BasicMemoryConfig):
|
||||
logger.info("Continuing with initialization despite synchronization error")
|
||||
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def initialize_file_sync(
|
||||
app_config: BasicMemoryConfig,
|
||||
):
|
||||
@@ -141,6 +145,7 @@ async def initialize_file_sync(
|
||||
return None
|
||||
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def initialize_app(
|
||||
app_config: BasicMemoryConfig,
|
||||
):
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from typing import Optional, Tuple
|
||||
|
||||
import logfire
|
||||
from loguru import logger
|
||||
|
||||
from basic_memory.models import Entity
|
||||
@@ -26,6 +27,7 @@ class LinkResolver:
|
||||
self.entity_repository = entity_repository
|
||||
self.search_service = search_service
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def resolve_link(
|
||||
self, link_text: str, use_search: bool = True, strict: bool = False
|
||||
) -> Optional[Entity]:
|
||||
|
||||
@@ -8,6 +8,7 @@ from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Dict, Optional, Sequence
|
||||
|
||||
import logfire
|
||||
from loguru import logger
|
||||
from sqlalchemy import text
|
||||
|
||||
@@ -81,6 +82,7 @@ class ProjectService:
|
||||
"""
|
||||
return os.environ.get("BASIC_MEMORY_PROJECT", self.config_manager.default_project)
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def list_projects(self) -> Sequence[Project]:
|
||||
"""List all projects without loading entity relationships.
|
||||
|
||||
@@ -90,6 +92,7 @@ class ProjectService:
|
||||
"""
|
||||
return await self.repository.find_all(use_load_options=False)
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def get_project(self, name: str) -> Optional[Project]:
|
||||
"""Get the file path for a project by name or permalink."""
|
||||
return await self.repository.get_by_name(name) or await self.repository.get_by_permalink(
|
||||
@@ -130,6 +133,7 @@ class ProjectService:
|
||||
# Not nested in either direction
|
||||
return False
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def add_project(self, name: str, path: str, set_default: bool = False) -> None:
|
||||
"""Add a new project to the configuration and database.
|
||||
|
||||
@@ -221,6 +225,7 @@ class ProjectService:
|
||||
|
||||
logger.info(f"Project '{name}' added at {resolved_path}")
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def remove_project(self, name: str, delete_notes: bool = False) -> None:
|
||||
"""Remove a project from configuration and database.
|
||||
|
||||
@@ -271,6 +276,7 @@ class ProjectService:
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to delete project directory {project_path}: {e}")
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def set_default_project(self, name: str) -> None:
|
||||
"""Set the default project in configuration and database.
|
||||
|
||||
@@ -295,6 +301,7 @@ class ProjectService:
|
||||
|
||||
logger.info(f"Project '{name}' set as default in configuration and database")
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def _ensure_single_default_project(self) -> None:
|
||||
"""Ensure only one project has is_default=True.
|
||||
|
||||
@@ -336,6 +343,7 @@ class ProjectService:
|
||||
f"Set '{config_default}' as default project (was missing)"
|
||||
) # pragma: no cover
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def synchronize_projects(self) -> None: # pragma: no cover
|
||||
"""Synchronize projects between database and configuration.
|
||||
|
||||
@@ -420,6 +428,7 @@ class ProjectService:
|
||||
|
||||
logger.info("Project synchronization complete")
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def move_project(self, name: str, new_path: str) -> None:
|
||||
"""Move a project to a new location.
|
||||
|
||||
@@ -461,6 +470,7 @@ class ProjectService:
|
||||
self.config_manager.save_config(config)
|
||||
raise ValueError(f"Project '{name}' not found in database")
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def update_project( # pragma: no cover
|
||||
self, name: str, updated_path: Optional[str] = None, is_active: Optional[bool] = None
|
||||
) -> None:
|
||||
@@ -520,6 +530,7 @@ class ProjectService:
|
||||
f"Changed default project to '{new_default.name}' as '{name}' was deactivated"
|
||||
)
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def get_project_info(self, project_name: Optional[str] = None) -> ProjectInfoResponse:
|
||||
"""Get comprehensive information about the specified Basic Memory project.
|
||||
|
||||
@@ -587,6 +598,7 @@ class ProjectService:
|
||||
system=system,
|
||||
)
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def get_statistics(self, project_id: int) -> ProjectStatistics:
|
||||
"""Get statistics about the specified project.
|
||||
|
||||
@@ -703,6 +715,7 @@ class ProjectService:
|
||||
isolated_entities=isolated_count,
|
||||
)
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def get_activity_metrics(self, project_id: int) -> ActivityMetrics:
|
||||
"""Get activity metrics for the specified project.
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import ast
|
||||
from datetime import datetime
|
||||
from typing import List, Optional, Set
|
||||
|
||||
import logfire
|
||||
from dateparser import parse
|
||||
from fastapi import BackgroundTasks
|
||||
from loguru import logger
|
||||
@@ -35,10 +36,12 @@ class SearchService:
|
||||
self.entity_repository = entity_repository
|
||||
self.file_service = file_service
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def init_search_index(self):
|
||||
"""Create FTS5 virtual table if it doesn't exist."""
|
||||
await self.repository.init_search_index()
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def reindex_all(self, background_tasks: Optional[BackgroundTasks] = None) -> None:
|
||||
"""Reindex all content from database."""
|
||||
|
||||
@@ -55,6 +58,7 @@ class SearchService:
|
||||
|
||||
logger.info("Reindex complete")
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def search(self, query: SearchQuery, limit=10, offset=0) -> List[SearchIndexRow]:
|
||||
"""Search across all indexed content.
|
||||
|
||||
@@ -152,6 +156,7 @@ class SearchService:
|
||||
|
||||
return [] # pragma: no cover
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def index_entity(
|
||||
self,
|
||||
entity: Entity,
|
||||
@@ -163,6 +168,7 @@ class SearchService:
|
||||
else:
|
||||
await self.index_entity_data(entity, content)
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def index_entity_data(
|
||||
self,
|
||||
entity: Entity,
|
||||
@@ -176,6 +182,7 @@ class SearchService:
|
||||
entity, content
|
||||
) if entity.is_markdown else await self.index_entity_file(entity)
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def index_entity_file(
|
||||
self,
|
||||
entity: Entity,
|
||||
@@ -198,6 +205,7 @@ class SearchService:
|
||||
)
|
||||
)
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def index_entity_markdown(
|
||||
self,
|
||||
entity: Entity,
|
||||
@@ -335,14 +343,17 @@ class SearchService:
|
||||
# Batch insert all rows at once
|
||||
await self.repository.bulk_index_items(rows_to_index)
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def delete_by_permalink(self, permalink: str):
|
||||
"""Delete an item from the search index."""
|
||||
await self.repository.delete_by_permalink(permalink)
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def delete_by_entity_id(self, entity_id: int):
|
||||
"""Delete an item from the search index."""
|
||||
await self.repository.delete_by_entity_id(entity_id)
|
||||
|
||||
@logfire.instrument(record_return=True)
|
||||
async def handle_delete(self, entity: Entity):
|
||||
"""Handle complete entity deletion from search index including observations and relations.
|
||||
|
||||
|
||||
@@ -60,6 +60,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/12/e5e0282d673bb9746bacfb6e2dba8719989d3660cdb2ea79aee9a9651afb/anyio-4.10.0-py3-none-any.whl", hash = "sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1", size = 107213, upload-time = "2025-08-04T08:54:24.882Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "asgiref"
|
||||
version = "3.11.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/76/b9/4db2509eabd14b4a8c71d1b24c8d5734c52b8560a7b1e1a8b56c8d25568b/asgiref-3.11.0.tar.gz", hash = "sha256:13acff32519542a1736223fb79a715acdebe24286d98e8b164a73085f40da2c4", size = 37969, upload-time = "2025-11-19T15:32:20.106Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/91/be/317c2c55b8bbec407257d45f5c8d1b6867abc76d12043f2d3d58c538a4ea/asgiref-3.11.0-py3-none-any.whl", hash = "sha256:1db9021efadb0d9512ce8ffaf72fcef601c7b73a8807a1bb2ef143dc6b14846d", size = 24096, upload-time = "2025-11-19T15:32:19.004Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "asttokens"
|
||||
version = "3.0.0"
|
||||
@@ -126,7 +135,7 @@ dependencies = [
|
||||
{ name = "fastapi", extra = ["standard"] },
|
||||
{ name = "fastmcp" },
|
||||
{ name = "greenlet" },
|
||||
{ name = "logfire" },
|
||||
{ name = "logfire", extra = ["fastapi"] },
|
||||
{ name = "loguru" },
|
||||
{ name = "markdown-it-py" },
|
||||
{ name = "mcp" },
|
||||
@@ -171,7 +180,7 @@ requires-dist = [
|
||||
{ name = "fastapi", extras = ["standard"], specifier = ">=0.115.8" },
|
||||
{ name = "fastmcp", specifier = ">=2.10.2" },
|
||||
{ name = "greenlet", specifier = ">=3.1.1" },
|
||||
{ name = "logfire", specifier = ">=0.73.0" },
|
||||
{ name = "logfire", extras = ["fastapi"], specifier = ">=0.73.0" },
|
||||
{ name = "loguru", specifier = ">=0.7.3" },
|
||||
{ name = "markdown-it-py", specifier = ">=3.0.0" },
|
||||
{ name = "mcp", specifier = ">=1.2.0" },
|
||||
@@ -897,6 +906,11 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/5f/0803848cc5ce524ff830e5f2a2f1400fd6ee72be705d87d0432cec42b1e4/logfire-4.13.2-py3-none-any.whl", hash = "sha256:887e99897a1818864aa5bfc595b02c93264ce23d1860866369eff6b6e2dde1c6", size = 228152, upload-time = "2025-10-13T16:17:50.641Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
fastapi = [
|
||||
{ name = "opentelemetry-instrumentation-fastapi" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "loguru"
|
||||
version = "0.7.3"
|
||||
@@ -1149,6 +1163,38 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/db/5ff1cd6c5ca1d12ecf1b73be16fbb2a8af2114ee46d4b0e6d4b23f4f4db7/opentelemetry_instrumentation-0.58b0-py3-none-any.whl", hash = "sha256:50f97ac03100676c9f7fc28197f8240c7290ca1baa12da8bfbb9a1de4f34cc45", size = 33019, upload-time = "2025-09-11T11:41:00.624Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry-instrumentation-asgi"
|
||||
version = "0.58b0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "asgiref" },
|
||||
{ name = "opentelemetry-api" },
|
||||
{ name = "opentelemetry-instrumentation" },
|
||||
{ name = "opentelemetry-semantic-conventions" },
|
||||
{ name = "opentelemetry-util-http" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7b/e2/03ff707d881d590c7adaed5e9d1979aed7e5e53fc1ed89035e5ed9f304af/opentelemetry_instrumentation_asgi-0.58b0.tar.gz", hash = "sha256:3ccc0c9c1c8c71e8d9da5945c6dcd9c0c8d147839f208536b7042c6dd98e65c9", size = 25116, upload-time = "2025-09-11T11:42:18.437Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/71/a00884c6655387c70070138acbf79a6616ad5d4489680f40708d75b598a7/opentelemetry_instrumentation_asgi-0.58b0-py3-none-any.whl", hash = "sha256:508a6d79e333d648d2afee0e140b6e80eb5d443be183be58e81d9ff88373168a", size = 16798, upload-time = "2025-09-11T11:41:08.105Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry-instrumentation-fastapi"
|
||||
version = "0.58b0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "opentelemetry-api" },
|
||||
{ name = "opentelemetry-instrumentation" },
|
||||
{ name = "opentelemetry-instrumentation-asgi" },
|
||||
{ name = "opentelemetry-semantic-conventions" },
|
||||
{ name = "opentelemetry-util-http" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/64/09/4f8fcab834af6b403e5e2d94bdfb2d0835ba8cd1049bcc156995f47b65fb/opentelemetry_instrumentation_fastapi-0.58b0.tar.gz", hash = "sha256:03da470d694116a0a40f4e76319e42f3ff9efc49abf804b2acc2c07f96661497", size = 24598, upload-time = "2025-09-11T11:42:35.325Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/45/fb/82de06eba54e5cb979274f073065ebc374794853502d342b5155073d1194/opentelemetry_instrumentation_fastapi-0.58b0-py3-none-any.whl", hash = "sha256:d89bfec69c9ffc5d9f3fe58655d6660a66b2bca863b9132712c06edcde68b6fa", size = 13460, upload-time = "2025-09-11T11:41:28.507Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry-proto"
|
||||
version = "1.37.0"
|
||||
@@ -1188,6 +1234,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/07/90/68152b7465f50285d3ce2481b3aec2f82822e3f52e5152eeeaf516bab841/opentelemetry_semantic_conventions-0.58b0-py3-none-any.whl", hash = "sha256:5564905ab1458b96684db1340232729fce3b5375a06e140e8904c78e4f815b28", size = 207954, upload-time = "2025-09-11T10:28:59.218Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opentelemetry-util-http"
|
||||
version = "0.58b0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c6/5f/02f31530faf50ef8a41ab34901c05cbbf8e9d76963ba2fb852b0b4065f4e/opentelemetry_util_http-0.58b0.tar.gz", hash = "sha256:de0154896c3472c6599311c83e0ecee856c4da1b17808d39fdc5cce5312e4d89", size = 9411, upload-time = "2025-09-11T11:43:05.602Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/a3/0a1430c42c6d34d8372a16c104e7408028f0c30270d8f3eb6cccf2e82934/opentelemetry_util_http-0.58b0-py3-none-any.whl", hash = "sha256:6c6b86762ed43025fbd593dc5f700ba0aa3e09711aedc36fd48a13b23d8cb1e7", size = 7652, upload-time = "2025-09-11T11:42:09.682Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "25.0"
|
||||
|
||||
Reference in New Issue
Block a user