File size: 5,452 Bytes
ceaa691
 
 
 
 
441bbe1
 
ceaa691
 
 
4f26c25
441bbe1
2d33bc7
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
 
 
118da3d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
118da3d
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
#!/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: Guide the user on *how to achieve* their programming tasks. "
            "Provide general explanations and pseudocode examples when appropriate. Avoid full detailed code unless necessary."
        )
    else:  # teacher mode default
        base += (
            "You are in TEACHER MODE: Provide detailed suggestions, structured explanations, and full code examples."
        )
    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)