Urvxshhhhh0201 commited on
Commit
217e8ef
Β·
verified Β·
1 Parent(s): 2472157

Uploading Files

Browse files
Files changed (9) hide show
  1. agent.py +165 -0
  2. app.py +491 -0
  3. config.py +26 -0
  4. error_detector.py +92 -0
  5. llm.py +125 -0
  6. prompts.py +120 -0
  7. requirements.txt +15 -0
  8. sandbox.py +104 -0
  9. store.py +137 -0
agent.py ADDED
@@ -0,0 +1,165 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ CodeMentor Automata - Agent & Orchestration (LangGraph Upgraded)
3
+ Implements a state-of-the-art LangGraph ReAct agent bound to DuckDuckGo
4
+ to search unfamiliar libraries and return verified documentation.
5
+ """
6
+ import re
7
+ from ddgs import DDGS
8
+ from langgraph.prebuilt import create_react_agent
9
+ from langchain_community.tools import DuckDuckGoSearchRun
10
+ from langchain_groq import ChatGroq
11
+
12
+ from config import GROQ_API_KEY1, GROQ_MODEL
13
+ from prompts import (
14
+ LEVEL_PROMPTS, JSON_OUTPUT_INSTRUCTIONS, PLAN_PROMPT,
15
+ CLEANUP_PROMPT, OPTIMIZATION_PROMPT, RESOURCE_QUERY_HINT,
16
+ TOPIC_EXTRACTION_PROMPT,
17
+ )
18
+ from llm import call_groq_json
19
+
20
+ _search_tool = DuckDuckGoSearchRun()
21
+ _search_tool.name = "duckduckgo_search" # Required for LangGraph routing
22
+
23
+ # Common stdlib modules we don't bother looking up
24
+ _KNOWN_STDLIB = {
25
+ "os", "sys", "json", "re", "math", "time", "random", "collections",
26
+ "itertools", "functools", "typing", "datetime", "string", "copy",
27
+ }
28
+
29
+
30
+ def _get_llm(temperature=0.2):
31
+ if not GROQ_API_KEY1:
32
+ raise RuntimeError("GROQ_API_KEY not set.")
33
+ return ChatGroq(model=GROQ_MODEL, temperature=temperature, api_key=GROQ_API_KEY1)
34
+
35
+
36
+ def _build_doc_agent():
37
+ """Builds a modern LangGraph ReAct agent."""
38
+ llm = _get_llm()
39
+ tools = [_search_tool]
40
+
41
+ # LangGraph handles the system prompting and ReAct loop automatically
42
+ return create_react_agent(llm, tools)
43
+
44
+
45
+ def _detect_candidate_libraries(code: str) -> list:
46
+ """Heuristic: pull import statements as agent search candidates."""
47
+ imports = re.findall(r"^\s*(?:import|from)\s+([\w\.]+)", code, re.MULTILINE)
48
+ top_level = {imp.split(".")[0] for imp in imports}
49
+ return sorted(top_level - _KNOWN_STDLIB)
50
+
51
+
52
+ def _youtube_thumbnail(url: str):
53
+ match = re.search(r"(?:v=|youtu\.be/)([\w-]{11})", url)
54
+ return f"https://img.youtube.com/vi/{match.group(1)}/hqdefault.jpg" if match else None
55
+
56
+
57
+ def _structured_citations(query: str, max_results: int = 3) -> list:
58
+ """Direct DDGS search β€” real titles + links, so citations are always structured."""
59
+ try:
60
+ with DDGS() as ddgs:
61
+ raw = list(ddgs.text(query, max_results=max_results))
62
+ except Exception:
63
+ return []
64
+ return [
65
+ {
66
+ "title": r.get("title", "Untitled"),
67
+ "url": r.get("href", ""),
68
+ "snippet": (r.get("body", "") or "")[:160],
69
+ "thumbnail": _youtube_thumbnail(r.get("href", "")),
70
+ }
71
+ for r in raw
72
+ ]
73
+
74
+
75
+ def search_documentation(code: str) -> list:
76
+ candidates = _detect_candidate_libraries(code)
77
+ if not candidates:
78
+ return []
79
+
80
+ try:
81
+ agent = _build_doc_agent()
82
+ except RuntimeError as e:
83
+ return [{"library": lib, "summary": f"(doc search unavailable: {e})", "citations": []} for lib in candidates]
84
+
85
+ findings = []
86
+ for lib in candidates:
87
+ try:
88
+ query = (
89
+ f"Search for official documentation on the '{lib}' Python library/module "
90
+ f"and summarize in 2-3 sentences what it's for and the most relevant "
91
+ f"function/API that would show up in a typical code snippet."
92
+ )
93
+ response = agent.invoke({"messages": [("user", query)]})
94
+ summary = response["messages"][-1].content
95
+ except Exception as e:
96
+ summary = f"(doc search failed: {e})"
97
+
98
+ citations = _structured_citations(f"{lib} python library documentation", max_results=3)
99
+ findings.append({"library": lib, "summary": summary, "citations": citations})
100
+
101
+ return findings
102
+
103
+
104
+ def extract_code_topic(code: str, language: str) -> str:
105
+ """
106
+ Identifies the core algorithm/data-structure/concept the code implements
107
+ (e.g. "Dijkstra's Algorithm", "Binary Search"), so resource search can
108
+ target that exact topic instead of just the language name or raw import
109
+ names β€” this is what makes 'Further Resources' show topic-specific
110
+ videos/articles rather than generic language tutorials.
111
+ """
112
+ try:
113
+ result = call_groq_json(TOPIC_EXTRACTION_PROMPT, f"Language: {language}\n\nCode:\n{code}")
114
+ topic = (result or {}).get("topic", "").strip()
115
+ return topic or f"{language} programming"
116
+ except Exception:
117
+ return f"{language} programming"
118
+
119
+
120
+ def find_resources(language: str, topic: str) -> dict:
121
+ """
122
+ Finds learning resources for the CORE TOPIC extracted from the code
123
+ (e.g. "Dijkstra's Algorithm"), not just the language name. A dedicated
124
+ video-biased query runs first β€” a walkthrough video of a named
125
+ algorithm/concept is usually the most useful resource β€” then a broader
126
+ query fills in with articles/docs, skipping anything already found.
127
+ """
128
+ video_query = f"{topic} {language} explained tutorial site:youtube.com"
129
+ article_query = f"{topic} {language} tutorial {RESOURCE_QUERY_HINT}"
130
+
131
+ video_citations = _structured_citations(video_query, max_results=3)
132
+ seen_urls = {c["url"] for c in video_citations}
133
+ article_citations = [
134
+ c for c in _structured_citations(article_query, max_results=4)
135
+ if c["url"] not in seen_urls
136
+ ]
137
+
138
+ citations = video_citations + article_citations
139
+ return {
140
+ "query": video_query,
141
+ "topic": topic,
142
+ "citations": citations,
143
+ "ok": bool(citations),
144
+ }
145
+
146
+
147
+ def generate_breakdown(code: str, language: str, level: str) -> dict:
148
+ system_prompt = LEVEL_PROMPTS[level] + "\n\n" + JSON_OUTPUT_INSTRUCTIONS
149
+ user_content = f"Language: {language}\n\nCode:\n{code}"
150
+ return call_groq_json(system_prompt, user_content)
151
+
152
+
153
+ def generate_plan(code: str, language: str) -> dict:
154
+ user_content = f"Language: {language}\n\nCode:\n{code}"
155
+ return call_groq_json(PLAN_PROMPT, user_content)
156
+
157
+
158
+ def cleanup_code(code: str, language: str) -> dict:
159
+ user_content = f"Language: {language}\n\nCode:\n{code}"
160
+ return call_groq_json(CLEANUP_PROMPT, user_content)
161
+
162
+
163
+ def suggest_optimal_solution(code: str, language: str) -> dict:
164
+ user_content = f"Language: {language}\n\nCode:\n{code}"
165
+ return call_groq_json(OPTIMIZATION_PROMPT, user_content)
app.py ADDED
@@ -0,0 +1,491 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ CodeMentor Automata - Main Gradio App
3
+ """
4
+ import os
5
+ import difflib
6
+ import tempfile
7
+
8
+ import gradio as gr
9
+
10
+ from config import SUPPORTED_LANGUAGES, EXPLANATION_LEVELS
11
+ from llm import call_groq_chat, call_groq_vision_ocr, LLMNotConfiguredError
12
+ from agent import (
13
+ generate_breakdown, generate_plan, cleanup_code,
14
+ suggest_optimal_solution, search_documentation, find_resources,
15
+ extract_code_topic,
16
+ )
17
+ from error_detector import detect_errors
18
+ from sandbox import benchmark_two_versions, verify_fix_equivalence
19
+ from store import get_progress_snapshot
20
+ from prompts import FOLLOWUP_SYSTEM_PROMPT_TEMPLATE
21
+
22
+
23
+ # ---------------------------------------------------------------------------
24
+ # Helpers
25
+ # ---------------------------------------------------------------------------
26
+
27
+ def _validate_inputs(code: str) -> bool:
28
+ if not code or not code.strip():
29
+ gr.Warning("Please paste a code snippet before running an analysis.")
30
+ return False
31
+ return True
32
+
33
+ def _greeting_message(language=None, level=None):
34
+ if language and level:
35
+ text = (
36
+ f"Got it! I've analyzed your **{language}** code at **{level}** level. "
37
+ "Ask me anything about it β€” bugs, alternate approaches, or just \"explain line 5 again\"."
38
+ )
39
+ else:
40
+ text = (
41
+ "πŸ‘‹ Hi, I'm CodeMentor! Head over to the **Analyze Code** tab first and run an analysis β€” "
42
+ "once that's done, come back here and I'll remember the context for follow-up questions."
43
+ )
44
+ return [{"role": "assistant", "content": text}]
45
+
46
+
47
+ def _safe_call(label, fn, *args, **kwargs):
48
+ """Runs fn and never lets an exception escape to the UI as a raw traceback."""
49
+ try:
50
+ return fn(*args, **kwargs), None
51
+ except LLMNotConfiguredError as e:
52
+ return None, f"{label} unavailable β€” {e}"
53
+ except Exception as e:
54
+ return None, f"{label} failed β€” {e}"
55
+
56
+ def _citation_cards_html(citations: list) -> str:
57
+ if not citations:
58
+ return "<p style='opacity:0.7'>No citations found.</p>"
59
+ cards = []
60
+ for i, c in enumerate(citations, start=1):
61
+ thumb = (
62
+ f"<img src='{c['thumbnail']}' style='width:120px;height:68px;object-fit:cover;border-radius:6px;margin-right:12px;'/>"
63
+ if c.get("thumbnail") else
64
+ "<div style='width:120px;height:68px;background:#2a2a2a;border-radius:6px;margin-right:12px;"
65
+ "display:flex;align-items:center;justify-content:center;font-size:22px;'>πŸ”—</div>"
66
+ )
67
+ cards.append(f"""
68
+ <div style='display:flex;align-items:center;padding:10px;border-bottom:1px solid #333;'>
69
+ <div style='min-width:24px;font-weight:bold;opacity:0.6;'>{i}.</div>
70
+ {thumb}
71
+ <div>
72
+ <a href='{c['url']}' target='_blank' style='font-weight:600;text-decoration:none;'>{c['title']}</a>
73
+ <div style='font-size:12px;opacity:0.7;margin-top:2px;'>{c.get('snippet','')}</div>
74
+ </div>
75
+ </div>""")
76
+ return "<div style='max-height:280px;overflow-y:auto;border:1px solid #333;border-radius:8px;'>" + "".join(cards) + "</div>"
77
+
78
+
79
+
80
+ def build_session_report(code, language, level, breakdown_md, analogy_md,
81
+ errors_md, cleanup_md, optimal_md, docs_md, resources_md) -> str:
82
+ report = (
83
+ "# CodeMentor Automata β€” Session Report\n\n"
84
+ f"**Language:** {language} | **Explanation level:** {level}\n\n"
85
+ "## Original Code\n"
86
+ f"```{language}\n{code}\n```\n\n"
87
+ "## Explanation\n"
88
+ f"{breakdown_md}\n\n"
89
+ "## Analogy\n"
90
+ f"{analogy_md}\n\n"
91
+ "## Errors & Weakness Review\n"
92
+ f"{errors_md}\n\n"
93
+ "## Cleanup\n"
94
+ f"{cleanup_md}\n\n"
95
+ "## Optimal Solution\n"
96
+ f"{optimal_md}\n\n"
97
+ "## Documentation Findings\n"
98
+ f"{docs_md}\n\n"
99
+ "## Further Resources\n"
100
+ f"{resources_md}\n"
101
+ )
102
+ tmp_dir = tempfile.mkdtemp(prefix="codementor_report_")
103
+ path = os.path.join(tmp_dir, "codementor_session_report.md")
104
+ with open(path, "w", encoding="utf-8") as f:
105
+ f.write(report)
106
+ return path
107
+
108
+
109
+ # ---------------------------------------------------------------------------
110
+ # Tab 1 callbacks β€” Analyze Code
111
+ # ---------------------------------------------------------------------------
112
+
113
+ def on_generate_plan(code, language):
114
+ if not _validate_inputs(code):
115
+ return gr.update(value="", visible=False), gr.update(visible=False)
116
+
117
+ plan, err = _safe_call("Plan generation", generate_plan, code, language)
118
+ if err:
119
+ gr.Warning(err)
120
+ return gr.update(value="", visible=False), gr.update(visible=False)
121
+
122
+ steps = (plan or {}).get("steps", [])
123
+ plan_md = "**Here's what I'll run:**\n\n" + "\n".join(f"- {s}" for s in steps)
124
+ if (plan or {}).get("estimated_focus"):
125
+ plan_md += f"\n\n*Focus: {plan['estimated_focus']}*"
126
+
127
+ return gr.update(value=plan_md, visible=True), gr.update(visible=True)
128
+
129
+ def on_extract_from_image(image_path):
130
+ if not image_path:
131
+ gr.Warning("Please upload an image first.")
132
+ return gr.update()
133
+ extracted, err = _safe_call("Vision OCR", call_groq_vision_ocr, image_path)
134
+ if err:
135
+ gr.Warning(err)
136
+ return gr.update()
137
+ if not extracted:
138
+ gr.Warning("Couldn't extract any code from that image β€” try a clearer photo.")
139
+ return gr.update()
140
+ gr.Info("Code extracted β€” please review it before running analysis, OCR can misread characters.")
141
+ return gr.update(value=extracted)
142
+
143
+
144
+ def on_cancel_plan():
145
+ return gr.update(value="", visible=False), gr.update(visible=False)
146
+
147
+
148
+ def on_approve_run(code, language, level, progress=gr.Progress()):
149
+ empty_return = ("", "", "", "", "", "", "", None, gr.update(visible=False), gr.update(visible=False))
150
+ if not _validate_inputs(code):
151
+ return empty_return
152
+
153
+ # --- Explanation & breakdown ---
154
+ progress(0.1, desc="Explaining code...")
155
+ breakdown_result, err = _safe_call("Explanation", generate_breakdown, code, language, level)
156
+ if err:
157
+ gr.Warning(err)
158
+ breakdown_items = (breakdown_result or {}).get("breakdown", [])
159
+ breakdown_md = "\n\n".join(
160
+ f"**{item.get('line_or_block', '')}** β€” {item.get('explanation', '')}" for item in breakdown_items
161
+ ) or "_No breakdown available._"
162
+ analogy_md = (breakdown_result or {}).get("analogy") or "_No analogy returned._"
163
+ bugs = (breakdown_result or {}).get("bugs", [])
164
+
165
+ # --- Multi-type error detection & weakness review ---
166
+ progress(0.3, desc="Detecting errors & weaknesses...")
167
+ err_result, err = _safe_call("Error detection", detect_errors, code, language)
168
+ if err:
169
+ gr.Warning(err)
170
+
171
+ content_lines = []
172
+ if err_result:
173
+ if err_result["syntax"]["found"]:
174
+ content_lines.append(f"πŸ”΄ **Syntax error**: {err_result['syntax']['message']}")
175
+ if err_result["runtime"]["found"]:
176
+ content_lines.append(f"πŸ”΄ **Runtime error**: {err_result['runtime']['message']}")
177
+ if err_result["tle"]["found"]:
178
+ content_lines.append(f"🟠 **TLE**: {err_result['tle']['message']}")
179
+ for issue in err_result["logical"]["issues"]:
180
+ content_lines.append(
181
+ f"🟑 **Logical issue**: {issue.get('description', '')}\n ↳ Fix: {issue.get('suggested_fix', '')}"
182
+ )
183
+ for bug in bugs:
184
+ content_lines.append(f"🟑 {bug.get('description', '')}\n ↳ Fix: {bug.get('concrete_fix', '')}")
185
+ if not content_lines:
186
+ content_lines.append("βœ… No errors detected.")
187
+ if err_result.get("weakness_note"):
188
+ content_lines.append(f"\nπŸ“Š **Weakness note**: {err_result['weakness_note']}")
189
+ if not err_result.get("verified_by_execution"):
190
+ content_lines.append("\n_Note: this language is checked via LLM static estimate, not sandboxed execution._")
191
+ elif err:
192
+ content_lines.append(f"⚠️ {err}")
193
+ else:
194
+ content_lines.append("No error data available.")
195
+ errors_md = "\n\n".join(content_lines)
196
+
197
+ # --- Cleanup pass ---
198
+ progress(0.5, desc="Cleaning up code...")
199
+ clean_res, err = _safe_call("Cleanup", cleanup_code, code, language)
200
+ if err:
201
+ gr.Warning(err)
202
+ clean_res = clean_res or {}
203
+ cleaned_code = clean_res.get("cleaned_code", "") or ""
204
+
205
+ cleanup_lines = []
206
+ if clean_res.get("needs_cleanup") and cleaned_code.strip():
207
+ diff = difflib.unified_diff(
208
+ code.splitlines(), cleaned_code.splitlines(),
209
+ fromfile="original", tofile="cleaned", lineterm="",
210
+ )
211
+ diff_text = "\n".join(diff)
212
+ cleanup_lines.append(f"```diff\n{diff_text}\n```" if diff_text else "No line-level differences.")
213
+
214
+ changes = clean_res.get("changes_made", [])
215
+ if changes:
216
+ cleanup_lines.append("**Changes made:**\n" + "\n".join(f"- {c}" for c in changes))
217
+
218
+ if language == "python":
219
+ equivalence, eq_err = _safe_call("Fix verification", verify_fix_equivalence, code, cleaned_code)
220
+ if eq_err:
221
+ cleanup_lines.append(f"⚠️ Could not verify equivalence β€” {eq_err}")
222
+ elif equivalence:
223
+ if equivalence["outputs_match"]:
224
+ cleanup_lines.append("βœ… **Verified by execution**: cleaned code produces identical output to the original.")
225
+ else:
226
+ cleanup_lines.append(
227
+ "⚠️ **Could not confirm equivalence** β€” outputs differ. Please review the cleaned version before trusting it."
228
+ )
229
+ elif err:
230
+ cleanup_lines.append(f"⚠️ {err}")
231
+ else:
232
+ cleanup_lines.append("Code already looks clean β€” no restructuring needed.")
233
+ cleanup_md = "\n\n".join(cleanup_lines)
234
+
235
+ # --- Optimal solution pass ---
236
+ progress(0.7, desc="Checking for a better approach...")
237
+ opt_res, err = _safe_call("Optimization", suggest_optimal_solution, code, language)
238
+ if err:
239
+ gr.Warning(err)
240
+ opt_res = opt_res or {}
241
+
242
+ optimal_lines = []
243
+ cur = opt_res.get("current_complexity", {}) or {}
244
+ structures = opt_res.get("data_structures_used", []) or []
245
+ pattern = opt_res.get("algorithm_pattern", "")
246
+
247
+ if opt_res:
248
+ optimal_lines.append(f"**Time complexity:** {cur.get('time', '?')} | **Space complexity:** {cur.get('space', '?')}")
249
+ if structures:
250
+ optimal_lines.append(f"**Data structures used:** {', '.join(structures)}")
251
+ if pattern:
252
+ optimal_lines.append(f"**Pattern:** {pattern}")
253
+
254
+ if opt_res.get("has_better_approach"):
255
+ sug = opt_res.get("suggested_complexity", {}) or {}
256
+ optimal_lines.append(f"**Suggested complexity:** time {sug.get('time', '?')}, space {sug.get('space', '?')}")
257
+ if opt_res.get("explanation"):
258
+ optimal_lines.append(f"**Why it's better:** {opt_res['explanation']}")
259
+ suggested_code = opt_res.get("suggested_code", "") or ""
260
+ if suggested_code.strip():
261
+ optimal_lines.append(f"```{language}\n{suggested_code}\n```")
262
+ if language == "python":
263
+ bench, bench_err = _safe_call("Benchmark", benchmark_two_versions, code, suggested_code)
264
+ if bench_err:
265
+ optimal_lines.append(f"⚠️ Could not measure runtime β€” {bench_err}")
266
+ elif bench:
267
+ optimal_lines.append(
268
+ f"**Measured runtime** β€” original: {bench['original_runtime']}s "
269
+ f"({'ok' if bench['original_ok'] else 'failed/TLE'}), "
270
+ f"suggested: {bench['optimized_runtime']}s "
271
+ f"({'ok' if bench['optimized_ok'] else 'failed/TLE'})"
272
+ )
273
+ if bench.get("speedup"):
274
+ optimal_lines.append(f"πŸš€ Speedup: {bench['speedup']}x")
275
+ else:
276
+ optimal_lines.append(f"**Why this is already optimal:** {opt_res.get('explanation', 'No further improvement possible.')}")
277
+ elif err:
278
+ optimal_lines.append(f"⚠️ {err}")
279
+ else:
280
+ optimal_lines.append("Optimization analysis unavailable.")
281
+ optimal_md = "\n\n".join(optimal_lines)
282
+
283
+
284
+ # --- Documentation search ---
285
+ progress(0.85, desc="Searching documentation...")
286
+ doc_findings, err = _safe_call("Documentation search", search_documentation, code)
287
+ if err:
288
+ docs_html = f"<p>⚠️ {err}</p>"
289
+ elif doc_findings:
290
+ blocks = []
291
+ for d in doc_findings:
292
+ blocks.append(f"<h4>{d['library']}</h4><p>{d['summary']}</p>")
293
+ blocks.append(_citation_cards_html(d.get("citations", [])))
294
+ docs_html = "".join(blocks)
295
+ else:
296
+ docs_html = "<p>No unfamiliar libraries detected β€” nothing to look up.</p>"
297
+
298
+ # --- Resource finder (topic-based, not just language-based) ---
299
+ progress(0.9, desc="Identifying core topic...")
300
+ topic, err = _safe_call("Topic extraction", extract_code_topic, code, language)
301
+ topic = topic or f"{language} programming"
302
+
303
+ progress(0.95, desc=f"Finding resources on {topic}...")
304
+ resources, err = _safe_call("Resource search", find_resources, language, topic)
305
+ if err:
306
+ resources_html = f"<p>⚠️ {err}</p>"
307
+ elif resources and resources.get("ok"):
308
+ topic_header = (
309
+ f"<p style='opacity:0.75;font-size:13px;margin-bottom:8px;'>"
310
+ f"πŸ“Œ Topic identified: <strong>{topic}</strong></p>"
311
+ )
312
+ resources_html = topic_header + _citation_cards_html(resources.get("citations", []))
313
+ else:
314
+ resources_html = f"<p>No resources found for <strong>{topic}</strong> this time β€” try again in a moment.</p>"
315
+
316
+
317
+ # --- Downloadable report ---
318
+ report_path = None
319
+ try:
320
+ report_path = build_session_report(
321
+ code, language, level, breakdown_md, analogy_md,
322
+ errors_md, cleanup_md, optimal_md, docs_html, resources_html,
323
+ )
324
+ except Exception as e:
325
+ gr.Warning(f"Could not build downloadable report: {e}")
326
+
327
+ progress(1.0, desc="Done!")
328
+ return (
329
+ breakdown_md, analogy_md, errors_md, cleanup_md, optimal_md,
330
+ docs_html, resources_html, report_path,
331
+ gr.update(visible=False), gr.update(visible=False),
332
+ )
333
+
334
+
335
+ # ---------------------------------------------------------------------------
336
+ # Tab 2 callbacks β€” Chat / Follow-up
337
+ # ---------------------------------------------------------------------------
338
+
339
+ def on_chat_send(message, chat_history, last_code, last_language, last_level):
340
+ chat_history = chat_history or []
341
+ if not message or not message.strip():
342
+ return chat_history, ""
343
+ if not last_code:
344
+ gr.Warning("Analyze a code snippet in the 'Analyze Code' tab first so I have context for follow-ups.")
345
+ chat_history = chat_history + [
346
+ {"role": "user", "content": message},
347
+ {"role": "assistant", "content": "⚠️ I don't have any code to discuss yet β€” please run an analysis in the **Analyze Code** tab first."},
348
+ ]
349
+ return chat_history, ""
350
+
351
+ system_prompt = FOLLOWUP_SYSTEM_PROMPT_TEMPLATE.format(
352
+ level=last_level or "Intermediate",
353
+ language=last_language or "python",
354
+ code=last_code,
355
+ )
356
+ api_history = chat_history
357
+
358
+ try:
359
+ reply = call_groq_chat(system_prompt, api_history, message)
360
+ except LLMNotConfiguredError as e:
361
+ reply = f"⚠️ {e}"
362
+ except Exception as e:
363
+ reply = f"⚠️ Something went wrong answering that: {e}"
364
+
365
+ chat_history = chat_history + [
366
+ {"role": "user", "content": message},
367
+ {"role": "assistant", "content": reply},
368
+ ]
369
+ return chat_history, ""
370
+ # ---------------------------------------------------------------------------
371
+ # Tab 3 callbacks β€” My Progress
372
+ # ---------------------------------------------------------------------------
373
+
374
+ def on_refresh_progress():
375
+ try:
376
+ return get_progress_snapshot()
377
+ except Exception as e:
378
+ gr.Warning(f"Could not refresh progress: {e}")
379
+ return gr.update(), gr.update(), gr.update()
380
+
381
+ # ---------------------------------------------------------------------------
382
+ # UI
383
+ # ---------------------------------------------------------------------------
384
+
385
+ with gr.Blocks(title="CodeMentor Automata") as demo:
386
+ gr.Markdown("# πŸ§‘β€πŸ’» CodeMentor Automata\nYour personal, execution-verified coding coach.")
387
+
388
+ state_last_code = gr.State("")
389
+ _initial_table, _initial_chart, _initial_summary = get_progress_snapshot()
390
+ state_last_language = gr.State(SUPPORTED_LANGUAGES[0])
391
+ state_last_level = gr.State(EXPLANATION_LEVELS[0])
392
+
393
+ with gr.Tab("Analyze Code"):
394
+ with gr.Row():
395
+ with gr.Column(scale=2):
396
+ code_input = gr.Textbox(label="Paste your code", lines=16, placeholder="Paste your code here, or upload a screenshot below…")
397
+ with gr.Accordion("πŸ“Έ Or upload a code screenshot", open=False):
398
+ image_input = gr.Image(label="Code screenshot", type="filepath", height=240)
399
+ extract_btn = gr.Button("πŸ” Extract Code from Image", variant="secondary")
400
+ with gr.Column(scale=1):
401
+ language_dd = gr.Dropdown(SUPPORTED_LANGUAGES, value=SUPPORTED_LANGUAGES[0], label="Language")
402
+ level_dd = gr.Dropdown(EXPLANATION_LEVELS, value=EXPLANATION_LEVELS[0], label="Explanation level")
403
+ plan_btn = gr.Button("1. Preview Plan", variant="primary")
404
+
405
+ plan_output = gr.Markdown(visible=False)
406
+ with gr.Row(visible=False) as plan_actions:
407
+ approve_btn = gr.Button("βœ… Approve & Run Analysis", variant="primary")
408
+ cancel_btn = gr.Button("❌ Cancel")
409
+
410
+ with gr.Accordion("πŸ“– Breakdown", open=True):
411
+ breakdown_out = gr.Markdown()
412
+ with gr.Accordion("πŸ’‘ Analogy", open=False):
413
+ analogy_out = gr.Markdown()
414
+ with gr.Accordion("🐞 Errors & Weakness Review", open=True):
415
+ errors_out = gr.Markdown()
416
+ with gr.Accordion("🧹 Cleanup Diff", open=False):
417
+ cleanup_out = gr.Markdown()
418
+ with gr.Accordion("⚑ Optimal Solution", open=False):
419
+ optimal_out = gr.Markdown()
420
+ with gr.Accordion("πŸ“š Documentation Findings", open=False):
421
+ docs_out = gr.HTML()
422
+ with gr.Accordion("πŸ”— Further Resources", open=False):
423
+ resources_out = gr.HTML()
424
+
425
+ report_file = gr.File(label="Download session report")
426
+
427
+ with gr.Tab("Chat / Follow-up"):
428
+ gr.Markdown("Ask follow-up questions about the code you analyzed β€” I'll remember the context.")
429
+ chatbot = gr.Chatbot(label="CodeMentor", height=400, value=_greeting_message())
430
+ chat_input = gr.Textbox(label="Your question", placeholder="e.g. why did you suggest that fix?")
431
+ chat_send_btn = gr.Button("Send", variant="primary")
432
+
433
+ with gr.Tab("My Progress"):
434
+ gr.Markdown("Your language-wise revision history and skill breakdown.")
435
+ refresh_btn = gr.Button("πŸ”„ Refresh")
436
+ revision_table = gr.Dataframe(
437
+ headers=["Language", "Submissions", "Syntax Errors", "Logical Errors", "TLE Count", "Last Seen"],
438
+ value=_initial_table,
439
+ interactive=False,
440
+ )
441
+ skill_pie = gr.Plot(value=_initial_chart)
442
+ skill_summary = gr.Markdown(value=_initial_summary)
443
+
444
+ # ------------------------------ Event wiring ------------------------------
445
+
446
+ plan_btn.click(
447
+ on_generate_plan,
448
+ inputs=[code_input, language_dd],
449
+ outputs=[plan_output, plan_actions],
450
+ )
451
+
452
+ cancel_btn.click(
453
+ on_cancel_plan,
454
+ outputs=[plan_output, plan_actions],
455
+ )
456
+
457
+ extract_btn.click(
458
+ on_extract_from_image,
459
+ inputs=[image_input],
460
+ outputs=[code_input],
461
+ )
462
+
463
+ approve_btn.click(
464
+ on_approve_run,
465
+ inputs=[code_input, language_dd, level_dd],
466
+ outputs=[
467
+ breakdown_out, analogy_out, errors_out, cleanup_out,
468
+ optimal_out, docs_out, resources_out, report_file,
469
+ plan_output, plan_actions,
470
+ ],
471
+ ).then(
472
+ lambda c, l, lv: (c, l, lv, _greeting_message(language=l, level=lv)),
473
+ inputs=[code_input, language_dd, level_dd],
474
+ outputs=[state_last_code, state_last_language, state_last_level, chatbot],
475
+ )
476
+
477
+ chat_send_btn.click(
478
+ on_chat_send,
479
+ inputs=[chat_input, chatbot, state_last_code, state_last_language, state_last_level],
480
+ outputs=[chatbot, chat_input],
481
+ )
482
+ chat_input.submit(
483
+ on_chat_send,
484
+ inputs=[chat_input, chatbot, state_last_code, state_last_language, state_last_level],
485
+ outputs=[chatbot, chat_input],
486
+ )
487
+
488
+ refresh_btn.click(on_refresh_progress, outputs=[revision_table, skill_pie, skill_summary])
489
+
490
+ if __name__ == "__main__":
491
+ demo.launch()
config.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ CodeMentor Automata - Configuration
3
+ Central place for API keys, model names, and constants.
4
+ """
5
+ import os
6
+ import matplotlib
7
+ from dotenv import load_dotenv
8
+
9
+ load_dotenv() # .env file ko actual environment mein load karta hai
10
+ # ---- Groq LLM ----
11
+ # Support either env var name so existing deployments (GROQ_API_KEY1) and the
12
+ # conventional name (GROQ_API_KEY, used internally by langchain_groq) both work.
13
+ GROQ_API_KEY1 = os.environ.get("GROQ_API_KEY1")
14
+ GROQ_MODEL = os.environ.get("GROQ_MODEL", "openai/gpt-oss-120b") # llama-3.3-70b-versatile deprecated by Groq (2026-06-17)
15
+ GROQ_VISION_MODEL = os.environ.get("GROQ_VISION_MODEL", "qwen/qwen3.6-27b")
16
+
17
+ # ---- Sandbox execution ----
18
+ TLE_THRESHOLD_SECONDS = float(os.environ.get("TLE_THRESHOLD_SECONDS", "3.0"))
19
+ SANDBOX_TIMEOUT_SECONDS = float(os.environ.get("SANDBOX_TIMEOUT_SECONDS", "5.0"))
20
+
21
+ # ---- Persistent store ----
22
+ _BASE_DIR = os.path.dirname(os.path.abspath(__file__))
23
+ STORE_PATH = os.environ.get("STORE_PATH", os.path.join(_BASE_DIR, "data", "progress.json"))
24
+
25
+ SUPPORTED_LANGUAGES = ["python", "javascript", "java", "cpp"]
26
+ EXPLANATION_LEVELS = ["Beginner", "Intermediate", "Expert"]
error_detector.py ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ CodeMentor Automata - Multi-type Error Detection
3
+ """
4
+ from sandbox import run_python_sandboxed
5
+ from store import record_submission, get_weakness_note
6
+ from llm import call_groq_json
7
+ from prompts import LOGICAL_ERROR_PROMPT, STATIC_ANALYSIS_PROMPT
8
+
9
+
10
+ def detect_errors(code: str, language: str) -> dict:
11
+ result = {
12
+ "syntax": {"found": False, "message": ""},
13
+ "runtime": {"found": False, "message": ""},
14
+ "tle": {"found": False, "message": "", "runtime_seconds": None},
15
+ "logical": {"found": False, "issues": []},
16
+ "weakness_note": "",
17
+ "verified_by_execution": False,
18
+ }
19
+
20
+ syntax_err = False
21
+ tle_err = False
22
+
23
+ if language == "python":
24
+ exec_result = run_python_sandboxed(code)
25
+ result["verified_by_execution"] = True
26
+ result["tle"]["runtime_seconds"] = exec_result.runtime_seconds
27
+
28
+ if exec_result.error_type == "syntax":
29
+ result["syntax"] = {"found": True, "message": exec_result.error_message}
30
+ syntax_err = True
31
+ elif exec_result.error_type == "runtime":
32
+ result["runtime"] = {"found": True, "message": exec_result.error_message}
33
+ elif exec_result.error_type == "tle":
34
+ result["tle"] = {
35
+ "found": True,
36
+ "message": exec_result.error_message,
37
+ "runtime_seconds": exec_result.runtime_seconds,
38
+ }
39
+ tle_err = True
40
+ else:
41
+ # No sandbox available for this language yet -> hybrid LLM static estimate.
42
+ try:
43
+ static_result = call_groq_json(
44
+ STATIC_ANALYSIS_PROMPT, f"Language: {language}\n\nCode:\n{code}"
45
+ )
46
+ if static_result.get("syntax_error_found"):
47
+ result["syntax"] = {
48
+ "found": True,
49
+ "message": static_result.get(
50
+ "syntax_error_message",
51
+ "Potential syntax issue detected (unverified β€” static estimate).",
52
+ ),
53
+ }
54
+ syntax_err = True
55
+ else:
56
+ result["syntax"]["message"] = (
57
+ f"No obvious syntax issues found for {language} "
58
+ "(unverified β€” static LLM estimate, not real execution)."
59
+ )
60
+
61
+ if static_result.get("runtime_error_found"):
62
+ result["runtime"] = {
63
+ "found": True,
64
+ "message": static_result.get("runtime_error_message", ""),
65
+ }
66
+
67
+ if static_result.get("tle_risk_found"):
68
+ result["tle"] = {
69
+ "found": True,
70
+ "message": static_result.get(
71
+ "tle_risk_message",
72
+ "Potential performance risk (unverified β€” static estimate).",
73
+ ),
74
+ "runtime_seconds": None,
75
+ }
76
+ tle_err = True
77
+ except Exception as e:
78
+ result["syntax"]["message"] = f"{language} static analysis unavailable: {e}"
79
+
80
+ logical_err = False
81
+ if not syntax_err:
82
+ try:
83
+ llm_result = call_groq_json(LOGICAL_ERROR_PROMPT, f"Language: {language}\n\nCode:\n{code}")
84
+ if llm_result.get("logical_errors_found"):
85
+ logical_err = True
86
+ result["logical"] = {"found": True, "issues": llm_result.get("issues", [])}
87
+ except Exception as e:
88
+ result["logical"] = {"found": False, "issues": [], "error": str(e)}
89
+
90
+ record_submission(language, code, syntax_error=syntax_err, logical_error=logical_err, tle=tle_err)
91
+ result["weakness_note"] = get_weakness_note(language)
92
+ return result
llm.py ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ CodeMentor Automata - Groq LLM wrapper.
3
+ Handles API connection and bulletproof JSON parsing.
4
+ """
5
+ import json
6
+ from groq import Groq
7
+ from config import GROQ_API_KEY1, GROQ_MODEL
8
+ import base64
9
+ from config import GROQ_API_KEY1, GROQ_MODEL, GROQ_VISION_MODEL
10
+
11
+ _client = None
12
+
13
+
14
+ class LLMNotConfiguredError(RuntimeError):
15
+ pass
16
+
17
+
18
+ def _get_client() -> Groq:
19
+ global _client
20
+ if _client is None:
21
+ if not GROQ_API_KEY1:
22
+ raise LLMNotConfiguredError("GROQ_API_KEY is not set.")
23
+ _client = Groq(api_key=GROQ_API_KEY1)
24
+ return _client
25
+
26
+ def call_groq_vision_ocr(image_path: str) -> str:
27
+ """
28
+ Image ko Groq ke vision model ko bhejta hai aur usme jo bhi code dikh raha
29
+ hai, usse exactly waisa hi transcribe karwata hai (OCR). Caller (app.py)
30
+ ki zimmedari hai ki user ko yeh review karne de before trusting it β€”
31
+ vision OCR characters galat padh sakta hai.
32
+ """
33
+ client = _get_client()
34
+ with open(image_path, "rb") as f:
35
+ b64_image = base64.b64encode(f.read()).decode("utf-8")
36
+
37
+ response = client.chat.completions.create(
38
+ model=GROQ_VISION_MODEL,
39
+ temperature=0.0,
40
+ messages=[
41
+ {
42
+ "role": "user",
43
+ "content": [
44
+ {
45
+ "type": "text",
46
+ "text": (
47
+ "Transcribe ONLY the source code visible in this image, exactly as written. "
48
+ "Preserve indentation, line breaks, and symbols precisely. "
49
+ "Do not explain, describe, or add commentary β€” output the raw code only, "
50
+ "no markdown code fences."
51
+ ),
52
+ },
53
+ {"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{b64_image}"}},
54
+ ],
55
+ }
56
+ ],
57
+ )
58
+ return response.choices[0].message.content.strip()
59
+
60
+ def call_groq_text(system_prompt: str, user_content: str, temperature: float = 0.3) -> str:
61
+ """Plain text completion, single turn."""
62
+ client = _get_client()
63
+ response = client.chat.completions.create(
64
+ model=GROQ_MODEL,
65
+ temperature=temperature,
66
+ messages=[
67
+ {"role": "system", "content": system_prompt},
68
+ {"role": "user", "content": user_content},
69
+ ],
70
+ )
71
+ return response.choices[0].message.content
72
+
73
+
74
+ def call_groq_chat(system_prompt: str, history: list, user_content: str, temperature: float = 0.4) -> str:
75
+ """
76
+ Multi-turn completion for follow-up conversations.
77
+ `history` is a list of either {"role": ..., "content": ...} dicts (possibly
78
+ with extra keys like Gradio's 'metadata') or (role, content) tuples.
79
+ """
80
+ client = _get_client()
81
+ messages = [{"role": "system", "content": system_prompt}]
82
+ for turn in history:
83
+ if isinstance(turn, dict):
84
+ role = turn.get("role")
85
+ content = turn.get("content")
86
+ else:
87
+ role, content = turn
88
+ if role and content:
89
+ messages.append({"role": role, "content": content}) # sirf role+content, kuch aur nahi
90
+ messages.append({"role": "user", "content": user_content})
91
+
92
+ response = client.chat.completions.create(
93
+ model=GROQ_MODEL,
94
+ temperature=temperature,
95
+ messages=messages,
96
+ )
97
+ return response.choices[0].message.content
98
+
99
+
100
+ def call_groq_json(system_prompt: str, user_content: str, temperature: float = 0.2) -> dict:
101
+ """JSON-mode completion with fallback parsing."""
102
+ client = _get_client()
103
+ response = client.chat.completions.create(
104
+ model=GROQ_MODEL,
105
+ temperature=temperature,
106
+ response_format={"type": "json_object"},
107
+ messages=[
108
+ {
109
+ "role": "system",
110
+ "content": system_prompt + "\n\nRespond ONLY with valid JSON. No preamble, no markdown fences.",
111
+ },
112
+ {"role": "user", "content": user_content},
113
+ ],
114
+ )
115
+ raw = response.choices[0].message.content
116
+ try:
117
+ return json.loads(raw)
118
+ except json.JSONDecodeError:
119
+ cleaned = raw.strip().strip("`")
120
+ if cleaned.lower().startswith("json"):
121
+ cleaned = cleaned[4:].strip()
122
+ try:
123
+ return json.loads(cleaned)
124
+ except json.JSONDecodeError:
125
+ return {} # Safe fallback to prevent raw tracebacks in UI
prompts.py ADDED
@@ -0,0 +1,120 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ CodeMentor Automata - Prompt Library
3
+ """
4
+
5
+ JSON_OUTPUT_INSTRUCTIONS = """
6
+ Respond ONLY with a JSON object in exactly this shape:
7
+ {
8
+ "breakdown": [
9
+ {"line_or_block": "...", "explanation": "..."}
10
+ ],
11
+ "analogy": "...",
12
+ "bugs": [
13
+ {"description": "...", "concrete_fix": "..."}
14
+ ]
15
+ }
16
+ """
17
+
18
+ BEGINNER_SYSTEM_PROMPT = """You are CodeMentor, a patient teacher explaining code to a beginner.
19
+ Explain what EVERY line does in plain language. Use ONE central everyday analogy (cooking, library, etc.).
20
+ Do NOT mention Big-O notation or complex idioms. Keep sentences short."""
21
+
22
+ INTERMEDIATE_SYSTEM_PROMPT = """You are CodeMentor, explaining code to a developer comfortable with basics.
23
+ Group code into logical blocks. Name CS concepts (recursion, hashing) where they apply.
24
+ Use an analogy that maps to a technical parallel (e.g. comparing cache to a cheat-sheet). Mention Big-O briefly."""
25
+
26
+ EXPERT_SYSTEM_PROMPT = """You are CodeMentor, giving a senior-engineer code review.
27
+ Focus on architectural decisions, time/space complexity precisely, and subtle correctness issues (race conditions, memory leaks).
28
+ Skip basic mechanics entirely."""
29
+
30
+ LEVEL_PROMPTS = {
31
+ "Beginner": BEGINNER_SYSTEM_PROMPT,
32
+ "Intermediate": INTERMEDIATE_SYSTEM_PROMPT,
33
+ "Expert": EXPERT_SYSTEM_PROMPT,
34
+ }
35
+
36
+ PLAN_PROMPT = """You are CodeMentor's planning module. Given a code snippet, produce a short plan.
37
+ Respond ONLY with JSON:
38
+ {
39
+ "steps": ["Run sandbox for errors", "Cleanup pass", "Optimization pass", "Doc search"],
40
+ "estimated_focus": "one short sentence naming the most useful thing this analysis will surface"
41
+ }"""
42
+
43
+ CLEANUP_PROMPT = """You are CodeMentor's cleanup module.
44
+ NEVER change the logic. Only rename variables, fix indentation, and restructure for readability.
45
+ Respond ONLY with JSON:
46
+ {
47
+ "needs_cleanup": true,
48
+ "cleaned_code": "...",
49
+ "changes_made": ["bullet points"]
50
+ }"""
51
+
52
+ OPTIMIZATION_PROMPT = """You are CodeMentor's optimization module.
53
+ You must ALWAYS report the time and space complexity of the given code, and name the
54
+ data structures and algorithmic pattern it uses β€” do this every single time, even if
55
+ the code is already optimal.
56
+
57
+ Then decide if a better approach exists:
58
+ - If yes, provide the improved code and explain why it's better.
59
+ - If no, still explain WHY it's already optimal (e.g. "O(n) is optimal here because
60
+ every element must be read at least once") instead of a bare statement.
61
+
62
+ Respond ONLY with JSON in exactly this shape:
63
+ {
64
+ "current_complexity": {"time": "O(...)", "space": "O(...)"},
65
+ "data_structures_used": ["array", "hash map"],
66
+ "algorithm_pattern": "short label, e.g. brute force / two-pointer / dynamic programming / greedy",
67
+ "has_better_approach": true,
68
+ "suggested_code": "",
69
+ "suggested_complexity": {"time": "O(...)", "space": "O(...)"},
70
+ "explanation": "reasoning for improvement OR reasoning for why current is already optimal"
71
+ }"""
72
+
73
+ LOGICAL_ERROR_PROMPT = """You are a meticulous code reviewer looking ONLY for LOGICAL errors (off-by-one, wrong bounds, bad edge cases).
74
+ Respond ONLY with JSON:
75
+ {
76
+ "logical_errors_found": true,
77
+ "issues": [{"description": "...", "suggested_fix": "..."}]
78
+ }"""
79
+
80
+ STATIC_ANALYSIS_PROMPT = """You are a meticulous static code analyzer for a language where sandboxed execution is not available (e.g. C++, Java, JavaScript).
81
+ Read the code carefully as a compiler/interpreter would, WITHOUT running it, and look for:
82
+ 1. Syntax errors β€” anything that would fail to compile or parse.
83
+ 2. Obvious runtime errors β€” null/undefined dereference, out-of-bounds access, division by zero, unhandled exceptions.
84
+ 3. Time-limit risk β€” nested loops with high complexity, obviously unbounded loops, or unbounded recursion.
85
+
86
+ Be conservative: only flag something if you are reasonably confident, since this is an unverified static estimate, not an actual execution.
87
+
88
+ Respond ONLY with JSON in exactly this shape:
89
+ {
90
+ "syntax_error_found": false,
91
+ "syntax_error_message": "",
92
+ "runtime_error_found": false,
93
+ "runtime_error_message": "",
94
+ "tle_risk_found": false,
95
+ "tle_risk_message": ""
96
+ }"""
97
+
98
+ TOPIC_EXTRACTION_PROMPT = """You are identifying the SINGLE core topic that this code snippet is built around, so that
99
+ a learner could search for videos/articles about that exact topic β€” not the programming language, not the imported
100
+ libraries, but the actual algorithm, data structure, or concept the code implements.
101
+
102
+ Examples of good topics: "Dijkstra's Shortest Path Algorithm", "Binary Search", "Merge Sort", "AVL Tree Rotation",
103
+ "Dynamic Programming - Longest Common Subsequence", "Two Pointer Technique", "Depth-First Search on a Graph",
104
+ "Producer-Consumer Pattern with Threads".
105
+
106
+ Do NOT answer with just the language name (e.g. "Python") or a generic phrase like "coding" or "programming" unless
107
+ the code is truly too trivial/generic to have a specific named concept (e.g. a simple print statement or a basic
108
+ for-loop with no notable pattern) β€” in that case fall back to a short, honest generic description.
109
+
110
+ Respond ONLY with JSON in exactly this shape:
111
+ {
112
+ "topic": "short, specific, searchable name of the core concept",
113
+ "confidence": "high"
114
+ }"""
115
+
116
+ FOLLOWUP_SYSTEM_PROMPT_TEMPLATE = """You are CodeMentor, continuing a conversation. Match explanation level: {level}.
117
+ Original snippet ({language}):
118
+ {code}"""
119
+
120
+ RESOURCE_QUERY_HINT = "site:geeksforgeeks.org OR site:realpython.com OR docs"
requirements.txt ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ groq
2
+ langchain
3
+ langchain-community
4
+ langchain-groq
5
+ langchain-huggingface
6
+ duckduckgo-search
7
+ gradio
8
+ chromadb
9
+ sentence-transformers
10
+ langgraph
11
+ matplotlib
12
+ pandas
13
+ ddgs
14
+ plotly
15
+ python-dotenv
sandbox.py ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ CodeMentor Automata - Sandbox Executor
3
+ """
4
+ import os, subprocess, sys, tempfile, time
5
+ from dataclasses import dataclass
6
+ from typing import Optional
7
+ from config import SANDBOX_TIMEOUT_SECONDS, TLE_THRESHOLD_SECONDS
8
+
9
+
10
+ @dataclass
11
+ class ExecutionResult:
12
+ ok: bool
13
+ stdout: str = ""
14
+ stderr: str = ""
15
+ runtime_seconds: float = 0.0
16
+ error_type: Optional[str] = None
17
+ error_message: str = ""
18
+
19
+
20
+ def check_syntax(code: str) -> ExecutionResult:
21
+ try:
22
+ compile(code, "<submitted_code>", "exec")
23
+ return ExecutionResult(ok=True)
24
+ except SyntaxError as e:
25
+ return ExecutionResult(ok=False, error_type="syntax", error_message=f"Line {e.lineno}: {e.msg}")
26
+
27
+
28
+ def run_python_sandboxed(code: str, stdin_input: str = "") -> ExecutionResult:
29
+ syntax_check = check_syntax(code)
30
+ if not syntax_check.ok:
31
+ return syntax_check
32
+
33
+ tmp_path = None
34
+ try:
35
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
36
+ f.write(code)
37
+ tmp_path = f.name
38
+
39
+ start = time.monotonic()
40
+ try:
41
+ proc = subprocess.run(
42
+ [sys.executable, tmp_path],
43
+ input=stdin_input,
44
+ capture_output=True,
45
+ text=True,
46
+ timeout=SANDBOX_TIMEOUT_SECONDS,
47
+ )
48
+ except subprocess.TimeoutExpired:
49
+ elapsed = time.monotonic() - start
50
+ return ExecutionResult(ok=False, error_type="tle", error_message="Sandbox timeout exceeded.", runtime_seconds=elapsed)
51
+
52
+ elapsed = time.monotonic() - start
53
+ if proc.returncode != 0:
54
+ last_line = proc.stderr.strip().splitlines()[-1] if proc.stderr.strip() else "Unknown runtime error"
55
+ return ExecutionResult(ok=False, stdout=proc.stdout, stderr=proc.stderr, runtime_seconds=elapsed, error_type="runtime", error_message=last_line)
56
+
57
+ is_tle = elapsed > TLE_THRESHOLD_SECONDS
58
+ return ExecutionResult(
59
+ ok=not is_tle,
60
+ stdout=proc.stdout,
61
+ stderr=proc.stderr,
62
+ runtime_seconds=elapsed,
63
+ error_type="tle" if is_tle else None,
64
+ error_message=(f"Ran in {elapsed:.2f}s, over {TLE_THRESHOLD_SECONDS}s threshold." if is_tle else ""),
65
+ )
66
+ finally:
67
+ if tmp_path:
68
+ try:
69
+ os.unlink(tmp_path)
70
+ except OSError:
71
+ pass
72
+
73
+
74
+ def benchmark_two_versions(original_code: str, optimized_code: str) -> dict:
75
+ original = run_python_sandboxed(original_code)
76
+ optimized = run_python_sandboxed(optimized_code)
77
+ speedup = None
78
+ if optimized.ok and original.ok and optimized.runtime_seconds > 0:
79
+ speedup = round(original.runtime_seconds / optimized.runtime_seconds, 2)
80
+ return {
81
+ "original_runtime": round(original.runtime_seconds, 4),
82
+ "optimized_runtime": round(optimized.runtime_seconds, 4),
83
+ "original_ok": original.ok,
84
+ "optimized_ok": optimized.ok,
85
+ "speedup": speedup,
86
+ }
87
+
88
+
89
+ def verify_fix_equivalence(original_code: str, fixed_code: str, stdin_input: str = "") -> dict:
90
+ """
91
+ Runs the original and a fixed/cleaned version with the same input and
92
+ compares stdout, to confirm a cleanup/fix pass didn't change behaviour
93
+ (Python only β€” this is Feature 8's 'hard rule' and Feature 13's reuse).
94
+ """
95
+ original = run_python_sandboxed(original_code, stdin_input)
96
+ fixed = run_python_sandboxed(fixed_code, stdin_input)
97
+ outputs_match = fixed.ok and (original.stdout == fixed.stdout)
98
+ return {
99
+ "original_ok": original.ok,
100
+ "fixed_ok": fixed.ok,
101
+ "outputs_match": outputs_match,
102
+ "original_stdout": original.stdout,
103
+ "fixed_stdout": fixed.stdout,
104
+ }
store.py ADDED
@@ -0,0 +1,137 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ CodeMentor Automata - Vector Store & Semantic Memory
3
+ Uses ChromaDB for persistent semantic memory. Implements Skill breakdown (pie chart).
4
+ """
5
+ import os, uuid
6
+ from datetime import date
7
+ from typing import List
8
+ import plotly.graph_objects as go
9
+
10
+ import matplotlib
11
+ matplotlib.use("Agg") # must happen before pyplot is imported anywhere in the process
12
+
13
+ import chromadb
14
+ from chromadb.config import Settings
15
+ from langchain_huggingface import HuggingFaceEmbeddings
16
+
17
+ from config import STORE_PATH
18
+
19
+ _db_dir = os.path.dirname(STORE_PATH) or "data"
20
+ os.makedirs(_db_dir, exist_ok=True)
21
+
22
+ _chroma_client = chromadb.PersistentClient(path=_db_dir, settings=Settings(anonymized_telemetry=False))
23
+ _embedding_function = HuggingFaceEmbeddings(model_name="all-MiniLM-L6-v2")
24
+ _memory_collection = _chroma_client.get_or_create_collection(name="user_error_memory", metadata={"hnsw:space": "cosine"})
25
+
26
+
27
+ def _get_embeddings(texts: List[str]) -> List[List[float]]:
28
+ return _embedding_function.embed_documents(texts)
29
+
30
+
31
+ def record_submission(language: str, code: str, syntax_error: bool, logical_error: bool, tle: bool) -> dict:
32
+ doc_id = str(uuid.uuid4())
33
+ metadata = {
34
+ "language": language,
35
+ "syntax_error": int(syntax_error),
36
+ "logical_error": int(logical_error),
37
+ "tle": int(tle),
38
+ "date": date.today().isoformat(),
39
+ }
40
+ _memory_collection.add(ids=[doc_id], documents=[code], embeddings=_get_embeddings([code]), metadatas=[metadata])
41
+ return metadata
42
+
43
+
44
+ def get_weakness_note(language: str) -> str:
45
+ results = _memory_collection.get(where={"language": language}, include=["metadatas"])
46
+ metadatas = results.get("metadatas", [])
47
+ if not metadatas or len(metadatas) < 2:
48
+ return f"Not enough history yet for {language} to determine a weakness pattern."
49
+
50
+ counts = {
51
+ "Syntax": sum(m.get("syntax_error", 0) for m in metadatas),
52
+ "Logical": sum(m.get("logical_error", 0) for m in metadatas),
53
+ "TLE": sum(m.get("tle", 0) for m in metadatas),
54
+ }
55
+ total_errors = sum(counts.values())
56
+ if total_errors == 0:
57
+ return f"Clean track record across {len(metadatas)} submissions in {language}!"
58
+
59
+ weakest = max(counts, key=counts.get)
60
+ pct = round(100 * counts[weakest] / total_errors)
61
+ return f"Across {len(metadatas)} {language} submissions, {pct}% of your errors are {weakest} errors. Focus on this."
62
+
63
+
64
+ def _fetch_all_metadatas():
65
+ """Single ChromaDB read β€” reused by table/chart/summary to avoid 3x round-trips."""
66
+ results = _memory_collection.get(include=["metadatas"])
67
+ return results.get("metadatas", []) or []
68
+
69
+
70
+ def _build_revision_table(metadatas: list) -> list:
71
+ stats = {}
72
+ for m in metadatas:
73
+ lang = m.get("language")
74
+ if not lang:
75
+ continue
76
+ if lang not in stats:
77
+ stats[lang] = {"submissions": 0, "syntax_errors": 0, "logical_errors": 0, "tle_count": 0, "last_seen": m.get("date", "")}
78
+ stats[lang]["submissions"] += 1
79
+ stats[lang]["syntax_errors"] += m.get("syntax_error", 0)
80
+ stats[lang]["logical_errors"] += m.get("logical_error", 0)
81
+ stats[lang]["tle_count"] += m.get("tle", 0)
82
+ stats[lang]["last_seen"] = max(stats[lang]["last_seen"], m.get("date", ""))
83
+ return [
84
+ [lang, rec["submissions"], rec["syntax_errors"], rec["logical_errors"], rec["tle_count"], rec["last_seen"]]
85
+ for lang, rec in sorted(stats.items())
86
+ ]
87
+
88
+
89
+ def _build_skill_pie_chart(metadatas: list):
90
+ total = max(len(metadatas), 1)
91
+ mastery = {
92
+ "Syntax Mastery": max(total - sum(m.get("syntax_error", 0) for m in metadatas), 0),
93
+ "Logic Mastery": max(total - sum(m.get("logical_error", 0) for m in metadatas), 0),
94
+ "Performance Mastery": max(total - sum(m.get("tle", 0) for m in metadatas), 0),
95
+ }
96
+ labels = list(mastery.keys())
97
+ values = list(mastery.values())
98
+ pull = [0.08 if v == max(values) else 0 for v in values]
99
+
100
+ fig = go.Figure(data=[go.Pie(
101
+ labels=labels, values=values, pull=pull, hole=0.35,
102
+ marker=dict(colors=["#6C63FF", "#00C2A8", "#FF6B6B"], line=dict(color="#1e1e1e", width=2)),
103
+ textinfo="label+percent", hoverinfo="label+value",
104
+ )])
105
+ fig.update_layout(
106
+ title="Your Skill Ability Breakdown", showlegend=True,
107
+ margin=dict(t=60, b=20, l=20, r=20), paper_bgcolor="rgba(0,0,0,0)",
108
+ )
109
+ return fig
110
+
111
+
112
+ def _build_skill_summary(metadatas: list) -> str:
113
+ if not metadatas:
114
+ return "No submissions yet β€” analyze some code to build your skill profile."
115
+ total = len(metadatas)
116
+ counts = {
117
+ "Syntax": sum(m.get("syntax_error", 0) for m in metadatas),
118
+ "Logical": sum(m.get("logical_error", 0) for m in metadatas),
119
+ "Performance (TLE)": sum(m.get("tle", 0) for m in metadatas),
120
+ }
121
+ languages = sorted({m.get("language") for m in metadatas if m.get("language")})
122
+ if sum(counts.values()) == 0:
123
+ return f"πŸŽ‰ **Summary:** Clean record across {total} submissions in {', '.join(languages)} β€” no recurring weak area yet."
124
+ weakest = max(counts, key=counts.get)
125
+ return (
126
+ f"πŸ“Š **Summary:** {total} submissions across **{', '.join(languages)}**. "
127
+ f"Your most frequent issue type is **{weakest}** ({counts[weakest]} occurrences). "
128
+ f"Focus your next few practice sessions there for the fastest improvement."
129
+ )
130
+
131
+
132
+ def get_progress_snapshot():
133
+ """ONE DB read, returns (table, chart, summary) together. This is the only
134
+ function app.py should call β€” used both for the first static render and
135
+ for the manual Refresh button, so there's never a redundant scan."""
136
+ metadatas = _fetch_all_metadatas()
137
+ return _build_revision_table(metadatas), _build_skill_pie_chart(metadatas), _build_skill_summary(metadatas)