WebashalarForML commited on
Commit
4f26c25
·
verified ·
1 Parent(s): 4807519

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +171 -249
app.py CHANGED
@@ -1,297 +1,219 @@
1
  #!/usr/bin/env python3
2
- # filename: app_refactored.py
3
  import os
4
  import json
5
  import logging
6
  import re
7
- from pathlib import Path
8
- from typing import Dict, Any, List, Optional, Tuple
9
-
10
  from flask import Flask, request, jsonify
11
  from flask_cors import CORS
12
  from dotenv import load_dotenv
 
13
 
14
- # Replace with your LLM client import; kept generic here.
15
- # from langchain_groq import ChatGroq
 
16
 
17
- # === Config ===
18
  load_dotenv()
19
  GROQ_API_KEY = os.getenv("GROQ_API_KEY")
20
  if not GROQ_API_KEY:
 
21
  raise RuntimeError("GROQ_API_KEY not set in environment")
22
 
23
- LLM_MODEL = os.getenv("LLM_MODEL", "meta-llama/llama-4-scout-17b-16e-instruct")
24
- LLM_TIMEOUT_SECONDS = float(os.getenv("LLM_TIMEOUT_SECONDS", "20"))
25
- MAX_HISTORY_MESSAGES = int(os.getenv("MAX_HISTORY_MESSAGES", "12"))
26
- VALID_LANGUAGES = {"python", "javascript", "java", "c++", "c#", "go", "ruby", "php", "typescript", "swift"}
27
-
28
- # === Logging ===
29
- logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
30
- logger = logging.getLogger("code-assistant")
31
-
32
- # === LLM client (example) ===
33
- # NOTE: adapt this block to match your SDK. Keep a tolerant accessor for response text.
34
- class DummyLLM:
35
- def __init__(self, **kwargs):
36
- self.kwargs = kwargs
37
- def invoke(self, messages: List[Dict[str, str]], timeout: Optional[float] = None):
38
- # stub: replace with real client call
39
- class Resp: pass
40
- r = Resp()
41
- r.content = json.dumps({
42
- "assistant_reply": "This is a dummy reply. Replace with real LLM client.",
43
- "code_snippet": "",
44
- "state_updates": {"conversationSummary": "dummy", "language": "Python"},
45
- "suggested_tags": ["example"]
46
- })
47
- return r
48
 
49
- # llm = ChatGroq(model=LLM_MODEL, api_key=GROQ_API_KEY, temperature=0.1, max_tokens=2048)
50
- llm = DummyLLM(model=LLM_MODEL, api_key=GROQ_API_KEY) # replace with real client
 
 
 
 
 
51
 
52
- # === Prompt ===
53
- SYSTEM_PROMPT = (
54
- "You are an expert programming assistant. Prefer to return a JSON object with keys: "
55
- "assistant_reply (string), code_snippet (string, optional, can be multiline), "
56
- "state_updates (object), suggested_tags (array). If including code, put it in triple backticks. "
57
- "Do NOT escape newlines in code_snippet; return natural multi-line strings."
58
  )
59
 
60
- # === Utilities ===
61
- def clamp_summary(s: str, max_len: int = 1200) -> str:
62
- s = (s or "").strip()
63
- return s if len(s) <= max_len else s[:max_len-3] + "..."
64
 
65
- def canonicalize_language(text: Optional[str]) -> Optional[str]:
66
- if not text:
67
- return None
68
- t = text.strip().lower()
69
- # quick membership test
70
- for lang in VALID_LANGUAGES:
71
- if lang in t or t == lang:
72
- return lang
73
- return None
 
 
 
 
74
 
75
- def try_parse_json(s: str) -> Optional[Dict[str, Any]]:
 
 
 
 
76
  try:
77
- return json.loads(s)
78
- except Exception:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  return None
80
 
81
- def extract_code_fence(text: str) -> Optional[str]:
82
- m = re.search(r"```(?:[a-zA-Z0-9_+\-]*)\n([\s\S]*?)```", text)
83
- return m.group(1).strip() if m else None
84
-
85
- def parse_llm_output(raw: str) -> Dict[str, Any]:
86
  """
87
- Tolerant multi-strategy parser:
88
- 1) Direct JSON
89
- 2) JSON inside a ```json``` fence
90
- 3) Heuristic extraction: assistant_reply lines, code fences for code_snippet, simple state_updates line (json)
91
  """
