Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 17 additions & 12 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2916,8 +2916,16 @@ async def _run_mine_subprocess():
_backfill_state: dict[str, Any] = {"in_progress": False}
_backfill_lock = asyncio.Lock()

from search_models import BackfillAgeBody # noqa: E402
from fastapi import Body # noqa: E402


@app.post("/backfill-age")
async def backfill_age(request: Request, x_api_key: str | None = Header(default=None)):
async def backfill_age(
request: Request,
body: BackfillAgeBody = Body(default_factory=BackfillAgeBody),
x_api_key: str | None = Header(default=None),
):
"""Trigger AGE graph backfill from existing drawer rows.

Runs `mempalace-backfill-age` (or `python -m mempalace.backfill_age`)
Expand Down Expand Up @@ -2950,20 +2958,17 @@ async def backfill_age(request: Request, x_api_key: str | None = Header(default=
if not dsn:
raise HTTPException(status_code=500, detail="no postgres DSN available")

body = await request.json() if request.headers.get("content-type") == "application/json" else {}
# palace-daemon#179 Option C: body fields (wing, skip_palace,
# skip_entities, restart) already validated + wing-canonicalized
# by BackfillAgeBody at parse time.
cmd = [sys.executable, "-m", "mempalace.backfill_age", "--dsn", dsn]
# Normalize wing so the backfill filter matches drawers stored
# under the canonical wing slug. Pre-fix: backfill-age with
# wing="Palace_Daemon" looked for drawers with that literal value
# and found nothing (they're stored as "palace_daemon").
wing_filter = _rooms.normalize_wing_filter(body.get("wing"))
if wing_filter:
cmd += ["--wing", wing_filter]
if body.get("skip_palace"):
if body.wing:
cmd += ["--wing", body.wing]
if body.skip_palace:
cmd.append("--skip-palace")
if body.get("skip_entities"):
if body.skip_entities:
cmd.append("--skip-entities")
if body.get("restart"):
if body.restart:
cmd.append("--restart")

_backfill_state["in_progress"] = True
Expand Down
24 changes: 24 additions & 0 deletions search_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,30 @@ def _validate_fusion_mode(cls, v):
return v


class BackfillAgeBody(BaseModel):
"""Body for POST /backfill-age.

All fields optional with safe defaults — the endpoint accepts an
empty POST body (e.g. ``curl -X POST .../backfill-age`` with no
Content-Type) and falls back to ``backfill everything``.

Wing here is a *filter* (read-side semantic): restrict the backfill
scope to drawers under one wing. Empty/None means "all wings."
Normalize via ``rooms.normalize_wing_filter`` so a caller passing
``Palace_Daemon`` finds the drawers stored under ``palace_daemon``.
"""

wing: "str | None" = Field(None, description="Optional wing filter.")
skip_palace: bool = Field(False, description="Skip Wing/Room/Drawer structure.")
skip_entities: bool = Field(False, description="Skip per-drawer entity extraction.")
restart: bool = Field(False, description="Clear checkpoint, start fresh.")

@field_validator("wing")
@classmethod
def _normalize_wing(cls, v):
return _canon_wing(v)


class SearchAgeFusedBody(BaseModel):
"""Body for POST /search/age-fused."""

Expand Down
Loading