Files
Deal/archive/leadradar-legacy/backend/app/constants.py
T
Rustam Khalimov 713d554dc2 Нормализовать переводы строк в LF
Решение по TD-STYLE-ANALYZERS: LF — инструменты проекта (Python/Node) пишут LF,
CRLF-.sh не работают на Linux CI (sh scripts/ci.sh), большинство файлов уже были
LF. Добавлен .gitattributes (* text=auto eol=lf, бинарные исключения),
.editorconfig переведён на lf, 1029 файлов конвертированы, git add --renormalize.
Из индекса убраны закравшиеся archive/**/__pycache__/*.pyc.
2026-09-11 19:01:42 +03:00

254 lines
20 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Базовые константы: палитры, стадии, промпты, валюты, провайдеры AI.
Значения согласованы с фронтенд-прототипом (frontend/src/data.js),
чтобы интеграция была безболезненной.
"""
# Палитра колонок-досок (по умолчанию)
PALETTE = ["#818cf8", "#fbbf24", "#22d3ee", "#e879f9", "#34d399", "#fb7185", "#a78bfa", "#f97316"]
# Колонки не создаются по умолчанию: их создаёт пользователь или предлагает
# ИИ (suggested=TRUE — ждёт решения пользователя).
# Служебные колонки дашборда
SERVICE_COLS = {"inbox", "archive", "trash", "taken"}
# Стадии канбана «Выбранных»
PIPELINE_STAGES = [
{"id": "planned", "name": "Запланировано", "color": "#818cf8", "terminal": False},
{"id": "reply", "name": "Отклик", "color": "#38bdf8", "terminal": False},
{"id": "agree", "name": "Согласование", "color": "#a78bfa", "terminal": False},
{"id": "work", "name": "В работе", "color": "#fbbf24", "terminal": False},
{"id": "review", "name": "Проверка", "color": "#f97316", "terminal": False},
{"id": "ready", "name": "Готово", "color": "#4ade80", "terminal": False},
{"id": "hold", "name": "Отложено", "color": "#94a3b8", "terminal": False},
{"id": "finished", "name": "Выполнено", "color": "#2bd576", "terminal": True},
{"id": "rejected", "name": "Отклонено", "color": "#ff6b6b", "terminal": True},
]
# Валюты и мок-курсы (до первого успешного запроса к ЦБ)
CURRENCIES = [
{"code": "RUB", "name": "Российский рубль", "symbol": "₽"},
{"code": "USD", "name": "Доллар США", "symbol": "$"},
{"code": "EUR", "name": "Евро", "symbol": "€"},
{"code": "CNY", "name": "Китайский юань", "symbol": "¥"},
{"code": "KZT", "name": "Казахстанский тенге", "symbol": "₸"},
{"code": "BYN", "name": "Белорусский рубль", "symbol": "Br"},
{"code": "USDT", "name": "Tether (USDT)", "symbol": "₮"},
{"code": "GBP", "name": "Британский фунт", "symbol": "£"},
]
MOCK_RATES = {
"RUB": 1.0,
"USD": 92.5,
"EUR": 99.9,
"CNY": 13.1,
"KZT": 0.19,
"BYN": 28.6,
"USDT": 92.5,
"GBP": 117.4,
}
CBR_URL = "https://www.cbr-xml-daily.ru/daily_json.js"
# Стоп-фразы по умолчанию (этап 1 фильтра — без ИИ)
DEFAULT_STOP_PHRASES = ["взаимный пиар", "резюме", "ищу работу", "набор в команду"]
DEFAULT_MIN_LEN = 24
# Промпты. В тексте можно использовать плейсхолдеры, которые подставляются из
# настроек «Сферы и ключей» при каждом вызове ИИ:
# {domain} — domainDescription (что для вас заявка/лид, ваша сфера)
# {keywords} — domainKeywords (общие слова-маркеры заявок)
DEFAULT_AI_PROMPT = (
"Ты — классификатор входящих сообщений. Сообщение — ЗАЯВКА (лид), только если в нём есть конкретный"
" запрос или предложение по делу: заказ услуги/товара/работы, поиск исполнителя или найм человека."
" Направление вашей сферы:\n{domain}\n\n"
"Частые слова-маркеры заявок в вашей сфере: {keywords}\n\n"
"Определи по тексту:\n"
"1. is_spam — true, если это НЕ заявка: служебные сообщения (коды входа/подтверждения, уведомления),"
" приветствия и поздравления, флуд и обсуждения без задачи, вопросы без конкретики, реклама, скам,"
" фин. пирамиды, резюме соискателей, взаимный пиар, приглашения в чаты, рассылки."
" Если сомневаешься — ставь true (лучше пропустить сообщение, чем засорить карточками)\n"
"2. board — id подходящей доски из списка. Выбирай по КРИТЕРИЯМ колонки"
" (в скобках указаны её направление, тематика, ключевые слова, уровень, бюджет, описание), а не только по названию;"
" если ни одна колонка не подходит под текст — верни null (карточка пойдёт в «Неразобранное»)\n"
"3. is_vacancy — true, если это НАЙМ/постоянная или проектная занятость: ищут человека в команду"
" (признаки: вакансия, грейд/уровень, «в компанию», hr, зарплата за месяц);"
" false — разовая сделка: заказ/услуга/товар (фриланс, подряд, «нужно сделать/купить», цена за работу)\n"
"4. title — короткий заголовок (4–9 слов, без эмодзи, хэштегов и знаков препинания в конце)."
" Запрещено: markdown, ссылки в любом виде ([текст](url), голые url) — только чистый текст\n"
"5. company — кто разместил заявку: компания/бренд/частное лицо/заказчик. Только факт из текста;"
" не указано — верни пустую строку \"\"\n"
"6. format — формат работы/выполнения: удалённо/офис/гибрид, город, график. Коротко; нет — \"\"\n"
"7. task — 1–2 предложения: что за задача/роль и в чём суть (для кого, что нужно сделать)."
" Ёмко, без пересказа всего объявления и без служебных строк\n"
"8. requirements — JSON-массив строк ключевых требований к кандидату/исполнителю"
" (что нужно уметь/иметь, пункты «Требования/Обязанности/Нужно»). Нет — []\n"
"9. plus — JSON-массив строк «будет плюсом»/«приветствуется»/«желательно». Нет — []\n"
"10. conditions — условия одной строкой: оплата/ЗП/вилка, сроки, объём, тип занятости. Нет — \"\"\n"
"11. stack — JSON-массив строк (не строка!): технологии/предметы/услуги/материалы из заявки."
" Названия пиши слитно как в оригинале: \".NET\", \"C#\", \"Node.js\", \"ASP.NET Core\" — не разбивай на отдельные буквы (2–8)\n"
"12. budget — объект { from, to, currency }: одна сумма → from=to=X; «до X» → from=null, to=X;"
" диапазон «от X до Y» → from=X, to=Y. Если суммы нет — null\n"
"13. contacts — JSON-массив строк контактов ДЛЯ СВЯЗИ: телефон, email, @username (не бот),"
" ссылка на профиль человека (LinkedIn, t.me/…). НЕ включай ботов, каналы/группы и ссылки на"
" вакансию/пост/форму. Если контакта в тексте нет — пустой массив []\n"
"Поля 5–10 — это блок «О заявке» на карточке: заполняй их только фактами из текста, ничего не выдумывай."
" Запрещено: markdown-разметка, ссылки в любом виде, эмодзи, хэштеги, «от»/«привет», дословное копирование исходника.\n"
"Отвечай строго в формате JSON."
)
DEFAULT_AI_FILTER_PROMPT = (
"Ты — страж входящих сообщений каналов. Пропускай только реальные заявки/лиды по вашей сфере"
" (заказ, услуга, товар, найм — с конкретикой).\n"
"Ваша сфера и что считать заявкой:\n{domain}\n\n"
"Слова-маркеры заявок: {keywords}\n\n"
"НЕ пропускай:\n"
"- служебные сообщения: коды входа/подтверждения, уведомления, приветствия, поздравления\n"
"- просто сообщения без задачи и конкретики: флуд, обсуждения, вопросы «кто работал с …»\n"
"- рекламу и саморекламу\n"
"- скам, фин. пирамиды, «заработок»\n"
"- резюме, поиск работы соискателями\n"
"- взаимный пиар, приглашения в чаты\n"
"- рассылки и дайджесты без прямых заявок\n"
"При сомнении — не пропускай.\n"
'Верни строго JSON: { "pass": true|false, "reason": "причина отказа или null" }'
)
# Промпт структуры карточки (блок «О заявке»). Отдельный от классификатора:
# задаёт, какие поля и как заполнять, чтобы все карточки имели одинаковую
# структуру текста (разной длины). Добавляется к промпту классификатора
# отдельным блоком (см. services/ai.classify). Редактируется в UI.
DEFAULT_AI_CARD_PROMPT = (
"Ты также возвращаешь содержимое блока «О заявке» карточки — поля company, format,"
" task, requirements, plus, conditions. Карточка всегда собирается из одних и тех же блоков:"
" одинаковая структура у всех карточек, различается только длина.\n"
"1. company — кто ищет/разместил: компания, бренд, агентство, частное лицо, заказчик. Одно предложение, факт из текста.\n"
"2. format — формат работы: удалённо/офис/гибрид/разъездной, город/страна, график (5/2, full-time, part-time). Коротко.\n"
"3. task — 1–2 предложения по шаблону: что за задача/роль → для кого → что нужно сделать/какой результат."
" Пиши ёмко, не пересказывай объявление дословно.\n"
"4. requirements — вынеси сюда только реальные требования/обязанности из текста"
" (пункты списков «Требования», «Обязанности», «Что предстоит делать», «Нужно»):"
" каждый пункт короткой строкой в JSON-массиве. Не придумывай сверх текста.\n"
"5. plus — только то, что отмечено как «будет плюсом»/«приветствуется»/«желательно»."
" Нет таких пунктов — пустой массив [].\n"
"6. conditions — условия одной строкой: оплата/ЗП/вилка/гонорар, сроки, объём, тип занятости."
" Не дублируй budget-числа в других полях.\n"
"Для не-IT сфер «стек/требования» означают материалы, услуги, навыки, инструменты — по смыслу заявки."
" Запрещено во всех полях: markdown-разметка, ссылки в любом виде ([текст](url), голые url),"
" эмодзи, хэштеги, «от»/«привет»/служебные строки канала."
)
# Дефолтные маркеры локального разбора (настраиваются в UI «Сфера и ключи»).
DEFAULT_HIRE_MARKERS = [
"вакансия", "вакансию", "вакансии", "вакантна", "вакант", "нанимаем", "найм", "full-time",
"на постоянную", "занятость", "в офис", "официальное оформление", "пятидневка",
"грейд", "в компанию", "полная занятость", "на постоянную работу", "в команду", "нанимает",
"на постоянную основу", "в штат",
]
DEFAULT_LEVEL_TERMS = [
"junior", "джун", "джуниор", "middle", "мидл", "mid", "senior", "сеньор", "сеньйор",
"lead", "тимлид", "тиэмлид", "architect", "архитектор", "стажёр", "стажер", "intern", "trainee",
]
# Маркеры, по которым сообщение опознаётся как резюме/самопрезентация
# соискателя («ищу работу», «моё резюме»). По умолчанию такие сообщения
# отсекаются на этапе 1 (до ИИ и правил колонок), если пользователь ищет
# вакансии и заказы, а не кандидатов. Редактируется в «Сфере и ключах»;
# выключите отсев — резюме начнут собираться как обычные лиды.
# Слово «резюме» имеет контекстный guard (см. pipeline._resume_reason):
# «…вакансия…, присылайте резюме» — это объявление работодателя, его не режем.
DEFAULT_RESUME_MARKERS = [
"резюме", "#резюме", "моё резюме", "мое резюме", "ищу работу", "ищу вакансию",
"рассмотрю предложения", "готов к собеседованию", "в поиске работы",
"ищу проект", "ищу подработку",
]
# AI-провайдеры (активен один)
AI_PROVIDERS = [
{"id": "deepseek", "name": "DeepSeek", "base": "https://api.deepseek.com", "local": False,
"models": ["deepseek-v4-flash", "deepseek-v4-pro", "deepseek-v4-flash-vision-exp"]},
{"id": "openai", "name": "OpenAI", "base": "https://api.openai.com/v1", "local": False,
"models": ["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"]},
{"id": "openrouter", "name": "OpenRouter", "base": "https://openrouter.ai/api/v1", "local": False,
"models": ["deepseek/deepseek-v4-flash", "anthropic/claude-sonnet-5", "openai/gpt-5.6-luna",
"google/gemini-3.8-flash", "qwen/qwen3.8-flash"]},
{"id": "anthropic", "name": "Anthropic Claude", "base": "https://api.anthropic.com", "local": False,
"api_style": "anthropic",
"models": ["claude-sonnet-5", "claude-opus-5", "claude-fable-5-1", "claude-haiku-4-5-20251001"]},
{"id": "ollama", "name": "Ollama (локально)", "base": "http://localhost:11434/v1", "local": True,
"models": ["qwen3-coder:30b", "qwen3-coder:480b", "qwen3:32b"]},
{"id": "lmstudio", "name": "LM Studio (локально)", "base": "http://localhost:1234/v1", "local": True,
"models": ["qwen3-coder-30b", "qwen3-coder-480b", "llama-3.3-70b"]},
{"id": "custom", "name": "Другой (OpenAI-совместимый)", "base": "https://", "local": False, "models": []},
]
# Настройки по умолчанию (ключи -> значение; не секреты)
DEFAULT_SETTINGS = {
# хранилище
"autoArchive": True,
"archiveAfterDays": 14, # 1..30
"archiveClearDays": 90,
"trashClearDays": 7,
# фильтры входящих
"minLen": DEFAULT_MIN_LEN,
"stopPhrases": DEFAULT_STOP_PHRASES,
"mlEnabled": True, # локальный ML-слой (обучается на действиях пользователя)
"aiEnabled": True, # полный выключатель ИИ: false = только фильтр + ML
"aiFilterEnabled": True,
"aiFilterPrompt": DEFAULT_AI_FILTER_PROMPT,
"aiPrompt": DEFAULT_AI_PROMPT,
"cardPrompt": DEFAULT_AI_CARD_PROMPT, # структура карточки/блока «О заявке» (отдельный промпт)
# сфера и ключи (универсальный классификатор: редактируется в UI)
"domainDescription": "",
"domainKeywords": [],
"hireMarkers": DEFAULT_HIRE_MARKERS,
"levelTerms": DEFAULT_LEVEL_TERMS,
"resumeMarkers": DEFAULT_RESUME_MARKERS,
"blockResumes": True, # True = ищем вакансии/заказы, резюме соискателей отсекаем
# тип заявок, которые собираем: both | vacancy | freelance (этап 1, до ИИ)
"wantedType": "both",
# «без указания суммы карточку не создаём»: отдельно для найма и заказов
"budgetRequiredHire": False,
"budgetRequiredOrder": False,
# подписи типов на карточках (по ситуации/сфере пользователя)
"hireLabel": "вакансия",
"orderLabel": "фриланс",
# личная библиотека промптов пользователя: [{id, name, description, prompt}]
"myPrompts": [],
# напоминания
"remindersEnabled": True,
# валюта и курсы
"conversionOn": True,
"targetCurrency": "RUB",
"rateSource": "cbr", # cbr | mock
# UI-состояния колонок
"colState": {},
# AI: активный провайдер и конфиги (ключи хранятся здесь же, наружу маскируются)
"aiProvider": "deepseek",
"aiConfigs": {
p["id"]: {"apiKey": "", "baseUrl": p["base"], "model": (p["models"] or [""])[0]}
for p in AI_PROVIDERS
},
# telegram-ключи: {api_id, api_hash, account}
"tgKeys": {"apiId": "", "apiHash": ""},
# авто-мониторинг новых чатов/каналов (добавлены с другого клиента)
"autoMonitorNew": True,
# поиск каналов (Discovery)
"discJoinLimit": 50, # суточный лимит авто-вступлений (общий)
"discJoinDelayMin": 50, # сек, нижняя граница паузы между авто-вступлениями
"discJoinDelayMax": 70, # сек, верхняя граница
"discEvalSample": 10, # размер выборки сообщений при оценке
"discEvalThreshold": 40, # % подходящих сообщений
}
# Диалоговые цвета (детерминированно по имени)
DIALOG_HUES = ["#3b82f6", "#38bdf8", "#f472b6", "#f59e0b", "#a78bfa", "#34d399", "#fb7185", "#22c55e"]
# Множители единиц времени
DAY_MS = 86_400_000
HOUR_MS = 3_600_000
MIN_MS = 60_000