Evil-paradox007 commited on
Commit
f2bd9bd
·
verified ·
1 Parent(s): e1dc16d

Upload 4 files

Browse files
Files changed (4) hide show
  1. Dockerfile +24 -0
  2. README.md +12 -5
  3. app.py +423 -0
  4. requirements.txt +8 -0
Dockerfile ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ WORKDIR /app
4
+
5
+ # Install system dependencies
6
+ RUN apt-get update && apt-get install -y \
7
+ build-essential \
8
+ && rm -rf /var/lib/apt/lists/*
9
+
10
+ # Copy requirements first for caching
11
+ COPY requirements.txt .
12
+ RUN pip install --no-cache-dir -r requirements.txt
13
+
14
+ # Copy application
15
+ COPY . .
16
+
17
+ # Create data directory
18
+ RUN mkdir -p /data
19
+
20
+ # Expose port (Hugging Face Spaces uses 7860)
21
+ EXPOSE 7860
22
+
23
+ # Run the application
24
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
README.md CHANGED
@@ -1,10 +1,17 @@
1
  ---
2
- title: Chatbase Api
3
- emoji: 🏆
4
- colorFrom: indigo
5
- colorTo: indigo
6
  sdk: docker
7
  pinned: false
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Chatbase Clone API
3
+ emoji: 🤖
4
+ colorFrom: purple
5
+ colorTo: blue
6
  sdk: docker
7
  pinned: false
8
  ---
9
 
10
+ # Chatbase Clone API
11
+
12
+ RAG-powered chatbot backend using FAISS + Gemini 1.5 Flash.
13
+
14
+ ## Environment Variables
15
+
16
+ Set these in your Space settings:
17
+ - `GEMINI_API_KEY` — Your Google AI Studio API key
app.py ADDED
@@ -0,0 +1,423 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # =============================================================================
2
+ # 🤖 CHATBASE CLONE — BACKEND API
3
+ # =============================================================================
4
+ # Deploy this to Hugging Face Spaces (Docker SDK)
5
+ # Provides RAG-powered chat using FAISS + Gemini 1.5 Flash
6
+ # =============================================================================
7
+
8
+ import os
9
+ import json
10
+ import hashlib
11
+ import time
12
+ import shutil
13
+ from pathlib import Path
14
+ from typing import Optional, List, Dict, Any
15
+
16
+ import numpy as np
17
+ import faiss
18
+ from fastapi import FastAPI, HTTPException, Request
19
+ from fastapi.middleware.cors import CORSMiddleware
20
+ from fastapi.responses import StreamingResponse, JSONResponse
21
+ from pydantic import BaseModel, Field
22
+ from sentence_transformers import SentenceTransformer
23
+ import google.generativeai as genai
24
+
25
+ # =============================================================================
26
+ # Configuration
27
+ # =============================================================================
28
+
29
+ DATA_DIR = os.environ.get("DATA_DIR", "/data")
30
+ GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY", "")
31
+ EMBED_MODEL_NAME = "all-MiniLM-L6-v2"
32
+ GEMINI_MODEL = "gemini-1.5-flash"
33
+ TOP_K = 5 # Number of chunks to retrieve
34
+ GEMINI_TIMEOUT = 30 # Seconds before Gemini API request times out
35
+
36
+ # =============================================================================
37
+ # Initialize
38
+ # =============================================================================
39
+
40
+ app = FastAPI(
41
+ title="Chatbase Clone API",
42
+ description="RAG-powered chatbot backend using FAISS + Gemini 1.5 Flash",
43
+ version="1.0.0",
44
+ )
45
+
46
+ # CORS — allow all origins for development
47
+ app.add_middleware(
48
+ CORSMiddleware,
49
+ allow_origins=["*"],
50
+ allow_credentials=True,
51
+ allow_methods=["*"],
52
+ allow_headers=["*"],
53
+ )
54
+
55
+ # Load embedding model at startup
56
+ print("Loading embedding model...")
57
+ embed_model = SentenceTransformer(EMBED_MODEL_NAME)
58
+ EMBED_DIM = embed_model.get_sentence_embedding_dimension()
59
+
60
+ # Configure Gemini
61
+ if GEMINI_API_KEY:
62
+ genai.configure(api_key=GEMINI_API_KEY)
63
+ gemini_model = genai.GenerativeModel(GEMINI_MODEL)
64
+ else:
65
+ gemini_model = None
66
+ print("⚠️ GEMINI_API_KEY not set. Chat will not work until configured.")
67
+
68
+ # In-memory store for chatbot data
69
+ chatbots: Dict[str, Dict[str, Any]] = {}
70
+
71
+ # =============================================================================
72
+ # Pydantic Models
73
+ # =============================================================================
74
+
75
+ class ChunkData(BaseModel):
76
+ chunk_id: int
77
+ source_url: str = ""
78
+ page_title: str = ""
79
+ chunk_index: int = 0
80
+ total_chunks: int = 0
81
+ content: str
82
+
83
+ class CreateChatbotRequest(BaseModel):
84
+ chatbot_id: str
85
+ source_url: str = ""
86
+ total_chunks: int = 0
87
+ chunk_config: dict = {}
88
+ chunks: List[ChunkData]
89
+
90
+ class ChatMessage(BaseModel):
91
+ role: str # "user" or "assistant"
92
+ content: str
93
+
94
+ class ChatRequest(BaseModel):
95
+ message: str
96
+ history: List[ChatMessage] = []
97
+ stream: bool = False
98
+
99
+ class ChatbotSettings(BaseModel):
100
+ name: Optional[str] = None
101
+ system_prompt: Optional[str] = None
102
+ temperature: Optional[float] = None
103
+ initial_message: Optional[str] = None
104
+ theme_color: Optional[str] = None
105
+
106
+ # =============================================================================
107
+ # Helper Functions
108
+ # =============================================================================
109
+
110
+ def get_chatbot_dir(chatbot_id: str) -> Path:
111
+ """Get the directory for a specific chatbot's data."""
112
+ safe_id = hashlib.md5(chatbot_id.encode()).hexdigest()[:12]
113
+ return Path(DATA_DIR) / f"chatbot_{safe_id}"
114
+
115
+ def build_faiss_index(chunks: List[ChunkData]) -> tuple:
116
+ """Build a FAISS index from text chunks."""
117
+ texts = [c.content for c in chunks]
118
+ print(f" Embedding {len(texts)} chunks...")
119
+ embeddings = embed_model.encode(texts, show_progress_bar=True, batch_size=32)
120
+ embeddings = np.array(embeddings, dtype='float32')
121
+
122
+ # Normalize for cosine similarity
123
+ faiss.normalize_L2(embeddings)
124
+
125
+ # Create index
126
+ index = faiss.IndexFlatIP(EMBED_DIM) # Inner product = cosine on normalized
127
+ index.add(embeddings)
128
+
129
+ return index, texts
130
+
131
+ def search_chunks(chatbot_id: str, query: str, top_k: int = TOP_K) -> List[Dict]:
132
+ """Search for relevant chunks using FAISS."""
133
+ bot = chatbots.get(chatbot_id)
134
+ if not bot:
135
+ return []
136
+
137
+ query_embedding = embed_model.encode([query])
138
+ query_embedding = np.array(query_embedding, dtype='float32')
139
+ faiss.normalize_L2(query_embedding)
140
+
141
+ scores, indices = bot['index'].search(query_embedding, top_k)
142
+
143
+ results = []
144
+ for score, idx in zip(scores[0], indices[0]):
145
+ if idx < len(bot['texts']):
146
+ results.append({
147
+ 'content': bot['texts'][idx],
148
+ 'score': float(score),
149
+ })
150
+
151
+ return results
152
+
153
+ def build_rag_prompt(query: str, context_chunks: List[Dict], history: List[ChatMessage], system_prompt: str = "") -> str:
154
+ """Build the RAG prompt for Gemini."""
155
+ if not system_prompt:
156
+ system_prompt = (
157
+ "You are a helpful AI assistant. Answer questions based on the provided context. "
158
+ "If the context doesn't contain relevant information, say so honestly. "
159
+ "Be concise, accurate, and helpful. Use markdown formatting when appropriate."
160
+ )
161
+
162
+ context_text = "\n\n---\n\n".join([c['content'] for c in context_chunks])
163
+
164
+ prompt = f"""{system_prompt}
165
+
166
+ ## Context from Knowledge Base:
167
+ {context_text}
168
+
169
+ ## Conversation History:
170
+ """
171
+ for msg in history[-6:]: # Last 6 messages for context
172
+ role = "User" if msg.role == "user" else "Assistant"
173
+ prompt += f"{role}: {msg.content}\n"
174
+
175
+ prompt += f"\nUser: {query}\n\nAssistant:"
176
+
177
+ return prompt
178
+
179
+ # =============================================================================
180
+ # API Endpoints
181
+ # =============================================================================
182
+
183
+ @app.get("/")
184
+ async def root():
185
+ return {"status": "ok", "service": "Chatbase Clone API", "version": "1.0.0"}
186
+
187
+ @app.get("/api/health")
188
+ async def health():
189
+ return {
190
+ "status": "healthy",
191
+ "gemini_configured": gemini_model is not None,
192
+ "chatbots_loaded": len(chatbots),
193
+ "embed_model": EMBED_MODEL_NAME,
194
+ }
195
+
196
+ # --- Chatbot CRUD ---
197
+
198
+ @app.post("/api/chatbot")
199
+ async def create_chatbot(request: CreateChatbotRequest):
200
+ """Create a new chatbot from chunked data."""
201
+ chatbot_id = request.chatbot_id
202
+
203
+ if not request.chunks:
204
+ raise HTTPException(status_code=400, detail="No chunks provided")
205
+
206
+ print(f"📦 Creating chatbot: {chatbot_id} with {len(request.chunks)} chunks")
207
+
208
+ # Build FAISS index
209
+ index, texts = build_faiss_index(request.chunks)
210
+
211
+ # Store in memory
212
+ chatbots[chatbot_id] = {
213
+ 'id': chatbot_id,
214
+ 'source_url': request.source_url,
215
+ 'total_chunks': len(request.chunks),
216
+ 'index': index,
217
+ 'texts': texts,
218
+ 'created_at': time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
219
+ 'settings': {
220
+ 'name': chatbot_id,
221
+ 'system_prompt': '',
222
+ 'temperature': 0.7,
223
+ 'initial_message': f"Hi! I'm an AI assistant trained on {request.source_url}. How can I help you?",
224
+ 'theme_color': '#6C63FF',
225
+ },
226
+ 'chat_count': 0,
227
+ 'message_count': 0,
228
+ }
229
+
230
+ # Save to disk for persistence
231
+ bot_dir = get_chatbot_dir(chatbot_id)
232
+ bot_dir.mkdir(parents=True, exist_ok=True)
233
+ faiss.write_index(index, str(bot_dir / "index.faiss"))
234
+
235
+ # Save metadata
236
+ meta = {k: v for k, v in chatbots[chatbot_id].items() if k not in ('index', 'texts')}
237
+ meta['texts'] = texts
238
+ with open(bot_dir / "meta.json", 'w') as f:
239
+ json.dump(meta, f, indent=2)
240
+
241
+ print(f"✅ Chatbot '{chatbot_id}' created successfully!")
242
+
243
+ return {
244
+ "status": "success",
245
+ "chatbot_id": chatbot_id,
246
+ "total_chunks": len(request.chunks),
247
+ "message": f"Chatbot '{chatbot_id}' created with {len(request.chunks)} chunks",
248
+ }
249
+
250
+
251
+ @app.get("/api/chatbots")
252
+ async def list_chatbots():
253
+ """List all chatbots."""
254
+ bots = []
255
+ for bot_id, bot in chatbots.items():
256
+ bots.append({
257
+ 'id': bot['id'],
258
+ 'name': bot['settings']['name'],
259
+ 'source_url': bot['source_url'],
260
+ 'total_chunks': bot['total_chunks'],
261
+ 'created_at': bot['created_at'],
262
+ 'chat_count': bot.get('chat_count', 0),
263
+ 'message_count': bot.get('message_count', 0),
264
+ 'settings': bot['settings'],
265
+ })
266
+ return {"chatbots": bots}
267
+
268
+
269
+ @app.get("/api/chatbot/{chatbot_id}")
270
+ async def get_chatbot(chatbot_id: str):
271
+ """Get chatbot details."""
272
+ if chatbot_id not in chatbots:
273
+ raise HTTPException(status_code=404, detail="Chatbot not found")
274
+
275
+ bot = chatbots[chatbot_id]
276
+ return {
277
+ 'id': bot['id'],
278
+ 'name': bot['settings']['name'],
279
+ 'source_url': bot['source_url'],
280
+ 'total_chunks': bot['total_chunks'],
281
+ 'created_at': bot['created_at'],
282
+ 'chat_count': bot.get('chat_count', 0),
283
+ 'message_count': bot.get('message_count', 0),
284
+ 'settings': bot['settings'],
285
+ }
286
+
287
+
288
+ @app.put("/api/chatbot/{chatbot_id}/settings")
289
+ async def update_settings(chatbot_id: str, settings: ChatbotSettings):
290
+ """Update chatbot settings."""
291
+ if chatbot_id not in chatbots:
292
+ raise HTTPException(status_code=404, detail="Chatbot not found")
293
+
294
+ bot = chatbots[chatbot_id]
295
+ update_data = settings.dict(exclude_none=True)
296
+ bot['settings'].update(update_data)
297
+
298
+ return {"status": "success", "settings": bot['settings']}
299
+
300
+
301
+ @app.delete("/api/chatbot/{chatbot_id}")
302
+ async def delete_chatbot(chatbot_id: str):
303
+ """Delete a chatbot."""
304
+ if chatbot_id not in chatbots:
305
+ raise HTTPException(status_code=404, detail="Chatbot not found")
306
+
307
+ del chatbots[chatbot_id]
308
+
309
+ # Remove from disk
310
+ bot_dir = get_chatbot_dir(chatbot_id)
311
+ if bot_dir.exists():
312
+ shutil.rmtree(bot_dir)
313
+
314
+ return {"status": "success", "message": f"Chatbot '{chatbot_id}' deleted"}
315
+
316
+
317
+ # --- Chat ---
318
+
319
+ @app.post("/api/chat/{chatbot_id}")
320
+ async def chat(chatbot_id: str, request: ChatRequest):
321
+ """Send a message and get an AI response."""
322
+ if chatbot_id not in chatbots:
323
+ raise HTTPException(status_code=404, detail="Chatbot not found")
324
+
325
+ if not gemini_model:
326
+ raise HTTPException(status_code=500, detail="Gemini API key not configured. Set GEMINI_API_KEY environment variable.")
327
+
328
+ bot = chatbots[chatbot_id]
329
+ bot['chat_count'] = bot.get('chat_count', 0) + 1
330
+ bot['message_count'] = bot.get('message_count', 0) + 1
331
+
332
+ # Retrieve relevant chunks
333
+ context_chunks = search_chunks(chatbot_id, request.message, TOP_K)
334
+
335
+ # Build prompt
336
+ prompt = build_rag_prompt(
337
+ query=request.message,
338
+ context_chunks=context_chunks,
339
+ history=request.history,
340
+ system_prompt=bot['settings'].get('system_prompt', ''),
341
+ )
342
+
343
+ # Generate response
344
+ if request.stream:
345
+ async def generate():
346
+ import signal
347
+ try:
348
+ response = gemini_model.generate_content(
349
+ prompt,
350
+ generation_config=genai.GenerationConfig(
351
+ temperature=bot['settings'].get('temperature', 0.7),
352
+ max_output_tokens=2048,
353
+ ),
354
+ stream=True,
355
+ request_options={'timeout': GEMINI_TIMEOUT},
356
+ )
357
+ for chunk in response:
358
+ if chunk.text:
359
+ yield f"data: {json.dumps({'content': chunk.text})}\n\n"
360
+ yield "data: [DONE]\n\n"
361
+ except Exception as e:
362
+ yield f"data: {json.dumps({'error': str(e)})}\n\n"
363
+
364
+ return StreamingResponse(generate(), media_type="text/event-stream")
365
+ else:
366
+ try:
367
+ response = gemini_model.generate_content(
368
+ prompt,
369
+ generation_config=genai.GenerationConfig(
370
+ temperature=bot['settings'].get('temperature', 0.7),
371
+ max_output_tokens=2048,
372
+ ),
373
+ request_options={'timeout': GEMINI_TIMEOUT},
374
+ )
375
+ return {
376
+ "response": response.text,
377
+ "sources": [{"content": c['content'][:200], "score": c['score']} for c in context_chunks[:3]],
378
+ }
379
+ except Exception as e:
380
+ raise HTTPException(status_code=500, detail=f"Gemini error: {str(e)}")
381
+
382
+
383
+ # --- Startup: Load existing chatbots ---
384
+
385
+ @app.on_event("startup")
386
+ async def startup_load():
387
+ """Load previously saved chatbots from disk."""
388
+ data_path = Path(DATA_DIR)
389
+ if not data_path.exists():
390
+ data_path.mkdir(parents=True, exist_ok=True)
391
+ return
392
+
393
+ for bot_dir in data_path.iterdir():
394
+ if bot_dir.is_dir() and bot_dir.name.startswith("chatbot_"):
395
+ meta_path = bot_dir / "meta.json"
396
+ index_path = bot_dir / "index.faiss"
397
+
398
+ if meta_path.exists() and index_path.exists():
399
+ try:
400
+ with open(meta_path) as f:
401
+ meta = json.load(f)
402
+
403
+ index = faiss.read_index(str(index_path))
404
+ texts = meta.pop('texts', [])
405
+
406
+ chatbots[meta['id']] = {
407
+ **meta,
408
+ 'index': index,
409
+ 'texts': texts,
410
+ }
411
+ print(f" ✅ Loaded chatbot: {meta['id']}")
412
+ except Exception as e:
413
+ print(f" ⚠️ Failed to load {bot_dir.name}: {e}")
414
+
415
+ print(f"📦 Loaded {len(chatbots)} chatbots from disk")
416
+
417
+
418
+ # =============================================================================
419
+ # Run with: uvicorn app:app --host 0.0.0.0 --port 7860
420
+ # =============================================================================
421
+ if __name__ == "__main__":
422
+ import uvicorn
423
+ uvicorn.run(app, host="0.0.0.0", port=7860)
requirements.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ fastapi==0.104.1
2
+ uvicorn==0.24.0
3
+ faiss-cpu==1.7.4
4
+ sentence-transformers==2.2.2
5
+ google-generativeai==0.8.3
6
+ python-multipart==0.0.6
7
+ numpy==1.26.2
8
+ pydantic==2.5.2