omni-cortex 1.0.4__py3-none-any.whl → 1.3.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.
Files changed (22) hide show
  1. omni_cortex-1.3.0.data/data/share/omni-cortex/dashboard/backend/chat_service.py +308 -0
  2. {omni_cortex-1.0.4.data → omni_cortex-1.3.0.data}/data/share/omni-cortex/dashboard/backend/database.py +286 -0
  3. omni_cortex-1.3.0.data/data/share/omni-cortex/dashboard/backend/image_service.py +543 -0
  4. omni_cortex-1.3.0.data/data/share/omni-cortex/dashboard/backend/logging_config.py +92 -0
  5. {omni_cortex-1.0.4.data → omni_cortex-1.3.0.data}/data/share/omni-cortex/dashboard/backend/main.py +385 -42
  6. {omni_cortex-1.0.4.data → omni_cortex-1.3.0.data}/data/share/omni-cortex/dashboard/backend/models.py +93 -0
  7. omni_cortex-1.3.0.data/data/share/omni-cortex/dashboard/backend/project_config.py +170 -0
  8. {omni_cortex-1.0.4.data → omni_cortex-1.3.0.data}/data/share/omni-cortex/dashboard/backend/project_scanner.py +45 -22
  9. {omni_cortex-1.0.4.data → omni_cortex-1.3.0.data}/data/share/omni-cortex/dashboard/backend/uv.lock +414 -1
  10. {omni_cortex-1.0.4.dist-info → omni_cortex-1.3.0.dist-info}/METADATA +26 -2
  11. omni_cortex-1.3.0.dist-info/RECORD +20 -0
  12. omni_cortex-1.0.4.data/data/share/omni-cortex/dashboard/backend/chat_service.py +0 -140
  13. omni_cortex-1.0.4.dist-info/RECORD +0 -17
  14. {omni_cortex-1.0.4.data → omni_cortex-1.3.0.data}/data/share/omni-cortex/dashboard/backend/pyproject.toml +0 -0
  15. {omni_cortex-1.0.4.data → omni_cortex-1.3.0.data}/data/share/omni-cortex/dashboard/backend/websocket_manager.py +0 -0
  16. {omni_cortex-1.0.4.data → omni_cortex-1.3.0.data}/data/share/omni-cortex/hooks/post_tool_use.py +0 -0
  17. {omni_cortex-1.0.4.data → omni_cortex-1.3.0.data}/data/share/omni-cortex/hooks/pre_tool_use.py +0 -0
  18. {omni_cortex-1.0.4.data → omni_cortex-1.3.0.data}/data/share/omni-cortex/hooks/stop.py +0 -0
  19. {omni_cortex-1.0.4.data → omni_cortex-1.3.0.data}/data/share/omni-cortex/hooks/subagent_stop.py +0 -0
  20. {omni_cortex-1.0.4.dist-info → omni_cortex-1.3.0.dist-info}/WHEEL +0 -0
  21. {omni_cortex-1.0.4.dist-info → omni_cortex-1.3.0.dist-info}/entry_points.txt +0 -0
  22. {omni_cortex-1.0.4.dist-info → omni_cortex-1.3.0.dist-info}/licenses/LICENSE +0 -0
