alaselababatunde commited on
Commit
d48566f
·
1 Parent(s): a55a81e
Files changed (1) hide show
  1. main.py +58 -171
main.py CHANGED
@@ -1,9 +1,5 @@
1
  import os
2
- import json
3
  import tempfile
4
- import traceback
5
- from typing import Optional
6
-
7
  from fastapi import FastAPI, UploadFile, File, Header, HTTPException, Body
8
  from fastapi.middleware.cors import CORSMiddleware
9
  from pydantic import BaseModel
@@ -12,15 +8,15 @@ from langchain.prompts import PromptTemplate
12
  from langchain_huggingface import HuggingFaceEndpoint
13
  from langdetect import detect, DetectorFactory
14
  from huggingface_hub.utils import HfHubHTTPError
15
- from smebuilder_vector import retriever # retriever that exposes .get_relevant_documents(...)
16
 
 
17
  DetectorFactory.seed = 0
18
 
19
- # ----------------- CONFIG -----------------
20
  SPITCH_API_KEY = os.getenv("SPITCH_API_KEY")
21
  HF_MODEL = os.getenv("HF_MODEL", "deepseek-ai/deepseek-coder-1.3b-instruct")
22
  FRONTEND_ORIGIN = os.getenv("ALLOWED_ORIGIN", "*")
23
- PROJECT_API_KEY = os.getenv("PROJECT_API_KEY", "")
24
 
25
  if not SPITCH_API_KEY:
26
  raise RuntimeError("Set SPITCH_API_KEY in environment before starting.")
@@ -30,14 +26,13 @@ os.environ["SPITCH_API_KEY"] = SPITCH_API_KEY
30
  spitch_client = Spitch()
31
 
32
  # HuggingFace LLM
33
- # NOTE: pass generation params explicitly (pydantic validation requires explicit params)
34
  llm = HuggingFaceEndpoint(
35
  repo_id=HF_MODEL,
36
  temperature=0.7,
37
  top_p=0.9,
38
  do_sample=True,
39
  repetition_penalty=1.1,
40
- max_new_tokens=2048,
41
  )
42
 
43
  # FastAPI app
@@ -57,11 +52,10 @@ chat_template = """You are DevAssist, an AI coding assistant.
57
 
58
  Guidelines:
59
  - Always format responses in Markdown.
60
- - Do NOT use inline formatting like bold (**), italics (*), or underlines.
61
- - Use plain text section headers (e.g., Explanation:, Steps to Fix:, Fixed Code:).
62
- - Use bullet points (-, 1.) for steps.
63
- - Use fenced code blocks (```python ... ```) for code.
64
- - Be friendly yet professional; explain step by step.
65
 
66
  Question: {question}
67
 
@@ -69,16 +63,16 @@ Answer:
69
  """
70
 
71
  stt_chat_template = """You are DevAssist, an AI coding assistant.
72
- - The input is transcribed speech. Interpret it as a dev question.
73
- - Provide clear answers with code examples (use markdown triple backticks).
74
- - If input is unclear, ask a clarifying question.
75
 
76
  Spoken Question: {speech}
77
  Answer:
78
  """
79
 
80
  autodoc_template = """You are DevAssist DocBot.
81
- - Read the code and produce professional documentation in markdown.
82
 
83
  Code: {code}
84
  Documentation:
@@ -86,24 +80,21 @@ Documentation:
86
 
87
  sme_template = """
88
  You are a senior full-stack engineer specializing in modern front-end development.
89
- Your job is to generate production-ready code for websites and apps.
90
 
91
  Guidelines:
92
  - Always return three separate files: index.html, styles.css, and script.js
93
- - HTML must be semantic, responsive, and mobile-first (include <meta name="viewport">)
94
- - CSS should use Flexbox/Grid and include hover/transition effects
95
- - JavaScript should add interactivity (e.g. button actions, basic animations, toggles)
96
- - Include a hero section, a feature grid, testimonials, and footer
97
- - Use realistic content (avoid lorem ipsum), sensible copy, and accessible markup
98
- - Return only valid JSON with the keys: "files" -> { "index.html": "...", "styles.css": "...", "script.js": "..." }
99
 
100
- User Prompt:
101
- {user_prompt}
102
 
