Update app.py
Browse files
app.py
CHANGED
|
@@ -36,19 +36,18 @@ if not GROQ_API_KEY:
|
|
| 36 |
# raise ValueError("GROQ_API_KEY not set in environment")
|
| 37 |
exit(1)
|
| 38 |
|
| 39 |
-
# --- Flask app setup
|
| 40 |
BASE_DIR = Path(__file__).resolve().parent
|
| 41 |
static_folder = BASE_DIR / "static"
|
| 42 |
|
| 43 |
-
# The 'app' object MUST be defined before its first use, e.g., in @app.route
|
| 44 |
app = Flask(__name__, static_folder=str(static_folder), static_url_path="/static")
|
| 45 |
CORS(app)
|
|
|
|
| 46 |
# --- LLM setup ---
|
| 47 |
-
# Using a model that's good for coding tasks
|
| 48 |
llm = ChatGroq(
|
| 49 |
-
model=os.getenv("LLM_MODEL", "meta-llama/llama-4-scout-17b-16e-instruct"),
|
| 50 |
-
temperature=0,
|
| 51 |
-
|
| 52 |
api_key=GROQ_API_KEY,
|
| 53 |
)
|
| 54 |
|
|
@@ -60,35 +59,39 @@ You are an expert programming assistant. Your role is to provide code suggestion
|
|
| 60 |
- **Language Adaptation:** Adjust your suggestions, code, and explanations to the programming language specified in the 'language' field of the 'AssistantState'.
|
| 61 |
|
| 62 |
STRICT OUTPUT FORMAT (JSON ONLY):
|
| 63 |
-
Return a single JSON object with the following keys
|
| 64 |
-
- assistant_reply: string //
|
|
|
|
| 65 |
- state_updates: object // updates to the internal state, must include: language, conversationSummary
|
| 66 |
- suggested_tags: array of strings // a list of 1-3 relevant tags for the assistant_reply
|
| 67 |
|
| 68 |
Rules:
|
|
|
|
| 69 |
- ALWAYS include `assistant_reply` as a non-empty string.
|
| 70 |
-
-
|
| 71 |
-
- Do NOT produce any text outside the JSON object.
|
| 72 |
-
- Be concise in the non-code parts of `assistant_reply`.
|
| 73 |
"""
|
| 74 |
|
| 75 |
-
|
| 76 |
def extract_json_from_llm_response(raw_response: str) -> dict:
|
| 77 |
default = {
|
| 78 |
-
"assistant_reply": "I'm sorry
|
|
|
|
| 79 |
"state_updates": {"conversationSummary": "", "language": "Python"},
|
| 80 |
"suggested_tags": [],
|
| 81 |
}
|
| 82 |
-
|
| 83 |
if not raw_response or not isinstance(raw_response, str):
|
| 84 |
return default
|
| 85 |
|
| 86 |
-
|
|
|
|
| 87 |
json_string = m.group(1).strip() if m else raw_response
|
| 88 |
|
|
|
|
| 89 |
first = json_string.find('{')
|
| 90 |
last = json_string.rfind('}')
|
| 91 |
candidate = json_string[first:last+1] if first != -1 and last != -1 and first < last else json_string
|
|
|
|
|
|
|
| 92 |
candidate = re.sub(r',\s*(?=[}\]])', '', candidate)
|
| 93 |
|
| 94 |
try:
|
|
@@ -97,11 +100,18 @@ def extract_json_from_llm_response(raw_response: str) -> dict:
|
|
| 97 |
logger.warning("Failed to parse JSON from LLM output: %s. Candidate: %s", e, candidate)
|
| 98 |
return default
|
| 99 |
|
| 100 |
-
|
|
|
|
|
|
|
| 101 |
parsed.setdefault("state_updates", {})
|
| 102 |
parsed["state_updates"].setdefault("conversationSummary", "")
|
| 103 |
parsed["state_updates"].setdefault("language", "Python")
|
| 104 |
parsed.setdefault("suggested_tags", [])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
return parsed
|
| 106 |
else:
|
| 107 |
logger.warning("Parsed JSON missing 'assistant_reply' or invalid format. Returning default.")
|
|
@@ -120,7 +130,7 @@ def detect_language_from_text(text: str) -> Optional[str]:
|
|
| 120 |
return None
|
| 121 |
|
| 122 |
# --- Flask routes ---
|
| 123 |
-
@app.route("/", methods=["GET"])
|
| 124 |
def serve_frontend():
|
| 125 |
try:
|
| 126 |
return app.send_static_file("frontend.html")
|
|
@@ -133,7 +143,6 @@ def chat():
|
|
| 133 |
if not isinstance(data, dict):
|
| 134 |
return jsonify({"error": "invalid request body"}), 400
|
| 135 |
|
| 136 |
-
# chat_history now receives the full conversation history from the corrected frontend
|
| 137 |
chat_history: List[Dict[str, str]] = data.get("chat_history") or []
|
| 138 |
assistant_state: AssistantState = data.get("assistant_state") or {}
|
| 139 |
|
|
@@ -164,15 +173,11 @@ def chat():
|
|
| 164 |
state["language"] = detected_lang
|
| 165 |
|
| 166 |
# 3. Inject Contextual Hint and State into the LAST user message
|
| 167 |
-
# This ensures the LLM has immediate access to the *summarized* history and current language.
|
| 168 |
context_hint = f"Current Language: {state['language']}. Conversation Summary so far: {state['conversationSummary']}"
|
| 169 |
|
| 170 |
-
# Update the content of the last message in llm_messages
|
| 171 |
if llm_messages and llm_messages[-1]["role"] == "user":
|
| 172 |
-
# Overwrite the last user message to include the context hint
|
| 173 |
llm_messages[-1]["content"] = f"USER MESSAGE: {last_user_message}\n\n[CONTEXT HINT: {context_hint}]"
|
| 174 |
elif last_user_message:
|
| 175 |
-
# Should not happen with the corrected frontend, but handles fresh start gracefully
|
| 176 |
llm_messages.append({"role": "user", "content": f"USER MESSAGE: {last_user_message}\n\n[CONTEXT HINT: {context_hint}]"})
|
| 177 |
|
| 178 |
|
|
@@ -180,13 +185,12 @@ def chat():
|
|
| 180 |
logger.info("Invoking LLM with full history and prepared prompt...")
|
| 181 |
llm_response = llm.invoke(llm_messages)
|
| 182 |
raw_response = llm_response.content if hasattr(llm_response, "content") else str(llm_response)
|
| 183 |
-
|
| 184 |
logger.info(f"Raw LLM response: {raw_response}")
|
| 185 |
parsed_result = extract_json_from_llm_response(raw_response)
|
| 186 |
|
| 187 |
except Exception as e:
|
| 188 |
logger.exception("LLM invocation failed")
|
| 189 |
-
# CRITICAL FIX: The Groq model might still be the problem if environment is inconsistent.
|
| 190 |
error_detail = str(e)
|
| 191 |
if 'decommissioned' in error_detail:
|
| 192 |
error_detail = "LLM Model Error: The model is likely decommissioned. Please check the 'LLM_MODEL' environment variable or the default model in app.py."
|
|
@@ -195,19 +199,28 @@ def chat():
|
|
| 195 |
# 4. State Update from LLM
|
| 196 |
updated_state_from_llm = parsed_result.get("state_updates", {})
|
| 197 |
|
| 198 |
-
# CRUCIAL: Update state with the NEW summary generated by the LLM
|
| 199 |
if 'conversationSummary' in updated_state_from_llm:
|
| 200 |
state["conversationSummary"] = updated_state_from_llm["conversationSummary"]
|
| 201 |
if 'language' in updated_state_from_llm:
|
| 202 |
state["language"] = updated_state_from_llm["language"]
|
| 203 |
|
| 204 |
assistant_reply = parsed_result.get("assistant_reply")
|
| 205 |
-
|
| 206 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 207 |
|
| 208 |
-
# 5. Final Response Payload
|
| 209 |
response_payload = {
|
| 210 |
-
"assistant_reply":
|
| 211 |
"updated_state": state,
|
| 212 |
"suggested_tags": parsed_result.get("suggested_tags", []),
|
| 213 |
}
|
|
|
|
| 36 |
# raise ValueError("GROQ_API_KEY not set in environment")
|
| 37 |
exit(1)
|
| 38 |
|
| 39 |
+
# --- Flask app setup ---
|
| 40 |
BASE_DIR = Path(__file__).resolve().parent
|
| 41 |
static_folder = BASE_DIR / "static"
|
| 42 |
|
|
|
|
| 43 |
app = Flask(__name__, static_folder=str(static_folder), static_url_path="/static")
|
| 44 |
CORS(app)
|
| 45 |
+
|
| 46 |
# --- LLM setup ---
|
|
|
|
| 47 |
llm = ChatGroq(
|
| 48 |
+
model=os.getenv("LLM_MODEL", "meta-llama/llama-4-scout-17b-16e-instruct"),
|
| 49 |
+
temperature=0.1, # Set a lower, deterministic temperature
|
| 50 |
+
max_tokens=2048, # Ensure max_tokens is set to avoid truncation
|
| 51 |
api_key=GROQ_API_KEY,
|
| 52 |
)
|
| 53 |
|
|
|
|
| 59 |
- **Language Adaptation:** Adjust your suggestions, code, and explanations to the programming language specified in the 'language' field of the 'AssistantState'.
|
| 60 |
|
| 61 |
STRICT OUTPUT FORMAT (JSON ONLY):
|
| 62 |
+
Return a single JSON object with the following keys. **The JSON object MUST be enclosed in a single ```json block.**
|
| 63 |
+
- assistant_reply: string // A natural language reply to the user (short and helpful). Do NOT include code blocks here.
|
| 64 |
+
- code_snippet: string // If suggesting code, provide it here in a markdown code block (e.g., ```python\\nprint('Hello')\\n```). If no code is required, use an empty string: "".
|
| 65 |
- state_updates: object // updates to the internal state, must include: language, conversationSummary
|
| 66 |
- suggested_tags: array of strings // a list of 1-3 relevant tags for the assistant_reply
|
| 67 |
|
| 68 |
Rules:
|
| 69 |
+
- ALWAYS include all four top-level keys: `assistant_reply`, `code_snippet`, `state_updates`, and `suggested_tags`.
|
| 70 |
- ALWAYS include `assistant_reply` as a non-empty string.
|
| 71 |
+
- Do NOT produce any text outside the JSON block.
|
|
|
|
|
|
|
| 72 |
"""
|
| 73 |
|
|
|
|
| 74 |
def extract_json_from_llm_response(raw_response: str) -> dict:
|
| 75 |
default = {
|
| 76 |
+
"assistant_reply": "I'm sorry, I couldn't process the response correctly. Could you please rephrase?",
|
| 77 |
+
"code_snippet": "",
|
| 78 |
"state_updates": {"conversationSummary": "", "language": "Python"},
|
| 79 |
"suggested_tags": [],
|
| 80 |
}
|
| 81 |
+
|
| 82 |
if not raw_response or not isinstance(raw_response, str):
|
| 83 |
return default
|
| 84 |
|
| 85 |
+
# Use a non-greedy regex to find the JSON content inside the first code block
|
| 86 |
+
m = re.search(r"```json\s*([\s\S]*?)\s*```", raw_response)
|
| 87 |
json_string = m.group(1).strip() if m else raw_response
|
| 88 |
|
| 89 |
+
# Further refine candidate to just the JSON object content
|
| 90 |
first = json_string.find('{')
|
| 91 |
last = json_string.rfind('}')
|
| 92 |
candidate = json_string[first:last+1] if first != -1 and last != -1 and first < last else json_string
|
| 93 |
+
|
| 94 |
+
# Remove trailing commas which can break JSON parsing
|
| 95 |
candidate = re.sub(r',\s*(?=[}\]])', '', candidate)
|
| 96 |
|
| 97 |
try:
|
|
|
|
| 100 |
logger.warning("Failed to parse JSON from LLM output: %s. Candidate: %s", e, candidate)
|
| 101 |
return default
|
| 102 |
|
| 103 |
+
# Validate and clean up the parsed dictionary
|
| 104 |
+
if isinstance(parsed, dict) and "assistant_reply" in parsed:
|
| 105 |
+
parsed.setdefault("code_snippet", "")
|
| 106 |
parsed.setdefault("state_updates", {})
|
| 107 |
parsed["state_updates"].setdefault("conversationSummary", "")
|
| 108 |
parsed["state_updates"].setdefault("language", "Python")
|
| 109 |
parsed.setdefault("suggested_tags", [])
|
| 110 |
+
|
| 111 |
+
# Ensure reply is not empty
|
| 112 |
+
if not parsed["assistant_reply"].strip():
|
| 113 |
+
parsed["assistant_reply"] = "I need a clearer instruction to provide a reply."
|
| 114 |
+
|
| 115 |
return parsed
|
| 116 |
else:
|
| 117 |
logger.warning("Parsed JSON missing 'assistant_reply' or invalid format. Returning default.")
|
|
|
|
| 130 |
return None
|
| 131 |
|
| 132 |
# --- Flask routes ---
|
| 133 |
+
@app.route("/", methods=["GET"])
|
| 134 |
def serve_frontend():
|
| 135 |
try:
|
| 136 |
return app.send_static_file("frontend.html")
|
|
|
|
| 143 |
if not isinstance(data, dict):
|
| 144 |
return jsonify({"error": "invalid request body"}), 400
|
| 145 |
|
|
|
|
| 146 |
chat_history: List[Dict[str, str]] = data.get("chat_history") or []
|
| 147 |
assistant_state: AssistantState = data.get("assistant_state") or {}
|
| 148 |
|
|
|
|
| 173 |
state["language"] = detected_lang
|
| 174 |
|
| 175 |
# 3. Inject Contextual Hint and State into the LAST user message
|
|
|
|
| 176 |
context_hint = f"Current Language: {state['language']}. Conversation Summary so far: {state['conversationSummary']}"
|
| 177 |
|
|
|
|
| 178 |
if llm_messages and llm_messages[-1]["role"] == "user":
|
|
|
|
| 179 |
llm_messages[-1]["content"] = f"USER MESSAGE: {last_user_message}\n\n[CONTEXT HINT: {context_hint}]"
|
| 180 |
elif last_user_message:
|
|
|
|
| 181 |
llm_messages.append({"role": "user", "content": f"USER MESSAGE: {last_user_message}\n\n[CONTEXT HINT: {context_hint}]"})
|
| 182 |
|
| 183 |
|
|
|
|
| 185 |
logger.info("Invoking LLM with full history and prepared prompt...")
|
| 186 |
llm_response = llm.invoke(llm_messages)
|
| 187 |
raw_response = llm_response.content if hasattr(llm_response, "content") else str(llm_response)
|
| 188 |
+
|
| 189 |
logger.info(f"Raw LLM response: {raw_response}")
|
| 190 |
parsed_result = extract_json_from_llm_response(raw_response)
|
| 191 |
|
| 192 |
except Exception as e:
|
| 193 |
logger.exception("LLM invocation failed")
|
|
|
|
| 194 |
error_detail = str(e)
|
| 195 |
if 'decommissioned' in error_detail:
|
| 196 |
error_detail = "LLM Model Error: The model is likely decommissioned. Please check the 'LLM_MODEL' environment variable or the default model in app.py."
|
|
|
|
| 199 |
# 4. State Update from LLM
|
| 200 |
updated_state_from_llm = parsed_result.get("state_updates", {})
|
| 201 |
|
|
|
|
| 202 |
if 'conversationSummary' in updated_state_from_llm:
|
| 203 |
state["conversationSummary"] = updated_state_from_llm["conversationSummary"]
|
| 204 |
if 'language' in updated_state_from_llm:
|
| 205 |
state["language"] = updated_state_from_llm["language"]
|
| 206 |
|
| 207 |
assistant_reply = parsed_result.get("assistant_reply")
|
| 208 |
+
code_snippet = parsed_result.get("code_snippet")
|
| 209 |
+
|
| 210 |
+
# 5. Final Response Payload: Combine the reply and the code snippet
|
| 211 |
+
# The frontend is expecting the code to be *in* the assistant_reply, so we stitch it back together.
|
| 212 |
+
final_reply_content = assistant_reply
|
| 213 |
+
if code_snippet and code_snippet.strip():
|
| 214 |
+
# Add a newline for clean separation if the reply isn't just whitespace
|
| 215 |
+
if final_reply_content.strip():
|
| 216 |
+
final_reply_content += "\n\n"
|
| 217 |
+
final_reply_content += code_snippet
|
| 218 |
+
|
| 219 |
+
if not final_reply_content.strip():
|
| 220 |
+
final_reply_content = "I'm here to help with your code! What programming language are you using?"
|
| 221 |
|
|
|
|
| 222 |
response_payload = {
|
| 223 |
+
"assistant_reply": final_reply_content, # Send combined reply + code
|
| 224 |
"updated_state": state,
|
| 225 |
"suggested_tags": parsed_result.get("suggested_tags", []),
|
| 226 |
}
|