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,165 @@
1
+ """
2
+ Command parser for the Knowledge Engine REPL.
3
+
4
+ This module handles parsing user input to identify commands, topic names, and edit mode input.
5
+ """
6
+
7
+ import shlex
8
+ from typing import Tuple, List
9
+
10
+
11
+ class CommandParser:
12
+ """
13
+ Parses user input to identify command type and extract arguments.
14
+
15
+ Supports three input types:
16
+ - Commands: Start with "/" (e.g., "/help", "/search python")
17
+ - Topic names: Regular text matching a topic in the Knowledge Engine
18
+ - Edit mode input: Code or commands while in edit mode
19
+ """
20
+
21
+ # Commands that are recognized by the REPL
22
+ VALID_COMMANDS = {
23
+ "help", "list", "search", "random", "categories", "category",
24
+ "path", "related", "progress", "stats", "hint", "tip", "tips",
25
+ "run", "modify", "exit_edit", "history", "clear_history", "session",
26
+ "reset_progress", "commands", "about", "tutorial", "next", "prev",
27
+ "goto", "exit", "quit"
28
+ }
29
+
30
+ @staticmethod
31
+ def parse(input_str: str) -> Tuple[str, List[str]]:
32
+ """
33
+ Parse user input into command type and arguments.
34
+
35
+ Args:
36
+ input_str: The user input string
37
+
38
+ Returns:
39
+ Tuple of (command_type, arguments) where:
40
+ - command_type: 'command', 'topic', or 'edit'
41
+ - arguments: List of argument strings
42
+
43
+ Raises:
44
+ ValueError: If input cannot be parsed
45
+
46
+ Example:
47
+ >>> parser = CommandParser()
48
+ >>> cmd_type, args = parser.parse("/help")
49
+ >>> cmd_type
50
+ 'command'
51
+ >>> args
52
+ ['help']
53
+
54
+ >>> cmd_type, args = parser.parse("/search python")
55
+ >>> args
56
+ ['search', 'python']
57
+
58
+ >>> cmd_type, args = parser.parse("Lists")
59
+ >>> cmd_type
60
+ 'topic'
61
+ >>> args
62
+ ['Lists']
63
+ """
64
+ if not input_str or not input_str.strip():
65
+ raise ValueError("Input cannot be empty")
66
+
67
+ input_str = input_str.strip()
68
+
69
+ # Check if it's a command (starts with /)
70
+ if input_str.startswith("/"):
71
+ return CommandParser._parse_command(input_str)
72
+
73
+ # Otherwise it's a topic name or edit mode input
74
+ return "topic", [input_str]
75
+
76
+ @staticmethod
77
+ def _parse_command(input_str: str) -> Tuple[str, List[str]]:
78
+ """
79
+ Parse a command string (starts with /).
80
+
81
+ Args:
82
+ input_str: Command string starting with /
83
+
84
+ Returns:
85
+ Tuple of ('command', [command_name, arg1, arg2, ...])
86
+
87
+ Raises:
88
+ ValueError: If command format is invalid
89
+ """
90
+ # Remove leading /
91
+ command_str = input_str[1:].strip()
92
+
93
+ if not command_str:
94
+ raise ValueError("Command cannot be empty")
95
+
96
+ # Use shlex to handle quoted arguments
97
+ try:
98
+ parts = shlex.split(command_str)
99
+ except ValueError as e:
100
+ raise ValueError(f"Invalid command format: {e}")
101
+
102
+ if not parts:
103
+ raise ValueError("Command cannot be empty")
104
+
105
+ command_name = parts[0].lower()
106
+ args = parts[1:] if len(parts) > 1 else []
107
+
108
+ # Validate command name
109
+ if command_name not in CommandParser.VALID_COMMANDS:
110
+ raise ValueError(f"Unknown command: {command_name}")
111
+
112
+ return "command", [command_name] + args
113
+
114
+ @staticmethod
115
+ def normalize_topic_name(name: str) -> str:
116
+ """
117
+ Normalize a topic name for comparison.
118
+
119
+ Args:
120
+ name: The topic name to normalize
121
+
122
+ Returns:
123
+ Normalized topic name (preserves case but strips whitespace)
124
+ """
125
+ return name.strip()
126
+
127
+ @staticmethod
128
+ def is_command(input_str: str) -> bool:
129
+ """
130
+ Check if input is a command.
131
+
132
+ Args:
133
+ input_str: The input string to check
134
+
135
+ Returns:
136
+ True if input starts with /
137
+ """
138
+ return input_str.strip().startswith("/")
139
+
140
+ @staticmethod
141
+ def extract_command_name(input_str: str) -> str:
142
+ """
143
+ Extract just the command name from a command string.
144
+
145
+ Args:
146
+ input_str: Command string starting with /
147
+
148
+ Returns:
149
+ The command name (lowercase)
150
+
151
+ Raises:
152
+ ValueError: If input is not a valid command
153
+ """
154
+ if not input_str.startswith("/"):
155
+ raise ValueError("Not a command")
156
+
157
+ command_str = input_str[1:].strip()
158
+ if not command_str:
159
+ raise ValueError("Command cannot be empty")
160
+
161
+ try:
162
+ parts = shlex.split(command_str)
163
+ return parts[0].lower()
164
+ except ValueError as e:
165
+ raise ValueError(f"Invalid command format: {e}")
@@ -0,0 +1,479 @@
1
+ """
2
+ Main REPL Engine for the Knowledge Engine Interactive REPL.
3
+
4
+ This module provides the main loop and orchestration for the REPL system.
5
+ """
6
+
7
+ from typing import Optional, List, Tuple
8
+ from fishertools.learn.knowledge_engine import KnowledgeEngine
9
+ from fishertools.learn.repl.command_parser import CommandParser
10
+ from fishertools.learn.repl.command_handler import CommandHandler
11
+ from fishertools.learn.repl.code_sandbox import CodeSandbox
12
+ from fishertools.learn.repl.session_manager import SessionManager
13
+
14
+
15
+ class REPLEngine:
16
+ """
17
+ Main REPL engine that orchestrates user interaction.
18
+
19
+ Manages:
20
+ - User input parsing
21
+ - Command execution
22
+ - Topic display
23
+ - Session state
24
+ - Code execution
25
+
26
+ Example:
27
+ >>> engine = REPLEngine()
28
+ >>> engine.start()
29
+ """
30
+
31
+ WELCOME_MESSAGE = """
32
+ ╔════════════════════════════════════════════════════════════════╗
33
+ ║ Welcome to the Knowledge Engine Interactive REPL! 🎓 ║
34
+ ║ ║
35
+ ║ Learn Python concepts interactively with examples and tips. ║
36
+ ║ Type /help to see available commands. ║
37
+ ║ Type /exit or /quit to exit. ║
38
+ ╚════════════════════════════════════════════════════════════════╝
39
+ """
40
+
41
+ def __init__(self, engine: Optional[KnowledgeEngine] = None,
42
+ session_manager: Optional[SessionManager] = None):
43
+ """
44
+ Initialize the REPL engine.
45
+
46
+ Args:
47
+ engine: Optional Knowledge Engine instance (creates new if not provided)
48
+ session_manager: Optional SessionManager instance (creates new if not provided)
49
+ """
50
+ self.engine = engine or KnowledgeEngine()
51
+ self.session_manager = session_manager or SessionManager()
52
+ self.command_handler = CommandHandler(self.engine, self.session_manager)
53
+ self.code_sandbox = CodeSandbox()
54
+ self.parser = CommandParser()
55
+
56
+ self.current_topic: Optional[str] = None
57
+ self.in_edit_mode = False
58
+ self.edit_topic: Optional[str] = None
59
+ self.edit_example_num: Optional[int] = None
60
+ self.edit_code: str = ""
61
+ self.running = False
62
+
63
+ def start(self) -> None:
64
+ """
65
+ Start the interactive REPL loop.
66
+
67
+ Displays welcome message and enters main loop.
68
+ """
69
+ print(self.WELCOME_MESSAGE)
70
+
71
+ # Load previous session if available
72
+ if self.session_manager.get_current_topic():
73
+ self.current_topic = self.session_manager.get_current_topic()
74
+ print(f"📚 Resuming previous session. Last topic: {self.current_topic}\n")
75
+
76
+ self.running = True
77
+
78
+ try:
79
+ while self.running:
80
+ self._prompt_and_process()
81
+ except KeyboardInterrupt:
82
+ print("\n\n👋 Goodbye! Your progress has been saved.")
83
+ self.session_manager.save_session()
84
+ except Exception as e:
85
+ print(f"\n❌ An unexpected error occurred: {e}")
86
+ print("Your progress has been saved.")
87
+ self.session_manager.save_session()
88
+
89
+ def _prompt_and_process(self) -> None:
90
+ """
91
+ Prompt for user input and process it.
92
+ """
93
+ try:
94
+ if self.in_edit_mode:
95
+ prompt = f"[Edit {self.edit_topic} Example {self.edit_example_num}]> "
96
+ elif self.current_topic:
97
+ prompt = f"[{self.current_topic}]> "
98
+ else:
99
+ prompt = "> "
100
+
101
+ user_input = input(prompt).strip()
102
+
103
+ if not user_input:
104
+ return
105
+
106
+ self._process_input(user_input)
107
+
108
+ except EOFError:
109
+ self.running = False
110
+
111
+ def _process_input(self, user_input: str) -> None:
112
+ """
113
+ Process user input.
114
+
115
+ Args:
116
+ user_input: The user's input string
117
+ """
118
+ try:
119
+ cmd_type, args = self.parser.parse(user_input)
120
+ except ValueError as e:
121
+ print(f"❌ {e}")
122
+ return
123
+
124
+ if cmd_type == "command":
125
+ self._handle_command(args)
126
+ elif cmd_type == "topic":
127
+ self._handle_topic_input(args[0])
128
+
129
+ def _handle_command(self, args: List[str]) -> None:
130
+ """
131
+ Handle a command.
132
+
133
+ Args:
134
+ args: Command and arguments [command_name, arg1, arg2, ...]
135
+ """
136
+ command = args[0]
137
+ command_args = args[1:] if len(args) > 1 else []
138
+
139
+ # Exit commands
140
+ if command in ["exit", "quit"]:
141
+ print("👋 Goodbye! Your progress has been saved.")
142
+ self.session_manager.save_session()
143
+ self.running = False
144
+ return
145
+
146
+ # Edit mode commands
147
+ if self.in_edit_mode:
148
+ if command == "exit_edit":
149
+ self._exit_edit_mode()
150
+ else:
151
+ print("❌ You are in edit mode. Type /exit_edit to exit.")
152
+ return
153
+
154
+ # Topic browsing commands
155
+ if command == "list":
156
+ output = self.command_handler.handle_list()
157
+ print(output)
158
+
159
+ elif command == "search":
160
+ keyword = " ".join(command_args) if command_args else ""
161
+ output = self.command_handler.handle_search(keyword)
162
+ print(output)
163
+
164
+ elif command == "random":
165
+ output = self.command_handler.handle_random()
166
+ print(output)
167
+
168
+ elif command == "categories":
169
+ output = self.command_handler.handle_categories()
170
+ print(output)
171
+
172
+ elif command == "category":
173
+ category = " ".join(command_args) if command_args else ""
174
+ output = self.command_handler.handle_category(category)
175
+ print(output)
176
+
177
+ elif command == "path":
178
+ output = self.command_handler.handle_path()
179
+ print(output)
180
+
181
+ elif command == "related":
182
+ output = self.command_handler.handle_related(self.current_topic)
183
+ print(output)
184
+
185
+ elif command == "next":
186
+ self._navigate_next()
187
+
188
+ elif command == "prev":
189
+ self._navigate_prev()
190
+
191
+ elif command == "goto":
192
+ topic = " ".join(command_args) if command_args else ""
193
+ self._navigate_to_topic(topic)
194
+
195
+ # Code execution commands
196
+ elif command == "run":
197
+ if not self.current_topic:
198
+ print("❌ No current topic. View a topic first.")
199
+ return
200
+
201
+ if not command_args:
202
+ print("❌ Please provide an example number.\nUsage: /run <number>")
203
+ return
204
+
205
+ try:
206
+ example_num = int(command_args[0])
207
+ self._run_example(example_num)
208
+ except ValueError:
209
+ print("❌ Example number must be an integer.")
210
+
211
+ elif command == "modify":
212
+ if not self.current_topic:
213
+ print("❌ No current topic. View a topic first.")
214
+ return
215
+
216
+ if not command_args:
217
+ print("❌ Please provide an example number.\nUsage: /modify <number>")
218
+ return
219
+
220
+ try:
221
+ example_num = int(command_args[0])
222
+ self._enter_edit_mode(example_num)
223
+ except ValueError:
224
+ print("❌ Example number must be an integer.")
225
+
226
+ # Progress commands
227
+ elif command == "progress":
228
+ output = self.command_handler.handle_progress()
229
+ print(output)
230
+
231
+ elif command == "stats":
232
+ output = self.command_handler.handle_stats()
233
+ print(output)
234
+
235
+ elif command == "reset_progress":
236
+ response = input("⚠️ Are you sure you want to reset all progress? (yes/no): ").strip().lower()
237
+ if response == "yes":
238
+ self.session_manager.reset_progress()
239
+ print("✅ Progress reset.")
240
+ else:
241
+ print("❌ Reset cancelled.")
242
+
243
+ # Session commands
244
+ elif command == "history":
245
+ history = self.session_manager.get_session_history()
246
+ if not history:
247
+ print("📋 Session history is empty.")
248
+ else:
249
+ print("📋 Session History:")
250
+ for i, topic in enumerate(history, 1):
251
+ print(f" {i}. {topic}")
252
+
253
+ elif command == "clear_history":
254
+ response = input("⚠️ Are you sure you want to clear session history? (yes/no): ").strip().lower()
255
+ if response == "yes":
256
+ self.session_manager.clear_session_history()
257
+ print("✅ History cleared.")
258
+ else:
259
+ print("❌ Clear cancelled.")
260
+
261
+ elif command == "session":
262
+ info = self.session_manager.get_session_info()
263
+ print("📊 Session Information:")
264
+ for key, value in info.items():
265
+ print(f" {key}: {value}")
266
+
267
+ # Help commands
268
+ elif command == "help":
269
+ help_cmd = command_args[0] if command_args else None
270
+ output = self.command_handler.handle_help(help_cmd)
271
+ print(output)
272
+
273
+ elif command == "commands":
274
+ output = self.command_handler.handle_commands()
275
+ print(output)
276
+
277
+ elif command == "about":
278
+ output = self.command_handler.handle_about()
279
+ print(output)
280
+
281
+ elif command == "hint":
282
+ output = self.command_handler.handle_hint(self.current_topic)
283
+ print(output)
284
+
285
+ elif command == "tip":
286
+ output = self.command_handler.handle_tip()
287
+ print(output)
288
+
289
+ elif command == "tips":
290
+ output = self.command_handler.handle_tips(self.current_topic)
291
+ print(output)
292
+
293
+ else:
294
+ print(f"❌ Unknown command: {command}")
295
+
296
+ def _handle_topic_input(self, topic_name: str) -> None:
297
+ """
298
+ Handle topic name input.
299
+
300
+ Args:
301
+ topic_name: Name of the topic to display
302
+ """
303
+ topic = self.engine.get_topic(topic_name)
304
+
305
+ if not topic:
306
+ # Try fuzzy matching
307
+ from difflib import get_close_matches
308
+ all_topics = self.engine.list_topics()
309
+ suggestions = get_close_matches(topic_name, all_topics, n=3, cutoff=0.6)
310
+
311
+ print(f"❌ Topic '{topic_name}' not found.")
312
+ if suggestions:
313
+ print("\nDid you mean:")
314
+ for suggestion in suggestions:
315
+ print(f" • {suggestion}")
316
+ return
317
+
318
+ self._display_topic(topic_name)
319
+
320
+ def _display_topic(self, topic_name: str) -> None:
321
+ """
322
+ Display a topic.
323
+
324
+ Args:
325
+ topic_name: Name of the topic to display
326
+ """
327
+ self.current_topic = topic_name
328
+ self.session_manager.set_current_topic(topic_name)
329
+ self.session_manager.mark_topic_viewed(topic_name)
330
+
331
+ output = self.command_handler.format_topic_display(topic_name)
332
+ print(output)
333
+
334
+ def _run_example(self, example_num: int) -> None:
335
+ """
336
+ Run a code example.
337
+
338
+ Args:
339
+ example_num: Number of the example to run
340
+ """
341
+ topic = self.engine.get_topic(self.current_topic)
342
+ if not topic:
343
+ print("❌ Topic not found.")
344
+ return
345
+
346
+ examples = topic.get("examples", [])
347
+
348
+ # Find the example (examples are 1-indexed)
349
+ if example_num < 1 or example_num > len(examples):
350
+ print(f"❌ Invalid example number. Available examples: 1-{len(examples)}")
351
+ return
352
+
353
+ example = examples[example_num - 1]
354
+ code = example.get("code", "")
355
+
356
+ if not code:
357
+ print("❌ Example has no code.")
358
+ return
359
+
360
+ print(f"\n▶️ Running Example {example_num}...\n")
361
+
362
+ success, output = self.code_sandbox.execute(code)
363
+
364
+ if success:
365
+ print(f"✅ Output:\n{output}")
366
+ self.session_manager.mark_example_executed(self.current_topic, example_num)
367
+ else:
368
+ print(f"❌ Error:\n{output}")
369
+
370
+ def _enter_edit_mode(self, example_num: int) -> None:
371
+ """
372
+ Enter edit mode for an example.
373
+
374
+ Args:
375
+ example_num: Number of the example to edit
376
+ """
377
+ topic = self.engine.get_topic(self.current_topic)
378
+ if not topic:
379
+ print("❌ Topic not found.")
380
+ return
381
+
382
+ examples = topic.get("examples", [])
383
+
384
+ if example_num < 1 or example_num > len(examples):
385
+ print(f"❌ Invalid example number. Available examples: 1-{len(examples)}")
386
+ return
387
+
388
+ example = examples[example_num - 1]
389
+ code = example.get("code", "")
390
+
391
+ if not code:
392
+ print("❌ Example has no code.")
393
+ return
394
+
395
+ self.in_edit_mode = True
396
+ self.edit_topic = self.current_topic
397
+ self.edit_example_num = example_num
398
+ self.edit_code = code
399
+
400
+ print(f"\n✏️ Editing Example {example_num}:")
401
+ print("=" * 50)
402
+ print(self.edit_code)
403
+ print("=" * 50)
404
+ print("Enter new code (type /exit_edit when done):")
405
+
406
+ def _exit_edit_mode(self) -> None:
407
+ """
408
+ Exit edit mode and execute the modified code.
409
+ """
410
+ if not self.in_edit_mode:
411
+ print("❌ Not in edit mode.")
412
+ return
413
+
414
+ print(f"\n▶️ Running modified Example {self.edit_example_num}...\n")
415
+
416
+ success, output = self.code_sandbox.execute(self.edit_code)
417
+
418
+ if success:
419
+ print(f"✅ Output:\n{output}")
420
+ self.session_manager.mark_example_executed(self.current_topic, self.edit_example_num)
421
+ else:
422
+ print(f"❌ Error:\n{output}")
423
+
424
+ self.in_edit_mode = False
425
+ self.edit_topic = None
426
+ self.edit_example_num = None
427
+ self.edit_code = ""
428
+
429
+ def _navigate_next(self) -> None:
430
+ """Navigate to the next topic in the learning path."""
431
+ path = self.engine.get_learning_path()
432
+
433
+ if not self.current_topic:
434
+ if path:
435
+ self._display_topic(path[0])
436
+ else:
437
+ print("❌ No topics available.")
438
+ return
439
+
440
+ try:
441
+ current_index = path.index(self.current_topic)
442
+ if current_index < len(path) - 1:
443
+ next_topic = path[current_index + 1]
444
+ self._display_topic(next_topic)
445
+ else:
446
+ print("📍 You are at the last topic in the learning path.")
447
+ except ValueError:
448
+ print("❌ Current topic not in learning path.")
449
+
450
+ def _navigate_prev(self) -> None:
451
+ """Navigate to the previous topic in the learning path."""
452
+ path = self.engine.get_learning_path()
453
+
454
+ if not self.current_topic:
455
+ print("❌ No current topic.")
456
+ return
457
+
458
+ try:
459
+ current_index = path.index(self.current_topic)
460
+ if current_index > 0:
461
+ prev_topic = path[current_index - 1]
462
+ self._display_topic(prev_topic)
463
+ else:
464
+ print("📍 You are at the first topic in the learning path.")
465
+ except ValueError:
466
+ print("❌ Current topic not in learning path.")
467
+
468
+ def _navigate_to_topic(self, topic_name: str) -> None:
469
+ """
470
+ Navigate to a specific topic.
471
+
472
+ Args:
473
+ topic_name: Name of the topic to navigate to
474
+ """
475
+ if not topic_name:
476
+ print("❌ Please provide a topic name.\nUsage: /goto <topic_name>")
477
+ return
478
+
479
+ self._handle_topic_input(topic_name)