WebashalarForML commited on
Commit
441bbe1
·
verified ·
1 Parent(s): 4bd2a59

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +164 -178
app.py CHANGED
@@ -3,17 +3,29 @@ import os
3
  import json
4
  import logging
5
  import re
6
- from typing import Dict, Any, List, Optional
 
7
  from flask import Flask, request, jsonify
8
  from flask_cors import CORS
9
  from dotenv import load_dotenv
10
  from langchain_groq import ChatGroq
 
11
 
12
- # --- Setup logging ---
 
 
 
 
 
 
 
 
 
 
13
  logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
14
  logger = logging.getLogger("code-assistant")
15
 
16
- # --- Load environment variables ---
17
  load_dotenv()
18
  GROQ_API_KEY = os.getenv("GROQ_API_KEY")
19
  if not GROQ_API_KEY:
@@ -21,7 +33,10 @@ if not GROQ_API_KEY:
21
  raise RuntimeError("GROQ_API_KEY not set in environment")
22
 
23
  # --- Flask app setup ---
24
- app = Flask(__name__)
 
 
 
25
  CORS(app)
26
 
27
  # --- LLM setup ---
@@ -33,203 +48,170 @@ llm = ChatGroq(
33
  )
34
 
35
  # --- Constants ---
36
- LLM_PARSE_ERROR_MESSAGE = (
37
- "Sorry, I couldn't understand the last response due to formatting issues. "
38
- "Please try rephrasing or simplifying your query."
39
- )
 
 
 
 
40
 
41
- SYSTEM_PROMPT = """
42
- You are an expert programming assistant. You help with code suggestions, bug fixes, explanations, and contextual help.
 
 
 
 
43
 
44
  Rules:
45
- - Always respond with a single JSON object enclosed in a ```json ... ``` code block.
46
- - The JSON must have these keys:
47
- - assistant_reply: string (short, helpful natural language reply, no code blocks)
48
- - code_snippet: string (code in markdown code block, with newlines escaped as \\n and backslashes as \\\\; empty string if none)
49
- - state_updates: object with keys:
50
- - conversationSummary: string (concise summary of the conversation so far)
51
- - language: string (programming language context)
52
- - suggested_tags: array of strings (1-3 relevant tags)
53
-
54
- - Always include all keys.
55
- - Adapt code and explanations to the language in state_updates.language.
56
  """
57
 
58
- def extract_json_from_response(text: str) -> Optional[Dict[str, Any]]:
59
- """
60
- Extract JSON object from LLM response text inside a ```json ... ``` block.
61
- Return None if parsing fails.
62
- """
 
 
 
 
 
 
 
 
 
 
63
  try:
64
- # Extract JSON code block content
65
- match = re.search(r"```json\s*([\s\S]*?)\s*```", text)
66
- json_text = match.group(1) if match else text
67
-
68
- # Find first and last braces to isolate JSON object
69
- first = json_text.find('{')
70
- last = json_text.rfind('}')
71
- if first == -1 or last == -1 or last < first:
72
- logger.warning("No valid JSON braces found in LLM response")
73
- return None
74
- json_str = json_text[first:last+1]
75
-
76
- # Remove trailing commas before } or ]
77
- json_str = re.sub(r",\s*(?=[}\]])", "", json_str)
78
-
79
- parsed = json.loads(json_str)
80
- return parsed
81
  except Exception as e:
82
- logger.warning(f"Failed to parse JSON from LLM response: {e}")
83
- return None
84
-
85
- def detect_language(text: str, default: str = "Python") -> str:
86
- """
87
- Detect programming language from user text.
88
- Returns detected language or default.
89
- """
90
- if not text:
91
  return default
92
- text_lower = text.lower()
93
- languages = ["python", "javascript", "java", "c++", "c#", "go", "ruby", "php", "typescript", "swift"]
94
- for lang in languages:
95
- if re.search(rf"\b(in|using|for)\s+{lang}\b", text_lower):
96
- return lang.capitalize()
97
- return default
98
-
99
- def build_llm_messages(
100
- system_prompt: str,
101
- chat_history: List[Dict[str, str]],
102
- conversation_summary: str,
103
- language: str,
104
- ) -> List[Dict[str, str]]:
105
- """
106
- Build messages list for LLM invocation.
107
- Inject conversation summary and language context into the last user message.
108
- """
109
- messages = [{"role": "system", "content": system_prompt}]
110
- for msg in chat_history:
111
- if msg.get("role") in ["user", "assistant"] and msg.get("content"):
112
- messages.append({"role": msg["role"], "content": msg["content"]})
113
-
114
- # Inject context hint into last user message
115
- for i in reversed(range(len(messages))):
116
- if messages[i]["role"] == "user":
117
- messages[i]["content"] += f"\n\n[Context: Language={language}, Summary={conversation_summary}]"
118
- break
119
  else:
