File size: 8,088 Bytes
ceaa691
 
 
 
 
441bbe1
 
ceaa691
 
 
4f26c25
441bbe1
51c2ea7
 
441bbe1
 
 
 
118da3d
441bbe1
 
4f26c25
 
4807519
441bbe1
ceaa691
42e73f2
 
4f26c25
4807519
42e73f2
4f26c25
441bbe1
 
 
 
4f26c25
4807519
4f26c25
 
 
 
 
 
 
4807519
7a6087c
441bbe1
 
 
 
 
 
 
 
 
 
ceaa691
7a6087c
 
118da3d
 
7a6087c
118da3d
7a6087c
 
 
99eddc5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118da3d
 
18453d3
118da3d
18453d3
 
 
 
 
 
99eddc5
118da3d
 
18453d3
 
 
 
 
 
 
 
 
 
 
118da3d
 
 
18453d3
 
 
 
 
 
 
118da3d
99eddc5
118da3d
18453d3
 
441bbe1
 
 
 
 
 
 
 
ceaa691
7a6087c
42e73f2
4f26c25
34bf502
 
441bbe1
34bf502
 
118da3d
 
 
7a6087c
 
 
 
 
 
 
 
 
 
 
441bbe1
118da3d
 
34bf502
 
441bbe1
7a6087c
 
 
 
 
 
 
 
 
 
118da3d
7a6087c
 
 
441bbe1
34bf502
 
441bbe1
7a6087c
34bf502
441bbe1
 
34bf502
 
 
 
118da3d
34bf502
 
441bbe1
2d33bc7
ceaa691
 
 
 
 
4f26c25
ecb34f8
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
#!/usr/bin/env python3
import os
import json
import logging
import re
from typing import Dict, List, Optional
from pathlib import Path
from flask import Flask, request, jsonify
from flask_cors import CORS
from dotenv import load_dotenv
from langchain_groq import ChatGroq
from typing_extensions import TypedDict


# --- Type Definitions ---
class AssistantState(TypedDict):
    conversationSummary: str
    language: str 
    mode: str  # "teacher" or "student"

# --- Logging ---
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger("code-assistant")

# --- Load environment ---
load_dotenv()
GROQ_API_KEY = os.getenv("GROQ_API_KEY")
if not GROQ_API_KEY:
    logger.error("GROQ_API_KEY not set in environment")
    raise RuntimeError("GROQ_API_KEY not set in environment")

# --- Flask app setup ---
BASE_DIR = Path(__file__).resolve().parent
static_folder = BASE_DIR / "static"

app = Flask(__name__, static_folder=str(static_folder), static_url_path="/static")
CORS(app)

# --- LLM setup ---
llm = ChatGroq(
    model=os.getenv("LLM_MODEL", "meta-llama/llama-4-scout-17b-16e-instruct"),
    temperature=0.1,
    max_tokens=2048,
    api_key=GROQ_API_KEY,
)

# --- Helper functions ---

def detect_language_from_text(text: str) -> Optional[str]:
    if not text:
        return None
    lower = text.lower()
    known_languages = ["python", "javascript", "java", "c++", "c#", "go", "ruby", "php", "typescript", "swift"]
    lang_match = re.search(r'\b(in|using|for)\s+(' + '|'.join(known_languages) + r')\b', lower)
    if lang_match:
        return lang_match.group(2).capitalize()
    return None

def update_summary(chat_history: List[Dict[str, str]]) -> str:
    """
    Simple heuristic summary: last 6 messages concatenated.
    Replace with your own summarization chain if desired.
    """
    recent_msgs = chat_history[-6:]
    summary = " | ".join(f"{m['role']}: {m['content'][:50].replace('\n',' ')}" for m in recent_msgs)
    return summary

# def build_system_prompt(language: str, conversation_summary: str, mode: str) -> str:
#     """
#     Build system prompt dynamically based on mode.
#     """
#     base = f"You are a helpful programming assistant. Current language: {language}. Conversation summary: {conversation_summary}\n\n"
    
