Update app.py
Browse files
app.py
CHANGED
|
@@ -51,7 +51,10 @@ llm = ChatGroq(
|
|
| 51 |
api_key=GROQ_API_KEY,
|
| 52 |
)
|
| 53 |
|
| 54 |
-
|
|
|
|
|
|
|
|
|
|
| 55 |
You are an expert programming assistant. Your role is to provide code suggestions, fix bugs, explain programming concepts, and offer contextual help based on the user's query and preferred programming language.
|
| 56 |
|
| 57 |
**CONTEXT HANDLING RULES (Follow these strictly):**
|
|
@@ -61,7 +64,7 @@ You are an expert programming assistant. Your role is to provide code suggestion
|
|
| 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
|
| 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 |
|
|
@@ -72,8 +75,10 @@ Rules:
|
|
| 72 |
"""
|
| 73 |
|
| 74 |
def extract_json_from_llm_response(raw_response: str) -> dict:
|
|
|
|
|
|
|
| 75 |
default = {
|
| 76 |
-
"assistant_reply":
|
| 77 |
"code_snippet": "",
|
| 78 |
"state_updates": {"conversationSummary": "", "language": "Python"},
|
| 79 |
"suggested_tags": [],
|
|
@@ -82,25 +87,25 @@ def extract_json_from_llm_response(raw_response: str) -> dict:
|
|
| 82 |
if not raw_response or not isinstance(raw_response, str):
|
| 83 |
return default
|
| 84 |
|
| 85 |
-
# Use
|
| 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 |
-
#
|
| 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:
|
| 98 |
parsed = json.loads(candidate)
|
| 99 |
except Exception as e:
|
| 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", {})
|
|
@@ -114,7 +119,7 @@ def extract_json_from_llm_response(raw_response: str) -> dict:
|
|
| 114 |
|
| 115 |
return parsed
|
| 116 |
else:
|
| 117 |
-
logger.warning("Parsed JSON missing
|
| 118 |
return default
|
| 119 |
|
| 120 |
def detect_language_from_text(text: str) -> Optional[str]:
|
|
@@ -146,18 +151,17 @@ def chat():
|
|
| 146 |
chat_history: List[Dict[str, str]] = data.get("chat_history") or []
|
| 147 |
assistant_state: AssistantState = data.get("assistant_state") or {}
|
| 148 |
|
| 149 |
-
# Initialize
|
| 150 |
state: AssistantState = {
|
| 151 |
"conversationSummary": assistant_state.get("conversationSummary", ""),
|
| 152 |
"language": assistant_state.get("language", "Python"),
|
| 153 |
"taggedReplies": assistant_state.get("taggedReplies", []),
|
| 154 |
}
|
| 155 |
|
| 156 |
-
# 1. Prepare LLM Messages from Full History
|
| 157 |
llm_messages = [{"role": "system", "content": PROGRAMMING_ASSISTANT_PROMPT}]
|
| 158 |
|
| 159 |
last_user_message = ""
|
| 160 |
-
|
| 161 |
for msg in chat_history:
|
| 162 |
role = msg.get("role")
|
| 163 |
content = msg.get("content")
|
|
@@ -166,15 +170,14 @@ def chat():
|
|
| 166 |
if role == "user":
|
| 167 |
last_user_message = content
|
| 168 |
|
| 169 |
-
# 2. Language Detection & State Update
|
| 170 |
detected_lang = detect_language_from_text(last_user_message)
|
| 171 |
if detected_lang and detected_lang.lower() != state["language"].lower():
|
| 172 |
logger.info("Detected new language: %s", detected_lang)
|
| 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:
|
|
@@ -196,22 +199,36 @@ def chat():
|
|
| 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."
|
| 197 |
return jsonify({"error": "LLM invocation failed", "detail": error_detail}), 500
|
| 198 |
|
| 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 '
|
|
|
|
| 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
|
|
@@ -220,7 +237,7 @@ def chat():
|
|
| 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,
|
| 224 |
"updated_state": state,
|
| 225 |
"suggested_tags": parsed_result.get("suggested_tags", []),
|
| 226 |
}
|
|
|
|
| 51 |
api_key=GROQ_API_KEY,
|
| 52 |
)
|
| 53 |
|
| 54 |
+
# --- Define the standard error message for failed parsing
|
| 55 |
+
LLM_PARSE_ERROR_MESSAGE = "I'm sorry, I couldn't process the last response correctly due to a formatting issue. Could you please rephrase or try a simpler query?"
|
| 56 |
+
|
| 57 |
+
PROGRAMMING_ASSISTANT_PROMPT = f"""
|
| 58 |
You are an expert programming assistant. Your role is to provide code suggestions, fix bugs, explain programming concepts, and offer contextual help based on the user's query and preferred programming language.
|
| 59 |
|
| 60 |
**CONTEXT HANDLING RULES (Follow these strictly):**
|
|
|
|
| 64 |
STRICT OUTPUT FORMAT (JSON ONLY):
|
| 65 |
Return a single JSON object with the following keys. **The JSON object MUST be enclosed in a single ```json block.**
|
| 66 |
- assistant_reply: string // A natural language reply to the user (short and helpful). Do NOT include code blocks here.
|
| 67 |
+
- code_snippet: string // If suggesting code, provide it here in a markdown code block. **CRITICALLY, you must escape all internal newlines as '\\n' and backslashes as '\\\\'** to keep the string value valid JSON. If no code is required, use an empty string: "".
|
| 68 |
- state_updates: object // updates to the internal state, must include: language, conversationSummary
|
| 69 |
- suggested_tags: array of strings // a list of 1-3 relevant tags for the assistant_reply
|
| 70 |
|
|
|
|
| 75 |
"""
|
| 76 |
|
| 77 |
def extract_json_from_llm_response(raw_response: str) -> dict:
|
| 78 |
+
# The default object is only used if parsing fails, providing a clean error message.
|
| 79 |
+
# The actual state preservation logic is in the /chat route.
|
| 80 |
default = {
|
| 81 |
+
"assistant_reply": LLM_PARSE_ERROR_MESSAGE,
|
| 82 |
"code_snippet": "",
|
| 83 |
"state_updates": {"conversationSummary": "", "language": "Python"},
|
| 84 |
"suggested_tags": [],
|
|
|
|
| 87 |
if not raw_response or not isinstance(raw_response, str):
|
| 88 |
return default
|
| 89 |
|
| 90 |
+
# 1. Use regex to find the JSON content inside the first code block (```json)
|
| 91 |
m = re.search(r"```json\s*([\s\S]*?)\s*```", raw_response)
|
| 92 |
json_string = m.group(1).strip() if m else raw_response
|
| 93 |
|
| 94 |
+
# 2. Refine candidate to just the JSON object content
|
| 95 |
first = json_string.find('{')
|
| 96 |
last = json_string.rfind('}')
|
| 97 |
candidate = json_string[first:last+1] if first != -1 and last != -1 and first < last else json_string
|
| 98 |
|
| 99 |
+
# 3. Remove trailing commas which can break JSON parsing
|
| 100 |
candidate = re.sub(r',\s*(?=[}\]])', '', candidate)
|
| 101 |
|
| 102 |
try:
|
| 103 |
parsed = json.loads(candidate)
|
| 104 |
except Exception as e:
|
| 105 |
+
logger.warning("Failed to parse JSON from LLM output: %s. Candidate: %s", e, candidate[:200]) # Truncate candidate for cleaner logs
|
| 106 |
return default
|
| 107 |
|
| 108 |
+
# 4. Validate and clean up the parsed dictionary
|
| 109 |
if isinstance(parsed, dict) and "assistant_reply" in parsed:
|
| 110 |
parsed.setdefault("code_snippet", "")
|
| 111 |
parsed.setdefault("state_updates", {})
|
|
|
|
| 119 |
|
| 120 |
return parsed
|
| 121 |
else:
|
| 122 |
+
logger.warning("Parsed JSON missing required keys or invalid format. Returning default.")
|
| 123 |
return default
|
| 124 |
|
| 125 |
def detect_language_from_text(text: str) -> Optional[str]:
|
|
|
|
| 151 |
chat_history: List[Dict[str, str]] = data.get("chat_history") or []
|
| 152 |
assistant_state: AssistantState = data.get("assistant_state") or {}
|
| 153 |
|
| 154 |
+
# Initialize state from input. This is the "safe" state.
|
| 155 |
state: AssistantState = {
|
| 156 |
"conversationSummary": assistant_state.get("conversationSummary", ""),
|
| 157 |
"language": assistant_state.get("language", "Python"),
|
| 158 |
"taggedReplies": assistant_state.get("taggedReplies", []),
|
| 159 |
}
|
| 160 |
|
| 161 |
+
# 1. Prepare LLM Messages from Full History (same as before)
|
| 162 |
llm_messages = [{"role": "system", "content": PROGRAMMING_ASSISTANT_PROMPT}]
|
| 163 |
|
| 164 |
last_user_message = ""
|
|
|
|
| 165 |
for msg in chat_history:
|
| 166 |
role = msg.get("role")
|
| 167 |
content = msg.get("content")
|
|
|
|
| 170 |
if role == "user":
|
| 171 |
last_user_message = content
|
| 172 |
|
| 173 |
+
# 2. Language Detection & State Update (same as before)
|
| 174 |
detected_lang = detect_language_from_text(last_user_message)
|
| 175 |
if detected_lang and detected_lang.lower() != state["language"].lower():
|
| 176 |
logger.info("Detected new language: %s", detected_lang)
|
| 177 |
state["language"] = detected_lang
|
| 178 |
|
| 179 |
+
# 3. Inject Contextual Hint and State into the LAST user message (same as before)
|
| 180 |
context_hint = f"Current Language: {state['language']}. Conversation Summary so far: {state['conversationSummary']}"
|
|
|
|
| 181 |
if llm_messages and llm_messages[-1]["role"] == "user":
|
| 182 |
llm_messages[-1]["content"] = f"USER MESSAGE: {last_user_message}\n\n[CONTEXT HINT: {context_hint}]"
|
| 183 |
elif last_user_message:
|
|
|
|
| 199 |
error_detail = "LLM Model Error: The model is likely decommissioned. Please check the 'LLM_MODEL' environment variable or the default model in app.py."
|
| 200 |
return jsonify({"error": "LLM invocation failed", "detail": error_detail}), 500
|
| 201 |
|
| 202 |
+
# 4. State Update from LLM (NEW ROBUST LOGIC)
|
| 203 |
+
|
| 204 |
+
# Check if parsing failed (by comparing the reply to the known error message)
|
| 205 |
+
if parsed_result.get("assistant_reply") == LLM_PARSE_ERROR_MESSAGE:
|
| 206 |
+
final_reply_content = LLM_PARSE_ERROR_MESSAGE
|
| 207 |
+
|
| 208 |
+
# State and tags remain as initialized (from the input assistant_state), fulfilling the user request.
|
| 209 |
+
response_payload = {
|
| 210 |
+
"assistant_reply": final_reply_content,
|
| 211 |
+
"updated_state": state, # Keep the original input state
|
| 212 |
+
"suggested_tags": [],
|
| 213 |
+
}
|
| 214 |
+
return jsonify(response_payload)
|
| 215 |
+
|
| 216 |
+
# Parsing was successful. Safely update the state.
|
| 217 |
updated_state_from_llm = parsed_result.get("state_updates", {})
|
| 218 |
|
| 219 |
if 'conversationSummary' in updated_state_from_llm:
|
| 220 |
state["conversationSummary"] = updated_state_from_llm["conversationSummary"]
|
| 221 |
+
# We allow the language to be updated only if it's explicitly set by the LLM AND it's a valid change.
|
| 222 |
+
if 'language' in updated_state_from_llm and updated_state_from_llm['language'].strip():
|
| 223 |
state["language"] = updated_state_from_llm["language"]
|
| 224 |
|
| 225 |
+
|
| 226 |
assistant_reply = parsed_result.get("assistant_reply")
|
| 227 |
code_snippet = parsed_result.get("code_snippet")
|
| 228 |
|
| 229 |
# 5. Final Response Payload: Combine the reply and the code snippet
|
|
|
|
| 230 |
final_reply_content = assistant_reply
|
| 231 |
if code_snippet and code_snippet.strip():
|
|
|
|
| 232 |
if final_reply_content.strip():
|
| 233 |
final_reply_content += "\n\n"
|
| 234 |
final_reply_content += code_snippet
|
|
|
|
| 237 |
final_reply_content = "I'm here to help with your code! What programming language are you using?"
|
| 238 |
|
| 239 |
response_payload = {
|
| 240 |
+
"assistant_reply": final_reply_content,
|
| 241 |
"updated_state": state,
|
| 242 |
"suggested_tags": parsed_result.get("suggested_tags", []),
|
| 243 |
}
|