120
- # No user message found, add a dummy one with context
121
- messages.append({"role": "user", "content": f"[Context: Language={language}, Summary={conversation_summary}]"})
 
 
 
 
 
 
 
 
 
 
122
 
123
- return messages
 
 
 
 
 
 
 
124
 
125
  @app.route("/chat", methods=["POST"])
126
  def chat():
127
- """
128
- Main chat endpoint.
129
- Expects JSON with keys:
130
- - chat_history: list of messages {role: "user"/"assistant", content: str}
131
- - assistant_state: {conversationSummary: str, language: str, taggedReplies: list}
132
- Returns JSON with:
133
- - assistant_reply: str
134
- - updated_state: dict
135
- - suggested_tags: list
136
- """
137
  data = request.get_json(force=True)
138
  if not isinstance(data, dict):
139
- return jsonify({"error": "Invalid request body"}), 400
140
-
141
- chat_history = data.get("chat_history", [])
142
- assistant_state = data.get("assistant_state", {})
143
-
144
- # Initialize state with defaults
145
- conversation_summary = assistant_state.get("conversationSummary", "")
146
- language = assistant_state.get("language", "Python")
147
- tagged_replies = assistant_state.get("taggedReplies", [])
148
-
149
- # Detect language from last user message if possible
150
- last_user_msg = ""
151
- for msg in reversed(chat_history):
152
- if msg.get("role") == "user" and msg.get("content"):
153
- last_user_msg = msg["content"]
154
- break
155
- detected_lang = detect_language(last_user_msg, default=language)
156
- if detected_lang.lower() != language.lower():
157
- logger.info(f"Language changed from {language} to {detected_lang}")
158
- language = detected_lang
159
-
160
- # Build messages for LLM
161
- messages = build_llm_messages(SYSTEM_PROMPT, chat_history, conversation_summary, language)
162
 
163
- try:
164
- logger.info("Invoking LLM...")
165
- llm_response = llm.invoke(messages)
166
- raw_text = getattr(llm_response, "content", str(llm_response))
167
- logger.info(f"LLM raw response: {raw_text}")
168
-
169
- parsed = extract_json_from_response(raw_text)
170
- if not parsed:
171
- raise ValueError("Failed to parse JSON from LLM response")
172
-
173
- # Validate keys
174
- required_keys = {"assistant_reply", "code_snippet", "state_updates", "suggested_tags"}
175
- if not required_keys.issubset(parsed.keys()):
176
- raise ValueError(f"Missing keys in LLM response JSON: {required_keys - parsed.keys()}")
177
-
178
- # Update state
179
- state_updates = parsed.get("state_updates", {})
180
- conversation_summary = state_updates.get("conversationSummary", conversation_summary)
181
- language = state_updates.get("language", language)
182
-
183
- # Compose final assistant reply with optional code snippet
184
- assistant_reply = parsed["assistant_reply"].strip()
185
- code_snippet = parsed["code_snippet"].strip()
186
- if code_snippet:
187
- # Unescape newlines and backslashes for display
188
- code_snippet_display = code_snippet.replace("\\n", "\n").replace("\\\\", "\\")
189
- assistant_reply += f"\n\n```{language.lower()}\n{code_snippet_display}\n```"
190
-
191
- # Prepare response
192
- response = {
193
- "assistant_reply": assistant_reply,
194
- "updated_state": {
195
- "conversationSummary": conversation_summary,
196
- "language": language,
197
- "taggedReplies": tagged_replies,
198
- },
199
- "suggested_tags": parsed.get("suggested_tags", []),
200
- }
201
- return jsonify(response)
202
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
203
  except Exception as e:
