LevinAleksey commited on
Commit
19b4bfa
·
verified ·
1 Parent(s): 02c20bb

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +215 -112
app.py CHANGED
@@ -1,5 +1,5 @@
1
  import os
2
- from typing import List, Dict, Any, Tuple, Optional
3
 
4
  import chainlit as cl
5
  from huggingface_hub import InferenceClient
@@ -7,7 +7,10 @@ from qdrant_client import QdrantClient
7
  from sentence_transformers import SentenceTransformer
8
 
9
 
10
- # --- КОНФИГУРАЦИЯ ---
 
 
 
11
  HF_TOKEN = os.getenv("HF_TOKEN")
12
  QDRANT_URL = os.getenv("QDRANT_URL")
13
  QDRANT_API_KEY = os.getenv("QDRANT_API_KEY")
@@ -15,194 +18,294 @@ QDRANT_API_KEY = os.getenv("QDRANT_API_KEY")
15
  MODEL_ID = "Qwen/Qwen2.5-7B-Instruct"
16
  QDRANT_COLLECTION = "sales_knowledge"
17
 
18
- # Память (сколько сообщений держим в истории)
19
- HISTORY_KEEP = 20 # хранить в сессии (user+assistant = 2 сообщения за ход)
20
- HISTORY_SEND_LAST = 10 # отправлять в LLM последние N сообщений
21
 
22
- # RAG настройки
23
  RAG_LIMIT = 4
24
- # Порог релевантности: для cosine embeddings MiniLM часто рабочие значения 0.20–0.35.
25
- # Точный порог зависит от того, как ты индексировал коллекцию.
26
- RAG_SCORE_THRESHOLD = 0.25
27
- RAG_MAX_CHARS = 2500 # чтобы не раздувать промпт (защита по размеру)
28
 
 
 
 
29
 
30
- # --- МОЩНЫЙ ПРОМПТ ПРОДАЖНИКА ---
31
  SALES_SYSTEM_PROMPT = """
32
- Ты — ведущий эксперт по внедрению ИИ и автоматизации (n8n, RAG, Chatbots).
33
- Твоя задача: Квалифицировать клиента и продать услуги агентства.
34
-
35
- ТВОИ ПРАВИЛА:
36
- 1. Тон: уверенный, деловой, экспертный. Не будь "роботом-слугой". Ты — партнер по бизнесу.
37
- 2. Цель: не просто ответить, а вывести клиента на следующий шаг (звонок, аудит, КП).
38
- 3. Если спрашивают цену: не называй цифру "в лоб" без контекста. Сначала уточни вводные, объясни ценность, потом дай вилку "от...".
39
- 4. Возражение "дорого": объясни, какие деньги/время теряются без автоматизации.
40
- 5. Краткость: 3–4 предложения за раз. Больше — только если попросили подробно.
41
- 6. В конце ответа задавай вовлекающий вопрос, НО только если он уместен (нужно уточнить вводные или продвинуть сделку).
42
- 7. Если в контексте из базы знаний НЕТ ответа — не выдумывай факты. Задай 1–2 уточняющих вопроса и предложи следующий шаг.
43
- """.strip()
44
 
 
 
45
 
46
- def _safe_env_check() -> Optional[str]:
47
- """Возвращает текст ошибки конфигурации, если что-то критично не задано."""
48
- if not HF_TOKEN:
49
- return "Не задан HF_TOKEN (секрет токена Hugging Face)."
50
- return None
 
51
 
 
52
 
53
- @cl.on_chat_start
54
- async def start():
55
- # 0) Проверка env
56
- err = _safe_env_check()
57
- if err:
58
- await cl.Message(content=f"⚠️ Конфигурация: {err}").send()
59
 
60
- # 1) Приветствие
61
- await cl.Message(
62
- content="👋 Привет! Я AI-архитектор. Готов обсудить автоматизацию твоего бизнеса. Какую задачу решаем?"
63
- ).send()
64
 
65
- # 2) Инициализация клиентов
66
- hf_client = InferenceClient(MODEL_ID, token=HF_TOKEN)
67
 
68
- q_client = None
69
- if QDRANT_URL and QDRANT_API_KEY:
70
- try:
71
- q_client = QdrantClient(url=QDRANT_URL, api_key=QDRANT_API_KEY)
72
- print("✅ Qdrant Connected")
73
- except Exception as e:
74
- print(f"❌ Qdrant Error: {e}")
75
 
76
- # 3) Encoder (эмбеддинги)
77
- encoder = SentenceTransformer("all-MiniLM-L6-v2")
78
 
79
- cl.user_session.set("hf_client", hf_client)
80
- cl.user_session.set("q_client", q_client)
81
- cl.user_session.set("encoder", encoder)
82
 
83
- # 4) История (память)
84
- cl.user_session.set("message_history", [])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
 
