socratic-debug 0.0.2__tar.gz → 0.0.4__tar.gz

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 (54) hide show
  1. {socratic_debug-0.0.2/socratic_debug.egg-info → socratic_debug-0.0.4}/PKG-INFO +1 -1
  2. {socratic_debug-0.0.2 → socratic_debug-0.0.4}/pyproject.toml +2 -2
  3. socratic_debug-0.0.4/socratic.py +28 -0
  4. {socratic_debug-0.0.2 → socratic_debug-0.0.4/socratic_debug.egg-info}/PKG-INFO +1 -1
  5. {socratic_debug-0.0.2 → socratic_debug-0.0.4}/socratic_debug.egg-info/SOURCES.txt +4 -1
  6. socratic_debug-0.0.4/socratic_debug.egg-info/entry_points.txt +2 -0
  7. {socratic_debug-0.0.2 → socratic_debug-0.0.4}/src/agent/nodes/evaluate_node.py +19 -7
  8. {socratic_debug-0.0.2 → socratic_debug-0.0.4}/src/agent/nodes/human_input_node.py +11 -8
  9. {socratic_debug-0.0.2 → socratic_debug-0.0.4}/src/agent/nodes/questioner_node.py +2 -2
  10. {socratic_debug-0.0.2 → socratic_debug-0.0.4}/src/agent/nodes/solution_node.py +2 -3
  11. {socratic_debug-0.0.2 → socratic_debug-0.0.4}/src/agent/runner.py +1 -1
  12. {socratic_debug-0.0.2 → socratic_debug-0.0.4}/src/cli.py +12 -20
  13. socratic_debug-0.0.4/src/events.py +67 -0
  14. socratic_debug-0.0.4/src/extractor.py +72 -0
  15. socratic_debug-0.0.4/src/hook.py +21 -0
  16. socratic_debug-0.0.4/src/watcher.py +88 -0
  17. socratic_debug-0.0.2/test/test_hock.py → socratic_debug-0.0.4/test/test_hook.py +4 -1
  18. socratic_debug-0.0.4/ui/adapter.py +23 -0
  19. {socratic_debug-0.0.2 → socratic_debug-0.0.4}/ui/app.py +54 -0
  20. {socratic_debug-0.0.2 → socratic_debug-0.0.4}/ui/translations.py +6 -0
  21. socratic_debug-0.0.4/ui/views.py +115 -0
  22. socratic_debug-0.0.2/socratic.py +0 -8
  23. socratic_debug-0.0.2/socratic_debug.egg-info/entry_points.txt +0 -2
  24. socratic_debug-0.0.2/src/extractor.py +0 -34
  25. socratic_debug-0.0.2/src/hook.py +0 -67
  26. socratic_debug-0.0.2/ui/views.py +0 -52
  27. {socratic_debug-0.0.2 → socratic_debug-0.0.4}/LICENSE +0 -0
  28. {socratic_debug-0.0.2 → socratic_debug-0.0.4}/README.md +0 -0
  29. {socratic_debug-0.0.2 → socratic_debug-0.0.4}/setup.cfg +0 -0
  30. {socratic_debug-0.0.2 → socratic_debug-0.0.4}/socratic_debug.egg-info/dependency_links.txt +0 -0
  31. {socratic_debug-0.0.2 → socratic_debug-0.0.4}/socratic_debug.egg-info/requires.txt +0 -0
  32. {socratic_debug-0.0.2 → socratic_debug-0.0.4}/socratic_debug.egg-info/top_level.txt +0 -0
  33. {socratic_debug-0.0.2 → socratic_debug-0.0.4}/src/__init__.py +0 -0
  34. {socratic_debug-0.0.2 → socratic_debug-0.0.4}/src/agent/graph/graph.py +0 -0
  35. {socratic_debug-0.0.2 → socratic_debug-0.0.4}/src/agent/llm/factory.py +0 -0
  36. {socratic_debug-0.0.2 → socratic_debug-0.0.4}/src/agent/llm/providers/__init__.py +0 -0
  37. {socratic_debug-0.0.2 → socratic_debug-0.0.4}/src/agent/llm/providers/ollama.py +0 -0
  38. {socratic_debug-0.0.2 → socratic_debug-0.0.4}/src/agent/llm/providers/openrouter.py +0 -0
  39. {socratic_debug-0.0.2 → socratic_debug-0.0.4}/src/agent/nodes/diagnose_node.py +0 -0
  40. {socratic_debug-0.0.2 → socratic_debug-0.0.4}/src/agent/prompts/diagnose_prompt.py +0 -0
  41. {socratic_debug-0.0.2 → socratic_debug-0.0.4}/src/agent/prompts/evaluation_prompt.py +0 -0
  42. {socratic_debug-0.0.2 → socratic_debug-0.0.4}/src/agent/prompts/mentor_prompt.py +0 -0
  43. {socratic_debug-0.0.2 → socratic_debug-0.0.4}/src/agent/prompts/questioner_prompt.py +0 -0
  44. {socratic_debug-0.0.2 → socratic_debug-0.0.4}/src/agent/prompts/solution_prompt.py +0 -0
  45. {socratic_debug-0.0.2 → socratic_debug-0.0.4}/src/agent/state/state.py +0 -0
  46. {socratic_debug-0.0.2 → socratic_debug-0.0.4}/src/config_creator.py +0 -0
  47. {socratic_debug-0.0.2 → socratic_debug-0.0.4}/src/config_template.py +0 -0
  48. {socratic_debug-0.0.2 → socratic_debug-0.0.4}/src/utils/read_yaml.py +0 -0
  49. {socratic_debug-0.0.2 → socratic_debug-0.0.4}/ui/__init__.py +0 -0
  50. {socratic_debug-0.0.2 → socratic_debug-0.0.4}/ui/bidi.py +0 -0
  51. {socratic_debug-0.0.2 → socratic_debug-0.0.4}/ui/console.py +0 -0
  52. {socratic_debug-0.0.2 → socratic_debug-0.0.4}/ui/styles.py +0 -0
  53. {socratic_debug-0.0.2 → socratic_debug-0.0.4}/ui/templates.py +0 -0
  54. {socratic_debug-0.0.2 → socratic_debug-0.0.4}/ui/window.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: socratic-debug
