Завершить аудит и подготовить AI Lead Processor к публикации
This commit is contained in:
1 parent
634490174f
commit
be36989483
14 files changed
+330
-135
No files matched your search
@@ -1,4 +1,5 @@
|
||||
"""Рабочее место менеджера сервисной компании."""
|
||||
|
||||
import os
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
@@ -11,7 +12,6 @@ from src.economics import calculate, estimate_request_usd
|
||||
from src.llm_service import classify
|
||||
from src.storage import append, snapshot
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
load_dotenv(ROOT / ".env")
|
||||
JOURNAL = Path(os.getenv("LEADS_FILE", ROOT / "data" / "processed_leads.xlsx"))
|
||||
@@ -20,19 +20,29 @@ COLORS = {"urgent": "red", "normal": "green", "spam": "gray"}
|
||||
|
||||
st.set_page_config(page_title="AI Lead Processor", layout="wide")
|
||||
st.title("AI Lead Processor — обработка клиентских заявок")
|
||||
st.write("ИИ анализирует входящие обращения, определяет их приоритет и подготавливает черновики ответов.")
|
||||
st.write(
|
||||
"ИИ анализирует входящие обращения, определяет их приоритет и подготавливает черновики ответов."
|
||||
)
|
||||
mode = st.radio("Режим обработки", ["REAL AI", "DEMO"], horizontal=True, key="mode")
|
||||
if mode == "DEMO":
|
||||
st.info("DEMO · Локальные правила. Можно проверить весь процесс без ключа.")
|
||||
else:
|
||||
st.caption("REAL AI · Текст передаётся в OpenAI. Для демонстрации используйте синтетические обращения.")
|
||||
st.caption(
|
||||
"REAL AI · Текст передаётся в OpenAI. Для демонстрации используйте синтетические обращения."
|
||||
)
|
||||
|
||||
def discard_pending():
|
||||
|
||||
def discard_pending() -> None:
|
||||
st.session_state.pop("pending", None)
|
||||
|
||||
text = st.text_area("Введите текст заявки клиента", key="lead", max_chars=4000,
|
||||
placeholder="Например, из стиральной машины течёт вода на пол.",
|
||||
on_change=discard_pending)
|
||||
|
||||
text = st.text_area(
|
||||
"Введите текст заявки клиента",
|
||||
key="lead",
|
||||
max_chars=4000,
|
||||
placeholder="Например, из стиральной машины течёт вода на пол.",
|
||||
on_change=discard_pending,
|
||||
)
|
||||
if st.button("Обработать заявку", key="process", type="primary"):
|
||||
discard_pending()
|
||||
try:
|
||||
@@ -42,7 +52,9 @@ if st.button("Обработать заявку", key="process", type="primary")
|
||||
except ValueError as exc:
|
||||
st.error(str(exc))
|
||||
if mode == "REAL AI":
|
||||
st.caption("Для демонстрации выберите DEMO и снова нажмите «Обработать заявку».")
|
||||
st.caption(
|
||||
"Для демонстрации выберите DEMO и снова нажмите «Обработать заявку»."
|
||||
)
|
||||
|
||||
pending = st.session_state.get("pending")
|
||||
if pending and pending[2] != mode:
|
||||
@@ -58,35 +70,66 @@ if pending:
|
||||
if used_mode == "REAL AI" and result.input_tokens is not None:
|
||||
usage += f" · Входные токены: {result.input_tokens} · Выходные токены: {result.output_tokens}"
|
||||
st.caption(usage)
|
||||
category = st.selectbox("Категория после проверки", list(LABELS),
|
||||
index=list(LABELS).index(result.category), format_func=LABELS.get, key=f"category-{uid}")
|
||||
category = st.selectbox(
|
||||
"Категория после проверки",
|
||||
list(LABELS),
|
||||
index=list(LABELS).index(result.category),
|
||||
format_func=LABELS.get,
|
||||
key=f"category-{uid}",
|
||||
)
|
||||
reply = ""
|
||||
if category != "spam":
|
||||
reply = st.text_area("Черновик ответа клиенту", value=result.draft_reply, max_chars=4000, key=f"reply-{uid}")
|
||||
reply = st.text_area(
|
||||
"Черновик ответа клиенту",
|
||||
value=result.draft_reply,
|
||||
max_chars=4000,
|
||||
key=f"reply-{uid}",
|
||||
)
|
||||
if st.button("Подтвердить и сохранить", key="save"):
|
||||
try:
|
||||
if category != "spam" and not reply.strip():
|
||||
raise ValueError("Заполните черновик ответа перед сохранением.")
|
||||
cost = (0.0 if used_mode == "DEMO" else estimate_request_usd(
|
||||
result.model, result.input_tokens, result.output_tokens, result.cached_tokens or 0))
|
||||
cost = (
|
||||
0.0
|
||||
if used_mode == "DEMO"
|
||||
else estimate_request_usd(
|
||||
result.model,
|
||||
result.input_tokens,
|
||||
result.output_tokens,
|
||||
result.cached_tokens or 0,
|
||||
)
|
||||
)
|
||||
reason = result.reason
|
||||
if category != result.category:
|
||||
reason = f"Менеджер изменил категорию «{LABELS[result.category]}» на «{LABELS[category]}». Исходная оценка: {reason}"
|
||||
append(JOURNAL, {
|
||||
"ID": uid, "Дата и время": datetime.now().astimezone().isoformat(timespec="seconds"),
|
||||
"Текст заявки": original, "Категория": LABELS[category],
|
||||
"Причина классификации": reason, "Черновик ответа": reply.strip(),
|
||||
"Статус проверки": "Проверено менеджером", "Режим обработки": used_mode,
|
||||
"Модель": result.model, "Входные токены": result.input_tokens,
|
||||
"Выходные токены": result.output_tokens, "Оценочная стоимость API-запроса, USD": cost,
|
||||
})
|
||||
append(
|
||||
JOURNAL,
|
||||
{
|
||||
"ID": uid,
|
||||
"Дата и время": datetime.now()
|
||||
.astimezone()
|
||||
.isoformat(timespec="seconds"),
|
||||
"Текст заявки": original,
|
||||
"Категория": LABELS[category],
|
||||
"Причина классификации": reason,
|
||||
"Черновик ответа": reply.strip(),
|
||||
"Статус проверки": "Проверено менеджером",
|
||||
"Режим обработки": used_mode,
|
||||
"Модель": result.model,
|
||||
"Входные токены": result.input_tokens,
|
||||
"Выходные токены": result.output_tokens,
|
||||
"Оценочная стоимость API-запроса, USD": cost,
|
||||
},
|
||||
)
|
||||
discard_pending()
|
||||
st.session_state.saved = True
|
||||
st.rerun()
|
||||
except ValueError as exc:
|
||||
st.error(str(exc))
|
||||
except OSError:
|
||||
st.error("Не удалось сохранить журнал. Закройте книгу в Excel, проверьте права на папку и повторите сохранение.")
|
||||
st.error(
|
||||
"Не удалось сохранить журнал. Закройте книгу в Excel, проверьте права на папку и повторите сохранение."
|
||||
)
|
||||
|
||||
if st.session_state.pop("saved", False):
|
||||
st.success("Заявка сохранена. Ответ клиенту отправляет менеджер.")
|
||||
@@ -97,13 +140,21 @@ try:
|
||||
st.dataframe(rows[::-1], hide_index=True, width="stretch")
|
||||
else:
|
||||
st.caption("Здесь появятся заявки, которые вы подтвердите.")
|
||||
st.download_button("Скачать Excel", workbook, file_name="processed_leads.xlsx",
|
||||
mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
|
||||
st.download_button(
|
||||
"Скачать Excel",
|
||||
workbook,
|
||||
file_name="processed_leads.xlsx",
|
||||
mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
)
|
||||
except (ValueError, OSError):
|
||||
st.error("Не удалось открыть журнал. Проверьте права доступа, целостность книги и заголовки листа «Заявки».")
|
||||
st.error(
|
||||
"Не удалось открыть журнал. Проверьте права доступа, целостность книги и заголовки листа «Заявки»."
|
||||
)
|
||||
|
||||
with st.expander("Экономический эффект"):
|
||||
st.caption("Иллюстративный сценарий для gpt-5-mini. Один запрос на заявку, 600 входных и 250 выходных токенов, без кэша.")
|
||||
st.caption(
|
||||
"Иллюстративный сценарий для gpt-5-mini. Один запрос на заявку, 600 входных и 250 выходных токенов, без кэша."
|
||||
)
|
||||
a, b, c = st.columns(3)
|
||||
count = a.number_input("Заявок в неделю", min_value=0, value=120)
|
||||
manual = b.number_input("Ручная обработка, мин", min_value=0.0, value=4.0)
|
||||
@@ -112,8 +163,14 @@ with st.expander("Экономический эффект"):
|
||||
rate = b.number_input("Условный курс, ₽ за доллар", min_value=0.0, value=90.0)
|
||||
weeks = c.number_input("Недель в месяце", min_value=0.0, value=4.33)
|
||||
e = calculate(count, manual, review, weeks, hourly, rate)
|
||||
st.write(f"Время за неделю: {e.before_hours:g} ч вручную → {e.after_hours:g} ч с проверкой. "
|
||||
f"Экономия: {e.saved_hours:g} ч в неделю и {e.monthly_saved_hours:.2f} ч в месяц.")
|
||||
st.write(f"API: {e.request_usd:.5f} USD за заявку · {e.weekly_api_usd:.4f} USD в неделю · {e.monthly_api_usd:.4f} USD в месяц.")
|
||||
st.write(f"Оценка высвобождённого времени за вычетом API: {e.weekly_net_rub:.2f} ₽ в неделю "
|
||||
f"и {e.monthly_net_rub:.2f} ₽ в месяц. Фактическая экономия фонда оплаты труда зависит от загрузки.")
|
||||
st.write(
|
||||
f"Время за неделю: {e.before_hours:g} ч вручную → {e.after_hours:g} ч с проверкой. "
|
||||
f"Экономия: {e.saved_hours:g} ч в неделю и {e.monthly_saved_hours:.2f} ч в месяц."
|
||||
)
|
||||
st.write(
|
||||
f"API: {e.request_usd:.5f} USD за заявку · {e.weekly_api_usd:.4f} USD в неделю · {e.monthly_api_usd:.4f} USD в месяц."
|
||||
)
|
||||
st.write(
|
||||
f"Оценка высвобождённого времени за вычетом API: {e.weekly_net_rub:.2f} ₽ в неделю "
|
||||
f"и {e.monthly_net_rub:.2f} ₽ в месяц. Фактическая экономия фонда оплаты труда зависит от загрузки."
|
||||
)
|
||||
Reference in new issue
Block a user