87
  def get_context(
88
  query: str,
89
  q_client: Optional[QdrantClient],
90
  encoder: SentenceTransformer,
91
- *,
92
- limit: int = RAG_LIMIT,
93
- score_threshold: float = RAG_SCORE_THRESHOLD,
94
- ) -> Tuple[str, List[Dict[str, Any]]]:
95
- """
96
- Возвращает (context_text, debug_hits).
97
- debug_hits можно использовать для логов/аналитики.
98
- """
99
  if not q_client:
100
- return "", []
101
 
102
  try:
103
  vector = encoder.encode(query).tolist()
 
104
  hits = q_client.search(
105
  collection_name=QDRANT_COLLECTION,
106
  query_vector=vector,
107
- limit=limit,
108
  with_payload=True,
109
  )
110
 
111
- # Фильтруем по score, чтобы не тащить мусор
112
- good_hits = [h for h in hits if getattr(h, "score", 0.0) >= score_threshold]
 
 
 
 
 
 
 
 
 
113
 
114
- chunks: List[str] = []
115
- debug: List[Dict[str, Any]] = []
116
- for h in good_hits:
117
- payload = h.payload or {}
118
- text = (payload.get("text") or "").strip()
119
  if text:
120
- chunks.append(text)
121
- debug.append(
122
- {
123
- "score": float(getattr(h, "score", 0.0)),
124
- "payload_keys": list(payload.keys()),
125
- }
126
- )
127
 
128
- context = "\n\n---\n\n".join(chunks).strip()
129
  if len(context) > RAG_MAX_CHARS:
130
- context = context[:RAG_MAX_CHARS].rstrip() + "…"
131
 
132
- return context, debug
133
 
134
  except Exception as e:
135
- print(f"RAG error: {e}")
136
- return "", []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
137
 
 
 
 
 
 
 
 
 
 
 
 
138
 
139
  @cl.on_message
140
  async def main(message: cl.Message):
 
141
  hf_client: InferenceClient = cl.user_session.get("hf_client")
142
  q_client: Optional[QdrantClient] = cl.user_session.get("q_client")
143
  encoder: SentenceTransformer = cl.user_session.get("encoder")
 
144
  history: List[Dict[str, str]] = cl.user_session.get("message_history") or []
145
 
146
  user_text = (message.content or "").strip()
 
147
  if not user_text:
148
- await cl.Message(content="Напиши, пожалуйста, вопрос текстом 🙂").send()
149
  return
150
 
151
- # 1) RAG по текущему вопросу
152
- context, _debug_hits = get_context(user_text, q_client, encoder)
 
 
 
153
 
154
- # 2) Собираем сообщения
155
- messages_payload: List[Dict[str, str]] = []
156
- messages_payload.append({"role": "system", "content": SALES_SYSTEM_PROMPT})
 
 
 
 
 
 
 
157
 
158
- # Контекст отдельным сообщением (НЕ мешаем в главный system prompt)
159
  if context:
160
- messages_payload.append(
161
- {
162
- "role": "system",
163
- "content": (
164
- "Ниже — фрагменты из базы знаний компании. Используй их как источник фактов.\n"
165
- "Если ответ не следует из этих фрагментов — не выдумывай, задай уточняющие вопросы.\n\n"
166
- f"{context}"
167
- ),
168
- }
169
- )
 
 
 
 
170
 
171
- # История (последние N сообщений)
172
- for msg in history[-HISTORY_SEND_LAST:]:
173
- # защита от мусора
174
- if isinstance(msg, dict) and "role" in msg and "content" in msg:
175
- messages_payload.append({"role": msg["role"], "content": msg["content"]})
176
 
177
- # Текущий вопрос пользователя
178
- messages_payload.append({"role": "user", "content": user_text})
 
179
 
180
- # 3) Стримим ответ
181
  msg = cl.Message(content="")
182
  await msg.send()
183
 
184
  full_response = ""
 
185
  try:
186
  stream = hf_client.chat_completion(
187
  messages=messages_payload,
188
- max_tokens=700, # для "3-4 предложения" 1024 часто избыточно
 
 
189
  stream=True,
190
- temperature=0.7,
191
  )
192
 
193
  for chunk in stream:
194
  token = chunk.choices[0].delta.content
 
195
  if token:
196
  full_response += token
197
  await msg.stream_token(token)
198
 
199
  await msg.update()
200
 
201
- # 4) Обновляем историю и обрезаем, чтобы не разрасталась
 
 
 
202
  history.append({"role": "user", "content": user_text})
203
  history.append({"role": "assistant", "content": full_response.strip()})
 
204
  history = history[-HISTORY_KEEP:]
 
205
  cl.user_session.set("message_history", history)
206
 
207
  except Exception as e:
208
- await cl.Message(content=f"Ошибка: {str(e)}").send()
 
1
  import os
