--- title: ExecChat emoji: 📊 colorFrom: red colorTo: yellow sdk: docker pinned: false --- # ExecChat — генератор ответов поддержки домовят Веб-приложение для помощи операторам поддержки сервиса уборки «Домовёнок»: по вопросу исполнителя (домовёнка) строится ответ на основе базы знаний и при необходимости вызываются инструменты (доходы, качество, штрафы). --- ## Как устроена система 1. **База знаний** — текст подгружается из Google Doc, режется на чанки (`CHUNK_SIZE=800`, перекрытие `CHUNK_OVERLAP=200`, `RecursiveCharacterTextSplitter`) и индексируется в FAISS (эмбеддинги OpenAI). При первом запуске, изменении документа или версии индекса (`INDEX_VERSION`) индекс пересобирается; хэш документа хранится в `doc_hash.pkl`. 2. **System prompt** — загружается из отдельного Google Doc (редактирование только там; версии — через историю документа). На странице — read-only просмотр и кнопка «Обновить промпт из документа». При `ENABLE_TOOLS=1` к промпту из документа дописывается блок про инструменты из кода. 3. **Классификатор social-intent** (`gpt-4.1-nano`) — чистые вежливые реплики (спасибо/привет/прощание/комплимент) получают короткий вежливый ответ и **не** уходят в эскалацию и RAG. 4. **Переписывание запроса** (`QUERY_REWRITE_MODEL`, по умолчанию `gpt-4.1-mini`) — шумное сообщение исполнителя превращается в чистый короткий поисковый запрос (опечатки, местоимения, учёт последних реплик). Качество rerank сильно зависит от этого шага. 5. **Retrieval + rerank** — FAISS отдаёт `RETRIEVAL_CANDIDATES=30` кандидатов, затем cross-encoder **`BAAI/bge-reranker-v2-m3`** переранжирует их; в контекст идут чанки из топ-`RERANK_TOP_N=8` с rerank-скором ≥ `RERANK_SCORE_THRESHOLD=0.1`. Чанки обрезаются до `MAX_CHUNK_CHARS=800`. Если порог не прошёл ни один чанк — бот **не эскалирует**, а вежливо отвечает, что вопрос вне темы сервиса (без вызова менеджера); эскалация остаётся только при сбое генерации или при явном провале guardrails (`ENABLE_OUTPUT_GUARDRAILS=1`). 6. **Ответ генерирует OpenAI GPT-4.1 mini** (`LLM_MODEL`, по умолчанию `gpt-4.1-mini`): system prompt + контекст из RAG + история диалога (последние 3 пары) + текущий вопрос. `max_tokens=512`. **Temperature** настраивается на странице (кнопка «Применить temperature»). 7. **Guardrails на выходе** — детерминированный `enforce_format` (≤2 абзацев, срез «приглашений задавать вопросы»). Проверка **grounding** (цельный ответ vs контекст, `gpt-4.1-nano`) и корректирующая регенерация **выключены по умолчанию** (`ENABLE_OUTPUT_GUARDRAILS=0`): давали ложные NOT_GROUNDED на корректных перефразах. При повторении галлюцинаций имеет смысл вернуть проверку в виде **claim-level** (по отдельным утверждениям), а не одного вердикта на весь ответ — тогда можно включить `ENABLE_OUTPUT_GUARDRAILS=1`. Модерация токсичности есть в коде, но по умолчанию выключена (`ENABLE_OUTPUT_MODERATION=False`). 8. **Инструменты (tools)** — при `ENABLE_TOOLS=1` модель может вызывать функции из `tools.py`. В `app_2.py` реализован цикл: запрос к модели → при наличии `tool_calls` выполняются инструменты → результаты передаются обратно в модель → ответ без вызовов возвращается пользователю. 9. **Многоходовый диалог** — в контекст ответа передаётся **весь диалог**, который был до сих пор (полная история сессии). Переписывание запроса при этом по-прежнему смотрит только на последние `MAX_HISTORY_TURNS=3` пары. > **Пометка для prod (coconut):** сейчас история берётся из `st.session_state` (одна Streamlit-сессия = один диалог). При реализации в **coconut** контекст нужно передавать **в рамках `dialog_id`** — собирать все сообщения конкретного диалога по его идентификатору, а не полагаться на сессию. Для длинных историй стоит делать суммаризацию старых сообщений, чтобы не упираться в контекст/стоимость. --- ## Структура проекта | Файл / папка | Назначение | |--------------|------------| | **`app_2.py`** | Точка входа: Streamlit-интерфейс, загрузка Google Doc, построение/загрузка FAISS, GPT-4.1 mini с инструментами, RAG, цикл вызова инструментов и формирование ответа. | | **`tools.py`** | Определения инструментов для LLM (декоратор `@tool` из LangChain). Сюда добавлять новые инструменты и подключать реальные API/БД. | | **`evals/`** | Встроенный харнесс проверок: прогон сценариев против **живого** промпта и базы знаний (`runtime_live.py`), детерминированные проверки (`checks.py`), LLM-судья (`judge.py`), чтение/запись сценариев в Google Sheet (`sheets.py`), стартовый набор (`scenarios_seed.yaml`). | | **`requirements.txt`** | Зависимости Python (Streamlit, LangChain, OpenAI, FAISS, Google API и т.д.). | | **`faiss_index/`** | Локальный индекс FAISS (создаётся при первом запуске). | | **`doc_hash.pkl`** | Хэш документа базы знаний | | **`prompt_hash.pkl`** | Хэш документа с system prompt | --- ## Инструменты (tools) Все инструменты объявлены в **`tools.py`** и возвращаются функцией **`get_all_tools()`**. - **`get_exec_next_orders`** — детали ближайших заказов исполнителя (`executive_id`). - **`get_exec_quality_params`** — параметры качества работы исполнителя (`executive_id`, опционально `period`, по умолчанию 14 дней, макс. 30). - **`get_exec_revenue_and_fines_feed`** — лента доходов и штрафов исполнителя (`executive_id`, опционально `period`, по умолчанию 7 дней, макс. 10). - **`get_exec_orders_history`** — история выполненных заказов исполнителя (`executive_id`, опционально `period`, по умолчанию 7 дней, макс. 10). Сейчас внутри — заглушки с текстом «Подключите ваш API». Чтобы подключить реальные данные: в каждой функции замените тело на вызов вашего API или БД и возвращайте строку с результатом (она попадёт в контекст модели). **Как добавить новый инструмент:** 1. В `tools.py` описать функцию с типами аргументов и **docstring** (по нему модель понимает, когда инструмент вызывать). 2. Обернуть функцию в `@tool` из `langchain_core.tools`. 3. Добавить её в список возврата функции `get_all_tools()`. Изменения в `app_2.py` не требуются — список инструментов подхватывается при инициализации. --- ## Проверка сценариев (evals) На странице есть скрытая admin-панель **«🧪 Проверка сценариев»**, которая прогоняет наборы тест-сценариев против **текущего** (живого) промпта и базы знаний — удобно после правок промпта/документа убедиться, что ничего не сломалось. Сценарии и их ожидания (`expect`) хранятся в Google-таблице, поэтому их можно редактировать прямо из UI, не трогая код. **Что умеет панель:** - **Прогон** — выбрать сценарии, запустить, увидеть таблицу PASS/FAIL с детализацией каждой проверки и ответом бота. При `ENABLE_TOOLS=1` тул-сценарии используют mock-инструменты с реальными именами (реальные API не вызываются). - **Редактор** — добавлять/редактировать/удалять сценарии и сохранять их в Google Sheet. Если таблица пустая — кнопка «Засеять стандартным набором» из `evals/scenarios_seed.yaml`. Сценарий идентифицируется по полю **`id`** (короткое уникальное имя латиницей). **Проверки (`expect`):** `escalate`, `format_ok`, `no_prompt_leak`, `tools_any`, `tools_none` (детерминированные) и `judge` — критерий на естественном языке, который оценивает LLM-судья (`JUDGE_MODEL`, по умолчанию `gpt-4o-mini`). Сценарий проходит, если прошли все заполненные проверки. `format_ok` и `no_prompt_leak` включены принудительно во всех сценариях, поэтому в UI их нет. **Инструменты — два независимых понятия (для агентов с tools):** - **Доступ** (`use_tools`, вход) — даём ли боту инструменты в этом сценарии: `Да` / `Нет` / `Авто`. `Да` даёт доступ **независимо** от глобальной галочки прогона; `Авто` берёт значение из настроек прогона («По умолчанию давать инструменты»); `Нет` — никогда. Жёсткое условие одно: сам агент должен поддерживать инструменты (для RAG-only агентов раздел скрыт). - **Проверка** (`tools_any` / `tools_none`, ассерт) — что бот сделал по факту: должен ли был вызвать инструмент (`tools_any` — достаточно любого из перечисленных) или не должен был (`tools_none`). Чтобы проверка `tools_any` могла пройти, в сценарии должен быть разрешён доступ. > **Инструменты в харнессе завязаны на `ENABLE_TOOLS`** (тот же флаг, что и для бота). Пока `ENABLE_TOOLS=0` (значение по умолчанию на проде), инструментов «нет» и для бота, и для проверок: тул-виджеты скрыты, mock-инструменты не подставляются, бот в евалах ведёт себя как tool-less. Когда инструменты будут реализованы — поставьте `ENABLE_TOOLS=1`, и тестирование инструментов вернётся без правок кода. На время `ENABLE_TOOLS=0` тул-сценарии (с проверкой `tools_any`) будут падать — не выбирайте их в прогоне или временно удалите из таблицы. **Доступ — за паролем.** Раздел появляется только если задан секрет `EVAL_PASSWORD`. Схема простая: вы сами придумываете пароль, кладёте его в секреты Space (`EVAL_PASSWORD`) и раздаёте доверенным людям. Без правильного пароля панель не разворачивается; это не полноценная авторизация, а лёгкий барьер, чтобы случайные пользователи не запускали платные прогоны. **Настройка хранилища (Google Sheet):** 1. Создайте Google-таблицу. Первая вкладка будет использоваться целиком (шапка пишется автоматически). 2. Расшарьте её на `client_email` того же сервисного аккаунта, что и для Google Docs, с правом **Редактор**. 3. В Google Cloud проекте включите **Google Sheets API**. 4. ID таблицы (из её URL) положите в секрет `SCENARIOS_SHEET_ID`. > Сервисному аккаунту нужен scope `https://www.googleapis.com/auth/spreadsheets` — он запрашивается в коде (`evals/sheets.py`), отдельной настройки не требует, важно лишь дать аккаунту доступ к самой таблице (шаг 2) и включить API (шаг 3). --- ## Переменные окружения и секреты | Переменная | Назначение | |------------|------------| | **`OPENAI_API_KEY`** | Ключ OpenAI (эмбеддинги FAISS + генерация ответов). | | **`GOOGLE_CREDENTIALS`** | JSON сервисного аккаунта Google (доступ к Google Doc). Либо в .env, либо в секретах Streamlit/HF под ключом `google`. | | **`LLM_MODEL`** | (опционально) модель генерации ответа, по умолчанию `gpt-4.1-mini`. | | **`QUERY_REWRITE_MODEL`** | (опционально) модель переписывания запроса, по умолчанию `gpt-4.1-mini`. | | **`RERANK_MODEL`** | (опционально) cross-encoder для rerank, по умолчанию `BAAI/bge-reranker-v2-m3`. | | **`SOCIAL_INTENT_MODEL`** | (опционально) модель классификатора social-intent, по умолчанию `gpt-4.1-nano`. | | **`ENABLE_TOOLS`** | Включить инструменты (`1` или `true`). По умолчанию `0` — инструменты выключены и у бота, и в харнессе проверок (тул-виджеты скрыты, mock-инструменты не подставляются). **На Hugging Face не задавайте** (останется версия без tools). Локально для разработки с tools добавьте в .env: `ENABLE_TOOLS=1`. | | **`ENABLE_OUTPUT_GUARDRAILS`** | (опционально) LLM-проверка grounding цельного ответа + регенерация при провале. По умолчанию `0` (выключено). Включать (`1`) только после перехода на claim-level проверку — см. раздел про guardrails выше. | | **`EVAL_PASSWORD`** | Пароль для входа в admin-панель «Проверка сценариев». Без него раздел скрыт. Придумайте сами и раздайте доверенным людям. | | **`SCENARIOS_SHEET_ID`** | ID Google-таблицы, где хранятся сценарии проверок (см. раздел ниже). | | **`JUDGE_MODEL`** | (опционально) модель LLM-судьи для проверок, по умолчанию `gpt-4o-mini`. | Локально достаточно создать `.env` с этими переменными. На Hugging Face Spaces секреты задаются в настройках Space (Settings → Variables and secrets). --- ## Запуск **Локально:** ```bash pip install -r requirements.txt streamlit run app_2.py ``` Приложение откроется по адресу http://localhost:8501. При первом запуске подтягивается Google Doc и строится FAISS (1–2 минуты). **На Hugging Face:** репозиторий разворачивается как Space (Docker); переменные окружения задаются в интерфейсе Space. --- ## Конфигурация в коде В `app_2.py` в начале заданы: - **`DOCUMENT_ID`** (база знаний) — `1p9J-knk-d7gIOnzDnjN_kuejP0x09VqIG3KJGMfVxh8` - **`PROMPT_DOCUMENT_ID`** (system prompt) — `1hk1PS4hkvp8MFDe0gGIDfYNsgzJCN5gMmyjMG0pkggM` - https://docs.google.com/document/d/1hk1PS4hkvp8MFDe0gGIDfYNsgzJCN5gMmyjMG0pkggM/edit - **`FAISS_INDEX_PATH`** — каталог для сохранения индекса FAISS. - **`HASH_FILE`** / **`PROMPT_HASH_FILE`** — хэши документов для кэширования. Перед деплоем расшарьте **оба** документа на `client_email` сервисного аккаунта (доступ «Просмотр»). Кнопка «Обновить базу знаний» сбрасывает кэш FAISS. После правки промпта в Google Doc нажмите **«Обновить промпт из документа»**. Если промпт-doc пуст, используется `DEFAULT_SYSTEM_TEMPLATE` из кода. --- ## Деплой на Hugging Face 1. Задать секреты в Space Settings → Variables and secrets: `OPENAI_API_KEY`, `GOOGLE_CREDENTIALS`. Для admin-панели проверок также `EVAL_PASSWORD` и `SCENARIOS_SHEET_ID` (см. раздел «Проверка сценариев»). 2. Удалить `ANTHROPIC_API_KEY` из секретов Space (больше не используется) 3. Загрузить файлы через `hf upload` (из-за `.docx` в git history обычный `git push` может быть заблокирован): ```bash hf upload DomovenokChatbots/ExecChat app_2.py README.md requirements.txt .gitignore --commit-message "GPT-4.1 mini + token optimization" hf upload DomovenokChatbots/ExecChat evals/ evals/ --commit-message "in-app eval harness" ``` ### Стоимость (ориентир) | | Было (Sonnet 4.6) | Стало (GPT-4.1 mini) | |--|-------------------|----------------------| | Input / 1M tok | ~$3 | ~$0.40 | | Output / 1M tok | ~$15 | ~$1.60 | | Типичный запрос | ~$0.02 | ~$0.002 | --- Конфигурация Space: https://huggingface.co/docs/hub/spaces-config-reference