Alex Zerocoder commited on
Commit
851f3c6
·
1 Parent(s): e4bb2ab

Deploy 2MOOD AI Workspace (Feedback + Messenger + KB)

Browse files
app.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ from app.main import build_interface
2
+
3
+ app = build_interface()
4
+
5
+ if __name__ == "__main__":
6
+ app.launch()
app/__init__.py ADDED
File without changes
app/__pycache__/__init__.cpython-311.pyc ADDED
Binary file (189 Bytes). View file
 
app/__pycache__/main.cpython-311.pyc ADDED
Binary file (10.6 kB). View file
 
app/_init_.py ADDED
File without changes
app/config.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from dotenv import load_dotenv
3
+
4
+ load_dotenv()
5
+
6
+ PROJECT_ROOT = os.path.dirname(os.path.dirname(__file__)) # .../app -> project root
7
+ DATA_DIR = os.path.join(PROJECT_ROOT, "data")
8
+ KB_DIR = os.path.join(DATA_DIR, "knowledge_base")
9
+ PROTOCOLS_DIR = os.path.join(DATA_DIR, "protocols_archive")
10
+
11
+ KB_FILES_DIR = os.path.join(KB_DIR, "files")
12
+ KB_INDEX_PATH = os.path.join(KB_DIR, "kb_index.json")
13
+ FEEDBACK_DB_PATH = os.path.join(DATA_DIR, "feedback.db")
14
+
15
+ OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "").strip()
16
+
17
+ TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN", "").strip()
18
+ TELEGRAM_CHAT_ID = os.getenv("TELEGRAM_CHAT_ID", "").strip()
app/core/configs.py ADDED
File without changes
app/core/logging.py ADDED
File without changes
app/core/settings.py ADDED
File without changes
app/integrations/__init__.py ADDED
File without changes
app/integrations/__pycache__/__init__.cpython-311.pyc ADDED
Binary file (202 Bytes). View file
 
app/integrations/__pycache__/telegram_bot.cpython-311.pyc ADDED
Binary file (2.04 kB). View file
 
