omni-cortex 1.12.1__py3-none-any.whl → 1.13.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 (27) hide show
  1. {omni_cortex-1.12.1.data → omni_cortex-1.13.0.data}/data/share/omni-cortex/dashboard/backend/chat_service.py +217 -16
  2. {omni_cortex-1.12.1.data → omni_cortex-1.13.0.data}/data/share/omni-cortex/dashboard/backend/database.py +224 -1
  3. {omni_cortex-1.12.1.data → omni_cortex-1.13.0.data}/data/share/omni-cortex/dashboard/backend/main.py +126 -37
  4. {omni_cortex-1.12.1.data → omni_cortex-1.13.0.data}/data/share/omni-cortex/dashboard/backend/models.py +31 -1
  5. {omni_cortex-1.12.1.dist-info → omni_cortex-1.13.0.dist-info}/METADATA +1 -1
  6. omni_cortex-1.13.0.dist-info/RECORD +26 -0
  7. omni_cortex-1.12.1.dist-info/RECORD +0 -26
  8. {omni_cortex-1.12.1.data → omni_cortex-1.13.0.data}/data/share/omni-cortex/dashboard/backend/.env.example +0 -0
  9. {omni_cortex-1.12.1.data → omni_cortex-1.13.0.data}/data/share/omni-cortex/dashboard/backend/backfill_summaries.py +0 -0
  10. {omni_cortex-1.12.1.data → omni_cortex-1.13.0.data}/data/share/omni-cortex/dashboard/backend/image_service.py +0 -0
  11. {omni_cortex-1.12.1.data → omni_cortex-1.13.0.data}/data/share/omni-cortex/dashboard/backend/logging_config.py +0 -0
  12. {omni_cortex-1.12.1.data → omni_cortex-1.13.0.data}/data/share/omni-cortex/dashboard/backend/project_config.py +0 -0
  13. {omni_cortex-1.12.1.data → omni_cortex-1.13.0.data}/data/share/omni-cortex/dashboard/backend/project_scanner.py +0 -0
  14. {omni_cortex-1.12.1.data → omni_cortex-1.13.0.data}/data/share/omni-cortex/dashboard/backend/prompt_security.py +0 -0
  15. {omni_cortex-1.12.1.data → omni_cortex-1.13.0.data}/data/share/omni-cortex/dashboard/backend/pyproject.toml +0 -0
  16. {omni_cortex-1.12.1.data → omni_cortex-1.13.0.data}/data/share/omni-cortex/dashboard/backend/security.py +0 -0
  17. {omni_cortex-1.12.1.data → omni_cortex-1.13.0.data}/data/share/omni-cortex/dashboard/backend/uv.lock +0 -0
  18. {omni_cortex-1.12.1.data → omni_cortex-1.13.0.data}/data/share/omni-cortex/dashboard/backend/websocket_manager.py +0 -0
  19. {omni_cortex-1.12.1.data → omni_cortex-1.13.0.data}/data/share/omni-cortex/hooks/post_tool_use.py +0 -0
  20. {omni_cortex-1.12.1.data → omni_cortex-1.13.0.data}/data/share/omni-cortex/hooks/pre_tool_use.py +0 -0
  21. {omni_cortex-1.12.1.data → omni_cortex-1.13.0.data}/data/share/omni-cortex/hooks/session_utils.py +0 -0
  22. {omni_cortex-1.12.1.data → omni_cortex-1.13.0.data}/data/share/omni-cortex/hooks/stop.py +0 -0
  23. {omni_cortex-1.12.1.data → omni_cortex-1.13.0.data}/data/share/omni-cortex/hooks/subagent_stop.py +0 -0
  24. {omni_cortex-1.12.1.data → omni_cortex-1.13.0.data}/data/share/omni-cortex/hooks/user_prompt.py +0 -0
  25. {omni_cortex-1.12.1.dist-info → omni_cortex-1.13.0.dist-info}/WHEEL +0 -0
  26. {omni_cortex-1.12.1.dist-info → omni_cortex-1.13.0.dist-info}/entry_points.txt +0 -0
  27. {omni_cortex-1.12.1.dist-info → omni_cortex-1.13.0.dist-info}/licenses/LICENSE +0 -0
@@ -45,31 +45,51 @@ def is_available() -> bool:
45
45
  def build_style_context_prompt(style_profile: dict) -> str:
46
46
  """Build a prompt section describing user's communication style."""
47
47
 
48
- tone_dist = style_profile.get("tone_distribution", {})
48
+ # Handle both camelCase (new format) and snake_case (old format)
49
+ tone_dist = style_profile.get("toneDistribution") or style_profile.get("tone_distribution", {})
49
50
  tone_list = ", ".join(tone_dist.keys()) if tone_dist else "neutral"
50
- avg_words = style_profile.get("avg_word_count", 20)
51
- question_freq = style_profile.get("question_frequency", 0)
51
+ avg_words = style_profile.get("avgWordCount") or style_profile.get("avg_word_count", 20)
52
+ question_pct = style_profile.get("questionPercentage") or (style_profile.get("question_frequency", 0) * 100)
53
+ primary_tone = style_profile.get("primaryTone") or style_profile.get("primary_tone", "direct")
52
54
 
53
- markers = style_profile.get("key_markers", [])
55
+ markers = style_profile.get("styleMarkers") or style_profile.get("key_markers", [])
54
56
  markers_text = "\n".join(f"- {m}" for m in markers) if markers else "- Direct and clear"
55
57
 
