omni-cortex 1.0.4__py3-none-any.whl → 1.2.0__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.2.0.data/data/share/omni-cortex/dashboard/backend/chat_service.py +290 -0
- {omni_cortex-1.0.4.data → omni_cortex-1.2.0.data}/data/share/omni-cortex/dashboard/backend/database.py +78 -0
- omni_cortex-1.2.0.data/data/share/omni-cortex/dashboard/backend/image_service.py +533 -0
- omni_cortex-1.2.0.data/data/share/omni-cortex/dashboard/backend/logging_config.py +92 -0
- {omni_cortex-1.0.4.data → omni_cortex-1.2.0.data}/data/share/omni-cortex/dashboard/backend/main.py +324 -42
- {omni_cortex-1.0.4.data → omni_cortex-1.2.0.data}/data/share/omni-cortex/dashboard/backend/models.py +93 -0
- omni_cortex-1.2.0.data/data/share/omni-cortex/dashboard/backend/project_config.py +170 -0
- {omni_cortex-1.0.4.data → omni_cortex-1.2.0.data}/data/share/omni-cortex/dashboard/backend/project_scanner.py +45 -22
- {omni_cortex-1.0.4.dist-info → omni_cortex-1.2.0.dist-info}/METADATA +26 -2
- omni_cortex-1.2.0.dist-info/RECORD +20 -0
- omni_cortex-1.0.4.data/data/share/omni-cortex/dashboard/backend/chat_service.py +0 -140
- omni_cortex-1.0.4.dist-info/RECORD +0 -17
- {omni_cortex-1.0.4.data → omni_cortex-1.2.0.data}/data/share/omni-cortex/dashboard/backend/pyproject.toml +0 -0
- {omni_cortex-1.0.4.data → omni_cortex-1.2.0.data}/data/share/omni-cortex/dashboard/backend/uv.lock +0 -0
- {omni_cortex-1.0.4.data → omni_cortex-1.2.0.data}/data/share/omni-cortex/dashboard/backend/websocket_manager.py +0 -0
- {omni_cortex-1.0.4.data → omni_cortex-1.2.0.data}/data/share/omni-cortex/hooks/post_tool_use.py +0 -0
- {omni_cortex-1.0.4.data → omni_cortex-1.2.0.data}/data/share/omni-cortex/hooks/pre_tool_use.py +0 -0
- {omni_cortex-1.0.4.data → omni_cortex-1.2.0.data}/data/share/omni-cortex/hooks/stop.py +0 -0
- {omni_cortex-1.0.4.data → omni_cortex-1.2.0.data}/data/share/omni-cortex/hooks/subagent_stop.py +0 -0
- {omni_cortex-1.0.4.dist-info → omni_cortex-1.2.0.dist-info}/WHEEL +0 -0
- {omni_cortex-1.0.4.dist-info → omni_cortex-1.2.0.dist-info}/entry_points.txt +0 -0
- {omni_cortex-1.0.4.dist-info → omni_cortex-1.2.0.dist-info}/licenses/LICENSE +0 -0
{omni_cortex-1.0.4.data → omni_cortex-1.2.0.data}/data/share/omni-cortex/dashboard/backend/main.py
RENAMED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import asyncio
|
|
5
5
|
import json
|
|
6
|
+
import traceback
|
|
6
7
|
from contextlib import asynccontextmanager
|
|
7
8
|
from datetime import datetime
|
|
8
9
|
from pathlib import Path
|
|
@@ -37,10 +38,34 @@ from database import (
|
|
|
37
38
|
search_memories,
|
|
38
39
|
update_memory,
|
|
39
40
|
)
|
|
40
|
-
from
|
|
41
|
+
from logging_config import log_success, log_error
|
|
42
|
+
from models import (
|
|
43
|
+
ChatRequest,
|
|
44
|
+
ChatResponse,
|
|
45
|
+
ConversationSaveRequest,
|
|
46
|
+
ConversationSaveResponse,
|
|
47
|
+
FilterParams,
|
|
48
|
+
MemoryUpdate,
|
|
49
|
+
ProjectInfo,
|
|
50
|
+
ProjectRegistration,
|
|
51
|
+
BatchImageGenerationRequest,
|
|
52
|
+
BatchImageGenerationResponse,
|
|
53
|
+
ImageRefineRequest,
|
|
54
|
+
SingleImageRequestModel,
|
|
55
|
+
SingleImageResponseModel,
|
|
56
|
+
)
|
|
57
|
+
from project_config import (
|
|
58
|
+
load_config,
|
|
59
|
+
add_registered_project,
|
|
60
|
+
remove_registered_project,
|
|
61
|
+
toggle_favorite,
|
|
62
|
+
add_scan_directory,
|
|
63
|
+
remove_scan_directory,
|
|
64
|
+
)
|
|
41
65
|
from project_scanner import scan_projects
|
|
42
66
|
from websocket_manager import manager
|
|
43
67
|
import chat_service
|
|
68
|
+
from image_service import image_service, ImagePreset, SingleImageRequest
|
|
44
69
|
|
|
45
70
|
|
|
46
71
|
class DatabaseChangeHandler(FileSystemEventHandler):
|
|
@@ -145,6 +170,70 @@ async def list_projects():
|
|
|
145
170
|
return scan_projects()
|
|
146
171
|
|
|
147
172
|
|
|
173
|
+
# --- Project Management Endpoints ---
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@app.get("/api/projects/config")
|
|
177
|
+
async def get_project_config():
|
|
178
|
+
"""Get project configuration (scan dirs, counts)."""
|
|
179
|
+
config = load_config()
|
|
180
|
+
return {
|
|
181
|
+
"scan_directories": config.scan_directories,
|
|
182
|
+
"registered_count": len(config.registered_projects),
|
|
183
|
+
"favorites_count": len(config.favorites),
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@app.post("/api/projects/register")
|
|
188
|
+
async def register_project(body: ProjectRegistration):
|
|
189
|
+
"""Manually register a project by path."""
|
|
190
|
+
success = add_registered_project(body.path, body.display_name)
|
|
191
|
+
if not success:
|
|
192
|
+
raise HTTPException(400, "Invalid path or already registered")
|
|
193
|
+
return {"success": True}
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
@app.delete("/api/projects/register")
|
|
197
|
+
async def unregister_project(path: str = Query(..., description="Project path to unregister")):
|
|
198
|
+
"""Remove a registered project."""
|
|
199
|
+
success = remove_registered_project(path)
|
|
200
|
+
if not success:
|
|
201
|
+
raise HTTPException(404, "Project not found")
|
|
202
|
+
return {"success": True}
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
@app.post("/api/projects/favorite")
|
|
206
|
+
async def toggle_project_favorite(path: str = Query(..., description="Project path to toggle favorite")):
|
|
207
|
+
"""Toggle favorite status for a project."""
|
|
208
|
+
is_favorite = toggle_favorite(path)
|
|
209
|
+
return {"is_favorite": is_favorite}
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
@app.post("/api/projects/scan-directories")
|
|
213
|
+
async def add_scan_dir(directory: str = Query(..., description="Directory path to add")):
|
|
214
|
+
"""Add a directory to auto-scan list."""
|
|
215
|
+
success = add_scan_directory(directory)
|
|
216
|
+
if not success:
|
|
217
|
+
raise HTTPException(400, "Invalid directory or already added")
|
|
218
|
+
return {"success": True}
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
@app.delete("/api/projects/scan-directories")
|
|
222
|
+
async def remove_scan_dir(directory: str = Query(..., description="Directory path to remove")):
|
|
223
|
+
"""Remove a directory from auto-scan list."""
|
|
224
|
+
success = remove_scan_directory(directory)
|
|
225
|
+
if not success:
|
|
226
|
+
raise HTTPException(404, "Directory not found")
|
|
227
|
+
return {"success": True}
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
@app.post("/api/projects/refresh")
|
|
231
|
+
async def refresh_projects():
|
|
232
|
+
"""Force rescan of all project directories."""
|
|
233
|
+
projects = scan_projects()
|
|
234
|
+
return {"count": len(projects)}
|
|
235
|
+
|
|
236
|
+
|
|
148
237
|
@app.get("/api/memories")
|
|
149
238
|
async def list_memories(
|
|
150
239
|
project: str = Query(..., description="Path to the database file"),
|
|
@@ -160,23 +249,30 @@ async def list_memories(
|
|
|
160
249
|
offset: int = 0,
|
|
161
250
|
):
|
|
162
251
|
"""Get memories with filtering and pagination."""
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
252
|
+
try:
|
|
253
|
+
if not Path(project).exists():
|
|
254
|
+
log_error("/api/memories", FileNotFoundError("Database not found"), project=project)
|
|
255
|
+
raise HTTPException(status_code=404, detail="Database not found")
|
|
256
|
+
|
|
257
|
+
filters = FilterParams(
|
|
258
|
+
memory_type=memory_type,
|
|
259
|
+
status=status,
|
|
260
|
+
tags=tags.split(",") if tags else None,
|
|
261
|
+
search=search,
|
|
262
|
+
min_importance=min_importance,
|
|
263
|
+
max_importance=max_importance,
|
|
264
|
+
sort_by=sort_by,
|
|
265
|
+
sort_order=sort_order,
|
|
266
|
+
limit=limit,
|
|
267
|
+
offset=offset,
|
|
268
|
+
)
|
|
178
269
|
|
|
179
|
-
|
|
270
|
+
memories = get_memories(project, filters)
|
|
271
|
+
log_success("/api/memories", count=len(memories), offset=offset, filters=bool(search or memory_type))
|
|
272
|
+
return memories
|
|
273
|
+
except Exception as e:
|
|
274
|
+
log_error("/api/memories", e, project=project)
|
|
275
|
+
raise
|
|
180
276
|
|
|
181
277
|
|
|
182
278
|
# NOTE: These routes MUST be defined before /api/memories/{memory_id} to avoid path conflicts
|
|
@@ -237,16 +333,25 @@ async def update_memory_endpoint(
|
|
|
237
333
|
project: str = Query(..., description="Path to the database file"),
|
|
238
334
|
):
|
|
239
335
|
"""Update a memory."""
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
336
|
+
try:
|
|
337
|
+
if not Path(project).exists():
|
|
338
|
+
log_error("/api/memories/update", FileNotFoundError("Database not found"), memory_id=memory_id)
|
|
339
|
+
raise HTTPException(status_code=404, detail="Database not found")
|
|
340
|
+
|
|
341
|
+
updated = update_memory(project, memory_id, updates)
|
|
342
|
+
if not updated:
|
|
343
|
+
log_error("/api/memories/update", ValueError("Memory not found"), memory_id=memory_id)
|
|
344
|
+
raise HTTPException(status_code=404, detail="Memory not found")
|
|
345
|
+
|
|
346
|
+
# Notify connected clients
|
|
347
|
+
await manager.broadcast("memory_updated", updated.model_dump(by_alias=True))
|
|
348
|
+
log_success("/api/memories/update", memory_id=memory_id, fields_updated=len(updates.model_dump(exclude_unset=True)))
|
|
349
|
+
return updated
|
|
350
|
+
except HTTPException:
|
|
351
|
+
raise
|
|
352
|
+
except Exception as e:
|
|
353
|
+
log_error("/api/memories/update", e, memory_id=memory_id)
|
|
354
|
+
raise
|
|
250
355
|
|
|
251
356
|
|
|
252
357
|
@app.delete("/api/memories/{memory_id}")
|
|
@@ -255,16 +360,25 @@ async def delete_memory_endpoint(
|
|
|
255
360
|
project: str = Query(..., description="Path to the database file"),
|
|
256
361
|
):
|
|
257
362
|
"""Delete a memory."""
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
363
|
+
try:
|
|
364
|
+
if not Path(project).exists():
|
|
365
|
+
log_error("/api/memories/delete", FileNotFoundError("Database not found"), memory_id=memory_id)
|
|
366
|
+
raise HTTPException(status_code=404, detail="Database not found")
|
|
367
|
+
|
|
368
|
+
deleted = delete_memory(project, memory_id)
|
|
369
|
+
if not deleted:
|
|
370
|
+
log_error("/api/memories/delete", ValueError("Memory not found"), memory_id=memory_id)
|
|
371
|
+
raise HTTPException(status_code=404, detail="Memory not found")
|
|
372
|
+
|
|
373
|
+
# Notify connected clients
|
|
374
|
+
await manager.broadcast("memory_deleted", {"id": memory_id})
|
|
375
|
+
log_success("/api/memories/delete", memory_id=memory_id)
|
|
376
|
+
return {"message": "Memory deleted", "id": memory_id}
|
|
377
|
+
except HTTPException:
|
|
378
|
+
raise
|
|
379
|
+
except Exception as e:
|
|
380
|
+
log_error("/api/memories/delete", e, memory_id=memory_id)
|
|
381
|
+
raise
|
|
268
382
|
|
|
269
383
|
|
|
270
384
|
@app.get("/api/memories/stats/summary")
|
|
@@ -454,16 +568,184 @@ async def chat_with_memories(
|
|
|
454
568
|
project: str = Query(..., description="Path to the database file"),
|
|
455
569
|
):
|
|
456
570
|
"""Ask a natural language question about memories."""
|
|
571
|
+
try:
|
|
572
|
+
if not Path(project).exists():
|
|
573
|
+
log_error("/api/chat", FileNotFoundError("Database not found"), question=request.question[:50])
|
|
574
|
+
raise HTTPException(status_code=404, detail="Database not found")
|
|
575
|
+
|
|
576
|
+
result = await chat_service.ask_about_memories(
|
|
577
|
+
project,
|
|
578
|
+
request.question,
|
|
579
|
+
request.max_memories,
|
|
580
|
+
)
|
|
581
|
+
|
|
582
|
+
log_success("/api/chat", question_len=len(request.question), sources=len(result.get("sources", [])))
|
|
583
|
+
return ChatResponse(**result)
|
|
584
|
+
except HTTPException:
|
|
585
|
+
raise
|
|
586
|
+
except Exception as e:
|
|
587
|
+
log_error("/api/chat", e, question=request.question[:50])
|
|
588
|
+
raise
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
@app.get("/api/chat/stream")
|
|
592
|
+
async def stream_chat(
|
|
593
|
+
project: str = Query(..., description="Path to the database file"),
|
|
594
|
+
question: str = Query(..., description="The question to ask"),
|
|
595
|
+
max_memories: int = Query(10, ge=1, le=50),
|
|
596
|
+
):
|
|
597
|
+
"""SSE endpoint for streaming chat responses."""
|
|
598
|
+
from fastapi.responses import StreamingResponse
|
|
599
|
+
|
|
457
600
|
if not Path(project).exists():
|
|
458
601
|
raise HTTPException(status_code=404, detail="Database not found")
|
|
459
602
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
603
|
+
async def event_generator():
|
|
604
|
+
try:
|
|
605
|
+
async for event in chat_service.stream_ask_about_memories(project, question, max_memories):
|
|
606
|
+
yield f"data: {json.dumps(event)}\n\n"
|
|
607
|
+
except Exception as e:
|
|
608
|
+
yield f"data: {json.dumps({'type': 'error', 'data': str(e)})}\n\n"
|
|
609
|
+
|
|
610
|
+
return StreamingResponse(
|
|
611
|
+
event_generator(),
|
|
612
|
+
media_type="text/event-stream",
|
|
613
|
+
headers={
|
|
614
|
+
"Cache-Control": "no-cache",
|
|
615
|
+
"Connection": "keep-alive",
|
|
616
|
+
"X-Accel-Buffering": "no",
|
|
617
|
+
}
|
|
464
618
|
)
|
|
465
619
|
|
|
466
|
-
|
|
620
|
+
|
|
621
|
+
@app.post("/api/chat/save", response_model=ConversationSaveResponse)
|
|
622
|
+
async def save_chat_conversation(
|
|
623
|
+
request: ConversationSaveRequest,
|
|
624
|
+
project: str = Query(..., description="Path to the database file"),
|
|
625
|
+
):
|
|
626
|
+
"""Save a chat conversation as a memory."""
|
|
627
|
+
try:
|
|
628
|
+
if not Path(project).exists():
|
|
629
|
+
log_error("/api/chat/save", FileNotFoundError("Database not found"))
|
|
630
|
+
raise HTTPException(status_code=404, detail="Database not found")
|
|
631
|
+
|
|
632
|
+
result = await chat_service.save_conversation(
|
|
633
|
+
project,
|
|
634
|
+
[msg.model_dump() for msg in request.messages],
|
|
635
|
+
request.referenced_memory_ids,
|
|
636
|
+
request.importance or 60,
|
|
637
|
+
)
|
|
638
|
+
|
|
639
|
+
log_success("/api/chat/save", memory_id=result["memory_id"], messages=len(request.messages))
|
|
640
|
+
return ConversationSaveResponse(**result)
|
|
641
|
+
except HTTPException:
|
|
642
|
+
raise
|
|
643
|
+
except Exception as e:
|
|
644
|
+
log_error("/api/chat/save", e)
|
|
645
|
+
raise
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
# --- Image Generation Endpoints ---
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
@app.get("/api/image/status")
|
|
652
|
+
async def get_image_status():
|
|
653
|
+
"""Check if image generation is available."""
|
|
654
|
+
return {
|
|
655
|
+
"available": image_service.is_available(),
|
|
656
|
+
"message": "Image generation ready" if image_service.is_available()
|
|
657
|
+
else "Configure GEMINI_API_KEY and install google-genai for image generation",
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
@app.get("/api/image/presets")
|
|
662
|
+
async def get_image_presets():
|
|
663
|
+
"""Get available image preset templates."""
|
|
664
|
+
return {"presets": image_service.get_presets()}
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
@app.post("/api/image/generate-batch", response_model=BatchImageGenerationResponse)
|
|
668
|
+
async def generate_images_batch(
|
|
669
|
+
request: BatchImageGenerationRequest,
|
|
670
|
+
db_path: str = Query(..., alias="project", description="Path to the database file"),
|
|
671
|
+
):
|
|
672
|
+
"""Generate multiple images with different presets/prompts."""
|
|
673
|
+
# Validate image count
|
|
674
|
+
if len(request.images) not in [1, 2, 4]:
|
|
675
|
+
return BatchImageGenerationResponse(
|
|
676
|
+
success=False,
|
|
677
|
+
errors=["Must request 1, 2, or 4 images"]
|
|
678
|
+
)
|
|
679
|
+
|
|
680
|
+
# Build memory context
|
|
681
|
+
memory_context = ""
|
|
682
|
+
if request.memory_ids:
|
|
683
|
+
memory_context = image_service.build_memory_context(db_path, request.memory_ids)
|
|
684
|
+
|
|
685
|
+
# Build chat context
|
|
686
|
+
chat_context = image_service.build_chat_context(request.chat_messages)
|
|
687
|
+
|
|
688
|
+
# Convert request models to internal format
|
|
689
|
+
image_requests = [
|
|
690
|
+
SingleImageRequest(
|
|
691
|
+
preset=ImagePreset(img.preset),
|
|
692
|
+
custom_prompt=img.custom_prompt,
|
|
693
|
+
aspect_ratio=img.aspect_ratio,
|
|
694
|
+
image_size=img.image_size
|
|
695
|
+
)
|
|
696
|
+
for img in request.images
|
|
697
|
+
]
|
|
698
|
+
|
|
699
|
+
result = await image_service.generate_batch(
|
|
700
|
+
requests=image_requests,
|
|
701
|
+
memory_context=memory_context,
|
|
702
|
+
chat_context=chat_context,
|
|
703
|
+
use_search_grounding=request.use_search_grounding
|
|
704
|
+
)
|
|
705
|
+
|
|
706
|
+
return BatchImageGenerationResponse(
|
|
707
|
+
success=result.success,
|
|
708
|
+
images=[
|
|
709
|
+
SingleImageResponseModel(
|
|
710
|
+
success=img.success,
|
|
711
|
+
image_data=img.image_data,
|
|
712
|
+
text_response=img.text_response,
|
|
713
|
+
thought_signature=img.thought_signature,
|
|
714
|
+
image_id=img.image_id,
|
|
715
|
+
error=img.error,
|
|
716
|
+
index=img.index
|
|
717
|
+
)
|
|
718
|
+
for img in result.images
|
|
719
|
+
],
|
|
720
|
+
errors=result.errors
|
|
721
|
+
)
|
|
722
|
+
|
|
723
|
+
|
|
724
|
+
@app.post("/api/image/refine", response_model=SingleImageResponseModel)
|
|
725
|
+
async def refine_image(request: ImageRefineRequest):
|
|
726
|
+
"""Refine an existing generated image with a new prompt."""
|
|
727
|
+
result = await image_service.refine_image(
|
|
728
|
+
image_id=request.image_id,
|
|
729
|
+
refinement_prompt=request.refinement_prompt,
|
|
730
|
+
aspect_ratio=request.aspect_ratio,
|
|
731
|
+
image_size=request.image_size
|
|
732
|
+
)
|
|
733
|
+
|
|
734
|
+
return SingleImageResponseModel(
|
|
735
|
+
success=result.success,
|
|
736
|
+
image_data=result.image_data,
|
|
737
|
+
text_response=result.text_response,
|
|
738
|
+
thought_signature=result.thought_signature,
|
|
739
|
+
image_id=result.image_id,
|
|
740
|
+
error=result.error
|
|
741
|
+
)
|
|
742
|
+
|
|
743
|
+
|
|
744
|
+
@app.post("/api/image/clear-conversation")
|
|
745
|
+
async def clear_image_conversation(image_id: Optional[str] = None):
|
|
746
|
+
"""Clear image conversation history. If image_id provided, clear only that image."""
|
|
747
|
+
image_service.clear_conversation(image_id)
|
|
748
|
+
return {"status": "cleared", "image_id": image_id}
|
|
467
749
|
|
|
468
750
|
|
|
469
751
|
# --- WebSocket Endpoint ---
|
{omni_cortex-1.0.4.data → omni_cortex-1.2.0.data}/data/share/omni-cortex/dashboard/backend/models.py
RENAMED
|
@@ -15,6 +15,31 @@ class ProjectInfo(BaseModel):
|
|
|
15
15
|
last_modified: Optional[datetime] = None
|
|
16
16
|
memory_count: int = 0
|
|
17
17
|
is_global: bool = False
|
|
18
|
+
is_favorite: bool = False
|
|
19
|
+
is_registered: bool = False
|
|
20
|
+
display_name: Optional[str] = None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ScanDirectory(BaseModel):
|
|
24
|
+
"""A directory being scanned for projects."""
|
|
25
|
+
|
|
26
|
+
path: str
|
|
27
|
+
project_count: int = 0
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ProjectRegistration(BaseModel):
|
|
31
|
+
"""Request to register a project."""
|
|
32
|
+
|
|
33
|
+
path: str
|
|
34
|
+
display_name: Optional[str] = None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ProjectConfigResponse(BaseModel):
|
|
38
|
+
"""Response with project configuration."""
|
|
39
|
+
|
|
40
|
+
scan_directories: list[str]
|
|
41
|
+
registered_count: int
|
|
42
|
+
favorites_count: int
|
|
18
43
|
|
|
19
44
|
|
|
20
45
|
class Memory(BaseModel):
|
|
@@ -138,3 +163,71 @@ class ChatResponse(BaseModel):
|
|
|
138
163
|
answer: str
|
|
139
164
|
sources: list[ChatSource]
|
|
140
165
|
error: Optional[str] = None
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class ConversationMessage(BaseModel):
|
|
169
|
+
"""A message in a conversation."""
|
|
170
|
+
|
|
171
|
+
role: str # 'user' or 'assistant'
|
|
172
|
+
content: str
|
|
173
|
+
timestamp: str
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class ConversationSaveRequest(BaseModel):
|
|
177
|
+
"""Request to save a conversation as memory."""
|
|
178
|
+
|
|
179
|
+
messages: list[ConversationMessage]
|
|
180
|
+
referenced_memory_ids: Optional[list[str]] = None
|
|
181
|
+
importance: Optional[int] = Field(default=60, ge=1, le=100)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class ConversationSaveResponse(BaseModel):
|
|
185
|
+
"""Response after saving a conversation."""
|
|
186
|
+
|
|
187
|
+
memory_id: str
|
|
188
|
+
summary: str
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
# --- Image Generation Models ---
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class SingleImageRequestModel(BaseModel):
|
|
195
|
+
"""Request for a single image in a batch."""
|
|
196
|
+
preset: str = "custom" # Maps to ImagePreset enum
|
|
197
|
+
custom_prompt: str = ""
|
|
198
|
+
aspect_ratio: str = "16:9"
|
|
199
|
+
image_size: str = "2K"
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class BatchImageGenerationRequest(BaseModel):
|
|
203
|
+
"""Request for generating multiple images."""
|
|
204
|
+
images: list[SingleImageRequestModel] # 1, 2, or 4 images
|
|
205
|
+
memory_ids: list[str] = []
|
|
206
|
+
chat_messages: list[dict] = [] # Recent chat for context
|
|
207
|
+
use_search_grounding: bool = False
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class ImageRefineRequest(BaseModel):
|
|
211
|
+
"""Request for refining an existing image."""
|
|
212
|
+
image_id: str
|
|
213
|
+
refinement_prompt: str
|
|
214
|
+
aspect_ratio: Optional[str] = None
|
|
215
|
+
image_size: Optional[str] = None
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
class SingleImageResponseModel(BaseModel):
|
|
219
|
+
"""Response for a single generated image."""
|
|
220
|
+
success: bool
|
|
221
|
+
image_data: Optional[str] = None # Base64 encoded
|
|
222
|
+
text_response: Optional[str] = None
|
|
223
|
+
thought_signature: Optional[str] = None
|
|
224
|
+
image_id: Optional[str] = None
|
|
225
|
+
error: Optional[str] = None
|
|
226
|
+
index: int = 0
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class BatchImageGenerationResponse(BaseModel):
|
|
230
|
+
"""Response for batch image generation."""
|
|
231
|
+
success: bool
|
|
232
|
+
images: list[SingleImageResponseModel] = []
|
|
233
|
+
errors: list[str] = []
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""Project configuration manager for user preferences."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import platform
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class RegisteredProject(BaseModel):
|
|
13
|
+
"""A manually registered project."""
|
|
14
|
+
|
|
15
|
+
path: str
|
|
16
|
+
display_name: Optional[str] = None
|
|
17
|
+
added_at: datetime
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class RecentProject(BaseModel):
|
|
21
|
+
"""A recently accessed project."""
|
|
22
|
+
|
|
23
|
+
path: str
|
|
24
|
+
last_accessed: datetime
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ProjectConfig(BaseModel):
|
|
28
|
+
"""User project configuration."""
|
|
29
|
+
|
|
30
|
+
version: int = 1
|
|
31
|
+
scan_directories: list[str] = []
|
|
32
|
+
registered_projects: list[RegisteredProject] = []
|
|
33
|
+
favorites: list[str] = []
|
|
34
|
+
recent: list[RecentProject] = []
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
CONFIG_PATH = Path.home() / ".omni-cortex" / "projects.json"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def get_default_scan_dirs() -> list[str]:
|
|
41
|
+
"""Return platform-appropriate default scan directories."""
|
|
42
|
+
home = Path.home()
|
|
43
|
+
|
|
44
|
+
dirs = [
|
|
45
|
+
str(home / "projects"),
|
|
46
|
+
str(home / "Projects"),
|
|
47
|
+
str(home / "code"),
|
|
48
|
+
str(home / "Code"),
|
|
49
|
+
str(home / "dev"),
|
|
50
|
+
str(home / "workspace"),
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
if platform.system() == "Windows":
|
|
54
|
+
dirs.insert(0, "D:/Projects")
|
|
55
|
+
|
|
56
|
+
return [d for d in dirs if Path(d).exists()]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def load_config() -> ProjectConfig:
|
|
60
|
+
"""Load config from disk, creating defaults if missing."""
|
|
61
|
+
if CONFIG_PATH.exists():
|
|
62
|
+
try:
|
|
63
|
+
data = json.loads(CONFIG_PATH.read_text(encoding="utf-8"))
|
|
64
|
+
return ProjectConfig(**data)
|
|
65
|
+
except Exception:
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
# Create default config
|
|
69
|
+
config = ProjectConfig(scan_directories=get_default_scan_dirs())
|
|
70
|
+
save_config(config)
|
|
71
|
+
return config
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def save_config(config: ProjectConfig) -> None:
|
|
75
|
+
"""Save config to disk."""
|
|
76
|
+
CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
77
|
+
CONFIG_PATH.write_text(config.model_dump_json(indent=2), encoding="utf-8")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def add_registered_project(path: str, display_name: Optional[str] = None) -> bool:
|
|
81
|
+
"""Register a new project by path."""
|
|
82
|
+
config = load_config()
|
|
83
|
+
|
|
84
|
+
# Validate path has cortex.db
|
|
85
|
+
db_path = Path(path) / ".omni-cortex" / "cortex.db"
|
|
86
|
+
if not db_path.exists():
|
|
87
|
+
return False
|
|
88
|
+
|
|
89
|
+
# Check if already registered
|
|
90
|
+
if any(p.path == path for p in config.registered_projects):
|
|
91
|
+
return False
|
|
92
|
+
|
|
93
|
+
config.registered_projects.append(
|
|
94
|
+
RegisteredProject(path=path, display_name=display_name, added_at=datetime.now())
|
|
95
|
+
)
|
|
96
|
+
save_config(config)
|
|
97
|
+
return True
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def remove_registered_project(path: str) -> bool:
|
|
101
|
+
"""Remove a registered project."""
|
|
102
|
+
config = load_config()
|
|
103
|
+
original_len = len(config.registered_projects)
|
|
104
|
+
config.registered_projects = [
|
|
105
|
+
p for p in config.registered_projects if p.path != path
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
if len(config.registered_projects) < original_len:
|
|
109
|
+
save_config(config)
|
|
110
|
+
return True
|
|
111
|
+
return False
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def toggle_favorite(path: str) -> bool:
|
|
115
|
+
"""Toggle favorite status for a project. Returns new favorite status."""
|
|
116
|
+
config = load_config()
|
|
117
|
+
|
|
118
|
+
if path in config.favorites:
|
|
119
|
+
config.favorites.remove(path)
|
|
120
|
+
is_favorite = False
|
|
121
|
+
else:
|
|
122
|
+
config.favorites.append(path)
|
|
123
|
+
is_favorite = True
|
|
124
|
+
|
|
125
|
+
save_config(config)
|
|
126
|
+
return is_favorite
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def update_recent(path: str) -> None:
|
|
130
|
+
"""Update recent projects list."""
|
|
131
|
+
config = load_config()
|
|
132
|
+
|
|
133
|
+
# Remove if already in list
|
|
134
|
+
config.recent = [r for r in config.recent if r.path != path]
|
|
135
|
+
|
|
136
|
+
# Add to front
|
|
137
|
+
config.recent.insert(0, RecentProject(path=path, last_accessed=datetime.now()))
|
|
138
|
+
|
|
139
|
+
# Keep only last 10
|
|
140
|
+
config.recent = config.recent[:10]
|
|
141
|
+
|
|
142
|
+
save_config(config)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def add_scan_directory(directory: str) -> bool:
|
|
146
|
+
"""Add a directory to scan list."""
|
|
147
|
+
config = load_config()
|
|
148
|
+
|
|
149
|
+
# Expand user path
|
|
150
|
+
expanded = str(Path(directory).expanduser())
|
|
151
|
+
|
|
152
|
+
if not Path(expanded).is_dir():
|
|
153
|
+
return False
|
|
154
|
+
|
|
155
|
+
if expanded not in config.scan_directories:
|
|
156
|
+
config.scan_directories.append(expanded)
|
|
157
|
+
save_config(config)
|
|
158
|
+
return True
|
|
159
|
+
return False
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def remove_scan_directory(directory: str) -> bool:
|
|
163
|
+
"""Remove a directory from scan list."""
|
|
164
|
+
config = load_config()
|
|
165
|
+
|
|
166
|
+
if directory in config.scan_directories:
|
|
167
|
+
config.scan_directories.remove(directory)
|
|
168
|
+
save_config(config)
|
|
169
|
+
return True
|
|
170
|
+
return False
|