204
- logger.exception("Error during LLM invocation or parsing")
 
 
 
 
 
 
205
  return jsonify({
206
  "assistant_reply": LLM_PARSE_ERROR_MESSAGE,
207
- "updated_state": {
208
- "conversationSummary": conversation_summary,
209
- "language": language,
210
- "taggedReplies": tagged_replies,
211
- },
212
  "suggested_tags": [],
213
- "error": str(e),
214
- }), 500
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
215
 
216
  @app.route("/tag_reply", methods=["POST"])
217
  def tag_reply():
218
- """
219
- Endpoint to save/bookmark a reply with tags.
220
- Expects JSON with keys:
221
- - reply: string
222
- - tags: list of strings
223
- - assistant_state: current state dict
224
- Returns updated state with new tagged reply appended.
225
- """
226
  data = request.get_json(force=True)
227
  if not isinstance(data, dict):
228
  return jsonify({"error": "invalid request body"}), 400
229
 
230
  reply_content = data.get("reply")
231
  tags = data.get("tags")
232
- assistant_state = data.get("assistant_state") or {}
233
 
234
  if not reply_content or not tags:
235
  return jsonify({"error": "Missing 'reply' or 'tags' in request"}), 400
@@ -238,20 +220,24 @@ def tag_reply():
238
  if not tags:
239
  return jsonify({"error": "Tags list cannot be empty"}), 400
240
 
241
- tagged_replies = assistant_state.get("taggedReplies", [])
242
- tagged_replies.append({"reply": reply_content, "tags": tags})
243
-
244
- updated_state = {
245
  "conversationSummary": assistant_state.get("conversationSummary", ""),
246
  "language": assistant_state.get("language", "Python"),
247
- "taggedReplies": tagged_replies,
248
  }
249
 
250
- logger.info(f"Tagged reply saved with tags: {tags}")
 
 
 
 
 
 
 
251
 
252
  return jsonify({
253
  "message": "Reply saved and tagged successfully.",
254
- "updated_state": updated_state,
255
  }), 200
256
 
257
  @app.route("/ping", methods=["GET"])
 
3
  import json
4
  import logging
5
  import re
6
+ from typing import Dict, List, Optional
7
+ from pathlib import Path
8
  from flask import Flask, request, jsonify
9
  from flask_cors import CORS
10
  from dotenv import load_dotenv
11
  from langchain_groq import ChatGroq
12
+ from typing_extensions import TypedDict
13
 
14
+ # --- Type Definitions ---
15
+ class TaggedReply(TypedDict):
16
+ reply: str
17
+ tags: List[str]
18
+
19
+ class AssistantState(TypedDict):
20
+ conversationSummary: str
21
+ language: str
22
+ taggedReplies: List[TaggedReply]
23
+
24
+ # --- Logging ---
25
  logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
26
  logger = logging.getLogger("code-assistant")
27
 
28
+ # --- Load environment ---
29
  load_dotenv()
30
  GROQ_API_KEY = os.getenv("GROQ_API_KEY")
31
  if not GROQ_API_KEY:
 
33
  raise RuntimeError("GROQ_API_KEY not set in environment")
34
 
35
  # --- Flask app setup ---
36
+ BASE_DIR = Path(__file__).resolve().parent
37
+ static_folder = BASE_DIR / "static"
38
+
39
+ app = Flask(__name__, static_folder=str(static_folder), static_url_path="/static")
40
  CORS(app)
41
 
42
  # --- LLM setup ---
 
48
  )
49
 
50
  # --- Constants ---
51
+ 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?"
52
+
53
+ PROGRAMMING_ASSISTANT_PROMPT = """
54
+ 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.
55
+
56
+ **CONTEXT HANDLING RULES (Follow these strictly):**
57
+ - **Conversation Summary:** At the end of every response, you MUST provide an updated, concise `conversationSummary` based on the entire chat history provided. This summary helps you maintain context.
58
+ - **Language Adaptation:** Adjust your suggestions, code, and explanations to the programming language specified in the 'language' field of the 'AssistantState'.
59
 
60
+ STRICT OUTPUT FORMAT (JSON ONLY):
61
+ Return a single JSON object with the following keys. **The JSON object MUST be enclosed in a single ```json block.**
62
+ - assistant_reply: string // A natural language reply to the user (short and helpful). Do NOT include code blocks here.
63
+ - 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: "".
64
+ - state_updates: object // updates to the internal state, must include: language, conversationSummary
65
+ - suggested_tags: array of strings // a list of 1-3 relevant tags for the assistant_reply
66
 
67
  Rules:
68
+ - ALWAYS include all four top-level keys: `assistant_reply`, `code_snippet`, `state_updates`, and `suggested_tags`.
69
+ - ALWAYS include `assistant_reply` as a non-empty string.
70
+ - Do NOT produce any text outside the JSON block.
 
 
 
 
 
 
 
 
71
  """