58
+ # Get sample messages for concrete examples
59
+ samples = style_profile.get("sampleMessages") or style_profile.get("sample_messages", [])
60
+ samples_text = ""
61
+ if samples:
62
+ samples_text = "\n**Examples of how the user actually writes:**\n"
63
+ for i, sample in enumerate(samples[:3], 1):
64
+ # Truncate long samples
65
+ truncated = sample[:200] + "..." if len(sample) > 200 else sample
66
+ samples_text += f'{i}. "{truncated}"\n'
67
+
56
68
  return f"""
57
- ## User Communication Style Profile
69
+ ## IMPORTANT: User Communication Style Mode ENABLED
58
70
 
59
- When the user requests content "in their style" or "like they write", follow these patterns:
71
+ You MUST write ALL responses in the user's personal communication style. This is NOT optional - every response should sound like the user wrote it themselves.
60
72
 
61
- **Typical Message Length:** ~{int(avg_words)} words
62
- **Common Tones:** {tone_list}
63
- **Question Frequency:** {int(question_freq * 100)}% of messages include questions
73
+ **User's Writing Profile:**
74
+ - Primary Tone: {primary_tone}
75
+ - Typical Message Length: ~{int(avg_words)} words per message
76
+ - Common Tones: {tone_list}
77
+ - Question Usage: {int(question_pct)}% of their messages include questions
64
78
 
65
- **Key Style Markers:**
79
+ **Style Markers to Emulate:**
66
80
  {markers_text}
67
-
68
- **Guidelines:**
69
- - Match the user's typical message length and structure
70
- - Use their common vocabulary patterns
71
- - Mirror their tone and formality level
72
- - If they're typically direct, be concise; if detailed, be comprehensive
81
+ {samples_text}
82
+ **MANDATORY Guidelines:**
83
+ 1. Write as if YOU are the user speaking - use their voice, not a formal assistant voice
84
+ 2. Match their casual/formal level - if they use contractions and slang, you should too
85
+ 3. Mirror their sentence structure and rhythm
86
+ 4. Use similar vocabulary and expressions they would use
87
+ 5. If their style is conversational, be conversational (e.g., "Right, so here's the deal...")
88
+ 6. If their style is direct, be direct and skip unnecessary pleasantries
89
+ 7. Do NOT use phrases like "Based on the memories" or "According to the data" if that's not how they write
90
+ 8. Study the example messages above and mimic that exact writing style
91
+
92
+ Remember: The user has enabled "Write in My Style" mode. Your response should sound EXACTLY like something they would write themselves.
73
93
  """
74
94
 
75
95
 
@@ -369,3 +389,184 @@ async def ask_about_memories(
369
389
  "sources": sources,
370
390
  "error": None,
371
391
  }
