Baktabek commited on
Commit
6d87dc1
·
verified ·
1 Parent(s): e70fe74

Deploy RAG assistant

Browse files
Files changed (4) hide show
  1. Dockerfile +19 -0
  2. README.md +13 -10
  3. app.py +412 -0
  4. requirements.txt +8 -0
Dockerfile ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ WORKDIR /app
4
+
5
+ RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
6
+
7
+ COPY requirements.txt .
8
+ RUN pip install --no-cache-dir -r requirements.txt
9
+
10
+ COPY app.py .
11
+
12
+ EXPOSE 7860
13
+
14
+ ENV PYTHONUNBUFFERED=1
15
+ ENV PORT=7860
16
+
17
+ HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 CMD curl -f http://localhost:7860/health || exit 1
18
+
19
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
README.md CHANGED
@@ -1,10 +1,13 @@
1
- ---
2
- title: Rag Onboarding
3
- emoji: 📚
4
- colorFrom: yellow
5
- colorTo: green
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: RAG Onboarding Assistant
3
+ emoji: 🤖
4
+ colorFrom: blue
5
+ colorTo: purple
6
+ sdk: docker
7
+ app_port: 7860
8
+ pinned: false
9
+ ---
10
+
11
+ # AI-RAG Onboarding Assistant
12
+
13
+ Sistema vopros-otvet dlya onboardinga sotrudnikov.
app.py ADDED
@@ -0,0 +1,412 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Standalone RAG сервер для демонстрации
4
+ Работает напрямую с Qdrant и OpenRouter API без микросервисов
5
+ """
6
+
7
+ from fastapi import FastAPI, HTTPException, UploadFile, File, Form
8
+ from fastapi.middleware.cors import CORSMiddleware
9
+ from fastapi.staticfiles import StaticFiles
10
+ from pydantic import BaseModel
11
+ from typing import List, Dict, Any, Optional
12
+ import uvicorn
13
+ import os
14
+ from pathlib import Path
15
+ import uuid
16
+ from datetime import datetime
17
+ import httpx
18
+
19
+ from qdrant_client import QdrantClient
20
+ from qdrant_client.models import PointStruct
21
+ from sentence_transformers import SentenceTransformer
22
+
23
+ # Конфигурация
24
+ QDRANT_HOST = os.getenv("QDRANT_HOST", "localhost")
25
+ QDRANT_PORT = int(os.getenv("QDRANT_PORT", "6333"))
26
+ QDRANT_API_KEY = os.getenv("QDRANT_API_KEY") # Для Qdrant Cloud
27
+ COLLECTION_NAME = "onboarding_documents"
28
+
29
+ # OpenRouter Configuration
30
+ OPENROUTER_API_KEY = os.getenv("OPENROUTER_API_KEY", "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")
31
+ OPENROUTER_MODEL = os.getenv("OPENROUTER_MODEL", "deepseek/deepseek-chat-v3-0324")
32
+ OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1/chat/completions"
33
+
34
+ # Redis (опционально, для кэширования)
35
+ REDIS_HOST = os.getenv("REDIS_HOST", "localhost")
36
+ REDIS_PORT = int(os.getenv("REDIS_PORT", "6379"))
37
+ REDIS_PASSWORD = os.getenv("REDIS_PASSWORD")
38
+
39
+ # Инициализация
40
+ app = FastAPI(title="AI-RAG Onboarding Demo", version="1.0.0")
41
+
42
+ # CORS
43
+ app.add_middleware(
44
+ CORSMiddleware,
45
+ allow_origins=["*"],
46
+ allow_credentials=True,
47
+ allow_methods=["*"],
48
+ allow_headers=["*"],
49
+ )
50
+
51
+ # Глобальные объекты
52
+ qdrant_client = None
53
+ embedding_model = None
54
+ httpx_client = None
55
+
56
+
57
+ class QueryRequest(BaseModel):
58
+ query: str
59
+ dept_id: str = "onboarding"
60
+ user_id: str = "demo_user"
61
+ session_id: Optional[str] = None
62
+
63
+
64
+ class Source(BaseModel):
65
+ text: str
66
+ score: float
67
+ metadata: Dict[str, Any]
68
+
69
+
70
+ class QueryResponse(BaseModel):
71
+ answer: str
72
+ sources: List[Source]
73
+ metadata: Dict[str, Any]
74
+
75
+
76
+ class DocumentUploadResponse(BaseModel):
77
+ document_id: str
78
+ chunks_created: int
79
+ message: str
80
+ metadata: Dict[str, Any]
81
+
82
+
83
+ @app.on_event("startup")
84
+ async def startup_event():
85
+ """Инициализация при запуске"""
86
+ global qdrant_client, embedding_model, httpx_client
87
+
88
+ print("🚀 Запуск RAG сервера...")
89
+
90
+ # Qdrant
91
+ print("🔌 Подключение к Qdrant...")
92
+ if QDRANT_API_KEY:
93
+ # Qdrant Cloud
94
+ qdrant_client = QdrantClient(
95
+ host=QDRANT_HOST,
96
+ port=QDRANT_PORT,
97
+ api_key=QDRANT_API_KEY,
98
+ https=True
99
+ )
100
+ print("✅ Qdrant Cloud подключен")
101
+ else:
102
+ # Локальный Qdrant
103
+ qdrant_client = QdrantClient(host=QDRANT_HOST, port=QDRANT_PORT)
104
+ print("✅ Qdrant подключен")
105
+
106
+ # Embedding model
107
+ print("🧠 Загрузка модели эмбеддингов...")
108
+ # Используем многоязычную модель для правильной работы с русским языком
109
+ embedding_model = SentenceTransformer('sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2')
110
+ print("✅ Модель эмбеддингов загружена (многоязычная)")
111
+
112
+ # HTTP client для OpenRouter
113
+ print("🤖 Настройка OpenRouter API...")
114
+ httpx_client = httpx.AsyncClient(timeout=30.0)
115
+ print(f"✅ OpenRouter настроен (модель: {OPENROUTER_MODEL})")
116
+
117
+ print("✨ Сервер готов!")
118
+
119
+
120
+ @app.get("/health")
121
+ async def health_check():
122
+ """Health check endpoint"""
123
+ return {
124
+ "status": "healthy",
125
+ "qdrant": "connected" if qdrant_client else "disconnected",
126
+ "embedding_model": "loaded" if embedding_model else "not loaded",
127
+ "llm": "configured" if httpx_client else "not configured"
128
+ }
129
+
130
+
131
+ @app.post("/api/v1/query", response_model=QueryResponse)
132
+ async def query(request: QueryRequest):
133
+ """Основной endpoint для запросов"""
134
+
135
+ try:
136
+ import time
137
+ start_time = time.time()
138
+
139
+ # 1. Генерация эмбеддинга запроса
140
+ query_embedding = embedding_model.encode(request.query).tolist()
141
+
142
+ # 2. Поиск в Qdrant с ограничением количества
143
+ # Используем только limit без score_threshold чтобы получить топ-3 САМЫХ релевантных
144
+ # даже если их score не очень высокий
145
+ search_results = qdrant_client.search(
146
+ collection_name=COLLECTION_NAME,
147
+ query_vector=query_embedding,
148
+ limit=3, # Только топ-3 самых релевантных по score
149
+ with_payload=True
150
+ )
151
+
152
+ # 3. Подготовка контекста
153
+ sources = []
154
+ context_parts = []
155
+
156
+ for idx, hit in enumerate(search_results, 1):
157
+ source = Source(
158
+ text=hit.payload.get('text', ''),
159
+ score=hit.score,
160
+ metadata={
161
+ 'title': hit.payload.get('title', 'Unknown'),
162
+ 'chunk_index': hit.payload.get('chunk_index', 0),
163
+ 'doc_type': hit.payload.get('doc_type', 'unknown'),
164
+ 'department': hit.payload.get('department', 'unknown'),
165
+ 'last_updated': hit.payload.get('last_updated', 'unknown'),
166
+ }
167
+ )
168
+ sources.append(source)
169
+ context_parts.append(f"[Источник {idx}] {hit.payload.get('text', '')}")
170
+
171
+ context = "\n\n".join(context_parts)
172
+
173
+ # 4. Генерация ответа с помощью Gemini
174
+ prompt = f"""Ты - помощник по онбордингу новых сотрудников компании. Используй предоставленный контекст для ответа на вопрос.
175
+
176
+ КОНТЕКСТ:
177
+ {context}
178
+
179
+ ВОПРОС: {request.query}
180
+
181
+ ИНСТРУКЦИИ:
182
+ - Отвечай на русском языке используя Markdown форматирование
183
+ - Используй только информацию из контекста
184
+ - Структурируй ответ понятно и кратко с помощью заголовков (##, ###), списков (-, *), жирного текста (**текст**)
185
+ - **ВАЖНО ДЛЯ ССЫЛОК:** Оборачивай ЦЕЛЫЕ ФРАЗЫ в ссылки, а не отдельные слова. Ссылка должна читаться естественно как часть предложения.
186
+
187
+ ✅ ПРАВИЛЬНО (естественное чтение):
188
+ - "Встреча в [кабинете 101](source:1)"
189
+ - "Обратитесь к [HR-менеджеру](source:2)"
190
+ - "Перейдите на [https://account.company.kz](source:3)"
191
+ - "Временный пароль действителен [24 часа](source:1)"
192
+ - "Получите логин от [IT-отдела](source:2)"
193
+
194
+ ❌ НЕПРАВИЛЬНО (разрывает текст):
195
+ - "Временный пароль от [HR-менеджера](source:1) для входа" (разрывает фразу)
196
+ - "Действителен только [24 часа](source:1) !" (отдельные слова)
197
+
198
+ - Делай ссылки МИНИМАЛЬНЫМИ - только ключевой факт, не целое предложение
199
+ - Используй ссылки для конкретных фактов: адреса сайтов, номера кабинетов, имена должностей, временные интервалы
200
+ - Если информации недостаточно, честно скажи об этом
201
+ - Будь дружелюбным и помогающим
202
+
203
+ ОТВЕТ:"""
204
+
205
+ # 4. Генерация ответа с помощью OpenRouter
206
+ try:
207
+ response = await httpx_client.post(
208
+ OPENROUTER_BASE_URL,
209
+ headers={
210
+ "Authorization": f"Bearer {OPENROUTER_API_KEY}",
211
+ "HTTP-Referer": "https://github.com/baltabekpro/ai-rag-core",
212
+ "X-Title": "AI-RAG Onboarding"
213
+ },
214
+ json={
215
+ "model": OPENROUTER_MODEL,
216
+ "messages": [
217
+ {
218
+ "role": "user",
219
+ "content": prompt
220
+ }
221
+ ],
222
+ "temperature": 0.3,
223
+ "max_tokens": 500
224
+ },
225
+ timeout=30.0
226
+ )
227
+ response.raise_for_status()
228
+
229
+ result = response.json()
230
+ answer = result["choices"][0]["message"]["content"]
231
+
232
+ except httpx.HTTPStatusError as e:
233
+ if e.response.status_code == 429:
234
+ raise HTTPException(
235
+ status_code=503,
236
+ detail="AI сервис временно перегружен. Попробуйте через минуту."
237
+ )
238
+ raise HTTPException(
239
+ status_code=500,
240
+ detail=f"Ошибка OpenRouter API: {e.response.text}"
241
+ )
242
+ except Exception as e:
243
+ raise HTTPException(
244
+ status_code=500,
245
+ detail=f"Ошибка генерации ответа: {str(e)}"
246
+ )
247
+
248
+ # 5. Метаданные
249
+ processing_time = int((time.time() - start_time) * 1000)
250
+
251
+ return QueryResponse(
252
+ answer=answer,
253
+ sources=sources,
254
+ metadata={
255
+ "processing_time": processing_time,
256
+ "model": OPENROUTER_MODEL,
257
+ "sources_count": len(sources),
258
+ "department": request.dept_id
259
+ }
260
+ )
261
+
262
+ except Exception as e:
263
+ import traceback
264
+ error_trace = traceback.format_exc()
265
+ print(f"❌ ОШИБКА в /api/v1/query: {error_trace}")
266
+ raise HTTPException(status_code=500, detail=f"Ошибка обработки запроса: {str(e)}")
267
+
268
+
269
+ @app.get("/api/v1/stats")
270
+ async def get_stats():
271
+ """Статистика системы"""
272
+ try:
273
+ collection_info = qdrant_client.get_collection(COLLECTION_NAME)
274
+ return {
275
+ "collection": COLLECTION_NAME,
276
+ "documents_count": collection_info.points_count,
277
+ "vector_size": collection_info.config.params.vectors.size,
278
+ "status": "operational"
279
+ }
280
+ except Exception as e:
281
+ raise HTTPException(status_code=500, detail=str(e))
282
+
283
+
284
+ @app.post("/api/v1/documents/upload", response_model=DocumentUploadResponse)
285
+ async def upload_document(
286
+ file: UploadFile = File(...),
287
+ title: Optional[str] = Form(None),
288
+ department: str = Form("onboarding"),
289
+ doc_type: str = Form("guide")
290
+ ):
291
+ """
292
+ Загрузка и индексация документа
293
+
294
+ Поддерживаемые форматы: .txt, .md
295
+
296
+ Процесс:
297
+ 1. Чтение файла
298
+ 2. Разбивка на чанки (512 токенов)
299
+ 3. Генерация эмбеддингов
300
+ 4. Сохранение в Qdrant
301
+ """
302
+ try:
303
+ import time
304
+ start_time = time.time()
305
+
306
+ # Проверка формата файла
307
+ allowed_extensions = ['.txt', '.md']
308
+ file_ext = os.path.splitext(file.filename)[1].lower()
309
+
310
+ if file_ext not in allowed_extensions:
311
+ raise HTTPException(
312
+ status_code=400,
313
+ detail=f"Неподдерживаемый формат файла. Разрешены: {', '.join(allowed_extensions)}"
314
+ )
315
+
316
+ # Чтение содержимого
317
+ content = await file.read()
318
+ text = content.decode('utf-8')
319
+
320
+ if not text.strip():
321
+ raise HTTPException(status_code=400, detail="Файл пустой")
322
+
323
+ # Используем название файла если title не указан
324
+ doc_title = title or file.filename
325
+
326
+ # ID документа
327
+ document_id = str(uuid.uuid4())
328
+
329
+ # Разбивка на чанки (простая - по 512 токенов ~2000 символов)
330
+ chunk_size = 2000
331
+ overlap = 200
332
+ chunks = []
333
+
334
+ for i in range(0, len(text), chunk_size - overlap):
335
+ chunk = text[i:i + chunk_size]
336
+ if chunk.strip():
337
+ chunks.append(chunk.strip())
338
+
339
+ if not chunks:
340
+ raise HTTPException(status_code=400, detail="Не удалось создать чанки из документа")
341
+
342
+ # Генерация эмбеддингов и сохранение
343
+ points = []
344
+
345
+ for idx, chunk in enumerate(chunks):
346
+ # Эмбеддинг
347
+ embedding = embedding_model.encode(chunk).tolist()
348
+
349
+ # Point для Qdrant
350
+ point = PointStruct(
351
+ id=str(uuid.uuid4()),
352
+ vector=embedding,
353
+ payload={
354
+ "text": chunk,
355
+ "document_id": document_id,
356
+ "chunk_index": idx,
357
+ "title": doc_title,
358
+ "department": department,
359
+ "doc_type": doc_type,
360
+ "last_updated": datetime.utcnow().isoformat(),
361
+ "filename": file.filename
362
+ }
363
+ )
364
+ points.append(point)
365
+
366
+ # Загрузка в Qdrant
367
+ qdrant_client.upsert(
368
+ collection_name=COLLECTION_NAME,
369
+ points=points
370
+ )
371
+
372
+ processing_time = int((time.time() - start_time) * 1000)
373
+
374
+ return DocumentUploadResponse(
375
+ document_id=document_id,
376
+ chunks_created=len(chunks),
377
+ message=f"Документ '{doc_title}' успешно загружен и проиндексирован",
378
+ metadata={
379
+ "processing_time_ms": processing_time,
380
+ "filename": file.filename,
381
+ "file_size": len(content),
382
+ "chunk_size": chunk_size,
383
+ "department": department,
384
+ "doc_type": doc_type
385
+ }
386
+ )
387
+
388
+ except HTTPException:
389
+ raise
390
+ except Exception as e:
391
+ import traceback
392
+ error_trace = traceback.format_exc()
393
+ print(f"❌ ОШИБКА в /api/v1/documents/upload: {error_trace}")
394
+ raise HTTPException(status_code=500, detail=f"Ошибка загрузки документа: {str(e)}")
395
+
396
+
397
+ if __name__ == "__main__":
398
+ print("=" * 70)
399
+ print(" AI-RAG Onboarding - Standalone Demo Server")
400
+ print("=" * 70)
401
+ print()
402
+ print("📍 Сервер запускается на: http://localhost:8081")
403
+ print("📄 API документация: http://localhost:8081/docs")
404
+ print("💬 Откройте frontend/chat.html и измените API URL на http://localhost:8081")
405
+ print()
406
+
407
+ uvicorn.run(
408
+ app,
409
+ host="0.0.0.0",
410
+ port=8081,
411
+ log_level="info"
412
+ )
requirements.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ fastapi==0.104.1
2
+ uvicorn[standard]==0.24.0
3
+ qdrant-client==1.7.0
4
+ sentence-transformers==2.2.2
5
+ torch==2.1.0
6
+ numpy==1.24.3
7
+ httpx==0.25.1
8
+ python-multipart==0.0.6