72
 
73
+ def extract_json_from_llm_response(raw_response: str) -> dict:
74
+ default = {
75
+ "assistant_reply": LLM_PARSE_ERROR_MESSAGE,
76
+ "code_snippet": "",
77
+ "state_updates": {"conversationSummary": "", "language": "Python"},
78
+ "suggested_tags": [],
79
+ }
80
+ if not raw_response or not isinstance(raw_response, str):
81
+ return default
82
+ m = re.search(r"```json\s*([\s\S]*?)\s*```", raw_response)
83
+ json_string = m.group(1).strip() if m else raw_response
84
+ first = json_string.find('{')
85
+ last = json_string.rfind('}')
86
+ candidate = json_string[first:last+1] if first != -1 and last != -1 and first < last else json_string
87
+ candidate = re.sub(r',\s*(?=[}\]])', '', candidate)
88
  try:
89
+ parsed = json.loads(candidate)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  except Exception as e:
91
+ logger.warning("Failed to parse JSON from LLM output: %s. Candidate: %s", e, candidate[:200])
 
 
 
 
 
 
 
 
92
  return default
93
+ if isinstance(parsed, dict) and "assistant_reply" in parsed:
94
+ parsed.setdefault("code_snippet", "")
95
+ parsed.setdefault("state_updates", {})
96
+ parsed["state_updates"].setdefault("conversationSummary", "")
97
+ parsed["state_updates"].setdefault("language", "Python")
98
+ parsed.setdefault("suggested_tags", [])
99
+ if not parsed["assistant_reply"].strip():
100
+ parsed["assistant_reply"] = "I need a clearer instruction to provide a reply."
101
+ return parsed
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  else:
103
+ logger.warning("Parsed JSON missing required keys or invalid format. Returning default.")
104
+ return default
105
+
106
+ def detect_language_from_text(text: str) -> Optional[str]:
107
+ if not text:
108
+ return None
109
+ lower = text.lower()
110
+ known_languages = ["python", "javascript", "java", "c++", "c#", "go", "ruby", "php", "typescript", "swift"]
111
+ lang_match = re.search(r'\b(in|using|for)\s+(' + '|'.join(known_languages) + r')\b', lower)
112
+ if lang_match:
113
+ return lang_match.group(2).capitalize()
114
+ return None
115
 
116
+ # --- Routes ---
117
+
118
+ @app.route("/", methods=["GET"])
119
+ def serve_frontend():
120
+ try:
121
+ return app.send_static_file("frontend.html")
122
+ except Exception:
123
+ return "<h3>frontend.html not found in static/ — please add your frontend.html there.</h3>", 404
124
 
125
  @app.route("/chat", methods=["POST"])
126
  def chat():
 
 
 
 
 
 
 
 
 
 
127
  data = request.get_json(force=True)
128
  if not isinstance(data, dict):
129
+ return jsonify({"error": "invalid request body"}), 400
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
 