2
+ from typing import List, Dict, Optional
3
 
4
  import chainlit as cl
5
  from huggingface_hub import InferenceClient
 
7
  from sentence_transformers import SentenceTransformer
8
 
9
 
10
+ # ================================
11
+ # CONFIG
12
+ # ================================
13
+
14
  HF_TOKEN = os.getenv("HF_TOKEN")
15
  QDRANT_URL = os.getenv("QDRANT_URL")
16
  QDRANT_API_KEY = os.getenv("QDRANT_API_KEY")
 
18
  MODEL_ID = "Qwen/Qwen2.5-7B-Instruct"
19
  QDRANT_COLLECTION = "sales_knowledge"
20
 
21
+ HISTORY_KEEP = 20
22
+ HISTORY_SEND_LAST = 10
 
23
 
 
24
  RAG_LIMIT = 4
25
+ RAG_SCORE_THRESHOLD = 0.27
26
+ RAG_MAX_CHARS = 2500
27
+
 
28
 
29
+ # ================================
30
+ # ELITE SALES SYSTEM PROMPT
31
+ # ================================
32
 
 
33
  SALES_SYSTEM_PROMPT = """
34
+ Ты — элитный AI-архитектор и консультант по автоматизации бизнеса.
35
+ Ты работаешь только с серьезными проектами и не продаешь "дешевые решения".
 
 
 
 
 
 
 
 
 
 
36
 
37
+ ТВОЯ ЦЕЛЬ:
38
+ Провести клиента через экспертный диалог → квалифицировать → показать ценность → вывести на созвон или аудит.
39
 
40
+ СТИЛЬ:
41
+ - Уверенный
42
+ - Спокойный
43
+ - Дорогой
44
+ - Без суеты
45
+ - Без заискивания
46
 
47
+ Говори как senior-консультант.
48
 
49
+ ----------------------------------
 
 
 
 
 
50
 
51
+ ПОЛИТИКА СТОИМОСТИ:
 
 
 
52
 
53
+ Запрещено называть цену, пока не понятны:
 
54
 
55
+ 1) Что автоматизируем
56
+ 2) Где живут данные / интеграции
57
+ 3) Масштаб
 
 
 
 
58
 
59
+ Если цену спросили рано:
 
60
 
61
+ - не называй даже диапазон
62
+ - объясни почему рано
63
+ - задай ОДИН сильный вопрос
64
 
65
+ Не устраивай допрос.
66
+
67
+ ОДИН вопрос за сообщение.
68
+
69
+ ----------------------------------
70
+
71
+ ANTI-HALLUCINATION:
72
+
73
+ - не придумывай цены
74
+ - не придумывай кейсы
75
+ - не обещай сроки
76
+ - не фантазируй
77
+
78
+ Нет данных → честно скажи.
79
+
80
+ ----------------------------------
81
 
82
+ DISCOVERY:
83
+
84
+ Всегда оценивай:
85
+ "Хватает ли данных для оценки?"
86
+
87
+ Если нет → продолжай квалификацию.
88
+
89
+ ----------------------------------
90
+
91
+ RAG:
92
+
93
+ Используй контекст как единственный источник фактов.
94
+
95
+ Нет в базе → не выдумывай.
96
+
97
+ ----------------------------------
98
+
99
+ СТИЛЬ СООБЩЕНИЙ:
100
+
101
+ - 3–5 предложений
102
+ - без воды
103
+ - плотный текст
104
+ - экспертная подача
105
+
106
+ Иногда уместен вопрос в конце — но только если он двигает сделку.
107
+
108
+ ----------------------------------
109
+
110
+ ПОЗИЦИОНИРОВАНИЕ:
111
+
112
+ Ты архитектор решений.
113
+
114
+ Не бойся звучать дорого.
115
+ """.strip()
116
+
117
+
118
+ # ================================
119
+ # SAFETY CHECK
120
+ # ================================
121
+
122
+ def check_env():
123
+ if not HF_TOKEN:
124
+ raise ValueError("HF_TOKEN is missing!")
125
+
126
+
127
+ # ================================
128
+ # RAG RETRIEVAL
129
+ # ================================
130
 
131
  def get_context(
132
  query: str,
133
  q_client: Optional[QdrantClient],
134
  encoder: SentenceTransformer,
135
+ ) -> str:
136
+
 
 
 
 
 
 
137
  if not q_client:
138
+ return ""
139
 
140
  try:
141
  vector = encoder.encode(query).tolist()
142
+
143
  hits = q_client.search(
144
  collection_name=QDRANT_COLLECTION,
145
  query_vector=vector,
146
+ limit=RAG_LIMIT,
147
  with_payload=True,
148
  )
149
 
