Skip to content

Commit ec353c4

Browse files
macayu17claude
andcommitted
Fix graph UX bugs + parallelize entity extraction
Graph UI: - Side panel now closable (X button, click on canvas pane, ESC via reset) - Click same entity twice to collapse instead of stack - Memory node clicks open detail panel with full content + linked entities + neighbors - Reset view button clears all expansions - Dedupe nodes/edges so re-expanding doesn't double up - Filter pills now visually muted when inactive (opacity 0.45) - Auto-clear status messages after 6s - Empty/filtered/loading states use the dashboard serif typography - Canvas re-centers on clicked node so expanded children stay visible API perf: - backfill_entities_for_user now parallelizes with asyncio.gather + semaphore(5) 100-memory backfill: ~100s sequential -> ~20s parallel - _run_graph_extraction (per-conversation) parallelizes the same way (sem 3) Dashboard fetch hygiene: - Add AbortController-based request timeout (30s default, 5min for /graph/extract and /memories/import). Browser no longer hangs forever on slow endpoints. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 1ebf2d4 commit ec353c4

4 files changed

Lines changed: 352 additions & 98 deletions

File tree

api/services/extraction.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -152,13 +152,19 @@ async def _run_graph_extraction(
152152
return
153153
from api.services.graph import extract_entities_for_memory
154154

155-
try:
156-
async with get_pool().acquire() as db:
157-
for memory_id, content in memory_refs:
158-
try:
155+
pool = get_pool()
156+
semaphore = asyncio.Semaphore(3)
157+
158+
async def _one(memory_id: UUID, content: str) -> None:
159+
async with semaphore:
160+
try:
161+
async with pool.acquire() as db:
159162
await extract_entities_for_memory(memory_id, content, user_id, resolved, db)
160-
except Exception as inner_error:
161-
logger.warning("Entity extraction failed for memory %s: %s", memory_id, inner_error)
163+
except Exception as inner_error:
164+
logger.warning("Entity extraction failed for memory %s: %s", memory_id, inner_error)
165+
166+
try:
167+
await asyncio.gather(*(_one(mid, content) for mid, content in memory_refs))
162168
except Exception as outer_error:
163169
logger.warning("Graph extraction task failed: %s", outer_error)
164170

api/services/graph.py

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1+
import asyncio
12
import logging
23
from uuid import UUID
34

45
import asyncpg
56

7+
from api.db.connection import get_pool
8+
69
from api.services.providers.base import ExtractionProvider
710
from api.services.providers.factory import build_extraction_provider
811
from api.services.provider_keys import ResolvedProvider
@@ -186,6 +189,7 @@ async def backfill_entities_for_user(
186189
user_id: UUID,
187190
db: asyncpg.Connection,
188191
resolved: ResolvedProvider,
192+
concurrency: int = 5,
189193
) -> dict[str, int]:
190194
memories = await db.fetch(
191195
"""
@@ -198,12 +202,22 @@ async def backfill_entities_for_user(
198202
""",
199203
user_id,
200204
)
201-
processed = 0
202-
entities_total = 0
203-
for memory in memories:
204-
count = await extract_entities_for_memory(
205-
memory["id"], str(memory["content"]), user_id, resolved, db
206-
)
207-
processed += 1
208-
entities_total += count
209-
return {"processed": processed, "entities_created": entities_total}
205+
if not memories:
206+
return {"processed": 0, "entities_created": 0}
207+
208+
semaphore = asyncio.Semaphore(concurrency)
209+
pool = get_pool()
210+
211+
async def _process(memory_row: asyncpg.Record) -> int:
212+
async with semaphore:
213+
async with pool.acquire() as conn:
214+
try:
215+
return await extract_entities_for_memory(
216+
memory_row["id"], str(memory_row["content"]), user_id, resolved, conn
217+
)
218+
except Exception as error:
219+
logger.warning("Backfill failed for memory %s: %s", memory_row["id"], error)
220+
return 0
221+
222+
results = await asyncio.gather(*(_process(memory) for memory in memories))
223+
return {"processed": len(memories), "entities_created": sum(results)}

0 commit comments

Comments
 (0)