Spaces:
Running
Running
Uploading Files
Browse files- agent.py +165 -0
- app.py +491 -0
- config.py +26 -0
- error_detector.py +92 -0
- llm.py +125 -0
- prompts.py +120 -0
- requirements.txt +15 -0
- sandbox.py +104 -0
- 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)
|