alaselababatunde commited on
Commit
c27fb7c
·
1 Parent(s): 4b43cb5
Files changed (3) hide show
  1. .~lock.SME_Builder_Dataset.csv# +0 -1
  2. main.py +157 -41
  3. smebuilder_vector.py +26 -16
.~lock.SME_Builder_Dataset.csv# DELETED
@@ -1 +0,0 @@
1
- ,alash-studios,alash-studios-HP-EliteBook-840-G3,19.09.2025 18:30,file:///home/alash-studios/.config/libreoffice/4;
 
 
main.py CHANGED
@@ -1,5 +1,9 @@
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,11 +12,11 @@ from langchain.prompts import PromptTemplate
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
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", "*")
@@ -26,13 +30,14 @@ os.environ["SPITCH_API_KEY"] = SPITCH_API_KEY
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
@@ -81,21 +86,24 @@ Documentation:
81
 
82
  sme_template = """
83
  You are a senior full-stack engineer specializing in modern front-end development.
84
- Your job is to generate **production-ready code** for websites and apps.
85
 
86
  Guidelines:
87
  - Always return three separate files: index.html, styles.css, and script.js
88
- - HTML must be semantic, responsive, and mobile-first (use <meta viewport>)
89
  - CSS should use Flexbox/Grid and include hover/transition effects
90
- - JavaScript must add interactivity (e.g. button actions, animations, toggles)
91
- - Include a hero section, feature grid, testimonials, and footer
92
- - Fill with realistic content (no lorem ipsum, no placeholders)
93
- - Return **only valid JSON** with keys: "files" { "index.html": "...", "styles.css": "...", "script.js": "..." }
 
 
 
94
 
95
- Prompt: {user_prompt}
96
- Context: {context}
97
 
98
- Output:
99
  """
100
 
101
  # ----------------- CHAINS -----------------
@@ -112,8 +120,9 @@ class AutoDocRequest(BaseModel):
112
  code: str
113
 
114
  # ----------------- AUTH -----------------
115
- def check_auth(authorization: str | None):
116
  if not PROJECT_API_KEY:
 
117
  return
118
  if not authorization or not authorization.startswith("Bearer "):
119
  raise HTTPException(status_code=401, detail="Missing bearer token")
