omni-cortex 1.0.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- omni_cortex-1.0.4.data/data/share/omni-cortex/dashboard/backend/chat_service.py +140 -0
- omni_cortex-1.0.4.data/data/share/omni-cortex/dashboard/backend/database.py +729 -0
- omni_cortex-1.0.4.data/data/share/omni-cortex/dashboard/backend/main.py +661 -0
- omni_cortex-1.0.4.data/data/share/omni-cortex/dashboard/backend/models.py +140 -0
- omni_cortex-1.0.4.data/data/share/omni-cortex/dashboard/backend/project_scanner.py +141 -0
- omni_cortex-1.0.4.data/data/share/omni-cortex/dashboard/backend/pyproject.toml +23 -0
- omni_cortex-1.0.4.data/data/share/omni-cortex/dashboard/backend/uv.lock +697 -0
- omni_cortex-1.0.4.data/data/share/omni-cortex/dashboard/backend/websocket_manager.py +82 -0
- omni_cortex-1.0.4.data/data/share/omni-cortex/hooks/post_tool_use.py +160 -0
- omni_cortex-1.0.4.data/data/share/omni-cortex/hooks/pre_tool_use.py +159 -0
- omni_cortex-1.0.4.data/data/share/omni-cortex/hooks/stop.py +184 -0
- omni_cortex-1.0.4.data/data/share/omni-cortex/hooks/subagent_stop.py +120 -0
- omni_cortex-1.0.4.dist-info/METADATA +295 -0
- omni_cortex-1.0.4.dist-info/RECORD +17 -0
- omni_cortex-1.0.4.dist-info/WHEEL +4 -0
- omni_cortex-1.0.4.dist-info/entry_points.txt +4 -0
- omni_cortex-1.0.4.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,661 @@
|
|
|
1
|
+
"""FastAPI backend for Omni-Cortex Web Dashboard."""
|
|
2
|
+
# Trigger reload for relationship graph column fix
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
import json
|
|
6
|
+
from contextlib import asynccontextmanager
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
import uvicorn
|
|
12
|
+
from fastapi import FastAPI, HTTPException, Query, WebSocket, WebSocketDisconnect
|
|
13
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
14
|
+
from fastapi.staticfiles import StaticFiles
|
|
15
|
+
from fastapi.responses import FileResponse
|
|
16
|
+
from watchdog.events import FileSystemEventHandler
|
|
17
|
+
from watchdog.observers import Observer
|
|
18
|
+
|
|
19
|
+
from database import (
|
|
20
|
+
bulk_update_memory_status,
|
|
21
|
+
delete_memory,
|
|
22
|
+
get_activities,
|
|
23
|
+
get_activity_heatmap,
|
|
24
|
+
get_all_tags,
|
|
25
|
+
get_memories,
|
|
26
|
+
get_memories_needing_review,
|
|
27
|
+
get_memory_by_id,
|
|
28
|
+
get_memory_growth,
|
|
29
|
+
get_memory_stats,
|
|
30
|
+
get_recent_sessions,
|
|
31
|
+
get_relationship_graph,
|
|
32
|
+
get_relationships,
|
|
33
|
+
get_sessions,
|
|
34
|
+
get_timeline,
|
|
35
|
+
get_tool_usage,
|
|
36
|
+
get_type_distribution,
|
|
37
|
+
search_memories,
|
|
38
|
+
update_memory,
|
|
39
|
+
)
|
|
40
|
+
from models import ChatRequest, ChatResponse, FilterParams, MemoryUpdate, ProjectInfo
|
|
41
|
+
from project_scanner import scan_projects
|
|
42
|
+
from websocket_manager import manager
|
|
43
|
+
import chat_service
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class DatabaseChangeHandler(FileSystemEventHandler):
|
|
47
|
+
"""Handle database file changes for real-time updates."""
|
|
48
|
+
|
|
49
|
+
def __init__(self, ws_manager, loop):
|
|
50
|
+
self.ws_manager = ws_manager
|
|
51
|
+
self.loop = loop
|
|
52
|
+
self._debounce_task: Optional[asyncio.Task] = None
|
|
53
|
+
self._last_path: Optional[str] = None
|
|
54
|
+
|
|
55
|
+
def on_modified(self, event):
|
|
56
|
+
if event.src_path.endswith("cortex.db") or event.src_path.endswith("global.db"):
|
|
57
|
+
# Debounce rapid changes
|
|
58
|
+
self._last_path = event.src_path
|
|
59
|
+
if self._debounce_task is None or self._debounce_task.done():
|
|
60
|
+
self._debounce_task = asyncio.run_coroutine_threadsafe(
|
|
61
|
+
self._debounced_notify(), self.loop
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
async def _debounced_notify(self):
|
|
65
|
+
await asyncio.sleep(0.5) # Wait for rapid changes to settle
|
|
66
|
+
if self._last_path:
|
|
67
|
+
await self.ws_manager.broadcast("database_changed", {"path": self._last_path})
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# File watcher
|
|
71
|
+
observer: Optional[Observer] = None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@asynccontextmanager
|
|
75
|
+
async def lifespan(app: FastAPI):
|
|
76
|
+
"""Manage file watcher lifecycle."""
|
|
77
|
+
global observer
|
|
78
|
+
loop = asyncio.get_event_loop()
|
|
79
|
+
handler = DatabaseChangeHandler(manager, loop)
|
|
80
|
+
observer = Observer()
|
|
81
|
+
|
|
82
|
+
# Watch common project directories
|
|
83
|
+
watch_paths = [
|
|
84
|
+
Path.home() / ".omni-cortex",
|
|
85
|
+
Path("D:/Projects"),
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
for watch_path in watch_paths:
|
|
89
|
+
if watch_path.exists():
|
|
90
|
+
observer.schedule(handler, str(watch_path), recursive=True)
|
|
91
|
+
print(f"[Watcher] Monitoring: {watch_path}")
|
|
92
|
+
|
|
93
|
+
observer.start()
|
|
94
|
+
print("[Server] File watcher started")
|
|
95
|
+
|
|
96
|
+
yield
|
|
97
|
+
|
|
98
|
+
observer.stop()
|
|
99
|
+
observer.join()
|
|
100
|
+
print("[Server] File watcher stopped")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# FastAPI app
|
|
104
|
+
app = FastAPI(
|
|
105
|
+
title="Omni-Cortex Dashboard",
|
|
106
|
+
description="Web dashboard for viewing and managing Omni-Cortex memories",
|
|
107
|
+
version="0.1.0",
|
|
108
|
+
lifespan=lifespan,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# CORS for frontend dev server
|
|
112
|
+
app.add_middleware(
|
|
113
|
+
CORSMiddleware,
|
|
114
|
+
allow_origins=["http://localhost:5173", "http://127.0.0.1:5173"],
|
|
115
|
+
allow_credentials=True,
|
|
116
|
+
allow_methods=["*"],
|
|
117
|
+
allow_headers=["*"],
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
# Static files for production build
|
|
121
|
+
DASHBOARD_DIR = Path(__file__).parent.parent
|
|
122
|
+
DIST_DIR = DASHBOARD_DIR / "frontend" / "dist"
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def setup_static_files():
|
|
126
|
+
"""Mount static files if dist directory exists (production build)."""
|
|
127
|
+
if DIST_DIR.exists():
|
|
128
|
+
# Mount assets directory
|
|
129
|
+
assets_dir = DIST_DIR / "assets"
|
|
130
|
+
if assets_dir.exists():
|
|
131
|
+
app.mount("/assets", StaticFiles(directory=str(assets_dir)), name="assets")
|
|
132
|
+
print(f"[Static] Serving assets from: {assets_dir}")
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# Call setup at module load
|
|
136
|
+
setup_static_files()
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
# --- REST Endpoints ---
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@app.get("/api/projects", response_model=list[ProjectInfo])
|
|
143
|
+
async def list_projects():
|
|
144
|
+
"""List all discovered omni-cortex project databases."""
|
|
145
|
+
return scan_projects()
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@app.get("/api/memories")
|
|
149
|
+
async def list_memories(
|
|
150
|
+
project: str = Query(..., description="Path to the database file"),
|
|
151
|
+
memory_type: Optional[str] = Query(None, alias="type"),
|
|
152
|
+
status: Optional[str] = None,
|
|
153
|
+
tags: Optional[str] = None,
|
|
154
|
+
search: Optional[str] = None,
|
|
155
|
+
min_importance: Optional[int] = None,
|
|
156
|
+
max_importance: Optional[int] = None,
|
|
157
|
+
sort_by: str = "last_accessed",
|
|
158
|
+
sort_order: str = "desc",
|
|
159
|
+
limit: int = 50,
|
|
160
|
+
offset: int = 0,
|
|
161
|
+
):
|
|
162
|
+
"""Get memories with filtering and pagination."""
|
|
163
|
+
if not Path(project).exists():
|
|
164
|
+
raise HTTPException(status_code=404, detail="Database not found")
|
|
165
|
+
|
|
166
|
+
filters = FilterParams(
|
|
167
|
+
memory_type=memory_type,
|
|
168
|
+
status=status,
|
|
169
|
+
tags=tags.split(",") if tags else None,
|
|
170
|
+
search=search,
|
|
171
|
+
min_importance=min_importance,
|
|
172
|
+
max_importance=max_importance,
|
|
173
|
+
sort_by=sort_by,
|
|
174
|
+
sort_order=sort_order,
|
|
175
|
+
limit=limit,
|
|
176
|
+
offset=offset,
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
return get_memories(project, filters)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
# NOTE: These routes MUST be defined before /api/memories/{memory_id} to avoid path conflicts
|
|
183
|
+
@app.get("/api/memories/needs-review")
|
|
184
|
+
async def get_memories_needing_review_endpoint(
|
|
185
|
+
project: str = Query(..., description="Path to the database file"),
|
|
186
|
+
days_threshold: int = 30,
|
|
187
|
+
limit: int = 50,
|
|
188
|
+
):
|
|
189
|
+
"""Get memories that may need freshness review."""
|
|
190
|
+
if not Path(project).exists():
|
|
191
|
+
raise HTTPException(status_code=404, detail="Database not found")
|
|
192
|
+
|
|
193
|
+
return get_memories_needing_review(project, days_threshold, limit)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
@app.post("/api/memories/bulk-update-status")
|
|
197
|
+
async def bulk_update_status_endpoint(
|
|
198
|
+
project: str = Query(..., description="Path to the database file"),
|
|
199
|
+
memory_ids: list[str] = [],
|
|
200
|
+
status: str = "fresh",
|
|
201
|
+
):
|
|
202
|
+
"""Update status for multiple memories at once."""
|
|
203
|
+
if not Path(project).exists():
|
|
204
|
+
raise HTTPException(status_code=404, detail="Database not found")
|
|
205
|
+
|
|
206
|
+
valid_statuses = ["fresh", "needs_review", "outdated", "archived"]
|
|
207
|
+
if status not in valid_statuses:
|
|
208
|
+
raise HTTPException(status_code=400, detail=f"Invalid status. Must be one of: {valid_statuses}")
|
|
209
|
+
|
|
210
|
+
count = bulk_update_memory_status(project, memory_ids, status)
|
|
211
|
+
|
|
212
|
+
# Notify connected clients
|
|
213
|
+
await manager.broadcast("memories_bulk_updated", {"count": count, "status": status})
|
|
214
|
+
|
|
215
|
+
return {"updated_count": count, "status": status}
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
@app.get("/api/memories/{memory_id}")
|
|
219
|
+
async def get_memory(
|
|
220
|
+
memory_id: str,
|
|
221
|
+
project: str = Query(..., description="Path to the database file"),
|
|
222
|
+
):
|
|
223
|
+
"""Get a single memory by ID."""
|
|
224
|
+
if not Path(project).exists():
|
|
225
|
+
raise HTTPException(status_code=404, detail="Database not found")
|
|
226
|
+
|
|
227
|
+
memory = get_memory_by_id(project, memory_id)
|
|
228
|
+
if not memory:
|
|
229
|
+
raise HTTPException(status_code=404, detail="Memory not found")
|
|
230
|
+
return memory
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
@app.put("/api/memories/{memory_id}")
|
|
234
|
+
async def update_memory_endpoint(
|
|
235
|
+
memory_id: str,
|
|
236
|
+
updates: MemoryUpdate,
|
|
237
|
+
project: str = Query(..., description="Path to the database file"),
|
|
238
|
+
):
|
|
239
|
+
"""Update a memory."""
|
|
240
|
+
if not Path(project).exists():
|
|
241
|
+
raise HTTPException(status_code=404, detail="Database not found")
|
|
242
|
+
|
|
243
|
+
updated = update_memory(project, memory_id, updates)
|
|
244
|
+
if not updated:
|
|
245
|
+
raise HTTPException(status_code=404, detail="Memory not found")
|
|
246
|
+
|
|
247
|
+
# Notify connected clients
|
|
248
|
+
await manager.broadcast("memory_updated", updated.model_dump(by_alias=True))
|
|
249
|
+
return updated
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
@app.delete("/api/memories/{memory_id}")
|
|
253
|
+
async def delete_memory_endpoint(
|
|
254
|
+
memory_id: str,
|
|
255
|
+
project: str = Query(..., description="Path to the database file"),
|
|
256
|
+
):
|
|
257
|
+
"""Delete a memory."""
|
|
258
|
+
if not Path(project).exists():
|
|
259
|
+
raise HTTPException(status_code=404, detail="Database not found")
|
|
260
|
+
|
|
261
|
+
deleted = delete_memory(project, memory_id)
|
|
262
|
+
if not deleted:
|
|
263
|
+
raise HTTPException(status_code=404, detail="Memory not found")
|
|
264
|
+
|
|
265
|
+
# Notify connected clients
|
|
266
|
+
await manager.broadcast("memory_deleted", {"id": memory_id})
|
|
267
|
+
return {"message": "Memory deleted", "id": memory_id}
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
@app.get("/api/memories/stats/summary")
|
|
271
|
+
async def memory_stats(
|
|
272
|
+
project: str = Query(..., description="Path to the database file"),
|
|
273
|
+
):
|
|
274
|
+
"""Get memory statistics."""
|
|
275
|
+
if not Path(project).exists():
|
|
276
|
+
raise HTTPException(status_code=404, detail="Database not found")
|
|
277
|
+
|
|
278
|
+
return get_memory_stats(project)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
@app.get("/api/search")
|
|
282
|
+
async def search(
|
|
283
|
+
q: str = Query(..., min_length=1),
|
|
284
|
+
project: str = Query(..., description="Path to the database file"),
|
|
285
|
+
limit: int = 20,
|
|
286
|
+
):
|
|
287
|
+
"""Search memories."""
|
|
288
|
+
if not Path(project).exists():
|
|
289
|
+
raise HTTPException(status_code=404, detail="Database not found")
|
|
290
|
+
|
|
291
|
+
return search_memories(project, q, limit)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
@app.get("/api/activities")
|
|
295
|
+
async def list_activities(
|
|
296
|
+
project: str = Query(..., description="Path to the database file"),
|
|
297
|
+
event_type: Optional[str] = None,
|
|
298
|
+
tool_name: Optional[str] = None,
|
|
299
|
+
limit: int = 100,
|
|
300
|
+
offset: int = 0,
|
|
301
|
+
):
|
|
302
|
+
"""Get activity log entries."""
|
|
303
|
+
if not Path(project).exists():
|
|
304
|
+
raise HTTPException(status_code=404, detail="Database not found")
|
|
305
|
+
|
|
306
|
+
return get_activities(project, event_type, tool_name, limit, offset)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
@app.get("/api/timeline")
|
|
310
|
+
async def get_timeline_view(
|
|
311
|
+
project: str = Query(..., description="Path to the database file"),
|
|
312
|
+
hours: int = 24,
|
|
313
|
+
include_memories: bool = True,
|
|
314
|
+
include_activities: bool = True,
|
|
315
|
+
):
|
|
316
|
+
"""Get timeline of recent activity."""
|
|
317
|
+
if not Path(project).exists():
|
|
318
|
+
raise HTTPException(status_code=404, detail="Database not found")
|
|
319
|
+
|
|
320
|
+
return get_timeline(project, hours, include_memories, include_activities)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
@app.get("/api/tags")
|
|
324
|
+
async def list_tags(
|
|
325
|
+
project: str = Query(..., description="Path to the database file"),
|
|
326
|
+
):
|
|
327
|
+
"""Get all tags with counts."""
|
|
328
|
+
if not Path(project).exists():
|
|
329
|
+
raise HTTPException(status_code=404, detail="Database not found")
|
|
330
|
+
|
|
331
|
+
return get_all_tags(project)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
@app.get("/api/types")
|
|
335
|
+
async def list_types(
|
|
336
|
+
project: str = Query(..., description="Path to the database file"),
|
|
337
|
+
):
|
|
338
|
+
"""Get memory type distribution."""
|
|
339
|
+
if not Path(project).exists():
|
|
340
|
+
raise HTTPException(status_code=404, detail="Database not found")
|
|
341
|
+
|
|
342
|
+
return get_type_distribution(project)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
@app.get("/api/sessions")
|
|
346
|
+
async def list_sessions(
|
|
347
|
+
project: str = Query(..., description="Path to the database file"),
|
|
348
|
+
limit: int = 20,
|
|
349
|
+
):
|
|
350
|
+
"""Get recent sessions."""
|
|
351
|
+
if not Path(project).exists():
|
|
352
|
+
raise HTTPException(status_code=404, detail="Database not found")
|
|
353
|
+
|
|
354
|
+
return get_sessions(project, limit)
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
# --- Stats Endpoints for Charts ---
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
@app.get("/api/stats/activity-heatmap")
|
|
361
|
+
async def get_activity_heatmap_endpoint(
|
|
362
|
+
project: str = Query(..., description="Path to the database file"),
|
|
363
|
+
days: int = 90,
|
|
364
|
+
):
|
|
365
|
+
"""Get activity counts grouped by day for heatmap visualization."""
|
|
366
|
+
if not Path(project).exists():
|
|
367
|
+
raise HTTPException(status_code=404, detail="Database not found")
|
|
368
|
+
|
|
369
|
+
return get_activity_heatmap(project, days)
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
@app.get("/api/stats/tool-usage")
|
|
373
|
+
async def get_tool_usage_endpoint(
|
|
374
|
+
project: str = Query(..., description="Path to the database file"),
|
|
375
|
+
limit: int = 10,
|
|
376
|
+
):
|
|
377
|
+
"""Get tool usage statistics."""
|
|
378
|
+
if not Path(project).exists():
|
|
379
|
+
raise HTTPException(status_code=404, detail="Database not found")
|
|
380
|
+
|
|
381
|
+
return get_tool_usage(project, limit)
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
@app.get("/api/stats/memory-growth")
|
|
385
|
+
async def get_memory_growth_endpoint(
|
|
386
|
+
project: str = Query(..., description="Path to the database file"),
|
|
387
|
+
days: int = 30,
|
|
388
|
+
):
|
|
389
|
+
"""Get memory creation over time."""
|
|
390
|
+
if not Path(project).exists():
|
|
391
|
+
raise HTTPException(status_code=404, detail="Database not found")
|
|
392
|
+
|
|
393
|
+
return get_memory_growth(project, days)
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
# --- Session Context Endpoints ---
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
@app.get("/api/sessions/recent")
|
|
400
|
+
async def get_recent_sessions_endpoint(
|
|
401
|
+
project: str = Query(..., description="Path to the database file"),
|
|
402
|
+
limit: int = 5,
|
|
403
|
+
):
|
|
404
|
+
"""Get recent sessions with summaries."""
|
|
405
|
+
if not Path(project).exists():
|
|
406
|
+
raise HTTPException(status_code=404, detail="Database not found")
|
|
407
|
+
|
|
408
|
+
return get_recent_sessions(project, limit)
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
# --- Relationship Graph Endpoints ---
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
@app.get("/api/relationships")
|
|
415
|
+
async def get_relationships_endpoint(
|
|
416
|
+
project: str = Query(..., description="Path to the database file"),
|
|
417
|
+
memory_id: Optional[str] = None,
|
|
418
|
+
):
|
|
419
|
+
"""Get memory relationships for graph visualization."""
|
|
420
|
+
if not Path(project).exists():
|
|
421
|
+
raise HTTPException(status_code=404, detail="Database not found")
|
|
422
|
+
|
|
423
|
+
return get_relationships(project, memory_id)
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
@app.get("/api/relationships/graph")
|
|
427
|
+
async def get_relationship_graph_endpoint(
|
|
428
|
+
project: str = Query(..., description="Path to the database file"),
|
|
429
|
+
center_id: Optional[str] = None,
|
|
430
|
+
depth: int = 2,
|
|
431
|
+
):
|
|
432
|
+
"""Get graph data centered on a memory with configurable depth."""
|
|
433
|
+
if not Path(project).exists():
|
|
434
|
+
raise HTTPException(status_code=404, detail="Database not found")
|
|
435
|
+
|
|
436
|
+
return get_relationship_graph(project, center_id, depth)
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
# --- Chat Endpoint ---
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
@app.get("/api/chat/status")
|
|
443
|
+
async def chat_status():
|
|
444
|
+
"""Check if chat service is available."""
|
|
445
|
+
return {
|
|
446
|
+
"available": chat_service.is_available(),
|
|
447
|
+
"message": "Chat is available" if chat_service.is_available() else "Set GEMINI_API_KEY environment variable to enable chat",
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
@app.post("/api/chat", response_model=ChatResponse)
|
|
452
|
+
async def chat_with_memories(
|
|
453
|
+
request: ChatRequest,
|
|
454
|
+
project: str = Query(..., description="Path to the database file"),
|
|
455
|
+
):
|
|
456
|
+
"""Ask a natural language question about memories."""
|
|
457
|
+
if not Path(project).exists():
|
|
458
|
+
raise HTTPException(status_code=404, detail="Database not found")
|
|
459
|
+
|
|
460
|
+
result = await chat_service.ask_about_memories(
|
|
461
|
+
project,
|
|
462
|
+
request.question,
|
|
463
|
+
request.max_memories,
|
|
464
|
+
)
|
|
465
|
+
|
|
466
|
+
return ChatResponse(**result)
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
# --- WebSocket Endpoint ---
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
@app.websocket("/ws")
|
|
473
|
+
async def websocket_endpoint(websocket: WebSocket):
|
|
474
|
+
"""WebSocket endpoint for real-time updates."""
|
|
475
|
+
client_id = await manager.connect(websocket)
|
|
476
|
+
try:
|
|
477
|
+
# Send initial connection confirmation
|
|
478
|
+
await manager.send_to_client(client_id, "connected", {"client_id": client_id})
|
|
479
|
+
|
|
480
|
+
# Keep connection alive and handle messages
|
|
481
|
+
while True:
|
|
482
|
+
data = await websocket.receive_text()
|
|
483
|
+
# Echo back for ping/pong
|
|
484
|
+
if data == "ping":
|
|
485
|
+
await manager.send_to_client(client_id, "pong", {})
|
|
486
|
+
except WebSocketDisconnect:
|
|
487
|
+
await manager.disconnect(client_id)
|
|
488
|
+
except Exception as e:
|
|
489
|
+
print(f"[WS] Error: {e}")
|
|
490
|
+
await manager.disconnect(client_id)
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
# --- Export Endpoints ---
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
@app.get("/api/export")
|
|
497
|
+
async def export_memories(
|
|
498
|
+
project: str = Query(..., description="Path to the database file"),
|
|
499
|
+
format: str = Query("json", description="Export format: json, markdown, csv"),
|
|
500
|
+
memory_ids: Optional[str] = Query(None, description="Comma-separated memory IDs to export, or all if empty"),
|
|
501
|
+
include_relationships: bool = Query(True, description="Include memory relationships"),
|
|
502
|
+
):
|
|
503
|
+
"""Export memories to specified format."""
|
|
504
|
+
from fastapi.responses import Response
|
|
505
|
+
import csv
|
|
506
|
+
import io
|
|
507
|
+
|
|
508
|
+
if not Path(project).exists():
|
|
509
|
+
raise HTTPException(status_code=404, detail="Database not found")
|
|
510
|
+
|
|
511
|
+
# Get memories
|
|
512
|
+
if memory_ids:
|
|
513
|
+
ids = memory_ids.split(",")
|
|
514
|
+
memories = [get_memory_by_id(project, mid) for mid in ids if mid.strip()]
|
|
515
|
+
memories = [m for m in memories if m is not None]
|
|
516
|
+
else:
|
|
517
|
+
from models import FilterParams
|
|
518
|
+
filters = FilterParams(limit=1000, offset=0, sort_by="created_at", sort_order="desc")
|
|
519
|
+
memories = get_memories(project, filters)
|
|
520
|
+
|
|
521
|
+
# Get relationships if requested
|
|
522
|
+
relationships = []
|
|
523
|
+
if include_relationships:
|
|
524
|
+
relationships = get_relationships(project)
|
|
525
|
+
|
|
526
|
+
if format == "json":
|
|
527
|
+
export_data = {
|
|
528
|
+
"exported_at": datetime.now().isoformat(),
|
|
529
|
+
"project": project,
|
|
530
|
+
"memory_count": len(memories),
|
|
531
|
+
"memories": [m.model_dump(by_alias=True) for m in memories],
|
|
532
|
+
"relationships": relationships if include_relationships else [],
|
|
533
|
+
}
|
|
534
|
+
return Response(
|
|
535
|
+
content=json.dumps(export_data, indent=2, default=str),
|
|
536
|
+
media_type="application/json",
|
|
537
|
+
headers={"Content-Disposition": f"attachment; filename=memories_export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"}
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
elif format == "markdown":
|
|
541
|
+
md_lines = [
|
|
542
|
+
f"# Omni-Cortex Memory Export",
|
|
543
|
+
f"",
|
|
544
|
+
f"**Exported:** {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
|
|
545
|
+
f"**Total Memories:** {len(memories)}",
|
|
546
|
+
f"",
|
|
547
|
+
"---",
|
|
548
|
+
"",
|
|
549
|
+
]
|
|
550
|
+
for m in memories:
|
|
551
|
+
md_lines.extend([
|
|
552
|
+
f"## {m.type.title()}: {m.content[:50]}{'...' if len(m.content) > 50 else ''}",
|
|
553
|
+
f"",
|
|
554
|
+
f"**ID:** `{m.id}`",
|
|
555
|
+
f"**Type:** {m.type}",
|
|
556
|
+
f"**Status:** {m.status}",
|
|
557
|
+
f"**Importance:** {m.importance_score}",
|
|
558
|
+
f"**Created:** {m.created_at}",
|
|
559
|
+
f"**Tags:** {', '.join(m.tags) if m.tags else 'None'}",
|
|
560
|
+
f"",
|
|
561
|
+
"### Content",
|
|
562
|
+
f"",
|
|
563
|
+
m.content,
|
|
564
|
+
f"",
|
|
565
|
+
"### Context",
|
|
566
|
+
f"",
|
|
567
|
+
m.context or "_No context_",
|
|
568
|
+
f"",
|
|
569
|
+
"---",
|
|
570
|
+
"",
|
|
571
|
+
])
|
|
572
|
+
return Response(
|
|
573
|
+
content="\n".join(md_lines),
|
|
574
|
+
media_type="text/markdown",
|
|
575
|
+
headers={"Content-Disposition": f"attachment; filename=memories_export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.md"}
|
|
576
|
+
)
|
|
577
|
+
|
|
578
|
+
elif format == "csv":
|
|
579
|
+
output = io.StringIO()
|
|
580
|
+
writer = csv.writer(output)
|
|
581
|
+
writer.writerow(["id", "type", "status", "importance", "content", "context", "tags", "created_at", "last_accessed"])
|
|
582
|
+
for m in memories:
|
|
583
|
+
writer.writerow([
|
|
584
|
+
m.id,
|
|
585
|
+
m.type,
|
|
586
|
+
m.status,
|
|
587
|
+
m.importance_score,
|
|
588
|
+
m.content,
|
|
589
|
+
m.context or "",
|
|
590
|
+
",".join(m.tags) if m.tags else "",
|
|
591
|
+
m.created_at,
|
|
592
|
+
m.last_accessed or "",
|
|
593
|
+
])
|
|
594
|
+
return Response(
|
|
595
|
+
content=output.getvalue(),
|
|
596
|
+
media_type="text/csv",
|
|
597
|
+
headers={"Content-Disposition": f"attachment; filename=memories_export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"}
|
|
598
|
+
)
|
|
599
|
+
|
|
600
|
+
else:
|
|
601
|
+
raise HTTPException(status_code=400, detail=f"Unsupported format: {format}. Use json, markdown, or csv.")
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
# --- Health Check ---
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
@app.get("/health")
|
|
608
|
+
async def health_check():
|
|
609
|
+
"""Health check endpoint."""
|
|
610
|
+
return {
|
|
611
|
+
"status": "healthy",
|
|
612
|
+
"websocket_connections": manager.connection_count,
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
# --- Static File Serving (SPA) ---
|
|
617
|
+
# These routes must come AFTER all API routes
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
@app.get("/")
|
|
621
|
+
async def serve_root():
|
|
622
|
+
"""Serve the frontend index.html."""
|
|
623
|
+
index_file = DIST_DIR / "index.html"
|
|
624
|
+
if index_file.exists():
|
|
625
|
+
return FileResponse(str(index_file))
|
|
626
|
+
return {"message": "Omni-Cortex Dashboard API", "docs": "/docs"}
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
@app.get("/{path:path}")
|
|
630
|
+
async def serve_spa(path: str):
|
|
631
|
+
"""Catch-all route to serve SPA for client-side routing."""
|
|
632
|
+
# Skip API routes and known paths
|
|
633
|
+
if path.startswith(("api/", "ws", "health", "docs", "openapi", "redoc")):
|
|
634
|
+
raise HTTPException(status_code=404, detail="Not found")
|
|
635
|
+
|
|
636
|
+
# Check if it's a static file
|
|
637
|
+
file_path = DIST_DIR / path
|
|
638
|
+
if file_path.exists() and file_path.is_file():
|
|
639
|
+
return FileResponse(str(file_path))
|
|
640
|
+
|
|
641
|
+
# Otherwise serve index.html for SPA routing
|
|
642
|
+
index_file = DIST_DIR / "index.html"
|
|
643
|
+
if index_file.exists():
|
|
644
|
+
return FileResponse(str(index_file))
|
|
645
|
+
|
|
646
|
+
raise HTTPException(status_code=404, detail="Not found")
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
def run():
|
|
650
|
+
"""Run the dashboard server."""
|
|
651
|
+
uvicorn.run(
|
|
652
|
+
"main:app",
|
|
653
|
+
host="0.0.0.0",
|
|
654
|
+
port=8765,
|
|
655
|
+
reload=True,
|
|
656
|
+
reload_dirs=[str(Path(__file__).parent)],
|
|
657
|
+
)
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
if __name__ == "__main__":
|
|
661
|
+
run()
|