150 lines
4.7 KiB
Python
150 lines
4.7 KiB
Python
"""Standalone EverMemOS-compatible consolidation service.
|
|
|
|
This is a lightweight local service for POC use. It intentionally exposes the
|
|
same HTTP contract that Memory Gateway calls:
|
|
|
|
POST /v1/sessions/consolidate
|
|
|
|
The service does not own Memory Gateway's metadata database. It receives
|
|
episodes and existing memories in the request, returns candidate/promoted
|
|
MemoryRecord payloads, and creates Obsidian review drafts for high-value or
|
|
conflicting candidates.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import hashlib
|
|
import logging
|
|
from typing import Any
|
|
|
|
from fastapi import FastAPI
|
|
from pydantic import BaseModel, Field
|
|
|
|
from .config import load_config, set_config
|
|
from .repositories import InMemoryRepository
|
|
from .schemas import AccessContext, EpisodeRecord, MemoryRecord
|
|
from .workers.evermemos_worker import EverMemOSWorker
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class ConsolidateRequest(BaseModel):
|
|
schema_version: str = "memory-gateway.evermemos.consolidate.v1"
|
|
session_id: str
|
|
context: dict[str, Any]
|
|
min_importance: float = 0.6
|
|
target_namespace: str | None = None
|
|
episodes: list[dict[str, Any]] = Field(default_factory=list)
|
|
existing_memories: list[dict[str, Any]] = Field(default_factory=list)
|
|
|
|
|
|
class MemoryIngestRequest(BaseModel):
|
|
workspace_id: str | None = None
|
|
user_id: str
|
|
session_id: str
|
|
turn_id: str
|
|
role: str = "user"
|
|
content: str
|
|
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
source_type: str | None = None
|
|
source_event_id: str | None = None
|
|
|
|
|
|
app = FastAPI(title="Local EverMemOS POC Service", version="0.1.0")
|
|
|
|
|
|
@app.get("/health")
|
|
async def health() -> dict[str, Any]:
|
|
return {
|
|
"status": "ok",
|
|
"service": "evermemos-local",
|
|
"version": "0.1.0",
|
|
"contract": "memory-gateway.evermemos.consolidate.v1",
|
|
}
|
|
|
|
|
|
@app.post("/api/v1/memories")
|
|
async def ingest_memory(request: MemoryIngestRequest) -> dict[str, Any]:
|
|
"""Accept message-level ingest for local real-adapter smoke tests.
|
|
|
|
This POC endpoint intentionally does not persist raw conversation content.
|
|
It only returns a stable backend reference that Memory Gateway can store as
|
|
control-plane metadata.
|
|
"""
|
|
seed = "|".join(
|
|
[
|
|
request.workspace_id or "",
|
|
request.user_id,
|
|
request.session_id,
|
|
request.turn_id,
|
|
request.source_event_id or "",
|
|
]
|
|
)
|
|
memory_id = "em_" + hashlib.sha256(seed.encode("utf-8")).hexdigest()[:24]
|
|
return {
|
|
"status": "success",
|
|
"memory_id": memory_id,
|
|
"native_uri": f"evermemos://memories/{memory_id}",
|
|
"metadata": {
|
|
"schema_version": "evermemos.local.ingest.v1",
|
|
"source_channel": request.metadata.get("source_channel") or request.metadata.get("channel"),
|
|
},
|
|
}
|
|
|
|
|
|
@app.post("/v1/sessions/consolidate")
|
|
async def consolidate_session(request: ConsolidateRequest) -> dict[str, Any]:
|
|
repo = InMemoryRepository()
|
|
ctx = AccessContext.model_validate(request.context)
|
|
|
|
for item in request.existing_memories:
|
|
try:
|
|
repo.upsert_memory(MemoryRecord.model_validate(item))
|
|
except Exception as exc: # noqa: BLE001
|
|
logger.warning("Skipping invalid existing memory: %s", exc)
|
|
|
|
for item in request.episodes:
|
|
try:
|
|
repo.append_episode(EpisodeRecord.model_validate(item))
|
|
except Exception as exc: # noqa: BLE001
|
|
logger.warning("Skipping invalid episode: %s", exc)
|
|
|
|
worker = EverMemOSWorker(repo)
|
|
result = worker.consolidate_session(
|
|
session_id=request.session_id,
|
|
ctx=ctx,
|
|
min_importance=request.min_importance,
|
|
target_namespace=request.target_namespace,
|
|
)
|
|
return {
|
|
"status": "ok",
|
|
"backend": "evermemos-local",
|
|
"result": {
|
|
"session_id": result.session_id,
|
|
"episodes": result.episodes,
|
|
"candidates": [memory.model_dump(mode="json") for memory in result.candidates],
|
|
"promoted": [memory.model_dump(mode="json") for memory in result.promoted],
|
|
"duplicates": result.duplicates,
|
|
"conflicts": result.conflicts,
|
|
"review_drafts": result.review_drafts,
|
|
},
|
|
}
|
|
|
|
|
|
def main() -> None:
|
|
import uvicorn
|
|
|
|
parser = argparse.ArgumentParser(description="Run the local EverMemOS POC service.")
|
|
parser.add_argument("--config", default="config.yaml")
|
|
parser.add_argument("--host", default="127.0.0.1")
|
|
parser.add_argument("--port", type=int, default=1995)
|
|
args = parser.parse_args()
|
|
|
|
config = load_config(args.config)
|
|
set_config(config)
|
|
uvicorn.run(app, host=args.host, port=args.port, log_level=config.logging.level.lower())
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|