kollabor 0.4.9__py3-none-any.whl → 0.4.15__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 (192) hide show
  1. agents/__init__.py +2 -0
  2. agents/coder/__init__.py +0 -0
  3. agents/coder/agent.json +4 -0
  4. agents/coder/api-integration.md +2150 -0
  5. agents/coder/cli-pretty.md +765 -0
  6. agents/coder/code-review.md +1092 -0
  7. agents/coder/database-design.md +1525 -0
  8. agents/coder/debugging.md +1102 -0
  9. agents/coder/dependency-management.md +1397 -0
  10. agents/coder/git-workflow.md +1099 -0
  11. agents/coder/refactoring.md +1454 -0
  12. agents/coder/security-hardening.md +1732 -0
  13. agents/coder/system_prompt.md +1448 -0
  14. agents/coder/tdd.md +1367 -0
  15. agents/creative-writer/__init__.py +0 -0
  16. agents/creative-writer/agent.json +4 -0
  17. agents/creative-writer/character-development.md +1852 -0
  18. agents/creative-writer/dialogue-craft.md +1122 -0
  19. agents/creative-writer/plot-structure.md +1073 -0
  20. agents/creative-writer/revision-editing.md +1484 -0
  21. agents/creative-writer/system_prompt.md +690 -0
  22. agents/creative-writer/worldbuilding.md +2049 -0
  23. agents/data-analyst/__init__.py +30 -0
  24. agents/data-analyst/agent.json +4 -0
  25. agents/data-analyst/data-visualization.md +992 -0
  26. agents/data-analyst/exploratory-data-analysis.md +1110 -0
  27. agents/data-analyst/pandas-data-manipulation.md +1081 -0
  28. agents/data-analyst/sql-query-optimization.md +881 -0
  29. agents/data-analyst/statistical-analysis.md +1118 -0
  30. agents/data-analyst/system_prompt.md +928 -0
  31. agents/default/__init__.py +0 -0
  32. agents/default/agent.json +4 -0
  33. agents/default/dead-code.md +794 -0
  34. agents/default/explore-agent-system.md +585 -0
  35. agents/default/system_prompt.md +1448 -0
  36. agents/kollabor/__init__.py +0 -0
  37. agents/kollabor/analyze-plugin-lifecycle.md +175 -0
  38. agents/kollabor/analyze-terminal-rendering.md +388 -0
  39. agents/kollabor/code-review.md +1092 -0
  40. agents/kollabor/debug-mcp-integration.md +521 -0
  41. agents/kollabor/debug-plugin-hooks.md +547 -0
  42. agents/kollabor/debugging.md +1102 -0
  43. agents/kollabor/dependency-management.md +1397 -0
  44. agents/kollabor/git-workflow.md +1099 -0
  45. agents/kollabor/inspect-llm-conversation.md +148 -0
  46. agents/kollabor/monitor-event-bus.md +558 -0
  47. agents/kollabor/profile-performance.md +576 -0
  48. agents/kollabor/refactoring.md +1454 -0
  49. agents/kollabor/system_prompt copy.md +1448 -0
  50. agents/kollabor/system_prompt.md +757 -0
  51. agents/kollabor/trace-command-execution.md +178 -0
  52. agents/kollabor/validate-config.md +879 -0
  53. agents/research/__init__.py +0 -0
  54. agents/research/agent.json +4 -0
  55. agents/research/architecture-mapping.md +1099 -0
  56. agents/research/codebase-analysis.md +1077 -0
  57. agents/research/dependency-audit.md +1027 -0
  58. agents/research/performance-profiling.md +1047 -0
  59. agents/research/security-review.md +1359 -0
  60. agents/research/system_prompt.md +492 -0
  61. agents/technical-writer/__init__.py +0 -0
  62. agents/technical-writer/agent.json +4 -0
  63. agents/technical-writer/api-documentation.md +2328 -0
  64. agents/technical-writer/changelog-management.md +1181 -0
  65. agents/technical-writer/readme-writing.md +1360 -0
  66. agents/technical-writer/style-guide.md +1410 -0
  67. agents/technical-writer/system_prompt.md +653 -0
  68. agents/technical-writer/tutorial-creation.md +1448 -0
  69. core/__init__.py +0 -2
  70. core/application.py +343 -88
  71. core/cli.py +229 -10
  72. core/commands/menu_renderer.py +463 -59
  73. core/commands/registry.py +14 -9
  74. core/commands/system_commands.py +2461 -14
  75. core/config/loader.py +151 -37
  76. core/config/service.py +18 -6
  77. core/events/bus.py +29 -9
  78. core/events/executor.py +205 -75
  79. core/events/models.py +27 -8
  80. core/fullscreen/command_integration.py +20 -24
  81. core/fullscreen/components/__init__.py +10 -1
  82. core/fullscreen/components/matrix_components.py +1 -2
  83. core/fullscreen/components/space_shooter_components.py +654 -0
  84. core/fullscreen/plugin.py +5 -0
  85. core/fullscreen/renderer.py +52 -13
  86. core/fullscreen/session.py +52 -15
  87. core/io/__init__.py +29 -5
  88. core/io/buffer_manager.py +6 -1
  89. core/io/config_status_view.py +7 -29
  90. core/io/core_status_views.py +267 -347
  91. core/io/input/__init__.py +25 -0
  92. core/io/input/command_mode_handler.py +711 -0
  93. core/io/input/display_controller.py +128 -0
  94. core/io/input/hook_registrar.py +286 -0
  95. core/io/input/input_loop_manager.py +421 -0
  96. core/io/input/key_press_handler.py +502 -0
  97. core/io/input/modal_controller.py +1011 -0
  98. core/io/input/paste_processor.py +339 -0
  99. core/io/input/status_modal_renderer.py +184 -0
  100. core/io/input_errors.py +5 -1
  101. core/io/input_handler.py +211 -2452
  102. core/io/key_parser.py +7 -0
  103. core/io/layout.py +15 -3
  104. core/io/message_coordinator.py +111 -2
  105. core/io/message_renderer.py +129 -4
  106. core/io/status_renderer.py +147 -607
  107. core/io/terminal_renderer.py +97 -51
  108. core/io/terminal_state.py +21 -4
  109. core/io/visual_effects.py +816 -165
  110. core/llm/agent_manager.py +1063 -0
  111. core/llm/api_adapters/__init__.py +44 -0
  112. core/llm/api_adapters/anthropic_adapter.py +432 -0
  113. core/llm/api_adapters/base.py +241 -0
  114. core/llm/api_adapters/openai_adapter.py +326 -0
  115. core/llm/api_communication_service.py +167 -113
  116. core/llm/conversation_logger.py +322 -16
  117. core/llm/conversation_manager.py +556 -30
  118. core/llm/file_operations_executor.py +84 -32
  119. core/llm/llm_service.py +934 -103
  120. core/llm/mcp_integration.py +541 -57
  121. core/llm/message_display_service.py +135 -18
  122. core/llm/plugin_sdk.py +1 -2
  123. core/llm/profile_manager.py +1183 -0
  124. core/llm/response_parser.py +274 -56
  125. core/llm/response_processor.py +16 -3
  126. core/llm/tool_executor.py +6 -1
  127. core/logging/__init__.py +2 -0
  128. core/logging/setup.py +34 -6
  129. core/models/resume.py +54 -0
  130. core/plugins/__init__.py +4 -2
  131. core/plugins/base.py +127 -0
  132. core/plugins/collector.py +23 -161
  133. core/plugins/discovery.py +37 -3
  134. core/plugins/factory.py +6 -12
  135. core/plugins/registry.py +5 -17
  136. core/ui/config_widgets.py +128 -28
  137. core/ui/live_modal_renderer.py +2 -1
  138. core/ui/modal_actions.py +5 -0
  139. core/ui/modal_overlay_renderer.py +0 -60
  140. core/ui/modal_renderer.py +268 -7
  141. core/ui/modal_state_manager.py +29 -4
  142. core/ui/widgets/base_widget.py +7 -0
  143. core/updates/__init__.py +10 -0
  144. core/updates/version_check_service.py +348 -0
  145. core/updates/version_comparator.py +103 -0
  146. core/utils/config_utils.py +685 -526
  147. core/utils/plugin_utils.py +1 -1
  148. core/utils/session_naming.py +111 -0
  149. fonts/LICENSE +21 -0
  150. fonts/README.md +46 -0
  151. fonts/SymbolsNerdFont-Regular.ttf +0 -0
  152. fonts/SymbolsNerdFontMono-Regular.ttf +0 -0
  153. fonts/__init__.py +44 -0
  154. {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/METADATA +54 -4
  155. kollabor-0.4.15.dist-info/RECORD +228 -0
  156. {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/top_level.txt +2 -0
  157. plugins/agent_orchestrator/__init__.py +39 -0
  158. plugins/agent_orchestrator/activity_monitor.py +181 -0
  159. plugins/agent_orchestrator/file_attacher.py +77 -0
  160. plugins/agent_orchestrator/message_injector.py +135 -0
  161. plugins/agent_orchestrator/models.py +48 -0
  162. plugins/agent_orchestrator/orchestrator.py +403 -0
  163. plugins/agent_orchestrator/plugin.py +976 -0
  164. plugins/agent_orchestrator/xml_parser.py +191 -0
  165. plugins/agent_orchestrator_plugin.py +9 -0
  166. plugins/enhanced_input/box_styles.py +1 -0
  167. plugins/enhanced_input/color_engine.py +19 -4
  168. plugins/enhanced_input/config.py +2 -2
  169. plugins/enhanced_input_plugin.py +61 -11
  170. plugins/fullscreen/__init__.py +6 -2
  171. plugins/fullscreen/example_plugin.py +1035 -222
  172. plugins/fullscreen/setup_wizard_plugin.py +592 -0
  173. plugins/fullscreen/space_shooter_plugin.py +131 -0
  174. plugins/hook_monitoring_plugin.py +436 -78
  175. plugins/query_enhancer_plugin.py +66 -30
  176. plugins/resume_conversation_plugin.py +1494 -0
  177. plugins/save_conversation_plugin.py +98 -32
  178. plugins/system_commands_plugin.py +70 -56
  179. plugins/tmux_plugin.py +154 -78
  180. plugins/workflow_enforcement_plugin.py +94 -92
  181. system_prompt/default.md +952 -886
  182. core/io/input_mode_manager.py +0 -402
  183. core/io/modal_interaction_handler.py +0 -315
  184. core/io/raw_input_processor.py +0 -946
  185. core/storage/__init__.py +0 -5
  186. core/storage/state_manager.py +0 -84
  187. core/ui/widget_integration.py +0 -222
  188. core/utils/key_reader.py +0 -171
  189. kollabor-0.4.9.dist-info/RECORD +0 -128
  190. {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/WHEEL +0 -0
  191. {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/entry_points.txt +0 -0
  192. {kollabor-0.4.9.dist-info → kollabor-0.4.15.dist-info}/licenses/LICENSE +0 -0
@@ -14,6 +14,8 @@ from pathlib import Path
14
14
  from typing import Any, Dict, List, Optional
15
15
  from uuid import uuid4
16
16
 
17
+ from ..utils.session_naming import generate_session_name
18
+
17
19
  logger = logging.getLogger(__name__)
18
20
 
19
21
 
@@ -33,9 +35,8 @@ class KollaborConversationLogger:
33
35
  self.conversations_dir = conversations_dir
34
36
  self.conversations_dir.mkdir(parents=True, exist_ok=True)
35
37
 
36
- # Session management
37
- timestamp = datetime.now().strftime('%Y-%m-%d_%H%M%S')
38
- self.session_id = f"session_{timestamp}"
38
+ # Session management - use memorable session names
39
+ self.session_id = generate_session_name()
39
40
  self.session_file = self.conversations_dir / f"{self.session_id}.jsonl"
40
41
 
41
42
  # Conversation state
@@ -47,19 +48,24 @@ class KollaborConversationLogger:
47
48
  self.user_patterns = []
48
49
  self.project_context = {}
49
50
  self.conversation_themes = []
51
+
52
+ # Dynamic context (set by llm_service before log_conversation_start)
53
+ self.app_version = "unknown"
54
+ self.active_plugins = []
50
55
  self.file_interactions = {}
51
56
 
52
- # Memory management
53
- self.memory_dir = self.conversations_dir.parent / "conversation_memory"
57
+ # Memory management (inside conversations/)
58
+ self.memory_dir = self.conversations_dir / "memory"
54
59
  self.memory_dir.mkdir(parents=True, exist_ok=True)
55
60
  self._load_conversation_memory()
56
61
 
57
62
  logger.info(f"Conversation logger initialized: {self.session_id}")
58
63
 
59
- async def initialize(self):
64
+ async def initialize(self) -> bool:
60
65
  """Initialize async resources for conversation logger."""
61
66
  # Any async initialization can happen here
62
67
  logger.debug("Conversation logger async initialization complete")
68
+ return True
63
69
 
64
70
  async def shutdown(self):
65
71
  """Shutdown conversation logger and save state."""
@@ -299,6 +305,38 @@ class KollaborConversationLogger:
299
305
  logger.warning(f"Failed to find related sessions: {e}")
300
306
  return related
301
307
 
308
+ def log_message(self, role: str, content: str, parent_uuid: Optional[str] = None,
309
+ metadata: Optional[Dict] = None) -> None:
310
+ """Generic message logging - routes to appropriate role-specific method.
311
+
312
+ This is a synchronous wrapper that schedules the async logging.
313
+
314
+ Args:
315
+ role: Message role (user, assistant, system)
316
+ content: Message content
317
+ parent_uuid: Optional parent message UUID
318
+ metadata: Optional additional metadata
319
+ """
320
+ import asyncio
321
+
322
+ async def _log():
323
+ if role == "user":
324
+ await self.log_user_message(content, parent_uuid, metadata)
325
+ elif role == "assistant":
326
+ await self.log_assistant_message(content, parent_uuid or "", metadata)
327
+ elif role == "system":
328
+ await self.log_system_message(content, parent_uuid or "", metadata)
329
+ else:
330
+ logger.warning(f"Unknown role for logging: {role}")
331
+
332
+ # Try to get running loop and schedule, otherwise log synchronously
333
+ try:
334
+ loop = asyncio.get_running_loop()
335
+ loop.create_task(_log())
336
+ except RuntimeError:
337
+ # No running loop - skip async logging
338
+ logger.debug(f"Skipping async log for {role} message (no event loop)")
339
+
302
340
  async def log_conversation_start(self):
303
341
  """Log conversation root structure with metadata."""
304
342
  root_message = {
@@ -310,15 +348,9 @@ class KollaborConversationLogger:
310
348
  "timestamp": datetime.now().isoformat() + "Z",
311
349
  "cwd": self._get_working_directory(),
312
350
  "gitBranch": self._get_git_branch(),
313
- "version": "1.0.0",
351
+ "version": self.app_version,
314
352
  "conversation_context": {
315
- "project_type": "python_terminal_app",
316
- "active_plugins": ["llm_service", "hook_system", "conversation_logger"],
317
- "user_profile": {
318
- "expertise_level": "advanced",
319
- "preferred_communication": "direct",
320
- "coding_style": "pythonic"
321
- },
353
+ "active_plugins": self.active_plugins,
322
354
  "session_goals": [],
323
355
  "conversation_summary": ""
324
356
  },
@@ -330,7 +362,7 @@ class KollaborConversationLogger:
330
362
  }
331
363
  }
332
364
  }
333
-
365
+
334
366
  await self._append_to_jsonl(root_message)
335
367
  logger.info(f"Logged conversation start: {self.session_id}")
336
368
 
@@ -470,4 +502,278 @@ class KollaborConversationLogger:
470
502
  # Save conversation memory
471
503
  self._save_conversation_memory()
472
504
 
473
- logger.info(f"Logged conversation end: {self.session_id}")
505
+ logger.info(f"Logged conversation end: {self.session_id}")
506
+
507
+ def list_sessions(self, filters: Dict = None) -> List[Dict]:
508
+ """List available sessions with metadata.
509
+
510
+ Args:
511
+ filters: Optional filters for sessions
512
+
513
+ Returns:
514
+ List of session metadata
515
+ """
516
+ sessions = []
517
+
518
+ try:
519
+ # Find both old format (session_*) and new format (YYMMDDHHMM-*) files
520
+ all_files = (
521
+ list(self.conversations_dir.glob("session_*.jsonl")) +
522
+ list(self.conversations_dir.glob("[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]-*.jsonl"))
523
+ )
524
+
525
+ for session_file in all_files:
526
+ try:
527
+ session_info = self._parse_session_file(session_file, filters)
528
+ if session_info:
529
+ sessions.append(session_info)
530
+ except Exception as e:
531
+ logger.warning(f"Failed to parse session file {session_file}: {e}")
532
+ continue
533
+
534
+ # Sort by start time (newest first)
535
+ sessions.sort(key=lambda x: x.get("start_time", ""), reverse=True)
536
+
537
+ except Exception as e:
538
+ logger.error(f"Failed to scan sessions directory: {e}")
539
+
540
+ return sessions
541
+
542
+ def _parse_session_file(self, session_file: Path, filters: Dict = None) -> Optional[Dict]:
543
+ """Parse a session file and extract metadata.
544
+
545
+ Args:
546
+ session_file: Path to session JSONL file
547
+ filters: Optional filters
548
+
549
+ Returns:
550
+ Session metadata or None if should be filtered out
551
+ """
552
+ try:
553
+ session_info = {
554
+ "session_id": session_file.stem,
555
+ "file_path": str(session_file),
556
+ "start_time": None,
557
+ "end_time": None,
558
+ "message_count": 0,
559
+ "working_directory": "unknown",
560
+ "git_branch": "unknown",
561
+ "topics": [],
562
+ "duration": None,
563
+ "size_bytes": session_file.stat().st_size,
564
+ "preview_messages": []
565
+ }
566
+
567
+ with open(session_file, 'r') as f:
568
+ lines = f.readlines()
569
+
570
+ user_messages = []
571
+ assistant_messages = []
572
+
573
+ for line in lines:
574
+ try:
575
+ data = json.loads(line.strip())
576
+
577
+ # Extract metadata from conversation start
578
+ if data.get("type") == "conversation_metadata":
579
+ session_info["start_time"] = data.get("startTime")
580
+ session_info["working_directory"] = data.get("cwd", "unknown")
581
+ session_info["git_branch"] = data.get("gitBranch", "unknown")
582
+
583
+ # Extract end time
584
+ elif data.get("type") == "conversation_end":
585
+ session_info["end_time"] = data.get("endTime")
586
+ summary = data.get("summary", {})
587
+ session_info["topics"] = summary.get("themes", [])
588
+
589
+ # Count messages
590
+ elif data.get("type") == "user":
591
+ user_messages.append(data)
592
+ session_info["message_count"] += 1
593
+
594
+ # Store preview messages (first 3 user messages)
595
+ if len(user_messages) <= 3:
596
+ content = data.get("message", {}).get("content", "")
597
+ preview = content[:100] + "..." if len(content) > 100 else content
598
+ session_info["preview_messages"].append({
599
+ "role": "user",
600
+ "content": preview,
601
+ "timestamp": data.get("timestamp")
602
+ })
603
+
604
+ elif data.get("type") == "assistant":
605
+ assistant_messages.append(data)
606
+ session_info["message_count"] += 1
607
+
608
+ # Store preview assistant messages
609
+ if len(assistant_messages) <= 2:
610
+ content = data.get("message", {}).get("content", [])
611
+ if isinstance(content, list) and content:
612
+ text_content = content[0].get("text", "")
613
+ else:
614
+ text_content = str(content)
615
+
616
+ preview = text_content[:100] + "..." if len(text_content) > 100 else text_content
617
+ session_info["preview_messages"].append({
618
+ "role": "assistant",
619
+ "content": preview,
620
+ "timestamp": data.get("timestamp")
621
+ })
622
+
623
+ except json.JSONDecodeError:
624
+ continue
625
+
626
+ # Calculate duration
627
+ if session_info["start_time"] and session_info["end_time"]:
628
+ try:
629
+ start = datetime.fromisoformat(session_info["start_time"].replace('Z', '+00:00'))
630
+ end = datetime.fromisoformat(session_info["end_time"].replace('Z', '+00:00'))
631
+ duration = end - start
632
+ session_info["duration"] = str(int(duration.total_seconds() / 60)) + "m"
633
+ except:
634
+ session_info["duration"] = "unknown"
635
+
636
+ # Apply filters
637
+ if filters:
638
+ if "date_range" in filters:
639
+ # Filter by date range
640
+ pass # TODO: Implement date filtering
641
+
642
+ if "working_directory" in filters:
643
+ if session_info["working_directory"] != filters["working_directory"]:
644
+ return None
645
+
646
+ if "git_branch" in filters:
647
+ if session_info["git_branch"] != filters["git_branch"]:
648
+ return None
649
+
650
+ return session_info
651
+
652
+ except Exception as e:
653
+ logger.warning(f"Failed to parse session file {session_file}: {e}")
654
+ return None
655
+
656
+ def get_session_summary(self, session_id: str) -> Dict:
657
+ """Get session summary for preview.
658
+
659
+ Args:
660
+ session_id: Session identifier
661
+
662
+ Returns:
663
+ Session summary
664
+ """
665
+ try:
666
+ # Find session file
667
+ session_file = None
668
+ for file_path in self.conversations_dir.glob(f"{session_id}*.jsonl"):
669
+ session_file = file_path
670
+ break
671
+
672
+ if not session_file:
673
+ return {}
674
+
675
+ session_info = self._parse_session_file(session_file)
676
+ if not session_info:
677
+ return {}
678
+
679
+ # Extract additional summary information
680
+ summary = {
681
+ "metadata": session_info,
682
+ "key_topics": session_info.get("topics", []),
683
+ "user_patterns": [], # TODO: Extract from memory
684
+ "project_context": {
685
+ "working_directory": session_info.get("working_directory"),
686
+ "git_branch": session_info.get("git_branch"),
687
+ "files_mentioned": [] # TODO: Extract from messages
688
+ },
689
+ "compatibility_score": 1.0 # TODO: Calculate based on environment
690
+ }
691
+
692
+ return summary
693
+
694
+ except Exception as e:
695
+ logger.error(f"Failed to get session summary for {session_id}: {e}")
696
+ return {}
697
+
698
+ def search_sessions(self, query: str) -> List[Dict]:
699
+ """Search sessions by content.
700
+
701
+ Args:
702
+ query: Search query
703
+
704
+ Returns:
705
+ List of matching sessions
706
+ """
707
+ results = []
708
+ query_lower = query.lower()
709
+
710
+ try:
711
+ # Search both old format (session_*) and new format (YYMMDDHHMM-*) files
712
+ all_files = (
713
+ list(self.conversations_dir.glob("session_*.jsonl")) +
714
+ list(self.conversations_dir.glob("[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]-*.jsonl"))
715
+ )
716
+
717
+ for session_file in all_files:
718
+ try:
719
+ # Quick filename check first
720
+ if query_lower in session_file.name.lower():
721
+ session_info = self._parse_session_file(session_file)
722
+ if session_info:
723
+ results.append(session_info)
724
+ continue
725
+
726
+ # Content search
727
+ with open(session_file, 'r') as f:
728
+ content = f.read().lower()
729
+
730
+ if query_lower in content:
731
+ session_info = self._parse_session_file(session_file)
732
+ if session_info:
733
+ # Add search relevance info
734
+ session_info["search_relevance"] = self._calculate_search_relevance(
735
+ content, query_lower
736
+ )
737
+ results.append(session_info)
738
+
739
+ except Exception as e:
740
+ logger.warning(f"Failed to search session file {session_file}: {e}")
741
+ continue
742
+
743
+ # Sort by relevance (most relevant first)
744
+ results.sort(key=lambda x: x.get("search_relevance", 0), reverse=True)
745
+
746
+ except Exception as e:
747
+ logger.error(f"Failed to search sessions: {e}")
748
+
749
+ return results
750
+
751
+ def _calculate_search_relevance(self, content: str, query: str) -> float:
752
+ """Calculate search relevance score.
753
+
754
+ Args:
755
+ content: Session content
756
+ query: Search query
757
+
758
+ Returns:
759
+ Relevance score (0.0 to 1.0)
760
+ """
761
+ # Simple relevance calculation
762
+ query_words = query.split()
763
+ content_words = content.split()
764
+
765
+ if not query_words:
766
+ return 0.0
767
+ if not content_words:
768
+ return 0.0
769
+
770
+ # Count exact matches
771
+ exact_matches = content.count(query)
772
+
773
+ # Count word matches
774
+ word_matches = sum(1 for word in query_words if word in content)
775
+
776
+ # Calculate relevance based on frequency and coverage
777
+ relevance = (exact_matches * 0.5 + word_matches * 0.1) / len(content_words)
778
+
779
+ return min(relevance * 100, 1.0) # Normalize to 0-1