app/integrations/telegram_bot.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import requests
3
+
4
+
5
+ def _get_env():
6
+ token = os.getenv("TELEGRAM_BOT_TOKEN", "").strip()
7
+ chat_id = os.getenv("TELEGRAM_CHAT_ID", "").strip()
8
+ enabled = bool(token and chat_id)
9
+ return enabled, token, chat_id
10
+
11
+
12
+ def send_telegram_message(text: str) -> tuple[bool, str]:
13
+ """
14
+ Returns (ok, message)
15
+ """
16
+ enabled, token, chat_id = _get_env()
17
+ if not enabled:
18
+ return False, "Telegram отключён: нет TELEGRAM_BOT_TOKEN / TELEGRAM_CHAT_ID в .env"
19
+
20
+ url = f"https://api.telegram.org/bot{token}/sendMessage"
21
+ payload = {
22
+ "chat_id": chat_id,
23
+ "text": text,
24
+ "disable_web_page_preview": True
25
+ }
26
+
27
+ try:
28
+ r = requests.post(url, json=payload, timeout=15)
29
+ if r.status_code != 200:
30
+ return False, f"Telegram error: HTTP {r.status_code} — {r.text}"
31
+ data = r.json()
32
+ if not data.get("ok"):
33
+ return False, f"Telegram error: {data}"
34
+ return True, "Отправлено в Telegram"
35
+ except Exception as e:
36
+ return False, f"Telegram exception: {e}"
app/knowledge/__init__.py ADDED
File without changes
app/knowledge/embeddings.py ADDED
File without changes
app/knowledge/loader.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from pypdf import PdfReader
3
+
4
+ def extract_text_from_pdf(pdf_path: str) -> str:
5
+ reader = PdfReader(pdf_path)
6
+ chunks = []
7
+ for page in reader.pages:
8
+ chunks.append(page.extract_text() or "")
9
+ return "\n".join(chunks).strip()
10
+
11
+ def save_uploaded_pdf(src_path: str, dest_dir: str) -> str:
12
+ os.makedirs(dest_dir, exist_ok=True)
13
+ base = os.path.basename(src_path)
14
+ dest_path = os.path.join(dest_dir, base)
15
+ # перезапись ок для пилота
16
+ with open(src_path, "rb") as fsrc, open(dest_path, "wb") as fdst:
17
+ fdst.write(fsrc.read())
18
+ return dest_path
app/knowledge/retriever.py ADDED
File without changes
app/knowledge/vector_store.py ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import math
3
+ import os
4
+ import re
5
+ from dataclasses import dataclass
6
+ from typing import List, Dict, Any
7
+
8
+ TOKEN_RE = re.compile(r"[A-Za-zА-Яа-я0-9_]+")
9
+
10
+ def tokenize(text: str) -> List[str]:
11
+ return [t.lower() for t in TOKEN_RE.findall(text)]
12
+
13
+ @dataclass
14
+ class Doc:
15
+ doc_id: str
16
+ title: str
17
+ text: str
18
+ source_path: str
19
+
20
+ class BM25Index:
21
+ """
22
+ Лёгкий BM25 без зависимостей.
23
+ Хранит документы + статистику в JSON.
24
+ """
25
+ def __init__(self, k1: float = 1.5, b: float = 0.75):
26
+ self.k1 = k1
27
+ self.b = b
28
+ self.docs: List[Doc] = []
29
+ self.df: Dict[str, int] = {} # document frequency per term
30
+ self.tf: List[Dict[str, int]] = [] # term frequency per doc
31
+ self.doc_len: List[int] = []
32
+ self.avgdl: float = 0.0
33
+
34
+ def add(self, doc: Doc):
35
+ tokens = tokenize(doc.text)
36
+ tf_map: Dict[str, int] = {}
37
+ for t in tokens:
38
+ tf_map[t] = tf_map.get(t, 0) + 1
39
+ # update df
40
+ for t in set(tf_map.keys()):
41
+ self.df[t] = self.df.get(t, 0) + 1
42
+
43
+ self.docs.append(doc)
44
+ self.tf.append(tf_map)
45
+ self.doc_len.append(len(tokens))
46
+ self.avgdl = sum(self.doc_len) / max(1, len(self.doc_len))
47
+
48
+ def idf(self, term: str) -> float:
49
+ N = len(self.docs)
50
+ n = self.df.get(term, 0)
51
+ # classic BM25 idf
52
+ return math.log(1 + (N - n + 0.5) / (n + 0.5)) if N > 0 else 0.0
53
+
54
+ def score(self, query: str, i: int) -> float:
55
+ q_terms = tokenize(query)
56
+ score = 0.0
57
+ dl = self.doc_len[i]
58
+ denom_norm = (1 - self.b) + self.b * (dl / (self.avgdl or 1.0))
59
+ tf_map = self.tf[i]
60
+ for term in q_terms:
61
+ f = tf_map.get(term, 0)
62
+ if f <= 0:
63
+ continue
64
+ idf = self.idf(term)
65
+ score += idf * (f * (self.k1 + 1)) / (f + self.k1 * denom_norm)
66
+ return score
67
+
68
+ def search(self, query: str, top_k: int = 5) -> List[Dict[str, Any]]:
69
+ scored = []
70
+ for i, d in enumerate(self.docs):
71
+ s = self.score(query, i)
72
+ if s > 0:
73
+ scored.append((s, d))
74
+ scored.sort(key=lambda x: x[0], reverse=True)
75
+ results = []
76
+ for s, d in scored[:top_k]:
77
+ # короткий сниппет для UI
78
+ snippet = (d.text[:600] + "…") if len(d.text) > 600 else d.text
79
+ results.append({
80
+ "score": round(s, 4),
81
+ "doc_id": d.doc_id,
82
+ "title": d.title,
83
+ "source_path": d.source_path,
84
+ "snippet": snippet
85
+ })
86
+ return results
87
+
88
+ def to_json(self) -> Dict[str, Any]:
89
+ return {
90
+ "k1": self.k1,
91
+ "b": self.b,
92
+ "docs": [{"doc_id": d.doc_id, "title": d.title, "text": d.text, "source_path": d.source_path} for d in self.docs],
93
+ "df": self.df,
94
+ "tf": self.tf,
95
+ "doc_len": self.doc_len,
96
+ "avgdl": self.avgdl
97
+ }
98
+
99
+ @staticmethod
100
+ def from_json(obj: Dict[str, Any]) -> "BM25Index":
101
+ idx = BM25Index(k1=obj.get("k1", 1.5), b=obj.get("b", 0.75))
102
+ idx.docs = [Doc(**d) for d in obj.get("docs", [])]
103
+ idx.df = obj.get("df", {})
104
+ idx.tf = obj.get("tf", [])
105
+ idx.doc_len = obj.get("doc_len", [])
106
+ idx.avgdl = obj.get("avgdl", 0.0)
107
+ return idx
108
+
109
+ def save_index(path: str, index: BM25Index):
110
+ os.makedirs(os.path.dirname(path), exist_ok=True)
111
+ with open(path, "w", encoding="utf-8") as f:
112
+ json.dump(index.to_json(), f, ensure_ascii=False)
113
+
114
+ def load_index(path: str) -> BM25Index:
115
+ if not os.path.exists(path):
116
+ return BM25Index()
117
+ with open(path, "r", encoding="utf-8") as f:
118
+ obj = json.load(f)
119
+ return BM25Index.from_json(obj)
app/main.py ADDED
@@ -0,0 +1,247 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ import csv
4
+ from datetime import datetime
5
+ from pathlib import Path
6
+ from typing import Optional, Tuple, List, Dict
7
+
8
+ import gradio as gr
9
+ import requests
10
+
11
+ # Опционально: подтягиваем .env при локальном запуске
12
+ try:
13
+ from dotenv import load_dotenv
14
+ except Exception:
15
+ load_dotenv = None
16
+
17
+
18
+ # --- Fix imports when запуск: `python app\main.py` из корня проекта ---
19
+ PROJECT_ROOT = Path(__file__).resolve().parents[1]
20
+ if str(PROJECT_ROOT) not in sys.path:
21
+ sys.path.insert(0, str(PROJECT_ROOT))
22
+
23
+ if load_dotenv:
24
+ load_dotenv(PROJECT_ROOT / ".env")
25
+
26
+
27
+ # --- Paths ---
28
+ DATA_DIR = PROJECT_ROOT / "data"
29
+ FEEDBACK_DIR = DATA_DIR / "feedback"
30
+ FEEDBACK_CSV = FEEDBACK_DIR / "feedback.csv"
31
+
32
+
33
+ # --- Telegram integration ---
34
+ def send_telegram_message(text: str) -> Tuple[bool, str]:
35
+ """
36
+ Отправляет сообщение в Telegram (если TELEGRAM_BOT_TOKEN и TELEGRAM_CHAT_ID заданы).
37
+ Возвращает (ok, details).
38
+ """
39
+ token = os.getenv("TELEGRAM_BOT_TOKEN", "").strip()
40
+ chat_id = os.getenv("TELEGRAM_CHAT_ID", "").strip()
41
+
42
+ if not token or not chat_id:
43
+ return False, "Telegram: пропущено (нет TELEGRAM_BOT_TOKEN или TELEGRAM_CHAT_ID)"
44
+
45
+ url = f"https://api.telegram.org/bot{token}/sendMessage"
46
+ try:
47
+ r = requests.post(url, data={"chat_id": chat_id, "text": text}, timeout=10)
48
+ if r.status_code == 200:
49
+ return True, "Telegram: отправлено ✅"
50
+ return False, f"Telegram: ошибка {r.status_code}: {r.text[:200]}"
51
+ except Exception as e:
52
+ return False, f"Telegram: исключение: {e}"
53
+
54
+
55
+ # --- Feedback storage ---
56
+ def ensure_feedback_csv_exists() -> None:
57
+ FEEDBACK_DIR.mkdir(parents=True, exist_ok=True)
58
+ if not FEEDBACK_CSV.exists():
59
+ with FEEDBACK_CSV.open("w", newline="", encoding="utf-8") as f:
60
+ w = csv.writer(f)
61
+ w.writerow(["timestamp", "category", "rating", "contact", "feedback_text"])
62
+
63
+
64
+ def append_feedback_row(category: str, rating: int, contact: str, feedback_text: str) -> None:
65
+ ensure_feedback_csv_exists()
66
+ ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
67
+ with FEEDBACK_CSV.open("a", newline="", encoding="utf-8") as f:
68
+ w = csv.writer(f)
69
+ w.writerow([ts, category, rating, contact.strip(), feedback_text.strip()])
70
+
71
+
72
+ # --- UI logic ---
73
+ def save_feedback(category: str, rating: int, contact: str, feedback_text: str):
74
+ if not feedback_text or not feedback_text.strip():
75
+ return "⚠️ Введите текст обратной связи.", gr.update(value=feedback_text), None
76
+
77
+ if rating is None:
78
+ return "⚠️ Выберите оценку (1–5).", gr.update(value=feedback_text), None
79
+
80
+ try:
81
+ append_feedback_row(category, int(rating), contact or "", feedback_text)
82
+ except Exception as e:
83
+ return f"❌ Не удалось сохранить в CSV: {e}", gr.update(value=feedback_text), None
84
+
85
+ # Telegram уведомление (не блокирует сохранение)
86
+ tg_text = (
87
+ "📝 2MOOD Feedback\n"
88
+ f"Категория: {category}\n"
89
+ f"Оценка: {rating}\n"
90
+ f"Контакт: {contact or '-'}\n"
91
+ f"Текст: {feedback_text.strip()}"
92
+ )
93
+ tg_ok, tg_details = send_telegram_message(tg_text)
94
+
95
+ status = "✅ Отзыв сохранён в CSV.\n\n"
96
+ status += f"Файл: `{FEEDBACK_CSV.as_posix()}`\n\n"
97
+ status += ("✅ " if tg_ok else "⚠️ ") + tg_details
98
+
99
+ # очистим поле текста + выдадим файл (чтобы сразу можно было скачать при желании)
100
+ return status, gr.update(value=""), str(FEEDBACK_CSV)
101
+
102
+
103
+ def get_feedback_csv() -> Tuple[Optional[str], str]:
104
+ if FEEDBACK_CSV.exists() and FEEDBACK_CSV.stat().st_size > 0:
105
+ return str(FEEDBACK_CSV), "✅ Готово: файл доступен для скачивания."
106
+ return None, "⚠️ Файл ещё не создан или пуст."
107
+
108
+
109
+ # --- Messenger logic (Gradio Chatbot: messages format) ---
110
+ def messenger_reply(user_message: str, history: Optional[List[Dict[str, str]]]):
111
+ """
112
+ Gradio Chatbot с type="messages" ожидает:
113
+ [{"role":"user","content":"..."}, {"role":"assistant","content":"..."} ...]
114
+ """
115
+ history = history or []
116
+
117
+ msg = (user_message or "").strip()
118
+ if not msg:
119
+ return "", history
120
+
121
+ history.append({"role": "user", "content": msg})
122
+ history.append({"role": "assistant", "content": f"Echo: {msg}"})
123
+
124
+ return "", history
125
+
126
+
127
+ def build_interface():
128
+ ensure_feedback_csv_exists()
129
+
130
+ with gr.Blocks(title="2MOOD AI Workspace (Pilot)") as demo:
131
+ gr.Markdown("# 2MOOD AI Workspace")
132
+ gr.Markdown("База знаний + Messenger + Обратная связь")
133
+
134
+ with gr.Tab("📚 База знаний"):
135
+ gr.Markdown("Загрузка PDF и индексирование (пока заглушка)")
136
+ gr.File(label="Загрузить PDF")
137
+ gr.Button("Добавить в базу")
138
+
139
+ with gr.Tab("💬 2MOOD Messenger"):
140
+ # ВАЖНО: фиксируем формат, чтобы не ловить ошибку role/content
141
+ chat = gr.Chatbot(height=420, type="messages")
142
+ msg = gr.Textbox(label="Сообщение", placeholder="Напишите сообщение…")
143
+ send = gr.Button("Отправить", variant="primary")
144
+
145
+ send.click(messenger_reply, inputs=[msg, chat], outputs=[msg, chat])
146
+
147
+ with gr.Tab("📝 Обратная связь"):
148
+ category = gr.Dropdown(
149
+ label="Категория",
150
+ choices=["Другое", "Messenger", "База знаний", "UI/UX", "Ошибка/Баг", "Идея/Feature"],
151
+ value="Другое",
152
+ )
153
+
154
+ rating = gr.Radio(
155
+ label="Оценка",
156
+ choices=[1, 2, 3, 4, 5],
157
+ value=5,
158
+ )
159
+
160
+ contact = gr.Textbox(label="Контакт (опционально): Telegram/почта")
161
+ feedback_text = gr.Textbox(
162
+ label="Текст обратной связи",
163
+ lines=6,
164
+ placeholder="Например: не хватает кнопки…, ошибка…, хочу экспорт в Word…",
165
+ )
166
+
167
+ submit = gr.Button("Отправить", variant="primary")
168
+ status = gr.Markdown()
169
+
170
+ # Кнопка скачивания
171
+ download_btn = gr.Button("📥 Скачать все отзывы")
172
+ download_file = gr.File(label="Файл с отзывами", interactive=False)
173
+ download_status = gr.Markdown()
174
+
175
+ # Покажем текущий путь
176
+ gr.Markdown(f"Файл логов: `{FEEDBACK_CSV.as_posix()}`")
177
+
178
+ submit.click(
179
+ save_feedback,
180
+ inputs=[category, rating, contact, feedback_text],
181
+ outputs=[status, feedback_text, download_file],
182
+ )
183
+
184
+ download_btn.click(
185
+ get_feedback_csv,
186
+ inputs=None,
187
+ outputs=[download_file, download_status],
188
+ )
189
+
190
+ return demo
191
+
192
+
193
+ def launch_with_port_fallback(demo: gr.Blocks):
194
+ """
195
+ Устойчивый запуск:
196
+ - сначала локально (127.0.0.1, share=False)
197
+ - если Gradio считает, что localhost недоступен (proxy/настройки) -> fallback share=True
198
+ """
199
+ # Частая причина "localhost is not accessible" — прокси/NO_PROXY
200
+ os.environ.setdefault("NO_PROXY", "localhost,127.0.0.1")
201
+ os.environ.setdefault("no_proxy", "localhost,127.0.0.1")
202
+
203
+ env_port = os.getenv("GRADIO_SERVER_PORT") or os.getenv("PORT") or ""
204
+ ports: List[int] = []
205
+ if str(env_port).isdigit():
206
+ ports.append(int(env_port))
207
+
208
+ ports.extend([7860] + list(range(7861, 7871)))
209
+
210
+ last_error: Optional[Exception] = None
211
+
212
+ for port in ports:
213
+ # 1) Локальная попытка
214
+ try:
215
+ demo.launch(
216
+ server_name="127.0.0.1",
217
+ server_port=port,
218
+ share=False,
219
+ inbrowser=True,
220
+ )
221
+ return
222
+ except Exception as e:
223
+ last_error = e
224
+ msg = str(e)
225
+
226
+ # 2) Fallback: если localhost недоступен по мнению Gradio
227
+ if "localhost is not accessible" in msg or "shareable link must be created" in msg:
228
+ try:
229
+ demo.launch(
230
+ server_name="0.0.0.0",
231
+ server_port=port,
232
+ share=True,
233
+ inbrowser=False,
234
+ )
235
+ return
236
+ except Exception as e2:
237
+ last_error = e2
238
+ continue
239
+
240
+ continue
241
+
242
+ raise RuntimeError(f"Не удалось запустить Gradio ни на одном порту. Последняя ошибка: {last_error}")
243
+
244
+
245
+ if __name__ == "__main__":
246
+ app = build_interface()
247
+ launch_with_port_fallback(app)
app/messendger/__init__.py ADDED
File without changes
app/messendger/chat_engine.py ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from openai import OpenAI
2
+ from app.config import OPENAI_API_KEY
3
+
4
+ class ChatEngine:
5
+ def __init__(self):
6
+ self.client = OpenAI(api_key=OPENAI_API_KEY) if OPENAI_API_KEY else None
7
+
8
+ def answer(self, user_text: str, context: str = "") -> str:
9
+ # fallback, если ключа нет
10
+ if not self.client:
11
+ return "⚠️ OPENAI_API_KEY не задан. (Пилотный режим) Ваш вопрос: " + user_text
12
+
13
+ system = (
14
+ "Ты ассистент 2MOOD. Отвечай кратко и по делу. "
15
+ "Если дан контекст из базы знаний — опирайся на него и добавляй ссылки на источники (имя файла). "
16
+ "Если контекста нет — скажи, что в базе знаний не найдено подтверждение, и предложи уточнить."
17
+ )
18
+
19
+ user = f"Вопрос: {user_text}\n\nКонтекст из базы знаний:\n{context or '(нет)'}"
20
+
21
+ resp = self.client.chat.completions.create(
22
+ model="gpt-4o-mini",
23
+ messages=[
24
+ {"role": "system", "content": system},
25
+ {"role": "user", "content": user},
26
+ ],
27
+ temperature=0.2,
28
+ )
29
+ return resp.choices[0].message.content.strip()
app/messendger/feedback_handler.py ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sqlite3
3
+ from datetime import datetime
4
+ from app.config import FEEDBACK_DB_PATH
5
+ from app.integrations.telegram_bot import send_telegram_message
6
+
7
+ def _init_db():
8
+ os.makedirs(os.path.dirname(FEEDBACK_DB_PATH), exist_ok=True)
9
+ with sqlite3.connect(FEEDBACK_DB_PATH) as conn:
10
+ conn.execute("""
11
+ CREATE TABLE IF NOT EXISTS feedback (
12
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
13
+ created_at TEXT,
14
+ author TEXT,
15
+ channel TEXT,
16
+ topic TEXT,
17
+ rating INTEGER,
18
+ message TEXT
19
+ )
20
+ """)
21
+ conn.commit()
22
+
23
+ def save_feedback(author: str, channel: str, topic: str, rating: int, message: str) -> str:
24
+ _init_db()
25
+ ts = datetime.utcnow().isoformat(timespec="seconds") + "Z"
26
+ with sqlite3.connect(FEEDBACK_DB_PATH) as conn:
27
+ conn.execute(
28
+ "INSERT INTO feedback(created_at, author, channel, topic, rating, message) VALUES(?,?,?,?,?,?)",
29
+ (ts, author.strip(), channel.strip(), topic.strip(), int(rating), message.strip())
30
+ )
31
+ conn.commit()
32
+ return f"✅ Сохранено (UTC {ts})"
33
+
34
+ def send_feedback_to_telegram(author: str, channel: str, topic: str, rating: int, message: str) -> str:
35
+ text = (
36
+ "🧾 2MOOD Feedback\n"
37
+ f"Автор: {author}\n"
38
+ f"Канал: {channel}\n"
39
+ f"Тема: {topic}\n"
40
+ f"Оценка: {rating}/5\n\n"
41
+ f"{message}"
42
+ )
43
+ ok, info = send_telegram_message(text)
44
+ return "✅ " + info if ok else "⚠️ " + info
app/protocols/__init__.py ADDED
File without changes
app/protocols/pdf_export.py ADDED
File without changes
app/protocols/protocol_generator.py ADDED
File without changes
app/protocols/stt.py ADDED
File without changes
app/services/knowledge_service.py ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import uuid
3
+ from app.config import KB_FILES_DIR, KB_INDEX_PATH
4
+ from app.knowledge.loader import extract_text_from_pdf, save_uploaded_pdf
5
+ from app.knowledge.vector_store import BM25Index, Doc, save_index, load_index
6
+
7
+ class KnowledgeService:
8
+ def __init__(self):
9
+ os.makedirs(KB_FILES_DIR, exist_ok=True)
10
+ self.index: BM25Index = load_index(KB_INDEX_PATH)
11
+
12
+ def add_pdf(self, uploaded_path: str) -> str:
13
+ dest_path = save_uploaded_pdf(uploaded_path, KB_FILES_DIR)
14
+ text = extract_text_from_pdf(dest_path)
15
+ if not text:
16
+ return f"⚠️ Не удалось извлечь текст из PDF: {os.path.basename(dest_path)}"
17
+
18
+ doc = Doc(
19
+ doc_id=str(uuid.uuid4())[:8],
20
+ title=os.path.basename(dest_path),
21
+ text=text,
22
+ source_path=dest_path
23
+ )
24
+ self.index.add(doc)
25
+ save_index(KB_INDEX_PATH, self.index)
26
+ return f"✅ Добавлено в базу: {doc.title} (id={doc.doc_id})"
27
+
28
+ def list_docs(self) -> str:
29
+ if not self.index.docs:
30
+ return "Пока нет документов. Загрузите PDF."
31
+ lines = [f"- {d.title} (id={d.doc_id})" for d in self.index.docs]
32
+ return "\n".join(lines)
33
+
34
+ def search(self, query: str, top_k: int = 5):
35
+ return self.index.search(query, top_k=top_k)
36
+
37
+ def build_context(self, query: str, top_k: int = 4) -> str:
38
+ hits = self.search(query, top_k=top_k)
39
+ if not hits:
40
+ return ""
41
+ blocks = []
42
+ for h in hits:
43
+ blocks.append(
44
+ f"[Источник: {h['title']} | score={h['score']}]\n{h['snippet']}"
45
+ )
46
+ return "\n\n---\n\n".join(blocks)
app/services/messenger_service.py ADDED
File without changes
app/services/protocol_service.py ADDED
File without changes
app/ui/__init__.py ADDED
File without changes
app/ui/gradio_app.py ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ from app.services.knowledge_service import KnowledgeService
3
+ from app.messenger.chat_engine import ChatEngine
4
+ from app.messenger.feedback_handler import save_feedback, send_feedback_to_telegram
5
+
6
+ kb = KnowledgeService()
7
+ chat = ChatEngine()
8
+
9
+ def kb_add(pdf_file):
10
+ if pdf_file is None:
11
+ return "Загрузите PDF."
12
+ return kb.add_pdf(pdf_file)
13
+
14
+ def kb_list():
15
+ return kb.list_docs()
16
+
17
+ def kb_search(query, top_k):
18
+ hits = kb.search(query, top_k=int(top_k))
19
+ if not hits:
20
+ return "Ничего не найдено."
21
+ out = []
22
+ for h in hits:
23
+ out.append(f"### {h['title']} (score={h['score']})\n{h['snippet']}\n")
24
+ return "\n\n".join(out)
25
+
26
+ def messenger_ask(history, user_text, top_k):
27
+ context = kb.build_context(user_text, top_k=int(top_k))
28
+ answer = chat.answer(user_text, context=context)
29
+ history = history + [(user_text, answer)]
30
+ return history, ""
31
+
32
+ def messenger_quick(btn, history):
33
+ presets = {
34
+ "📌 Спросить про документ": "Что в базе знаний сказано про требования к карманам/фурнитуре/посадке?",
35
+ "🧩 Предложить улучшение": "Предложи 5 улучшений процесса, опираясь на базу знаний и лучшие практики.",
36
+ "🧾 Сформулировать вопрос": "Сформулируй уточняющий вопрос для дизайнера, чтобы убрать двусмысленность."
37
+ }
38
+ return history, presets.get(btn, "")
39
+
40
+ def feedback_submit(author, channel, topic, rating, message, also_telegram):
41
+ if not message or len(message.strip()) < 5:
42
+ return "⚠️ Сообщение слишком короткое."
43
+ saved = save_feedback(author, channel, topic, rating, message)
44
+ if also_telegram:
45
+ tg = send_feedback_to_telegram(author, channel, topic, rating, message)
46
+ return saved + "\n" + tg
47
+ return saved
48
+
49
+ def build_demo():
50
+ with gr.Blocks(title="2MOOD Feedback Knowledge Base (Pilot)") as demo:
51
+ gr.Markdown("# 2MOOD Feedback Knowledge Base (Pilot)\nПилот: база знаний (PDF) + сбор обратной связи + messenger.")
52
+
53
+ with gr.Tabs():
54
+ with gr.Tab("📚 База знаний"):
55
+ with gr.Row():
56
+ pdf = gr.File(label="Загрузить PDF", file_types=[".pdf"])
57
+ add_btn = gr.Button("➕ Добавить в базу")
58
+ add_out = gr.Textbox(label="Статус загрузки", lines=2)
59
+
60
+ with gr.Row():
61
+ list_btn = gr.Button("📄 Список документов")
62
+ docs_out = gr.Markdown()
63
+
64
+ gr.Markdown("---")
65
+ query = gr.Textbox(label="Поиск по базе", placeholder="например: 'параметры кармана' / 'правки по изделию'")
66
+ top_k = gr.Slider(1, 10, value=5, step=1, label="Top-K")
67
+ search_btn = gr.Button("🔎 Найти")
68
+ search_out = gr.Markdown()
69
+
70
+ add_btn.click(fn=kb_add, inputs=[pdf], outputs=[add_out])
71
+ list_btn.click(fn=kb_list, inputs=[], outputs=[docs_out])
72
+ search_btn.click(fn=kb_search, inputs=[query, top_k], outputs=[search_out])
73
+
74
+ with gr.Tab("💬 2MOOD Messenger"):
75
+ gr.Markdown("Чат-виджет: отвечает с опорой на базу знаний (RAG).")
76
+ history = gr.Chatbot(label="Диалог", height=380)
77
+ user_text = gr.Textbox(label="Сообщение", placeholder="Напишите вопрос…")
78
+ with gr.Row():
79
+ ask_btn = gr.Button("Отправить")
80
+ k = gr.Slider(1, 8, value=4, step=1, label="Top-K контекста")
81
+ with gr.Row():
82
+ b1 = gr.Button("📌 Спросить про документ")
83
+ b2 = gr.Button("🧩 Предложить улучшение")
84
+ b3 = gr.Button("🧾 Сформулировать вопрос")
85
+
86
+ ask_btn.click(fn=messenger_ask, inputs=[history, user_text, k], outputs=[history, user_text])
87
+ b1.click(fn=messenger_quick, inputs=[b1, history], outputs=[history, user_text])
88
+ b2.click(fn=messenger_quick, inputs=[b2, history], outputs=[history, user_text])
89
+ b3.click(fn=messenger_quick, inputs=[b3, history], outputs=[history, user_text])
90
+
91
+ with gr.Tab("📝 Обратная связь"):
92
+ gr.Markdown("Форма обратной связи: сохраняем в SQLite + опционально отправляем в Telegram.")
93
+ author = gr.Textbox(label="Автор", value="2MOOD User")
94
+ channel = gr.Dropdown(["Web Form", "Interview", "Telegram", "Email"], value="Web Form", label="Канал")
95
+ topic = gr.Dropdown(["KB", "Messenger", "Protocols", "UX", "Bug", "Feature"], value="KB", label="Тема")
96
+ rating = gr.Slider(1, 5, value=5, step=1, label="Оценка (1–5)")
97
+ message = gr.Textbox(label="Сообщение", lines=6, placeholder="Что улучшить? Что мешает? Что понравилось?")
98
+ also_tg = gr.Checkbox(label="Отправить копию в Telegram", value=True)
99
+ submit = gr.Button("✅ Отправить")
100
+ fb_out = gr.Textbox(label="Статус", lines=3)
101
+
102
+ submit.click(fn=feedback_submit, inputs=[author, channel, topic, rating, message, also_tg], outputs=[fb_out])
103
+
104
+ return demo
data/feedback/feedback.csv ADDED
@@ -0,0 +1 @@
 
 
1
+ 2026-02-15 04:54:12,Messenger,3,,Интерфейс Мессенджера не позволяет прикладывать фото.,,
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ gradio==4.44.1
2
+ openai>=1.0.0
3
+ pypdf>=4.0.0
4
+ python-dotenv>=1.0.0
5
+ requests>=2.31.0
6
+ reportlab>=4.0.0
7
+ huggingface_hub==0.25.2