103
- Context:
104
- {context}
105
-
106
- Return:
107
  """
108
 
109
  # ----------------- CHAINS -----------------
@@ -120,9 +111,9 @@ class AutoDocRequest(BaseModel):
120
  code: str
121
 
122
  # ----------------- AUTH -----------------
123
- def check_auth(authorization: Optional[str] = None):
124
- if not PROJECT_API_KEY:
125
- # No API key enforced in this environment
126
  return
127
  if not authorization or not authorization.startswith("Bearer "):
128
  raise HTTPException(status_code=401, detail="Missing bearer token")
@@ -130,29 +121,13 @@ def check_auth(authorization: Optional[str] = None):
130
  if token != PROJECT_API_KEY:
131
  raise HTTPException(status_code=403, detail="Invalid token")
132
 
133
- # ----------------- HELPERS -----------------
134
- def try_parse_json(maybe_str: str):
135
- """Try to parse JSON; if fails, return None."""
136
- try:
137
- return json.loads(maybe_str)
138
- except Exception:
139
- # attempt to find a JSON substring
140
- import re
141
- m = re.search(r"\{[\s\S]*\}\s*$", maybe_str.strip())
142
- if m:
143
- try:
144
- return json.loads(m.group(0))
145
- except Exception:
146
- return None
147
- return None
148
-
149
  # ----------------- ENDPOINTS -----------------
150
  @app.get("/")
151
  def root():
152
- return {"status": "DevAssist AI Backend running"}
153
 
154
  @app.post("/chat")
155
- def chat(req: ChatRequest, authorization: Optional[str] = Header(None)):
156
  check_auth(authorization)
157
  try:
158
  answer = chat_chain.invoke({"question": req.question})
@@ -163,123 +138,72 @@ def chat(req: ChatRequest, authorization: Optional[str] = Header(None)):
163
  raise e
164
 
165
  @app.post("/stt")
166
- async def stt_audio(
167
- file: UploadFile = File(...),
168
- lang_hint: Optional[str] = None,
169
- authorization: Optional[str] = Header(None),
170
- ):
171
  check_auth(authorization)
172
- suffix = os.path.splitext(file.filename)[1] or ".wav"
173
 
174
- # create temp file
175
  with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tf:
176
- content = await file.read()
177
- tf.write(content)
178
  tmp_path = tf.name
179
 
180
  try:
181
- # transcribe
182
  if lang_hint:
183
  resp = spitch_client.speech.transcribe(language=lang_hint, content=open(tmp_path, "rb").read())
184
  else:
185
  resp = spitch_client.speech.transcribe(content=open(tmp_path, "rb").read())
186
  except Exception:
187
- # fallback to english transcription if something fails
188
  resp = spitch_client.speech.transcribe(language="en", content=open(tmp_path, "rb").read())
189
 
190
  transcription = getattr(resp, "text", "") or (resp.get("text", "") if isinstance(resp, dict) else "")
 
191
  try:
192
  detected_lang = detect(transcription) if transcription.strip() else "en"
193
  except Exception:
194
- detected_lang = "en"
195
 
196
  translation = transcription
197
  if detected_lang != "en":
198
  try:
199
  translation_resp = spitch_client.text.translate(text=transcription, source=detected_lang, target="en")
200
- translation = getattr(translation_resp, "text", "") or (translation_resp.get("text", "") if isinstance(translation_resp, dict) else translation)
201
  except Exception:
202
  translation = transcription
203
 
204
- # call the STT chain (LLM)
205
- try:
206
- reply = stt_chain.invoke({"speech": translation})
207
- except Exception as e:
208
- # on LLM problems return transcription anyway
209
- reply = f"(LLM error) Transcription: {translation}"
210
-
211
- # cleanup temp file to avoid storage bloat
212
- try:
213
- os.remove(tmp_path)
214
- except Exception:
215
- pass
216
-
217
  return {
218
  "transcription": transcription,
219
  "detected_language": detected_lang,
220
  "translation": translation,
221
- "reply": reply.strip() if isinstance(reply, str) else str(reply),
222
  }
223
 
224
  @app.post("/autodoc")
225
- def autodoc(req: AutoDocRequest, authorization: Optional[str] = Header(None)):
226
  check_auth(authorization)
227
  docs = autodoc_chain.invoke({"code": req.code})
228
  return {"documentation": docs.strip() if isinstance(docs, str) else str(docs)}
229
 
230
  @app.post("/sme/generate")
231
- async def sme_generate(payload: dict = Body(...), authorization: Optional[str] = Header(None)):
232
- """
233
- Payload expected: { "user_prompt": "Create ...", (optionally) "force_simple": true }
234
- Returns: success, data (if success) or error
235
- """
236
  check_auth(authorization)
237
- user_prompt = payload.get("user_prompt", "")
238
- if not user_prompt or not user_prompt.strip():
239
- raise HTTPException(status_code=400, detail="user_prompt is required")
240
-
241
- # Get context from retriever (if available)
242
  try:
243
- context_docs = retriever.get_relevant_documents(user_prompt) if retriever else []
244
- context = "\n\n".join([getattr(d, "page_content", str(d)) for d in context_docs]) if context_docs else "No extra context"
245
- except Exception:
246
- context = "No extra context"
247
-
248
- # Invoke SME chain
249
- try:
250
- raw = sme_chain.invoke({"user_prompt": user_prompt, "context": context})
251
- # Try to parse returned JSON
252
- parsed = None
253
- if isinstance(raw, str):
254
- parsed = try_parse_json(raw)
255
- elif isinstance(raw, dict):
256
- parsed = raw
257
-
258
- if parsed:
259
- return {"success": True, "data": parsed}
260
- else:
261
- # If model didn't return strict JSON, return helpful error + raw output so frontend can show it
262
- return {"success": False, "error": "LLM did not return valid JSON", "raw": raw}
263
  except HfHubHTTPError as e:
264
  if "exceeded" in str(e).lower() or "quota" in str(e).lower():
265
  return {"success": False, "error": "⚠️ Token quota for today has been used. Please come back in 24 hours."}
266
  raise e
267
- except Exception as e:
268
- # Debug info for devs (but don't leak sensitive internals in production)
269
- return {"success": False, "error": "SME generation failed", "details": str(e), "trace": traceback.format_exc()}
270
 
271
  @app.post("/sme/speech-generate")
272
- async def sme_speech_generate(
273
- file: UploadFile = File(...),
274
- lang_hint: Optional[str] = None,
275
- authorization: Optional[str] = Header(None),
276
- ):
277
  check_auth(authorization)
278
- suffix = os.path.splitext(file.filename)[1] or ".wav"
279
 
 
280
  with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tf:
281
- content = await file.read()
282
- tf.write(content)
283
  tmp_path = tf.name
284
 
285
  try:
@@ -291,72 +215,35 @@ async def sme_speech_generate(
291
  resp = spitch_client.speech.transcribe(language="en", content=open(tmp_path, "rb").read())
292
 
293
  transcription = getattr(resp, "text", "") or (resp.get("text", "") if isinstance(resp, dict) else "")
 
294
  try:
295
  detected_lang = detect(transcription) if transcription.strip() else "en"
296
  except Exception:
297
- detected_lang = "en"
298
 
299
  translation = transcription
300
  if detected_lang != "en":
301
  try:
302
  translation_resp = spitch_client.text.translate(text=transcription, source=detected_lang, target="en")
303
- translation = getattr(translation_resp, "text", "") or (translation_resp.get("text", "") if isinstance(translation_resp, dict) else translation)
304
  except Exception:
305
  translation = transcription
306
 
307
- # Get context docs for the transcribed prompt
308
  try:
309
- context_docs = retriever.get_relevant_documents(translation) if retriever else []
310
- context = "\n\n".join([getattr(d, "page_content", str(d)) for d in context_docs]) if context_docs else "No extra context"
311
- except Exception:
312
- context = "No extra context"
313
-
314
- # Invoke SME chain
315
- try:
316
- raw = sme_chain.invoke({"user_prompt": translation, "context": context})
317
- parsed = None
318
- if isinstance(raw, str):
319
- parsed = try_parse_json(raw)
320
- elif isinstance(raw, dict):
321
- parsed = raw
322
-
323
- # cleanup tmp file
324
- try:
325
- os.remove(tmp_path)
326
- except Exception:
327
- pass
328
-
329
- if parsed:
330
- return {
331
- "success": True,
332
- "transcription": transcription,
333
- "detected_language": detected_lang,
334
- "translation": translation,
335
- "sme_site": parsed,
336
- }
337
- else:
338
- return {
339
- "success": False,
340
- "error": "LLM did not return valid JSON",
341
- "raw": raw,
342
- "transcription": transcription,
343
- "detected_language": detected_lang,
344
- "translation": translation,
345
- }
346
  except HfHubHTTPError as e:
347
- try:
348
- os.remove(tmp_path)
349
- except Exception:
350
- pass
351
  if "exceeded" in str(e).lower() or "quota" in str(e).lower():
352
  return {"success": False, "error": "⚠️ Token quota for today has been used. Please come back in 24 hours."}
353
  raise e
354
- except Exception as e:
355
- try:
356
- os.remove(tmp_path)
357
- except Exception:
358
- pass
359
- return {"success": False, "error": "SME generation failed", "details": str(e), "trace": traceback.format_exc()}
360
 
361
  # ----------------- MAIN -----------------
362
  if __name__ == "__main__":
 
1
  import os
 
2
  import tempfile
 
 
 
3
  from fastapi import FastAPI, UploadFile, File, Header, HTTPException, Body
4
  from fastapi.middleware.cors import CORSMiddleware
5
  from pydantic import BaseModel
 
8
  from langchain_huggingface import HuggingFaceEndpoint
9
  from langdetect import detect, DetectorFactory
10
  from huggingface_hub.utils import HfHubHTTPError
11
+ from smebuilder_vector import retriever # Retriever for context injection
12
 
13
+ # ----------------- CONFIG -----------------
14
  DetectorFactory.seed = 0
15
 
 
16
  SPITCH_API_KEY = os.getenv("SPITCH_API_KEY")
17
  HF_MODEL = os.getenv("HF_MODEL", "deepseek-ai/deepseek-coder-1.3b-instruct")
18
  FRONTEND_ORIGIN = os.getenv("ALLOWED_ORIGIN", "*")
19
+ PROJECT_API_KEY = os.getenv("PROJECT_API_KEY", "super-secret-123") # Default if not set
20
 
21
  if not SPITCH_API_KEY:
22
  raise RuntimeError("Set SPITCH_API_KEY in environment before starting.")
 
26
  spitch_client = Spitch()
27
 
28
  # HuggingFace LLM
 
29
  llm = HuggingFaceEndpoint(
30
  repo_id=HF_MODEL,
31
  temperature=0.7,
32
  top_p=0.9,
33
  do_sample=True,
34
  repetition_penalty=1.1,
35
+ max_new_tokens=2048
36
  )
37
 
38
  # FastAPI app
 
52
 
53
  Guidelines:
54
  - Always format responses in Markdown.
55
+ - Use section headers: Explanation:, Steps:, Fixed Code:
56
+ - Use bullet points for steps.
57
+ - Use fenced code blocks for code.
58
+ - Be friendly yet professional.
 
59
 
60
  Question: {question}
61
 
 
63
  """
