fishertools 0.2.1__py3-none-any.whl → 0.4.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 (69) hide show
  1. fishertools/__init__.py +16 -5
  2. fishertools/errors/__init__.py +11 -3
  3. fishertools/errors/exception_types.py +282 -0
  4. fishertools/errors/explainer.py +87 -1
  5. fishertools/errors/models.py +73 -1
  6. fishertools/errors/patterns.py +40 -0
  7. fishertools/examples/cli_example.py +156 -0
  8. fishertools/examples/learn_example.py +65 -0
  9. fishertools/examples/logger_example.py +176 -0
  10. fishertools/examples/menu_example.py +101 -0
  11. fishertools/examples/storage_example.py +175 -0
  12. fishertools/input_utils.py +185 -0
  13. fishertools/learn/__init__.py +19 -2
  14. fishertools/learn/examples.py +88 -1
  15. fishertools/learn/knowledge_engine.py +321 -0
  16. fishertools/learn/repl/__init__.py +19 -0
  17. fishertools/learn/repl/cli.py +31 -0
  18. fishertools/learn/repl/code_sandbox.py +229 -0
  19. fishertools/learn/repl/command_handler.py +544 -0
  20. fishertools/learn/repl/command_parser.py +165 -0
  21. fishertools/learn/repl/engine.py +479 -0
  22. fishertools/learn/repl/models.py +121 -0
  23. fishertools/learn/repl/session_manager.py +284 -0
  24. fishertools/learn/repl/test_code_sandbox.py +261 -0
  25. fishertools/learn/repl/test_code_sandbox_pbt.py +148 -0
  26. fishertools/learn/repl/test_command_handler.py +224 -0
  27. fishertools/learn/repl/test_command_handler_pbt.py +189 -0
  28. fishertools/learn/repl/test_command_parser.py +160 -0
  29. fishertools/learn/repl/test_command_parser_pbt.py +100 -0
  30. fishertools/learn/repl/test_engine.py +190 -0
  31. fishertools/learn/repl/test_session_manager.py +310 -0
  32. fishertools/learn/repl/test_session_manager_pbt.py +182 -0
  33. fishertools/learn/test_knowledge_engine.py +241 -0
  34. fishertools/learn/test_knowledge_engine_pbt.py +180 -0
  35. fishertools/patterns/__init__.py +46 -0
  36. fishertools/patterns/cli.py +175 -0
  37. fishertools/patterns/logger.py +140 -0
  38. fishertools/patterns/menu.py +99 -0
  39. fishertools/patterns/storage.py +127 -0
  40. fishertools/readme_transformer.py +631 -0
  41. fishertools/safe/__init__.py +6 -1
  42. fishertools/safe/files.py +329 -1
  43. fishertools/transform_readme.py +105 -0
  44. fishertools-0.4.0.dist-info/METADATA +104 -0
  45. fishertools-0.4.0.dist-info/RECORD +131 -0
  46. {fishertools-0.2.1.dist-info → fishertools-0.4.0.dist-info}/WHEEL +1 -1
  47. tests/test_documentation_properties.py +329 -0
  48. tests/test_documentation_structure.py +349 -0
  49. tests/test_errors/test_exception_types.py +446 -0
  50. tests/test_errors/test_exception_types_pbt.py +333 -0
  51. tests/test_errors/test_patterns.py +52 -0
  52. tests/test_input_utils/__init__.py +1 -0
  53. tests/test_input_utils/test_input_utils.py +65 -0
  54. tests/test_learn/test_examples.py +179 -1
  55. tests/test_learn/test_explain_properties.py +307 -0
  56. tests/test_patterns_cli.py +611 -0
  57. tests/test_patterns_docstrings.py +473 -0
  58. tests/test_patterns_logger.py +465 -0
  59. tests/test_patterns_menu.py +440 -0
  60. tests/test_patterns_storage.py +447 -0
  61. tests/test_readme_enhancements_v0_3_1.py +2036 -0
  62. tests/test_readme_transformer/__init__.py +1 -0
  63. tests/test_readme_transformer/test_readme_infrastructure.py +1023 -0
  64. tests/test_readme_transformer/test_transform_readme_integration.py +431 -0
  65. tests/test_safe/test_files.py +726 -1
  66. fishertools-0.2.1.dist-info/METADATA +0 -256
  67. fishertools-0.2.1.dist-info/RECORD +0 -81
  68. {fishertools-0.2.1.dist-info → fishertools-0.4.0.dist-info}/licenses/LICENSE +0 -0
  69. {fishertools-0.2.1.dist-info → fishertools-0.4.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,544 @@
1
+ """
2
+ Command handler for the Knowledge Engine REPL.
3
+
4
+ This module handles execution of all REPL commands and returns formatted output.
5
+ """
6
+
7
+ import random
8
+ from typing import List, Dict, Optional, Any
9
+ from difflib import get_close_matches
10
+
11
+ from fishertools.learn.knowledge_engine import KnowledgeEngine
12
+ from fishertools.learn.repl.session_manager import SessionManager
13
+ from fishertools.learn.repl.models import TopicDisplay, ExampleDisplay
14
+
15
+
16
+ class CommandHandler:
17
+ """
18
+ Handles execution of REPL commands and returns formatted output.
19
+
20
+ Supports commands for:
21
+ - Topic browsing (/list, /search, /random, /categories, /category, /path)
22
+ - Topic information (/related)
23
+ - Progress tracking (/progress, /stats)
24
+ - Help and information (/help, /commands, /about)
25
+ - Hints and tips (/hint, /tip, /tips)
26
+
27
+ Example:
28
+ >>> engine = KnowledgeEngine()
29
+ >>> manager = SessionManager()
30
+ >>> handler = CommandHandler(engine, manager)
31
+ >>> output = handler.handle_list()
32
+ >>> print(output)
33
+ """
34
+
35
+ def __init__(self, engine: KnowledgeEngine, session_manager: SessionManager):
36
+ """
37
+ Initialize the command handler.
38
+
39
+ Args:
40
+ engine: Knowledge Engine instance
41
+ session_manager: Session Manager instance
42
+ """
43
+ self.engine = engine
44
+ self.session_manager = session_manager
45
+
46
+ def handle_list(self) -> str:
47
+ """
48
+ Handle /list command - display all topics organized by category.
49
+
50
+ Returns:
51
+ Formatted string with all topics organized by category
52
+ """
53
+ output = "📚 Available Topics:\n"
54
+ output += "=" * 50 + "\n\n"
55
+
56
+ categories = sorted(self.engine.categories.keys())
57
+
58
+ for category in categories:
59
+ topics = self.engine.get_topics_by_category(category)
60
+ output += f"📂 {category}\n"
61
+ for topic in topics:
62
+ output += f" • {topic}\n"
63
+ output += "\n"
64
+
65
+ output += f"Total topics: {len(self.engine.list_topics())}\n"
66
+ output += "Type a topic name to view it, or use /search <keyword> to find topics.\n"
67
+
68
+ return output
69
+
70
+ def handle_search(self, keyword: str) -> str:
71
+ """
72
+ Handle /search command - search for topics by keyword.
73
+
74
+ Args:
75
+ keyword: Keyword to search for
76
+
77
+ Returns:
78
+ Formatted string with search results
79
+ """
80
+ if not keyword or not keyword.strip():
81
+ return "❌ Please provide a search keyword.\nUsage: /search <keyword>"
82
+
83
+ results = self.engine.search_topics(keyword)
84
+
85
+ if not results:
86
+ # Try fuzzy matching for suggestions
87
+ all_topics = self.engine.list_topics()
88
+ suggestions = get_close_matches(keyword, all_topics, n=3, cutoff=0.6)
89
+
90
+ output = f"❌ No topics found for '{keyword}'.\n"
91
+ if suggestions:
92
+ output += "\nDid you mean:\n"
93
+ for suggestion in suggestions:
94
+ output += f" • {suggestion}\n"
95
+ return output
96
+
97
+ output = f"🔍 Search results for '{keyword}':\n"
98
+ output += "=" * 50 + "\n\n"
99
+
100
+ for topic in results:
101
+ output += f"• {topic}\n"
102
+
103
+ output += f"\nFound {len(results)} topic(s).\n"
104
+ output += "Type a topic name to view it.\n"
105
+
106
+ return output
107
+
108
+ def handle_random(self) -> str:
109
+ """
110
+ Handle /random command - display a random topic.
111
+
112
+ Returns:
113
+ Formatted string with random topic name
114
+ """
115
+ topic_dict = self.engine.get_random_topic()
116
+ topic_name = topic_dict.get("topic", "Unknown")
117
+
118
+ output = f"🎲 Random topic: {topic_name}\n"
119
+ output += f"Type '{topic_name}' to view it.\n"
120
+
121
+ return output
122
+
123
+ def handle_categories(self) -> str:
124
+ """
125
+ Handle /categories command - display all available categories.
126
+
127
+ Returns:
128
+ Formatted string with all categories
129
+ """
130
+ output = "📂 Available Categories:\n"
131
+ output += "=" * 50 + "\n\n"
132
+
133
+ categories = sorted(self.engine.categories.keys())
134
+
135
+ for category in categories:
136
+ count = len(self.engine.get_topics_by_category(category))
137
+ output += f"• {category} ({count} topics)\n"
138
+
139
+ output += f"\nTotal categories: {len(categories)}\n"
140
+ output += "Use /category <name> to view topics in a category.\n"
141
+
142
+ return output
143
+
144
+ def handle_category(self, category_name: str) -> str:
145
+ """
146
+ Handle /category command - display topics in a category.
147
+
148
+ Args:
149
+ category_name: Name of the category
150
+
151
+ Returns:
152
+ Formatted string with topics in the category
153
+ """
154
+ if not category_name or not category_name.strip():
155
+ return "❌ Please provide a category name.\nUsage: /category <name>"
156
+
157
+ topics = self.engine.get_topics_by_category(category_name)
158
+
159
+ if not topics:
160
+ # Try fuzzy matching
161
+ all_categories = list(self.engine.categories.keys())
162
+ suggestions = get_close_matches(category_name, all_categories, n=3, cutoff=0.6)
163
+
164
+ output = f"❌ Category '{category_name}' not found.\n"
165
+ if suggestions:
166
+ output += "\nDid you mean:\n"
167
+ for suggestion in suggestions:
168
+ output += f" • {suggestion}\n"
169
+ return output
170
+
171
+ output = f"📂 Topics in '{category_name}':\n"
172
+ output += "=" * 50 + "\n\n"
173
+
174
+ for topic in topics:
175
+ output += f"• {topic}\n"
176
+
177
+ output += f"\nTotal topics: {len(topics)}\n"
178
+ output += "Type a topic name to view it.\n"
179
+
180
+ return output
181
+
182
+ def handle_path(self) -> str:
183
+ """
184
+ Handle /path command - display the recommended learning path.
185
+
186
+ Returns:
187
+ Formatted string with learning path
188
+ """
189
+ path = self.engine.get_learning_path()
190
+
191
+ output = "🛤️ Recommended Learning Path:\n"
192
+ output += "=" * 50 + "\n\n"
193
+
194
+ for i, topic_name in enumerate(path, 1):
195
+ topic = self.engine.get_topic(topic_name)
196
+ difficulty = topic.get("difficulty", "Unknown")
197
+ output += f"{i}. {topic_name} ({difficulty})\n"
198
+
199
+ output += f"\nTotal topics: {len(path)}\n"
200
+ output += "Type a topic name to start learning.\n"
201
+
202
+ return output
203
+
204
+ def handle_related(self, topic_name: str) -> str:
205
+ """
206
+ Handle /related command - display related topics.
207
+
208
+ Args:
209
+ topic_name: Name of the current topic
210
+
211
+ Returns:
212
+ Formatted string with related topics
213
+ """
214
+ if not topic_name:
215
+ return "❌ No current topic. View a topic first.\n"
216
+
217
+ related = self.engine.get_related_topics(topic_name)
218
+
219
+ if not related:
220
+ return f"ℹ️ No related topics for '{topic_name}'.\n"
221
+
222
+ output = f"🔗 Topics related to '{topic_name}':\n"
223
+ output += "=" * 50 + "\n\n"
224
+
225
+ for topic in related:
226
+ output += f"• {topic}\n"
227
+
228
+ output += f"\nTotal related topics: {len(related)}\n"
229
+ output += "Type a topic name to view it.\n"
230
+
231
+ return output
232
+
233
+ def handle_progress(self) -> str:
234
+ """
235
+ Handle /progress command - display learning progress.
236
+
237
+ Returns:
238
+ Formatted string with progress statistics
239
+ """
240
+ progress = self.session_manager.get_progress()
241
+
242
+ output = "📊 Learning Progress:\n"
243
+ output += "=" * 50 + "\n\n"
244
+ output += f"Topics viewed: {progress.viewed_topics}\n"
245
+ output += f"Examples executed: {progress.executed_examples}\n"
246
+ output += f"Session duration: {progress.session_duration:.1f} seconds\n"
247
+
248
+ if progress.last_viewed_topic:
249
+ output += f"Last viewed topic: {progress.last_viewed_topic}\n"
250
+
251
+ return output
252
+
253
+ def handle_stats(self) -> str:
254
+ """
255
+ Handle /stats command - display detailed statistics.
256
+
257
+ Returns:
258
+ Formatted string with detailed statistics
259
+ """
260
+ progress = self.session_manager.get_progress()
261
+ session_info = self.session_manager.get_session_info()
262
+
263
+ output = "📈 Detailed Statistics:\n"
264
+ output += "=" * 50 + "\n\n"
265
+ output += f"Topics viewed: {progress.viewed_topics}\n"
266
+ output += f"Examples executed: {progress.executed_examples}\n"
267
+ output += f"Session duration: {session_info['session_duration_seconds']:.1f} seconds\n"
268
+ output += f"Session history length: {session_info['session_history_length']}\n"
269
+ output += f"Created at: {session_info['created_at']}\n"
270
+ output += f"Last updated: {session_info['last_updated']}\n"
271
+
272
+ return output
273
+
274
+ def handle_hint(self, topic_name: str) -> str:
275
+ """
276
+ Handle /hint command - display a hint for the current topic.
277
+
278
+ Args:
279
+ topic_name: Name of the current topic
280
+
281
+ Returns:
282
+ Formatted string with a hint
283
+ """
284
+ if not topic_name:
285
+ return "❌ No current topic. View a topic first.\n"
286
+
287
+ topic = self.engine.get_topic(topic_name)
288
+ if not topic:
289
+ return f"❌ Topic '{topic_name}' not found.\n"
290
+
291
+ tips = topic.get("tips", [])
292
+ if not tips:
293
+ return f"ℹ️ No hints available for '{topic_name}'.\n"
294
+
295
+ hint = random.choice(tips)
296
+
297
+ output = f"💡 Hint for '{topic_name}':\n"
298
+ output += "=" * 50 + "\n\n"
299
+ output += f"{hint}\n"
300
+
301
+ return output
302
+
303
+ def handle_tip(self) -> str:
304
+ """
305
+ Handle /tip command - display a learning tip.
306
+
307
+ Returns:
308
+ Formatted string with a learning tip
309
+ """
310
+ tips = [
311
+ "💡 Try modifying the examples to see how they work!",
312
+ "💡 Use /search to find topics related to what you're learning.",
313
+ "💡 Check /progress to see how much you've learned.",
314
+ "💡 Use /related to explore connected concepts.",
315
+ "💡 Practice by running and modifying examples.",
316
+ "💡 Read the common mistakes section to avoid errors.",
317
+ "💡 Use /path to follow a structured learning path.",
318
+ ]
319
+
320
+ tip = random.choice(tips)
321
+ return f"{tip}\n"
322
+
323
+ def handle_tips(self, topic_name: str) -> str:
324
+ """
325
+ Handle /tips command - display all tips for a topic.
326
+
327
+ Args:
328
+ topic_name: Name of the current topic
329
+
330
+ Returns:
331
+ Formatted string with all tips
332
+ """
333
+ if not topic_name:
334
+ return "❌ No current topic. View a topic first.\n"
335
+
336
+ topic = self.engine.get_topic(topic_name)
337
+ if not topic:
338
+ return f"❌ Topic '{topic_name}' not found.\n"
339
+
340
+ tips = topic.get("tips", [])
341
+ if not tips:
342
+ return f"ℹ️ No tips available for '{topic_name}'.\n"
343
+
344
+ output = f"💡 Tips for '{topic_name}':\n"
345
+ output += "=" * 50 + "\n\n"
346
+
347
+ for i, tip in enumerate(tips, 1):
348
+ output += f"{i}. {tip}\n"
349
+
350
+ return output
351
+
352
+ def handle_help(self, command: Optional[str] = None) -> str:
353
+ """
354
+ Handle /help command - display help information.
355
+
356
+ Args:
357
+ command: Optional specific command to get help for
358
+
359
+ Returns:
360
+ Formatted string with help information
361
+ """
362
+ if command:
363
+ return self._get_command_help(command)
364
+
365
+ output = "📖 Available Commands:\n"
366
+ output += "=" * 50 + "\n\n"
367
+ output += "Topic Browsing:\n"
368
+ output += " /list - Show all topics by category\n"
369
+ output += " /search <keyword> - Search for topics\n"
370
+ output += " /random - Show a random topic\n"
371
+ output += " /categories - Show all categories\n"
372
+ output += " /category <name> - Show topics in a category\n"
373
+ output += " /path - Show recommended learning path\n\n"
374
+ output += "Topic Navigation:\n"
375
+ output += " /related - Show related topics\n"
376
+ output += " /next - Go to next topic in path\n"
377
+ output += " /prev - Go to previous topic in path\n"
378
+ output += " /goto <topic> - Go to specific topic\n\n"
379
+ output += "Code Execution:\n"
380
+ output += " /run <num> - Run example number\n"
381
+ output += " /modify <num> - Modify and run example\n"
382
+ output += " /exit_edit - Exit edit mode\n\n"
383
+ output += "Progress Tracking:\n"
384
+ output += " /progress - Show learning progress\n"
385
+ output += " /stats - Show detailed statistics\n"
386
+ output += " /reset_progress - Reset all progress\n\n"
387
+ output += "Session Management:\n"
388
+ output += " /history - Show session history\n"
389
+ output += " /clear_history - Clear session history\n"
390
+ output += " /session - Show session info\n\n"
391
+ output += "Help and Information:\n"
392
+ output += " /help - Show this help message\n"
393
+ output += " /help <command> - Get help for a command\n"
394
+ output += " /commands - Show all commands\n"
395
+ output += " /about - About the REPL\n"
396
+ output += " /hint - Get a hint for current topic\n"
397
+ output += " /tip - Get a learning tip\n"
398
+ output += " /tips - Show all tips for current topic\n\n"
399
+ output += "Session Control:\n"
400
+ output += " /exit, /quit - Exit the REPL\n\n"
401
+ output += "Type /help <command> for more details on a specific command.\n"
402
+
403
+ return output
404
+
405
+ def _get_command_help(self, command: str) -> str:
406
+ """
407
+ Get detailed help for a specific command.
408
+
409
+ Args:
410
+ command: Command name
411
+
412
+ Returns:
413
+ Formatted help text for the command
414
+ """
415
+ help_text = {
416
+ "list": "Show all available topics organized by category.\nUsage: /list",
417
+ "search": "Search for topics by keyword.\nUsage: /search <keyword>\nExample: /search list",
418
+ "random": "Display a random topic to encourage exploration.\nUsage: /random",
419
+ "categories": "Show all available topic categories.\nUsage: /categories",
420
+ "category": "Show all topics in a specific category.\nUsage: /category <name>\nExample: /category \"Basic Types\"",
421
+ "path": "Show the recommended learning path from beginner to advanced.\nUsage: /path",
422
+ "related": "Show topics related to the current topic.\nUsage: /related",
423
+ "progress": "Show your learning progress.\nUsage: /progress",
424
+ "stats": "Show detailed learning statistics.\nUsage: /stats",
425
+ "hint": "Get a hint for the current topic.\nUsage: /hint",
426
+ "tip": "Get a random learning tip.\nUsage: /tip",
427
+ "tips": "Show all tips for the current topic.\nUsage: /tips",
428
+ "help": "Show help information.\nUsage: /help or /help <command>",
429
+ "run": "Run a code example.\nUsage: /run <example_number>\nExample: /run 1",
430
+ "modify": "Modify and run a code example.\nUsage: /modify <example_number>\nExample: /modify 1",
431
+ "exit_edit": "Exit edit mode.\nUsage: /exit_edit",
432
+ "history": "Show your session history.\nUsage: /history",
433
+ "clear_history": "Clear your session history.\nUsage: /clear_history",
434
+ "session": "Show current session information.\nUsage: /session",
435
+ "reset_progress": "Reset all learning progress.\nUsage: /reset_progress",
436
+ "commands": "Show all available commands.\nUsage: /commands",
437
+ "about": "Show information about the REPL.\nUsage: /about",
438
+ "exit": "Exit the REPL.\nUsage: /exit or /quit",
439
+ "quit": "Exit the REPL.\nUsage: /quit or /exit",
440
+ }
441
+
442
+ command_lower = command.lower()
443
+ if command_lower in help_text:
444
+ return f"📖 Help for '{command}':\n" + "=" * 50 + "\n\n" + help_text[command_lower] + "\n"
445
+ else:
446
+ return f"❌ No help available for '{command}'.\nType /help to see all commands.\n"
447
+
448
+ def handle_commands(self) -> str:
449
+ """
450
+ Handle /commands command - show all commands categorized.
451
+
452
+ Returns:
453
+ Formatted string with all commands
454
+ """
455
+ output = "📋 All Commands:\n"
456
+ output += "=" * 50 + "\n\n"
457
+ output += "Topic Browsing: /list, /search, /random, /categories, /category, /path\n"
458
+ output += "Navigation: /related, /next, /prev, /goto\n"
459
+ output += "Code: /run, /modify, /exit_edit\n"
460
+ output += "Progress: /progress, /stats, /reset_progress\n"
461
+ output += "Session: /history, /clear_history, /session\n"
462
+ output += "Help: /help, /commands, /about, /hint, /tip, /tips\n"
463
+ output += "Control: /exit, /quit\n\n"
464
+ output += "Type /help <command> for details on a specific command.\n"
465
+
466
+ return output
467
+
468
+ def handle_about(self) -> str:
469
+ """
470
+ Handle /about command - show information about the REPL.
471
+
472
+ Returns:
473
+ Formatted string with about information
474
+ """
475
+ output = "ℹ️ About Knowledge Engine REPL:\n"
476
+ output += "=" * 50 + "\n\n"
477
+ output += "The Knowledge Engine REPL is an interactive learning tool for Python.\n"
478
+ output += "It provides:\n"
479
+ output += " • Interactive topic browsing and discovery\n"
480
+ output += " • Safe code execution in a sandbox environment\n"
481
+ output += " • Learning progress tracking\n"
482
+ output += " • Contextual hints and guidance\n"
483
+ output += " • Session persistence for resuming learning\n\n"
484
+ output += "Type /help to get started.\n"
485
+
486
+ return output
487
+
488
+ def format_topic_display(self, topic_name: str) -> str:
489
+ """
490
+ Format a topic for display.
491
+
492
+ Args:
493
+ topic_name: Name of the topic to display
494
+
495
+ Returns:
496
+ Formatted string with topic information
497
+ """
498
+ topic = self.engine.get_topic(topic_name)
499
+ if not topic:
500
+ return f"❌ Topic '{topic_name}' not found.\n"
501
+
502
+ output = f"\n📚 {topic_name}\n"
503
+ output += "=" * 50 + "\n\n"
504
+
505
+ # Category and difficulty
506
+ category = topic.get("category", "Unknown")
507
+ difficulty = topic.get("difficulty", "Unknown")
508
+ output += f"Category: {category} | Difficulty: {difficulty}\n\n"
509
+
510
+ # Description
511
+ description = topic.get("description", "")
512
+ if description:
513
+ output += f"Description:\n{description}\n\n"
514
+
515
+ # Examples
516
+ examples = topic.get("examples", [])
517
+ if examples:
518
+ output += "Examples:\n"
519
+ for i, example in enumerate(examples, 1):
520
+ output += f"\n Example {i}: {example.get('description', '')}\n"
521
+ output += f" ```python\n"
522
+ code_lines = example.get('code', '').split('\n')
523
+ for line in code_lines:
524
+ output += f" {line}\n"
525
+ output += f" ```\n"
526
+
527
+ # Common mistakes
528
+ mistakes = topic.get("common_mistakes", [])
529
+ if mistakes:
530
+ output += "\nCommon Mistakes:\n"
531
+ for mistake in mistakes:
532
+ output += f" ⚠️ {mistake}\n"
533
+
534
+ # Related topics
535
+ related = self.engine.get_related_topics(topic_name)
536
+ if related:
537
+ output += "\nRelated Topics:\n"
538
+ for related_topic in related:
539
+ output += f" • {related_topic}\n"
540
+
541
+ output += "\n" + "=" * 50 + "\n"
542
+ output += "Commands: /run <num>, /modify <num>, /related, /next, /prev, /goto <topic>\n"
543
+
544
+ return output