92
- default = {
93
- "assistant_reply": "I couldn't parse the model response. Please rephrase or simplify the request.",
94
- "code_snippet": "",
95
- "state_updates": {"conversationSummary": "", "language": "python"},
96
- "suggested_tags": [],
97
- "parse_ok": False,
98
- }
99
- if not raw or not isinstance(raw, str):
100
  return default
101
-
102
- raw = raw.strip()
103
-
104
- # 1) direct JSON
105
- parsed = try_parse_json(raw)
106
- if parsed and isinstance(parsed, dict) and "assistant_reply" in parsed:
107
- parsed.setdefault("code_snippet", "")
108
- parsed.setdefault("state_updates", {})
109
- parsed.setdefault("suggested_tags", [])
110
- parsed["parse_ok"] = True
111
- return parsed
112
-
113
- # 2) JSON inside any code fence (```json ... ```)
114
- m_json_fence = re.search(r"```json\s*([\s\S]*?)```", raw, re.IGNORECASE)
115
- if m_json_fence:
116
- candidate = m_json_fence.group(1)
117
- parsed = try_parse_json(candidate)
118
- if parsed and "assistant_reply" in parsed:
119
- parsed.setdefault("code_snippet", "")
120
- parsed.setdefault("state_updates", {})
121
- parsed.setdefault("suggested_tags", [])
122
- parsed["parse_ok"] = True
123
- return parsed
124
-
125
- # 3) Heuristics: find assistant_reply: ...; code fence for code; state_updates as inline JSON
126
- assistant_reply = ""
127
- code_snippet = ""
128
- state_updates = {}
129
- suggested_tags = []
130
-
131
- # a) extract code fence (first code block)
132
- code_snippet = extract_code_fence(raw) or ""
133
-
134
- # b) extract assistant_reply by looking for lines like "assistant_reply:" or markdown bold
135
- m = re.search(r'assistant_reply\s*[:\-]\s*(["\']?)([\s\S]*?)(?=\n[a-z_]+[\s\-:]{1}|$)', raw, re.IGNORECASE)
136
- if m:
137
- assistant_reply = m.group(2).strip()
138
  else:
139
- # fallback: take everything up to the first code fence or up to "state_updates"
140
- cut_idx = raw.find("```")
141
- state_idx = raw.lower().find("state_updates")
142
- end = min([i for i in (cut_idx if cut_idx>=0 else len(raw), state_idx if state_idx>=0 else len(raw))])
143
- assistant_reply = raw[:end].strip()
144
- # strip any leading labels like "**assistant_reply**:" or similar
145
- assistant_reply = re.sub(r'^\**\s*assistant_reply\**\s*[:\-]?\s*', '', assistant_reply, flags=re.IGNORECASE).strip()
146
-
147
- # c) find state_updates JSON block if present
148
- m_state = re.search(r"state_updates\s*[:\-]?\s*(\{[\s\S]*?\})", raw, re.IGNORECASE)
149
- if m_state:
150
- try:
151
- state_updates = json.loads(m_state.group(1))
152
- except Exception:
153
- state_updates = {}
154
-
155
- # d) suggested_tags simple extract
156
- m_tags = re.search(r"suggested_tags\s*[:\-]?\s*(\[[^\]]*\])", raw, re.IGNORECASE)
157
- if m_tags:
158
- try:
159
- suggested_tags = json.loads(m_tags.group(1))
160
- except Exception:
161
- suggested_tags = []
162
-
163
- result = {
164
- "assistant_reply": assistant_reply or default["assistant_reply"],
165
- "code_snippet": code_snippet or "",
166
- "state_updates": state_updates or {"conversationSummary": "", "language": "python"},
167
- "suggested_tags": suggested_tags or [],
168
- "parse_ok": bool(assistant_reply or code_snippet),
169
- }
170
- return result
171
 
172
- # === Flask app ===
173
- BASE_DIR = Path(__file__).resolve().parent
174
- app = Flask(__name__, static_folder=str(BASE_DIR / "static"), static_url_path="/static")
175
- CORS(app)
176
-
177
- @app.route("/", methods=["GET"])
178
- def serve_frontend():
179
- try:
180
- return app.send_static_file("frontend.html")
181
- except Exception:
182
- return "<h3>frontend.html not found in static/ — please add your frontend.html there.</h3>", 404
183
 
184
  @app.route("/chat", methods=["POST"])
185
  def chat():