392
+
393
+
394
+ # Platform-specific formatting guidance
395
+ PLATFORM_FORMATS = {
396
+ "skool_post": "Skool community post - can be longer, use formatting, be educational",
397
+ "dm": "Direct message - conversational, personal, concise",
398
+ "email": "Email - professional greeting/closing, clear structure",
399
+ "comment": "Comment reply - brief, direct, engaging",
400
+ "general": "General response - balanced approach",
401
+ }
402
+
403
+ # Response templates with structural guidance
404
+ TEMPLATES = {
405
+ "answer": "Directly answer their question with clear explanation",
406
+ "guide": "Provide step-by-step guidance or recommendations",
407
+ "redirect": "Acknowledge and redirect to a relevant resource",
408
+ "acknowledge": "Acknowledge their point and add follow-up question",
409
+ }
410
+
411
+
412
+ def build_compose_prompt(
413
+ incoming_message: str,
414
+ style_profile: dict,
415
+ context_type: str,
416
+ template: Optional[str],
417
+ tone_level: int,
418
+ memory_context: str,
419
+ ) -> str:
420
+ """Build the prompt for composing a response in user's style.
421
+
422
+ Args:
423
+ incoming_message: The message to respond to
424
+ style_profile: User's style profile dictionary
425
+ context_type: Platform context (skool_post, dm, email, comment, general)
426
+ template: Optional response template (answer, guide, redirect, acknowledge)
427
+ tone_level: Tone formality level (0-100)
428
+ memory_context: Relevant memories formatted as context
429
+
430
+ Returns:
431
+ Complete prompt for response generation
432
+ """
433
+ # Get platform-specific formatting guidance
434
+ platform_guidance = PLATFORM_FORMATS.get(context_type, PLATFORM_FORMATS["general"])
435
+
436
+ # Get template guidance
437
+ template_guidance = ""
438
+ if template:
439
+ template_guidance = f"\n**Response Structure:** {TEMPLATES.get(template, '')}"
440
+
441
+ # Convert tone level to guidance
442
+ if tone_level < 25:
443
+ tone_guidance = "Very casual and relaxed - use slang, contractions, informal language"
444
+ elif tone_level < 50:
445
+ tone_guidance = "Casual but clear - conversational with some structure"
446
+ elif tone_level < 75:
447
+ tone_guidance = "Professional but approachable - clear and organized"
448
+ else:
449
+ tone_guidance = "Very professional and formal - polished and structured"
450
+
451
+ # Build style context
452
+ style_context = build_style_context_prompt(style_profile)
453
+
454
+ # Build the complete prompt
455
+ prompt = f"""{style_context}
456
+
457
+ ## RESPONSE COMPOSITION TASK
458
+
459
+ You need to respond to the following message:
460
+
461
+ <incoming_message>
462
+ {xml_escape(incoming_message)}
463
+ </incoming_message>
464
+
465
+ **Context:** {platform_guidance}
466
+ **Tone Level:** {tone_guidance}{template_guidance}
467
+
468
+ """
469
+
470
+ # Add memory context if provided
471
+ if memory_context:
472
+ prompt += f"""
473
+ **Relevant Knowledge from Your Memories:**
474
+
475
+ <memories>
476
+ {memory_context}
477
+ </memories>
478
+
479
+ Use this information naturally in your response if relevant. Don't explicitly cite "memories" - just use the knowledge as if you remember it.
480
+
481
+ """
482
+
483
+ prompt += """
484
+ **Your Task:**
485
+ 1. Write a response to the incoming message in YOUR voice (the user's voice)
486
+ 2. Use the knowledge from your memories naturally if relevant
487
+ 3. Match the tone level specified above
488
+ 4. Follow the platform context guidelines
489
+ 5. Sound exactly like something you would write yourself
490
+
491
+ Write the response now:"""
492
+
493
+ return prompt
494
+
495
+
496
+ async def compose_response(
497
+ db_path: str,
498
+ incoming_message: str,
499
+ context_type: str = "general",
500
+ template: Optional[str] = None,
501
+ tone_level: int = 50,
502
+ include_memories: bool = True,
503
+ style_profile: Optional[dict] = None,
504
+ ) -> dict:
505
+ """Compose a response to an incoming message in the user's style.
506
+
507
+ Args:
508
+ db_path: Path to the database file
509
+ incoming_message: The message to respond to
510
+ context_type: Platform context (skool_post, dm, email, comment, general)
511
+ template: Optional response template (answer, guide, redirect, acknowledge)
512
+ tone_level: Tone formality level (0-100)
513
+ include_memories: Whether to include relevant memories
514
+ style_profile: User's style profile dictionary
515
+
516
+ Returns:
517
+ Dict with response, sources, and metadata
518
+ """
519
+ if not is_available():
520
+ return {
521
+ "response": "Chat is not available. Please configure GEMINI_API_KEY or GOOGLE_API_KEY environment variable.",
522
+ "sources": [],
523
+ "error": "api_key_missing",
524
+ }
525
+
526
+ client = get_client()
527
+ if not client:
528
+ return {
529
+ "response": "Failed to initialize Gemini client.",
530
+ "sources": [],
531
+ "error": "client_init_failed",
532
+ }
533
+
534
+ # Get relevant memories if requested
535
+ memory_context = ""
536
+ sources = []
537
+ if include_memories:
538
+ memory_context, sources = _get_memories_and_sources(db_path, incoming_message, max_memories=5)
539
+
540
+ # Get or compute style profile
541
+ if not style_profile:
542
+ from database import compute_style_profile_from_messages
543
+ style_profile = compute_style_profile_from_messages(db_path)
544
+
545
+ # Build the compose prompt
546
+ prompt = build_compose_prompt(
547
+ incoming_message=incoming_message,
548
+ style_profile=style_profile,
549
+ context_type=context_type,
550
+ template=template,
551
+ tone_level=tone_level,
552
+ memory_context=memory_context,
553
+ )
554
+
555
+ try:
556
+ response = client.models.generate_content(
557
+ model="gemini-2.0-flash",
558
+ contents=prompt,
559
+ )
560
+ composed_response = response.text
561
+ except Exception as e:
562
+ return {
563
+ "response": f"Failed to generate response: {str(e)}",
564
+ "sources": sources,
565
+ "error": "generation_failed",
566
+ }
567
+
568
+ return {
569
+ "response": composed_response,
570
+ "sources": sources,
571
+ "error": None,
572
+ }
@@ -1189,10 +1189,14 @@ def get_user_messages(
1189
1189
  except (json.JSONDecodeError, TypeError):
1190
1190
  pass
1191
1191
 
1192
+ # Get primary tone (first in the list) for frontend compatibility
1193
+ primary_tone = tone_indicators[0] if tone_indicators else None
1194
+
1192
1195
  messages.append({
1193
1196
  "id": row["id"],
1194
1197
  "session_id": row["session_id"],
1195
- "timestamp": row["timestamp"],
1198
+ "created_at": row["timestamp"], # Frontend expects created_at
1199
+ "timestamp": row["timestamp"], # Keep for backward compatibility
1196
1200
  "content": row["content"],
1197
1201
  "word_count": row["word_count"],
1198
1202
  "char_count": row["char_count"],
@@ -1200,6 +1204,7 @@ def get_user_messages(
1200
1204
  "has_code_blocks": bool(row["has_code_blocks"]),
1201
1205
  "has_questions": bool(row["has_questions"]),
1202
1206
  "has_commands": bool(row["has_commands"]),
1207
+ "tone": primary_tone, # Frontend expects single tone string
1203
1208
  "tone_indicators": tone_indicators,
1204
1209
  "project_path": row["project_path"],
1205
1210
  })
@@ -1428,3 +1433,221 @@ def _row_to_sample(row) -> dict:
1428
1433
  "has_questions": bool(row["has_questions"]),
1429
1434
  "tone_indicators": tone_indicators,
1430
1435
  }
1436
+
1437
+
1438
+ def get_style_samples_by_category(db_path: str, samples_per_tone: int = 3) -> dict:
1439
+ """Get sample user messages grouped by style category.
1440
+
1441
+ Maps tone_indicators to frontend categories:
1442
+ - professional: direct, polite, formal tones
1443
+ - casual: casual tones
1444
+ - technical: technical tones
1445
+ - creative: unique patterns, inquisitive tones
1446
+
1447
+ Args:
1448
+ db_path: Path to database
1449
+ samples_per_tone: Max samples per category
1450
+
1451
+ Returns:
1452
+ Dict with professional, casual, technical, creative lists
1453
+ """
1454
+ conn = get_connection(db_path)
1455
+
1456
+ # Check if user_messages table exists
1457
+ table_check = conn.execute(
1458
+ "SELECT name FROM sqlite_master WHERE type='table' AND name='user_messages'"
1459
+ ).fetchone()
1460
+
1461
+ if not table_check:
1462
+ conn.close()
1463
+ return {
1464
+ "professional": [],
1465
+ "casual": [],
1466
+ "technical": [],
1467
+ "creative": []
1468
+ }
1469
+
1470
+ result = {
1471
+ "professional": [],
1472
+ "casual": [],
1473
+ "technical": [],
1474
+ "creative": []
1475
+ }
1476
+
1477
+ # Mapping from tone_indicators to categories
1478
+ tone_to_category = {
1479
+ "direct": "professional",
1480
+ "polite": "professional",
1481
+ "formal": "professional",
1482
+ "casual": "casual",
1483
+ "technical": "technical",
1484
+ "inquisitive": "creative",
1485
+ "urgent": "professional",
1486
+ }
1487
+
1488
+ # Get all messages with tone indicators
1489
+ cursor = conn.execute(
1490
+ """SELECT content, tone_indicators FROM user_messages
1491
+ WHERE tone_indicators IS NOT NULL AND tone_indicators != '[]'
1492
+ ORDER BY timestamp DESC LIMIT 200"""
1493
+ )
1494
+
1495
+ for row in cursor.fetchall():
1496
+ content = row["content"]
1497
+ try:
1498
+ tones = json.loads(row["tone_indicators"]) if row["tone_indicators"] else []
1499
+ except (json.JSONDecodeError, TypeError):
1500
+ tones = []
1501
+
1502
+ # Map to categories
1503
+ for tone in tones:
1504
+ category = tone_to_category.get(tone.lower(), "creative")
1505
+ if len(result[category]) < samples_per_tone:
1506
+ # Truncate content for preview
1507
+ preview = content[:200] + "..." if len(content) > 200 else content
1508
+ if preview not in result[category]:
1509
+ result[category].append(preview)
1510
+ break # Only add to first matching category
1511
+
1512
+ # Fill any empty categories with recent messages
1513
+ if any(len(v) == 0 for v in result.values()):
1514
+ cursor = conn.execute(
1515
+ "SELECT content FROM user_messages ORDER BY timestamp DESC LIMIT ?",
1516
+ (samples_per_tone * 4,)
1517
+ )
1518
+ fallback_messages = [
1519
+ row["content"][:200] + "..." if len(row["content"]) > 200 else row["content"]
1520
+ for row in cursor.fetchall()
1521
+ ]
1522
+
1523
+ for category in result:
1524
+ if len(result[category]) == 0 and fallback_messages:
1525
+ # Take messages for empty categories
1526
+ for msg in fallback_messages[:samples_per_tone]:
1527
+ if msg not in [m for v in result.values() for m in v]:
1528
+ result[category].append(msg)
1529
+
1530
+ conn.close()
1531
+ return result
1532
+
1533
+
1534
+ def compute_style_profile_from_messages(db_path: str) -> Optional[dict]:
1535
+ """Compute a style profile from user_messages table.
1536
+
1537
+ This is used when no pre-computed profile exists.
1538
+
1539
+ Returns format expected by frontend StyleProfileCard:
1540
+ - total_messages: int
1541
+ - avg_word_count: float
1542
+ - primary_tone: str
1543
+ - question_percentage: float
1544
+ - tone_distribution: dict[str, int]
1545
+ - style_markers: list[str]
1546
+ """
1547
+ conn = get_connection(db_path)
1548
+
1549
+ # Check if user_messages table exists
1550
+ table_check = conn.execute(
1551
+ "SELECT name FROM sqlite_master WHERE type='table' AND name='user_messages'"
1552
+ ).fetchone()
1553
+
1554
+ if not table_check:
1555
+ conn.close()
1556
+ return None
1557
+
1558
+ # Get total count and averages
1559
+ stats = conn.execute(
1560
+ """SELECT
1561
+ COUNT(*) as total,
1562
+ AVG(word_count) as avg_words,
1563
+ AVG(char_count) as avg_chars,
1564
+ SUM(CASE WHEN has_questions = 1 THEN 1 ELSE 0 END) as question_count
1565
+ FROM user_messages"""
1566
+ ).fetchone()
1567
+
1568
+ if not stats or stats["total"] == 0:
1569
+ conn.close()
1570
+ return None
1571
+
1572
+ total_messages = stats["total"]
1573
+ avg_word_count = stats["avg_words"] or 0
1574
+ question_percentage = (stats["question_count"] / total_messages * 100) if total_messages > 0 else 0
1575
+
1576
+ # Compute tone distribution
1577
+ tone_distribution = {}
1578
+ cursor = conn.execute(
1579
+ "SELECT tone_indicators FROM user_messages WHERE tone_indicators IS NOT NULL AND tone_indicators != '[]'"
1580
+ )
1581
+ for row in cursor.fetchall():
1582
+ try:
1583
+ tones = json.loads(row["tone_indicators"]) if row["tone_indicators"] else []
1584
+ for tone in tones:
1585
+ tone_lower = tone.lower()
1586
+ tone_distribution[tone_lower] = tone_distribution.get(tone_lower, 0) + 1
1587
+ except (json.JSONDecodeError, TypeError):
1588
+ pass
1589
+
1590
+ # Determine primary tone (most common)
1591
+ primary_tone = "direct"
1592
+ if tone_distribution:
1593
+ primary_tone = max(tone_distribution, key=tone_distribution.get)
1594
+
1595
+ # Generate style markers based on the data
1596
+ style_markers = []
1597
+
1598
+ if avg_word_count < 15:
1599
+ style_markers.append("Concise")
1600
+ elif avg_word_count > 40:
1601
+ style_markers.append("Detailed")
1602
+ else:
1603
+ style_markers.append("Balanced length")
1604
+
1605
+ if question_percentage > 40:
1606
+ style_markers.append("Question-driven")
1607
+ elif question_percentage < 10:
1608
+ style_markers.append("Statement-focused")
1609
+
1610
+ # Check for code usage
1611
+ code_stats = conn.execute(
1612
+ "SELECT SUM(CASE WHEN has_code_blocks = 1 THEN 1 ELSE 0 END) * 100.0 / COUNT(*) as code_pct FROM user_messages"
1613
+ ).fetchone()
1614
+ if code_stats and code_stats["code_pct"] and code_stats["code_pct"] > 20:
1615
+ style_markers.append("Code-heavy")
1616
+
1617
+ # Add primary tone to markers
1618
+ tone_labels = {
1619
+ "direct": "Direct",
1620
+ "polite": "Polite",
1621
+ "technical": "Technical",
1622
+ "casual": "Casual",
1623
+ "inquisitive": "Inquisitive",
1624
+ "urgent": "Urgent",
1625
+ }
1626
+ if primary_tone in tone_labels:
1627
+ style_markers.append(tone_labels[primary_tone])
1628
+
1629
+ if not style_markers:
1630
+ style_markers.append("Building profile...")
1631
+
1632
+ # Get sample messages to show the AI how the user actually writes
1633
+ sample_messages = []
1634
+ cursor = conn.execute(
1635
+ """SELECT content FROM user_messages
1636
+ WHERE length(content) > 20 AND length(content) < 500
1637
+ AND has_commands = 0
1638
+ ORDER BY timestamp DESC LIMIT 5"""
1639
+ )
1640
+ for row in cursor.fetchall():
1641
+ sample_messages.append(row["content"])
1642
+
1643
+ conn.close()
1644
+
1645
+ return {
1646
+ "totalMessages": total_messages,
1647
+ "avgWordCount": round(avg_word_count, 1),
1648
+ "primaryTone": primary_tone,
1649
+ "questionPercentage": round(question_percentage, 1),
1650
+ "toneDistribution": tone_distribution,
1651
+ "styleMarkers": style_markers,
1652
+ "sampleMessages": sample_messages,
1653
+ }
@@ -54,6 +54,8 @@ from database import (
54
54
  get_skill_usage,
55
55
  get_style_profile,
56
56
  get_style_samples,
57
+ get_style_samples_by_category,
58
+ compute_style_profile_from_messages,
57
59
  get_timeline,
58
60
  get_tool_usage,
59
61
  get_type_distribution,
@@ -73,6 +75,8 @@ from models import (
73
75
  BulkDeleteRequest,
74
76
  ChatRequest,
75
77
  ChatResponse,
78
+ ComposeRequest,
79
+ ComposeResponse,
76
80
  ConversationSaveRequest,
77
81
  ConversationSaveResponse,
78
82
  FilterParams,
@@ -1023,7 +1027,11 @@ async def chat_with_memories(
1023
1027
  style_context = None
1024
1028
  if request.use_style:
1025
1029
  try:
1026
- style_context = get_style_profile(project)
1030
+ # First try computed profile from user_messages (richer data)
1031
+ style_context = compute_style_profile_from_messages(project)
1032
+ # Fall back to stored profile if no user_messages
1033
+ if not style_context:
1034
+ style_context = get_style_profile(project)
1027
1035
  except Exception:
1028
1036
  pass # Graceful fallback if no style data
1029
1037
 
@@ -1061,7 +1069,11 @@ async def stream_chat(
1061
1069
  style_context = None
1062
1070
  if use_style:
1063
1071
  try:
1064
- style_context = get_style_profile(project)
1072
+ # First try computed profile from user_messages (richer data)
1073
+ style_context = compute_style_profile_from_messages(project)
1074
+ # Fall back to stored profile if no user_messages
1075
+ if not style_context:
1076
+ style_context = get_style_profile(project)
1065
1077
  except Exception:
1066
1078
  pass # Graceful fallback if no style data
1067
1079
 
@@ -1110,6 +1122,60 @@ async def save_chat_conversation(
1110
1122
  raise
1111
1123
 
1112
1124
 
1125
+ @app.post("/api/compose-response", response_model=ComposeResponse)
1126
+ @rate_limit("10/minute")
1127
+ async def compose_response_endpoint(
1128
+ request: ComposeRequest,
1129
+ project: str = Query(..., description="Path to the database file"),
1130
+ ):
1131
+ """Compose a response to an incoming message in the user's style."""
1132
+ try:
1133
+ if not Path(project).exists():
1134
+ log_error("/api/compose-response", FileNotFoundError("Database not found"))
1135
+ raise HTTPException(status_code=404, detail="Database not found")
1136
+
1137
+ # Get style profile
1138
+ style_profile = compute_style_profile_from_messages(project)
1139
+
1140
+ # Compose the response
1141
+ result = await chat_service.compose_response(
1142
+ db_path=project,
1143
+ incoming_message=request.incoming_message,
1144
+ context_type=request.context_type,
1145
+ template=request.template,
1146
+ tone_level=request.tone_level,
1147
+ include_memories=request.include_memories,
1148
+ style_profile=style_profile,
1149
+ )
1150
+
1151
+ if result.get("error"):
1152
+ log_error("/api/compose-response", Exception(result["error"]))
1153
+ raise HTTPException(status_code=500, detail=result["error"])
1154
+
1155
+ # Build response model
1156
+ import uuid
1157
+ from datetime import datetime
1158
+ response = ComposeResponse(
1159
+ id=str(uuid.uuid4()),
1160
+ response=result["response"],
1161
+ sources=result["sources"],
1162
+ style_applied=bool(style_profile and style_profile.get("total_messages", 0) > 0),
1163
+ tone_level=request.tone_level,
1164
+ template_used=request.template,
1165
+ incoming_message=request.incoming_message,
1166
+ context_type=request.context_type,
1167
+ created_at=datetime.now().isoformat(),
1168
+ )
1169
+
1170
+ log_success("/api/compose-response", context=request.context_type, tone=request.tone_level)
1171
+ return response
1172
+ except HTTPException:
1173
+ raise
1174
+ except Exception as e:
1175
+ log_error("/api/compose-response", e)
1176
+ raise HTTPException(status_code=500, detail=str(e))
1177
+
1178
+
1113
1179
  # --- Image Generation Endpoints ---
1114
1180
 
1115
1181
 
@@ -1262,6 +1328,7 @@ async def list_user_messages(
1262
1328
  )
1263
1329
 
1264
1330
  total_count = get_user_message_count(project, session_id=session_id)
1331
+ has_more = (offset + len(messages)) < total_count
1265
1332
 
1266
1333
  log_success("/api/user-messages", count=len(messages), total=total_count)
1267
1334
  return UserMessagesResponse(
@@ -1269,6 +1336,7 @@ async def list_user_messages(
1269
1336
  total_count=total_count,
1270
1337
  limit=limit,
1271
1338
  offset=offset,
1339
+ has_more=has_more,
1272
1340
  )
1273
1341
  except HTTPException:
1274
1342
  raise
@@ -1321,79 +1389,100 @@ async def delete_user_messages_bulk_endpoint(
1321
1389
  raise HTTPException(status_code=500, detail=str(e))
1322
1390
 
1323
1391
 
1324
- @app.get("/api/style-profile")
1392
+ @app.get("/api/style/profile")
1325
1393
  async def get_style_profile_endpoint(
1326
1394
  project: str = Query(..., description="Path to the database file"),
1327
1395
  project_path: Optional[str] = Query(None, description="Project-specific profile path, or None for global"),
1328
1396
  ):
1329
1397
  """Get user style profile for style analysis.
1330
1398
 
1331
- Returns aggregated style metrics including:
1332
- - Average word/char counts
1333
- - Common phrases
1334
- - Formality score
1335
- - Question/command frequencies
1336
- - Greeting and instruction patterns
1399
+ Returns style metrics computed from user messages:
1400
+ - total_messages: Total message count
1401
+ - avg_word_count: Average words per message
1402
+ - primary_tone: Most common tone (direct, polite, technical, etc.)
1403
+ - question_percentage: Percentage of messages containing questions
1404
+ - tone_distribution: Count of messages by tone
1405
+ - style_markers: Descriptive labels for writing style
1337
1406
  """
1338
1407
  try:
1339
1408
  if not Path(project).exists():
1340
1409
  raise HTTPException(status_code=404, detail="Database not found")
1341
1410
 
1411
+ # First try to get pre-computed profile from user_style_profiles table
1342
1412
  profile = get_style_profile(project, project_path=project_path)
1343
1413
 
1414
+ # If no stored profile, compute from user_messages
1415
+ if not profile:
1416
+ profile = compute_style_profile_from_messages(project)
1417
+
1418
+ # If still no profile (no user_messages), return empty structure
1344
1419
  if not profile:
1345
- # Return empty profile structure if none exists
1346
1420
  return {
1347
- "id": None,
1348
- "project_path": project_path,
1349
- "total_messages": 0,
1350
- "avg_word_count": None,
1351
- "avg_char_count": None,
1352
- "common_phrases": [],
1353
- "vocabulary_richness": None,
1354
- "formality_score": None,
1355
- "question_frequency": None,
1356
- "command_frequency": None,
1357
- "code_block_frequency": None,
1358
- "punctuation_style": None,
1359
- "greeting_patterns": [],
1360
- "instruction_style": None,
1361
- "sample_messages": [],
1362
- "created_at": None,
1363
- "updated_at": None,
1421
+ "totalMessages": 0,
1422
+ "avgWordCount": 0,
1423
+ "primaryTone": "direct",
1424
+ "questionPercentage": 0,
1425
+ "toneDistribution": {},
1426
+ "styleMarkers": ["No data available yet"],
1427
+ }
1428
+
1429
+ # Convert stored profile format to frontend expected format if needed
1430
+ if "totalMessages" in profile:
1431
+ # Already in camelCase format from compute_style_profile_from_messages
1432
+ pass
1433
+ elif "id" in profile:
1434
+ # Convert stored profile (from user_style_profiles table) to frontend format
1435
+ tone_dist = {}
1436
+ # Stored profile doesn't have tone_distribution, so compute it
1437
+ computed = compute_style_profile_from_messages(project)
1438
+ if computed:
1439
+ tone_dist = computed.get("toneDistribution", {})
1440
+ primary_tone = computed.get("primaryTone", "direct")
1441
+ style_markers = computed.get("styleMarkers", [])
1442
+ else:
1443
+ primary_tone = "direct"
1444
+ style_markers = []
1445
+
1446
+ profile = {
1447
+ "totalMessages": profile.get("total_messages", 0),
1448
+ "avgWordCount": profile.get("avg_word_count", 0) or 0,
1449
+ "primaryTone": primary_tone,
1450
+ "questionPercentage": (profile.get("question_frequency", 0) or 0) * 100,
1451
+ "toneDistribution": tone_dist,
1452
+ "styleMarkers": style_markers or profile.get("greeting_patterns", []) or [],
1364
1453
  }
1365
1454
 
1366
- log_success("/api/style-profile", has_profile=True)
1455
+ log_success("/api/style/profile", has_profile=True, total_messages=profile.get("totalMessages", 0))
1367
1456
  return profile
1368
1457
  except HTTPException:
1369
1458
  raise
1370
1459
  except Exception as e:
1371
- log_error("/api/style-profile", e)
1460
+ log_error("/api/style/profile", e)
1372
1461
  raise HTTPException(status_code=500, detail=str(e))
1373
1462
 
1374
1463
 
1375
- @app.get("/api/style-samples", response_model=list[StyleSample])
1464
+ @app.get("/api/style/samples")
1376
1465
  async def get_style_samples_endpoint(
1377
1466
  project: str = Query(..., description="Path to the database file"),
1378
- limit: int = Query(10, ge=1, le=50),
1467
+ samples_per_tone: int = Query(3, ge=1, le=10, description="Max samples per tone category"),
1379
1468
  ):
1380
1469
  """Get sample user messages for style analysis preview.
1381
1470
 
1382
- Returns a diverse selection of messages showcasing different writing styles,
1383
- including recent messages, messages with code blocks, and longer messages.
1471
+ Returns messages grouped by style category (professional, casual, technical, creative).
1384
1472
  """
1385
1473
  try:
1386
1474
  if not Path(project).exists():
1387
1475
  raise HTTPException(status_code=404, detail="Database not found")
1388
1476
 
1389
- samples = get_style_samples(project, limit=limit)
1477
+ samples = get_style_samples_by_category(project, samples_per_tone=samples_per_tone)
1390
1478
 
1391
- log_success("/api/style-samples", count=len(samples))
1392
- return [StyleSample(**s) for s in samples]
1479
+ total_count = sum(len(v) for v in samples.values())
1480
+ log_success("/api/style/samples", count=total_count)
1481
+ return samples
1393
1482
  except HTTPException:
1394
1483
  raise
1395
1484
  except Exception as e:
1396
- log_error("/api/style-samples", e)
1485
+ log_error("/api/style/samples", e)
1397
1486
  raise HTTPException(status_code=500, detail=str(e))
1398
1487
 
1399
1488
 
@@ -294,7 +294,8 @@ class UserMessage(BaseModel):
294
294
 
295
295
  id: str
296
296
  session_id: Optional[str] = None
297
- timestamp: str
297
+ timestamp: Optional[str] = None # Backward compatibility
298
+ created_at: Optional[str] = None # Frontend expects created_at
298
299
  content: str
299
300
  word_count: Optional[int] = None
300
301
  char_count: Optional[int] = None
@@ -302,6 +303,7 @@ class UserMessage(BaseModel):
302
303
  has_code_blocks: bool = False
303
304
  has_questions: bool = False
304
305
  has_commands: bool = False
306
+ tone: Optional[str] = None # Primary tone for frontend
305
307
  tone_indicators: list[str] = []
306
308
  project_path: Optional[str] = None
307
309
 
@@ -328,6 +330,7 @@ class UserMessagesResponse(BaseModel):
328
330
  total_count: int
329
331
  limit: int
330
332
  offset: int
333
+ has_more: bool = False # Whether more results are available
331
334
 
332
335
 
333
336
  class StyleSample(BaseModel):
@@ -368,3 +371,30 @@ class BulkDeleteRequest(BaseModel):
368
371
  """Request body for bulk delete operations."""
369
372
 
370
373
  message_ids: list[str] = Field(..., min_length=1, max_length=100)
374
+
375
+
376
+ # --- Response Composer Models ---
377
+
378
+
379
+ class ComposeRequest(BaseModel):
380
+ """Request for composing a response in user's style."""
381
+
382
+ incoming_message: str = Field(..., min_length=1, max_length=5000)
383
+ context_type: str = Field(default="general") # skool_post, dm, email, comment, general
384
+ template: Optional[str] = None # answer, guide, redirect, acknowledge
385
+ tone_level: int = Field(default=50, ge=0, le=100) # 0=casual, 100=professional
386
+ include_memories: bool = Field(default=True)
387
+
388
+
389
+ class ComposeResponse(BaseModel):
390
+ """Response from compose endpoint."""
391
+
392
+ id: str
393
+ response: str
394
+ sources: list[ChatSource]
395
+ style_applied: bool
396
+ tone_level: int
397
+ template_used: Optional[str]
398
+ incoming_message: str
399
+ context_type: str
400
+ created_at: str
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: omni-cortex
3
- Version: 1.12.1
3
+ Version: 1.13.0
4
4
  Summary: Give Claude Code a perfect memory - auto-logs everything, searches smartly, and gets smarter over time
5
5
  Project-URL: Homepage, https://github.com/AllCytes/Omni-Cortex
6
6
  Project-URL: Repository, https://github.com/AllCytes/Omni-Cortex
@@ -0,0 +1,26 @@
1
+ omni_cortex-1.13.0.data/data/share/omni-cortex/hooks/post_tool_use.py,sha256=zdaKChi8zOghRlHswisCBSQE3kW1MtmM6AFfI_ivvpI,16581
2
+ omni_cortex-1.13.0.data/data/share/omni-cortex/hooks/pre_tool_use.py,sha256=3_V6Qw5m40eGrMmm5i94vINzeVxmcJvivdPa69H3AOI,8585
3
+ omni_cortex-1.13.0.data/data/share/omni-cortex/hooks/session_utils.py,sha256=3SKPCytqWuRPOupWdzmwBoKBDJqtLcT1Nle_pueDQUY,5746
4
+ omni_cortex-1.13.0.data/data/share/omni-cortex/hooks/stop.py,sha256=UroliJsyIS9_lj29-1d_r-80V4AfTMUFCaOjJZv3lwM,6976
5
+ omni_cortex-1.13.0.data/data/share/omni-cortex/hooks/subagent_stop.py,sha256=V9HQSFGNOfkg8ZCstPEy4h5V8BP4AbrVr8teFzN1kNk,3314
6
+ omni_cortex-1.13.0.data/data/share/omni-cortex/hooks/user_prompt.py,sha256=WNHJvhnkb9rXQ_HDpr6eLpM5vwy1Y1xl1EUoqyNC-x8,6859
7
+ omni_cortex-1.13.0.data/data/share/omni-cortex/dashboard/backend/.env.example,sha256=9xS7-UiWlMddRwzlyyyKNHAMlNTsgH-2sPV266guJpQ,372
8
+ omni_cortex-1.13.0.data/data/share/omni-cortex/dashboard/backend/backfill_summaries.py,sha256=ElchfcBv4pmVr2PsePCgFlCyuvf4_jDJj_C3AmMhu7U,8973
9
+ omni_cortex-1.13.0.data/data/share/omni-cortex/dashboard/backend/chat_service.py,sha256=QGNxVX-9bJw4kot6mPieGD2QIbmzvPYSGDGOpv3p_-Y,18567
10
+ omni_cortex-1.13.0.data/data/share/omni-cortex/dashboard/backend/database.py,sha256=_sWqLjx_mWOxqNpfbv-bChtPfQkHzUNzly1pGu_zPKI,54199
11
+ omni_cortex-1.13.0.data/data/share/omni-cortex/dashboard/backend/image_service.py,sha256=NP6ojFpHb6iNTYRkXqYu1CL6WvooZpZ54mjLiWSWG_g,19205
12
+ omni_cortex-1.13.0.data/data/share/omni-cortex/dashboard/backend/logging_config.py,sha256=WnunFGET9zlsn9WBpVsio2zI7BiUQanE0xzAQQxIhII,3944
13
+ omni_cortex-1.13.0.data/data/share/omni-cortex/dashboard/backend/main.py,sha256=-GwRioHjuUaMiP1gNuyqzs6LUxvIgOUHyirCcfQ6pRs,59364
14
+ omni_cortex-1.13.0.data/data/share/omni-cortex/dashboard/backend/models.py,sha256=_gQoBaavttuRgLIvhCQsZ0zmuON6aKWbAFhdB1YFVbM,11164
15
+ omni_cortex-1.13.0.data/data/share/omni-cortex/dashboard/backend/project_config.py,sha256=ZxGoeRpHvN5qQyf2hRxrAZiHrPSwdQp59f0di6O1LKM,4352
16
+ omni_cortex-1.13.0.data/data/share/omni-cortex/dashboard/backend/project_scanner.py,sha256=lwFXS8iJbOoxf7FAyo2TjH25neaMHiJ8B3jS57XxtDI,5713
17
+ omni_cortex-1.13.0.data/data/share/omni-cortex/dashboard/backend/prompt_security.py,sha256=LcdZhYy1CfpSq_4BPO6lMJ15phc2ZXLUSBAnAvODVCI,3423
18
+ omni_cortex-1.13.0.data/data/share/omni-cortex/dashboard/backend/pyproject.toml,sha256=9pbbGQXLe1Xd06nZAtDySCHIlfMWvPaB-C6tGZR6umc,502
19
+ omni_cortex-1.13.0.data/data/share/omni-cortex/dashboard/backend/security.py,sha256=nQsoPE0n5dtY9ive00d33W1gL48GgK7C5Ae0BK2oW2k,3479
20
+ omni_cortex-1.13.0.data/data/share/omni-cortex/dashboard/backend/uv.lock,sha256=miB9zGGSirBkjDE-OZTPCnv43Yc98xuAz_Ne8vTNFHg,186004
21
+ omni_cortex-1.13.0.data/data/share/omni-cortex/dashboard/backend/websocket_manager.py,sha256=gNQLd94AcC-InumGQmUolREhiogCzilYWpLN8SRZjHI,3645
22
+ omni_cortex-1.13.0.dist-info/METADATA,sha256=N9ZCvUc2F0jnkuXvtXl1ISgDipNbwvtsMHritpaSDVo,15712
23
+ omni_cortex-1.13.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
24
+ omni_cortex-1.13.0.dist-info/entry_points.txt,sha256=rohx4mFH2ffZmMb9QXPZmFf-ZGjA3jpKVDVeET-ttiM,150
25
+ omni_cortex-1.13.0.dist-info/licenses/LICENSE,sha256=oG_397owMmi-Umxp5sYocJ6RPohp9_bDNnnEu9OUphg,1072
26
+ omni_cortex-1.13.0.dist-info/RECORD,,
@@ -1,26 +0,0 @@
1
- omni_cortex-1.12.1.data/data/share/omni-cortex/hooks/post_tool_use.py,sha256=zdaKChi8zOghRlHswisCBSQE3kW1MtmM6AFfI_ivvpI,16581
2
- omni_cortex-1.12.1.data/data/share/omni-cortex/hooks/pre_tool_use.py,sha256=3_V6Qw5m40eGrMmm5i94vINzeVxmcJvivdPa69H3AOI,8585
3
- omni_cortex-1.12.1.data/data/share/omni-cortex/hooks/session_utils.py,sha256=3SKPCytqWuRPOupWdzmwBoKBDJqtLcT1Nle_pueDQUY,5746
4
- omni_cortex-1.12.1.data/data/share/omni-cortex/hooks/stop.py,sha256=UroliJsyIS9_lj29-1d_r-80V4AfTMUFCaOjJZv3lwM,6976
5
- omni_cortex-1.12.1.data/data/share/omni-cortex/hooks/subagent_stop.py,sha256=V9HQSFGNOfkg8ZCstPEy4h5V8BP4AbrVr8teFzN1kNk,3314
6
- omni_cortex-1.12.1.data/data/share/omni-cortex/hooks/user_prompt.py,sha256=WNHJvhnkb9rXQ_HDpr6eLpM5vwy1Y1xl1EUoqyNC-x8,6859
7
- omni_cortex-1.12.1.data/data/share/omni-cortex/dashboard/backend/.env.example,sha256=9xS7-UiWlMddRwzlyyyKNHAMlNTsgH-2sPV266guJpQ,372
8
- omni_cortex-1.12.1.data/data/share/omni-cortex/dashboard/backend/backfill_summaries.py,sha256=ElchfcBv4pmVr2PsePCgFlCyuvf4_jDJj_C3AmMhu7U,8973
9
- omni_cortex-1.12.1.data/data/share/omni-cortex/dashboard/backend/chat_service.py,sha256=5vUzNL3AIfkqVMwooXEqCSkWAkN1HP0vToN1sn3x3Z4,11285
10
- omni_cortex-1.12.1.data/data/share/omni-cortex/dashboard/backend/database.py,sha256=WwxgXVo5gztFjaKj-iANYgK4tOGGPARsHg28hkJtADQ,46494
11
- omni_cortex-1.12.1.data/data/share/omni-cortex/dashboard/backend/image_service.py,sha256=NP6ojFpHb6iNTYRkXqYu1CL6WvooZpZ54mjLiWSWG_g,19205
12
- omni_cortex-1.12.1.data/data/share/omni-cortex/dashboard/backend/logging_config.py,sha256=WnunFGET9zlsn9WBpVsio2zI7BiUQanE0xzAQQxIhII,3944
13
- omni_cortex-1.12.1.data/data/share/omni-cortex/dashboard/backend/main.py,sha256=ezIgMX3WtwQoJnrxi3M1I-gRSPh69Qmv6F0va7tSbxs,55122
14
- omni_cortex-1.12.1.data/data/share/omni-cortex/dashboard/backend/models.py,sha256=VymhQz6GCPo5d7wyn_Yg1njKugGbzx5--bnVP42MyBg,10111
15
- omni_cortex-1.12.1.data/data/share/omni-cortex/dashboard/backend/project_config.py,sha256=ZxGoeRpHvN5qQyf2hRxrAZiHrPSwdQp59f0di6O1LKM,4352
16
- omni_cortex-1.12.1.data/data/share/omni-cortex/dashboard/backend/project_scanner.py,sha256=lwFXS8iJbOoxf7FAyo2TjH25neaMHiJ8B3jS57XxtDI,5713
17
- omni_cortex-1.12.1.data/data/share/omni-cortex/dashboard/backend/prompt_security.py,sha256=LcdZhYy1CfpSq_4BPO6lMJ15phc2ZXLUSBAnAvODVCI,3423
18
- omni_cortex-1.12.1.data/data/share/omni-cortex/dashboard/backend/pyproject.toml,sha256=9pbbGQXLe1Xd06nZAtDySCHIlfMWvPaB-C6tGZR6umc,502
19
- omni_cortex-1.12.1.data/data/share/omni-cortex/dashboard/backend/security.py,sha256=nQsoPE0n5dtY9ive00d33W1gL48GgK7C5Ae0BK2oW2k,3479
20
- omni_cortex-1.12.1.data/data/share/omni-cortex/dashboard/backend/uv.lock,sha256=miB9zGGSirBkjDE-OZTPCnv43Yc98xuAz_Ne8vTNFHg,186004
21
- omni_cortex-1.12.1.data/data/share/omni-cortex/dashboard/backend/websocket_manager.py,sha256=gNQLd94AcC-InumGQmUolREhiogCzilYWpLN8SRZjHI,3645
22
- omni_cortex-1.12.1.dist-info/METADATA,sha256=AOOi2hbe_RTrqeyPZvN9go13VyndPomtxhQw7lPGX7k,15712
23
- omni_cortex-1.12.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
24
- omni_cortex-1.12.1.dist-info/entry_points.txt,sha256=rohx4mFH2ffZmMb9QXPZmFf-ZGjA3jpKVDVeET-ttiM,150
25
- omni_cortex-1.12.1.dist-info/licenses/LICENSE,sha256=oG_397owMmi-Umxp5sYocJ6RPohp9_bDNnnEu9OUphg,1072
26
- omni_cortex-1.12.1.dist-info/RECORD,,