@@ -121,13 +130,29 @@ def check_auth(authorization: str | None):
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,20 +163,28 @@ def chat(req: ChatRequest, authorization: str | None = Header(None)):
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 "")
@@ -164,44 +197,89 @@ async def stt_audio(file: UploadFile = File(...), lang_hint: str | None = None,
164
  if detected_lang != "en":
165
  try:
166
  translation_resp = spitch_client.text.translate(text=transcription, source=detected_lang, target="en")
167
- translation = getattr(translation_resp, "text", "") or translation_resp.get("text", "")
168
  except Exception:
169
  translation = transcription
170
 
171
- reply = stt_chain.invoke({"speech": translation})
 
 
 
 
 
 
 
 
 
 
 
 
172
  return {
173
  "transcription": transcription,
174
  "detected_language": detected_lang,
175
  "translation": translation,
176
- "reply": reply.strip() if isinstance(reply, str) else str(reply)
177
  }
178
 
179
  @app.post("/autodoc")
180
- def autodoc(req: AutoDocRequest, authorization: str | None = Header(None)):
181
  check_auth(authorization)
182
  docs = autodoc_chain.invoke({"code": req.code})
183
  return {"documentation": docs.strip() if isinstance(docs, str) else str(docs)}
184
 
185
  @app.post("/sme/generate")
186
- async def sme_generate(payload: dict = Body(...)):
 
 
 
 
 
 
 
 
 
 
187
  try:
188
- user_prompt = payload.get("user_prompt", "")
189
- context_docs = retriever.get_relevant_documents(user_prompt)
190
- context = "\n".join([doc.page_content for doc in context_docs]) if context_docs else "No extra context"
191
- response = sme_chain.invoke({"user_prompt": user_prompt, "context": context})
192
- return {"success": True, "data": response}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
193
  except HfHubHTTPError as e:
194
  if "exceeded" in str(e).lower() or "quota" in str(e).lower():
195
  return {"success": False, "error": "⚠️ Token quota for today has been used. Please come back in 24 hours."}
196
  raise e
 
 
 
197
 
198
  @app.post("/sme/speech-generate")
199
- async def sme_speech_generate(file: UploadFile = File(...), lang_hint: str | None = None, authorization: str | None = Header(None)):
 
 
 
 
200
  check_auth(authorization)
201
-
202
  suffix = os.path.splitext(file.filename)[1] or ".wav"
 
203
  with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tf:
204
- tf.write(await file.read())
 
205
  tmp_path = tf.name
206
 
207
  try:
@@ -222,25 +300,63 @@ async def sme_speech_generate(file: UploadFile = File(...), lang_hint: str | Non
222
  if detected_lang != "en":
223
  try:
224
  translation_resp = spitch_client.text.translate(text=transcription, source=detected_lang, target="en")
225
- translation = getattr(translation_resp, "text", "") or translation_resp.get("text", "")
226
  except Exception:
227
  translation = transcription
228
 
 
 
 
 
 
 
 
 
229
  try:
230
- context_docs = retriever.get_relevant_documents(translation)
231
- context = "\n".join([doc.page_content for doc in context_docs]) if context_docs else "No extra context"
232
- sme_response = sme_chain.invoke({"user_prompt": translation, "context": context})
233
- return {
234
- "success": True,
235
- "transcription": transcription,
236
- "detected_language": detected_lang,
237
- "translation": translation,
238
- "sme_site": sme_response
239
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
240
  except HfHubHTTPError as e:
 
 
 
 
241
  if "exceeded" in str(e).lower() or "quota" in str(e).lower():
242
  return {"success": False, "error": "⚠️ Token quota for today has been used. Please come back in 24 hours."}
243
  raise e
 
 
 
 
 
 
244
 
245
  # ----------------- MAIN -----------------
246
  if __name__ == "__main__":
 
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
  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", "*")
 
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
 
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
  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
  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
  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 "")
 
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:
 
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__":
smebuilder_vector.py CHANGED
@@ -11,24 +11,23 @@ COLLECTION_NAME = "landing_page_generation_examples"
11
  EMBEDDING_MODEL = os.getenv("HF_EMBEDDING_MODEL", "intfloat/e5-large-v2")
12
  HF_CACHE_DIR = os.getenv("HF_CACHE_DIR", "/app/huggingface_cache")
13
 
14
-
15
  os.makedirs(HF_CACHE_DIR, exist_ok=True)
16
  os.makedirs(DB_LOCATION, exist_ok=True)
17
 
18
  # ----------------- LOAD DATASET -----------------
19
  if not os.path.exists(DATASET_PATH):
 
20
  raise FileNotFoundError(f"Dataset file not found: {DATASET_PATH}")
21
 
22
  df = pd.read_csv(DATASET_PATH)
23
 
24
  # ----------------- EMBEDDINGS -----------------
25
- embeddings = HuggingFaceEmbeddings(
26
- model_name=EMBEDDING_MODEL
27
- )
28
 
29
  # ----------------- VECTOR STORE -----------------
30
- # Only add documents if DB is empty
31
- add_documents = not os.listdir(DB_LOCATION)
32
 
33
  vector_store = Chroma(
34
  collection_name=COLLECTION_NAME,
@@ -39,19 +38,30 @@ vector_store = Chroma(
39
  if add_documents:
40
  documents = []
41
  for i, row in df.iterrows():
42
- content = " ".join([
43
- str(row.get("prompt", "")),
44
- str(row.get("html_code", "")),
45
- str(row.get("css_code", "")),
46
- str(row.get("js_code", "")),
47
- str(row.get("sector", ""))
48
- ]).strip()
 
 
 
 
49
  documents.append(Document(page_content=content, metadata={"id": str(i)}))
50
-
51
  if documents:
52
  vector_store.add_documents(documents=documents)
53
 
54
  # ----------------- RETRIEVER -----------------
55
- retriever = vector_store.as_retriever(search_kwargs={"k": 20})
 
 
 
 
 
 
 
56
 
57
- print(f"Vector store ready with {vector_store._collection.count()} documents.")
 
11
  EMBEDDING_MODEL = os.getenv("HF_EMBEDDING_MODEL", "intfloat/e5-large-v2")
12
  HF_CACHE_DIR = os.getenv("HF_CACHE_DIR", "/app/huggingface_cache")
13
 
14
+ # ensure directories exist
15
  os.makedirs(HF_CACHE_DIR, exist_ok=True)
16
  os.makedirs(DB_LOCATION, exist_ok=True)
17
 
18
  # ----------------- LOAD DATASET -----------------
19
  if not os.path.exists(DATASET_PATH):
20
+ # If dataset is optional, consider returning an empty retriever. For now raise so developer notices.
21
  raise FileNotFoundError(f"Dataset file not found: {DATASET_PATH}")
22
 
23
  df = pd.read_csv(DATASET_PATH)
24
 
25
  # ----------------- EMBEDDINGS -----------------
26
+ embeddings = HuggingFaceEmbeddings(model_name=EMBEDDING_MODEL)
 
 
27
 
28
  # ----------------- VECTOR STORE -----------------
29
+ # if directory is empty then we should add documents; otherwise assume already persisted
30
+ add_documents = not bool(os.listdir(DB_LOCATION))
31
 
32
  vector_store = Chroma(
33
  collection_name=COLLECTION_NAME,
 
38
  if add_documents:
39
  documents = []
40
  for i, row in df.iterrows():
41
+ # build a single text blob per row combining prompt + code + sector
42
+ content_pieces = [
43
+ str(row.get("prompt", "")).strip(),
44
+ str(row.get("html_code", "")).strip(),
45
+ str(row.get("css_code", "")).strip(),
46
+ str(row.get("js_code", "")).strip(),
47
+ str(row.get("sector", "")).strip(),
48
+ ]
49
+ content = " \n".join([p for p in content_pieces if p])
50
+ if not content:
51
+ continue
52
  documents.append(Document(page_content=content, metadata={"id": str(i)}))
53
+
54
  if documents:
55
  vector_store.add_documents(documents=documents)
56
 
57
  # ----------------- RETRIEVER -----------------
58
+ retriever = vector_store.as_retriever(search_kwargs={"k": 8})
59
+
60
+ # Helpful info (no heavy introspection)
61
+ try:
62
+ # avoid private attributes; just confirm connectivity
63
+ count = len(vector_store._collection.get()["ids"]) if hasattr(vector_store, "_collection") else "unknown"
64
+ except Exception:
65
+ count = "unknown"
66
 
67
+ print(f"SME vector store initialized. collection={COLLECTION_NAME}, documents={count}")