186
- payload = request.get_json(force=True, silent=True)
187
- if not isinstance(payload, dict):
188
- return jsonify({"error": "invalid request body"}), 400
189
-
190
- chat_history = payload.get("chat_history", [])
191
- assistant_state = payload.get("assistant_state", {})
 
 
 
 
 
 
 
192
 
193
- # validate/normalize assistant_state
194
- state = {
195
- "conversationSummary": assistant_state.get("conversationSummary", "").strip(),
196
- "language": assistant_state.get("language", "python").strip().lower(),
197
- "taggedReplies": assistant_state.get("taggedReplies", []),
198
- }
199
 
200
- # limit history length to recent messages to control token usage
201
- if isinstance(chat_history, list) and len(chat_history) > MAX_HISTORY_MESSAGES:
202
- chat_history = chat_history[-MAX_HISTORY_MESSAGES:]
203
 
204
- # build messages for LLM (do not mutate user's last message)
205
- messages = [{"role": "system", "content": SYSTEM_PROMPT}]
206
- for m in chat_history:
207
- if not isinstance(m, dict):
208
- continue
209
- role = m.get("role")
210
- content = m.get("content")
211
- if role in ("user", "assistant") and content:
212
- messages.append({"role": role, "content": content})
 
213
 
214
- # append a supplemental context message (do not overwrite)
215
- context_hint = f"[CONTEXT] language={state['language']} summary={clamp_summary(state['conversationSummary'], 300)}"
216
- messages.append({"role": "system", "content": context_hint})
217
 
218
- # call LLM (wrap in try/except)
219
  try:
220
- raw_resp = llm.invoke(messages, timeout=LLM_TIMEOUT_SECONDS)
221
- # tolerate different shapes
222
- raw_text = getattr(raw_resp, "content", None) or getattr(raw_resp, "text", None) or str(raw_resp)
223
- logger.info("LLM raw text: %.300s", raw_text.replace('\n', ' ')[:300])
224
- except Exception as e:
225
- logger.exception("LLM invocation error")
226
- return jsonify({"error": "LLM invocation failed", "detail": str(e)}), 500
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
227
 
228
- parsed = parse_llm_output(raw_text)
229
-
230
- # If parse failed, don't overwrite the existing state; give helpful message.
231
- if not parsed.get("parse_ok"):
232
- logger.warning("Parse failure. Returning fallback message.")
233
  return jsonify({
234
- "assistant_reply": parsed["assistant_reply"],
235
- "code_snippet": "",
236
- "updated_state": state,
 
 
237
  "suggested_tags": [],
238
- "parse_ok": False,
239
- }), 200
240
-
241
- # Validate and apply state_updates conservatively
242
- updates = parsed.get("state_updates", {}) or {}
243
- if isinstance(updates, dict):
244
- if "conversationSummary" in updates:
245
- state["conversationSummary"] = clamp_summary(str(updates["conversationSummary"]))
246
- if "language" in updates:
247
- lang = canonicalize_language(str(updates["language"]))
248
- if lang:
249
- state["language"] = lang
250
-
251
- # limit suggested tags
252
- tags = parsed.get("suggested_tags", []) or []
253
- if isinstance(tags, list):
254
- tags = [str(t).strip() for t in tags if t and isinstance(t, (str,))]
255
- tags = tags[:3]
256
-
257
- return jsonify({
258
- "assistant_reply": parsed.get("assistant_reply", ""),
259
- "code_snippet": parsed.get("code_snippet", ""),
260
- "updated_state": state,
261
- "suggested_tags": tags,
262
- "parse_ok": True,
263
- }), 200
264
-
265
- @app.route("/tag_reply", methods=["POST"])
266
- def tag_reply():
267
- data = request.get_json(force=True, silent=True)
268
- if not isinstance(data, dict):
269
- return jsonify({"error": "invalid request body"}), 400
270
-
271
- reply_content = data.get("reply")
272
- tags = data.get("tags", [])
273
- if not reply_content or not tags or not isinstance(tags, list):
274
- return jsonify({"error": "Missing 'reply' or 'tags' in request"}), 400
275
-
276
- tags_clean = [str(t).strip().lower() for t in tags if re.match(r'^[\w\-]{1,30}$', str(t).strip())]
277
- if not tags_clean:
278
- return jsonify({"error": "No valid tags provided"}), 400
279
-
280
- assistant_state = data.get("assistant_state", {})
281
- state = {
282
- "conversationSummary": assistant_state.get("conversationSummary", ""),
283
- "language": assistant_state.get("language", "python"),
284
- "taggedReplies": assistant_state.get("taggedReplies", []),
285
- }
286
-
287
- state["taggedReplies"].append({"reply": reply_content, "tags": tags_clean})
288
- logger.info("Tagged reply saved: %s", tags_clean)
289
- return jsonify({"message": "Reply saved", "updated_state": state}), 200
290
 
