File size: 8,928 Bytes
bb12219
 
3c9086c
2bab06e
bb12219
 
f0502f7
bb12219
cab03d6
bb12219
351bb59
d48566f
bb12219
d48566f
bb12219
 
 
ee2e099
f0502f7
2bab06e
bb12219
 
 
 
 
 
 
3c9086c
f0502f7
5e51aba
26fd760
 
 
 
d48566f
f0502f7
bb12219
3c9086c
bb12219
 
 
 
 
 
 
 
 
 
2bab06e
2df5630
e92af5d
 
 
d48566f
 
 
 
e92af5d
2df5630
e92af5d
 
 
 
bb12219
d48566f
 
 
f51917e
bb12219
f51917e
 
bb12219
 
d48566f
f51917e
bb12219
f51917e
 
bb12219
7b32b5c
caa2aff
d48566f
caa2aff
 
 
d48566f
 
 
 
 
c27fb7c
d48566f
 
caa2aff
d48566f
170f71d
 
f51917e
 
 
 
5e51aba
7b32b5c
2bab06e
bb12219
 
 
 
 
 
f51917e
d48566f
3c9086c
f0502f7
bb12219
 
 
 
 
 
3c9086c
 
 
 
 
 
2bab06e
3c9086c
 
 
 
 
bb12219
3c9086c
 
 
 
 
2bab06e
3c9086c
d48566f
bb12219
d48566f
bb12219
 
3c9086c
 
 
bb12219
2bab06e
3c9086c
2bab06e
3c9086c
f0502f7
3c9086c
bb12219
f51917e
d48566f
bb12219
 
 
d48566f
bb12219
 
 
 
 
d48566f
bb12219
 
 
3c9086c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2bab06e
 
 
 
3c9086c
2bab06e
bb12219
 
d48566f
bb12219
3c9086c
 
f0502f7
f96cd6f
d48566f
c27fb7c
f51917e
2bab06e
 
 
3c9086c
 
 
 
9ec1122
 
 
 
3c9086c
9ec1122
 
 
3c9086c
9ec1122
 
 
 
 
3c9086c
9ec1122
3c9086c
 
f96cd6f
351bb59
f0502f7
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
import os
import tempfile
import traceback
from fastapi import FastAPI, UploadFile, File, Header, HTTPException, Body
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from spitch import Spitch
from langchain.prompts import PromptTemplate
from langchain_huggingface import HuggingFaceEndpoint
from langdetect import detect, DetectorFactory
from huggingface_hub.utils import HfHubHTTPError
from smebuilder_vector import retriever  # Retriever for context injection

# ----------------- CONFIG -----------------
DetectorFactory.seed = 0

SPITCH_API_KEY = os.getenv("SPITCH_API_KEY")
HF_MODEL = os.getenv("HF_MODEL", "deepseek-ai/deepseek-coder-1.3b-instruct")
FRONTEND_ORIGIN = os.getenv("ALLOWED_ORIGIN", "*")
PROJECT_API_KEY = os.getenv("PROJECT_API_KEY")

if not SPITCH_API_KEY:
    raise RuntimeError("Set SPITCH_API_KEY in environment before starting.")

os.environ["SPITCH_API_KEY"] = SPITCH_API_KEY
spitch_client = Spitch()

# HuggingFace LLM
llm = HuggingFaceEndpoint(
    repo_id=HF_MODEL,
    temperature=0.7,
    top_p=0.9,
    do_sample=True,
    repetition_penalty=1.1,
    max_new_tokens=2048
)

# ----------------- FASTAPI -----------------
app = FastAPI(title="DevAssist AI Backend (FastAPI + LangChain)")

app.add_middleware(
    CORSMiddleware,
    allow_origins=[FRONTEND_ORIGIN] if FRONTEND_ORIGIN != "*" else ["*"],
    allow_credentials=True,
    allow_methods=["GET", "POST", "OPTIONS"],
    allow_headers=["Authorization", "Content-Type"],
)

# ----------------- PROMPT TEMPLATES -----------------
chat_template = """You are DevAssist, an AI coding assistant.

Guidelines:
- Always format responses in Markdown.
- Use section headers: Explanation:, Steps:, Fixed Code:
- Use bullet points for steps.
- Use fenced code blocks for code.
- Be friendly yet professional.

Question: {question}

Answer:
"""

stt_chat_template = """You are DevAssist, an AI coding assistant.
The input is transcribed speech. Interpret it as a developer question.
Provide clear answers with code examples.
If unclear, ask for clarification.

Spoken Question: {speech}
Answer:
"""

autodoc_template = """You are DevAssist DocBot.
Read the code and produce professional documentation in markdown.

Code: {code}
Documentation:
"""

sme_template = """
You are a senior full-stack engineer specializing in modern front-end development.
Your job is to generate **production-ready code** for websites and apps.

Guidelines:
- Always return three separate files: index.html, styles.css, and script.js
- HTML must be semantic, responsive, and mobile-first
- CSS should use Flexbox/Grid with hover/transition effects
- JavaScript must add interactivity (animations, toggles, button actions)
- Include hero, feature grid, testimonials, and footer
- Use realistic content (no lorem ipsum, no placeholders)

Prompt: {user_prompt}
Context: {context}

Output:
"""