#     if mode == "student":
#         base += (
#             "You are in STUDENT MODE: Your goal is to engage the user in problem-solving and learning. "
#             "Do NOT give complete answers or full code. Instead:\n"
#             " - Ask guiding questions to make the user think.\n"
#             " - Give hints, small examples, or pseudocode to help the user discover the solution.\n"
#             " - Encourage step-by-step problem solving and curiosity.\n"
#             " - Only provide full solutions as a last resort if the user is completely stuck."
#         )
#     else:  # teacher mode default
#         base += (
#             "You are in TEACHER MODE: Provide detailed suggestions, structured explanations, and full code examples. "
#             "Explain reasoning clearly and comprehensively."
#         )
#     return base
def build_system_prompt(language: str, conversation_summary: str, mode: str) -> str:
    """
    Build system prompt dynamically based on mode, restricting to code/problem-solving only.
    """
    base = (
        f"You are a helpful programming assistant. "
        f"Your sole purpose is to assist with coding, programming, debugging, and technical problem solving. "
        f"Current language: {language}. "
        f"Conversation summary: {conversation_summary}\n\n"
    )

    if mode == "student":
        base += (
            "You are in STUDENT MODE.\n"
            "Your goal is to engage the user ONLY in programming, coding, and problem-solving tasks.\n"
            "STRICT RULES:\n"
            " - ❌ Do NOT give complete answers or full code unless the user is completely stuck.\n"
            " - ❌ Do NOT answer general knowledge, personal, or unrelated questions (e.g., names, trivia, history, etc.).\n"
            " - ❌ Politely refuse any out-of-context or non-programming queries by replying: "
            "\"I'm here only to help with programming or technical problem-solving questions.\"\n"
            " - ✅ ALWAYS guide the user through the problem-solving process instead of directly giving an answer.\n"
            " - ✅ Ask guiding questions to make the user think about coding problems.\n"
            " - ✅ Give hints, small examples, or pseudocode to help the user discover the solution.\n"
            " - ✅ Encourage step-by-step problem solving and curiosity.\n"
        )
    else:  # teacher mode default
        base += (
            "You are in TEACHER MODE.\n"
            "Your goal is to provide detailed explanations, structured reasoning, and complete code examples when needed.\n"
            "STRICT RULES:\n"
            " - ❌ Only answer questions related to programming, coding, or technical problem solving.\n"
            " - ❌ Politely refuse any unrelated, personal, or general knowledge questions by replying: "
            "\"I'm here only to help with programming or technical problem-solving questions.\"\n"
            " - ✅ Provide clear reasoning, best practices, and full working examples for programming tasks.\n"
        )

    return base

    return base
# --- Routes ---

@app.route("/", methods=["GET"])
def serve_frontend():
    try:
        return app.send_static_file("frontend.html")
    except Exception:
        return "<h3>frontend.html not found in static/ — please add your frontend.html there.</h3>", 404

@app.route("/chat", methods=["POST"])
def chat():
    data = request.get_json(force=True)
    chat_history = data.get("chat_history", [])
    assistant_state = data.get("assistant_state", {})

    conversation_summary = assistant_state.get("conversationSummary", "")
    language = assistant_state.get("language", "Python")
    mode = assistant_state.get("mode", "teacher").lower()
    if mode not in ("teacher", "student"):
        mode = "teacher"

    # Detect language from last user message
    last_user_msg = ""
    for msg in reversed(chat_history):
        if msg.get("role") == "user" and msg.get("content"):
            last_user_msg = msg["content"]
            break
    detected_lang = detect_language_from_text(last_user_msg)
    if detected_lang and detected_lang.lower() != language.lower():
        logger.info(f"Detected new language: {detected_lang}")
        language = detected_lang

    # Build system prompt based on mode
    system_prompt = build_system_prompt(language, conversation_summary, mode)
    messages = [{"role": "system", "content": system_prompt}]
    messages.extend(chat_history)

    try:
        llm_response = llm.invoke(messages)
        assistant_reply = llm_response.content if hasattr(llm_response, "content") else str(llm_response)
    except Exception as e:
        logger.exception("LLM invocation failed")
        return jsonify({
            "assistant_reply": "Sorry, the assistant is currently unavailable. Please try again later.",
            "updated_state": {
                "conversationSummary": conversation_summary,
                "language": language,
                "mode": mode,
            },
            "chat_history": chat_history,
        }), 500

    # Append assistant reply to chat history
    chat_history.append({"role": "assistant", "content": assistant_reply})

    # Update conversation summary
    conversation_summary = update_summary(chat_history)

    return jsonify({
        "assistant_reply": assistant_reply,
        "updated_state": {
            "conversationSummary": conversation_summary,
            "language": language,
            "mode": mode,
        },
        "chat_history": chat_history,
    })

@app.route("/ping", methods=["GET"])
def ping():
    return jsonify({"status": "ok"})

if __name__ == "__main__":
    port = int(os.getenv("PORT", 7860))
    app.run(host="0.0.0.0", port=port, debug=True)