fastapi app for knowledge routes

This commit is contained in:
phernandez
2024-12-14 09:33:18 -06:00
parent 01defe8ef1
commit 1ed4c72faa
14 changed files with 1124 additions and 43 deletions
+11
View File
@@ -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
View File
@@ -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 }
+4
View File
@@ -0,0 +1,4 @@
"""Basic Memory API module."""
from .app import app
__all__ = ["app"]
+25
View File
@@ -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")
+11
View File
@@ -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))]
+4
View File
@@ -0,0 +1,4 @@
"""API routers."""
from . import knowledge
__all__ = ["knowledge"]
+59
View File
@@ -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)
+3 -23
View File
@@ -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)
+7 -4
View File
@@ -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
-10
View File
@@ -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."""
+2 -2
View File
@@ -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:
+8 -1
View File
@@ -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
View File
@@ -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
Generated
+979 -1
View File
File diff suppressed because it is too large Load Diff