mirror of
https://github.com/basicmachines-co/basic-memory
synced 2026-06-21 13:47:35 +00:00
fastapi app for knowledge routes
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="PYTHON_MODULE" version="4">
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/.venv" />
|
||||
</content>
|
||||
<orderEntry type="jdk" jdkName="Python 3.9 (basic-memory)" jdkType="Python SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
+10
-1
@@ -3,7 +3,7 @@ name = "basic-memory"
|
||||
version = "0.1.0"
|
||||
description = "Local-first knowledge management combining Zettelkasten with knowledge graphs"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
requires-python = ">=3.12.1"
|
||||
license = { text = "AGPL-3.0-or-later" }
|
||||
dependencies = [
|
||||
"sqlalchemy>=2.0.0",
|
||||
@@ -18,6 +18,7 @@ dependencies = [
|
||||
"pydantic-settings>=2.6.1",
|
||||
"loguru>=0.7.3",
|
||||
"pyright>=1.1.390",
|
||||
"basic-foundation",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
@@ -29,6 +30,7 @@ dev = [
|
||||
"ruff>=0.1.6",
|
||||
]
|
||||
|
||||
|
||||
[project.scripts]
|
||||
basic-memory = "basic_memory.cli.main:app"
|
||||
|
||||
@@ -37,6 +39,10 @@ requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
pythonpath = [
|
||||
"src",
|
||||
"tests"
|
||||
]
|
||||
addopts = "--cov=basic_memory -ra -q"
|
||||
testpaths = ["tests"]
|
||||
asyncio_mode = "strict"
|
||||
@@ -47,3 +53,6 @@ asyncio_default_fixture_loop_scope = "function"
|
||||
[tool.ruff]
|
||||
line-length = 100
|
||||
target-version = "py312"
|
||||
|
||||
[tool.uv.sources]
|
||||
basic-foundation = { path = "../basic-foundation", editable = true }
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
"""Basic Memory API module."""
|
||||
from .app import app
|
||||
|
||||
__all__ = ["app"]
|
||||
@@ -0,0 +1,25 @@
|
||||
"""FastAPI application for basic-memory knowledge graph API."""
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import FastAPI
|
||||
from loguru import logger
|
||||
|
||||
from .routers import knowledge
|
||||
from ..config import ProjectConfig
|
||||
|
||||
|
||||
# Initialize FastAPI app
|
||||
app = FastAPI(
|
||||
title="Basic Memory API",
|
||||
description="Knowledge graph API for basic-memory",
|
||||
version="0.1.0"
|
||||
)
|
||||
|
||||
# Include routers
|
||||
app.include_router(knowledge.router)
|
||||
|
||||
# Add startup event
|
||||
@app.on_event("startup")
|
||||
async def startup_event():
|
||||
"""Log when the API starts."""
|
||||
logger.info("Starting Basic Memory API")
|
||||
@@ -0,0 +1,11 @@
|
||||
"""FastAPI dependency functions."""
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import Depends
|
||||
|
||||
from basic_memory.config import project_path
|
||||
from basic_memory.deps import get_project_services
|
||||
from basic_memory.services import MemoryService
|
||||
|
||||
MemoryServiceDep = Annotated[MemoryService, Depends(get_project_services(project_path))]
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
"""API routers."""
|
||||
from . import knowledge
|
||||
|
||||
__all__ = ["knowledge"]
|
||||
@@ -0,0 +1,59 @@
|
||||
"""Router for knowledge graph operations."""
|
||||
from fastapi import APIRouter
|
||||
|
||||
|
||||
from basic_memory.api.deps import MemoryServiceDep
|
||||
from basic_memory.schemas import (
|
||||
CreateEntitiesInput, CreateEntitiesResponse,
|
||||
SearchNodesInput, SearchNodesResponse,
|
||||
CreateRelationsInput, CreateRelationsResponse,
|
||||
EntityOut, RelationOut, ObservationsIn, ObservationsOut, ObservationOut
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/knowledge", tags=["knowledge"])
|
||||
|
||||
|
||||
@router.post("/entities", response_model=CreateEntitiesResponse)
|
||||
async def create_entities(
|
||||
data: CreateEntitiesInput,
|
||||
memory_service: MemoryServiceDep
|
||||
) -> CreateEntitiesResponse:
|
||||
"""Create new entities in the knowledge graph."""
|
||||
entities = await memory_service.create_entities(data.entities)
|
||||
return CreateEntitiesResponse(entities=[EntityOut.model_validate(entity) for entity in entities])
|
||||
|
||||
@router.get("/entities/{entity_id}", response_model=EntityOut)
|
||||
async def get_entity(
|
||||
entity_id: str,
|
||||
memory_service: MemoryServiceDep
|
||||
) -> EntityOut:
|
||||
"""Get a specific entity by ID."""
|
||||
entity = await memory_service.get_entity(entity_id)
|
||||
return EntityOut.model_validate(entity)
|
||||
|
||||
@router.post("/relations", response_model=CreateRelationsResponse)
|
||||
async def create_relations(
|
||||
data: CreateRelationsInput,
|
||||
memory_service: MemoryServiceDep
|
||||
) -> CreateRelationsResponse:
|
||||
"""Create relations between entities."""
|
||||
relations = await memory_service.create_relations(data.relations)
|
||||
return CreateRelationsResponse(relations=[RelationOut.model_validate(relation) for relation in relations])
|
||||
|
||||
@router.post("/observations", response_model=ObservationsOut)
|
||||
async def add_observations(
|
||||
data: ObservationsIn,
|
||||
memory_service: MemoryServiceDep
|
||||
) -> ObservationsOut:
|
||||
"""Add observations to an entity."""
|
||||
observations = await memory_service.add_observations(data)
|
||||
return ObservationsOut(entity_id=data.entity_id, observations=[ObservationOut.model_validate(observation) for observation in observations]) # pyright: ignore [reportCallIssue]
|
||||
|
||||
@router.post("/search", response_model=SearchNodesResponse)
|
||||
async def search_nodes(
|
||||
data: SearchNodesInput,
|
||||
memory_service: MemoryServiceDep
|
||||
) -> SearchNodesResponse:
|
||||
"""Search for entities in the knowledge graph."""
|
||||
matches = await memory_service.search_nodes(data.query)
|
||||
return SearchNodesResponse(matches=[EntityOut.model_validate(entity) for entity in matches], query=data.query)
|
||||
@@ -1,7 +1,5 @@
|
||||
"""Configuration management for basic-memory."""
|
||||
from pathlib import Path
|
||||
from typing import Optional, AsyncGenerator
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
from pydantic import Field, field_validator
|
||||
@@ -38,24 +36,6 @@ class ProjectConfig(BaseSettings):
|
||||
v.mkdir(parents=True)
|
||||
return v
|
||||
|
||||
@asynccontextmanager
|
||||
async def get_testing_services(
|
||||
config: ProjectConfig,
|
||||
memory_service: Optional["MemoryService"] = None
|
||||
) -> AsyncGenerator["MemoryService", None]:
|
||||
"""Get services for testing with optional pre-configured service.
|
||||
|
||||
Args:
|
||||
config: Project configuration
|
||||
memory_service: Optional pre-configured service for testing
|
||||
|
||||
Yields:
|
||||
Configured MemoryService instance with proper lifecycle management
|
||||
"""
|
||||
if memory_service:
|
||||
yield memory_service
|
||||
return
|
||||
|
||||
from basic_memory.deps import get_project_services
|
||||
async with get_project_services(config.path) as service:
|
||||
yield service
|
||||
# Load project config
|
||||
config = ProjectConfig()
|
||||
project_path = Path(config.path)
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
"""Dependency injection functions for basic-memory services."""
|
||||
from contextlib import asynccontextmanager
|
||||
from pathlib import Path
|
||||
from typing import AsyncGenerator
|
||||
|
||||
from loguru import logger
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, AsyncEngine
|
||||
|
||||
from basic_memory.config import ProjectConfig
|
||||
from basic_memory.repository.entity_repository import EntityRepository
|
||||
from basic_memory.repository.observation_repository import ObservationRepository
|
||||
from basic_memory.repository.relation_repository import RelationRepository
|
||||
from basic_memory.services import EntityService, ObservationService, RelationService, MemoryService
|
||||
from basic_memory.db import DatabaseType, get_database_url, init_database, get_session
|
||||
|
||||
|
||||
async def get_entity_repo(session: AsyncSession) -> EntityRepository:
|
||||
"""Get an EntityRepository instance."""
|
||||
return EntityRepository(session) # Entity type is handled in EntityRepository.__init__
|
||||
@@ -59,7 +62,7 @@ async def get_memory_service(
|
||||
)
|
||||
|
||||
@asynccontextmanager
|
||||
async def get_engine(db_type=DatabaseType.FILESYSTEM, project_path: Path = None):
|
||||
async def get_engine(project_path: Path, db_type=DatabaseType.FILESYSTEM ):
|
||||
"""Get database engine for project with proper lifecycle management."""
|
||||
url = get_database_url(db_type, project_path)
|
||||
engine = await init_database(url)
|
||||
@@ -94,8 +97,8 @@ async def get_memory_service_session(engine: AsyncEngine, project_path: Path):
|
||||
yield memory_service
|
||||
|
||||
@asynccontextmanager
|
||||
async def get_project_services(project_path: Path):
|
||||
async def get_project_services(project_path: Path) -> AsyncGenerator[MemoryService, None]:
|
||||
"""Get all services for a project with full lifecycle management."""
|
||||
async with get_engine(project_path=project_path) as engine:
|
||||
async with get_memory_service_session(engine, project_path) as services:
|
||||
yield services
|
||||
async with get_memory_service_session(engine, project_path) as service_session:
|
||||
yield service_session
|
||||
@@ -97,12 +97,6 @@ class OpenNodesInput(BaseModel):
|
||||
"""Input schema for open_nodes tool."""
|
||||
names: Annotated[List[str], Len(min_length=1)]
|
||||
|
||||
class AddObservationsInput(BaseModel):
|
||||
"""Input schema for add_observations tool."""
|
||||
entity_id: str = Field(alias="entityId")
|
||||
observations: List[ObservationIn]
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
class CreateRelationsInput(BaseModel):
|
||||
"""Input schema for create_relations tool."""
|
||||
relations: List[RelationIn]
|
||||
@@ -129,10 +123,6 @@ class OpenNodesResponse(SQLAlchemyOut):
|
||||
"""Response for open_nodes tool."""
|
||||
entities: List[EntityOut]
|
||||
|
||||
class AddObservationsResponse(SQLAlchemyOut):
|
||||
"""Response for add_observations tool."""
|
||||
entity_id: str
|
||||
added_observations: List[ObservationOut]
|
||||
|
||||
class CreateRelationsResponse(SQLAlchemyOut):
|
||||
"""Response for create_relations tool."""
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Service for managing entities in the database."""
|
||||
from pathlib import Path
|
||||
from typing import List, Dict, Any
|
||||
from typing import List, Dict, Any, Sequence
|
||||
|
||||
from basic_memory.repository.entity_repository import EntityRepository
|
||||
from basic_memory.schemas import EntityIn
|
||||
@@ -20,7 +20,7 @@ class EntityService:
|
||||
self.entity_repo = entity_repo
|
||||
logger.debug(f"Initialized EntityService with path: {project_path}")
|
||||
|
||||
async def search(self, query: str) -> List[Entity]:
|
||||
async def search(self, query: str) -> Sequence[Entity]:
|
||||
"""Search entities using LIKE pattern matching."""
|
||||
logger.debug(f"Searching entities with query: {query}")
|
||||
try:
|
||||
|
||||
@@ -106,6 +106,12 @@ class MemoryService:
|
||||
logger.error(f"Failed to clean up file for {entity.id}: {cleanup_error}")
|
||||
raise
|
||||
|
||||
async def get_entity(self, entity_id):
|
||||
logger.debug(f"Get entity {entity_id} entities")
|
||||
entity = self.entity_service.get_entity(entity_id)
|
||||
logger.debug(f"Found entity {entity}")
|
||||
return entity
|
||||
|
||||
async def create_relations(self, relations_data: List[RelationIn]) -> List[Relation]:
|
||||
"""Create multiple relations between entities."""
|
||||
logger.debug(f"Creating {len(relations_data)} relations")
|
||||
@@ -233,4 +239,5 @@ class MemoryService:
|
||||
return entities
|
||||
except Exception as e:
|
||||
logger.exception("Failed to open nodes")
|
||||
raise
|
||||
raise
|
||||
|
||||
|
||||
+1
-1
@@ -38,7 +38,7 @@ def test_config(tmp_path):
|
||||
@pytest_asyncio.fixture(scope="function")
|
||||
async def engine(test_config):
|
||||
"""Create an async engine using in-memory SQLite database"""
|
||||
async with get_engine(db_type=DatabaseType.MEMORY, project_path=test_config.path) as engine:
|
||||
async with get_engine(project_path=test_config.path, db_type=DatabaseType.MEMORY) as engine:
|
||||
yield engine
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user