3
- Version: 0.0.2
3
+ Version: 0.0.4
4
4
  Summary: Autonomous pedagogical debugging mentor powered by LangGraph, Rich, and LangSmith
5
5
  Author: Mohamed Elawakey
6
6
  Requires-Python: >=3.10
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "socratic-debug"
7
- version = "0.0.2"
7
+ version = "0.0.4"
8
8
  description = "Autonomous pedagogical debugging mentor powered by LangGraph, Rich, and LangSmith"
9
9
  authors = [{ name = "Mohamed Elawakey" }]
10
10
  readme = "README.md"
@@ -20,7 +20,7 @@ dependencies = [
20
20
  ]
21
21
 
22
22
  [project.scripts]
23
- socratic = "src.cli:main"
23
+ socratic = "socratic:main"
24
24
 
25
25
  [tool.setuptools]
26
26
  py-modules = ["socratic"]
@@ -0,0 +1,28 @@
1
+ """Socratic Debug — Autonomous pedagogical debugging mentor."""
2
+
3
+ from src.hook import install as _core_install
4
+ from src.agent.runner import run_socratic_agent
5
+
6
+
7
+ def _wire_ui():
8
+ try:
9
+ from ui.adapter import register_ui_adapter
10
+ register_ui_adapter()
11
+ except Exception:
12
+ pass
13
+
14
+
15
+ def install():
16
+ """Binds UI presentation adapter and installs system exception hook."""
17
+ _wire_ui()
18
+ _core_install()
19
+
20
+
21
+ def main():
22
+ """Primary CLI composition root: connects UI adapter to core and executes CLI."""
23
+ _wire_ui()
24
+ from src.cli import main as _cli_main
25
+ _cli_main()
26
+
27
+
28
+ __all__ = ["install", "run_socratic_agent", "main"]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: socratic-debug
3
- Version: 0.0.2
3
+ Version: 0.0.4
4
4
  Summary: Autonomous pedagogical debugging mentor powered by LangGraph, Rich, and LangSmith
5
5
  Author: Mohamed Elawakey
6
6
  Requires-Python: >=3.10
@@ -12,8 +12,10 @@ src/__init__.py
12
12
  src/cli.py
13
13
  src/config_creator.py
14
14
  src/config_template.py
15
+ src/events.py
15
16
  src/extractor.py
16
17
  src/hook.py
18
+ src/watcher.py
17
19
  src/agent/runner.py
18
20
  src/agent/graph/graph.py
19
21
  src/agent/llm/factory.py
@@ -32,8 +34,9 @@ src/agent/prompts/questioner_prompt.py
32
34
  src/agent/prompts/solution_prompt.py
33
35
  src/agent/state/state.py
34
36
  src/utils/read_yaml.py
35
- test/test_hock.py
37
+ test/test_hook.py
36
38
  ui/__init__.py
39
+ ui/adapter.py
37
40
  ui/app.py
38
41
  ui/bidi.py
39
42
  ui/console.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ socratic = socratic:main
@@ -1,19 +1,31 @@
1
1
  from src.agent.prompts.evaluation_prompt import EvaluationPrompt
2
2
  from src.agent.state.state import SocraticAgentState
3
3
  from src.agent.llm.factory import LLMFactory
4
+ from src.events import EventBus
4
5
 
5
- from ui.views import SocraticUI
6
6
 
7
7
  class EvaluationNode:
8
8
  @staticmethod
9
9
  def evaluation_node(state: SocraticAgentState):
10
+ user_input = state.get("user_input") or (
11
+ state["dialogue_history"][-1]["content"] if state.get("dialogue_history") else ""
12
+ )
13
+
14
+ # Immediate exit bypass: if user typed exit / quit / q
15
+ if state.get("is_solved") or user_input.lower().strip() in ["exit", "quit", "q"]:
16
+ EventBus.emit_evaluator_verdict("SOLVED")
17
+ return {
18
+ "eval_status": "surrendered",
19
+ "is_solved": True,
20
+ }
21
+
10
22
  prompt = EvaluationPrompt.evaluation_prompt(
11
- error_type=state["error_type"],
12
- code_snippet=state["code_snippet"],
13
- root_cause=state["root_cause"],
14
- user_input=state.get("user_input") or state["dialogue_history"][-1]["content"]
23
+ error_type=state["error_type"],
24
+ code_snippet=state["code_snippet"],
25
+ root_cause=state["root_cause"],
26
+ user_input=user_input,
15
27
  )
16
-
28
+
17
29
  response = LLMFactory.get_llm().invoke(prompt)
18
30
  verdict = response.content.strip().upper()
19
31
 
@@ -30,7 +42,7 @@ class EvaluationNode:
30
42
  status = "struggling"
31
43
  is_solved = False
32
44
 
33
- SocraticUI.display_evaluator_verdict(verdict_key)
45
+ EventBus.emit_evaluator_verdict(verdict_key)
34
46
 