291
  @app.route("/ping", methods=["GET"])
292
  def ping():
293
  return jsonify({"status": "ok"})
294
 
295
  if __name__ == "__main__":
296
- port = int(os.getenv("PORT", "7860"))
297
  app.run(host="0.0.0.0", port=port, debug=True)
 
1
  #!/usr/bin/env python3
 
2
  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:
20
+ logger.error("GROQ_API_KEY not set in environment")
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 ---
28
+ llm = ChatGroq(
29
+ model=os.getenv("LLM_MODEL", "meta-llama/llama-4-scout-17b-16e-instruct"),
30
+ temperature=0.1,
31
+ max_tokens=2048,
32
+ api_key=GROQ_API_KEY,
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}
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
 
148
+ # Detect language from last user message if possible
149
+ last_user_msg = ""
150
+ for msg in reversed(chat_history):
151
+ if msg.get("role") == "user" and msg.get("content"):
152
+ last_user_msg = msg["content"]
153
+ break
154
+ detected_lang = detect_language(last_user_msg, default=language)
155
+ if detected_lang.lower() != language.lower():
156
+ logger.info(f"Language changed from {language} to {detected_lang}")
157
+ language = detected_lang
158
 
159
+ # Build messages for LLM
160
+ messages = build_llm_messages(SYSTEM_PROMPT, chat_history, conversation_summary, language)
 
161
 
 
162
  try:
163
+ logger.info("Invoking LLM...")
164
+ llm_response = llm.invoke(messages)
165
+ raw_text = getattr(llm_response, "content", str(llm_response))
166
+ logger.info(f"LLM raw response: {raw_text}")
167
+
168
+ parsed = extract_json_from_response(raw_text)
169
+ if not parsed:
170
+ raise ValueError("Failed to parse JSON from LLM response")
171
+
172
+ # Validate keys
173
+ required_keys = {"assistant_reply", "code_snippet", "state_updates", "suggested_tags"}
174
+ if not required_keys.issubset(parsed.keys()):
175
+ raise ValueError(f"Missing keys in LLM response JSON: {required_keys - parsed.keys()}")
176
+
177
+ # Update state
178
+ state_updates = parsed.get("state_updates", {})
179
+ conversation_summary = state_updates.get("conversationSummary", conversation_summary)
180
+ language = state_updates.get("language", language)
181
+
182
+ # Compose final assistant reply with optional code snippet
183
+ assistant_reply = parsed["assistant_reply"].strip()
184
+ code_snippet = parsed["code_snippet"].strip()
185
+ if code_snippet:
186
+ # Unescape newlines and backslashes for display
187
+ code_snippet_display = code_snippet.replace("\\n", "\n").replace("\\\\", "\\")
188
+ assistant_reply += f"\n\n```{language.lower()}\n{code_snippet_display}\n```"
189
+
190
+ # Prepare response
191
+ response = {
192
+ "assistant_reply": assistant_reply,
193
+ "updated_state": {
194
+ "conversationSummary": conversation_summary,
195
+ "language": language,
196
+ },
197
+ "suggested_tags": parsed.get("suggested_tags", []),
198
+ }
199
+ return jsonify(response)
200
 
201
+ except Exception as e:
202
+ logger.exception("Error during LLM invocation or parsing")
 
 
 
203
  return jsonify({
204
+ "assistant_reply": LLM_PARSE_ERROR_MESSAGE,
205
+ "updated_state": {
206
+ "conversationSummary": conversation_summary,
207
+ "language": language,
208
+ },
209
  "suggested_tags": [],
210
+ "error": str(e),
211
+ }), 500
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
212
 
213
  @app.route("/ping", methods=["GET"])
214
  def ping():
215
  return jsonify({"status": "ok"})
216
 
217
  if __name__ == "__main__":
218
+ port = int(os.getenv("PORT", 7860))
219
  app.run(host="0.0.0.0", port=port, debug=True)