150
+ good_chunks = []
151
+
152
+ for hit in hits:
153
+ score = getattr(hit, "score", 0.0)
154
+
155
+ # агрессивная фильтрация
156
+ if score < RAG_SCORE_THRESHOLD:
157
+ continue
158
+
159
+ payload = hit.payload or {}
160
+ text = payload.get("text")
161
 
 
 
 
 
 
162
  if text:
163
+ good_chunks.append(text.strip())
164
+
165
+ if not good_chunks:
166
+ return ""
167
+
168
+ context = "\n\n---\n\n".join(good_chunks)
 
169
 
 
170
  if len(context) > RAG_MAX_CHARS:
171
+ context = context[:RAG_MAX_CHARS]
172
 
173
+ return context
174
 
175
  except Exception as e:
176
+ print("RAG ERROR:", e)
177
+ return ""
178
+
179
+
180
+ # ================================
181
+ # CHAT START
182
+ # ================================
183
+
184
+ @cl.on_chat_start
185
+ async def start():
186
+
187
+ check_env()
188
+
189
+ await cl.Message(
190
+ content="👋 Привет! Я AI-архитектор. Помогаю компаниям внедрять ИИ и автоматизацию. Расскажи, какую задачу хочешь решить?"
191
+ ).send()
192
+
193
+ hf_client = InferenceClient(MODEL_ID, token=HF_TOKEN)
194
+
195
+ q_client = None
196
+ if QDRANT_URL and QDRANT_API_KEY:
197
+ try:
198
+ q_client = QdrantClient(
199
+ url=QDRANT_URL,
200
+ api_key=QDRANT_API_KEY,
201
+ timeout=10
202
+ )
203
+ print("✅ Qdrant connected")
204
+ except Exception as e:
205
+ print("❌ Qdrant error:", e)
206
 
207
+ encoder = SentenceTransformer("all-MiniLM-L6-v2")
208
+
209
+ cl.user_session.set("hf_client", hf_client)
210
+ cl.user_session.set("q_client", q_client)
211
+ cl.user_session.set("encoder", encoder)
212
+ cl.user_session.set("message_history", [])
213
+
214
+
215
+ # ================================
216
+ # MAIN MESSAGE HANDLER
217
+ # ================================
218
 
219
  @cl.on_message
220
  async def main(message: cl.Message):
221
+
222
  hf_client: InferenceClient = cl.user_session.get("hf_client")
223
  q_client: Optional[QdrantClient] = cl.user_session.get("q_client")
224
  encoder: SentenceTransformer = cl.user_session.get("encoder")
225
+
226
  history: List[Dict[str, str]] = cl.user_session.get("message_history") or []
227
 
228
  user_text = (message.content or "").strip()
229
+
230
  if not user_text:
231
+ await cl.Message(content="Напиши вопрос 🙂").send()
232
  return
233
 
234
+ # =========================
235
+ # RAG
236
+ # =========================
237
+
238
+ context = get_context(user_text, q_client, encoder)
239
 
240
+ # =========================
241
+ # BUILD MESSAGES
242
+ # =========================
243
+
244
+ messages_payload = []
245
+
246
+ messages_payload.append({
247
+ "role": "system",
248
+ "content": SALES_SYSTEM_PROMPT
249
+ })
250
 
 
251
  if context:
252
+ messages_payload.append({
253
+ "role": "system",
254
+ "content": f"""
255
+ КОНТЕКСТ ИЗ БАЗЫ ЗНАНИЙ.
256
+ Используй только эти данные как факты.
257
+
258
+ {context}
259
+ """
260
+ })
261
+
262
+ # memory trimming
263
+ history = history[-HISTORY_SEND_LAST:]
264
+
265
+ messages_payload.extend(history)
266
 
267
+ messages_payload.append({
268
+ "role": "user",
269
+ "content": user_text
270
+ })
 
271
 
272
+ # =========================
273
+ # STREAM RESPONSE
274
+ # =========================
275
 
 
276
  msg = cl.Message(content="")
277
  await msg.send()
278
 
279
  full_response = ""
280
+
281
  try:
282
  stream = hf_client.chat_completion(
283
  messages=messages_payload,
284
+ max_tokens=450,
285
+ temperature=0.5,
286
+ top_p=0.9,
287
  stream=True,
 
288
  )
289
 
290
  for chunk in stream:
291
  token = chunk.choices[0].delta.content
292
+
293
  if token:
294
  full_response += token
295
  await msg.stream_token(token)
296
 
297
  await msg.update()
298
 
299
+ # =========================
300
+ # SAVE MEMORY
301
+ # =========================
302
+
303
  history.append({"role": "user", "content": user_text})
304
  history.append({"role": "assistant", "content": full_response.strip()})
305
+
306
  history = history[-HISTORY_KEEP:]
307
+
308
  cl.user_session.set("message_history", history)
309
 
310
  except Exception as e:
311
+ await cl.Message(content=f"Ошибка LLM: {str(e)}").send()