64
 
65
  stt_chat_template = """You are DevAssist, an AI coding assistant.
66
+ The input is transcribed speech. Interpret it as a developer question.
67
+ Provide clear answers with code examples.
68
+ If unclear, ask for clarification.
69
 
70
  Spoken Question: {speech}
71
  Answer:
72
  """
73
 
74
  autodoc_template = """You are DevAssist DocBot.
75
+ Read the code and produce professional documentation in markdown.
76
 
77
  Code: {code}
78
  Documentation:
 
80
 
81
  sme_template = """
82
  You are a senior full-stack engineer specializing in modern front-end development.
83
+ Your job is to generate **production-ready code** for websites and apps.
84
 
85
  Guidelines:
86
  - Always return three separate files: index.html, styles.css, and script.js
87
+ - HTML must be semantic, responsive, and mobile-first
88
+ - CSS should use Flexbox/Grid with hover/transition effects
89
+ - JavaScript must add interactivity (animations, toggles, button actions)
90
+ - Include hero, feature grid, testimonials, and footer
91
+ - Use realistic content (no lorem ipsum, no placeholders)
92
+ - Return ONLY valid JSON: { "files": { "index.html": "...", "styles.css": "...", "script.js": "..." } }
93
 
94
+ Prompt: {user_prompt}
95
+ Context: {context}
96
 