# ----------------- CHAINS -----------------
chat_chain = PromptTemplate(input_variables=["question"], template=chat_template) | llm
stt_chain = PromptTemplate(input_variables=["speech"], template=stt_chat_template) | llm
autodoc_chain = PromptTemplate(input_variables=["code"], template=autodoc_template) | llm
sme_chain = PromptTemplate(input_variables=["user_prompt", "context"], template=sme_template) | llm

# ----------------- REQUEST MODELS -----------------
class ChatRequest(BaseModel):
    question: str

class AutoDocRequest(BaseModel):
    code: str

# ----------------- AUTH -----------------
def check_auth(authorization: str | None):
    if not PROJECT_API_KEY:
        return
    if not authorization or not authorization.startswith("Bearer "):
        raise HTTPException(status_code=401, detail="Missing bearer token")
    token = authorization.split(" ", 1)[1]
    if token != PROJECT_API_KEY:
        raise HTTPException(status_code=403, detail="Invalid token")

# ----------------- HELPER FUNCTIONS -----------------
def run_chain(chain, input_dict: dict):
    """
    Safely run a LangChain PromptTemplate | HuggingFaceEndpoint chain.
    Converts output to string and captures errors.
    """
    try:
        # Render template
        if hasattr(chain, "prompt"):
            prompt_text = chain.prompt.format(**input_dict)
        else:
            prompt_text = str(input_dict)

        # Generate using HuggingFaceEndpoint (expects str input)
        output = chain.llm.generate([{"role": "user", "content": prompt_text}])
        return output.generations[0][0].text.strip()
    except Exception:
        return {"success": False, "error": "⚠️ LLM error", "details": traceback.format_exc()}

async def process_audio(file: UploadFile, lang_hint: str | None = None):
    suffix = os.path.splitext(file.filename)[1] or ".wav"
    with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tf:
        tf.write(await file.read())
        tmp_path = tf.name

    with open(tmp_path, "rb") as f:
        audio_bytes = f.read()

    try:
        if lang_hint:
            resp = spitch_client.speech.transcribe(language=lang_hint, content=audio_bytes)
        else:
            resp = spitch_client.speech.transcribe(content=audio_bytes)
    except Exception:
        resp = spitch_client.speech.transcribe(language="en", content=audio_bytes)

    transcription = getattr(resp, "text", "") or (resp.get("text", "") if isinstance(resp, dict) else "")
    detected_lang = "en"
    try:
        detected_lang = detect(transcription) if transcription.strip() else "en"
    except Exception:
        pass

    translation = transcription
    if detected_lang != "en":
        try:
            translation_resp = spitch_client.text.translate(text=transcription, source=detected_lang, target="en")
            translation = getattr(translation_resp, "text", "") or translation_resp.get("text", "")
        except Exception:
            translation = transcription

    return transcription, detected_lang, translation

# ----------------- ENDPOINTS -----------------
@app.get("/")
def root():
    return {"status": "✅ DevAssist AI Backend running"}

@app.post("/chat")
def chat(req: ChatRequest, authorization: str | None = Header(None)):
    check_auth(authorization)
    result = run_chain(chat_chain, {"question": req.question})
    return result if isinstance(result, dict) else {"reply": result}

@app.post("/stt")
async def stt_audio(file: UploadFile = File(...), lang_hint: str | None = None, authorization: str | None = Header(None)):
    check_auth(authorization)
    transcription, detected_lang, translation = await process_audio(file, lang_hint)
    result = run_chain(stt_chain, {"speech": translation})
    return {
        "transcription": transcription,
        "detected_language": detected_lang,
        "translation": translation,
        "reply": result if isinstance(result, str) else result.get("reply", "")
    }

@app.post("/autodoc")
def autodoc(req: AutoDocRequest, authorization: str | None = Header(None)):
    check_auth(authorization)
    result = run_chain(autodoc_chain, {"code": req.code})
    return result if isinstance(result, dict) else {"documentation": result}

@app.post("/sme/generate")
async def sme_generate(payload: dict = Body(...), authorization: str | None = Header(None)):
    check_auth(authorization)
    try:
        user_prompt = payload.get("user_prompt", "")
        context_docs = retriever.get_relevant_documents(user_prompt)
        context = "\n".join([doc.page_content for doc in context_docs]) if context_docs else "No extra context"
        result = run_chain(sme_chain, {"user_prompt": user_prompt, "context": context})
        return {"success": True, "data": result if isinstance(result, str) else result.get("reply", "")}
    except Exception:
        return {"success": False, "error": "⚠️ LLM error", "details": traceback.format_exc()}

@app.post("/sme/speech-generate")
async def sme_speech_generate(file: UploadFile = File(...), lang_hint: str | None = None, authorization: str | None = Header(None)):
    check_auth(authorization)
    transcription, detected_lang, translation = await process_audio(file, lang_hint)
    try:
        context_docs = retriever.get_relevant_documents(translation)
        context = "\n".join([doc.page_content for doc in context_docs]) if context_docs else "No extra context"
        result = run_chain(sme_chain, {"user_prompt": translation, "context": context})
        return {
            "success": True,
            "transcription": transcription,
            "detected_language": detected_lang,
            "translation": translation,
            "sme_site": result if isinstance(result, str) else result.get("reply", "")
        }
    except Exception:
        return {"success": False, "error": "⚠️ LLM error", "details": traceback.format_exc()}

# ----------------- MAIN -----------------
if __name__ == "__main__":
    import uvicorn
    uvicorn.run("main:app", host="0.0.0.0", port=7860, reload=False)