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.
- {socratic_debug-0.0.2/socratic_debug.egg-info → socratic_debug-0.0.4}/PKG-INFO +1 -1
- {socratic_debug-0.0.2 → socratic_debug-0.0.4}/pyproject.toml +2 -2
- socratic_debug-0.0.4/socratic.py +28 -0
- {socratic_debug-0.0.2 → socratic_debug-0.0.4/socratic_debug.egg-info}/PKG-INFO +1 -1
- {socratic_debug-0.0.2 → socratic_debug-0.0.4}/socratic_debug.egg-info/SOURCES.txt +4 -1
- socratic_debug-0.0.4/socratic_debug.egg-info/entry_points.txt +2 -0
- {socratic_debug-0.0.2 → socratic_debug-0.0.4}/src/agent/nodes/evaluate_node.py +19 -7
- {socratic_debug-0.0.2 → socratic_debug-0.0.4}/src/agent/nodes/human_input_node.py +11 -8
- {socratic_debug-0.0.2 → socratic_debug-0.0.4}/src/agent/nodes/questioner_node.py +2 -2
- {socratic_debug-0.0.2 → socratic_debug-0.0.4}/src/agent/nodes/solution_node.py +2 -3
- {socratic_debug-0.0.2 → socratic_debug-0.0.4}/src/agent/runner.py +1 -1
- {socratic_debug-0.0.2 → socratic_debug-0.0.4}/src/cli.py +12 -20
- socratic_debug-0.0.4/src/events.py +67 -0
- socratic_debug-0.0.4/src/extractor.py +72 -0
- socratic_debug-0.0.4/src/hook.py +21 -0
- socratic_debug-0.0.4/src/watcher.py +88 -0
- socratic_debug-0.0.2/test/test_hock.py → socratic_debug-0.0.4/test/test_hook.py +4 -1
- socratic_debug-0.0.4/ui/adapter.py +23 -0
- {socratic_debug-0.0.2 → socratic_debug-0.0.4}/ui/app.py +54 -0
- {socratic_debug-0.0.2 → socratic_debug-0.0.4}/ui/translations.py +6 -0
- socratic_debug-0.0.4/ui/views.py +115 -0
- socratic_debug-0.0.2/socratic.py +0 -8
- socratic_debug-0.0.2/socratic_debug.egg-info/entry_points.txt +0 -2
- socratic_debug-0.0.2/src/extractor.py +0 -34
- socratic_debug-0.0.2/src/hook.py +0 -67
- socratic_debug-0.0.2/ui/views.py +0 -52
- {socratic_debug-0.0.2 → socratic_debug-0.0.4}/LICENSE +0 -0
- {socratic_debug-0.0.2 → socratic_debug-0.0.4}/README.md +0 -0
- {socratic_debug-0.0.2 → socratic_debug-0.0.4}/setup.cfg +0 -0
- {socratic_debug-0.0.2 → socratic_debug-0.0.4}/socratic_debug.egg-info/dependency_links.txt +0 -0
- {socratic_debug-0.0.2 → socratic_debug-0.0.4}/socratic_debug.egg-info/requires.txt +0 -0
- {socratic_debug-0.0.2 → socratic_debug-0.0.4}/socratic_debug.egg-info/top_level.txt +0 -0
- {socratic_debug-0.0.2 → socratic_debug-0.0.4}/src/__init__.py +0 -0
- {socratic_debug-0.0.2 → socratic_debug-0.0.4}/src/agent/graph/graph.py +0 -0
- {socratic_debug-0.0.2 → socratic_debug-0.0.4}/src/agent/llm/factory.py +0 -0
- {socratic_debug-0.0.2 → socratic_debug-0.0.4}/src/agent/llm/providers/__init__.py +0 -0
- {socratic_debug-0.0.2 → socratic_debug-0.0.4}/src/agent/llm/providers/ollama.py +0 -0
- {socratic_debug-0.0.2 → socratic_debug-0.0.4}/src/agent/llm/providers/openrouter.py +0 -0
- {socratic_debug-0.0.2 → socratic_debug-0.0.4}/src/agent/nodes/diagnose_node.py +0 -0
- {socratic_debug-0.0.2 → socratic_debug-0.0.4}/src/agent/prompts/diagnose_prompt.py +0 -0
- {socratic_debug-0.0.2 → socratic_debug-0.0.4}/src/agent/prompts/evaluation_prompt.py +0 -0
- {socratic_debug-0.0.2 → socratic_debug-0.0.4}/src/agent/prompts/mentor_prompt.py +0 -0
- {socratic_debug-0.0.2 → socratic_debug-0.0.4}/src/agent/prompts/questioner_prompt.py +0 -0
- {socratic_debug-0.0.2 → socratic_debug-0.0.4}/src/agent/prompts/solution_prompt.py +0 -0
- {socratic_debug-0.0.2 → socratic_debug-0.0.4}/src/agent/state/state.py +0 -0
- {socratic_debug-0.0.2 → socratic_debug-0.0.4}/src/config_creator.py +0 -0
- {socratic_debug-0.0.2 → socratic_debug-0.0.4}/src/config_template.py +0 -0
- {socratic_debug-0.0.2 → socratic_debug-0.0.4}/src/utils/read_yaml.py +0 -0
- {socratic_debug-0.0.2 → socratic_debug-0.0.4}/ui/__init__.py +0 -0
- {socratic_debug-0.0.2 → socratic_debug-0.0.4}/ui/bidi.py +0 -0
- {socratic_debug-0.0.2 → socratic_debug-0.0.4}/ui/console.py +0 -0
- {socratic_debug-0.0.2 → socratic_debug-0.0.4}/ui/styles.py +0 -0
- {socratic_debug-0.0.2 → socratic_debug-0.0.4}/ui/templates.py +0 -0
- {socratic_debug-0.0.2 → socratic_debug-0.0.4}/ui/window.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "socratic-debug"
|
|
7
|
-
version = "0.0.
|
|
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 = "
|
|
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"]
|
|
@@ -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/
|
|
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
|
|
@@ -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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
16
|
-
|
|
13
|
+
print("""
|
|
14
|
+
🧠 Socratic Debug — Autonomous Pedagogical Debugger
|
|
17
15
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
83
|
-
|
|
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
|
-
|
|
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)
|
socratic_debug-0.0.2/socratic.py
DELETED
|
@@ -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}"
|
socratic_debug-0.0.2/src/hook.py
DELETED
|
@@ -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
|
socratic_debug-0.0.2/ui/views.py
DELETED
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|