35
47
  return {
36
48
  "eval_status": status,
@@ -1,18 +1,21 @@
1
1
  from src.agent.state.state import SocraticAgentState
2
- from ui.views import SocraticUI
3
- from ui.console import console
2
+ from src.events import EventBus
4
3
 
5
4
 
6
5
  class HumanInputNode:
7
6
  @staticmethod
8
7
  def human_input_node(state: SocraticAgentState) -> dict:
9
- user_input = SocraticUI.prompt_user_input()
10
-
11
- if user_input.lower().strip() in ["exit", "quit", "q"]:
12
- console.print("\n[bold yellow]👋 Exiting Socratic debugging session. Keep coding![/bold yellow]\n")
13
- return {"is_solved": True}
8
+ user_input = EventBus.request_user_input()
14
9
 
15
10
  history = list(state.get("dialogue_history", []))
16
11
  history.append({"role": "student", "content": user_input})
17
-
12
+
13
+ if user_input.lower().strip() in ["exit", "quit", "q"]:
14
+ return {
15
+ "user_input": user_input,
16
+ "dialogue_history": history,
17
+ "is_solved": True,
18
+ "eval_status": "surrendered",
19
+ }
20
+
18
21
  return {"user_input": user_input, "dialogue_history": history}
@@ -3,7 +3,7 @@ from src.agent.state.state import SocraticAgentState
3
3
  from src.agent.llm.factory import LLMFactory
4
4
  from src.utils.read_yaml import ReadYaml
5
5
 
6
- from ui.views import SocraticUI
6
+ from src.events import EventBus
7
7
 
8
8
  config = ReadYaml.read_yaml()
9
9
 
@@ -33,7 +33,7 @@ class QuestionerNode:
33
33
 
34
34
  response = LLMFactory.get_llm().invoke(prompt)
35
35
 
36
- SocraticUI.display_mentor_message(
36
+ EventBus.emit_mentor_message(
37
37
  content=response.content,
38
38
  hint_level=current_hint,
39
39
  )
@@ -2,8 +2,7 @@ from src.agent.prompts.solution_prompt import SolutionPrompt
2
2
  from src.agent.state.state import SocraticAgentState
3
3
  from src.agent.llm.factory import LLMFactory
4
4
  from src.utils.read_yaml import ReadYaml
5
-
6
- from ui.views import SocraticUI
5
+ from src.events import EventBus
7
6
 
8
7
  config = ReadYaml.read_yaml()
9
8
 
@@ -25,7 +24,7 @@ class SolutionNode:
25
24
 
26
25
  response = LLMFactory.get_llm().invoke(prompt)
27
26
 
28
- SocraticUI.display_solution(response.content)
27
+ EventBus.emit_solution(response.content)
29
28
 
30
29
  history = list(state.get("dialogue_history", []))
31
30
  history.append({"role": "mentor", "content": response.content})
@@ -2,7 +2,7 @@ from src.extractor import extract_crash_context
2
2
  from src.agent.graph.graph import SocraticGraph
3
3
 
4
4
  def run_socratic_agent(exception_type, exception_value, exception_traceback):
5
- context = extract_crash_context(exception_traceback)
5
+ context = extract_crash_context(exception_traceback, exception_value)
6
6
 
7
7
  initial_state = {
8
8
  "file_path": context["file_path"],
@@ -1,31 +1,29 @@
1
1
  import sys
2
- import os
3
2
  from pathlib import Path
4
3
 
5
- # Add project root to sys.path so modules and UI resolve everywhere
4
+ # Add project root to sys.path so modules resolve everywhere
6
5
  project_root = str(Path(__file__).resolve().parent.parent)
7
6
  if project_root not in sys.path:
8
7
  sys.path.insert(0, project_root)
9
8
 
10
9
  from src.hook import install
11
- from ui.console import console
12
10
 
13
11
 
14
12
  def print_usage():
15
- console.print("""
16
- [bold cyan]🧠 Socratic Debug — Autonomous Pedagogical Debugger[/bold cyan]
13
+ print("""
14
+ 🧠 Socratic Debug — Autonomous Pedagogical Debugger
17
15
 
18
- [bold yellow]Usage:[/bold yellow]
19
- [green]socratic run <script.py>[/green] Run a Python file under Socratic supervision
20
- [green]socratic init[/green] Create a default .socratic.yaml config file
21
- [green]socratic --help[/green] Show this help message
16
+ Usage:
17
+ socratic run <script.py> Run a Python file under Socratic supervision
18
+ socratic init Create a default .socratic.yaml config file
19
+ socratic --help Show this help message
22
20
  """)
23
21
 
24
22
 
25
23
  def run_target_script(script_path: str, script_args: list):
26
24
  resolved_path = Path(script_path).resolve()
27
25
  if not resolved_path.exists():
28
- console.print(f"[bold red]Error:[/bold red] File not found: [yellow]{resolved_path}[/yellow]")
26
+ print(f"Error: File not found: {resolved_path}")
29
27
  sys.exit(1)
30
28
 
31
29
  # Automatically install Socratic hook before user code executes
@@ -50,7 +48,7 @@ def run_target_script(script_path: str, script_args: list):
50
48
  except SystemExit:
51
49
  raise
52
50
  except Exception:
53
- # sys.excepthook will catch this cleanly and launch Socratic GUI!
51
+ # sys.excepthook will catch this cleanly and launch Socratic!
54
52
  sys.excepthook(*sys.exc_info())
55
53
 
56
54
 
@@ -68,19 +66,13 @@ def main():
68
66
  elif command == "init":
69
67
  from src.config_creator import create_config
70
68
  create_config()
71
- console.print("[bold green]✔[/bold green] Created [cyan].socratic.yaml[/cyan] with default settings.")
69
+ print("✔ Created .socratic.yaml with default settings.")
72
70
  sys.exit(0)
73
71
 
74
- elif command == "session":
75
- # Internal command executed inside the dedicated popup window
76
- target_file = sys.argv[2]
77
- extra_args = sys.argv[3:]
78
- run_target_script(target_file, extra_args, force_in_process=True)
79
-
80
72
  elif command == "run":
81
73
  if len(sys.argv) < 3:
82
- console.print("[bold red]Error:[/bold red] Missing script file to run.")
83
- console.print("Example: [green]socratic run app.py[/green]")
74
+ print("Error: Missing script file to run.")
75
+ print("Example: socratic run app.py")
84
76
  sys.exit(1)
85
77
 
86
78
  target_file = sys.argv[2]
@@ -0,0 +1,67 @@
1
+ from typing import Callable, Optional, Any, Dict
2
+
3
+
4
+ class EventBus:
5
+ # Callbacks registered by external UI layers (PyQt6, Webview, CLI, etc.)
6
+ _on_mentor_message: Optional[Callable[[str, int], None]] = None
7
+ _on_user_input_requested: Optional[Callable[[], str]] = None
8
+ _on_evaluator_verdict: Optional[Callable[[str], None]] = None
9
+ _on_solution: Optional[Callable[[str], None]] = None
10
+ _on_crash: Optional[Callable[[Dict[str, Any], Any, Any, Any], None]] = None
11
+
12
+ @classmethod
13
+ def register_handlers(
14
+ cls,
15
+ on_mentor_message: Optional[Callable[[str, int], None]] = None,
16
+ on_user_input_requested: Optional[Callable[[], str]] = None,
17
+ on_evaluator_verdict: Optional[Callable[[str], None]] = None,
18
+ on_solution: Optional[Callable[[str], None]] = None,
19
+ on_crash: Optional[Callable[[Dict[str, Any], Any, Any, Any], None]] = None,
20
+ ):
21
+ """Registers presentation callbacks into the Core event bus."""
22
+ if on_mentor_message:
23
+ cls._on_mentor_message = on_mentor_message
24
+ if on_user_input_requested:
25
+ cls._on_user_input_requested = on_user_input_requested
26
+ if on_evaluator_verdict:
27
+ cls._on_evaluator_verdict = on_evaluator_verdict
28
+ if on_solution:
29
+ cls._on_solution = on_solution
30
+ if on_crash:
31
+ cls._on_crash = on_crash
32
+
33
+ @classmethod
34
+ def emit_mentor_message(cls, content: str, hint_level: int = 1):
35
+ if cls._on_mentor_message:
36
+ cls._on_mentor_message(content, hint_level)
37
+ else:
38
+ # Fallback pure standard output
39
+ print(f"\n[Socratic Mentor (Level {hint_level})]: {content}\n")
40
+
41
+ @classmethod
42
+ def request_user_input(cls) -> str:
43
+ if cls._on_user_input_requested:
44
+ return cls._on_user_input_requested()
45
+ return input("❯❯ Your Response: ").strip()
46
+
47
+ @classmethod
48
+ def emit_evaluator_verdict(cls, verdict: str):
49
+ if cls._on_evaluator_verdict:
50
+ cls._on_evaluator_verdict(verdict)
51
+ else:
52
+ print(f" 🔍 Verdict: {verdict}")
53
+
54
+ @classmethod
55
+ def emit_solution(cls, solution_content: str):
56
+ if cls._on_solution:
57
+ cls._on_solution(solution_content)
58
+ else:
59
+ print(f"\n🏆 Wrap-Up:\n{solution_content}\n")
60
+
61
+ @classmethod
62
+ def emit_crash(cls, context: Dict[str, Any], exc_type: Any, exc_val: Any, exc_tb: Any):
63
+ if cls._on_crash:
64
+ cls._on_crash(context, exc_type, exc_val, exc_tb)
65
+ else:
66
+ print(f"\n💥 {exc_type.__name__}: {exc_val}")
67
+ print(f"📍 {context.get('file_path')}:{context.get('line_number')}\n")
@@ -0,0 +1,72 @@
1
+ import traceback
2
+ from pathlib import Path
3
+
4
+
5
+ def extract_crash_context(exception_traceback, exception_value=None) -> dict:
6
+ # 1. Handle SyntaxError / IndentationError where location is on exception_value
7
+ if isinstance(exception_value, SyntaxError):
8
+ file_path = exception_value.filename or "unknown"
9
+ line_number = exception_value.lineno or 1
10
+ line_content = (exception_value.text or "").strip()
11
+ snippet = get_code_snippet(file_path, line_number)
12
+
13
+ return {
14
+ "file_path": file_path,
15
+ "line_number": line_number,
16
+ "function_name": "<module>",
17
+ "line_content": line_content,
18
+ "code_snippet": snippet,
19
+ }
20
+
21
+ # 2. Handle Runtime Errors via Traceback Frames
22
+ frames = traceback.extract_tb(exception_traceback) if exception_traceback else []
23
+
24
+ # Find the deepest frame that belongs to user code (ignore socratic internal frames)
25
+ socratic_src_dir = str(Path(__file__).resolve().parent)
26
+ chosen_frame = None
27
+
28
+ for frame in reversed(frames):
29
+ if not frame.filename.startswith(socratic_src_dir):
30
+ chosen_frame = frame
31
+ break
32
+
33
+ if chosen_frame is None and frames:
34
+ chosen_frame = frames[-1]
35
+
36
+ if chosen_frame:
37
+ snippet = get_code_snippet(chosen_frame.filename, chosen_frame.lineno)
38
+ return {
39
+ "file_path": chosen_frame.filename,
40
+ "line_number": chosen_frame.lineno,
41
+ "function_name": chosen_frame.name,
42
+ "line_content": chosen_frame.line or "",
43
+ "code_snippet": snippet,
44
+ }
45
+
46
+ return {
47
+ "file_path": "unknown",
48
+ "line_number": 1,
49
+ "function_name": "<unknown>",
50
+ "line_content": "",
51
+ "code_snippet": "",
52
+ }
53
+
54
+
55
+ def get_code_snippet(file_path: str, line_number: int, context_lines: int = 5) -> str:
56
+ try:
57
+ with open(file_path, "r", encoding="utf-8") as f:
58
+ lines = f.readlines()
59
+
60
+ start_lines = max(1, line_number - context_lines)
61
+ end_lines = min(len(lines), line_number + context_lines)
62
+
63
+ snippet_lines = []
64
+ for i in range(start_lines, end_lines + 1):
65
+ prefix = ">" if i == line_number else " "
66
+ line_text = lines[i - 1]
67
+ snippet_lines.append(f"{prefix} {i:3d} | {line_text}")
68
+
69
+ return "".join(snippet_lines)
70
+
71
+ except Exception as e:
72
+ return f"Error getting code snippet: {e}"
@@ -0,0 +1,21 @@
1
+ import sys
2
+ from src.extractor import extract_crash_context
3
+ from src.config_creator import create_config
4
+ from src.events import EventBus
5
+
6
+
7
+ def socratic_excepthook(exception_type, exception_value, exception_traceback):
8
+ """Intercepts uncaught exceptions and delegates to the decoupled EventBus."""
9
+ context = extract_crash_context(exception_traceback, exception_value)
10
+ EventBus.emit_crash(
11
+ context=context,
12
+ exc_type=exception_type,
13
+ exc_val=exception_value,
14
+ exc_tb=exception_traceback,
15
+ )
16
+
17
+
18
+ def install():
19
+ """Installs Socratic as the global system exception hook."""
20
+ create_config()
21
+ sys.excepthook = socratic_excepthook
@@ -0,0 +1,88 @@
1
+ import os
2
+ import time
3
+ import ast
4
+ import subprocess
5
+ import sys
6
+ import threading
7
+
8
+
9
+ class FileMonitor(threading.Thread):
10
+ def __init__(
11
+ self,
12
+ file_path: str,
13
+ target_error: str,
14
+ on_solved_callback,
15
+ on_failed_callback,
16
+ ):
17
+ super().__init__(daemon=True)
18
+ self.file_path = file_path
19
+ self.target_error = target_error.strip()
20
+ self.on_solved_callback = on_solved_callback
21
+ self.on_failed_callback = on_failed_callback
22
+ self.last_mtime = os.path.getmtime(file_path)
23
+ self.running = True
24
+ self._last_reported_err = ""
25
+
26
+ def run(self):
27
+ while self.running:
28
+ time.sleep(0.5)
29
+
30
+ try:
31
+ current_mtime = os.path.getmtime(self.file_path)
32
+ except OSError:
33
+ continue
34
+
35
+ if current_mtime != self.last_mtime:
36
+ self.last_mtime = current_mtime
37
+ time.sleep(1.8)
38
+
39
+ # Skip if user is actively typing and mtime kept updating
40
+ try:
41
+ if os.path.getmtime(self.file_path) != current_mtime:
42
+ continue
43
+ except OSError:
44
+ continue
45
+
46
+ # Quick AST syntax validation (ignore incomplete statements while typing)
47
+ try:
48
+ with open(self.file_path, "r", encoding="utf-8") as file:
49
+ ast.parse(file.read())
50
+ except (SyntaxError, OSError):
51
+ continue
52
+
53
+ # Execute script in silent background subprocess
54
+ try:
55
+ res = subprocess.run(
56
+ [sys.executable, self.file_path],
57
+ capture_output=True,
58
+ text=True,
59
+ timeout=7,
60
+ )
61
+
62
+ # Scenario A: Clean execution with zero errors
63
+ if res.returncode == 0:
64
+ self.on_solved_callback()
65
+ self.running = False
66
+ break
67
+
68
+ # If the script has a SyntaxError, the user is still writing or broke the syntax
69
+ if "SyntaxError" in res.stderr:
70
+ continue
71
+
72
+ # Scenario B: Target error is resolved and code progresses past it
73
+ if self.target_error and self.target_error not in res.stderr:
74
+ # Ensure it's not a generic failure before the target error
75
+ self.on_solved_callback()
76
+ self.running = False
77
+ break
78
+
79
+ # Scenario C: Target error still occurs
80
+ current_err = res.stderr.strip()
81
+ if current_err != self._last_reported_err:
82
+ self._last_reported_err = current_err
83
+ self.on_failed_callback(res.stderr)
84
+
85
+ except subprocess.TimeoutExpired:
86
+ continue
87
+ except Exception:
88
+ continue
@@ -17,7 +17,10 @@ print("*" * 20)
17
17
  number_1 = 10
18
18
  number_2 = 0
19
19
 
20
- print(number_1 / number_2)
20
+ if number_2 == 0:
21
+ print("sorry we can't do it")
22
+ else:
23
+ print(number_1 / number_2)
21
24
 
22
25
  # test a logic error
23
26
  print("make a logic error in the average function")
@@ -0,0 +1,23 @@
1
+ from src.events import EventBus
2
+ from src.agent.runner import run_socratic_agent
3
+ from ui.views import SocraticUI
4
+
5
+
6
+ def register_ui_adapter():
7
+ """Binds the SocraticUI presentation layer to the Core EventBus.
8
+
9
+ This ensures complete decoupling: Core never imports UI.
10
+ """
11
+ EventBus.register_handlers(
12
+ on_mentor_message=SocraticUI.display_mentor_message,
13
+ on_user_input_requested=SocraticUI.prompt_user_input,
14
+ on_evaluator_verdict=SocraticUI.display_evaluator_verdict,
15
+ on_solution=SocraticUI.display_solution,
16
+ on_crash=lambda ctx, etype, evalue, tb: SocraticUI.launch(
17
+ context=ctx,
18
+ exception_type=etype,
19
+ exception_value=evalue,
20
+ exception_traceback=tb,
21
+ agent_runner_func=run_socratic_agent,
22
+ ),
23
+ )
@@ -17,6 +17,8 @@ try:
17
17
  user_input_requested = pyqtSignal()
18
18
  solution_received = pyqtSignal(str)
19
19
  evaluator_verdict_received = pyqtSignal(str)
20
+ live_solution_detected = pyqtSignal()
21
+ live_error_detected = pyqtSignal(str)
20
22
 
21
23
  bridge = UIBridge()
22
24
 
@@ -38,6 +40,8 @@ except (ImportError, OSError):
38
40
  user_input_requested = MockSignal()
39
41
  solution_received = MockSignal()
40
42
  evaluator_verdict_received = MockSignal()
43
+ live_solution_detected = MockSignal()
44
+ live_error_detected = MockSignal()
41
45
 
42
46
  bridge = DummyBridge()
43
47
 
@@ -158,6 +162,8 @@ class SocraticWindow(QMainWindow):
158
162
 
159
163
  self.send_button = QPushButton(self.translations["send_btn"], self)
160
164
  self.send_button.setObjectName("sendButton")
165
+ if Qt is not None:
166
+ self.send_button.setCursor(Qt.CursorShape.PointingHandCursor)
161
167
  self.send_button.clicked.connect(self._on_send_clicked)
162
168
  self.send_button.setEnabled(False)
163
169
 
@@ -170,6 +176,43 @@ class SocraticWindow(QMainWindow):
170
176
  bridge.user_input_requested.connect(self._enable_user_input)
171
177
  bridge.solution_received.connect(self._append_solution)
172
178
  bridge.evaluator_verdict_received.connect(self._append_verdict)
179
+ bridge.live_solution_detected.connect(self._on_live_solution_detected)
180
+ bridge.live_error_detected.connect(self._on_live_error_detected)
181
+
182
+ def _on_live_solution_detected(self):
183
+ self.status_badge.setText(self.translations["status_solved"])
184
+ self.status_badge.setStyleSheet("color: #ffffff; background-color: #238636;")
185
+
186
+ celebration_html = (
187
+ f"<div style='background-color: #162a1c; border: 1px solid #238636; "
188
+ f"border-radius: 8px; padding: 12px; margin: 8px 0; color: #3fb950; font-size: 14px;'>"
189
+ f"<b>{self.translations['live_solved_msg']}</b></div>"
190
+ )
191
+ self.chat_area.append(celebration_html)
192
+ self._scroll_to_bottom()
193
+
194
+ # Switch send button to Close button
195
+ self.input_field.setEnabled(False)
196
+ self.send_button.setEnabled(True)
197
+ self.send_button.setText(self.translations["close_btn"])
198
+ self.send_button.setStyleSheet("background-color: #238636; color: #ffffff;")
199
+ try:
200
+ self.send_button.clicked.disconnect()
201
+ except TypeError:
202
+ pass
203
+ self.send_button.clicked.connect(self.close)
204
+
205
+ # Release background worker thread if waiting for user input
206
+ submit_gui_user_input("q")
207
+
208
+ def _on_live_error_detected(self, error_trace: str):
209
+ retry_html = (
210
+ f"<div style='background-color: #2b1d20; border: 1px solid #da3633; "
211
+ f"border-radius: 8px; padding: 10px; margin: 8px 0; color: #ff7b72; font-size: 13px;'>"
212
+ f"<b>{self.translations['live_retry_msg']}</b></div>"
213
+ )
214
+ self.chat_area.append(retry_html)
215
+ self._scroll_to_bottom()
173
216
 
174
217
  def _append_mentor_message(self, content: str, hint_level: int):
175
218
  html_msg = ChatTemplates.mentor_bubble(
@@ -222,6 +265,17 @@ class SocraticWindow(QMainWindow):
222
265
  self.chat_area.append(sol_html)
223
266
  self._scroll_to_bottom()
224
267
 
268
+ # Switch send button to Close Session button
269
+ self.input_field.setEnabled(False)
270
+ self.send_button.setEnabled(True)
271
+ self.send_button.setText(self.translations["close_btn"])
272
+ self.send_button.setStyleSheet("background-color: #238636; color: #ffffff;")
273
+ try:
274
+ self.send_button.clicked.disconnect()
275
+ except TypeError:
276
+ pass
277
+ self.send_button.clicked.connect(self.close)
278
+
225
279
  def _scroll_to_bottom(self):
226
280
  cursor = self.chat_area.textCursor()
227
281
  cursor.movePosition(QTextCursor.MoveOperation.End)
@@ -13,6 +13,9 @@ TRANSLATIONS = {
13
13
  "hint_level": "مستوى التلميح",
14
14
  "user_tag": "إجابتك",
15
15
  "wrapup_title": "🏆 الحل النهائي وأفضل الممارسات البرمجية",
16
+ "live_solved_msg": "🎉 عاش يا بطل! لقطت إنك عدلت الكود واشتغل بنجاح والمشكلة اتحلت خلاص!",
17
+ "live_retry_msg": "👀 لقطت تعديلك على الكود، بس لسه فيه خطأ بيظهر. راجع التعديل وفكر في النقطة اللي اتكلمنا فيها!",
18
+ "close_btn": "إنهاء الجلسة",
16
19
  },
17
20
  "english": {
18
21
  "app_title": "🧠 Socratic Debug — Autonomous Mentor",
@@ -27,5 +30,8 @@ TRANSLATIONS = {
27
30
  "hint_level": "Hint Level",
28
31
  "user_tag": "Your Response",
29
32
  "wrapup_title": "🏆 Socratic Wrap-Up & Defensive Best Practice",
33
+ "live_solved_msg": "🎉 Awesome! Detected your code change and the script executed successfully with no errors!",
34
+ "live_retry_msg": "👀 Detected your code change, but an error is still occurring. Review the change and keep thinking!",
35
+ "close_btn": "Close Session",
30
36
  }
31
37
  }
@@ -0,0 +1,115 @@
1
+ import os
2
+ import sys
3
+ import threading
4
+ from ui.app import bridge, wait_for_gui_user_input
5
+ from ui.console import console
6
+ from rich.panel import Panel
7
+ from rich.markdown import Markdown
8
+
9
+
10
+ class SocraticUI:
11
+ """Unified UI layer: seamlessly routes to PyQt6 Desktop Terminal or CLI Fallback."""
12
+
13
+ @staticmethod
14
+ def is_gui_active() -> bool:
15
+ return os.environ.get("SOCRATIC_GUI_ACTIVE") == "1"
16
+
17
+ @staticmethod
18
+ def launch(context: dict, exception_type, exception_value, exception_traceback, agent_runner_func):
19
+ """Encapsulates GUI initialization, file monitor, background agent thread, or headless fallback."""
20
+ has_display = bool(os.environ.get("DISPLAY") or os.environ.get("WAYLAND_DISPLAY") or sys.platform == "win32")
21
+
22
+ if has_display:
23
+ try:
24
+ from PyQt6.QtWidgets import QApplication
25
+ from ui.app import SocraticWindow
26
+
27
+ os.environ["SOCRATIC_GUI_ACTIVE"] = "1"
28
+
29
+ app = QApplication.instance()
30
+ if app is None:
31
+ app = QApplication(sys.argv)
32
+
33
+ window = SocraticWindow(
34
+ error_type=exception_type.__name__,
35
+ error_message=str(exception_value),
36
+ file_path=context["file_path"],
37
+ line_number=context["line_number"],
38
+ line_content=context["line_content"],
39
+ )
40
+ window.show()
41
+
42
+ # Launch LangGraph agent in background worker thread
43
+ agent_thread = threading.Thread(
44
+ target=agent_runner_func,
45
+ args=(exception_type, exception_value, exception_traceback),
46
+ daemon=True,
47
+ )
48
+ agent_thread.start()
49
+
50
+ # Launch real-time file monitor if target file exists
51
+ if os.path.exists(context["file_path"]):
52
+ from src.watcher import FileMonitor
53
+ file_monitor = FileMonitor(
54
+ file_path=context["file_path"],
55
+ target_error=exception_type.__name__,
56
+ on_solved_callback=lambda: bridge.live_solution_detected.emit(),
57
+ on_failed_callback=lambda err: bridge.live_error_detected.emit(err),
58
+ )
59
+ file_monitor.start()
60
+
61
+ app.exec()
62
+ return
63
+ except Exception:
64
+ # Fallback gracefully to terminal mode if GUI setup fails
65
+ pass
66
+
67
+ # Headless / Terminal Fallback Mode
68
+ os.environ["SOCRATIC_GUI_ACTIVE"] = "0"
69
+ SocraticUI.display_crash_banner(
70
+ error_type=exception_type.__name__,
71
+ error_message=str(exception_value),
72
+ file_path=context["file_path"],
73
+ line_number=context["line_number"],
74
+ line_content=context["line_content"],
75
+ )
76
+ agent_runner_func(exception_type, exception_value, exception_traceback)
77
+
78
+ @staticmethod
79
+ def display_crash_banner(error_type: str, error_message: str, file_path: str, line_number: int, line_content: str):
80
+ # In CLI fallback mode, print Rich banner
81
+ if not SocraticUI.is_gui_active():
82
+ console.print(f"\n[bold red]💥 {error_type}: {error_message}[/bold red]")
83
+ console.print(f"[dim]📍 {file_path}:{line_number}[/dim]\n")
84
+
85
+ @staticmethod
86
+ def display_mentor_message(content: str, hint_level: int = 1):
87
+ if SocraticUI.is_gui_active():
88
+ bridge.mentor_message_received.emit(content, hint_level)
89
+ else:
90
+ md = Markdown(content)
91
+ panel = Panel(md, title=f"Socratic Mentor (Level {hint_level}/3)", border_style="cyan")
92
+ console.print(panel)
93
+
94
+ @staticmethod
95
+ def prompt_user_input() -> str:
96
+ if SocraticUI.is_gui_active():
97
+ return wait_for_gui_user_input()
98
+ else:
99
+ return console.input("[bold yellow]❯❯ Your Response: [/bold yellow]").strip()
100
+
101
+ @staticmethod
102
+ def display_evaluator_verdict(verdict: str):
103
+ if SocraticUI.is_gui_active():
104
+ bridge.evaluator_verdict_received.emit(verdict)
105
+ else:
106
+ console.print(f" [bold yellow]🔍 {verdict}[/bold yellow]\n")
107
+
108
+ @staticmethod
109
+ def display_solution(solution_content: str):
110
+ if SocraticUI.is_gui_active():
111
+ bridge.solution_received.emit(solution_content)
112
+ else:
113
+ md = Markdown(solution_content)
114
+ panel = Panel(md, title="🏆 Socratic Wrap-Up", border_style="green")
115
+ console.print(panel)
@@ -1,8 +0,0 @@
1
- """
2
- Socratic Debug — Autonomous pedagogical debugging mentor.
3
- """
4
-
5
- from src.hook import install
6
- from src.agent.runner import run_socratic_agent
7
-
8
- __all__ = ["install", "run_socratic_agent"]
@@ -1,2 +0,0 @@
1
- [console_scripts]
2
- socratic = src.cli:main
@@ -1,34 +0,0 @@
1
- import traceback
2
-
3
- def extract_crash_context(exception_traceback) -> dict:
4
- frames = traceback.extract_tb(exception_traceback)
5
- last_frame = frames[-1]
6
-
7
- snippet = get_code_snippet(last_frame.filename, last_frame.lineno)
8
-
9
- return {
10
- "file_path": last_frame.filename,
11
- "line_number": last_frame.lineno,
12
- "function_name": last_frame.name,
13
- "line_content": last_frame.line,
14
- "code_snippet": snippet
15
- }
16
-
17
- def get_code_snippet(file_path: str, line_number: int, context_lines: int = 5) -> str:
18
- try:
19
- with open(file_path, "r", encoding="utf-8") as f:
20
- lines = f.readlines()
21
-
22
- start_lines = max(1, line_number - context_lines)
23
- end_lines = min(len(lines), line_number + context_lines)
24
-
25
- snippet_lines = []
26
- for i in range(start_lines, end_lines + 1):
27
- prefix = ">" if i == line_number else " "
28
- line_text = lines[i - 1]
29
- snippet_lines.append(f"{prefix} {i:3d} | {line_text}")
30
-
31
- return "".join(snippet_lines)
32
-
33
- except Exception as e:
34
- return f"Error getting code snippet: {e}"
@@ -1,67 +0,0 @@
1
- import sys
2
- import os
3
- import threading
4
- from src.extractor import extract_crash_context
5
- from src.config_creator import create_config
6
- from src.agent.runner import run_socratic_agent
7
- from ui.views import SocraticUI
8
-
9
-
10
- def socratic_excepthook(exception_type, exception_value, exception_traceback):
11
- """Intercepts uncaught exceptions and launches the Socratic GUI Terminal window."""
12
- context = extract_crash_context(exception_traceback)
13
-
14
- # Check if graphical desktop display is available
15
- has_display = bool(os.environ.get("DISPLAY") or os.environ.get("WAYLAND_DISPLAY") or sys.platform == "win32")
16
-
17
- if has_display:
18
- try:
19
- from PyQt6.QtWidgets import QApplication
20
- from ui.app import SocraticWindow
21
-
22
- os.environ["SOCRATIC_GUI_ACTIVE"] = "1"
23
-
24
- app = QApplication.instance()
25
- if app is None:
26
- app = QApplication(sys.argv)
27
-
28
- # Create and display the centered Socratic Terminal window
29
- window = SocraticWindow(
30
- error_type=exception_type.__name__,
31
- error_message=str(exception_value),
32
- file_path=context["file_path"],
33
- line_number=context["line_number"],
34
- line_content=context["line_content"],
35
- )
36
- window.show()
37
-
38
- # Run LangGraph State Machine in background worker thread
39
- agent_thread = threading.Thread(
40
- target=run_socratic_agent,
41
- args=(exception_type, exception_value, exception_traceback),
42
- daemon=True,
43
- )
44
- agent_thread.start()
45
-
46
- # Run Qt GUI event loop
47
- app.exec()
48
- return
49
- except Exception as e:
50
- # Fallback to terminal mode if GUI setup fails
51
- pass
52
-
53
- # Headless / Terminal Fallback Mode
54
- os.environ["SOCRATIC_GUI_ACTIVE"] = "0"
55
- SocraticUI.display_crash_banner(
56
- error_type=exception_type.__name__,
57
- error_message=str(exception_value),
58
- file_path=context["file_path"],
59
- line_number=context["line_number"],
60
- line_content=context["line_content"],
61
- )
62
- run_socratic_agent(exception_type, exception_value, exception_traceback)
63
-
64
- # use the hook
65
- def install():
66
- create_config()
67
- sys.excepthook = socratic_excepthook
@@ -1,52 +0,0 @@
1
- import os
2
- from ui.app import bridge, wait_for_gui_user_input
3
- from ui.console import console
4
- from rich.panel import Panel
5
- from rich.markdown import Markdown
6
-
7
-
8
- class SocraticUI:
9
- """Unified UI layer: seamlessly routes to PyQt6 Desktop Terminal or CLI Fallback."""
10
-
11
- @staticmethod
12
- def is_gui_active() -> bool:
13
- return os.environ.get("SOCRATIC_GUI_ACTIVE") == "1"
14
-
15
- @staticmethod
16
- def display_crash_banner(error_type: str, error_message: str, file_path: str, line_number: int, line_content: str):
17
- # In CLI fallback mode, print Rich banner
18
- if not SocraticUI.is_gui_active():
19
- console.print(f"\n[bold red]💥 {error_type}: {error_message}[/bold red]")
20
- console.print(f"[dim]📍 {file_path}:{line_number}[/dim]\n")
21
-
22
- @staticmethod
23
- def display_mentor_message(content: str, hint_level: int = 1):
24
- if SocraticUI.is_gui_active():
25
- bridge.mentor_message_received.emit(content, hint_level)
26
- else:
27
- md = Markdown(content)
28
- panel = Panel(md, title=f"Socratic Mentor (Level {hint_level}/3)", border_style="cyan")
29
- console.print(panel)
30
-
31
- @staticmethod
32
- def prompt_user_input() -> str:
33
- if SocraticUI.is_gui_active():
34
- return wait_for_gui_user_input()
35
- else:
36
- return console.input("[bold yellow]❯❯ Your Response: [/bold yellow]").strip()
37
-
38
- @staticmethod
39
- def display_evaluator_verdict(verdict: str):
40
- if SocraticUI.is_gui_active():
41
- bridge.evaluator_verdict_received.emit(verdict)
42
- else:
43
- console.print(f" [bold yellow]🔍 {verdict}[/bold yellow]\n")
44
-
45
- @staticmethod
46
- def display_solution(solution_content: str):
47
- if SocraticUI.is_gui_active():
48
- bridge.solution_received.emit(solution_content)
49
- else:
50
- md = Markdown(solution_content)
51
- panel = Panel(md, title="🏆 Socratic Wrap-Up", border_style="green")
52
- console.print(panel)
File without changes
File without changes
File without changes