mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-08-05 02:45:28 +00:00
get_memory_graph/neighbors used require_user() (returns "" when auth is disabled/bypassed) while every other memory route uses get_current_user() via a local _owner() helper (returns None in that case). MemoryManager.load() and _verify_memory_owner() both special-case None as "no filter / bypass" but treat "" as a real owner to match against — and add_entry() never stamps owner="" on new entries. Net effect: in single-user/no-auth mode the graph endpoint always returned zero nodes and the links endpoints always 404'd. Found while seeding real data through the API to visually verify Milestone 2 — switching to _owner() (get_current_user) matches memory_routes.py's convention and fixes both endpoints.
150 lines
6.2 KiB
Python
150 lines
6.2 KiB
Python
# routes/memory/memory_graph_routes.py
|
|
"""Memory Graph View endpoints: read-only graph derivation plus manual
|
|
relationship (link) editing between a user's own memories.
|
|
|
|
Kept as a separate router (not folded into memory_routes.py's wildcard-heavy
|
|
router) but MUST be included in app.py before that router — see the comment
|
|
at the include_router call site. `GET /api/memory/graph` would otherwise be
|
|
swallowed by memory_routes.py's `GET /api/memory/{memory_id}` wildcard if
|
|
that router's routes were checked first.
|
|
"""
|
|
from typing import Dict, List, Optional
|
|
import logging
|
|
|
|
from fastapi import APIRouter, HTTPException, Query, Request
|
|
|
|
from services.memory import MemoryManager
|
|
from src.auth_helpers import get_current_user, require_privilege
|
|
from src.memory_graph import (
|
|
DEFAULT_MAX_EDGES_PER_NODE,
|
|
DEFAULT_MIN_SIMILARITY,
|
|
build_graph,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def setup_memory_graph_routes(memory_manager: MemoryManager, memory_vector=None):
|
|
"""Set up Memory Graph View routes."""
|
|
router = APIRouter(prefix="/api/memory", tags=["memory-graph"])
|
|
|
|
def _owner(request: Request) -> Optional[str]:
|
|
return get_current_user(request)
|
|
|
|
def _verify_memory_owner(memory: dict, user: Optional[str]):
|
|
"""Raise 404 if user doesn't own this memory. Mirrors
|
|
memory_routes.py's _verify_memory_owner: strict ownership so a
|
|
legacy/null-owner memory never leaks across accounts."""
|
|
if user is None:
|
|
return # Auth disabled
|
|
if memory.get("owner") != user:
|
|
raise HTTPException(404, "Memory not found")
|
|
|
|
@router.get("/graph")
|
|
def get_memory_graph(
|
|
request: Request,
|
|
category: Optional[List[str]] = Query(None),
|
|
min_similarity: float = Query(DEFAULT_MIN_SIMILARITY, ge=0.0, le=1.0),
|
|
max_edges_per_node: int = Query(DEFAULT_MAX_EDGES_PER_NODE, ge=1, le=50),
|
|
include_session_edges: bool = Query(True),
|
|
include_manual_edges: bool = Query(True),
|
|
limit: int = Query(1000, ge=1, le=5000),
|
|
):
|
|
"""Return the caller's own memories as a derived node/edge graph."""
|
|
user = _owner(request)
|
|
memories = memory_manager.load(owner=user)
|
|
return build_graph(
|
|
memories,
|
|
memory_vector,
|
|
categories=category,
|
|
min_similarity=min_similarity,
|
|
max_edges_per_node=max_edges_per_node,
|
|
include_session_edges=include_session_edges,
|
|
include_manual_edges=include_manual_edges,
|
|
limit=limit,
|
|
)
|
|
|
|
@router.get("/graph/{memory_id}/neighbors")
|
|
def get_memory_graph_neighbors(
|
|
request: Request,
|
|
memory_id: str,
|
|
min_similarity: float = Query(DEFAULT_MIN_SIMILARITY, ge=0.0, le=1.0),
|
|
max_edges_per_node: int = Query(DEFAULT_MAX_EDGES_PER_NODE, ge=1, le=50),
|
|
):
|
|
"""Lazy drill-down: one node plus its immediate derived neighbors.
|
|
|
|
For graphs too large to render whole (see build_graph's `limit`/
|
|
`truncated`), the frontend can expand a single node on demand instead
|
|
of the server ever needing to compute/return the entire graph.
|
|
"""
|
|
user = _owner(request)
|
|
memories = memory_manager.load(owner=user)
|
|
target = next((m for m in memories if m.get("id") == memory_id), None)
|
|
if target is None:
|
|
raise HTTPException(404, "Memory not found")
|
|
_verify_memory_owner(target, user)
|
|
|
|
full = build_graph(
|
|
memories,
|
|
memory_vector,
|
|
min_similarity=min_similarity,
|
|
max_edges_per_node=max_edges_per_node,
|
|
limit=len(memories) or 1,
|
|
)
|
|
neighbor_ids = {
|
|
(e["target"] if e["source"] == memory_id else e["source"])
|
|
for e in full["edges"]
|
|
if memory_id in (e["source"], e["target"])
|
|
}
|
|
neighbor_ids.add(memory_id)
|
|
nodes = [n for n in full["nodes"] if n["id"] in neighbor_ids]
|
|
edges = [e for e in full["edges"] if e["source"] in neighbor_ids and e["target"] in neighbor_ids]
|
|
return {"nodes": nodes, "edges": edges, "meta": {"node_count": len(nodes), "edge_count": len(edges)}}
|
|
|
|
@router.post("/{memory_id}/links")
|
|
def add_memory_link(request: Request, memory_id: str, target_id: str = Query(...)):
|
|
"""Create an explicit manual relationship between two of the caller's
|
|
own memories (the Memory Graph View's "draw a link" affordance)."""
|
|
require_privilege(request, "can_manage_memory")
|
|
user = _owner(request)
|
|
if target_id == memory_id:
|
|
raise HTTPException(400, "A memory cannot link to itself")
|
|
|
|
all_mem = memory_manager.load_all()
|
|
source = next((m for m in all_mem if m.get("id") == memory_id), None)
|
|
if source is None:
|
|
raise HTTPException(404, "Memory not found")
|
|
_verify_memory_owner(source, user)
|
|
target = next((m for m in all_mem if m.get("id") == target_id), None)
|
|
if target is None:
|
|
raise HTTPException(404, "Target memory not found")
|
|
_verify_memory_owner(target, user)
|
|
|
|
links = list(source.get("links") or [])
|
|
if target_id not in links:
|
|
links.append(target_id)
|
|
source["links"] = links
|
|
memory_manager.save(all_mem)
|
|
return {"ok": True, "links": links}
|
|
|
|
@router.delete("/{memory_id}/links/{target_id}")
|
|
def remove_memory_link(request: Request, memory_id: str, target_id: str):
|
|
"""Remove a manual relationship. Idempotent — removing a link that
|
|
doesn't exist is not an error, matching how memory delete/pin already
|
|
treat repeat calls as harmless in this codebase."""
|
|
require_privilege(request, "can_manage_memory")
|
|
user = _owner(request)
|
|
all_mem = memory_manager.load_all()
|
|
source = next((m for m in all_mem if m.get("id") == memory_id), None)
|
|
if source is None:
|
|
raise HTTPException(404, "Memory not found")
|
|
_verify_memory_owner(source, user)
|
|
|
|
links = list(source.get("links") or [])
|
|
if target_id in links:
|
|
links = [l for l in links if l != target_id]
|
|
source["links"] = links
|
|
memory_manager.save(all_mem)
|
|
return {"ok": True, "links": links}
|
|
|
|
return router
|