@@ -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
@@ -20,8 +21,11 @@ from database import (
20
21
  bulk_update_memory_status,
21
22
  delete_memory,
22
23
  get_activities,
24
+ get_activity_detail,
23
25
  get_activity_heatmap,
24
26
  get_all_tags,
27
+ get_command_usage,
28
+ get_mcp_usage,
25
29
  get_memories,
26
30
  get_memories_needing_review,
27
31
  get_memory_by_id,
@@ -31,16 +35,41 @@ from database import (
31
35
  get_relationship_graph,
32
36
  get_relationships,
33
37
  get_sessions,
38
+ get_skill_usage,
34
39
  get_timeline,
35
40
  get_tool_usage,
36
41
  get_type_distribution,
37
42
  search_memories,
38
43
  update_memory,
39
44
  )
40
- from models import ChatRequest, ChatResponse, FilterParams, MemoryUpdate, ProjectInfo
45
+ from logging_config import log_success, log_error
46
+ from models import (
47
+ ChatRequest,
48
+ ChatResponse,
49
+ ConversationSaveRequest,
50
+ ConversationSaveResponse,
51
+ FilterParams,
52
+ MemoryUpdate,
53
+ ProjectInfo,
54
+ ProjectRegistration,
55
+ BatchImageGenerationRequest,
56
+ BatchImageGenerationResponse,
57
+ ImageRefineRequest,
58
+ SingleImageRequestModel,
59
+ SingleImageResponseModel,
60
+ )
61
+ from project_config import (
62
+ load_config,
63
+ add_registered_project,
64
+ remove_registered_project,
65
+ toggle_favorite,
66
+ add_scan_directory,
67
+ remove_scan_directory,
68
+ )
41
69
  from project_scanner import scan_projects
42
70
  from websocket_manager import manager
43
71
  import chat_service
72
+ from image_service import image_service, ImagePreset, SingleImageRequest
44
73
 
45
74
 
46
75
  class DatabaseChangeHandler(FileSystemEventHandler):
@@ -145,6 +174,70 @@ async def list_projects():
145
174
  return scan_projects()
146
175
 
147
176
 
177
+ # --- Project Management Endpoints ---
178
+
179
+
180
+ @app.get("/api/projects/config")
181
+ async def get_project_config():
182
+ """Get project configuration (scan dirs, counts)."""
183
+ config = load_config()
184
+ return {
185
+ "scan_directories": config.scan_directories,
186
+ "registered_count": len(config.registered_projects),
187
+ "favorites_count": len(config.favorites),
188
+ }
189
+
190
+
191
+ @app.post("/api/projects/register")
192
+ async def register_project(body: ProjectRegistration):
193
+ """Manually register a project by path."""
194
+ success = add_registered_project(body.path, body.display_name)
195
+ if not success:
196
+ raise HTTPException(400, "Invalid path or already registered")
197
+ return {"success": True}
198
+
199
+
200
+ @app.delete("/api/projects/register")
201
+ async def unregister_project(path: str = Query(..., description="Project path to unregister")):
202
+ """Remove a registered project."""
203
+ success = remove_registered_project(path)
204
+ if not success:
205
+ raise HTTPException(404, "Project not found")
206
+ return {"success": True}
207
+
208
+
209
+ @app.post("/api/projects/favorite")
210
+ async def toggle_project_favorite(path: str = Query(..., description="Project path to toggle favorite")):
211
+ """Toggle favorite status for a project."""
212
+ is_favorite = toggle_favorite(path)
213
+ return {"is_favorite": is_favorite}
214
+
215
+
216
+ @app.post("/api/projects/scan-directories")
217
+ async def add_scan_dir(directory: str = Query(..., description="Directory path to add")):
218
+ """Add a directory to auto-scan list."""
219
+ success = add_scan_directory(directory)
220
+ if not success:
221
+ raise HTTPException(400, "Invalid directory or already added")
222
+ return {"success": True}
223
+
224
+
225
+ @app.delete("/api/projects/scan-directories")
226
+ async def remove_scan_dir(directory: str = Query(..., description="Directory path to remove")):
227
+ """Remove a directory from auto-scan list."""
228
+ success = remove_scan_directory(directory)
229
+ if not success:
230
+ raise HTTPException(404, "Directory not found")
231
+ return {"success": True}
232
+
233
+
234
+ @app.post("/api/projects/refresh")
235
+ async def refresh_projects():
236
+ """Force rescan of all project directories."""
237
+ projects = scan_projects()
238
+ return {"count": len(projects)}
239
+
240
+
148
241
  @app.get("/api/memories")
149
242
  async def list_memories(
150
243
  project: str = Query(..., description="Path to the database file"),
@@ -160,23 +253,30 @@ async def list_memories(
160
253
  offset: int = 0,
161
254
  ):
162
255
  """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
- )
256
+ try:
257
+ if not Path(project).exists():
258
+ log_error("/api/memories", FileNotFoundError("Database not found"), project=project)
259
+ raise HTTPException(status_code=404, detail="Database not found")
260
+
261
+ filters = FilterParams(
262
+ memory_type=memory_type,
263
+ status=status,
264
+ tags=tags.split(",") if tags else None,
265
+ search=search,
266
+ min_importance=min_importance,
267
+ max_importance=max_importance,
268
+ sort_by=sort_by,
269
+ sort_order=sort_order,
270
+ limit=limit,
271
+ offset=offset,
272
+ )
178
273
 
179
- return get_memories(project, filters)
274
+ memories = get_memories(project, filters)
275
+ log_success("/api/memories", count=len(memories), offset=offset, filters=bool(search or memory_type))
276
+ return memories
277
+ except Exception as e:
278
+ log_error("/api/memories", e, project=project)
279
+ raise
180
280
 
181
281
 
182
282
  # NOTE: These routes MUST be defined before /api/memories/{memory_id} to avoid path conflicts
@@ -237,16 +337,25 @@ async def update_memory_endpoint(
237
337
  project: str = Query(..., description="Path to the database file"),
238
338
  ):
239
339
  """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
340
+ try:
341
+ if not Path(project).exists():
342
+ log_error("/api/memories/update", FileNotFoundError("Database not found"), memory_id=memory_id)
343
+ raise HTTPException(status_code=404, detail="Database not found")
344
+
345
+ updated = update_memory(project, memory_id, updates)
346
+ if not updated:
347
+ log_error("/api/memories/update", ValueError("Memory not found"), memory_id=memory_id)
348
+ raise HTTPException(status_code=404, detail="Memory not found")
349
+
350
+ # Notify connected clients
351
+ await manager.broadcast("memory_updated", updated.model_dump(by_alias=True))
352
+ log_success("/api/memories/update", memory_id=memory_id, fields_updated=len(updates.model_dump(exclude_unset=True)))
353
+ return updated
354
+ except HTTPException:
355
+ raise
356
+ except Exception as e:
357
+ log_error("/api/memories/update", e, memory_id=memory_id)
358
+ raise
250
359
 
251
360
 
252
361
  @app.delete("/api/memories/{memory_id}")
@@ -255,16 +364,25 @@ async def delete_memory_endpoint(
255
364
  project: str = Query(..., description="Path to the database file"),
256
365
  ):
257
366
  """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}
367
+ try:
368
+ if not Path(project).exists():
369
+ log_error("/api/memories/delete", FileNotFoundError("Database not found"), memory_id=memory_id)
370
+ raise HTTPException(status_code=404, detail="Database not found")
371
+
372
+ deleted = delete_memory(project, memory_id)
373
+ if not deleted:
374
+ log_error("/api/memories/delete", ValueError("Memory not found"), memory_id=memory_id)
375
+ raise HTTPException(status_code=404, detail="Memory not found")
376
+
377
+ # Notify connected clients
378
+ await manager.broadcast("memory_deleted", {"id": memory_id})
379
+ log_success("/api/memories/delete", memory_id=memory_id)
380
+ return {"message": "Memory deleted", "id": memory_id}
381
+ except HTTPException:
382
+ raise
383
+ except Exception as e:
384
+ log_error("/api/memories/delete", e, memory_id=memory_id)
385
+ raise
268
386
 
269
387
 
270
388
  @app.get("/api/memories/stats/summary")
@@ -393,6 +511,63 @@ async def get_memory_growth_endpoint(
393
511
  return get_memory_growth(project, days)
394
512
 
395
513
 
514
+ # --- Command Analytics Endpoints ---
515
+
516
+
517
+ @app.get("/api/stats/command-usage")
518
+ async def get_command_usage_endpoint(
519
+ project: str = Query(..., description="Path to the database file"),
520
+ scope: Optional[str] = Query(None, description="Filter by scope: 'universal' or 'project'"),
521
+ days: int = Query(30, ge=1, le=365),
522
+ ):
523
+ """Get slash command usage statistics."""
524
+ if not Path(project).exists():
525
+ raise HTTPException(status_code=404, detail="Database not found")
526
+
527
+ return get_command_usage(project, scope, days)
528
+
529
+
530
+ @app.get("/api/stats/skill-usage")
531
+ async def get_skill_usage_endpoint(
532
+ project: str = Query(..., description="Path to the database file"),
533
+ scope: Optional[str] = Query(None, description="Filter by scope: 'universal' or 'project'"),
534
+ days: int = Query(30, ge=1, le=365),
535
+ ):
536
+ """Get skill usage statistics."""
537
+ if not Path(project).exists():
538
+ raise HTTPException(status_code=404, detail="Database not found")
539
+
540
+ return get_skill_usage(project, scope, days)
541
+
542
+
543
+ @app.get("/api/stats/mcp-usage")
544
+ async def get_mcp_usage_endpoint(
545
+ project: str = Query(..., description="Path to the database file"),
546
+ days: int = Query(30, ge=1, le=365),
547
+ ):
548
+ """Get MCP server usage statistics."""
549
+ if not Path(project).exists():
550
+ raise HTTPException(status_code=404, detail="Database not found")
551
+
552
+ return get_mcp_usage(project, days)
553
+
554
+
555
+ @app.get("/api/activities/{activity_id}")
556
+ async def get_activity_detail_endpoint(
557
+ activity_id: str,
558
+ project: str = Query(..., description="Path to the database file"),
559
+ ):
560
+ """Get full activity details including complete input/output."""
561
+ if not Path(project).exists():
562
+ raise HTTPException(status_code=404, detail="Database not found")
563
+
564
+ activity = get_activity_detail(project, activity_id)
565
+ if not activity:
566
+ raise HTTPException(status_code=404, detail="Activity not found")
567
+
568
+ return activity
569
+
570
+
396
571
  # --- Session Context Endpoints ---
397
572
 
398
573
 
@@ -454,16 +629,184 @@ async def chat_with_memories(
454
629
  project: str = Query(..., description="Path to the database file"),
455
630
  ):
456
631
  """Ask a natural language question about memories."""
632
+ try:
633
+ if not Path(project).exists():
634
+ log_error("/api/chat", FileNotFoundError("Database not found"), question=request.question[:50])
635
+ raise HTTPException(status_code=404, detail="Database not found")
636
+
637
+ result = await chat_service.ask_about_memories(
638
+ project,
639
+ request.question,
640
+ request.max_memories,
641
+ )
642
+
643
+ log_success("/api/chat", question_len=len(request.question), sources=len(result.get("sources", [])))
644
+ return ChatResponse(**result)
645
+ except HTTPException:
646
+ raise
647
+ except Exception as e:
648
+ log_error("/api/chat", e, question=request.question[:50])
649
+ raise
650
+
651
+
652
+ @app.get("/api/chat/stream")
653
+ async def stream_chat(
654
+ project: str = Query(..., description="Path to the database file"),
655
+ question: str = Query(..., description="The question to ask"),
656
+ max_memories: int = Query(10, ge=1, le=50),
657
+ ):
658
+ """SSE endpoint for streaming chat responses."""
659
+ from fastapi.responses import StreamingResponse
660
+
457
661
  if not Path(project).exists():
458
662
  raise HTTPException(status_code=404, detail="Database not found")
459
663
 
460
- result = await chat_service.ask_about_memories(
461
- project,
462
- request.question,
463
- request.max_memories,
664
+ async def event_generator():
665
+ try:
666
+ async for event in chat_service.stream_ask_about_memories(project, question, max_memories):
667
+ yield f"data: {json.dumps(event)}\n\n"
668
+ except Exception as e:
669
+ yield f"data: {json.dumps({'type': 'error', 'data': str(e)})}\n\n"
670
+
671
+ return StreamingResponse(
672
+ event_generator(),
673
+ media_type="text/event-stream",
674
+ headers={
675
+ "Cache-Control": "no-cache",
676
+ "Connection": "keep-alive",
677
+ "X-Accel-Buffering": "no",
678
+ }
464
679
  )
465
680
 
466
- return ChatResponse(**result)
681
+
682
+ @app.post("/api/chat/save", response_model=ConversationSaveResponse)
683
+ async def save_chat_conversation(
684
+ request: ConversationSaveRequest,
685
+ project: str = Query(..., description="Path to the database file"),
686
+ ):
687
+ """Save a chat conversation as a memory."""
688
+ try:
689
+ if not Path(project).exists():
690
+ log_error("/api/chat/save", FileNotFoundError("Database not found"))
691
+ raise HTTPException(status_code=404, detail="Database not found")
692
+
693
+ result = await chat_service.save_conversation(
694
+ project,
695
+ [msg.model_dump() for msg in request.messages],
696
+ request.referenced_memory_ids,
697
+ request.importance or 60,
698
+ )
699
+
700
+ log_success("/api/chat/save", memory_id=result["memory_id"], messages=len(request.messages))
701
+ return ConversationSaveResponse(**result)
702
+ except HTTPException:
703
+ raise
704
+ except Exception as e:
705
+ log_error("/api/chat/save", e)
706
+ raise
707
+
708
+
709
+ # --- Image Generation Endpoints ---
710
+
711
+
712
+ @app.get("/api/image/status")
713
+ async def get_image_status():
714
+ """Check if image generation is available."""
715
+ return {
716
+ "available": image_service.is_available(),
717
+ "message": "Image generation ready" if image_service.is_available()
718
+ else "Configure GEMINI_API_KEY and install google-genai for image generation",
719
+ }
720
+
721
+
722
+ @app.get("/api/image/presets")
723
+ async def get_image_presets():
724
+ """Get available image preset templates."""
725
+ return {"presets": image_service.get_presets()}
726
+
727
+
728
+ @app.post("/api/image/generate-batch", response_model=BatchImageGenerationResponse)
729
+ async def generate_images_batch(
730
+ request: BatchImageGenerationRequest,
731
+ db_path: str = Query(..., alias="project", description="Path to the database file"),
732
+ ):
733
+ """Generate multiple images with different presets/prompts."""
734
+ # Validate image count
735
+ if len(request.images) not in [1, 2, 4]:
736
+ return BatchImageGenerationResponse(
737
+ success=False,
738
+ errors=["Must request 1, 2, or 4 images"]
739
+ )
740
+
741
+ # Build memory context
742
+ memory_context = ""
743
+ if request.memory_ids:
744
+ memory_context = image_service.build_memory_context(db_path, request.memory_ids)
745
+
746
+ # Build chat context
747
+ chat_context = image_service.build_chat_context(request.chat_messages)
748
+
749
+ # Convert request models to internal format
750
+ image_requests = [
751
+ SingleImageRequest(
752
+ preset=ImagePreset(img.preset),
753
+ custom_prompt=img.custom_prompt,
754
+ aspect_ratio=img.aspect_ratio,
755
+ image_size=img.image_size
756
+ )
757
+ for img in request.images
758
+ ]
759
+
760
+ result = await image_service.generate_batch(
761
+ requests=image_requests,
762
+ memory_context=memory_context,
763
+ chat_context=chat_context,
764
+ use_search_grounding=request.use_search_grounding
765
+ )
766
+
767
+ return BatchImageGenerationResponse(
768
+ success=result.success,
769
+ images=[
770
+ SingleImageResponseModel(
771
+ success=img.success,
772
+ image_data=img.image_data,
773
+ text_response=img.text_response,
774
+ thought_signature=img.thought_signature,
775
+ image_id=img.image_id,
776
+ error=img.error,
777
+ index=img.index
778
+ )
779
+ for img in result.images
780
+ ],
781
+ errors=result.errors
782
+ )
783
+
784
+
785
+ @app.post("/api/image/refine", response_model=SingleImageResponseModel)
786
+ async def refine_image(request: ImageRefineRequest):
787
+ """Refine an existing generated image with a new prompt."""
788
+ result = await image_service.refine_image(
789
+ image_id=request.image_id,
790
+ refinement_prompt=request.refinement_prompt,
791
+ aspect_ratio=request.aspect_ratio,
792
+ image_size=request.image_size
793
+ )
794
+
795
+ return SingleImageResponseModel(
796
+ success=result.success,
797
+ image_data=result.image_data,
798
+ text_response=result.text_response,
799
+ thought_signature=result.thought_signature,
800
+ image_id=result.image_id,
801
+ error=result.error
802
+ )
803
+
804
+
805
+ @app.post("/api/image/clear-conversation")
806
+ async def clear_image_conversation(image_id: Optional[str] = None):
807
+ """Clear image conversation history. If image_id provided, clear only that image."""
808
+ image_service.clear_conversation(image_id)
809
+ return {"status": "cleared", "image_id": image_id}
467
810
 
468
811
 
469
812
  # --- WebSocket Endpoint ---
@@ -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] = []