131
+ chat_history: List[Dict[str, str]] = data.get("chat_history") or []
132
+ assistant_state: AssistantState = data.get("assistant_state") or {}
133
+
134
+ state: AssistantState = {
135
+ "conversationSummary": assistant_state.get("conversationSummary", ""),
136
+ "language": assistant_state.get("language", "Python"),
137
+ "taggedReplies": assistant_state.get("taggedReplies", []),
138
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
139
 
140
+ llm_messages = [{"role": "system", "content": PROGRAMMING_ASSISTANT_PROMPT}]
141
+
142
+ last_user_message = ""
143
+ for msg in chat_history:
144
+ role = msg.get("role")
145
+ content = msg.get("content")
146
+ if role in ["user", "assistant"] and content:
147
+ llm_messages.append({"role": role, "content": content})
148
+ if role == "user":
149
+ last_user_message = content
150
+
151
+ detected_lang = detect_language_from_text(last_user_message)
152
+ if detected_lang and detected_lang.lower() != state["language"].lower():
153
+ logger.info("Detected new language: %s", detected_lang)
154
+ state["language"] = detected_lang
155
+
156
+ context_hint = f"Current Language: {state['language']}. Conversation Summary so far: {state['conversationSummary']}"
157
+ if llm_messages and llm_messages[-1]["role"] == "user":
158
+ llm_messages[-1]["content"] = f"USER MESSAGE: {last_user_message}\n\n[CONTEXT HINT: {context_hint}]"
159
+ elif last_user_message:
160
+ llm_messages.append({"role": "user", "content": f"USER MESSAGE: {last_user_message}\n\n[CONTEXT HINT: {context_hint}]"})
161
+
162
+ try:
163
+ logger.info("Invoking LLM with full history and prepared prompt...")
164
+ llm_response = llm.invoke(llm_messages)
165
+ raw_response = llm_response.content if hasattr(llm_response, "content") else str(llm_response)
166
+ logger.info(f"Raw LLM response: {raw_response}")
167
+ parsed_result = extract_json_from_llm_response(raw_response)
168
  except Exception as e:
169
+ logger.exception("LLM invocation failed")
170
+ error_detail = str(e)
171
+ if 'decommissioned' in error_detail:
172
+ error_detail = "LLM Model Error: The model is likely decommissioned. Please check the 'LLM_MODEL' environment variable or the default model in app.py."
173
+ return jsonify({"error": "LLM invocation failed", "detail": error_detail}), 500
174
+
175
+ if parsed_result.get("assistant_reply") == LLM_PARSE_ERROR_MESSAGE:
176
  return jsonify({
177
  "assistant_reply": LLM_PARSE_ERROR_MESSAGE,
178
+ "updated_state": state,
 
 
 
 
179
  "suggested_tags": [],
180
+ })
181
+
182
+ updated_state_from_llm = parsed_result.get("state_updates", {})
183
+ if 'conversationSummary' in updated_state_from_llm:
184
+ state["conversationSummary"] = updated_state_from_llm["conversationSummary"]
185
+ if 'language' in updated_state_from_llm and updated_state_from_llm['language'].strip():
186
+ state["language"] = updated_state_from_llm["language"]
187
+
188
+ assistant_reply = parsed_result.get("assistant_reply")
189
+ code_snippet = parsed_result.get("code_snippet")
190
+
191
+ final_reply_content = assistant_reply
192
+ if code_snippet and code_snippet.strip():
193
+ if final_reply_content.strip():
194
+ final_reply_content += "\n\n"
195
+ final_reply_content += code_snippet
196
+
197
+ if not final_reply_content.strip():
198
+ final_reply_content = "I'm here to help with your code! What programming language are you using?"
199
+
200
+ return jsonify({
201
+ "assistant_reply": final_reply_content,
202
+ "updated_state": state,
203
+ "suggested_tags": parsed_result.get("suggested_tags", []),
204
+ })
205
 
206
  @app.route("/tag_reply", methods=["POST"])
207
  def tag_reply():
 
 
 
 
 
 
 
 
208
  data = request.get_json(force=True)
209
  if not isinstance(data, dict):
210
  return jsonify({"error": "invalid request body"}), 400
211
 
212
  reply_content = data.get("reply")
213
  tags = data.get("tags")
214
+ assistant_state: AssistantState = data.get("assistant_state") or {}
215
 
216
  if not reply_content or not tags:
217
  return jsonify({"error": "Missing 'reply' or 'tags' in request"}), 400
 
220
  if not tags:
221
  return jsonify({"error": "Tags list cannot be empty"}), 400
222
 
223
+ state: AssistantState = {
 
 
 
224
  "conversationSummary": assistant_state.get("conversationSummary", ""),
225
  "language": assistant_state.get("language", "Python"),
226
+ "taggedReplies": assistant_state.get("taggedReplies", []),
227
  }
228
 
229
+ new_tagged_reply: TaggedReply = {
230
+ "reply": reply_content,
231
+ "tags": tags,
232
+ }
233
+
234
+ state["taggedReplies"].append(new_tagged_reply)
235
+
236
+ logger.info("Reply tagged with: %s", tags)
237
 
238
  return jsonify({
239
  "message": "Reply saved and tagged successfully.",
240
+ "updated_state": state,
241
  }), 200
242
 
243
  @app.route("/ping", methods=["GET"])