97
+ Output:
 
 
 
98
  """
99
 
100
  # ----------------- CHAINS -----------------
 
111
  code: str
112
 
113
  # ----------------- AUTH -----------------
114
+ def check_auth(authorization: str | None):
115
+ """Validate Bearer token against PROJECT_API_KEY"""
116
+ if not PROJECT_API_KEY: # If not set, skip auth
117
  return
118
  if not authorization or not authorization.startswith("Bearer "):
119
  raise HTTPException(status_code=401, detail="Missing bearer token")
 
121
  if token != PROJECT_API_KEY:
122
  raise HTTPException(status_code=403, detail="Invalid token")
123
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
  # ----------------- ENDPOINTS -----------------
125
  @app.get("/")
126
  def root():
127
+ return {"status": "DevAssist AI Backend running"}
128
 
129
  @app.post("/chat")
130
+ def chat(req: ChatRequest, authorization: str | None = Header(None)):
131
  check_auth(authorization)
132
  try:
133
  answer = chat_chain.invoke({"question": req.question})
 
138
  raise e
139
 
140
  @app.post("/stt")
141
+ async def stt_audio(file: UploadFile = File(...), lang_hint: str | None = None, authorization: str | None = Header(None)):
 
 
 
 
142
  check_auth(authorization)
 
143
 
144
+ suffix = os.path.splitext(file.filename)[1] or ".wav"
145
  with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tf:
146
+ tf.write(await file.read())
 
147
  tmp_path = tf.name
148
 
149
  try:
 
150
  if lang_hint:
151
  resp = spitch_client.speech.transcribe(language=lang_hint, content=open(tmp_path, "rb").read())
152
  else:
153
  resp = spitch_client.speech.transcribe(content=open(tmp_path, "rb").read())
154
  except Exception:
 
155
  resp = spitch_client.speech.transcribe(language="en", content=open(tmp_path, "rb").read())
156
 
157
  transcription = getattr(resp, "text", "") or (resp.get("text", "") if isinstance(resp, dict) else "")
158
+ detected_lang = "en"
159
  try:
160
  detected_lang = detect(transcription) if transcription.strip() else "en"
161
  except Exception:
162
+ pass
163
 
164
  translation = transcription
165
  if detected_lang != "en":
166
  try:
167
  translation_resp = spitch_client.text.translate(text=transcription, source=detected_lang, target="en")
168
+ translation = getattr(translation_resp, "text", "") or translation_resp.get("text", "")
169
  except Exception:
170
  translation = transcription
171
 
172
+ reply = stt_chain.invoke({"speech": translation})
 
 
 
 
 
 
 
 
 
 
 
 
173
  return {
174
  "transcription": transcription,
175
  "detected_language": detected_lang,
176
  "translation": translation,
177
+ "reply": reply.strip() if isinstance(reply, str) else str(reply)
178
  }
179
 
180
  @app.post("/autodoc")
181
+ def autodoc(req: AutoDocRequest, authorization: str | None = Header(None)):
182
  check_auth(authorization)
183
  docs = autodoc_chain.invoke({"code": req.code})
184
  return {"documentation": docs.strip() if isinstance(docs, str) else str(docs)}
185
 
186
  @app.post("/sme/generate")
187
+ async def sme_generate(payload: dict = Body(...), authorization: str | None = Header(None)):
 
 
 
 
188
  check_auth(authorization)
 
 
 
 
 
189
  try:
190
+ user_prompt = payload.get("user_prompt", "")
191
+ context_docs = retriever.get_relevant_documents(user_prompt)
192
+ context = "\n".join([doc.page_content for doc in context_docs]) if context_docs else "No extra context"
193
+ response = sme_chain.invoke({"user_prompt": user_prompt, "context": context})
194
+ return {"success": True, "data": response}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
195
  except HfHubHTTPError as e:
196
  if "exceeded" in str(e).lower() or "quota" in str(e).lower():
197
  return {"success": False, "error": "⚠️ Token quota for today has been used. Please come back in 24 hours."}
198
  raise e
 
 
 
199
 
200
  @app.post("/sme/speech-generate")
201
+ async def sme_speech_generate(file: UploadFile = File(...), lang_hint: str | None = None, authorization: str | None = Header(None)):
 
 
 
 
202
  check_auth(authorization)
 
203
 
204
+ suffix = os.path.splitext(file.filename)[1] or ".wav"
205
  with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tf:
206
+ tf.write(await file.read())
 
207
  tmp_path = tf.name
208
 
209
  try:
 
215
  resp = spitch_client.speech.transcribe(language="en", content=open(tmp_path, "rb").read())
216
 
217
  transcription = getattr(resp, "text", "") or (resp.get("text", "") if isinstance(resp, dict) else "")
218
+ detected_lang = "en"
219
  try:
220
  detected_lang = detect(transcription) if transcription.strip() else "en"
221
  except Exception:
222
+ pass
223
 
224
  translation = transcription
225
  if detected_lang != "en":
226
  try:
227
  translation_resp = spitch_client.text.translate(text=transcription, source=detected_lang, target="en")
228
+ translation = getattr(translation_resp, "text", "") or translation_resp.get("text", "")
229
  except Exception:
230
  translation = transcription
231
 
 
232
  try:
233
+ context_docs = retriever.get_relevant_documents(translation)
234
+ context = "\n".join([doc.page_content for doc in context_docs]) if context_docs else "No extra context"
235
+ sme_response = sme_chain.invoke({"user_prompt": translation, "context": context})
236
+ return {
237
+ "success": True,
238
+ "transcription": transcription,
239
+ "detected_language": detected_lang,
240
+ "translation": translation,
241
+ "sme_site": sme_response
242
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
243
  except HfHubHTTPError as e:
 
 
 
 
244
  if "exceeded" in str(e).lower() or "quota" in str(e).lower():
245
  return {"success": False, "error": "⚠️ Token quota for today has been used. Please come back in 24 hours."}
246
  raise e
 
 
 
 
 
 
247
 
248
  # ----------------- MAIN -----------------
249
  if __name__ == "__main__":